Restored numbering

Restored numbering

Chapters stopped being numbered. Fixed.

--- src/scripts/modules/media/tabs/contents/contents.coffee
+++ src/scripts/modules/media/tabs/contents/contents.coffee
@@ -20,7 +20,7 @@
           sectionNumber = cumulativeChapters[depth] ? 0
         if not (
           atTopLevel or
-          isCcap and sectionNumber is 0 and title.match(/^Introduction/)
+          isCcap and sectionNumber is 0
         )
           sectionNumber += 1
           cumulativeChapters[depth] = sectionNumber
@@ -60,9 +60,14 @@

     initialize: () ->
       super()
-      @listenTo(@model, 'change:editable removeNode moveNode add:contents', @render)
-      cumulativeChapters = []
-      numberChapters(@model.attributes.contents.models) if @model.attributes.contents?.models?
+      @listenTo(@model, 'change:editable removeNode moveNode', @render)
+      @listenTo(@model, 'change:contents', =>
+        nodes = @model.attributes.contents?.models
+        if nodes?
+          cumulativeChapters = []
+          numberChapters(nodes)
+          @render()
+        )

       console.debug("Contents:", @model.attributes.contents)
       nodes = @model.attributes.contents?.models


stop doing that

stop doing that

--- src/management/authenticator.coffee
+++ src/management/authenticator.coffee
@@ -10,8 +10,6 @@
             #
             # no authorization in header
             # --------------------------
-            #
-            # * send the usual 401
             #

             response.writeHead 401,


Clean up poi generator code a bit

Clean up poi generator code a bit

--- web2/js/views/poi-generator.cjsx
+++ web2/js/views/poi-generator.cjsx
@@ -17,23 +17,8 @@

 module.exports = React.createClass
-    displayName: 'PoiGenerator'
-
-
-    render: ->
-
-        typeCheckbox = (name, label) =>
-            setType = @props.setType.bind this, name
-            id = "type-#{name}"
-            <GeocacheTypeCheckbox
-                id={id}
-                key={id}
-                label={label}
-                selected={@props["type-#{name}"]}
-                toggle={setType}
-            />
-
-        types = [
+    getInitialState: ->
+        types: [
             name: 'traditional', label: 'Traditional Geocache'
         ,
             name: 'multi', label: 'Multi Geocache'
@@ -45,6 +30,7 @@
             name: 'webcam', label: 'Webcam Geocache'
         ]

+    render: ->
         classes = classnames
             ui: true
             form: true
@@ -62,7 +48,7 @@

                     Geocache Types
-                    {typeCheckbox t.name, t.label for t in types}
+                    {@typeCheckbox t.name, t.label for t in @state.types}

                     Exclude finds by
@@ -109,3 +95,14 @@

+
+    typeCheckbox: (name, label) ->
+        setType = @props.setType.bind this, name
+        id = "type-#{name}"
+        <GeocacheTypeCheckbox
+            id={id}
+            key={id}
+            label={label}
+            selected={@props["type-#{name}"]}
+            toggle={setType}
+        />


feat(events): fire change event when updating model

Previously, the model update suppressed the `change` modelEvent.
This event is no longer silent. Marionette can now respond to
the `change` modelEvent for further action.

--- src/meshweaver.validatedView.coffee
+++ src/meshweaver.validatedView.coffee
@@ -110,9 +110,7 @@
     @inputChanged(e)

   inputChanged: (e) =>
-    @self.model.set @modelFromInputs(),
-      silent: true
-
+    @self.model.set @modelFromInputs()
     @self.trigger 'input-change', e

     if !@self.model.isNew() and @saveExistingOnChange


prepare for release branch #70 removed /admin route

--- src/server/controllers/index.coffee
+++ src/server/controllers/index.coffee
@@ -28,6 +28,6 @@
       )
     )

-    app.get('/admin', (req, res)->
-      res.render('admin_index', {})
-    )
+#    app.get('/admin', (req, res)->
+#      res.render('admin_index', {})
+#    )


webapp/project_store: forgeound → foreground

--- src/smc-webapp/project_store.coffee
+++ src/smc-webapp/project_store.coffee
@@ -295,10 +295,10 @@
                             @get_store().convert_sagenb_worksheet opts.path, (err, sagews_filename) =>
                                 if not err
                                     @open_file
-                                        path : sagews_filename
-                                        forgeound : opts.foreground
+                                        path               : sagews_filename
+                                        foreground         : opts.foreground
                                         foreground_project : opts.foreground_project
-                                        chat : opts.chat
+                                        chat               : opts.chat
                                 else
                                     require('./alerts').alert_message(type:"error",message:"Error converting Sage Notebook sws file -- #{err}")
                             return
@@ -308,10 +308,10 @@
                             @get_store().convert_docx_file opts.path, (err, new_filename) =>
                                 if not err
                                     @open_file
-                                        path : new_filename
-                                        forgeound : opts.foreground
+                                        path               : new_filename
+                                        foreground         : opts.foreground
                                         foreground_project : opts.foreground_project
-                                        chat : opts.chat
+                                        chat               : opts.chat
                                 else
                                     require('./alerts').alert_message(type:"error",message:"Error converting Microsoft docx file -- #{err}")
                             return


Some stuff might not be string for some reason

--- resources/assets/coffee/osu_common.coffee
+++ resources/assets/coffee/osu_common.coffee
@@ -244,7 +244,7 @@
     # Wrapping the string with quotes and escaping the used quotes inside
     # is sufficient. Use double quote as it's easy to figure out with
     # encodeURI (it doesn't escape single quote).
-    if osu.present(url) then "url(\"#{url.replaceAll('"', '%22')}\")" else null
+    if osu.present(url) then "url(\"#{url.toString().replaceAll('"', '%22')}\")" else null

   navigate: (url, keepScroll, {action = 'advance'} = {}) ->


[test] [fix] Added missing done() call

[test] [fix] Added missing done() call

--- test/api.coffee
+++ test/api.coffee
@@ -8,9 +8,9 @@
     api = new Api
     api.start

-  it 'should start, connect to engine, and provide a WS server', ->
+  it 'should start, connect to engine, and provide a WS server', (done) ->
     api = new Api
     engine = new Engine
     engine.start {port: 3033, host: "0.0.0.0" }, () =>
       api.start {port: 3022, host: "0.0.0.0", engineEndpoint: 'ws://0.0.0.0:3033' }, () =>
-        logger.info 'called api callback'
+        done()


improved parent locator

improved parent locator

--- src/angles-editor-view.coffee
+++ src/angles-editor-view.coffee
@@ -213,54 +213,70 @@
               _findParent = (row, column) ->
                 openTags = []
                 closedTags = []
+                allTags = []
                 isOpeningTag = false
                 isClosingTag = false

                 finalTag = ''
-                maxRow = editor.getSession().getLength() + 100
+                maxRow = editor.getSession().getLength()

                 _scanRow = (row, column) ->
                   return if row > maxRow
                   curColumn = 0
                   tokens = editor.getSession().getTokens(row)
+                  lastTag = null
                   for token in tokens
                     curColumn += token.value.length
+
+                    isfinal = ->
+                      switch
+                        when openTags.length == 0
+                          true
+                        when openTags.length == closedTags.length
+                          openTags.pop()
+                          closedTags.pop()
+                          false
+                        when openTags[openTags.length-1] == closedTags[closedTags.length-1]
+                          openTags.pop()
+                          closedTags.pop()
+                          false
+                        else
+                          false
+
+                    latestTag = token.value if token.type == "meta.tag.tag-name"

                     if curColumn > column
                       switch
                         when token.type == "meta.tag" and token.value == "<"
                           isOpeningTag = true
+                        # TODO: return latestTag when cursor is on a closing tag
+                        when token.type == "meta.tag.r" and token.value == ">" and openTags.length == 0
+                          # The cursor must be on a closing tag,
+                          # return element value
+                          return latestTag
                         when token.type == "meta.tag" and token.value == "</"
                           isClosingTag = true
                         when token.type == "meta.tag.r" and token.value == "/>"
-                          openTags.pop()
                           isOpeningTag = false
                           isClosingTag = false
+                          milestone = openTags[openTags.length-1]
+                          milestone = latestTag if !milestone?
+                          closedTags.push milestone
+                          return milestone if isfinal()
                         when token.type == "meta.tag.tag-name" and isOpeningTag
-                          openTags.push(token.value)
+                          allTags.push "<#{token.value}>"
+                          openTags.push token.value
                           isOpeningTag = false
+                          return token.value if isfinal()
                         when token.type == "meta.tag.tag-name" and isClosingTag
-                          #finalTag = token.value
-                          closedTags.push(token.value)
+                          allTags.push "</#{token.value}>"
+                          closedTags.push token.value
                           isClosingTag = false
-
-                  switch
-                    when closedTags.length == 0
-                      _scanRow(row+1, 0)
-                    when closedTags.length == 1 and openTags.length == 0
-                      finalTag = closedTags[0]
-                    else
-                      openTags.reverse()
-                      for tag in openTags
-                        pos = closedTags.lastIndexOf tag
-                        if pos >= 0
-                          closedTags.splice(pos)
-
-                      if finalTag == "" and closedTags.length > 0
-                        finalTag = closedTags[0]
+                          return token.value if isfinal()
+
+                  _scanRow(row+1, 0)

                 _scanRow(row, column)
-                finalTag

               pos = editor.getCursorPosition()
               ident = _findParent(pos.row, pos.column)


payment: getOwnerGroup => fetchOwnerGroup

It's important to name asynchronous methods as fetchX rather than getX

--- workers/social/lib/social/models/payment/plan.coffee
+++ workers/social/lib/social/models/payment/plan.coffee
@@ -112,7 +112,7 @@
       ]
     , callback

-  getOwnerGroup: (callback)->
+  fetchOwnerGroup: (callback) ->
     if @product.prefix isnt 'groupplan'
       callback null, 'koding'
     else


Troubleshoot: Healthcheck is added for checking bongo status

--- workers/social/lib/social/models/systemstatus.coffee
+++ workers/social/lib/social/models/systemstatus.coffee
@@ -17,6 +17,8 @@
           (signature Function)
         forceReload:
           (do signature)
+        healthCheck  :
+          (signature Function)
       instance       :
         cancel:
           (signature Function)
@@ -110,3 +112,6 @@
         callback()
       else
         callback callback new KodingError "Could not cancel the system status"
+
+  @healthCheck = secure (client, callback) ->
+    callback result:1


changed order

changed order

--- app/assets/javascripts/analytics/presenter.coffee
+++ app/assets/javascripts/analytics/presenter.coffee
@@ -81,6 +81,8 @@
           $(this).attr( 'timeHovered', sHovered )

   onBeforeUnload: =>
+    ga?('send', 'timing', 'PageView', 'total', @pageViewTime)
+
     if $('dfn.JS-tooltip.hovered').length > 0
       keyword = $('dfn.JS-tooltip.hovered').html()
       time = $('dfn.JS-tooltip.hovered').attr('timeHovered')
@@ -88,8 +90,6 @@
         'send', 'event', 'TooltipRead', 'hoverout', 'tooltipActive:true;' +
         "keyword:#{keyword}", time
       )
