实现commit的自动化

实现commit的自动化

--- biz/commit.coffee
+++ biz/commit.coffee
@@ -4,18 +4,124 @@
 _store = require('./store')
 _BaseEntity = require './BaseEntity'
 _schema = require '../schema/commit.json'
+_async = require 'async'
+_Issue = require './issue'
+
+#统一处理log，便于可以统一禁掉
+_log = (log)->
+  console.log log

 class Commit extends _BaseEntity
   constructor: ()->
     @schema = _schema
     super

+  #分析commit message中的标签，来关联对应的issue
+  analysisCommitMessage: (message, member_id, cb)->
+    #提取issue_id
+    issue_id = if message.match /#(\d+)/i then RegExp.$1 else 0
+    #提取done标签是否存在
+    done = /#(done|ok)/i.test(message)
+
+    _log "issue id -> #{issue_id}, done: #{done}"
+    #如果没有issue_id或没有完成，则不做任何处理
+    return cb null, issue_id if not done
+
+    #如果有done标签，则完成这个issue
+    issue = new _Issue(@member)
+    issue.finishedIssue issue_id, ()-> cb null, issue_id
+
+  #根据git用户名查找对应的用户id
+  findMemberWithGitUser: (gitUser, cb)->
+    sql = "SELECT id FROM member WHERE LOWER(git) = LOWER('#{gitUser}') LIMIT 1"
+    this.entity().knex.raw(sql).then (result)->
+      rows = result[0]
+      member_id = if rows.length > 0 then rows[0].id else 0
+      cb null, member_id
+
+  #保存单个commit
+  saveCommit: (projectId, commit, cb)->
+    self = @
+    queue = []
+    #根据commit.author.email查找对应的用户
+    queue.push(
+      (done)->
+        self.findMemberWithGitUser commit.author.email, done
+    )
+
+    #分析issue中的issue_id等特殊标签
+    queue.push(
+      (member_id, done)->
+        _log "#{commit.author.email}->#{member_id}"
+        #分析message中的信息，例如关于到issue，或者完成某个issue等
+        self.analysisCommitMessage commit.message, member_id, (err, issue_id)->
+          _log "#{commit.message}"
+          done err, member_id, issue_id
+    )
+
+    #保存commit
+    queue.push(
+      (member_id, issue_id, done)->
+        data =
+          issue_id: issue_id
+          project_id: projectId
+          creator: member_id
+          message: commit.message
+          sha: commit.id
+          timestamp: new Date(commit.timestamp)
+        self.save data, done
+    )
+
+    _async.waterfall queue, cb
+
+
+  #保存多个commit
+  saveCommits: (project_id, commits, cb)->
+    self = @
+    index = 0
+    _async.whilst(
+      (-> index < commits.length)
+      ((done)-> self.saveCommit(project_id, commits[index++], done))
+      cb
+    )
+
   #根据commit提供的信息，来查询是属于哪个项目以及属于哪个用户
-  findProject: (data, callback)->
+  findProject: (git_url, cb)->
+    #只取一条
+    sql = "SELECT id FROM project WHERE LOWER(repos) = LOWER('#{git_url}') LIMIT 1"
+    #查询结果
+    this.entity().knex.raw(sql).then((result)->
+      rows = result[0]
+      project_id = if rows.length > 0 then rows[0].id else 0
+      cb null, project_id
+    )

-  #保存数据
-  save: (data, callback)->
-    data.creator = this.member.member_id
-    super
+  ###
+    #处理git commit
+  ###
+  postCommits: (data, cb)->
+    console.log JSON.stringify(data)
+    return cb()
+    self = @
+    queue = []
+    #取得projectid
+    queue.push(
+      (done)->
+        _log data.repository.url
+        self.findProject data.repository.url, done
+    )
+
+    #保存每一个commit
+    queue.push(
+      (project_id, done)->
+        _log "匹配项目ID为：#{project_id}"
+        self.saveCommits project_id, data.commits, done
+    )
+    #queue.push
+    _async.waterfall queue, cb
+
+  #提交git commit
+  gitCommit: (req, res, next)->
+    @postCommits req.body, ()->res.end()

 module.exports = Commit


Fix CreativeNonLinearParser indentation

Fix CreativeNonLinearParser indentation

--- src/parser/creative_non_linear_parser.coffee
+++ src/parser/creative_non_linear_parser.coffee
@@ -10,12 +10,12 @@
         creative = new VASTCreativeNonLinear(creativeAttributes)

         for trackingEventsElement in @utils.childrenByName(creativeElement, "TrackingEvents")
-          for trackingElement in @utils.childrenByName(trackingEventsElement, "Tracking")
-            eventName = trackingElement.getAttribute("event")
-            trackingURLTemplate = @utils.parseNodeText(trackingElement)
+            for trackingElement in @utils.childrenByName(trackingEventsElement, "Tracking")
+                eventName = trackingElement.getAttribute("event")
+                trackingURLTemplate = @utils.parseNodeText(trackingElement)
             if eventName? and trackingURLTemplate?
-              creative.trackingEvents[eventName] ?= []
-              creative.trackingEvents[eventName].push trackingURLTemplate
+                creative.trackingEvents[eventName] ?= []
+                creative.trackingEvents[eventName].push trackingURLTemplate

         for nonlinearResource in @utils.childrenByName(creativeElement, "NonLinear")
             nonlinearAd = new VASTNonLinear()


ComputeProvider: Managed: fix tests, it requires group context from now on

--- workers/social/lib/social/models/computeproviders/managed.test.coffee
+++ workers/social/lib/social/models/computeproviders/managed.test.coffee
@@ -38,8 +38,8 @@

     it 'should fail to create managed vm if ip address is not valid', (done) ->

-      withConvertedUser ({ client, account, user }) ->
-        client.r = { account, user }
+      withConvertedUser ({ client, account, user, group }) ->
+        client.r = { account, user, group }

         options =
           label       : generateRandomString()


fix checksley initialization in login

fix checksley initialization in login

--- app/coffee/modules/auth.coffee
+++ app/coffee/modules/auth.coffee
@@ -221,6 +221,8 @@

 LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $events, $translate) ->
     link = ($scope, $el, $attrs) ->
+        form = new checksley.Form($el.find("form.login-form"))
+
         onSuccess = (response) ->
             if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login")
                 nextUrl = decodeURIComponent($routeParams['next'])
@@ -236,7 +238,6 @@
         submit = debounce 2000, (event) =>
             event.preventDefault()

-            form = new checksley.Form($el.find("form.login-form"))
             if not form.validate()
                 return


fix compile errors

fix compile errors

Signed-off-by: Jessica González <52329a27ea5670aeec0efeeb0773bf1b4f1affa1@missallsunday.com>

--- src/summernoteDrafts.coffee
+++ src/summernoteDrafts.coffee
@@ -1,20 +1,16 @@
-(function (factory) {
-    # global define
-    if (typeof define === 'function' && define.amd) {
-        # AMD. Register as an anonymous module.
-        define(['jquery'], factory);
-    } else if (typeof module === 'object' && module.exports) {
-        # Node/CommonJS
-        module.exports = factory(require('jquery'));
-    } else {
-        # Browser globals
-        factory(window.jQuery);
-    }
-}(function ($) {
+((factory) ->
+  if typeof define == 'function' and define.amd
+    define [ 'jquery' ], factory
+  else if typeof module == 'object' and module.exports
+    module.exports = factory(require('jquery'))
+  else
+    factory window.jQuery
+  return
+) ($) ->

   $.extend $.summernote.options,
   sDrafts:
-    storePrefix:'sDrafts',
+    storePrefix:'sDrafts'

   $.extend $.summernote.lang,
   'en-US':
@@ -30,7 +26,7 @@
       options = context.options
       lang = options.langInfo.sDrafts
       $editor = context.layoutInfo.editor
-      drafts = store.get(options.storePrefix)
+      drafts = store.get options.storePrefix

       context.memo 'button.sDraftsSave', () ->
         button = ui.button
@@ -39,11 +35,11 @@
             context.invoke 'sDrafts.show'
             false

-        button.render();
+        button.render

-      @initialize : ->
-        $container = options.dialogsInBody ? $(document.body) : $editor
-        body = '
' + '' + lang.provideName + '' + '<input class="note-link-imgUrl form-control" type="text" />
'
+      initialize : () ->
+        $container = if options.dialogsInBody then $(document.body) else $editor
+        body = '
' + '' + lang.provideName + '' + '<input class="note-draftName form-control" type="text" />
'
         footer = '<button href="#" class="btn btn-primary note-link-btn">' + lang.save + '</button>'

         @$dialog = ui.dialog
@@ -55,5 +51,23 @@

         @$dialog.render
         @$dialog.appendTo $container
+        return

-}));
+      destroy = () ->
+        ui.hideDialog @$dialog
+          .remove
+        return
+
+      show = () ->
+        ui.showDialog(@$dialog);
+        draftName = @$dialog.find '.note-draftName'
+          .val
+        $saveBtn = @$dialog.find '.note-link-btn'
+          .click (e) =>
+            e.preventDefault
+            @saveDraft draftName
+            false
+
+      saveDraft = (name) ->
+
+      return


#266: prevent the bare minimum of foot-shooting

--- chrome/content/zotero-better-bibtex/BetterBibTeXPatternFormatter.coffee
+++ chrome/content/zotero-better-bibtex/BetterBibTeXPatternFormatter.coffee
@@ -33,7 +33,7 @@
   concat: (pattern) ->
     result = (@reduce(part) for part in pattern)
     result = (part for part in result when part)
-    return result.join('').trim()
+    return result.join('').replace(/\s/, '')

   reduce: (step) ->
     value = @methods[step.method].apply(@, step.arguments)
@@ -124,7 +124,15 @@

     literal: (text) -> return text

-    property: (name) -> return @innerText(@item[name] || @item[name[0].toLowerCase() + name.slice(1)] || '')
+    property: (name) ->
+      if name.match(/^(Auth|Edtr|Editors)/)
+        [method, n, m] = name.split('_')
+        [method, withInitials] = method.split('+')
+        onlyEditors = (name[0] == 'E')
+        n = parseInt(n) if typeof n == 'string'
+        m = parseInt(m) if typeof m == 'string'
+        return @innerText(@methods[method.toLowerCase()](onlyEditors, withInitials, n, m))
+      return @innerText(@item[name] || @item[name[0].toLowerCase() + name.slice(1)] || '')

     id: -> return @item.itemID


linux

linux