-
-    ga?('send', 'timing', 'PageView', 'total', @pageViewTime)

     if @goalOffset
       ga?(


Changed file field validation to be based on hidden field value

--- app/assets/javascripts/catalog_admin/items.coffee
+++ app/assets/javascripts/catalog_admin/items.coffee
@@ -82,16 +82,18 @@
   return true

 dz_validate = ($dz) ->
-  if $($dz.element).attr('data-required') == 'true' && $dz.files.length == 0
-    $($dz.element).css('border', '2px solid #f00')
+  if $($dz.element).attr('data-required') == 'true'
     field_uuid = $($dz.element).attr('data-field')
-    $('#dz_msg_'+field_uuid).html('This field is required')
-    $('#dz_msg_'+field_uuid).addClass('red')
-    fname = $($dz.element).attr('data-fieldname')
-    $($dz.element).closest('.col-sm-6').prepend(
-      '
Field «'+fname+'» is required.
'
-    )
-    return false
+    $files = get_files_for_field(field_uuid)
+    if $files.length == 0
+      $($dz.element).css('border', '2px solid #f00')
+      $('#dz_msg_'+field_uuid).html('This field is required')
+      $('#dz_msg_'+field_uuid).addClass('red')
+      fname = $($dz.element).attr('data-fieldname')
+      $($dz.element).closest('.col-sm-6').prepend(
+        '
Field «'+fname+'» is required.
'
+      )
+      return false
   return true

 dz_multiple = ($dz) ->


[Prabha/Deepthi] filename to contain modified name

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -131,12 +131,13 @@
     if result # Use the S3 response to set the URL to avoid character encodings bugs
       content.url            = $(result).find("Location").text()
       content.filepath       = $('<a />').attr('href', content.url)[0].pathname
+      content.filename       = $(result).find('Key').text().split("/").pop()
     else # IE <= 9 retu      rn a null result object so we use the file object instead
       domain                 = $uploadForm.attr('action')
       content.filepath       = $uploadForm.find('input[name=key]').val().replace('/${filename}', '')
       content.url            = domain + content.filepath + '/' + encodeURIComponent(file.name)
+      content.filename       = file.name

-    content.filename         = file.name
     content.filesize         = file.size if 'size' of file
     content.lastModifiedDate = file.lastModifiedDate if 'lastModifiedDate' of file
     content.filetype         = file.type if 'type' of file


Minor fix

Minor fix

--- index.cjsx
+++ index.cjsx
@@ -12,12 +12,11 @@
   {_ships, _decks, _teitokuLv} = window
   console.log path if process.env.DEBUG?
   switch path
-    when '/kcsapi/api_start2'
-      reportInitEquipByRemodel()
     when '/kcsapi/api_req_combined_battle/airbattle', '/kcsapi/api_req_combined_battle/battle', '/kcsapi/api_req_combined_battle/midnight_battle', '/kcsapi/api_req_combined_battle/sp_midnight', '/kcsapi/api_req_sortie/battle', '/kcsapi/api_req_battle_midnight/battle', '/kcsapi/api_req_battle_midnight/sp_midnight', '/kcsapi/api_req_sortie/airbattle', '/kcsapi/api_req_combined_battle/battle_water'
       reportEnemy(body)
     when '/kcsapi/api_get_member/ship_deck', '/kcsapi/api_port/port'
       reportShipAttr(path)
+      reportInitEquipByRemodel()
       cacheSync()
     when '/kcsapi/api_req_map/start'
       whenMapStart(_ships)


Set cache directory in tasks

Set cache directory in tasks

--- src/task.coffee
+++ src/task.coffee
@@ -71,7 +71,8 @@
     stackCachePath = require('coffeestack').getCacheDirectory()
     taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
     bootstrap = """
-      #{coffeeCacheRequire}.register('#{coffeeCachePath}');
+      #{coffeeCacheRequire}.setCacheDirectory('#{coffeeCachePath}');
+      #{coffeeCacheRequire}.register();
       #{coffeeStackRequire}.setCacheDirectory('#{stackCachePath}');
       #{taskBootstrapRequire}
     """


output clean urls if set in server object (#16)

--- lib/index.coffee
+++ lib/index.coffee
@@ -40,13 +40,15 @@
     directory = if config.directory then config.directory else type
     tpl = path.join(@roots.root, config.template)
     locals   = _.merge({}, @roots.config.locals, post: p)
-    output = "#{directory}/#{p.slug}.html"
+    extension = if typeof @roots.config.server == 'object' && @roots.config.server.clean_urls then '' else '.html'
+    outputFile = "#{directory}/#{p.slug}.html"
+    outputLink = "#{directory}/#{p.slug}#{extension}"
     compiler = _.find @roots.config.compilers, (c) ->
       _.contains(c.extensions, path.extname(tpl).substring(1))

     compiler.renderFile(tpl, _.cloneDeep(locals))
-      .then((res) => @util.write(output, res.result))
-      .yield(output)
+      .then((res) => @util.write(outputFile, res.result))
+      .yield(outputLink)

   .then (urls) -> { urls: urls, posts: posts }


code cleaning: environment variable in uppercase

--- lib/token.coffee
+++ lib/token.coffee
@@ -110,7 +110,7 @@
 ## @callback {function} Continuation to pass control back to when complete
 ## Initialize tokens and permissions for Home and Proxy
 initHomeProxy = (callback) ->
-    token = process.env.token
+    token = process.env.TOKEN
     token = token.split('\n')[0]
     # Add home token and permissions
     tokens['home'] = token


Fix point snippet prefix

Fix point snippet prefix

--- .atom/snippets/coffee.cson
+++ .atom/snippets/coffee.cson
@@ -40,5 +40,5 @@
     prefix: ":"
     body: '${1:"${2:key}"}: ${3:value}'
   "Create Jasmine spy":
-    prefix: "pt"
+    prefix: "spy"
     body: 'jasmine.createSpy("${1:description}")$2'


OPCE19: fixed many bugs concerning styles

--- lib/y-rte.coffee
+++ lib/y-rte.coffee
@@ -7,15 +7,16 @@
 # @param [Rte] rte an rte instance
 relativeFromAbsolute = (position, rte)->
     index = 0
-
     while position > 0
       if index >= rte._rte.words.length
-        return {word: index, pos: 0}
+        position = 0
+        break
       if rte._rte.words[index].word.length > position
-        return {word: index, pos: position}
+        break
       else
         position -= rte._rte.words[index].word.length
         index += 1
+
     return {word: index, pos: position}

 # Function that translates an index from start (absolute position) into a
@@ -105,7 +106,7 @@
       retStart = @_relativeFromAbsolute start
       retEnd = @_relativeFromAbsolute end

-      @setStyle style
+      @setStyle (style or {})

       @left = @rte.getWord retStart.word
       @leftPos = retStart.pos
@@ -190,19 +191,20 @@
     selection.in(@)

-  # Returns true if the given selection and this selection are contiguous
+  # Returns true if the given selection and this selection overlap
   # and this selection is at left of the given one
   #
   # @param [Selection] selection the selection to compare to this
   atLeftOf: (selection) ->
-    (@right == selection.left and
-     @rightPos == selection.leftPos)
-
-  #TODO
+     @gt(selection.left, selection.leftPos, "right") and
+     @lt(selection.right, selection.rightPos, "left")
+
+  # Set the style to style
+  #
+  # @param [Object] style A style represented as an object
   setStyle: (@style) ->

   # Validate a selection if the start is before the end of the selection
-  #
   isValid: ->
     @lt(@right, @rightPos, "left")

@@ -218,38 +220,70 @@
   #   [           right selection           ]
   merge: (selection) ->
     if @ == selection
+      return                    # nothing to do
+
+    # if they have two styles that differ
+    for key, val of @style
+      if selection.style[key] != val
+        return
+    for key, val of selection.style
+      if val != @style[key]
+        return
+
+    if @atLeftOf selection # remove @
+      selToRemove = @
+      selToKeep = selection
+      left = @left
+      leftPos = @leftPos
+      right = selection.right
+      rightPos = selection.rightPos
+    else if selection.atLeftOf @ # remove selection
+      selToRemove = selection
+      selToKeep = @
+      left = selection.left
+      leftPos = selection.leftPos
+      right = @right
+      rightPos = selection.rightPos
+    else if @.in selection # remove @
+      selToRemove = @
+      selToKeep = selection
+      left = selection.left
+      leftPos = selection.leftPos
+      right = selection.right
+      rightPos = selection.rightPos
+    else if selection.in @ # remove selection
+      selToRemove = selection
+      selToKeep = @
+      left = @left
+      leftPos = @leftPos
+      right = @right
+      rightPos = @rightPos
+    else
       return
-    if @style != selection.style
-      return
-    if @atLeftOf selection
-      leftSel = @
-      rightSel = selection
-    else if selection.atLeftOf @
-      leftSel = selection
-      rightSel = @
-    else
-      return
-
-    # unbind words from left selection, remove it from selection list
-    # expand the selection at right to the left end of previous left selection
-    rightSel.left.removeSel rightSel, "left"
-
-    rightSel.left = leftSel.left
-    rightSel.leftPos = leftSel.leftPos
-
-    leftSel.unbind()
-    @rte.removeSel leftSel
+
+    selToRemove.unbind()
+    selToKeep.unbind()
+
+    selToKeep.left = left
+    selToKeep.leftPos = leftPos
+    selToKeep.right = right
+    selToKeep.rightPos = rightPos
+
+    selToKeep.bind left, right
+
+    @rte.removeSel selToRemove

   # Unbind selection from word
   unbind: ->
-    @left.removeSel @, "left"
-    @right.removeSel @, "right"
+    if @left and @right
+      @left.removeSel @, "left"
+      @right.removeSel @, "right"

     @left = null
     @right = null

   # Bind selection to word
-  bind: ->
+  bind: (@left, @right) ->
     @left.left.push @
     @right.right.push @

@@ -266,7 +300,7 @@
     newSel.right = @right
     newSel.rightPos = @rightPos

-    newSel.bind()
+    newSel.bind @left, @right

     newSel

@@ -381,15 +415,15 @@

   # @overload deleteWords (start, end)
-  # Delete all the words between start and end
-  #
-  # @param [Integer] start position of first word to delete
-  # @param [Integer] end position of last word to delete
+  #   Delete all the words between start and end
+  #
+  #   @param [Integer] start position of first word to delete
+  #   @param [Integer] end position of last word to delete
   #
   # @overload deleteWords (position)
-  # Delete the word at position
-  #
-  # @param [Integer] position position the word to delete
+  #   Delete the word at position
+  #
+  #   @param [Integer] position position the word to delete
   deleteWords: (start, end) ->
     if _.isUndefined(end)
       end = start+1
@@ -399,7 +433,8 @@

   # Merge two words at position
   #
-  # @param [Integer] n position of word where to perform merge. The merge will be done with the word at right (if any)
+  # @param [Integer] n position of word where to perform merge. The merge will
+  # be done with the word at right (if any)
   #
   merge: (index) ->
     if 0 <= index < @_rte.words.length
@@ -416,7 +451,8 @@
   #
   deleteSel: (selection) ->
     if not selection.isValid()
-      throw new Error "Invalid selection"
+      console.log selection
+      throw new Error "Invalid selection, got", selection

     left = selection.left
     right = selection.right
@@ -521,17 +557,17 @@
     {word: word, pos: pos}

   # Set the style of the selection and try to extend as much
-  # as possible existing ones
-  #
+  # as possible existing ones.
   setStyle: (selection, style)->
     if not selection.isValid()
-      throw new Error "Invalid selection"
-
-    selection.style = style or ""
+      console.log selection
+      throw new Error "Invalid selection, got", selection
+
+    selection.style = style or {}

     # Link the boundary words to selection
-    leftWord = selection.left
-    rightWord = selection.right
+    leftWord = selection.left or {right: []}
+    rightWord = selection.right or {left: []}

     # Merge left…
     for tmpSelection in leftWord.right when tmpSelection
@@ -557,15 +593,40 @@
         @deleteSel selection

       else if delta.insert?
+        console.log position, (position+delta.insert.length)
         @insert position, delta.insert
+        if delta.attributes?
+          selection = new Selection position, (position + delta.insert.length-1), @
         position += delta.insert.length

       else
         throw new Error "Unknown operation"

       if delta.attributes?
-        for attr in delta.attributes
-          @setAttr(selection.clone().style = attr)
+        @setStyle selection, delta.attributes
+
+      selectionList = @getSelections()
+      console.log selectionList
+      for sel in selectionList
+        sel.merge selection
+
+  # Return all the selections bound to left and right, except self
+  # @return [Array<Selection>] an array of selection
+  getSelections: ->
+    uniq = (value, index, self) ->
+      self.indexOf(value) == index and value != @
+
+    ret = []
+    if @left and @left.left
+      ret.append(@left.left)
+    if @left and @left.right
+      ret.append(@left.left)
+    if @right and @right.right
+      ret.append(@right.right)
+    if @right and @right.left
+      ret.append(@right.left)
+
+    ret.filter uniq # return a value with unique elements

   # Remove a selection from selection list
   #
@@ -579,7 +640,7 @@
         break

     # unbind selection
-    selection.unbind
+    selection.unbind()

 if window?
   window.Rte = Rte


Boyscouting: more concise syntax.

Boyscouting: more concise syntax.

--- core/app/backbone/factlink/base_controller.coffee
+++ core/app/backbone/factlink/base_controller.coffee
@@ -19,7 +19,7 @@
   getRouteFunction: (name) ->
     (args...) ->
       @openController()
-      @onAction() if @onAction?
+      @onAction?()
       @[name](args...)

   openController: ->


Check eventBus has handler registered, same as commandBus

--- source/server/infrastructure/commit_publisher.coffee
+++ source/server/infrastructure/commit_publisher.coffee
@@ -99,7 +99,7 @@
     commands = []
     # Only parse events that can be handled by this app
     for event in commit.changes.events
-      if @_supportsEjsonType event.type
+      if @_supportsEjsonType(event.type) and @eventBus.hasHandlerFor(event.type)
         EventType = Space.resolvePath(event.type)
         events.push EventType.fromData(event.data)
     # Only parse commands that can be handled by this app


Code cleanup

Code cleanup

--- test/basic.coffee
+++ test/basic.coffee
@@ -13,9 +13,10 @@
 describe 'Bootstrap tests', ->
   it "responds to --help", (done) ->
     exec("#{binubs} --help").then (p) ->
+      stdout = p.stdout.toString()
       expect(p.stderr.toString()).to.be ''
-      expect(p.stdout.toString()).to.match /usage:/
-      expect(p.stdout.toString()).to.match /options:/
+      expect(stdout).to.match /usage:/
+      expect(stdout).to.match /options:/
       done()
     , done

@@ -28,17 +29,19 @@

   it 'should be able to output log messages', (done) ->
     exec("#{binubs} -b test/helloworld.yml test").then (p) ->
+      stdout = p.stdout.toString()
       expect(p.stderr.toString()).to.be 'WARN: warning level\nERROR: error level\n'
-      expect(p.stdout.toString()).to.match /Hello World!/
-      expect(p.stdout.toString()).to.match /Done\./
-      expect(p.stdout.toString()).to.not.match /Bam!/
+      expect(stdout).to.match /Hello World!/
+      expect(stdout).to.match /Done\./
+      expect(stdout).to.not.match /Bam!/
       done()
     , done

   it 'should be able to get and set environment variables', (done) ->
     exec("#{binubs} -b test/test_env.yml test").then (p) ->
-      expect(p.stdout.toString()).to.match /composite\: still works!/
-      expect(p.stdout.toString()).to.match /Done\./
+      stdout = p.stdout.toString()
+      expect(stdout).to.match /composite\: still works!/
+      expect(stdout).to.match /Done\./
       done()
     , done

@@ -61,16 +64,18 @@
   describe 'Package JSON', ->
     it 'should be able to output version from packagejson', (done, error) ->
       exec("#{binubs} -b test/test_plugin_packagejson.yml test").then (p) ->
-        expect(p.stdout.toString()).to.match /version [0-9]+\.[0-9]+/
-        expect(p.stdout.toString()).to.match /Done\./
+        stdout = p.stdout.toString()
+        expect(stdout).to.match /version [0-9]+\.[0-9]+/
+        expect(stdout).to.match /Done\./
         done()
       , error

   describe 'Grab', ->
     it 'should be able to retrieve this package\'s master zip file', (done, error) ->
       exec("#{binubs} -b test/test_plugin_grab.yml test").then (p) ->
-        expect(p.stdout.toString()).to.match /Retrieving/
-        expect(p.stdout.toString()).to.match /Done\./
+        stdout = p.stdout.toString()
+        expect(stdout).to.match /Retrieving/
+        expect(stdout).to.match /Done\./
         expect(fs.existsSync 'test/master.zip').to.be.ok()
         done()
       , error


Try and fix inbox bug.

Try and fix inbox bug.

--- source/scripts/controllers/inbox.coffee
+++ source/scripts/controllers/inbox.coffee
@@ -4,6 +4,10 @@
 User = require '../models/user'

 setupInbox = ->
+
+  List.forEach (list) ->
+    if list.name is 'Inbox'
+      User.inbox = list.id

   return if List.exists User.inbox


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -1,7 +1,7 @@
 test "Color default color is black", ->
   color = Color()
-  equals(color.channels.r, 0, "default red channel is 0")
-  equals(color.channels.g, 0, "default green channel is 0")
-  equals(color.channels.b, 0, "default blue channel is 0")
-  equals(color.channels.a, 0, "default alpha channel is 0"
+  equals color.channels.r, 0, "default red channel is 0"
+  equals color.channels.g, 0, "default green channel is 0"
+  equals color.channels.b, 0, "default blue channel is 0"
+  equals color.channels.a, 0, "default alpha channel is 0"


test fix

test fix

--- test/blog.coffee
+++ test/blog.coffee
@@ -29,21 +29,19 @@
 		id = ''
 		beforeEach (done)->
 			promise = blog.createPost
-				title	: 	expected,
+				title	: 	expected
 				author 	:	'Mehfuz Hossain'
 				body	:	'Empty body'
 				publish :	false
 			promise.then (result) =>
 				id = result._id
-				done()
-
+				done()
 		it 'should skip draft posts', (done)->
 	 		promise= blog.find ''
 	 		promise.then (data) ->
 	 			for post in data.posts
 	 				post._id.should.not.equal id
 				done()
-
 		afterEach (done)->
 			blog.deletePost id, ()->
 				done()


previous fix broke tests

previous fix broke tests

--- test/index.coffee
+++ test/index.coffee
@@ -5,4 +5,4 @@
 index = require '../'

-describe 'index'
+describe 'index', () -> undefined


Allow 'only: true' in specs.

Allow 'only: true' in specs.

--- test/specs.coffee
+++ test/specs.coffee
@@ -26,9 +26,12 @@
         data = fs.readFileSync(spec, 'utf-8')
         [meta, input, output] = data.split('----\n')
         meta = if meta.length
-          coffee.compile(meta, bare: true)
+          eval coffee.compile(meta, bare: true)
         else
           {}
+
+        # Put 'only: true' on top of the file to temporarily isolate it
+        test = it.only if meta.only

         test name, do (spec, input, output) ->
           ->


Drop område collection before each test

Drop område collection before each test

--- test/suite.coffee
+++ test/suite.coffee
@@ -36,6 +36,7 @@
 beforeEach (done) ->
   redis.flushall()

+  mongo.områder.drop()
   mongo.steder.drop()
   mongo.turer.drop()


add event rpc test

add event rpc test

--- test/serf.coffee
+++ test/serf.coffee
@@ -6,14 +6,15 @@

 describe 'basic test', ->
   procs = []
-  client = undefined
+  clients = {}

   before (done) ->
     procs.push(spawn 'serf', ['agent', '-node=agent-one', '-bind=127.0.0.1:7946'])
     procs.push(spawn 'serf', ['agent', '-node=agent-two', '-bind=127.0.0.1:7947', '-rpc-addr=127.0.0.1:7374'])

     setTimeout ->
-      client = Serf.connect {port: 7373}, done
+      clients.one = Serf.connect {port: 7373}, ->
+        clients.two = Serf.connect {port: 7374}, done
     , 500

   after () ->
@@ -21,22 +22,31 @@
       do proc.kill

   it 'should define camel-casing methods', ->
-    assert.equal client['force-leave'], client.forceLeave
+    assert.equal clients.one['force-leave'], clients.one.forceLeave

   it 'should stats', (done) ->
-    client.stats {}, (result) ->
+    clients.one.stats {}, (result) ->
       assert.equal 'agent-one', result.agent.name
       done if result.Error is '' then undefined else result.Error

   it 'should join', (done) ->
-    client.join {Existing: ['127.0.0.1:7947'], Replay: false}, (result) ->
+    clients.one.join {Existing: ['127.0.0.1:7947'], Replay: false}, (result) ->
       done if result.Error is '' then undefined else result.Error

   it 'should members', (done) ->
-    client.members (result) ->
+    clients.one.members (result) ->
       assert.isArray result.Members
       assert.deepPropertyVal result, 'Members.length', 2
       do done

+  it 'should event', (done) ->
+    clients.two.stream {Type: 'user:foo'}, (data) ->
+      assert.equal 'user', data.Event
+      assert.equal 'foo', data.Name
+      assert.equal 'test payload', data.Payload.toString()
+      do done
+    clients.one.event {Name: 'foo', Payload: 'test payload', Coalesce: true}
+
+
   it 'should leave', ->
-    client.leave()
+    do clients.one.leave


Improve rename test

Improve rename test

--- test/tail.coffee
+++ test/tail.coffee
@@ -183,3 +183,5 @@
       fs.closeSync fdNew

     setTimeout writeMore, 1500
+    after ->
+      fs.unlinkSync newName


test: Add another test case for formatDate

--- test/test.coffee
+++ test/test.coffee
@@ -42,6 +42,8 @@
     it 'should return an expected result', ->
       assert.strictEqual bitterServer.formatDate('2013', '06', '09'),
         'June 9, 2013'
+      assert.strictEqual bitterServer.formatDate('2020', '12', '31'),
+        'December 31, 2020'

 describe 'server', ->
   before (done) ->


Added a getAcceptedFiles() test.

Added a getAcceptedFiles() test.

--- test/test.coffee
+++ test/test.coffee
@@ -714,6 +714,27 @@
         fileInput.name.should.equal "myFile[]"

+    describe "getAcceptedFiles()", ->
+      it "should only return accepted files", ->
+        mock1 = getMockFile()
+        mock2 = getMockFile()
+        mock3 = getMockFile()
+        mock4 = getMockFile()
+        dropzone.options.accept = (file, done) ->
+          if file in [ mock1, mock3 ]
+            done()
+          else
+            done "error"
+
+        dropzone.addFile mock1
+        dropzone.addFile mock2
+        dropzone.addFile mock3
+        dropzone.addFile mock4
+
+        files = dropzone.getAcceptedFiles()
+        files.should.eql [ mock1, mock3 ]
+
+
   describe "file handling", ->
     mockFile = null
     dropzone = null


clean up err conditional

clean up err conditional

--- test_meta.coffee
+++ test_meta.coffee
@@ -16,12 +16,11 @@
     select: 'Name CreateDate Owners Estimate'
     callback: (err, transaction) ->
         if err?
-            console.log "Query failed."
-        else
-            transaction.iter (story) ->
-                story.Name = 'Something Else'
-                issue = Story.create_in_context('Issue', {Name: 'New Issue'})
-            transaction.commit and_log_it
+            return console.log("query failed\n" + util.inspect(err))
+        transaction.iter (story) ->
+            story.Name = 'Something Else'
+            issue = Story.create_in_context('Issue', {Name: 'New Issue'})
+        transaction.commit and_log_it

 v1.with_poller (poller) ->


Improve doc on index.coffee, adapt to the last version of express, remove the 404 middleware which blocks rendering. Use http.server to create the server. Add callback on both application launch and mongo connection.

--- express/index.coffee
+++ express/index.coffee
@@ -1,6 +1,18 @@
+'use strict'
+###
+    Module dependencies.
+###
 express = require 'express'
+http = require('http')
 {join} = require 'path'
 {config} = require './config'
+
+app = express()
+server = http.createServer(app)
+app.set('port', process.env.PORT || 4000)
+###
+    Require routes.
+###
 routes = require './routes'
 backbone = require './routes/index'
 collector = require './routes/collector'
@@ -8,14 +20,8 @@
 room = require './routes/room'
 login = require './routes/login'

-# Init mongo connection only once
-mongo = require './mongo'
-mongo.initiate (db) ->
-console.log('Connection to mongo')

-
-app = express()
-# Allow Cross Origin
+# Allow Cross Origin middleware
 app.use '/', (req, res, next) ->
     res.header("Access-Control-Allow-Origin", "*")
     res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
@@ -24,6 +30,10 @@
     # TODO Non automatic content type
     next()

+###
+    Define all the middleware use by the application.
+    The order in which they are defined is important.
+###
 loginMiddleware = (req, res, next) ->
     # Login middleware
     # add loginMiddleware as second argument for
@@ -50,7 +60,21 @@
                 else
                     res.send 401
             )
+### Default 404 middleware ###
+#app.use routes.error('Page not found :(', 404)

+#Application name.
+app.locals.title = "Home Again API"
+
+
+# Init mongo connection only once
+require('./models/connection').initiate (db) ->
+    console.log('Connection is now established with mongoDB on homeAgain.')
+
+
+###
+    Configuration of the application.
+###
 app.configure 'production', ->
     app.use express.limit '5mb'

@@ -74,26 +98,26 @@
     app.locals.pretty = true

-
+###
+    Actions
+###
 app.all(/^\/admin.*$/, backbone.index)
 app.post '/collector/collect', loginMiddleware, collector.collect
+###Sensor actions###
 app.get '/sensor/:sensor_id', loginMiddleware, sensor.get
 app.get '/sensor', loginMiddleware, sensor.all
 app.put('/sensor/:sensor_id', loginMiddleware, sensor.put)

-
+###Room actions###
 app.get '/room/:room_id', loginMiddleware, room.get
 app.post '/room/', loginMiddleware, room.post
 app.get '/room', loginMiddleware, room.all

 app.post '/login', login.post
-
 app.get '/test', routes.test('Mocha Tests')

-
-### Default 404 middleware ###
-app.use routes.error('Page not found :(', 404)
+# Server launching
+server.listen app.get('port'), ()->
+    console.log('Express server listening on port', app.get('port'))

 module.exports = exports = app
-
-app.listen 4000


:dancer: Context menu shuffle :dancer:

:dancer: Context menu shuffle :dancer:

--- menus/git.cson
+++ menus/git.cson
@@ -40,23 +40,32 @@
   ]
   '.github-FilePatchView': [
     {
+      'type': 'separator'
+    }
+    {
       'label': 'Open File'
       'command': 'github:open-file'
+      'after': ['core:confirm']
     }
   ]
   '.github-FilePatchView--staged': [
     {
+      'type': 'separator'
+    }
+    {
       'label': 'Unstage Selection'
       'command': 'core:confirm'
+      'beforeGroupContaining': ['core:undo']
     }
   ]
   '.github-FilePatchView--unstaged': [
     {
+      'type': 'separator'
+    }
+    {
       'label': 'Stage Selection'
       'command': 'core:confirm'
-    }
-    {
-      'type': 'separator'
+      'beforeGroupContaining': ['core:undo']
     }
     {
       'label': 'Discard Selection'


Team progression graph draft.

Team progression graph draft.

--- picoCTF-web/web/coffee/progression_graphs.coffee
+++ picoCTF-web/web/coffee/progression_graphs.coffee
@@ -7,6 +7,12 @@
   title: "Team Score Progression",
   legend: {
     position: "none"
+  },
+  vAxis: {
+    title: "Score"
+  },
+  hAxis: {
+    ticks: []
   },
   pointSize: 3
 }
@@ -29,10 +35,11 @@

   return _.extend continuousBucket, buckets

-maxValuesFromBuckets = (buckets, sampleKey) ->
+maxValuesFromBucketsExtended = (buckets, sampleKey) ->
   maxValues = []

   lastInsertedValue = 0
+
   _.each buckets, (samples) ->
     values = _.pluck(samples, sampleKey)

@@ -45,21 +52,33 @@

   return maxValues

-@drawTeamProgressionGraph = (selector) ->
+progressionDataToPoints = (data, bucketWindow, dataPoints) ->
+      buckets = timestampsToBuckets data, "time", bucketWindow
+      steps = maxValuesFromBucketsExtended buckets, "score"
+
+      if steps.length < dataPoints
+        return steps
+
+      return _.rest(steps, steps.length - dataPoints)
+
+@drawTeamProgressionGraph = (selector, points) ->
   div = divFromSelector selector
   apiCall "GET", "/api/stats/team/score_progression", {}
   .done (data) ->
-    graphData = [["Time", "Score"]]
-
-    lastSubmission = null
-    _.each data.data, (submission) ->
-      if lastSubmission
-        graphData.push [submission.time - 1, lastSubmission.score]
-      graphData.push [submission.time, submission.score]
-      lastSubmission = submission
+    if data.data.length > 0
+
+      graphData = [
+        ["Time", "Score", {role: "tooltip"}]
+      ]

-    packagedData = google.visualization.arrayToDataTable graphData
+      steps = progressionDataToPoints data.data, 600, 30
+
+      (graphData.push(["", score, score]) for score in steps)

-    chart = new google.visualization.LineChart(div)
-    chart.draw(packagedData, teamGraphOptions)
+      console.log graphData

+      packagedData = google.visualization.arrayToDataTable graphData
+
+      chart = new google.visualization.SteppedAreaChart(div)
+      chart.draw(packagedData, teamGraphOptions)
+


Tiny fix

Tiny fix

--- theme/twi/src/frontend/view/error/http/NotFound.cjsx
+++ theme/twi/src/frontend/view/error/http/NotFound.cjsx
@@ -15,6 +15,8 @@

 mapStoresToProps = ({app}) -> ({app})

+
+# It's maaaaaaaagic, Alex :D (actually no)
 componentWillMount = -> @props.app.setTitle "Page not found"

 NotFound = ({app}) ->


Remove debug code

Remove debug code

--- src/util.coffee
+++ src/util.coffee
@@ -58,8 +58,6 @@
 #
 # Returns an Array of the text nodes.
 Util.getTextNodes = (node) ->
-  if not node.ownerDocument?
-    debugger
   document = node.ownerDocument
   walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false)
   nodes = (next while next = walker.nextNode())


Finish view cleanup

Finish view cleanup

--- src/view.coffee
+++ src/view.coffee
@@ -1279,7 +1279,7 @@
         return @bounds[line]

       # ## computeOwnPath
-      # Again exception: sockets containing block
+      # Again, exception: sockets containing block
       # should mimic blocks exactly.
       #
       # Under normal circumstances this shouldn't
@@ -1289,14 +1289,12 @@
       # and should hit-test properly.
       computeOwnPath: ->
         # Use cache if possible.
-        ###
         if @versions.path is @model.version and
            not @boundingBoxFlag
           return @path

         # Update our version number.
         @versions.path = @model.version
-        ###

         if @model.start.next.type is 'blockStart'
           view = @self.getViewFor @model.start.next.container
@@ -1332,7 +1330,15 @@
     # # IndentView
     class IndentView extends ContainerView
       constructor: -> super; @padding = 0
+
+      # ## computeOwnPath
+      # An Indent should also have no drawn
+      # or hit-tested path.
       computeOwnPath: -> @path = new draw.Path()
+
+      # ## computeDimensions
+      # An Indent should put some
+      # height in for empty lines.
       computeDimensions: ->
         super

@@ -1343,12 +1349,18 @@

         return @dimensions

+      # ## drawSelf
+      #
+      # Again, an Indent should draw nothing.
       drawSelf: -> null
+
+      # ## computeOwnDropArea
+      #
+      # Our drop area is a rectangle of
+      # height dropAreaHeight and a width
+      # equal to our last line width,
+      # positioned at the bottom of our last line.
       computeOwnDropArea: ->
-        # Our drop area is a rectangle of
-        # height dropAreaHeight and a width
-        # equal to our last line width,
-        # positioned at the bottom of our last line.
         @dropArea = new draw.Rectangle(
           @bounds[1].x,
           @bounds[1].y - @self.opts.dropAreaHeight / 2,
@@ -1370,9 +1382,20 @@
         @highlightArea.style.fillColor = '#fff'

     # # SegmentView
+    # Represents a Segment. Draws little, but
+    # recurses.
     class SegmentView extends ContainerView
       constructor: -> super; @padding = 0
+
+      # ## computeOwnPath
+      #
       computeOwnPath: -> @path = new draw.Path()
+
+      # ## computeOwnDropArea
+      #
+      # Lasso segments are not droppable;
+      # root segments of documents
+      # can be dropped at their beginning.
       computeOwnDropArea: ->
         if @model.isLassoSegment
           return @dropArea = null
@@ -1395,19 +1418,45 @@
           @highlightArea.style.strokeColor = '#fff'

           return null
+
+      # ## drawSelf
+      #
+      # A Segment has no drawn representation.
       drawSelf: (ctx, style) -> null
+
+      # ## draw
+      #
+      # A Segment can be a lasso select
+      # container, in which case it
+      # should set the "selected" style
+      # on all of its children during
+      # the draw phase.
       draw: (ctx, boundingRect, style) ->
         if @model.isLassoSegment then style.selected++
         super
         if @model.isLassoSegment then style.selected--
-
+
+    # # TextView
+    #
+    # TextView does not extend ContainerView.
+    # We contain a draw.TextElement to measure
+    # bounding boxes and draw text.
     class TextView extends GenericView
       constructor: (@model, @self) -> super
-
+
+      # ## computeChildren
+      #
+      # Text elements are one line
+      # and contain no children (and thus
+      # no multiline children, either)
       computeChildren: ->
-        @multilineChildrenData = [0]
+        @multilineChildrenData = [NO_MULTILINE]
         return 1

+      # ## computeDimensinos
+      #
+      # Set our dimensions to the measured dimensinos
+      # of our text value.
       computeDimensions: ->
         if @versions.dimensions is @model.version
           return @dimensions
@@ -1426,18 +1475,27 @@

         return @dimensions

-      computeBoundingBoxX: (left, line) ->
-        @textElement.point.x = left
+      # ## computeOwnPath
+      #
+      # Position the TextElement we point to
+      # to match our bounding box position.
+      computeOwnPath: ->
+        @textElement.point.x = @bounds[0].x
+        @textElement.point.y = @bounds[0].y
         super
-
-      computeBoundingBoxY: (top, line) ->
-        @textElement.point.y = top
-        super
-
+
+      # ## drawSelf
+      #
+      # Draw the text element itself.
       drawSelf: (ctx, style) ->
         @textElement.draw ctx
         return null

+      # ## Debug output
+
+      # ### debugDimensions
+      #
+      # Draw the text element wherever we're told.
       debugDimensions: (x, y, line, ctx) ->
         ctx.globalAlpha = 1
         oldPoint = @textElement.point
@@ -1445,9 +1503,12 @@
         @textElement.draw ctx
         @textElement.point = oldPoint
         ctx.globalAlpha = 0.1
-
+
+      # ### debugAllBoundingBoxes
+      # Draw our text element
       debugAllBoundingBoxes: (ctx) ->
         ctx.globalAlpha = 1
+        @computeOwnPath()
         @textElement.draw ctx
         ctx.globalAlpha = 0.1


Update the name of the temporary stored procedure created by the extendedErrorInfo test.

--- test/integration/errors-test.coffee
+++ test/integration/errors-test.coffee
@@ -62,7 +62,7 @@

   test.expect(9)

-  execProc = new Request "#divideByZero", (err) ->
+  execProc = new Request "#testExtendedErrorInfo", (err) ->
     test.ok(err instanceof Error)

     test.strictEqual(err.number, 50000)
@@ -73,13 +73,13 @@

     # The procedure name will actually be padded to 128 chars with underscores and
     # some random hexadecimal digits.
-    test.ok(err.procName?.indexOf("#divideByZero") == 0,
-      "err.procName should begin with #divideByZero, was actually #{err.procName}")
+    test.ok(err.procName?.indexOf("#testExtendedErrorInfo") == 0,
+      "err.procName should begin with #testExtendedErrorInfo, was actually #{err.procName}")
     test.strictEqual(err.lineNumber, 1, "err.lineNumber should be 1")

     connection.close()

-  createProc = new Request "create procedure #divideByZero as raiserror('test error message', 14, 42)", (err) ->
+  createProc = new Request "create procedure #testExtendedErrorInfo as raiserror('test error message', 14, 42)", (err) ->
     test.ifError(err)
     connection.callProcedure execProc


newdataset documentation test

newdataset documentation test

--- test/integration/new_dataset.coffee
+++ test/integration/new_dataset.coffee
@@ -7,6 +7,7 @@

 describe 'New dataset tool', ->
   browser = new Browser()
+  browser.waitDuration = "10s"

   before (done) ->
     browser.visit login_url, done
@@ -26,10 +27,8 @@
       result = browser.location.href
       result.should.include "#{url}/dataset/"

-    it 'shows me details of how to ssh in to my box', (done) ->
-      browser.wait ->
-        iframe = browser.query('iframe')
-        text = $(iframe).contents().find('body').html()
-        console.log text
-        text.should.include 'ssh in via blah'
-        done()
+    it 'shows me details of how to ssh in to my box', ->
+      iframe = browser.query('iframe')
+      text = $(iframe).contents().find('body').text()
+      text.should.include 'Add your SSH key'
+      text.should.include 'ickletest.'


fix Flow service completion fetch to not iterate through null variables

--- static/coffee/flows/services.coffee
+++ static/coffee/flows/services.coffee
@@ -924,9 +924,10 @@

         # update our auto completion options
         $http.get('/flow/completion/?flow=' + flowId).success (data) ->
-          Flow.completions = data.message_completions
-          Flow.function_completions = data.function_completions
-          Flow.variables_and_functions = [Flow.completions...,Flow.function_completions...]
+          if data.function_completions and data.message_completions
+            Flow.completions = data.message_completions
+            Flow.function_completions = data.function_completions
+            Flow.variables_and_functions = [Flow.completions...,Flow.function_completions...]

         $http.get('/contactfield/json/').success (fields) ->
           Flow.contactFields = fields


fixed tests

fixed tests

--- test/integration/statsDTests.coffee
+++ test/integration/statsDTests.coffee
@@ -65,7 +65,7 @@

       `it("should fetch global load Time metrics", function (done) {
         mock = sinon.mock(Statsd);
-        mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.timers." + domain + ".Channels.mean,'1hour'))&from=-1days&format=json")
+        mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.timers." + domain + ".channels.mean,'1hour'))&from=-1days&format=json")
         co(function* () {
           yield Statsd.retrieveAverageLoadTimePerHour();
         });
@@ -76,7 +76,7 @@

       `it("should fetch global status metrics ", function (done) {
         mock = sinon.mock(Statsd);
-        mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.counters." + domain + ".Channels.jjhreujiwh.Statuses.Processing.count,'1week'))&from=-1days&format=json");
+        mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.counters." + domain + ".channels.jjhreujiwh.statuses.Processing.count,'1week'))&from=-1days&format=json");
         co(function* () {
            yield Statsd.fetcGlobalStatusMetrics(['jjhreujiwh']);
         });