--- rodeo-versions.coffee
+++ rodeo-versions.coffee
@@ -7,8 +7,8 @@
   {
     version: version,
     urls: {
-      "linux-32"    : "linux-32.zip"
-      "linux-64"    : "linux-64.zip"
+      "linux-32"    : "#{BASE_URL}/#{version}/Rodeo-v#{version}-linux-32.zip"
+      "linux-64"    : "#{BASE_URL}/#{version}/Rodeo-v#{version}-linux-64.zip"
       "darwin_x64"  : "#{BASE_URL}/#{version}/Rodeo-v#{version}-darwin_64.zip"
       "windows_x64" : "#{BASE_URL}/#{version}/Rodeo-v#{version}-windows_64.zip"
       "windows_ia32": "#{BASE_URL}/#{version}/Rodeo-v#{version}-windows_32.zip"


AdminIntegrationItemView: Some minor refactor to allow subclass usage.

--- client/admin/lib/views/integrations/adminintegrationitemview.coffee
+++ client/admin/lib/views/integrations/adminintegrationitemview.coffee
@@ -57,9 +57,13 @@

     options.cssClass = 'integration-view'

+    { @integrationType } = data
+    @createButton data
+
     super options, data

-    { @integrationType } = @getData()
+
+  createButton: (data) ->

     @button    = new KDButtonView
       cssClass : 'solid compact green add'


Apps: Implemented Updated and Released dates in app catalog apps.

--- client/landing/app/Applications/Apps.kdapplication/Views/appview.coffee
+++ client/landing/app/Applications/Apps.kdapplication/Views/appview.coffee
@@ -207,11 +207,14 @@
       attributes  :
         src       : thumb

+    @updatedTimeAgo = new KDTimeAgoView {}, @getData().meta.createdAt
+
   viewAppended:->
     @setTemplate @pistachio()
     @template.update()

   pistachio:->
+    timeAgoText = if @getData().versions?.length > 1 then "Updated" else "Released"
     """

@@ -224,7 +227,7 @@
         {{> @installButton}}
         {{> @runButton}}
         {{> @updateButton}}
-
Version {{ #(manifest.version) || "---" }}
Updated: ---

+
Version {{ #(manifest.version) || "---" }}
#{timeAgoText} {{> @updatedTimeAgo}}

{{#(counts.installed) || 0}}
INSTALLS

{{#(meta.likes) || 0}}
Likes


range start is always left/bottom, etc.

range start is always left/bottom, etc.

--- bokehjs/src/coffee/renderer/guide/axis.coffee
+++ bokehjs/src/coffee/renderer/guide/axis.coffee
@@ -485,15 +485,9 @@
       side = @get('location')

       if side == 'left' or side == 'below'
-        if cstart < cend
-          loc = 'start'
-        else
-          loc = 'end'
+        loc = 'start'
       else if side == 'right' or side == 'above'
-        if cstart < cend
-          loc = 'end'
-        else
-          loc = 'start'
+        loc = 'end'

       return cross_range.get(loc)


Translate settings.cson to Spanish

Translate settings.cson to Spanish

Signed-off-by: Santos Gallegos <c7d20f0b590f7c49e45e22db249ad9b63899f894@outlook.com>

--- def/es/settings.cson
+++ def/es/settings.cson
@@ -1,5 +1,5 @@
 Settings:
-  "tab-title": "Settings"
+  "tab-title": "Ajustes"
   menu: [
     {
       _label: "Core"
@@ -11,378 +11,405 @@
     }
     {
       _label: "Keybindings"
-      value: "Keybindings"
+      value: "Atajos"
     }
     {
       _label: "Packages"
-      value: "Packages"
+      value: "Paquetes"
     }
     {
       _label: "Themes"
-      value: "Themes"
+      value: "Temas"
     }
     {
       _label: "Updates"
-      value: "Updates"
+      value: "Actualizaciones"
     }
     {
       _label: "Install"
-      value: "Install"
+      value: "Instalar"
     }
   ]
-  "btn-open-config-folder": "Open Config Folder"
+  "btn-open-config-folder": "Abrir Carpeta de Configuraciones"
   sectionHeadings: [
-    {_label: "Core Settings", value: "Core Settings"}
-    {_label: "Editor Settings", value: "Editor Settings"}
-    {_label: "Keybindings", value: "Keybindings"}
-    {_label: "Installed Packages", value: "Installed Packages"}
-    {_label: "Choose a Theme", value: "Choose a Theme"}
-    {_label: "Installed Themes", value: "Installed Themes"}
-    {_label: "Install Packages", value: "Install Packages"}
-    {_label: "Featured Packages", value: "Featured Packages"}
-    {_label: "Install Themes", value: "Install Themes"}
-    {_label: "Featured Themes", value: "Featured Themes"}
+    {_label: "Core Settings", value: "Ajustes del Core"}
+    {_label: "Editor Settings", value: "Ajustes del Editor"}
+    {_label: "Keybindings", value: "Atajos"}
+    {_label: "Installed Packages", value: "Paquetes Instalados"}
+    {_label: "Choose a Theme", value: "Elige un Tema"}
+    {_label: "Installed Themes", value: "Temas Instalados"}
+    {_label: "Install Packages", value: "Instalar Paquetes"}
+    {_label: "Featured Packages", value: "Paquetes Destacados"}
+    {_label: "Install Themes", value: "Instalar Temas"}
+    {_label: "Featured Themes", value: "Temas Destacados"}
   ]
-  "heading-available-updates": {_label: "Available Updates", value: "Available Updates"}
+  "heading-available-updates": {_label: "Available Updates", value: "Actualizaciones Disponibles"}
   subSectionHeadings: [
     {_label: "Invisible", value: "Invisible"}
-    {_label: "Community Packages", value: "Community Packages"}
-    {_label: "Core Packages", value: "Core Packages"}
-    {_label: "Development Packages", value: "Development Packages"}
-    {_label: "Git Packages", value: "Git Packages"}
-    {_label: "Community Themes", value: "Community Themes"}
-    {_label: "Core Themes", value: "Core Themes"}
-    {_label: "Development Themes", value: "Development Themes"}
-    {_label: "Git Themes", value: "Git Themes"}
+    {_label: "Community Packages", value: "Paquetes de la Comunidad"}
+    {_label: "Core Packages", value: "Paquetes del Core"}
+    {_label: "Development Packages", value: "Paquetes de Desarrollo"}
+    {_label: "Git Packages", value: "Paquetes de Git"}
+    {_label: "Community Themes", value: "Temas de la Comunidad"}
+    {_label: "Core Themes", value: "Temas del Core"}
+    {_label: "Development Themes", value: "Temas de Desarrollo"}
+    {_label: "Git Themes", value: "Temas de Git"}
   ]
   settings: {
     notes: [
       {
         _id: 'core-settings-note'
-        html: 'These are Atom\'s core settings which affect behavior
-               unrelated to text editing. Individual packages may have
-               their own additional settings found within their package
-               card in the Packages list.'
-      }
-      {
-        _id: 'editor-settings-note'
-        html: 'These settings are related to text editing. Some of these
-               can be overriden on a per-language basis. Check language
-               settings by clicking its package card in the
-               Packages list.'
+        html: 'Estos son los ajustes de Atom, los cuales no afectan
+               el comportamiento relacionado a editar texto. Paquetes
+               individuales pueden tener sus propios ajustes en la
+               lista de Paquetes.'
       }
     ]
     controls: [
       {
         _id: 'core.allowPendingPaneItems'
-        title: "Allow Pending PaneItems"
-        desc: "Allow items to be previewed without adding them to a pane
-               permanently, such as when single clicking files in the tree view."
+        title: "Permitir Ítems Pendientes en Paneles"
+        desc: "Permitir ítems a ser pre-visualizados sin agregarlos a un panel
+               de manera permanente, tal como dar un sólo click a archivos
+               en el tree view."
       }
       {
         _id: 'core.audioBeep'
-        title: "Audio Beep"
-        desc: "Trigger the system's beep sound when certain actions cannot be
-               executed or there are no results."
+        title: "Pitido"
+        desc: "Acciona el pitido del sistema cuando ciertas acciones no pueden
+               se ejecutadas o no hay resultados."
       }
       {
         _id: 'core.automaticallyUpdate'
-        title: "Automatically Update"
-        desc: "Automatically update Atom when a new release is available."
+        title: "Actualizar Automáticamente"
+        desc: "Actualizar automáticamente Atom cuando una nueva versión está
+               disponible."
       }
       {
         _id: 'core.autoHideMenuBar'
-        title: "Auto Hide Menu Bar"
-        desc: "Automatically hide the menu bar and toggle it by pressing Alt. This is only supported on Windows & Linux."
+        title: "Ocultar Automáticamente la Barra de Menú"
+        desc: "Ocultar automáticamente la barra de menú y mostrarla u ocultarla
+               presionando Alt. Esto es sólo soportado en Windows & Linux."
       }
       {
         _id: 'core.closeEmptyWindows'
-        title: "Close Empty Windows"
-        desc: "When a window with no open tabs or panes is given the 'Close Tab' command, close that window."
+        title: "Cerrar Ventanas Vacías"
+        desc: "Cuando a una ventana sin ninguna pestaña o panel le es dado el
+               comando `Cerrar Pestaña`, la ventana se cerrará."
       }
       {
         _id: 'core.destroyEmptyPanes'
-        title: "Destroy Empty Panes"
-        desc: "When the last tab of a pane is closed, remove that pane as well."
+        title: "Destruir Paneles Vacíos"
+        desc: "Cuando la última pestaña de un panel es cerrada, también eliminar
+               el panel."
       }
       {
         _id: 'core.excludeVcsIgnoredPaths'
-        title: "Exclude VCS Ignored Paths"
-        desc: "Files and directories ignored by the current project's VCS system will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders."
+        title: "Excluir Rutas Ignoradas por VCS"
+        desc: "Archivos y directorios ignorados por el VCS actual del proyecto
+               serán ignorados por algunos paquetes, tales como `fuzzy finder` y
+               `buscar y reemplazar`. Por ejemplo, proyectos usando Git tienen
+               rutas definidas en el archivo .gitignore. Paquetes individuales
+               pueden tener configuraciones adicionales para ignorar archivos y
+               directorios ignorados por el VCS."
       }
       {
         _id: 'core.fileEncoding'
-        title: "File Encoding"
-        desc: "Default character set encoding to use when reading and writing files."
+        title: "Codificación de Archivos"
+        desc: "La codificación de caracteres usada por defecto cuando se lee y
+               escribe archivos."
       }
       {
         _id: 'core.followSymlinks'
-        title: "Follow Symlinks"
-        desc: "Follow symbolic links when searching files and when opening files with the fuzzy finder."
+        title: "Seguir Enlaces Simbólicos"
+        desc: "Seguir enlaces simbólicos cuando se busca archivos y cuando se
+               abre archivos con el `fuzzy finder`."
       }
       {
         _id: 'core.ignoredNames'
-        title: "Ignored Names"
-        desc: "List of glob patterns. Files and directories matching these patterns will be ignored by some packages, such as the fuzzy finder and tree view. Individual packages might have additional config settings for ignoring names."
+        title: "Nombres Ignorados"
+        desc: "Lista de patrones globales. Archivos y directorios
+               que coinciden con estos patrones serán ignorados por algunos
+               paquetes, tal como `fuzzy finder` y `tree view`. Paquetes
+               individuales pueden tener configuraciones adicionales para
+               nombres a ser ignorados."
       }
       {
         _id: 'core.openEmptyEditorOnStart'
-        title: "Open Empty Editor On Start"
-        desc: "Automatically open an empty editor on startup."
+        title: "Abrir Editor Vació al Iniciar"
+        desc: "Abrir automáticamente un editor vacío al iniciar."
       }
       {
         _id: 'core.packagesWithKeymapsDisabled'
-        title: "Packages With Keymaps Disabled"
+        title: "Paquetes con Keymaps Deshabilitados"
       }
       {
         _id: 'core.projectHome'
-        title: "Project Home"
-        desc: "The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default."
+        title: "Directorio de Proyectos"
+        desc: "El directorio donde se asume que los proyectos están localizados.
+               Paquetes creados usando el Generador de Paquete serán almacenados
+               aquí por defecto."
       }
       {
         _id: 'core.reopenProjectMenuCount'
-        title: "Reopen Project Menu Count"
-        desc: "How many recent projects to show in the Reopen Project menu."
+        title: "Cuenta del Menú de Reabrir Proyecto"
+        desc: "Cuántos proyectos recientes mostrar el menú de Reabrir Proyecto."
       }
       {
         _id: 'core.restorePreviousWindowsOnStart'
-        title: "Restore Previous Windows On Start"
+        title: "Restaurar Ventana Previa al Iniciar"
       }
       {
         _id: 'core.telemetryConsent'
-        title: "Send Telemetry to the Atom Team"
-        desc: "Allow usage statistics and exception reports to be sent to the Atom team to help improve the product."
+        title: "Enviar Telemetría al Equipo de Atom"
+        desc: "Permitir enviar estadísticas de uso y reportes de excepción al
+               equipo de Atom para ayudar a mejorar el producto."
         select:
-          limited: "Allow limited anonymous usage stats, exception and crash reporting"
-          no: "Do not send any telemetry data"
-          undecided: "Undecided (Atom will ask again next time it is launched)"
+          limited: "Permitir limitadas estadísticas de uso, excepciones y
+                    reportes de fallos anónimas."
+          no: "No enviar ninguna información de telemetría."
+          undecided: "Indeciso (Atom te preguntará nuevamente la siguiente
+                      vez que sea ejecutado)."
       }
       {
         _id: 'core.useCustomTitleBar'
-        title: "Use Custom Title Bar"
+        title: "Usar Barra de Título Personalizada"
       }
       {
         _id: 'core.warnOnLargeFileLimit'
-        title: "Warn On Large File Limit"
-        desc: "Warn before opening files larger than this number of megabytes."
+        title: "Advertir sobre Límite de Archivos Grandes"
+        desc: "Advertir antes de abrir archivos más grandes que este número
+               de megabytes."
       }
       {
         _id: 'editor.atomicSoftTabs'
-        title: "Atomic Soft Tabs"
-        desc: "Skip over tab-length runs of leading whitespace when moving the cursor."
+        title: "Soft Tabs Atómicos"
+        desc: "Saltar la longitud de tabulación en espacios en blanco cuando
+               se mueva el cursor."
       }
       {
         _id: 'editor.autoIndent'
-        title: "Auto Indent"
-        desc: "Automatically indent the cursor when inserting a newline."
+        title: "Auto Indentar"
+        desc: "Indentar automáticamente el cursor cuando se inserta una
+               nueva línea."
       }
       {
         _id: 'editor.autoIndentOnPaste'
-        title: "Auto Indent On Paste"
-        desc: "Automatically indent pasted text based on the indentation of the previous line."
+        title: "Auto Indentar al Pegar"
+        desc: "Indentar automáticamente el texto pegado basado en la
+               indentación de la línea anterior."
       }
       {
         _id: 'editor.backUpBeforeSaving'
-        title: "Back Up Before Saving"
+        title: "Realizar una Copia de Seguridad Antes de Guardar."
         desc: ""
       }
       {
         _id: 'editor.confirmCheckoutHeadRevision'
-        title: "Confirm Checkout HEAD Revision"
-        desc: "Show confirmation dialog when checking out the HEAD revision and discarding changes to current file since last commit."
+        title: "Confirmar Cambio a HEAD"
+        desc: "Muestra un dialogo de confirmación cuando se cambia de
+               la rama HEAD y se descarta los cambios del archivo actual
+               desde el último commit."
       }
       {
         _id: 'editor.fontFamily'
-        title: "Font Family"
-        desc: "The name of the font family used for editor text."
+        title: "Tipo de Fuente"
+        desc: "Nombre de la fuente usada para el editor de texto."
       }
       {
         _id: 'editor.fontSize'
-        title: "Font Size"
-        desc: "Height in pixels of editor text."
+        title: "Tamaño de la Fuente"
+        desc: "Medida en pixeles del editor de texto."
       }
       {
         _id: 'editor.invisibles.cr'
-        title: "Invisibles Cr"
-        desc: "Character used to render carriage return characters
-               (for Microsoft-style line endings) when the Show
-               Invisibles setting is enabled."
+        title: "Cr Invisibles"
+        desc: "Caracteres usados para renderizar el carácter de retorno línea
+               (para finales de línea al estilo Microsoft) cuando la opción
+               Invisibles está activada."
       }
       {
         _id: 'editor.invisibles.eol'
-        title: "Invisibles Eol"
-        desc: "Character used to render newline characters (\n) when the
-               Show Invisibles setting is enabled."
+        title: "Eol Invisibles"
+        desc: "Carácter usado para renderizar el carácter de nueva línea (\n)
+               cuando la opción Mostrar Invisibles está activada."
       }
       {
         _id: 'editor.invisibles.space'
-        title: "Invisibles Space"
-        desc: "Character used to render leading and trailing space characters
-               when the Show Invisibles setting is enabled."
+        title: "Espacios Invisibles"
+        desc: "Caracteres usados para renderizar caracteres de espacio iniciales
+               y finales cuando la opción de Mostrar Invisibles
+               está activada."
       }
       {
         _id: 'editor.invisibles.tab'
-        title: "Invisibles Tab"
-        desc: "Character used to render hard tab characters (\t) when the
-               Show Invisibles setting is enabled."
+        title: "Tabulaciones Invisibles"
+        desc: "Carácter usado para renderizar caracteres de tabulación (\t)
+               cuando la opción Mostrar Invisibles está activada."
       }
       {
         _id: 'editor.lineHeight'
-        title: "Line Height"
-        desc: "Height of editor lines, as a multiplier of font size."
+        title: "Altura de la Línea"
+        desc: "Altura de las líneas del editor, como multiplicador del tamaño
+               de la fuente."
       }
       {
         _id: 'editor.nonWordCharacters'
-        title: "Non Word Characters"
-        desc: "A string of non-word characters to define word boundaries."
+        title: "Caracteres Que No Son Palabras"
+        desc: "Una cadena de caracteres que no son palabras para definir
+               límites de una palabra."
       }
       {
         _id: 'editor.preferredLineLength'
-        title: "Preferred Line Length"
-        desc: "Identifies the length of a line which is used when wrapping text
-               with the Soft Wrap At Preferred Line Length setting
-               enabled, in number of characters."
+        title: "Longitud de Línea Predefinida"
+        desc: "Identifica la longitud de la línea la cuál es usada cuando
+               se divide el texto con la opción Soft Wrap en Longitud
+               de Línea Predefinida habilitada, en número de caracteres."
       }
       {
         _id: 'editor.scrollPastEnd'
-        title: "Scroll Past End"
-        desc: "Allow the editor to be scrolled past the end of the last line."
+        title: "Desplazarse Pasado el Final"
+        desc: "Permite al editor ser desplazado (hacer scroll) pasada la
+               última línea."
       }
       {
         _id: 'editor.scrollSensitivity'
-        title: "Scroll Sensitivity"
-        desc: "Determines how fast the editor scrolls when using a mouse or
-               trackpad."
+        title: "Sensibilidad del Scroll"
+        desc: "Determina qué tan rápido el editor se desplaza cuando se usa
+               el mouse o el touch pad"
       }
       {
         _id: 'editor.showIndentGuide'
-        title: "Show Indent Guide"
-        desc: "Show indentation indicators in the editor."
+        title: "Mostrar Líneas de Indentación"
+        desc: "Mostrar indicadores de indentación en el editor."
       }
       {
         _id: 'editor.showInvisibles'
-        title: "Show Invisibles"
-        desc: "Render placeholders for invisible characters, such as tabs,
-               spaces and newlines."
+        title: "Mostrar Invisibles"
+        desc: "Renderiza marcadores para caracteres invisibles, tables como
+               tabulaciones, espacios y saltos de línea."
       }
       {
         _id: 'editor.showLineNumbers'
-        title: "Show Line Numbers"
-        desc: "Show line numbers in the editor's gutter."
+        title: "Mostrar Números de Línea"
+        desc: "Mostrar números de línea en el editor."
       }
       {
         _id: 'editor.softTabs'
         title: "Soft Tabs"
-        desc: "If the Tab Type config setting is set to \"auto\"
-               and autodetection of tab type from buffer content fails, then
-               this config setting determines whether a soft tab or a hard tab
-               will be inserted when the Tab key is pressed."
+        desc: "Si la opción Tipo de Tabulación está configurada
+               a \"auto\" y auto detección del tipo de tabulación del buffer
+               actual falla, entonces esta configuración determina si son
+               insertadas soft o hard tabs cuando la tecla de tabulación es
+               presionada."
       }
       {
         _id: 'editor.softWrap'
         title: "Soft Wrap"
-        desc: "Wraps lines that exceed the width of the window. When
-               Soft Wrap At Preferred Line Length is set, it will
-               wrap to the number of characters defined by the Preferred
-               Line Length setting."
+        desc: "Divide las líneas que exceden la anchura de la ventana. Si
+               Soft Wrap en Longitud de Línea Predefinida está
+               seleccionada, dividirá al número de caracteres definidos por
+               Longitud de Línea Predefinida."
       }
       {
         _id: 'editor.softWrapAtPreferredLineLength'
-        title: "Soft Wrap At Preferred Line Length"
-        desc: "Instead of wrapping lines to the window's width, wrap lines to
-               the number of characters defined by the Preferred Line
-               Length setting. This will only take effect when the soft
-               wrap config setting is enabled globally or for the current
-               language. Note: If you want to hide the wrap
-               guide (the vertical line) you can disable the
-               wrap-guide package."
+        title: "Soft Wrap en Longitud de Línea Predefinida"
+        desc: "En lugar de dividir las líneas a la anchura de la ventana, las
+               divide al número de caracteres definidos en Longitud de
+               Línea Predefinida. Sólo tomará efecto cuando la
+               configuración de soft wrap está activada globalmente o para el
+               lenguaje actual. Nota: Si quieres ocultar la
+               línea guía (la línea vertical) puedes deshabilitar el paquete
+               wrap-guide."
       }
       {
         _id: 'editor.softWrapHangingIndent'
-        title: "Soft Wrap Hanging Indent"
-        desc: "When soft wrap is enabled, defines length of additional
-               indentation applied to wrapped lines, in number of characters."
+        title: "Manejo de Indentación para Soft Wrap"
+        desc: "Cuando soft wrap está habilitado, define la longitud de la
+               indentación adicional aplicada para dividir las líneas, en
+               número de caracteres."
       }
       {
         _id: 'editor.tabLength'
-        title: "Tab Length"
-        desc: "Number of spaces used to represent a tab."
+        title: "Longitud de Tabulación"
+        desc: "Número de espacios usados para representar una tabulación"
       }
       {
         _id: 'editor.tabType'
-        title: "Tab Type"
-        desc: "Determine character inserted when Tab key is pressed. Possible
-               values: \"auto\", \"soft\" and \"hard\". When set to \"soft\" or
-               \"hard\", soft tabs (spaces) or hard tabs (tab characters) are
-               used. When set to \"auto\", the editor auto-detects the tab type
-               based on the contents of the buffer (it uses the first leading
-               whitespace on a non-comment line), or uses the value of the Soft
-               Tabs config setting if auto-detection fails."
+        title: "Tipo de Tabulación"
+        desc: "Determina el carácter insertado cuando la tecla de Tabulación
+               es presionada. Possibles valores: \"auto\", \"soft\" y \"hard\".
+               Cuando es \"soft\" o \"hard\", soft tabs (espacios) o hard tabs
+               (caracteres de tabulación) son usados. Cuando es \"auto\", el
+               editor auto-detecta el tipo de tabulación basado en el documento
+               del buffer (usa el primer espacio blanco de un texto que no sea
+               un comentario), o usa el valor de Soft Tabs si la auto-detección
+               falla."
       }
       {
         _id: 'editor.undoGroupingInterval'
-        title: "Undo Grouping Interval"
-        desc: "Time interval in milliseconds within which text editing
-               operations will be grouped together in the undo history."
+        title: "Intervalo de Agrupamiento de Deshacer"
+        desc: "Tiempo de intervalo en milisegundos dentro del cual las ediciones
+               de texto serán agrupadas en el historial de deshacer."
       }
       {
         _id: 'editor.zoomFontWhenCtrlScrolling'
-        title: "Zoom Font When Ctrl Scrolling"
-        desc: "Change the editor font size when pressing the Ctrl key and
-               scrolling the mouse up/down."
+        title: "Hacer Zoom a la Fuente con Ctrl + Scroll"
+        desc: "Cambia el tamaño de la fuente cuando se presiona la tecla Ctrl y
+               se hace scroll con el mouse."
       }
     ]
   }
   keymaps: {
     notes: {
-      text1: "You can override these keybindings by copying "
-      text2: "and pasting them into "
-      text3: "your keymap file"
+      text1: "Puedes sobreescribir estos atajos copiando "
+      text2: "y pegándolos en el "
+      text3: "archivo keymap"
       text4: ""   # optional
       _template: "${text1} <icon /> ${text2} ${text3} ${text4}"
     }
-    "search-bar-placeholder": "Search keybindings"
-    "table-header-keystroke": "Keystroke"
-    "table-header-command": "Command"
-    "table-header-source": "Source"
+    "search-bar-placeholder": "Buscar atajos"
+    "table-header-keystroke": "Tecla"
+    "table-header-command": "Comando"
+    "table-header-source": "Fuente"
     "table-header-selector": "Selector"
   }
   packages: {
   }
   themes: {
     notes: {
-      text1: "You can also style Atom by editing "
-      text2: "your stylesheet"
+      text1: "También puedes estilizar Atom editando "
+      text2: "tu hoja de estilos"
       text3: ""   # optional
       _template: "${text1} ${text2} ${text3}"
     }
-    title1: "UI Theme"
-    description1: "This styles the tabs, status bar, tree view, and dropdowns"
-    title2: "Syntax Theme"
-    description2: "This styles the text inside the editor"
+    title1: "Tema de UI"
+    description1: "Esto cambia las pestañas, barra de estado, tree view y las listas desplegables "
+    title2: "Tema de la Sintaxis"
+    description2: "Esto cambia el texto dentro del editor"
   }
   updates: {
-    "check-updates": "Check for Updates"
-    "update-all": "Update All"
-    "checking-updates": "Checking for updates..."
-    "all-updated-message": "All of your installed packages are up to date!"
+    "check-updates": "Buscar Actualizaciones"
+    "update-all": "Actualizar Todo"
+    "checking-updates": "Buscando actualizaciones..."
+    "all-updated-message": "¡Todos tus paquetes instalados están actualizados!"
   }
   install: {
     notes: {
-      text1: "Packages are published to "
-      text2: " and are installed to "
+      text1: "Los paquetes son publicados en "
+      text2: " y son instalados en "
       text3: ""   # optional
       _template: "${text1}atom.io${text2} /path/to/pkg ${text3}"
     }
-    "searchbar-placeholder": "Search packages"
-    "btn-packages": "Packages"
-    "btn-themes": "Themes"
+    "searchbar-placeholder": "Buscar paquetes"
+    "btn-packages": "Paquetes"
+    "btn-themes": "Temas"
   }
   "control-btns":{
-    install: "Install"
-    update: "Update to"
-    setting: "Settings"
-    uninstall: "Uninstall"
-    disable: "Disable"
-    enable: "Enable"
-  }
+    install: "Instalar"
+    update: "Actualizar a"
+    setting: "Ajustes"
+    uninstall: "Desinstalar"
+    disable: "Deshabilitar"
+    enable: "Habilitar"
+  }


Remove `oncomplete`

Remove `oncomplete`

--- src/request-queue-job.coffee
+++ src/request-queue-job.coffee
@@ -2,7 +2,6 @@
   constructor: (request, priority=RequestQueue.NORMAL) ->
     @request = request
     @priority = priority
-    @oncomplete = null

   run: (complete) ->
     @request.on "complete", complete(this)


7533: Fixed calendar footer links

7533: Fixed calendar footer links

--- app/assets/javascripts/backbone/views/reservation_calendar_view.coffee
+++ app/assets/javascripts/backbone/views/reservation_calendar_view.coffee
@@ -177,8 +177,8 @@
     @calendar.fullCalendar('option', 'minTime', @minTime())

   showAppropriateEarlyLink: ->
-    @$('#hide-early')[if @savedSettings.earlyMorning then 'show' else 'hide']()
-    @$('#show-early')[if @savedSettings.earlyMorning then 'hide' else 'show']()
+    @$('#hide-early').css(display: if @savedSettings.earlyMorning then 'inline' else 'none')
+    @$('#show-early').css(display: if @savedSettings.earlyMorning then 'none' else 'inline')

   storageKey: ->
     "calendar#{@resourceId}Settings"


support arrow key navigation in slides

support arrow key navigation in slides

--- app/assets/javascripts/training/components/training_slide_handler.cjsx
+++ app/assets/javascripts/training/components/training_slide_handler.cjsx
@@ -4,6 +4,7 @@
 ServerActions = require '../../actions/server_actions'
 Router          = require 'react-router'
 Link            = Router.Link
+Navigation      = Router.Navigation
 SlideLink       = require './slide_link'
 SlideMenu       = require './slide_menu'
 Quiz            = require './quiz'
@@ -19,7 +20,7 @@

 TrainingSlideHandler = React.createClass(
   displayName: 'TrainingSlideHandler'
-  mixins: [TrainingStore.mixin]
+  mixins: [TrainingStore.mixin, Navigation]
   getInitialState: ->
     slides: []
     previousSlide: { slug: '' }
@@ -62,6 +63,24 @@
     if @state.menuIsOpen
       e.stopPropagation()
       TrainingActions.toggleMenuOpen(currently: true)
+
+  keys: { rightKey: 39, leftKey: 37 }
+
+  handleKeyPress: (e) ->
+    navParams = library_id: @props.params.library_id, module_id: @props.params.module_id
+    if e.which == @keys.leftKey && @state.previousSlide?
+      params = _.extend navParams, slide_id: @state.previousSlide.slug
+      @transitionTo 'slide', params
+    if e.which == @keys.rightKey && @state.nextSlide?
+      params = _.extend navParams, slide_id: @state.nextSlide.slug
+      @transitionTo 'slide', params
+
+  componentDidMount: ->
+    window.addEventListener('keyup', @handleKeyPress)
+
+  componentWillUnmount: ->
+    window.removeEventListener('keyup', @handleKeyPress)
+
   render: ->
     disableNext = @state.currentSlide.assessment? && !@state.currentSlide.answeredCorrectly


keyboard shortcuts for formatting

keyboard shortcuts for formatting

--- src/smc-webapp/code-editor/cm-options.coffee
+++ src/smc-webapp/code-editor/cm-options.coffee
@@ -85,6 +85,28 @@
         if opts.bindings != 'emacs'
             extraKeys['Ctrl-P'] = -> actions.print()

+    if actions? and not opts.read_only and opts.bindings != 'emacs'  # emacs bindings really conflict with these
+        # Extra codemirror keybindings -- for some of our plugins
+        # inspired by http://www.door2windows.com/list-of-all-keyboard-shortcuts-for-sticky-notes-in-windows-7/
+        keybindings =
+            bold          : 'Cmd-B Ctrl-B'
+            italic        : 'Cmd-I Ctrl-I'
+            underline     : 'Cmd-U Ctrl-U'
+            comment       : 'Shift-Ctrl-3'
+            strikethrough : 'Shift-Cmd-X Shift-Ctrl-X'
+            subscript     : "Cmd-= Ctrl-="
+            superscript   : "Shift-Cmd-= Shift-Ctrl-="
+
+        # use a closure to bind cmd.
+        f = (key, cmd) ->
+            extraKeys[key] = (cm) =>
+                cm.edit_selection(cmd : cmd)
+                actions.set_syncstring_to_codemirror()
+
+        for cmd, keys of keybindings
+            for key in keys.split(' ')
+                f(key, cmd)
+
     if opts.match_xml_tags
         extraKeys['Ctrl-J'] = "toMatchingTag"


strip source which is in messages and breaks format

--- src/smc-webapp/jupyter/output-handler.coffee
+++ src/smc-webapp/jupyter/output-handler.coffee
@@ -125,6 +125,7 @@
         delete mesg.execution_state
         delete mesg.code
         delete mesg.status
+        delete mesg.source
         for k, v of mesg
             if misc.is_object(v) and misc.len(v) == 0
                 delete mesg[k]


add extra CKE adv_link plugin to ck-config.js for future releases

--- lib/templates/kuhsaft/assets/ck-config.js.coffee
+++ lib/templates/kuhsaft/assets/ck-config.js.coffee
@@ -12,6 +12,9 @@

   config.linkShowAdvancedTab = false
   config.linkShowTargetTab = false
+
+  config.removePlugins = 'link'
+  config.extraPlugins = 'adv_link'

   true


Fixed #64 by passing through .map extensions unchanged

--- lib/nap.coffee
+++ lib/nap.coffee
@@ -350,7 +350,7 @@
                  @_preprocessedCache[filename] = preprocess(data, filename)
                else
                  @_preprocessedCache[filename]
-    outputFilename = filename.replace /\.[^.]*$/, '' + '.' + type
+    outputFilename = if filename.match /\.map$/ then filename else filename.replace /\.[^.]*$/, '' + '.' + type
     obj[outputFilename] = contents
   obj


Fixes #4, #156, #226. Closes #128, #236

Fixes #4, #156, #226. Closes #128, #236

Finally fixes all the INDENT errors that we have been seeing by now
accounting for multiple chains in a chain call

--- src/rules/indentation.coffee
+++ src/rules/indentation.coffee
@@ -112,16 +112,10 @@
     isChainedCall: (tokenApi) ->
         { tokens, i } = tokenApi
         # Get the index of the second most recent new line.
-        lines = (i for token, i in tokens[..i] when token.newLine?)
-
-        lastNewLineIndex = if lines then lines[lines.length - 2] else null
-
-        # Bail out if there is no such token.
-        return false if not lastNewLineIndex?
+        newLineTokens = (i for token, i in tokens[..i] when token.newLine?)

         # Otherwise, figure out if that token or the next is an attribute
         # look-up.
-        tokens = [tokens[lastNewLineIndex], tokens[lastNewLineIndex + 1]]
-
-        return !!(t for t in tokens when t and t[0] == '.').length
-
+        for l in newLineTokens when (tokens[l][0] is '.' or tokens[l + 1][0] is '.')
+            return true
+        return false


search in pending for TL before sending understaffed (fixes #113)

--- server/methods/mail/sendMail/sendUnderstaffed.coffee
+++ server/methods/mail/sendMail/sendUnderstaffed.coffee
@@ -14,6 +14,8 @@
 			shiftData.teams[0] = team

 			for participant in team.participants when participant.teamleader
+				tlNeeded = false
+			for pendingUser in team.pending when pendingUser.teamleader
 				tlNeeded = false

 		if tlNeeded


Improve plain text copy/paste dataTransfer

Also fixes pasting from Trix into a plain text input in Firefox because it doesn't support Element.innerText

--- src/trix/controllers/input_controller.coffee
+++ src/trix/controllers/input_controller.coffee
@@ -349,13 +349,10 @@
     return unless dataTransfer?.setData?

     document = @responder?.getSelectedDocument().toSerializableDocument()
-    element = Trix.DocumentView.render(document)
-    html = element.innerHTML
-    text = element.innerText

     dataTransfer.setData("application/x-trix-document", JSON.stringify(document))
-    dataTransfer.setData("text/html", html)
-    dataTransfer.setData("text/plain", text)
+    dataTransfer.setData("text/html", Trix.DocumentView.render(document).innerHTML)
+    dataTransfer.setData("text/plain", document.toString().replace(/\n$/, ""))
     true

   canAcceptDataTransfer: (dataTransfer) ->


Tar uses / path separator even on Windows

--- src/ls-archive.coffee
+++ src/ls-archive.coffee
@@ -98,11 +98,12 @@
   tarStream = inputStream.pipe(require('tar').Parse())
   tarStream.on 'error', callback
   tarStream.on 'entry', (entry) ->
-    if entry.props.path[-1..] is path.sep
+    if entry.props.path[-1..] is '/'
       entryPath = entry.props.path[0...-1]
     else
       entryPath = entry.props.path
     entryType = parseInt(entry.props.type)
+    entryPath = entryPath.replace(/\//g, path.sep)
     entries.push(new ArchiveEntry(entryPath, entryType))
   tarStream.on 'end', ->
     entries = convertToTree(entries) if options.tree
@@ -159,7 +160,7 @@

   tarStream.on 'error', callback
   tarStream.on 'entry', (entry) ->
-    return unless filePath is entry.props.path
+    return unless filePath is entry.props.path.replace(/\//g, path.sep)

     if entry.props.type is '0'
       readEntry(entry, callback)


client: stop caning the CPU so much.

client: stop caning the CPU so much.

--- src/client.coffee
+++ src/client.coffee
@@ -167,6 +167,7 @@
     # the generated `obj_string` code changes.
     class WebGLViewer extends Backbone.View
         defaults: {}
+        throttle: 1

         initialize: ->
             @options = _.defaults @options, @defaults
@@ -236,8 +237,12 @@

         animate: =>
             if @object?
-                @controls.update()
-                @renderer.render @scene, @camera
+                @throttle = @throttle - 1
+                if @throttle is 0
+                    # XXX dont seem to seen this controls update call
+                    # @controls.update()
+                    @renderer.render @scene, @camera
+                    @throttle = 5
             window.requestAnimationFrame @animate

     # Create the model and components.


Listen for clicks on entire row - fix #6

--- src/client.coffee
+++ src/client.coffee
@@ -265,7 +265,7 @@
     updateSelected()
     event.stopImmediatePropagation()

-  $(document).on "click", "tbody .select", (event) ->
+  $(document).on "click", "tbody tr", (event) ->
     $(@).closest("tr").toggleClass("selected")
     updateSelected()
     event.stopImmediatePropagation()


tweak failover; rough draft of network stickiness

--- salvus/compute.coffee
+++ salvus/compute.coffee
@@ -37,9 +37,9 @@

 # obviously don't want to trigger this too quickly, since it may mean file loss.
-AUTOMATIC_FAILOVER_TIME_S = 60*10
-
-SERVER_STATUS_TIMEOUT_S = 5  # 5 seconds
+AUTOMATIC_FAILOVER_TIME_S = 60*5  # 5 minutes
+
+SERVER_STATUS_TIMEOUT_S = 7  # 7 seconds

 # todo -- these should be in a table in the database.
 DEFAULT_SETTINGS =
@@ -1839,7 +1839,7 @@
         dbg = @dbg("constructor")
         sqlite_db.select
             table   : 'projects'
-            columns : ['state', 'state_time', 'state_error', 'mintime']
+            columns : ['state', 'state_time', 'state_error', 'mintime', 'network']
             where   : {project_id : @project_id}
             cb      : (err, results) =>
                 if err
@@ -1850,11 +1850,13 @@
                     @_state      = undefined
                     @_state_time = new Date()
                     @_state_error = undefined
+                    @_network = false
                 else
                     @_state      = results[0].state
                     @_state_time = new Date(results[0].state_time)
                     @_state_error= results[0].state_error
                     @_mintime    = results[0].mintime
+                    @_network    = results[0].network
                     dbg("fetched project info from db: state=#{@_state}, state_time=#{@_state_time}, state_error=#{@_state_error}, mintime=#{@_mintime}s")
                     if not STATES[@_state]?.stable
                         dbg("updating non-stable state")
@@ -1905,7 +1907,7 @@
             args        : undefined
             at_most_one : false     # ignores subsequent args if set -- only use this for things where args don't matter
             timeout     : TIMEOUT
-            cb          : required
+            cb          : undefined
         dbg = @dbg("_command(action:'#{opts.action}')")

         if opts.at_most_one
@@ -1929,9 +1931,9 @@
                     v = @_command_cbs[opts.action]
                     delete @_command_cbs[opts.action]
                     for cb in v
-                        cb(err, result)
-                else
-                    opts.cb(err, result)
+                        cb?(err, result)
+                else
+                    opts.cb?(err, result)

     command: (opts) =>
         opts = defaults opts,
@@ -2119,6 +2121,10 @@
                         dbg("got new state -- #{@_state}")
                         @_update_state_db()
                         @_update_state_listeners()
+
+                        if @_state == "running"
+                            @_update_network()
+
                 v = @_update_state_cbs
                 delete @_update_state_cbs
                 dbg("calling #{v.length} callbacks")
@@ -2160,6 +2166,32 @@
                     opts.cb(err)
                 else
                     opts.cb(undefined, {})
+
+    _update_network: (cb) =>
+        @command
+            action     : 'network'
+            args       : if @_network then [] else ['--ban']
+            cb         : cb
+
+    set_network: (opts) =>
+        opts = defaults opts,
+            network : required
+            cb      : required
+        dbg = @dbg("network(network=#{opts.network})")
+        @_network = opts.network
+        resp = undefined
+        async.parallel([
+            (cb) =>
+                sqlite_db.update
+                    table : 'projects'
+                    set   : {network: opts.network}
+                    where : {project_id: @project_id}
+                    cb    : () => cb()
+            (cb) =>
+                @_update_network (err, r) =>
+                    resp = r
+                    cb(err)
+        ], (err) => opts.cb?(err, resp))

 secret_token = undefined
 read_secret_token = (cb) ->
@@ -2215,6 +2247,11 @@
             else if mesg.action == 'mintime'
                 p.set_mintime
                     mintime : mesg.args[0]
+                    cb      : (err, r) ->
+                        resp = r; cb(err)
+            else if mesg.action == 'network'
+                p.set_network
+                    network : mesg.args.length == 0 # no arg = enable
                     cb      : (err, r) ->
                         resp = r; cb(err)
             else


improved wikidata_get_many_entities tests


syntax simplification

syntax simplification

--- oridomi.coffee
+++ oridomi.coffee
@@ -244,8 +244,7 @@
     stage.style.display = 'none'
     stage.style.position = 'absolute'
     # Eliminate padding and margins since the stage is already the full width and height.
-    stage.style.padding = '0'
-    stage.style.margin = '0'
+    stage.style.margin = stage.style.padding = '0'
     # Apply 3D perspective and preserve any parent perspective.
     stage.style[css.perspective] = @settings.perspective + 'px'
     stage.style[css.transformStyle] = 'preserve-3d'


Fix a bug in cfb in which we were ouputting junk

--- src/openpgp/ocfb.iced
+++ src/openpgp/ocfb.iced
@@ -217,7 +217,7 @@
       @cipher.encryptBlock ablock.words, 0
       iblock = @next_block()
       ablock.xor iblock, {}
-      @out_bufs.push ablock.to_buffer()
+      @out_bufs.push ablock.to_buffer()[0...iblock.sigBytes]

     out = @compact()
     if not @resync then out = out[2...]


fix infinite loop in bang-bang

fix infinite loop in bang-bang

If you say "hubot !!!", you can cause bang-bang.coffee to go into an
infinite loop. This is because:

1. The filtering only removes entries that are exactly "!!"
2. The `respond` will match anything that starts with "!!"

Thus, hubot will go into an infinite loop like so:

    user: hubot !!!
    hubot: !!!
    hubot: !!!
    hubot: !!!
    hubot: !!!
    ...

--- src/scripts/bang-bang.coffee
+++ src/scripts/bang-bang.coffee
@@ -17,7 +17,7 @@
   robot.respond /(.+)/i, (msg) ->
     store msg

-  robot.respond /!!/i, (msg) ->
+  robot.respond /!!$/i, (msg) ->

     if exports.last_command?
       msg.send exports.last_command


Fixed URL to bitbucket POST service instructions

--- src/scripts/bitbucket.coffee
+++ src/scripts/bitbucket.coffee
@@ -8,7 +8,7 @@
 # Configuration:
 #   For instructions on how to set up BitBucket's POST service for your
 #   repositories, visit:
-#   http://confluence.atlassian.com/display/BITBUCKET/Setting+Up+the+bitbucket+POST+Service
+#   https://confluence.atlassian.com/display/BITBUCKET/POST+hook+management
 #
 # Author:
 #   JRusbatch


remove div from notebook

remove div from notebook

--- app/notebook.coffee
+++ app/notebook.coffee
@@ -12,7 +12,8 @@

 modules.export exports, 'notebook', ({cmd}) ->
   cmd 'save', 'Saves the current notebook to a file', ->
-    @div save @notebook, @input_cell
+    link = save @notebook, @input_cell
+    @add_component React.DOM.a {href: link.href}, 'Download Notebook'

   cmd 'load', 'Loads a script from a URL', (url, options={}) ->
     if arguments.length is 0
@@ -355,7 +356,6 @@
     text = JSON.stringify export_notebook input_cell
     blob = new Blob [text], type: notebook_content_type
     link = document.createElement 'a'
-    link.innerHTML = 'Download Notebook'
     link.href = window.webkitURL.createObjectURL blob
     link.download = 'notebook.lnb'
     link.click()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -176,10 +176,6 @@
       hsl[0] = (hsl[0] + degrees) % 360

       [@r, @g, @b, @a] = hslToRgb(hsl)
-
-      @r = @r.round()
-      @g = @g.round()
-      @b = @b.round()

       return this


Sanity check environment variables

Sanity check environment variables

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -20,6 +20,11 @@
 #   HUBOT_PAGERDUTY_SCHEDULE_ID

 pagerDutyUsers = {}
+pagerDutyUsername    = process.env.HUBOT_PAGERDUTY_USERNAME
+pagerDutyPassword    = process.env.HUBOT_PAGERDUTY_PASSWORD
+pagerDutySubdomain   = process.env.HUBOT_PAGERDUTY_SUBDOMAIN
+pagerDutyBaseUrl     = "https://#{pagerDutySubdomain}.pagerduty.com/api/v1"
+pagerDutyApiKey      = process.env.HUBOT_PAGERDUTY_APIKEY

 module.exports = (robot) ->
   robot.respond /pager( me)?$/i, (msg) ->
@@ -80,13 +85,26 @@
     withCurrentOncall msg, (username) ->
       msg.reply "#{username} is on call"

+missingEnvironmentForApi = (msg) ->
+  missingAnything = false
+  unless pagerDutySubdomain?
+    msg.send "PagerDuty Subdomain is missing:  Ensure that HUBOT_PAGERDUTY_SUBDOMAIN is set."
+    missingAnything |= true
+  unless pagerDutyUsername?
+    msg.send "PagerDuty username is missing:  Ensure that HUBOT_PAGERDUTY_USERNAME is set."
+    missingAnything |= true
+  unless pagerDutyPassword?
+    msg.send "PagerDuty password is missing:  Ensure that HUBOT_PAGERDUTY_PASSWORD is set."
+    missingAnything |= true
+  missingAnything
+
+
 pagerDutyGet = (msg, url, query, cb) ->
-  user    = process.env.HUBOT_PAGERDUTY_USERNAME
-  pass    = process.env.HUBOT_PAGERDUTY_PASSWORD
-  baseUrl = "https://#{process.env.HUBOT_PAGERDUTY_SUBDOMAIN}.pagerduty.com/api/v1"
-
-  auth = 'Basic ' + new Buffer(user + ':' + pass).toString('base64');
-  msg.http(baseUrl + url)
+  if missingEnvironmentForApi(msg)
+    return
+
+  auth = 'Basic ' + new Buffer(pagerDutyUsername + ':' + pagerDutyPassword).toString('base64')
+  msg.http(pagerDutyBaseUrl + url)
     .query(query)
     .headers(Authorization: auth, Accept: 'application/json')
     .get() (err, res, body) ->
@@ -100,13 +118,12 @@
       cb json_body

 pagerDutyPut = (msg, url, data, cb) ->
-  user    = process.env.HUBOT_PAGERDUTY_USERNAME
-  pass    = process.env.HUBOT_PAGERDUTY_PASSWORD
-  baseUrl = "https://#{process.env.HUBOT_PAGERDUTY_SUBDOMAIN}.pagerduty.com/api/v1"
+  if missingEnvironmentForApi(msg)
+    return

   json = JSON.stringify(data)
-  auth = 'Basic ' + new Buffer(user + ':' + pass).toString('base64');
-  msg.http(baseUrl + url)
+  auth = 'Basic ' + new Buffer(pagerDutyUsername + ':' + pagerDutyPassword).toString('base64')
+  msg.http(pagerDutyBaseUrl + url)
     .headers(Authorization: auth, Accept: 'application/json')
     .header("content-type","application/json")
     .header("content-length",json.length)
@@ -121,13 +138,12 @@
       cb json_body

 pagerDutyPost = (msg, url, data, cb) ->
-  user    = process.env.HUBOT_PAGERDUTY_USERNAME
-  pass    = process.env.HUBOT_PAGERDUTY_PASSWORD
-  baseUrl = "https://#{process.env.HUBOT_PAGERDUTY_SUBDOMAIN}.pagerduty.com/api/v1"
+  if missingEnvironmentForApi(msg)
+    return

   json = JSON.stringify(data)
-  auth = 'Basic ' + new Buffer(user + ':' + pass).toString('base64');
-  msg.http(baseUrl + url)
+  auth = 'Basic ' + new Buffer(pagerDutyUsername + ':' + pagerDutyPassword).toString('base64')
+  msg.http(pagerDutyBaseUrl + url)
     .headers(Authorization: auth, Accept: 'application/json')
     .header("content-type","application/json")
     .header("content-length",json.length)
@@ -173,8 +189,7 @@
     cb(json.incidents)

 pagerDutyIntegrationAPI = (msg, cmd, args, cb) ->
-  apikey = process.env.HUBOT_PAGERDUTY_APIKEY
-  unless apikey?
+  unless pagerDutyApiKey?
     msg.send "PagerDuty API service key is missing."
     msg.send "Ensure that HUBOT_PAGERDUTY_APIKEY is set."
     return
@@ -182,7 +197,7 @@
   data = null
   switch cmd
     when "trigger"
-      data = JSON.stringify { service_key: "#{apikey}", event_type: "trigger", description: "#{args}"}
+      data = JSON.stringify { service_key: pagerDutyApiKey, event_type: "trigger", description: "#{args}"}
       pagerDutyIntergrationPost msg, data, (json) ->
         cb(json)


Remove unused query

Remove unused query

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -375,9 +375,6 @@
   robot.respond /who('s|s| is|se)? (on call|oncall|on-call)( (?:for )?(.+))?/i, (msg) ->
     scheduleName = msg.match[4]

-    query = {}
-    query['query'] = scheduleName if scheduleName?
-
     displaySchedule = (s) ->
       withCurrentOncall msg, s, (username, schedule) ->
         msg.send "* #{username} is on call for #{schedule.name} - https://#{pagerDutySubdomain}.pagerduty.com/schedules##{schedule.id}\n"
@@ -385,7 +382,7 @@
     if scheduleName?
       withScheduleMatching msg, scheduleName, displaySchedule
     else
-      pagerDutyGet msg, "/schedules", query, (json) ->
+      pagerDutyGet msg, "/schedules", {}, (json) ->
         schedules = json.schedules
         if schedules.length > 0
           for s in schedules


支持新的配置文件方案，并检查配置文件是否存在

支持新的配置文件方案，并检查配置文件是否存在

--- src/common.coffee
+++ src/common.coffee
@@ -3,15 +3,21 @@
 ###
 _path = require 'path'
 _crypto = require 'crypto'
+_util = require 'util'
+_events = require 'events'
+
 _fs = require 'fs-extra'
-_events = require 'events'
 _ = require 'lodash'
+_mime = require 'mime'

 ENVISDEV = (->process.env.NODE_ENV is 'development')()
-WORKSPACE = "./config/#{process.env.NODE_ENV || 'development'}"
-_config = require WORKSPACE
-_util = require 'util'
-_mime = require 'mime'
+CONFIG_FILE = _path.join(__dirname, "../#{process.env.NODE_ENV || 'development'}.config")
+
+if not _fs.existsSync(CONFIG_FILE)
+  console.log "配置文件#{CONFIG_FILE}不存在，请检查".red
+  process.exit 1
+
+_config = require CONFIG_FILE
 _pageEvent = new _events.EventEmitter()

 #触发事件
@@ -32,7 +38,7 @@
 _config.uploads = exports.storagePath 'uploads'
 _config.uploadTemporary = exports.storagePath 'uploadTemporary'

-console.log "Workspace -> #{WORKSPACE}"
+console.log "Config file -> #{CONFIG_FILE}"
 console.log "Uploads -> #{_config.uploads}"
 console.log "uploadTemporary -> #{_config.uploadTemporary}"


I need to program better

I need to program better

--- machina.coffee
+++ machina.coffee
@@ -101,7 +101,7 @@
       next()

     for resource of @options.resources
-      do (config = @config(resource)) =>
+      do (resource, config = @config(resource)) =>
         resource_path = "/#{resource}"

         router = express.Router()


Refactor for dry out pane creation logic in tabview.

--- client/ide/views/tabview/idetabview.coffee
+++ client/ide/views/tabview/idetabview.coffee
@@ -30,37 +30,35 @@
         callback     : => @createDrawingBoard()
     }

+  createPane_: (view, paneOptions) ->
+    unless view or paneOptions
+      return new Error "Missing argument for createPane_ helper"
+
+    unless view instanceof KDView
+      return new Error "View must be an instance of KDView"
+
+    pane = new KDTabPaneView paneOptions
+    pane.addSubView view
+    @tabView.addPane pane
+
   createEditor: ->
     file      = FSHelper.createFileFromPath "localfile://Untitled.txt"
     content   = "This is my localfile"
     editor    = new EditorPane { file, content, delegate: this }
-    pane      = new KDTabPaneView
+    paneOptions =
       name    : file.name
       aceView : editor.ace

-    pane.addSubView editor
-    @tabView.addPane pane
+    @createPane_ editor, paneOptions

   createTerminal: ->
-    pane   = new KDTabPaneView
-      name : "Terminal"
-
-    pane.addSubView new TerminalPane
-    @tabView.addPane pane
+    @createPane_ new TerminalPane, { name: "Terminal" }

   createDrawingBoard: ->
-    pane   = new KDTabPaneView
-      name : "Drawing"
-
-    pane.addSubView new DrawingPane
-    @tabView.addPane pane
+    @createPane_ new DrawingPane,  { name: "Drawing"  }

   createPreview: ->
-    pane   = new KDTabPaneView
-      name : "Browser"
-
-    pane.addSubView new PreviewPane
-    @tabView.addPane pane
+    @createPane_ new PreviewPane,  { name: "Browser"  }

   removeOpenDocument: ->
     # TODO: This method is legacy, should be reimplemented in ace bundle.


minor cleanup, safeguard against empty translation

--- chrome/content/zotero-better-bibtex/schomd.coffee
+++ chrome/content/zotero-better-bibtex/schomd.coffee
@@ -116,9 +116,6 @@
     Zotero.BetterBibTeX.log("No results found")
     return []

-  # fetching items using IDs
-  items = Zotero.Items.get(results)
-
   return (
     {
       id: item.id
@@ -139,16 +136,18 @@
   format ?= 'json'
   displayOptions ?= {}

-  switch format
-    when 'json'
-      return (
-        {
-          id: item.id
-          library: item.libraryID
-          key: item.key
-          bibtex: Zotero.BetterBibTeX.translate(Zotero.BetterBibTeX.getTranslator(translator), {items: [item]}, displayOptions)
-        } for item in items
-      )
+  if format == 'json'
+    return (
+      {
+        id: item.id
+        library: item.libraryID
+        key: item.key
+        bibtex: Zotero.BetterBibTeX.translate(Zotero.BetterBibTeX.getTranslator(translator), {items: [item]}, displayOptions)
+      } for item in items
+    )

-    else
-      return Zotero.BetterBibTeX.translate(Zotero.BetterBibTeX.getTranslator(translator), {items: items}, displayOptions)
+
+  # translate doesn't like being passed an empty set of items
+  return Zotero.BetterBibTeX.translate(Zotero.BetterBibTeX.getTranslator(translator), {items: items}, displayOptions) if items.length != 0
+
+  return []


Call track without delay

Call track without delay

--- src/remote/services/hull.coffee
+++ src/remote/services/hull.coffee
@@ -41,10 +41,12 @@
     trackHandler = (req, route, callback, errback)->
       analytics = require('analytics')
       eventName = req.path.replace(/^track\//, '')
-      _.delay (-> analytics.track(eventName, req.params)), 500
-      req.path    = "t"
+
+      analytics.track(eventName, req.params)
+
+      req.path = "t"
       req.params.event ?= eventName
-      req.params  = { t: btoa(JSON.stringify(req.params)) }
+      req.params = { t: btoa(JSON.stringify(req.params)) }
       handler(req, route, callback, errback)

     require:


Tracks every non-GET API calls to Hull.io

--- src/remote/services/hull.coffee
+++ src/remote/services/hull.coffee
@@ -68,9 +68,10 @@
           # Reset token if the user has changed...
           accessToken = false

-        callback({ response: response, headers: headers, provider: 'hull' })
+        callback({ response: response, headers: headers, provider: 'hull' }) if _.isFunction(callback)

       request.fail(errback)
+      trackHandler({path: req.method, params: req}) unless req.path == 't' or ['get'].indexOf(req.method?.toLowerCase()) != -1

       return

@@ -83,6 +84,7 @@
       req.path = "t"
       req.params.event ?= eventName
       req.params = { t: btoa(JSON.stringify(req.params)) }
+      req.method ?= 'post'
       handler(req, callback, errback)

     require:


Groups: Reorder pricing buttons.

Groups: Reorder pricing buttons.

--- client/Main/payment/planupgradeconfirmform.coffee
+++ client/Main/payment/planupgradeconfirmform.coffee
@@ -6,14 +6,14 @@

     @buttonBar = new KDButtonBar
       buttons       :
+        cancel      :
+          title     : "CANCEL"
+          cssClass  : "solid"
+          callback  : => @emit 'Cancel'
         Buy         :
           title     : "CONFIRM ORDER"
           cssClass  : "solid green"
           callback  : => @emit 'PaymentConfirmed'
-        cancel      :
-          title     : "CANCEL"
-          cssClass  : "solid"
-          callback  : => @emit 'Cancel'

   viewAppended: ->
     @unsetClass 'kdview'


minor

minor

--- client/app/MainApp/maincontroller/mainview.coffee
+++ client/app/MainApp/maincontroller/mainview.coffee
@@ -134,7 +134,7 @@
       $('body').removeClass "loggedIn"
       @contentPanel.unsetClass "social"
       @mainTabView.hideHandleContainer()
-      # @buttonHolder.show()
+      @buttonHolder.show()
       @logo.hide()

     @changeHomeLayout isLoggedIn


Deprecate unused / unnecessary methods

Deprecate unused / unnecessary methods

--- src/config.coffee
+++ src/config.coffee
@@ -162,82 +162,89 @@
   isDefault: (keyPath) ->
     not _.valueForKeyPath(@settings, keyPath)?

-  # Extended: Push the value to the array at the key path.
+  ###
+  Section: To Deprecate
+  ###
+
+  # Retrieves the setting for the given key as an integer.
+  #
+  # * `keyPath` The {String} name of the key to retrieve
+  #
+  # Returns the value from Atom's default settings, the user's configuration
+  # file, or `NaN` if the key doesn't exist in either.
+  getInt: (keyPath) ->
+    parseInt(@get(keyPath))
+
+  # Retrieves the setting for the given key as a positive integer.
+  #
+  # * `keyPath` The {String} name of the key to retrieve
+  # * `defaultValue` The integer {Number} to fall back to if the value isn't
+  #                positive, defaults to 0.
+  #
+  # Returns the value from Atom's default settings, the user's configuration
+  # file, or `defaultValue` if the key value isn't greater than zero.
+  getPositiveInt: (keyPath, defaultValue=0) ->
+    Math.max(@getInt(keyPath), 0) or defaultValue
+
+  # Toggle the value at the key path.
+  #
+  # The new value will be `true` if the value is currently falsy and will be
+  # `false` if the value is currently truthy.
+  #
+  # * `keyPath` The {String} name of the key.
+  #
+  # Returns the new value.
+  toggle: (keyPath) ->
+    @set(keyPath, !@get(keyPath))
+
+  ###
+  Section: Deprecated
+  ###
+
+  # Unobserve all callbacks on a given key.
+  # * `keyPath` The {String} name of the key to unobserve.
+  #
+  unobserve: (keyPath) ->
+    deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.'
+
+  # Push the value to the array at the key path.
   #
   # * `keyPath` The {String} key path.
   # * `value` The value to push to the array.
   #
   # Returns the new array length {Number} of the setting.
   pushAtKeyPath: (keyPath, value) ->
+    deprecate 'Please remove from your code. Config::pushAtKeyPath is going away. Please push the value onto the array, and call Config::set'
     arrayValue = @get(keyPath) ? []
     result = arrayValue.push(value)
     @set(keyPath, arrayValue)
     result

-  # Extended: Add the value to the beginning of the array at the key path.
+  # Add the value to the beginning of the array at the key path.
   #
   # * `keyPath` The {String} key path.
   # * `value` The value to shift onto the array.
   #
   # Returns the new array length {Number} of the setting.
   unshiftAtKeyPath: (keyPath, value) ->
+    deprecate 'Please remove from your code. Config::unshiftAtKeyPath is going away. Please unshift the value onto the array, and call Config::set'
     arrayValue = @get(keyPath) ? []
     result = arrayValue.unshift(value)
     @set(keyPath, arrayValue)
     result

-  # Public: Remove the value from the array at the key path.
+  # Remove the value from the array at the key path.
   #
   # * `keyPath` The {String} key path.
   # * `value` The value to remove from the array.
   #
   # Returns the new array value of the setting.
   removeAtKeyPath: (keyPath, value) ->
+    deprecate 'Please remove from your code. Config::removeAtKeyPath is going away. Please remove the value from the array, and call Config::set'
     arrayValue = @get(keyPath) ? []
     result = _.remove(arrayValue, value)
     @set(keyPath, arrayValue)
     result
-
-  ###
-  Section: To Deprecate
-  ###
-
-  # Retrieves the setting for the given key as an integer.
-  #
-  # * `keyPath` The {String} name of the key to retrieve
-  #
-  # Returns the value from Atom's default settings, the user's configuration
-  # file, or `NaN` if the key doesn't exist in either.
-  getInt: (keyPath) ->
-    parseInt(@get(keyPath))
-
-  # Retrieves the setting for the given key as a positive integer.
-  #
-  # * `keyPath` The {String} name of the key to retrieve
-  # * `defaultValue` The integer {Number} to fall back to if the value isn't
-  #                positive, defaults to 0.
-  #
-  # Returns the value from Atom's default settings, the user's configuration
-  # file, or `defaultValue` if the key value isn't greater than zero.
-  getPositiveInt: (keyPath, defaultValue=0) ->
-    Math.max(@getInt(keyPath), 0) or defaultValue
-
-  # Toggle the value at the key path.
-  #
-  # The new value will be `true` if the value is currently falsy and will be
-  # `false` if the value is currently truthy.
-  #
-  # * `keyPath` The {String} name of the key.
-  #
-  # Returns the new value.
-  toggle: (keyPath) ->
-    @set(keyPath, !@get(keyPath))
-
-  # Unobserve all callbacks on a given key.
-  # * `keyPath` The {String} name of the key to unobserve.
-  #
-  unobserve: (keyPath) ->
-    @off("updated.#{keyPath.replace(/\./, '-')}")

   ###
   Section: Private


Fix config.get for schemas containing objects

The config.get method would not return the default values for the
properties in objects. Now with _.deepExtends, the default values are
the base while any new values overwrite the defaults. This way all
default values appear, including those nested in an object. This fixes
atom/settings-view#386 and fixes atom/settings-view#518.

--- src/config.coffee
+++ src/config.coffee
@@ -864,8 +864,8 @@
       defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)

     if value?
-      value = @deepClone(value)
-      _.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
+      defaultValue = @deepClone(defaultValue)
+      value = _.deepExtends(defaultValue, value) if isPlainObject(value) and isPlainObject(defaultValue)
     else
       value = @deepClone(defaultValue)


Additional error reporting for Jaba errors with no location information

--- app/lib/aether/problems.coffee
+++ app/lib/aether/problems.coffee
@@ -163,7 +163,10 @@
                        ranges.offsetToPos(error.range[1], code, codePrefix)]
       options.hint = error.message
     when 'jaba'
-      options.range = [error.location.start.offset, error.location.end.offset]
+      if error.location
+        options.range = [error.location.start.offset, error.location.end.offset]
+      else
+        console.error "Jaba error with no location information:", error
       options.hint = error.message
     else
       console.warn "Unhandled UserCodeProblem reporter", options.reporter


use Object.create null instead of {} so we keep from getting a prototype

--- lib/set.coffee
+++ lib/set.coffee
@@ -4,7 +4,7 @@

 exports.Set = class Set
         constructor: (arr = []) ->
-                @set = {}
+                @set = Object.create null
                 @set[a] = a for a in arr

         member: (el) -> hasOwn.call @set, el


Tests: Environments: Domains: Fixed assignDomain test.

--- client/test/lib/environments/domain.coffee
+++ client/test/lib/environments/domain.coffee
@@ -2,8 +2,9 @@
 assert  = require 'assert'
 environmentHelpers = require '../helpers/environmenthelpers.js'

-domainItem  = '.domains-view .kdlistitemview-domain:last-child'
-loader      = '.domains-view .in-progress.kdloader'
+domainItem  = '.kdlistitemview-domain:last-child'
+loader      = domainItem + '.in-progress'
+

 module.exports =

@@ -59,12 +60,11 @@
     browser
       .waitForElementVisible     domainItem, 20000
       .click                     domainItem + ' .koding-on-off.on'
-      .waitForElementVisible     loader, 10000
-      .waitForElementNotVisible  loader, 20000
+      .waitForElementVisible     '.in-progress', 10000
+      .waitForElementNotPresent  '.in-progress', 20000
       .refresh()

-    environmentHelpers.openVmSettingsModal(browser)
-    environmentHelpers.clickMoreButtonInVMSettingsModal(browser)
+    environmentHelpers.openDomainSettings(browser)

     browser
       .waitForElementVisible     domainItem + ' .koding-on-off.off', 20000


typo in fr locales

typo in fr locales

--- client/app/locales/fr.coffee
+++ client/app/locales/fr.coffee
@@ -231,7 +231,7 @@
         'welcome title': "Bienvenue dans votre Cozy"
         'welcome content': """

Cet assistant va vous aider à choisir, installer et configurer vos applications dans votre Cozy.

-
N'oubliez pas que Cozy est en phase beta, n'hésitez pas à nous contacter si vous rencontrez des diffcultés dans votre utilisation.

+
N'oubliez pas que Cozy est en phase beta, n'hésitez pas à nous contacter si vous rencontrez des difficultés dans votre utilisation.

         """
         'yes': "Activer l'application %{slug}"
         'no': "Non, merci"


app/test: add code block test path

app/test: add code block test path

--- client/app/test/index.coffee
+++ client/app/test/index.coffee
@@ -10,3 +10,4 @@
 require './util/stripHTMLtoText.test'
 require './util/proxifier.test'
 require './stacktemplatelistcontroller.test'
+require '../lib/components/codeblockmodal/test'


move getEditor fn

move getEditor fn

--- src/react-editor-view.coffee
+++ src/react-editor-view.coffee
@@ -117,14 +117,14 @@
       else
         @focusOnAttach = true

-  getEditor: ->
-    deprecate("Use EditorView::getModel instead")
-    @editor
-
   # Public: Get the underlying editor model for this view.
   #
   # Returns an {Editor}.
   getModel: -> @editor
+
+  getEditor: ->
+    deprecate('Use EditorView::getModel instead')
+    @editor

   Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeightInPixels()
   Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth()


set viewMediator element after init

set viewMediator element after init

--- src/scripts/app/app.main.coffee
+++ src/scripts/app/app.main.coffee
@@ -9,4 +9,7 @@

 $(document).ready ->
     router = require './router.coffee'
+    viewMediator = require './viewMediator.coffee'
+
+    viewMediator.el = document.querySelector 'body'
     Backbone.history.start { pushState: true }


Remove react explicit dependency in progressbar test

--- components/__tests__/ProgressBar.spec.cjsx
+++ components/__tests__/ProgressBar.spec.cjsx
@@ -1,4 +1,3 @@
-React       = require('react/addons')
 expect      = require('expect')
 utils       = require('./utils')


Optimize measuring long lines

Optimize measuring long lines

--- src/editor.coffee
+++ src/editor.coffee
@@ -11,6 +11,7 @@
 MeasureRange = document.createRange()
 TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
 NoScope = ['no-scope']
+LongLineLength = 1000

 # Private: Represents the entire visual pane in Atom.
 #
@@ -1589,13 +1590,11 @@
       content = textNode.textContent

       for char, i in content
+        # Don't continue caching long lines :racehorse:
+        break if index > LongLineLength and column < index

         # Dont return right away, finish caching the whole line
-        if index == column
-          returnLeft = left
-
-          # Don't cache lines longer than 100 chars
-          return returnLeft if lineElement.innerText.length > 160
+        returnLeft = left if index == column
         oldLeft = left

         scopes = @scopesForColumn(tokenizedLine, index)
@@ -1611,7 +1610,14 @@
           return 0 if rects.length == 0
           left = rects[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft())

-          @setCharacterWidthCache(scopes, char, left - oldLeft) if scopes?
+          if scopes?
+            cachedVal = left - oldLeft
+            @setCharacterWidthCache(scopes, char, cachedVal)
+
+        # Assume all the charachters are the same width when dealing with long
+        # lines :racehorse:
+        return column * cachedVal if index > LongLineLength
+

         index++


namespaced Movable

namespaced Movable

--- src/engine.coffee
+++ src/engine.coffee
@@ -151,7 +151,7 @@
     step = ->
       if !I.paused || frameAdvance
         secondsPerFrame = (1000 / I.FPS)
-        update(msPerFrame)
+        update(secondsPerFrame)
         I.age += 1

       draw()


Remove Batman.Navigator::setPath

Remove Batman.Navigator::setPath

--- src/routing/navigator.coffee
+++ src/routing/navigator.coffee
@@ -60,12 +60,9 @@
     @cachedPath = @_lastRedirect if @_lastRedirect

     if !@_lastRedirect or @_lastRedirect is path
-      @setPath(path, replaceState)
+      @[if replaceState then 'replaceState' else 'pushState'](null, '', path)

     path
-
-  setPath: (path, replaceState=false) ->
-    @[if replaceState then 'replaceState' else 'pushState'](null, '', path)

   push: (params) ->
     Batman.developer.deprecated("Navigator::push", "Please use Batman.redirect({}) instead.")


Fix typo

Fix typo

--- app/views/settings/data.coffee
+++ app/views/settings/data.coffee
@@ -37,8 +37,8 @@
       intSources: @intSources or []

     if @model.get('data_source').get('source_type')?
-      opts.sourceType = @model.get("data_source").get('source_type')
-      opts.source = @model.get('data_ource').get('source')
+      opts.sourceType = @model.get('data_source').get('source_type')
+      opts.source = @model.get('data_source').get('source')

       switch @model.get('data_source').get('source_type')
         when 'external'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/follow.coffee
+++ src/follow.coffee
@@ -2,7 +2,7 @@
   Object.reverseMerge I,
     velocity: Point(0, 0)

-  follow: () -
+  follow: (o) ->

   self.bind "update", ->
     I.velocity = player.position().subtract(self.position()).norm().scale(5)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/follow.coffee
+++ src/follow.coffee
@@ -53,7 +53,7 @@

   enemy.follow(player)

-  # => The enemy now has it's d
+  # => The enemy now has it's direction attribute set to


Make db dir if it does not exist

Make db dir if it does not exist

--- server/express/lib/page.coffee
+++ server/express/lib/page.coffee
@@ -12,6 +12,8 @@
 # Export a function that generates a page handler
 # when called with options object.
 module.exports = exports = (argv) ->
+  mkdirp argv.db, (e) ->
+    if e then throw e

   #### Private utility methods. ####
   load_parse = (loc, cb) ->


Register process port into Instance Status

--- server/startup/presence.coffee
+++ server/startup/presence.coffee
@@ -1,5 +1,5 @@
 Meteor.startup ->
-	InstanceStatus.registerInstance('rocket.chat')
+	InstanceStatus.registerInstance('rocket.chat', {port: process.env.PORT})
 	# InstanceStatus.activeLogs()
 	UserPresence.start()
 	# UserPresence.activeLogs()


Handle "In Progress - Critical" bug

Handle "In Progress - Critical" bug

If a game was near the end, it would show in the API as "In Progress - Critical" in the `detailedGameState` property. Because of the matching I was doing, that kept the time remaining from showing. This instead relies on the `abstractGameState`, which supposedly can only be `Preview`, `Live` or `Final`.

--- src/hockey.coffee
+++ src/hockey.coffee
@@ -59,9 +59,9 @@
         date = json.dates[0].date
         game = json.dates[0].games[0]
         # Handle in-progress games
-        if game.status.detailedState == 'Final'
+        if game.status.abstractGameState == 'Final'
           gameStatus = 'Final'
-        else if game.status.detailedState == 'In Progress'
+        else if game.status.abstractGameState == 'Live'
           gameStatus = "#{game.linescore.currentPeriodTimeRemaining} #{game.linescore.currentPeriodOrdinal}"
         else
           gameStatus = game.status.detailedState


Added step to save the file to disk before converting

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -1358,18 +1358,23 @@
             console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")
             return

-        webapp_client.exec
-            project_id  : @project_id
-            command     : "smc-sagews2ipynb #{@filename}"
-            bash        : true
-            err_on_exit : false
-            cb          : (err, output) =>
-                if err
-                    alert_message(type:"error", message:"Error occured converting #{@filename}")
-                else
-                    redux.getProjectActions(@project_id).open_file
-                        path               : base + '.ipynb'
-                        foreground         : true
+        async.series([
+            (cb) =>
+                @save(cb)
+            (cb) =>
+                webapp_client.exec
+                    project_id  : @project_id
+                    command     : "smc-sagews2ipynb #{@filename}"
+                    bash        : true
+                    err_on_exit : false
+                    cb          : (err, output) =>
+                        if err
+                            alert_message(type:"error", message:"Error occured converting #{@filename}")
+                        else
+                            redux.getProjectActions(@project_id).open_file
+                                path               : base + '.ipynb'
+                                foreground         : true
+        ])

     cut: (cm) =>
         if not cm?


Half-implement admin title access on private projects

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -155,7 +155,7 @@
         show_hidden = store.get('show_hidden') ? false

         relation = redux.getStore('projects').get_my_group(opts.project_id)
-        if not relation? or relation == 'public' or relation == 'admin'
+        if not relation? or relation in ['public', 'admin']
             @fetch_public_project_title(opts.project_id)
         actions.set_directory_files(store.get('current_path'), sort_by_time, show_hidden)
         redux.getActions('page').set_active_tab(opts.project_id) if opts.switch_to
@@ -208,11 +208,13 @@
             query :
                 public_projects : {project_id : project_id, title : null}
             cb    : (err, resp) =>
+                # TESTING-RR-JJ
+                console.log("RECIEVED", err, resp)
+                window.resp = resp
                 if not err
-                    # FUTURE: use the store somehow to report error?
                     title = resp?.query?.public_projects?.title
-                    title ?= "ANONYMOUS"
-                    @setState(public_project_titles : store.get('public_project_titles').set(project_id, title))
+                title ?= "PRIVATE -- Admin req"
+                @setState(public_project_titles : store.get('public_project_titles').set(project_id, title))

     # If something needs the store to fill in
     #    directory_tree.project_id = {updated:time, error:err, tree:list},


fixed cancel buttons on creating new cell modal

--- src/smc-webapp/sagews.coffee
+++ src/smc-webapp/sagews.coffee
@@ -802,8 +802,8 @@
                 if result
                     @_handle_input_cell_click0(e, mark)
                 else # what the user really wants...
-                    cm.focus()
-                    cm.setCursor({line:mark.find().from.line+1, ch:0})
+                    @codemirror.focus()
+                    @codemirror.setCursor({line:mark.find().from.line+1, ch:0})
         else
             @_handle_input_cell_click0(e, mark)
         return false


webapp/latex: fixing closures for async.series and _need_to_run states

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -2050,7 +2050,10 @@
         @pdf_updated = true
         if not opts.latex_command?
             opts.latex_command = @default_tex_command()
-        @_need_to_run = {latex: true}
+        @_need_to_run =
+            latex  : true   # initially, only latex is true
+            sage   : false  # either false or a filename
+            bibtex : false
         log = ''
         status = opts.status

@@ -2104,7 +2107,10 @@
         sagetex_file = @base_filename + '.sagetex.sage'
         not_latexmk = command.indexOf('latexmk') == -1
         sha_marker = 'sha1sums'
-        @_need_to_run.latex = false
+        @_need_to_run =
+            latex  : false
+            sage   : false  # either false or a filename
+            bibtex : false
         # yes x business recommended by http://tex.stackexchange.com/questions/114805/pdflatex-nonstopmode-with-tikz-stops-compiling
         @_exec
             command : "touch '#{@filename_tex}'; yes x | " + command + "; echo '#{sha_marker}'; sha1sum '#{sagetex_file}'"
@@ -2121,6 +2127,9 @@
                         output.stdout = output.stdout.slice(0,i)
                         for x in shas.split('\n')
                             v = x.split(/\s+/)
+                            if v.length != 2
+                                continue
+                            #if DEBUG then console.log(v, sagetex_file, @_sagetex_file_sha)
                             if v[1] == sagetex_file and v[0] != @_sagetex_file_sha
                                 @_need_to_run.sage = sagetex_file
                                 @_sagetex_file_sha = v[0]
@@ -2147,10 +2156,15 @@
                     if not_latexmk and log.indexOf(no_bbl) != -1
                         @_need_to_run.bibtex = true

+                    log += "\n\n#{misc.to_json(@_need_to_run)}\n@_sagetex_file_sha: #{@_sagetex_file_sha}"
+
                     @last_latex_log = log
                     cb?(false, log)

     _run_sage: (target, cb) =>
+        # don't run sage if target is false
+        if underscore.isBoolean(target) and not target
+            cb()
         if not target?
             target = @base_filename + '.sagetex.sage'
         @_exec


use the same throttling on file use updates -- it was circumvented by mark all

--- src/smc-webapp/file_use.cjsx
+++ src/smc-webapp/file_use.cjsx
@@ -112,24 +112,6 @@
             err = misc.to_json(err)
         @setState(errors: @redux.getStore('file_use').get_errors().push(immutable.Map({time:new Date(), err:err})))

-    # Mark a record (or array of them) x with the given action happening now for this user.
-    # INPUT:
-    #    - x: uuid or array of uuid's of file_use records
-    #    - action: 'read', 'seen', 'edit', 'chat'
-    mark: (x, action) =>
-        if not misc.is_array(x)
-            x = [x]
-        table = @redux.getTable('file_use')
-        account_id = @redux.getStore('account').get_account_id()
-        u = {"#{account_id}":{"#{action}":new Date()}}
-        f = (id, cb) =>
-            table.set {id:id, users:u}, (err)=>
-                if err then err += "(id=#{id}) #{err}"
-                cb(err)
-        async.map x, f, (err) =>
-            if err
-                @record_error("error marking record #{action} -- #{err}")
-
     mark_all: (action) =>
         if action == 'read'
             v = @redux.getStore('file_use').get_all_unread()
@@ -137,7 +119,9 @@
             v = @redux.getStore('file_use').get_all_unseen()
         else
             @record_error("mark_all: unknown action '#{action}'")
-        @mark((x.id for x in v), action)
+            return
+        for x in v
+            @mark_file(x.project_id, x.path, action)

     mark_file: (project_id, path, action, ttl=120) =>
         path = misc.original_path(path)


Made log 80 symbols wide

Made log 80 symbols wide

--- src/io/tty.coffee
+++ src/io/tty.coffee
@@ -37,7 +37,7 @@
 		@logs = []
 		@pendingLogs = []

-		@logWidth = 60 - TtyRenderer.strMore.length
+		@logWidth = 80 - TtyRenderer.strMore.length

 		@game
 		.on 'turn.player', => @showMoreLogs()
@@ -86,7 +86,7 @@
 			else null

 	renderLog: (x, y) ->
-		program.fillArea x, y, 60, 1, ' '
+		program.fillArea x, y, 80, 1, ' '

 		if @logs.length > 0
 			program.move x, y


change ''Can't set title -- you are not a collaborator on this project.' to console warnin

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -77,7 +77,7 @@

     set_project_title: (project_id, title) =>
         if not @have_project(project_id)
-            alert_message(type:'error', message:"Can't set title -- you are not a collaborator on this project.")
+            console.warn("Can't set title -- you are not a collaborator on this project.")
             return
         if store.get_title(project_id) == title
             # title is already set as requested; nothing to do
@@ -91,7 +91,7 @@

     set_project_description: (project_id, description) =>
         if not @have_project(project_id)
-            alert_message(type:'error', message:"Can't set description -- you are not a collaborator on this project.")
+            console.warn("Can't set description -- you are not a collaborator on this project.")
             return
         if store.get_description(project_id) == description
             # description is already set as requested; nothing to do


change chat background to white so I can see contrast....

--- src/smc-webapp/smc_chat.cjsx
+++ src/smc-webapp/smc_chat.cjsx
@@ -663,7 +663,7 @@
             padding      : "0"
             paddingRight : "10px"
             paddingBottom: paddingBottom
-
+            background   : 'white'

         mobile_chat_log_style =
             overflowY    : "auto"
@@ -672,6 +672,7 @@
             height       : "100%"
             margin       : "0px 0px 0px 13px"
             padding      : "0"
+            background   : 'white'

         if not IS_MOBILE
             <Grid>


Upgrade deprecations asking for space-pen to use atom-space-pen-views

--- exports/atom.coffee
+++ exports/atom.coffee
@@ -40,11 +40,11 @@
   Object.defineProperty module.exports, '$', get: ->
     deprecate """
       Requiring `$` from `atom` is no longer supported.
-      If you are using `space-pen`, please require `$` from `space-pen`. Otherwise require `jquery` instead:
-        `{$} = require 'space-pen'`
+      If you are using `space-pen`, please require `$` from `atom-space-pen-views`. Otherwise require `jquery` instead:
+        `{$} = require 'atom-space-pen-views'`
         or
         `$ = require 'jquery'`
-      Add `"space-pen": "^4"` to your package dependencies.
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
       Or add `"jquery": "^2"` to your package dependencies.
     """
     $
@@ -52,27 +52,27 @@
   Object.defineProperty module.exports, '$$', get: ->
     deprecate """
       Requiring `$$` from `atom` is no longer supported.
-      Please require `space-pen` instead:
-        `{$$} = require 'space-pen'`
-      Add `"space-pen": "^4"` to your package dependencies.
+      Please require `atom-space-pen-views` instead:
+        `{$$} = require 'atom-space-pen-views'`
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     $$

   Object.defineProperty module.exports, '$$$', get: ->
     deprecate """
       Requiring `$$$` from `atom` is no longer supported.
-      Please require `space-pen` instead:
-        `{$$$} = require 'space-pen'`
-      Add `"space-pen": "^4"` to your package dependencies.
+      Please require `atom-space-pen-views` instead:
+        `{$$$} = require 'atom-space-pen-views'`
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     $$$

   Object.defineProperty module.exports, 'View', get: ->
     deprecate """
       Requiring `View` from `atom` is no longer supported.
-      Please require `space-pen` instead:
-        `{View} = require 'space-pen'`
-      Add `"space-pen": "^4"` to your package dependencies.
+      Please require `atom-space-pen-views` instead:
+        `{View} = require 'atom-space-pen-views'`
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     View

@@ -81,7 +81,7 @@
       Requiring `EditorView` from `atom` is no longer supported.
       Please require `TextEditorView` from `atom-space-pen-view` instead:
         `{TextEditorView} = require 'atom-space-pen-views'`
-      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     require '../src/text-editor-view'

@@ -90,7 +90,7 @@
       Requiring `TextEditorView` from `atom` is no longer supported.
       Please require `TextEditorView` from `atom-space-pen-view` instead:
         `{TextEditorView} = require 'atom-space-pen-views'`
-      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     require '../src/text-editor-view'

@@ -100,7 +100,7 @@
       Please require `ScrollView` from `atom-space-pen-view` instead:
         `{ScrollView} = require 'atom-space-pen-views'`
       Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views
-      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     require '../src/scroll-view'

@@ -110,7 +110,7 @@
       Please require `SelectListView` from `atom-space-pen-view` instead:
         `{SelectListView} = require 'atom-space-pen-views'`
       Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views
-      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+      Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies.
     """
     require '../src/select-list-view'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -131,8 +131,8 @@
   catch e
     this.toString()

-String::startsWith = () ->
-
+String::startsWith = (str) ->
+  this.lastIndexOf(str, 0) is 0

 ###*
 Returns a new string in Title Case.


Don't show CC information for declined transactions.

--- node_modules_koding/koding-payment/lib/main.coffee
+++ node_modules_koding/koding-payment/lib/main.coffee
@@ -451,7 +451,7 @@
           for t in page.transactions.transaction
             account = t.details[0].account[0]
             billing = t.details[0].account[0].billing_info[0]
-            transactions.push
+            trans =
               uuid      : t.uuid[0]
               status    : t.status[0]
               amount    : parseInt t.amount_in_cents[0]['_']
@@ -459,9 +459,11 @@
               currency  : t.currency[0]
               datetime  : t.created_at[0]['_']
               refundable: t.refundable[0]['_']
-              owner     : "#{unescape billing.first_name[0]} #{unescape billing.last_name[0]}"
-              card      : "#{billing.card_type[0]} - #{billing.first_six.toString().substring(0,4)}-...-#{billing.last_four}"
               source    : t.source[0]
+            if trans.status in ['success', 'void']
+              trans.owner  = "#{unescape billing.first_name[0]} #{unescape billing.last_name[0]}"
+              trans.card   = "#{billing.card_type[0]} - #{billing.first_six.toString().substring(0,4)}-...-#{billing.last_four}"
+            transactions.push trans
       callback no, transactions

 addUserTransaction = (accountCode, data, callback)->


fixed matching (proper indexof default value)

--- src/linker.coffee
+++ src/linker.coffee
@@ -36,7 +36,7 @@
     for key, value of tag.attrs
         if el.attrs[key] isnt value
             return no if typeof value isnt 'string' or
-                        (el.attrs[key]?.indexOf(value) or -1) is -1
+                        (el.attrs[key]?.indexOf(value) ? -1) is -1
     return yes

 ##