@@ -87,7 +87,7 @@

       `it("should fetch channel transaction count metrics ", function (done){
           mock = sinon.mock(Statsd);
-          mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.counters." + domain + ".Channels.jjhreujiwh.count,'1day'))&from=-7days&format=json&target=transformNull(summarize(stats.timers." + domain + ".Channels.jjhreujiwh.sum,'1day','avg'))");
+          mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.counters." + domain + ".channels.jjhreujiwh.count,'1day'))&from=-7days&format=json&target=transformNull(summarize(stats.timers." + domain + ".channels.jjhreujiwh.sum,'1day','avg'))");
           co(function* () {
             yield Statsd.retrieveChannelMetrics('count','jjhreujiwh');
           });
@@ -98,7 +98,7 @@

       `it("should fetch channel status metrics ", function (done) {
           mock = sinon.mock(Statsd);
-          mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.counters." + domain + ".Channels.jjhreujiwh.Statuses.Processing.count,'1week'))&from=-1weeks&format=json");
+          mock.expects('fetchData').once().withExactArgs("/render?target=transformNull(summarize(stats.counters." + domain + ".channels.jjhreujiwh.statuses.Processing.count,'1week'))&from=-1weeks&format=json");
           co(function* () {
              yield Statsd.retrieveChannelMetrics('status','jjhreujiwh');
           });


Add test coverage for the success and fail options

--- test/javascripts/remote_test.coffee
+++ test/javascripts/remote_test.coffee
@@ -67,6 +67,53 @@
       assert.equal "/foo/bar", request.url
       assert.equal "POST", request.method

+  describe 'callbacks', ->
+
+    it 'will call options.fail() on HTTP failures', (done) ->
+      server = sinon.fakeServer.create()
+      server.respondWith("POST", "/foo/bar",
+            [422, { "Content-Type": "text/html" },
+             '
Error occured
'])
+      remote = new TurboGraft.Remote
+        httpRequestType: "POST"
+        httpUrl: "/foo/bar"
+        refreshOnError: "foo"
+        fail: done
+      , @initiating_target
+      remote.submit()
+
+      server.respond()
+
+    it 'will call options.fail() on XHR failures', (done) ->
+      xhr = sinon.useFakeXMLHttpRequest()
+
+      remote = new TurboGraft.Remote
+        httpRequestType: "POST"
+        httpUrl: "/foo/bar"
+        refreshOnError: "foo"
+        fail: done
+      , @initiating_target
+
+      simulateError = ->
+        listener(target: remote.xhr) for listener in remote.xhr.eventListeners.error
+
+      setTimeout(simulateError, 0)
+
+    it 'will call options.success() on success', (done) ->
+      server = sinon.fakeServer.create()
+      server.respondWith("POST", "/foo/bar",
+            [200, { "Content-Type": "text/html" },
+             '
Hey there
'])
+      remote = new TurboGraft.Remote
+        httpRequestType: "POST"
+        httpUrl: "/foo/bar"
+        refreshOnError: "foo"
+        success: done
+      , @initiating_target
+      remote.submit()
+
+      server.respond()
+
   describe 'TurboGraft events', ->

     beforeEach ->
@@ -74,7 +121,6 @@

     afterEach ->
       @refreshStub.restore()
-

     it 'allows turbograft:remote:init to set a header', ->
       $(@initiating_target).one "turbograft:remote:init", (event) ->


[improvement] Allowed tags to include a '?' (2m)

--- app/assets/javascripts/houston/feedback/views/comments_view.coffee
+++ app/assets/javascripts/houston/feedback/views/comments_view.coffee
@@ -265,7 +265,7 @@

   parseTags: (text)->
     tags = _.map text.split(/[,;]/), (tag)->
-      tag.compact().toLowerCase().replace(/\W+/g, '-')
+      tag.compact().toLowerCase().replace(/[^\w\?]+/g, '-')
     _.reject tags, (tag)-> !tag

   promptToImportCsv: (data)->


Continuing with tests over the history directive. Add more test cases

--- test/unit/directives/history.coffee
+++ test/unit/directives/history.coffee
@@ -61,6 +61,16 @@
     }

+    userStoryOnlyCommentHistorical = [
+        {
+            id: 1277,
+            content_type: "userstory",
+            created_date: "2014-03-14T21:24:33.099Z",
+            user: 1,
+            comment: "",
+            changed_fields: {}
+        }
+    ]
     userStoryHistorical = [
         {
             id: 1277,
@@ -456,10 +466,29 @@
         element = angular.element(template)
         $compile = _$compile_
         $rootScope = _$rootScope_
-
-
         $rootScope.constants = constants
     ))
+
+    it "should allow to draw an empty historical",  inject ($model)->
+        $rootScope.testHistorical = {models : []}
+        $rootScope.testType = ""
+
+        element = $compile(element)($rootScope)
+        $rootScope.$digest()
+
+        #TODO ...
+
+    it "should allow to draw a user story historical 2",  inject ($model)->
+        $rootScope.testHistorical = {
+            models : _.map(userStoryOnlyCommentHistorical, (item) =>
+                $model.make_model("userstory-history", item))
+        }
+        $rootScope.testType = "userstory"
+
+        element = $compile(element)($rootScope)
+        $rootScope.$digest()
+
+        #TODO ...

     it "should allow to draw a user story historical",  inject ($model)->
         $rootScope.testHistorical = {


fix reading user.receivers undefined bug

--- app/scripts/controllers/reporteditor.coffee
+++ app/scripts/controllers/reporteditor.coffee
@@ -3,7 +3,10 @@

     if not id
       currentUser = Auth.currentUser()
-      currentUser.$promise.then (user) ->
+      currentUser.$promise.then (user) ->
+        # 新用户没有receivers字段
+        user.receivers.to = {} if not user.receivers?.to
+        user.receivers.cc = {} if not user.receivers?.cc
         $scope.report.to = user.receivers.to
         $scope.report.cc = user.receivers.cc
         return


Provide keyboard navigation for base windows menus

--- menus/win32.cson
+++ menus/win32.cson
@@ -1,6 +1,6 @@
 'menu': [
   {
-    label: 'File'
+    label: '&File'
     submenu: [
       { label: 'New Window', command: 'application:new-window' }
       { label: 'New File', command: 'application:new-file' }
@@ -22,7 +22,7 @@
   }

   {
-    label: 'Edit'
+    label: '&Edit'
     submenu: [
       { label: 'Undo', command: 'core:undo' }
       { label: 'Redo', command: 'core:redo' }
@@ -83,7 +83,7 @@
   }

   {
-    label: 'View'
+    label: '&View'
     submenu: [
       { label: 'Reload', command: 'window:reload' }
       { label: 'Toggle Full Screen', command: 'window:toggle-full-screen' }
@@ -102,7 +102,7 @@
   }

   {
-    label: 'Selection'
+    label: '&Selection'
     submenu: [
       { label: 'Add Selection Above', command: 'editor:add-selection-above' }
       { label: 'Add Selection Below', command: 'editor:add-selection-below' }
@@ -121,7 +121,7 @@
   }

   {
-    label: 'Movement'
+    label: '&Movement'
     submenu: [
       { label: 'Move to Top', command: 'core:move-to-top' }
       { label: 'Move to Bottom', command: 'core:move-to-bottom' }
@@ -138,17 +138,17 @@
   }

   {
-    label: 'Find'
+    label: 'F&ind'
     submenu: []
   }

   {
-    label: 'Packages'
+    label: '&Packages'
     submenu: []
   }

   {
-    label: 'Window'
+    label: '&Window'
     submenu: [
       { label: 'Minimize', command: 'application:minimize' }
       { label: 'Zoom', command: 'application:zoom' }
@@ -158,7 +158,7 @@
   }

   {
-    label: 'Help'
+    label: '&Help'
     submenu: [
       { label: 'About Atom', command: 'application:about' }
       { label: "VERSION", enabled: false }


remove console.log

remove console.log

--- test/run.coffee
+++ test/run.coffee
@@ -7,10 +7,8 @@
       ids: ['a', 'b', 'c']
       graph: new Graph
       callback: (id) ->
-        console.log id
         id
     ).then (result) ->
-      console.log result
       test.done()

   'values from parents are passed to children': (test) ->


display message contents

display message contents

--- static/coffee/models.coffee
+++ static/coffee/models.coffee
@@ -80,6 +80,34 @@
         })
         @loadMessages()

+
+    # This contains more than 1 element. It doesn't fit into a ReactJS class.
+    renderMessage: (uid, subject, sender, date, contents) -> [
+        React.DOM.tr(
+            null
+            React.DOM.td(null, React.DOM.a({
+                href: FM.makeUrl(@safeID, uid)
+            }, subject))
+            React.DOM.td(null, sender)
+            React.DOM.td(null, date)
+        )
+        React.DOM.tr(
+            null
+            React.DOM.td(
+                {colSpan: 3}
+                React.DOM.dl({className: 'dl-horizontal'}
+                    React.DOM.dt(null, 'Subject')
+                    React.DOM.dd(null, subject)
+                    React.DOM.dt(null, 'From')
+                    React.DOM.dd(null, sender)
+                    React.DOM.dt(null, 'Date')
+                    React.DOM.dd(null, date)
+                )
+                React.DOM.pre(null, contents[0])
+            )
+        )
+    ]
+
     render: ->

         badge = if @state.messageCount
@@ -94,30 +122,30 @@
         })

         messageList = if @state.messages
-            messageNodes = @state.messages.map((message) => FM.Message({
-                key: message.mUid
-                subject: message.mSubject
-                sender: message.mSender
-                date: message.mDate
-                mailbox: @safeID
-            }))
-            React.DOM.div(null,
-                React.DOM.table({className: "table table-striped table-hover"},
-                    React.DOM.thead(null,
-                        React.DOM.tr(null,
-                            React.DOM.th(null, "Subject"),
-                            React.DOM.th(null, "From"),
-                            React.DOM.th(null, "Date")
+            messageNodes = @state.messages.map((message) => @renderMessage(
+                message.mUid
+                message.mSubject
+                message.mSender
+                message.mDate
+                message.mContents
+            ))
+            React.DOM.div(null
+                React.DOM.table({className: 'table table-striped table-hover'}
+                    React.DOM.thead(null
+                        React.DOM.tr(null
+                            React.DOM.th(null, 'Subject')
+                            React.DOM.th(null, 'From')
+                            React.DOM.th(null, 'Date')
                         )
-                    ),
+                    )
                     React.DOM.tbody(null, messageNodes)
                 )
             )
-        else React.DOM.div({}, "Loading...")
-
-        return React.DOM.div({className: 'panel panel-default'},
-            React.DOM.div({className: 'panel-heading'},
-                React.DOM.h4({className: 'panel-title'},
+        else React.DOM.div({}, 'Loading...')
+
+        return React.DOM.div({className: 'panel panel-default'}
+            React.DOM.div({className: 'panel-heading'}
+                React.DOM.h4({className: 'panel-title'}
                     React.DOM.a({
                         'data-toggle': 'collapse'
                         href: "##{@safeID}"
@@ -134,40 +162,6 @@
 })

-#FM.MessageList = React.createClass({
-#
-#    #renderSingle: ->
-#    #    message = this.state.message
-#    #    React.DOM.div(null,
-#    #        React.DOM.dl({className: "dl-horizontal"},
-#    #            React.DOM.dt(null, "Subject"),
-#    #            React.DOM.dd(null, message.message_subject),
-#    #            React.DOM.dt(null, "From"),
-#    #            React.DOM.dd(null, message.message_sender),
-#    #            React.DOM.dt(null, "Date"),
-#    #            React.DOM.dd(null, message.date)
-#    #        ),
-#    #        React.DOM.pre(null,
-#    #            this.state.message.contents[0]
-#    #        )
-#    #    )
-#
-#})
-
-
-FM.Message = React.createClass({
-    displayName: 'Message'
-    render: ->
-        React.DOM.tr(null,
-            React.DOM.td(null, React.DOM.a({
-                href: FM.makeUrl(@props.mailbox, @props.key)
-            }, @props.subject)),
-            React.DOM.td(null, @props.sender),
-            React.DOM.td(null, @props.date)
-        )
-});
-
-
 FM.PagerItem = React.createClass({

     displayName: 'PagerItem'
@@ -178,11 +172,11 @@
     render: ->
         # TODO: loading
         if @props.currentPage == @props.target
-            React.DOM.li({className: "active"}, React.DOM.span({}, @props.text))
+            React.DOM.li({className: 'active'}, React.DOM.span({}, @props.text))
         else if 0 < @props.target <= @props.total
             React.DOM.li({onClick: @handleClick}, React.DOM.span({}, @props.text))
         else
-            React.DOM.li({className: "disabled"}, React.DOM.span({}, @props.text))
+            React.DOM.li({className: 'disabled'}, React.DOM.span({}, @props.text))
 })

@@ -200,11 +194,11 @@
             handleClick: @props.handleClick
             text: text
         })
-        previous = makePagerItem(@props.currentPage - 1, "«")
-        next = makePagerItem(@props.currentPage + 1, "»")
+        previous = makePagerItem(@props.currentPage - 1, '«')
+        next = makePagerItem(@props.currentPage + 1, '»')
         # TODO: handle a large number of pages
         items = (makePagerItem(i) for i in [1..totalPages])
-        return React.DOM.ul({className: "pagination"}, previous, items, next)
+        return React.DOM.ul({className: 'pagination'}, previous, items, next)
 })

@@ -212,7 +206,7 @@
     displayName: 'Alert'
     render: ->
         if @props.code && @props.message
-            return React.DOM.div({className: "alert alert-danger", role: "alert"},
-                React.DOM.strong(null, @props.code), ": ", @props.message
+            return React.DOM.div({className: 'alert alert-danger', role: 'alert'}
+                React.DOM.strong(null, @props.code), ': ', @props.message
             );
 })


Constantize POLLING and call .off on api

--- src/navigation/notifications-model.coffee
+++ src/navigation/notifications-model.coffee
@@ -5,13 +5,13 @@
 POLL_INTERVAL = 5 * 60 * 1000 # 5 minutes
 STORAGE_KEY   = 'ox-cc-notifications'
 WINDOW = window
+POLLING = null

 pollForUpdate = ->
   api.channel.emit('notifications.send.fetch') unless window.document.hidden is true

 channel = new EventEmitter2

-polling = null

 activeNotifications = {}

@@ -56,15 +56,17 @@
 destroy = (windowImpl = window) ->
   WINDOW = windowImpl
   activeNotifications = {}
-  WINDOW.clearInterval(polling) if polling
+  api.channel.off 'notifications.fetch.*', loaded
+  WINDOW.clearInterval(POLLING) if POLLING
+  POLLING = null

 init = (windowImpl = window) ->
   WINDOW = windowImpl
-  return if polling
+  return if POLLING # if set that indicates we've been called twice
   api.channel.on 'notifications.fetch.*', loaded
   pollForUpdate()
-  polling = WINDOW.setInterval(pollForUpdate, POLL_INTERVAL)
+  POLLING = WINDOW.setInterval(pollForUpdate, POLL_INTERVAL)

 module.exports = {


Adding a factory function for Polygons.

Adding a factory function for Polygons.

--- tests/test_polygon.coffee
+++ tests/test_polygon.coffee
@@ -5,14 +5,15 @@
 module "Polygon"

+# A factory function for points.
+polygon = (points...) ->
+    vertices = (new wolf.Point(p[0], p[1]) for p in points)
+    return new wolf.Polygon({vertices : vertices})
+
 test "setPosition", 10, () ->

     # Assert that all vertices are updated on when moving.
-    t = new wolf.Polygon({vertices : [
-        new wolf.Point(0, 0)
-        new wolf.Point(0, 1)
-        new wolf.Point(1, 1)
-    ]})
+    t = polygon([0, 0], [0, 1], [1, 1])

     equals(t.x, 0, "initial x works")
     equals(t.y, 0, "initial y works")
@@ -37,14 +38,9 @@

 test "getCenterPoint", () ->
     # Assert that all vertices are updated on when moving.
-    square = new wolf.Polygon({vertices : [
-        new wolf.Point(0, 0)
-        new wolf.Point(2, 0)
-        new wolf.Point(2, 2)
-        new wolf.Point(0, 2)
-    ]})
-
+    square = polygon([0, 0], [2, 0], [2, 2], [0, 2])
     sc = square.getCenter()
     equals(sc.x, 1, "center x of a square is correct")
     equals(sc.y, 1, "center y of a square is correct")

+


Check anonymous for clicking link.

Check anonymous for clicking link.

--- app/views/play/ladder_view.coffee
+++ app/views/play/ladder_view.coffee
@@ -127,18 +127,18 @@
     session = (s for s in @sessions.models when s.get('team') is teamID)[0]
     modal = new LadderPlayModal({}, @level, session, teamID)
     @openModalView modal
-
+
   showApologeticSignupModal: ->
     SignupModal = require 'views/modal/signup_modal'
     @openModalView(new SignupModal({showRequiredError:true}))

   onClickedLink: (e) ->
     link = $(e.target).closest('a').attr('href')
-    if link?.startsWith '/play/level'
+    if link?.startsWith('/play/level') and me.get('anonymous')
       e.stopPropagation()
       e.preventDefault()
       @showApologeticSignupModal()
-
+
   destroy: ->
     clearInterval @refreshInterval
     @simulator.destroy()


fix(wrong-sorting): removed wrong sorting by sku

--- src/coffee/fetchstocks.coffee
+++ src/coffee/fetchstocks.coffee
@@ -8,7 +8,7 @@

   run: ->
-    query = @client.inventoryEntries.all().sort('sku').expand('supplyChannel')
+    query = @client.inventoryEntries.all().expand('supplyChannel')
     queryString = []
     if @queryString
       queryString = @queryString.split(' and ')


Fix underscore naming of vars from set_data

--- bokehjs/src/coffee/models/annotations/label.coffee
+++ bokehjs/src/coffee/models/annotations/label.coffee
@@ -28,11 +28,11 @@
     @set_visuals(@mget('source'))

   _set_data: () ->
-    @label_div = ($("<div>").addClass('label').hide() for i in @text)
-    @width = (null for i in @text)
-    @height = (null for i in @text)
-    @x_shift = (null for i in @text)
-    @y_shift = (null for i in @text)
+    @label_div = ($("<div>").addClass('label').hide() for i in @_text)
+    @width = (null for i in @_text)
+    @height = (null for i in @_text)
+    @x_shift = (null for i in @_text)
+    @y_shift = (null for i in @_text)

     ld = @mget("border_line_dash")
     if _.isArray(ld)
@@ -44,23 +44,23 @@
         @line_dash = ld

     ctx = @plot_view.canvas_view.ctx
-    for i in [0...@text.length]
+    for i in [0...@_text.length]
       @visuals.text.set_vectorize(ctx, i)
-      @width[i] = ctx.measureText(@text[i]).width
-      @height[i] = ctx.measureText(@text[i]).ascent / 1.175
+      @width[i] = ctx.measureText(@_text[i]).width
+      @height[i] = ctx.measureText(@_text[i]).ascent / 1.175
       [ @x_shift[i], @y_shift[i] ] = @_calculate_offset(ctx, @height[i], @width[i])

   _map_data: () ->
     if @mget('x_units') == "data"
-      vx = @xmapper.v_map_to_target(@x)
+      vx = @xmapper.v_map_to_target(@_x)
     else
-      vx = @x.slice(0) # make deep copy to not mutate
+      vx = @_x.slice(0) # make deep copy to not mutate
     sx = @canvas.v_vx_to_sx(vx)

     if @mget('y_units') == "data"
-      vy = @ymapper.v_map_to_target(@y)
+      vy = @ymapper.v_map_to_target(@_y)
     else
-      vy = @y.slice(0) # make deep copy to not mutate
+      vy = @_y.slice(0) # make deep copy to not mutate

     sy = @canvas.v_vy_to_sy(vy)

@@ -96,11 +96,11 @@

   _canvas_text: () ->
     ctx = @plot_view.canvas_view.ctx
-    for i in [0...@text.length]
+    for i in [0...@_text.length]
       ctx.save()

       ctx.rotate(@mget('angle'))
-      ctx.translate(@sx[i] + @x_offset[i], @sy[i] + @y_offset[i])
+      ctx.translate(@sx[i] + @_x_offset[i], @sy[i] + @_y_offset[i])

       ctx.beginPath()
       ctx.rect(@x_shift[i], @y_shift[i], @width[i], @height[i])
@@ -115,13 +115,13 @@

       if @visuals.text.doit
         @visuals.text.set_vectorize(ctx, i)
-        ctx.fillText(@text[i], 0, 0)
+        ctx.fillText(@_text[i], 0, 0)
       ctx.restore()

   _css_text: () ->
     ctx = @plot_view.canvas_view.ctx

-    for i in [0...@text.length]
+    for i in [0...@_text.length]
       @visuals.text.set_vectorize(ctx, i)
       @visuals.border_line.set_vectorize(ctx, i)
       @visuals.background_fill.set_vectorize(ctx, i)
@@ -132,18 +132,18 @@
       @label_div[i].hide()

       @label_div[i]
-        .html(@text[i])
+        .html(@_text[i])
         .css({
           'position': 'absolute'
-          'top': "#{@sy[i] + @y_offset[i] + @y_shift[i]}px"
-          'left': "#{@sx[i] + @x_offset[i] + @x_shift[i]}px"
-          'color': "#{@text_color[i]}"
-          'opacity': "#{@text_alpha[i]}"
-          'font-size': "#{@text_font_size[i]}"
+          'top': "#{@sy[i] + @_y_offset[i] + @y_shift[i]}px"
+          'left': "#{@sx[i] + @_x_offset[i] + @x_shift[i]}px"
+          'color': "#{@_text_color[i]}"
+          'opacity': "#{@_text_alpha[i]}"
+          'font-size': "#{@_text_font_size[i]}"
           'font-family': "#{@mget('text_font')}"
           'background-color': "#{@visuals.background_fill.color_value()}"
           'border-style': "#{@line_dash}"
-          'border-width': "#{@border_line_width[i]}"
+          'border-width': "#{@_border_line_width[i]}"
           'border-color': "#{@visuals.border_line.color_value()}"
           })
         .show()
@@ -157,8 +157,8 @@

   @define {
       x:            [ p.NumberSpec,                     ]
+      x_units:      [ p.SpatialUnits, 'data'            ]
       y:            [ p.NumberSpec,                     ]
-      x_units:      [ p.SpatialUnits, 'data'            ]
       y_units:      [ p.SpatialUnits, 'data'            ]
       text:         [ p.StringSpec,   { field :"text" } ]
       angle:        [ p.AngleSpec,    0                 ]


additional test

additional test

--- test.coffee
+++ test.coffee
@@ -77,14 +77,23 @@

       last.should.equal '> stopped'

-    it 'should enter() "playing" and "light" state after receiving "play message"', ->
+    it 'should enter() "playing" and "light" state after receiving "play" message', ->
       states.trigger 'play'
-
       [..., beforeLast, last] = output
       [last, beforeLast].should.contain '> running'
       [last, beforeLast].should.contain '> light'

-# states.trigger 'play'
+    it 'should exit() "active" state enter() "paused" state after receiving "pause" message', ->
+      states.trigger 'play'
+      states.trigger 'pause'
+
+      [..., beforeLast, last] = output
+      beforeLast
+        .should.equal '< running'
+
+      last
+        .should.equal '> paused'
+
 # states.trigger 'pause'
 # states.trigger 'flip'
 # states.trigger 'flip'


Fix a typo that caused the tests to fail.

--- test.coffee
+++ test.coffee
@@ -46,5 +46,4 @@
       constructor: ->

     obj = new ClassName()
-    asert.equal (typeOf obj), "classname"
-
+    assert.equal (typeOf obj), "classname"


Fixing for a in b() -> ...

Fixing for a in b() -> ...

--- lexer.coffee
+++ lexer.coffee
@@ -95,7 +95,6 @@
         tag = 'UNARY'
       else if tag in RELATION
         if tag isnt 'INSTANCEOF' and @seenFor
-          @seenFor = no
           tag = 'FOR' + tag
         else
           tag = 'RELATION'
@@ -232,6 +231,7 @@
   # can close multiple indents, so we need to know how far in we happen to be.
   lineToken: ->
     return 0 unless match = MULTI_DENT.exec @chunk
+    @seenFor = no
     indent = match[0]
     @line += count indent, '\n'
     prev = last @tokens, 1
@@ -305,7 +305,7 @@
   literalToken: ->
     if match = OPERATOR.exec @chunk
       [value] = match
-      @tagParameters() if CODE.test value
+      @tagParameters() if not @seenFor and CODE.test value
     else
       value = @chunk.charAt 0
     tag  = value


Another attempt to fix #1150

Another attempt to fix #1150

Here's how the algorithm in balancedString() was modified.  When we
encounter a slash in an interpolation, we:

    * try to find a heregex right after it; if found---skip it.  Three
      slashes always terminate a heregex, no matter if there is an open
      "#{" before them or not, so we don't have to bother about
      sub-interpolations inside the heregex.

    * try to find a regex right after it; if found---skip it.  Simple
      regexen can't contain interpolations.

    * otherwise, assume that the slash means division and carry on.

--- lexer.coffee
+++ lexer.coffee
@@ -414,6 +414,8 @@
           continue
       if end is '}' and letter in ['"', "'"]
         stack.push end = letter
+      else if end is '}' and letter is '/' and match = (HEREGEX.exec(str.slice i) or REGEX.exec(str.slice i))
+        i += match[0].length - 1
       else if end is '}' and letter is '{'
         stack.push end = '}'
       else if end is '"' and prev is '#' and letter is '{'


Add test for modifying the global API after subclassing.

--- test/api_test.coffee
+++ test/api_test.coffee
@@ -27,6 +27,13 @@
       instance = new Model()
       expect(instance.api.foo).eql 300

+    it 'modifying the global API after subclassing', ->
+      db2 = { foo: 300 }
+
+      Model = Ento()
+      Ento.object.api(db2)
+      expect(Model.api()).eq db2
+
     it "modifying api in model doesn't affect others", ->
       db2 = { foo: 300 }


bind => on

bind => on

--- test/bindable.coffee
+++ test/bindable.coffee
@@ -1,7 +1,7 @@
 module "Bindable"

 test "#bind and #trigger", 1, ->
-  o = Core().include(Bindable)
+  o = Core()

   o.bind("test", -> ok true)


Scroll while dragging

Scroll while dragging

--- client/publication.coffee
+++ client/publication.coffee
@@ -191,13 +191,15 @@
   if Session.equals 'currentPublicationRendered', true
     $(@find '.viewport').draggable
       containment: 'parent'
-      start: (e) ->
+      start: (e, ui) ->
         draggingViewport = true
         true # Ensure default runs
-      stop: (e) ->
-        draggingViewport = false
+      drag: (e, ui) ->
         $target = $(e.target)
         scrollToOffset $target.offset().top - $target.parent().offset().top
+        true # Ensure default runs
+      stop: (e, ui) ->
+        draggingViewport = false
         true # Ensure default runs
     $(@find '.viewport').height "#{ viewportHeightPercentage() }%"


Don't error if no labels

Don't error if no labels

--- app/assets/javascripts/app/checkin/label_set.js.coffee
+++ app/assets/javascripts/app/checkin/label_set.js.coffee
@@ -3,7 +3,7 @@
 class Checkin.LabelSet
   constructor: (@data, @label_templates) ->
     by_label_id = {}
-    for _, labels of @data.labels
+    for _, labels of @data.labels when labels
       for l in labels
         data = $.extend {}, l, @data
         code = data.barcode_id.substring(data.barcode_id.length-4)


juggernaut fix

juggernaut fix

--- app/assets/javascripts/app/models/juggernaut.js.coffee
+++ app/assets/javascripts/app/models/juggernaut.js.coffee
@@ -17,12 +17,12 @@
     model = App[message.model] or throw('Model required')
     switch message.type
       when 'create'
-        unless model.exists(msg.id)
-          model.create(msg.record)
+        unless model.exists(message.id)
+          model.create(message.record)
       when 'update'
-        model.update(msg.id, msg.record)
+        model.update(message.id, message.record)
       when 'destroy'
-        model.destroy(msg.id)
+        model.destroy(message.id)
       else
         throw("Unknown type: #{message.type}")


Add pagination to slider.

Add pagination to slider.

--- app/assets/javascripts/lib/components/slider.js.coffee
+++ app/assets/javascripts/lib/components/slider.js.coffee
@@ -27,6 +27,7 @@
       @slides = config.slides
       @slides_container = @$el.find(config.slides_container)
       @$slider_controls = $('

')
+      @$slider_pagination = $('

')
       @$next = $('1 of '+@$el.find(@slides).length+'')
       @$prev = $('1 of '+@$el.find(@slides).length+'')
       @$legacy = $('html.ie7, html.ie8, body.browserIE7, body.browserIE8')
@@ -40,6 +41,17 @@
     init: ->
       @$slider_controls.append(@$next).append(@$prev)
       @slides_container.append(@$slider_controls)
+
+      i=1
+      while i <= $(@slides).length
+        do (i) =>
+          $slideLink = $(''+i+'')
+          $slideLink.on 'click', =>
+            @_goToSlide(i)
+            return false
+          @$slider_pagination.append($slideLink);
+        i++
+      @slides_container.after(@$slider_pagination)

       @_setupSlideClasses()


#1436 Make cube.boundingBox optional

#1436 Make cube.boundingBox optional

--- app/assets/javascripts/oxalis/model/binary/cube.coffee
+++ app/assets/javascripts/oxalis/model/binary/cube.coffee
@@ -156,10 +156,11 @@

     boundary = @cubes[zoomStep].boundary

+    bbContainsBucket = if @boundingBox? then @boundingBox.containsBucket([x, y, z, zoomStep]) else true
     return x >= 0 and x < boundary[0] and
            y >= 0 and y < boundary[1] and
            z >= 0 and z < boundary[2] and
-           @boundingBox.containsBucket([x, y, z, zoomStep])
+           bbContainsBucket

   getBucketIndex : ([x, y, z, zoomStep]) ->


fix cdnUrlModifiers

fix cdnUrlModifiers

--- app/assets/javascripts/uploadcare/files/base.js.coffee
+++ app/assets/javascripts/uploadcare/files/base.js.coffee
@@ -50,7 +50,8 @@
       @isImage = data.is_image
       @imageInfo = data.image_info
       @isStored = data.is_stored
-      @cdnUrlModifiers = data.default_effects or null
+      if data.default_effects
+        @cdnUrlModifiers = "-/" + data.default_effects

       if not @onInfoReady.fired()
         @onInfoReady.fire @__fileInfo()


Clear cooldown on manual fire

Clear cooldown on manual fire

--- app/scripts/components/weapons/RapidWeaponLaser.coffee
+++ app/scripts/components/weapons/RapidWeaponLaser.coffee
@@ -20,7 +20,7 @@

     @shooting = no
     @lastShot = 0
-    @cooldown = 200
+    @cooldown = 250
     @shotsFired = 0
     @burstCount = Infinity
     @frontFire = yes
@@ -33,10 +33,10 @@
     @level = @determineLevel @xp
     if level isnt @level
       @cooldown = switch @level
-        when 0 then 200
-        when 1 then 150
-        when 2 then 100
-        when 3 then 50
+        when 0 then 250
+        when 1 then 200
+        when 2 then 150
+        when 3 then 100
       @trigger 'levelUp', @level

   determineLevel: (xp) ->
@@ -56,6 +56,7 @@
     else
       @shooting = no
       @shotsFired = 0
+      @lastShot = 500

   _autoFire: (fd) ->
     @lastShot += fd.dt
@@ -79,7 +80,7 @@
       when 2 then w: 14, speed: 560, h: 4
       when 3 then w: 18, speed: 565, h: 4

-    Crafty.e('Bullet')
+    Crafty.e('Bullet, IgnoreSun')
       .attr
         x: @x + @w
         y: @y + (@h / 2) - (settings.h / 2) + 1
@@ -104,7 +105,7 @@
       when 2 then w: 10, speed: 560, h: 3
       when 3 then w: 14, speed: 565, h: 3

-    Crafty.e('Bullet')
+    Crafty.e('Bullet, IgnoreSun')
       .attr
         x: @x + @w
         y: @y + (@h / 2) - (settings.h / 2) - 2


Move analytics event into shared lib

Move analytics event into shared lib

--- app/assets/javascripts/lib/analytics/analytics.js.coffee
+++ app/assets/javascripts/lib/analytics/analytics.js.coffee
@@ -5,7 +5,20 @@
     constructor: (data)->
       data ?= (window.lp.tracking)
       @config = $.extend(data, @_userAuth())
-
+
+    # Subscribe
+    listen: ->
+      $(@LISTENER).on ':cards/received', (e, data, state, analytics) =>
+        args = [state]
+        args.push(analytics.stack) if (analytics and analytics.stack)
+        @["_#{analytics.callback}"].apply(@,args)
+
+      $(@LISTENER).on ':cards/append/received', (e, data, state, analytics) =>
+        @["_#{analytics.callback}"](state) if analytics
+
+      $(@LISTENER).on ':page/received', (e, data, state, analytics) =>
+        @["_#{analytics.callback}"](analytics.url, analytics.stack)
+
     trackLink: (params = {}) ->
       @_save()
       @_add(params)


Avoid double logging

Avoid double logging

--- apps/artwork/components/partner_phone_number/view.coffee
+++ apps/artwork/components/partner_phone_number/view.coffee
@@ -1,6 +1,10 @@
+_ = require 'underscore'
 Backbone = require 'backbone'
 analytics = require '../../../../lib/analytics.coffee'
 template = -> require('./template.jade') arguments...
+
+logOnce = _.once (locations) ->
+  analytics.track.funnel "Displayed 'show phone number' button" if locations.length

 module.exports = class PartnerPhoneNumberView extends Backbone.View
   events:
@@ -24,8 +28,8 @@
   render: ->
     @$el.html template
       artwork: @model
-      locations: @locations()
+      locations: locations = @locations()

-    analytics.track.funnel "Displayed 'show phone number' button"
+    logOnce locations

     this


use continue button instead of next steps title

--- tutor/src/components/task-step/step-with-reading-content.cjsx
+++ tutor/src/components/task-step/step-with-reading-content.cjsx
@@ -1,5 +1,5 @@
 React = require 'react'
-
+{Button} = require 'react-bootstrap'
 {TaskStepStore} = require '../../flux/task-step'
 {TaskPanelStore} = require '../../flux/task-panel'
 {ArbitraryHtmlAndMath, ChapterSectionMixin} = require 'shared'
@@ -30,14 +30,10 @@
     @props.stepType is 'interactive'

   renderNextStepLink: ->
-    return null unless @props.nextStepTitle
-

-
-        Continue to “
-          {@props.nextStepTitle}
-        ” <Icon type="chevron-right" />
-
-

+    return null unless @props.onContinue
+    <Button bsStyle="primary" onClick={@props.onContinue}>
+      Continue
+    </Button>

   shouldOpenNewTab: -> true


it's even more specific about that.

it's even more specific about that.

--- test/foursquare-locator_test.coffee
+++ test/foursquare-locator_test.coffee
@@ -34,4 +34,4 @@
     expect(@robot.respond).to.have.been.calledWith(/foursquare forget ([a-zA-Z0-9]+)/i)

   it 'registers a hear listener', ->
-    expect(@robot.respond).to.have.been.calledWith(/where[ ']i?s ([a-zA-Z0-9 ]+)(\?)?/i)+    expect(@robot.respond).to.have.been.calledWith(/where[ ']i?s ([a-zA-Z0-9 ]+)(\?)?$/i)


Update dashing-src.coffee

Update dashing-src.coffee

--- vendor/assets/javascripts/dashing/dashing-src.coffee
+++ vendor/assets/javascripts/dashing/dashing-src.coffee
@@ -55,7 +55,9 @@
       "Last updated at #{hours}:#{minutes}"

   @::on 'ready', ->
-    Dashing.Widget.fire 'ready'
+    if !@ready
+      Dashing.Widget.fire 'ready'
+    @ready = true

   receiveData: (data) =>
     @mixin(data)


More code comments.

More code comments.

--- lib.coffee
+++ lib.coffee
@@ -65,11 +65,15 @@
         update.$set = {}
         update.$set[key] = value

+    # We checked that the "key" is an object.
     else
-      # We checked that the "key" is an object.
+      # We have to add "_connectionId", otherwise it will be removed.
       update = _.extend key,
         _connectionId: connectionId

+    # We make sure (on the server side) that "_connectionId" matches current
+    # connection to prevent malicious requests who guess the "subscriptionId"
+    # to modify the state of other subscriptions.
     collection.update
       _id: subscriptionId
       _connectionId: connectionId


Added helper method `setValue` to `PickerMixin`, which calls `onChange` handler when value changed.

--- lib.coffee
+++ lib.coffee
@@ -23,15 +23,18 @@
   componentWillReceiveProps: ({value})->
     # We need update value in state only when it differs from existing value
     @setState {value} if value? and value isnt @state.value
+
+  setValue: (value)->
+    # Set new value and call onChange handler
+    @setState {value}, @props.onChange if value? and value isnt @state.value

   changeValue: (fn)->
     {format, onChange} = @props
     {value} = @state
     date = moment value, format
     fn.call date
-    value = date.format format
-    @setState {value}, onChange
-
+    @setValue date.format format
+
   parseFormat: (format)->
     format.split formatRegex


Removed unused package reference

Removed unused package reference

--- src/scaffolding/plugins/git.coffee
+++ src/scaffolding/plugins/git.coffee
@@ -1,6 +1,4 @@
 Q = require 'q'
-
-nodegit = require('nodegit')
 fs = require 'fs'


Added code to construct geojs map when done loading

--- bokehjs/src/coffee/common/geojs_plot.coffee
+++ bokehjs/src/coffee/common/geojs_plot.coffee
@@ -37,8 +37,14 @@
         @map = geo.map(map_options)
         @map.createLayer('osm')

-      # TODO (bev) uncomment when script load and build_map are ready
-      #_.defer(build_map)
+      if window.ogs.geo
+        _.defer(build_map)
+      else
+        script = document.createElement('script');
+        script.type = 'text/javascript';
+        script.src = 'http://opengeoscience.github.io/geojs/lib/geo.all.js';
+        script.onload = build_map
+        document.body.appendChild(script);

     _map_hook: (ctx, frame_box) ->
       [left, top, width, height] = frame_box


fix cache selection logic

fix cache selection logic

--- bokehjs/src/coffee/common/properties.coffee
+++ bokehjs/src/coffee/common/properties.coffee
@@ -218,32 +218,32 @@
     ctx.setLineDashOffset(@dash_offset.value())

   set_vectorize: (ctx, i) ->
+    @cache_select("color", i)
     if ctx.strokeStyle != @cache.fill
-      @cache_select("color", i)
       ctx.strokeStyle = @cache.color

+    @cache_select("alpha", i)
     if ctx.globalAlpha != @cache.alpha
-      @cache_select("alpha", i)
       ctx.globalAlpha = @cache.alpha

+    @cache_select("width", i)
     if ctx.lineWidth != @cache.width
-      @cache_select("width", i)
       ctx.lineWidth = @cache.width

+    @cache_select("join", i)
     if ctx.lineJoin != @cache.join
-      @cache_select("join", i)
       ctx.lineJoin = @cache.join

+    @cache_select("cap", i)
     if ctx.lineCap != @cache.cap
-      @cache_select("cap", i)
       ctx.lineCap = @cache.cap

+    @cache_select("dash", i)
     if ctx.getLineDash() != @cache.dash
-      @cache_select("dash", i)
       ctx.setLineDash(@cache.dash)

+    @cache_select("dash_offset", i)
     if ctx.getLineDashOffset() != @cache.dash_offset
-      @cache_select("dash_offset", i)
       ctx.setLineDash(@cache.dash_offset)

@@ -271,12 +271,12 @@
     ctx.globalAlpha = @alpha.value()

   set_vectorize: (ctx, i) ->
+    @cache_select("color", i)
     if ctx.fillStyle != @cache.fill
-      @cache_select("color", i)
       ctx.fillStyle = @cache.color

+    @cache_select("alpha", i)
     if ctx.globalAlpha != @cache.alpha
-      @cache_select("alpha", i)
       ctx.globalAlpha = @cache.alpha

 class Text extends ContextProperties
@@ -343,24 +343,24 @@
     ctx.textBaseline = @baseline.value()

   set_vectorize: (ctx, i) ->
+    @cache_select("font", i)
     if ctx.font != @cache.font
-      @cache_select("font", i)
       ctx.font = @cache.font

+    @cache_select("color", i)
     if ctx.fillStyle != @cache.color
-      @cache_select("color", i)
       ctx.fillStyle = @cache.color

+    @cache_select("alpha", i)
     if ctx.globalAlpha != @cache.alpha
-      @cache_select("alpha", i)
       ctx.globalAlpha = @cache.alpha

+    @cache_select("align", i)
     if ctx.textAlign != @cache.align
-      @cache_select("align", i)
       ctx.textAlign = @cache.align

+    @cache_select("baseline", i)
     if ctx.textBaseline != @cache.baseline
-      @cache_select("baseline", i)
       ctx.textBaseline = @cache.baseline

 #


Fix switch of collaborator document leaders switching to solo mode

Before this fix the leader kept on using the shared document - this fix clones the document so that the shared collaborated document state is maintained

--- src/code/providers/lara-provider.coffee
+++ src/code/providers/lara-provider.coffee
@@ -302,8 +302,8 @@
               # the current user is still the leader so update the collaborator states to follow the leader (in case there are new collaborators) and load the existing document
               return setFollowers null, finished
             else
-              # the current user has gone from leader to solo mode so update run state to remove collaborator and load the existing document
-              return removeCollaborator null, finished
+              # the current user has gone from leader to solo mode so clone the document to preserve the collaborated document and update the run state to remove collaborator
+              return cloneDoc (err) -> removeCollaborator(err, finished)
           else
             if haveCollaborators
               # the current user has switched from follower to leader so clone the existing leader document, become the new leader and update the followers and load the new document


omit outputting footnotes if none exists

--- src/compiler/directives/footnote.coffee
+++ src/compiler/directives/footnote.coffee
@@ -26,6 +26,9 @@
     cb(null,"")

 FootnoteContent.transform = (input,cb) ->
+  if footnotes.length == 0
+    return input
+
   html = """
   <ol class="footnotes">
     #{footnotes.join "\n"}


Update tr.coffee (#6150)

Update tr.coffee (#6150)

Translated line 9,10,17,20,26,95,99,100,107


Update zh-HANS.coffee (#4977)

Update zh-HANS.coffee (#4977)

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -1697,19 +1697,19 @@
 #    hoc_and: "and"
 #    hoc_programming: "JavaScript/Python Programming"
 #    hoc_teacher_led: "Teacher-Led Lessons"
-#    hoc_teacher_led_desc1: "Download our"
+    hoc_teacher_led_desc1: "下载我们"
 #    hoc_teacher_led_link: "Introduction to Computer Science lesson plans"
 #    hoc_teacher_led_desc2: "to introduce your students to programming concepts using offline activities"
 #    hoc_group: "Group Gameplay"
 #    hoc_group_desc_1: "Teachers can use the lessons in conjunction with our Introduction to Computer Science course to track student progress. See our"
 #    hoc_group_link: "Getting Started Guide"
-#    hoc_group_desc_2: "for more details"
+    hoc_group_desc_2: "获取更多信息"
 #    hoc_additional_desc1: "For additional CodeCombat resources and activities, see our"
-#    hoc_additional_desc2: "Questions"
-#    hoc_additional_contact: "Get in touch"
+    hoc_additional_desc2: "问题"
+    hoc_additional_contact: "保持联系"
 #    revoke_confirm: "Are you sure you want to revoke a Full License from {{student_name}}? The license will become available to assign to another student."
-#    revoke_all_confirm: "Are you sure you want to revoke Full Licenses from all students in this class?"
-#    revoking: "Revoking..."
+    revoke_all_confirm: "你确定要撤销所有这个班级的所有学生的许可证吗？"
+    revoking: "撤销中"
     unused_licenses: "您有未使用的学生许可,当学生准备学习更多课程时您可以分配给他们!"
     remember_new_courses: "记得分配新课程!"
     more_info: "更多信息"
@@ -1756,18 +1756,18 @@
     success: "成功"
     in_progress: "进行中"
     not_started: "没有开始"
-#    mid_course: "Mid-Course"
-#    end_course: "End of Course"
-#    none: "None detected yet"
-#    explain_open_ended: "Note: Students are encouraged to solve this level creatively — one possible solution is provided below."
-#    level_label: "Level:"
-#    time_played_label: "Time Played:"
-#    back_to_resource_hub: "Back to Resource Hub"
-#    back_to_course_guides: "Back to Course Guides"
-#    print_guide: "Print this guide"
+    mid_course: "课程进行中"
+    end_course: "课程已结束"
+    none: "还没有发现"
+    explain_open_ended: "注意:鼓励学生创造性地解决这个问题——下面提供了一个可能的解决方法"
+    level_label: "级别"
+    time_played_label: "游戏时间："
+    back_to_resource_hub: "返回资源仓库"
+    back_to_course_guides: "返回课程指导"
+    print_guide: "打印指导手册"
     combo: "组合"
     combo_explanation: "学生至少使用一个列出的概念组合来挑战关卡，点击进度点查看学生代码。"
-#    concept: "Concept"
+    concept: "概念"

   share_licenses:
     share_licenses: "分享许可证"
@@ -1784,7 +1784,7 @@
     one_license_used: "（1个许可证被使用）"
     licenses_used: "（__licensesUsed__个许可证被使用）"
     more_info: "更多信息"
-
+
   sharing:
     game: "游戏"
     webpage: "网页"


latex/help: missing link about floats and images

--- src/smc-webapp/editor_latex.coffee
+++ src/smc-webapp/editor_latex.coffee
@@ -151,7 +151,8 @@
 1. Upload a PNG or PDF file via #{SiteName}'s "Files" interface.
    The uploaded image should be in the same directory as the `.tex` file
    Otherwise, use relative paths like `./images/filename.png` if it is in a subdirectory `images`.
-2. Follow [these instructions] about how to insert a graphic in a figure environment.
+2. Follow [these instructions](https://en.wikibooks.org/wiki/LaTeX/Floats,_Figures_and_Captions)
+   about how to insert a graphic in a figure environment.
    Do not forget `\\usepackage{graphicx}` in the preamble declaration.

 ### How to insert a backslash or dollar sign?


change declaration order

change declaration order

--- app/models/mailbox.coffee
+++ app/models/mailbox.coffee
@@ -286,18 +286,18 @@
     # reload
     id = Number(@ImapLastFetchedId) + 1
     console.log "Fetching mail #{@} | UID #{id}:#{id + limit})"
+
+    emitOnErr = (server, err) ->
+        if err
+            console.log err
+            server.emit "error", err if server?

     @connectImapServer (err, server) =>
         return emitOnErr(server, err) if err
         @loadInbox server, (err) =>
             return emitOnErr server, err if err
             loadNewMails(server, id)
-
-    emitOnErr = (server, err) ->
-        if err
-            console.log err
-            server.emit "error", err if server?
-
+
     loadNewMails = (server, id) =>
         range = "#{id}:#{id + limit}"
         server.search [['UID', range]], (err, results) =>


Testing

Testing

--- widgets/cusi_progress_bars/cusi_progress_bars.coffee
+++ widgets/cusi_progress_bars/cusi_progress_bars.coffee
@@ -120,7 +120,7 @@

       # Every x milliseconds, the function should run.
 #      stepInterval = 16.667
-      stepInterval = 1.667
+      stepInterval = 166.667

       # Change the duration based on the distance between points.
       duration = baseDuration + Math.abs(endpointDifference) * 25


New species!

New species!

--- src/definitions/creature-species.coffee
+++ src/definitions/creature-species.coffee
@@ -112,6 +112,11 @@
 	symbol: 'spaceBeeMonarch'
 	weight: 2 / 10000

+class classes.Haithera extends classes.Species
+	name: 'haithera'
+	symbol: 'haithera'
+	weight: 400
+
 makeName = (className) ->
 	className[0].toLowerCase() + className[1..]


Tutorial subject

Tutorial subject

--- app/models/subject.coffee
+++ app/models/subject.coffee
@@ -64,8 +64,8 @@

   @selectTutorial: =>
     tutorialSubject = @create
-      zooniverse_id: 'TUTORIAL_SUBJECT'
-      workflow_ids: ['50e9e3d33ae740f1f3000002']
+      zooniverse_id: 'APF0000x3t'
+      workflow_ids: ['50eb8c5c3ae74028ea000001']
       location: standard: 'images/tutorial-subject.jpg'
       coords: [-81.801, 76.14]
       metadata:


Request: prevent crash in case routes property hasn't been created yet

--- components/Request.coffee
+++ components/Request.coffee
@@ -50,6 +50,7 @@

   removeRoute: ->
     return unless @app and @verb and @path
+    return unless @app.routes
     routes = @app.routes[@verb]
     return unless routes
     for route, i in routes