Fix uploaded time

Fix uploaded time

--- app/assets/javascripts/date.coffee
+++ app/assets/javascripts/date.coffee
@@ -1,5 +1,5 @@
 $ ->
   $(".upload-date.needs-formatting").each (i, el) ->
     $(el).removeClass("needs-formatting")
-    m = moment(el.innerHTML)
+    m = moment.utc(el.innerHTML)
     el.innerHTML = m.fromNow()


Use config ip for pq request

Use config ip for pq request

--- app/assets/javascripts/print.js.coffee
+++ app/assets/javascripts/print.js.coffee
@@ -38,7 +38,7 @@
       $('#pq .alert-box').hide()
       $('#pq .done').toggle()
       $.ajax
-        url: '/print/pq.json'
+        url: $('#pq').data('url')
         type: 'POST'
         data:
           username: $('#print_username').val()


comment out disconnection detection, needs work.

--- app/assets/javascripts/init.coffee
+++ app/assets/javascripts/init.coffee
@@ -68,31 +68,31 @@
   $(document).ajaxError (e, xhr) ->
     if MCM.currentUser == undefined or $(".disconnect-notification").length > 0
       return
-
-    if xhr.status == 403
-      bootbox.dialog("You have been logged out.", [{
-        "label" : "Login",
-        "class" : "btn-primary"
-        "callback" : ->
-          window.location = "/"
-      }], { "classes" : "disconnect-notification" })
-    else
-      bootbox.dialog('You seem to have been disconnected.

', [{
-        "label" : "Reconnect",
-        "class" : "btn-primary"
-        "callback" : ->
-          $.ajax '/collectives', {
-            success: (rsp, check_status) ->
-              debugger;
-              Backbone.history.loadUrl(Backbone.history.fragment)
-              $(".disconnect-notification").modal("hide")
-            fail: (rsp, check_status) ->
-              debugger;
-              $(".disconnect-notification .reconnect").innerHTML(check_status)
-          }
-          return false
-      }], { "classes" : "disconnect-notification" })
+
+#    if xhr.status == 403
+#      bootbox.dialog("You have been logged out.", [{
+#        "label" : "Login",
+#        "class" : "btn-primary"
+#        "callback" : ->
+#          window.location = "/"
+#      }], { "classes" : "disconnect-notification" })
+#    else
+#      bootbox.dialog('You seem to have been disconnected.

', [{
+#        "label" : "Reconnect",
+#        "class" : "btn-primary"
+#        "callback" : ->
+#          $.ajax '/collectives', {
+#            success: (rsp, check_status) ->
+#              debugger;
+#              Backbone.history.loadUrl(Backbone.history.fragment)
+#              $(".disconnect-notification").modal("hide")
+#            fail: (rsp, check_status) ->
+#              debugger;
+#              $(".disconnect-notification .reconnect").innerHTML(check_status)
+#          }
+#          return false
+#      }], { "classes" : "disconnect-notification" })

 $ ->
   MCM.start()
-  Backbone.history.start()+  Backbone.history.start()


fix multiline stuff

fix multiline stuff

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -26,14 +26,11 @@
   messages.each (_, el) ->
     $(el).addClass 'sent_by_me'

-multiline = (selection) ->
-  selection.html().match(/
/)?
-
 addMultilineToMessages = ->
   $('.attachment_contents').each (_, el) ->
     selection = $(el)
     if selection.html().match(/
/)
-      selection.closest('content').addClass 'multiline_attachment'
+      selection.closest('.content').first().addClass('multiline_attachment')

 redraw = ->
   botifyMessages()


fix the display of the clue text in the match finder dialog

--- src/coffee/client/puzzle.coffee
+++ src/coffee/client/puzzle.coffee
@@ -537,8 +537,8 @@
         lines = text.split('\n')
         for line in lines
             parsed = parseClueLine(line)
-            if parsed == number
-                return parsed.secondPart
+            if parsed.number == number
+                return parsed.secondPart.trim()
         return ""

     closeMatchFinder: () ->
@@ -576,8 +576,8 @@
         else
             prevR--
             prevR++
-        if (prevR >= 0 and prevR < @height() and prevC >= 0 and prevC <= @width and g[prevR][prevC].open) or \
-           (nextR >= 0 and nextR < @height() and nextC >= 0 and nextC <= @width and g[nextR][nextC].open)
+        if (prevR >= 0 and prevR < @height() and prevC >= 0 and prevC < @width() and g[prevR][prevC].open) or \
+           (nextR >= 0 and nextR < @height() and nextC >= 0 and nextC < @width() and g[nextR][nextC].open)
             fail()
             return


Wrap "Add task" menu in a label

Wrap "Add task" menu in a label

--- app/classifier/tasks/combo/editor.cjsx
+++ app/classifier/tasks/combo/editor.cjsx
@@ -50,13 +50,16 @@
       else
         <DragReorderable tag="ul" items={@props.task.tasks} render={@renderSubtask} onChange={@setOrder} />}

-        <select value="stuck" onChange={@addTask}>
-          <option value="stuck" disabled>Add a task...</option>
-          {Object.keys(@props.workflow.tasks).map (taskKey) =>
-            taskDescription = @props.workflow.tasks[taskKey]
-            TaskComponent = tasks[taskDescription.type]
-            <option key={taskKey} value={taskKey} disabled={taskDescription.type is 'drawing'}>{TaskComponent.getTaskText taskDescription}</option>}
-        </select>
+
+          Add a task:{' '}
+          <select value="stuck" onChange={@addTask}>
+            <option value="stuck" disabled>Tasks...</option>
+            {Object.keys(@props.workflow.tasks).map (taskKey) =>
+              taskDescription = @props.workflow.tasks[taskKey]
+              TaskComponent = tasks[taskDescription.type]
+              <option key={taskKey} value={taskKey} disabled={taskDescription.type is 'drawing'}>{TaskComponent.getTaskText taskDescription}</option>}
+          </select>
+

         This task’s requirements are met when{' '}


Tidy up code and improve handling of lockdown mode

--- src/code/views/document-actions-view.coffee
+++ src/code/views/document-actions-view.coffee
@@ -20,8 +20,8 @@
       selectedLinks      = manager.getLinkInspection() or []

       @setState
-        #selectedNodes: selectedNodes
-        #selectedLinks: selectedLinks
+        selectedNodes: selectedNodes
+        selectedLinks: selectedLinks
         selectedItems: selectedNodes.concat selectedLinks

   undoClicked: ->
@@ -32,12 +32,10 @@

   deleteClicked: ->
     if @state.lockdown
+      # only allow deletion of links in lockdown mode
       @props.graphStore.removeSelectedLinks()
-      # if @state.selectedLinks && @state.selectedLinks.length
-        # only allow deletion of links in lockdown mode
-        # @props.graphStore.removeSelectedLinks()

-    else #if @state.selectedItems && @state.selectedItems.length > 0
+    else
       @props.graphStore.deleteSelected()

     @props.graphStore.selectionManager.clearSelection()
@@ -56,9 +54,9 @@
       unless @state.hideUndoRedo
         (div {className: 'misc-actions'},
           # lockdown mode only highlight delete button when we have a link selected
-          # @state.lockdown && @state.selectedLinks && @state.selectedLinks.length > 0 && (i {className: "icon-codap-trash", onClick: @deleteClicked}
-          # )
-          @state.selectedItems && @state.selectedItems.length > 0 && (i {className: "icon-codap-trash", onClick: @deleteClicked})
+          @state.lockdown && @state.selectedLinks && @state.selectedLinks.length > 0 && (i {className: "icon-codap-trash", onClick: @deleteClicked}
+          )
+          !@state.lockdown && @state.selectedItems && @state.selectedItems.length > 0 && (i {className: "icon-codap-trash", onClick: @deleteClicked})
           (i {className: "icon-codap-arrow-undo #{buttonClass @state.canUndo}", onClick: @undoClicked, disabled: not @state.canUndo})
           (i {className: "icon-codap-arrow-redo #{buttonClass @state.canRedo}", onClick: @redoClicked, disabled: not @state.canRedo})
         )


Implement global selection in relations table from New Chapter

Closes #136

--- app/frontend/javascripts/app.js.coffee
+++ app/frontend/javascripts/app.js.coffee
@@ -5,9 +5,6 @@
 App.Trix          = require 'script!trix'

 $(document).ready ->
-
-  # Activate tooltips
-  $('[data-toggle="tooltip"]').tooltip()

   $body = $('body')

@@ -26,6 +23,13 @@
     appStory.render()
     $( window ).resize appStory.resize

+  # Activate tooltips
+  $('[data-toggle="tooltip"]').tooltip()
+
+  # Setup select-all checkbox in Chapter new/edit
+  $('#relations_select_all').change (e) ->
+    $('.table tbody input[type=checkbox]').prop 'checked', $(this).prop('checked')
+
   # Add file input feedback
   # based on http://www.abeautifulsite.net/whipping-file-inputs-into-shape-with-bootstrap-3/
   $(document).on 'change', '.btn-file :file', () ->


Support for merging multiple properties into one

--- src/components/DuplicateProperty.coffee
+++ src/components/DuplicateProperty.coffee
@@ -3,15 +3,19 @@
 class DuplicateProperty extends noflo.Component
     constructor: ->
         @properties = {}
+        @separator = '/'

         @inPorts =
             property: new noflo.ArrayPort()
+            separator: new noflo.Port()
             in: new noflo.Port()
         @outPorts =
             out: new noflo.Port()

         @inPorts.property.on "data", (data) =>
             @setProperty data
+        @inPorts.separator.on "data", (data) =>
+            @separator = data

         @inPorts.in.on "begingroup", (group) =>
             @outPorts.out.beginGroup group
@@ -28,11 +32,22 @@
             return

         propParts = prop.split "="
-        @properties[propParts[0]] = propParts[1]
+        if propParts.length > 2
+            @properties[propParts.pop()] = propParts
+            return
+
+        @properties[propParts[1]] = propParts[0]

     addProperties: (object) ->
-        for original, newprop of @properties
-            object[newprop] = object[original]
+        for newprop, original of @properties
+            if typeof original is "string"
+                object[newprop] = object[original]
+                continue
+
+            newValues = []
+            for originalProp in original
+                newValues.push object[originalProp]
+            object[newprop] = newValues.join @separator

         @outPorts.out.send object


Enable harmony for jsx

Enable harmony for jsx

--- webpack.coffee
+++ webpack.coffee
@@ -73,8 +73,8 @@
 				test: /\.svg$/
 				loader: 'raw-loader'
 			,
-				test: /\.jsx$/
-				loader: 'jsx'
+				test: /\.jsx?$/
+				loader: 'jsx-loader?harmony'
 			,
 				test: /\.css$/
 				loaders: [
@@ -123,8 +123,8 @@
 				test: /\.scss$/
 				loader: ExtractTextPlugin.extract('style-loader', 'css-loader!autoprefixer-loader?{browsers:["last 2 version", "ie 10", "Android 4"]}!sass-loader')
 			,
-				test: /\.jsx$/
-				loader: 'jsx'
+				test: /\.jsx?$/
+				loader: 'jsx-loader?harmony'
 			,
 				test: /\.coffee$/
 				loader: 'coffee-loader'


Don't fail if there is no comment

Don't fail if there is no comment

--- examples/collection.coffee
+++ examples/collection.coffee
@@ -30,7 +30,7 @@

 class PostController
   postComment: ->
-    @model.comments.push(@newComment)
+    @model.comments.push(@newComment) if @newComment

   commentEdited: (event) ->
     @newComment = new Comment(body: event.target.value)


Don't initialize js-lib until document.readyState is interactive.

--- app/js/loader/loader_common.coffee
+++ app/js/loader/loader_common.coffee
@@ -30,46 +30,52 @@
     content && el.appendChild(content)
     el

-  # Add styles
-  style_tag = mkEl 'style', null, document.createTextNode(style_code)
-  document.getElementsByTagName('head')[0].appendChild style_tag
+  onReady = ->
+    # Add styles
+    style_tag = mkEl 'style', null, document.createTextNode(style_code)
+    document.getElementsByTagName('head')[0].appendChild style_tag

-  #### Create iframe so jslib's namespace (window) doesn't collide with any content window.
-  jslib_jail_iframe = mkEl 'iframe', 'factlink-iframe'
+    #### Create iframe so jslib's namespace (window) doesn't collide with any content window.
+    jslib_jail_iframe = mkEl 'iframe', 'factlink-iframe'

-  # Wrapper for increased CSS specificity
-  outerWrapperEl = mkEl 'div', 'factlink-containment-wrapper', jslib_jail_iframe
+    # Wrapper for increased CSS specificity
+    outerWrapperEl = mkEl 'div', 'factlink-containment-wrapper', jslib_jail_iframe

-  document.body.appendChild outerWrapperEl
+    document.body.appendChild outerWrapperEl

-  window.FACTLINK_ON_CORE_LOAD = ->
-    for name in methods
-      do (name) ->
-        # don't return the value, as we can't do that when storing calls
-        window.FACTLINK[name] = ->
-          jslib_jail_iframe.contentWindow.FactlinkJailRoot[name](arguments...)
-          return
+    window.FACTLINK_ON_CORE_LOAD = -> #called from jail-iframe when core iframe is ready.
+      for name in methods
+        do (name) ->
+          # don't return the value, as we can't do that when storing calls
+          window.FACTLINK[name] = ->
+            jslib_jail_iframe.contentWindow.FactlinkJailRoot[name](arguments...)
+            return

-    delete window.FACTLINK_ON_CORE_LOAD
+      delete window.FACTLINK_ON_CORE_LOAD

-    for methodCall in storedMethodCalls
-      window.FACTLINK[methodCall.name](methodCall.arguments...)
-    storedMethodCalls = undefined
+      for methodCall in storedMethodCalls
+        window.FACTLINK[methodCall.name](methodCall.arguments...)
+      storedMethodCalls = undefined

-  jail_window = jslib_jail_iframe.contentWindow
-  jail_window.FactlinkConfig = window.FactlinkConfig
-  jail_window.FrameCss = frame_style_code
+    jail_window = jslib_jail_iframe.contentWindow
+    jail_window.FactlinkConfig = window.FactlinkConfig
+    jail_window.FrameCss = frame_style_code

-  #### Load iframe with script tag
-  jslib_jail_doc = jail_window.document
+    #### Load iframe with script tag
+    jslib_jail_doc = jail_window.document

-  jslib_jail_doc.open()
-  jslib_jail_doc.write '<!DOCTYPE html><title></title>'
-  jslib_jail_doc.close()
+    jslib_jail_doc.open()
+    jslib_jail_doc.write '<!DOCTYPE html><title></title>'
+    jslib_jail_doc.close()

-  script_tag = jslib_jail_doc.createElement 'script'
-  script_tag.appendChild(jslib_jail_doc.createTextNode(jslib_jail_code))
-  jslib_jail_doc.documentElement.appendChild(script_tag)
+    script_tag = jslib_jail_doc.createElement 'script'
+    script_tag.appendChild(jslib_jail_doc.createTextNode(jslib_jail_code))
+    jslib_jail_doc.documentElement.appendChild(script_tag)
+
+  if /^(interactive|complete)$/.test(document.readyState)
+    onReady()
+  else
+    document.addEventListener('DOMContentLoaded',onReady);

 jslib_jail_code = __INLINE_JS_PLACEHOLDER__


Free decorators from array when they are destroyed.

--- lib/analysis_decorator.coffee
+++ lib/analysis_decorator.coffee
@@ -39,4 +39,5 @@
   refreshDecoratorsForPath: (fullPath) ->
     decorators = @decoratorMap[fullPath] || []
     for dec in decorators
-      dec.destroy()
+      dec.destroy()
+    @decoratorMap[fullPath] = []


keep the final dot in snippets

keep the final dot in snippets

--- app/scripts/filters/snippet.coffee
+++ app/scripts/filters/snippet.coffee
@@ -29,7 +29,7 @@
       if targetIndex isnt -1
         ret = (theParts[i].substr 0, targetIndex) + ""
         ret += (theParts[i].substr targetIndex, theMatch.length) + ""
-        ret += theParts[i].substr (targetIndex + theMatch.length), theParts[i].length
+        ret += theParts[i].substr (targetIndex + theMatch.length), theParts[i].length + '.'
         return ret
       i++
     trimmedString = string.substr(0, 100)


Filter empty room name

Filter empty room name

--- src/rocketchat.coffee
+++ src/rocketchat.coffee
@@ -62,7 +62,8 @@
 			@robot.logger.info "Successfully connected!"
 			@robot.logger.info RocketChatRoom

-			rooms = RocketChatRoom.split(',')
+			rooms = RocketChatRoom.split(',').filter (room) ->
+				room != ''
 			# @robot.logger.info JSON.stringify(rooms)

 			# Log in


Fix browser path resolution.

Fix browser path resolution.

--- browser/node.coffee
+++ browser/node.coffee
@@ -115,11 +115,10 @@
   normalize: (path) -> path

   resolve: (path) ->
+    absolute = path[0] == '/'
     components = path.split '/'
     for c, idx in components
-      if c == '.'
-        # when we implement dirs, this will resolve to the cwd
-        components[idx] = ''
-    # remove repeated //s, but take care not to remove a slash at the beginning
-    # if one exists (i.e. indicating root.)
-    (c for c, idx in components when c != '' or idx == 0).join '/'
+      components[idx] = '' if c == '.'
+    # remove repeated //s
+    path = (c for c, idx in components when c != '').join '/'
+    (if absolute then '/' else '') + path


autofix out of bound input

autofix out of bound input

--- src/app/bucket-list/bucket-list.coffee
+++ src/app/bucket-list/bucket-list.coffee
@@ -33,7 +33,12 @@
       bucket.getGroupContribution()

       $scope.$watch "round.buckets["+index+"].myContribution.amountDollars", (amountDollars) ->
+
         round.getMyAllocationsLeftCents(round.myAllocationsAmountCents)
+
+        if round.myAllocationsLeftCents < 0
+          bucket.myContribution.amountCents += round.myAllocationsLeftCents
+
         $scope.status = round.getStatus()

         if amountDollars >= 0


Fix itemSelector null in Zepto. Closes gh-603.

--- src/chaplin/views/collection_view.coffee
+++ src/chaplin/views/collection_view.coffee
@@ -70,7 +70,8 @@

   # Selector which identifies child elements belonging to collection
   # If empty, all children of $list are considered.
-  itemSelector: null
+  # Not null, because of Zepto bug https://github.com/madrobby/zepto/pull/768.
+  itemSelector: undefined

   # Filtering
   # ---------


Added new prompts to hi.coffee

Added new prompts to hi.coffee

--- src/scripts/hi.coffee
+++ src/scripts/hi.coffee
@@ -11,3 +11,9 @@

   robot.respond /whatup$/i, (msg) ->
     msg.send "just the same old server livin. you?"
+
+  robot.respond /you suck/i, (msg) ->
+    msg.send "robot sad."
+
+  robot.respond /how are you(\?)?/i, (msg) ->
+    msg.send "just swell. how you doin'?"


CUI.DragDropSelect

CUI.DragDropSelect

- Implement missing @destroy Function.


addHistory & link suppression

addHistory & link suppression

--- app/views/CNeditor/CNeditor.coffee
+++ app/views/CNeditor/CNeditor.coffee
@@ -459,7 +459,7 @@
                shortcut in ['-tab', '-return', '-backspace', '-suppr',
                             'CtrlShift-down', 'CtrlShift-up',
                             'CtrlShift-left', 'CtrlShift-right',
-                            'Ctrl-B', 'Ctrl-U', 'Ctrl-K',
+                            'Ctrl-B', 'Ctrl-U',
                             'Ctrl-V', 'Shift-tab', '-space', '-other', 'Alt-A']
             @_addHistory()

@@ -776,10 +776,10 @@
             segments = @_getLinkSegments()
             # case when the start break point is in a link
             if segments
-                @_showUrlPopover(segments,true)
+                @_showUrlPopover(segments,false)
             # case when the start break point is not in a link
             else
-                @_saveLine()
+                @_addHistory()
                 @_applyMetaDataOnSelection('A','http://')
                 segments = @_getLinkSegments()
                 @_showUrlPopover(segments,true)
@@ -787,40 +787,19 @@
         return true

-
-    _saveLine : () ->
-        # save selection
-        savedSel = @saveEditorSelection()
-        # duplicate line content
-        currentSel = @updateCurrentSel()
-        lineDiv = @currentSel.startLine.line$[0]
-        rg = document.createRange()
-        rg.selectNodeContents(lineDiv)
-        frag = rg.cloneContents()
-        # store
-        @savedLine =
-            lineDiv     : lineDiv
-            lineContent : frag
-            savedSel    : savedSel
-
-    _restoreSavedLine : () ->
-        # restore line content
-        rg = document.createRange()
-        rg.selectNodeContents(@savedLine.lineDiv)
-        rg.deleteContents()
-        @savedLine.lineDiv.appendChild(@savedLine.lineContent)
-        # restore selection
-        rangy.deserializeSelection @savedLine.savedSel, @linesDiv
-
-
-
     ###*
      * Show, positionate and initialise the popover for link edition.
-     * @param  {element} segment The segment of the link (<a>...</a>)
-    ###
-    _showUrlPopover : (segments, calledByLinkifySelection) ->
+     * @param  {array} segments  An array with the segments of
+     *                           the link [<a>,...<a>]. Must be created even if
+     *                           it is a creation in order to put a background
+     *                           on the segment where the link will be.
+     * @param  {boolean} isLinkCreation True is it is a creation. In this case,
+     *                                  if the process is canceled, the initial
+     *                                  state without link will be restored.
+    ###
+    _showUrlPopover : (segments, isLinkCreation) ->
         pop = @urlPopover
-        pop.calledByLinkifySelection = calledByLinkifySelection
+        pop.isLinkCreation = isLinkCreation

         # pop.initialSelRg = @getEditorSelection().getRangeAt(0).cloneRange()
         pop.initialSelRg = @currentSel.theoricalRange.cloneRange()
@@ -840,20 +819,57 @@

     _cancelUrlPopover : () =>
         pop = @urlPopover
+        pop.style.display = 'none'
         seg.style.removeProperty('background-color') for seg in pop.segments
-        # case of a linkifySelection() called and cancelled => reverse the
-        # modification
-        if pop.calledByLinkifySelection
-            @_restoreSavedLine()
-            pop.style.display = 'none'
+        # case of a link creation called and cancelled : a segment for the link
+        # to creat has already been added in order to show the selection when
+        # popover is visible. As it is canceled, we undo in order to remove this
+        # link.
+        if pop.isLinkCreation
+            rg = document.createRange()
+            rg.selectNodeContents(seg)
+            @currentSel.sel.setSingleRange rg
+            @unDo()
             @setFocus()
+        else
+            @currentSel.sel.setSingleRange(pop.initialSelRg)
+            @setFocus()
+
+        return true
+
+    _validateUrlPopover : () =>
+        pop = @urlPopover
+        segments = pop.segments
+
+        # in case of a link creation and the user validated an empty url, just
+        # cancel the link creation
+        if pop.urlInput.value == '' && pop.isLinkCreation
+            @_cancelUrlPopover()
             return true

-
-        # case of a deletion of the urlInput value, what means 'remove the link'
-        if pop.urlInput.value == '' or pop.urlInput.value == 'http:///'
-            bps = @_applyMetaData(pop.initialSelRg, false, 'A', [])
-            # 4- Position selection
+        # remove background of selection and hide popover
+        pop.style.display = 'none'
+        seg.style.removeProperty('background-color') for seg in segments
+
+        # in case of a link creation, addhistory has already be done, it must
+        # then be done if non a link creation.
+        if !pop.isLinkCreation
+            @currentSel.sel.setSingleRange pop.initialSelRg # otherwise addhistory will not work
+            @_addHistory()
+
+        # case of a deletion of the urlInput value => 'remove the link'
+        if pop.urlInput.value == ''
+            # bps = @_applyMetaData(pop.initialSelRg, false, 'A', [])
+            l = segments.length
+            bp1 =
+                cont : segments[0].firstChild
+                offset : 0
+            bp2 =
+                cont : segments[l-1].firstChild
+                offset : segments[l-1].firstChild.length
+            bps = [bp1,bp2]
+            @_applyAhrefToSegments(segments[0], segments[l-1], bps, false, 'A', '')
+            # Position selection
             rg = document.createRange()
             bp1 = bps[0]
             bp2 = bps[1]
@@ -865,22 +881,11 @@
                 sel = document.getSelection()
             sel.removeAllRanges()
             sel.addRange(rg)
-        else
-            @currentSel.sel.setSingleRange pop.initialSelRg
-        pop.style.display = 'none'
-        @setFocus()
-
-    _validateUrlPopover : () =>
-        pop = @urlPopover
-        segments = pop.segments
-        seg.style.removeProperty('background-color') for seg in segments
-
-        # case of a cancelation => remove links of segments
-        if pop.urlInput.value == ''
-            t
+            @setFocus()
+            return true

         # case if only href is changed but not the text
-        if pop.initialTxt == pop.textInput.value
+        else if pop.initialTxt == pop.textInput.value
             seg.href = pop.urlInput.value for seg in segments
             lastSeg = seg

@@ -895,8 +900,7 @@
                 parent.removeChild(seg)
             lastSeg = segments[0]

-        # hide popover and manage selection
-        pop.style.display = 'none'
+        # manage selection
         @_setCaret(lastSeg.firstChild, lastSeg.firstChild.length)
         @setFocus()

@@ -923,7 +927,7 @@
         [btnOK,btnCancel,btnDelete] = pop.querySelectorAll('button')
         btnOK.addEventListener('click',@_validateUrlPopover)
         btnCancel.addEventListener('click',@_cancelUrlPopover)
-        btnDelete.addEventListener 'click', () ->
+        btnDelete.addEventListener 'click', () =>
             pop.urlInput.value = ''
             @_validateUrlPopover()

@@ -2622,6 +2626,10 @@
     ### -------------------------------------------------------------------------
     #  unDo :
     # Undo the previous action
+
+    ###
+    ###*
+     * Undo the previous action
     ###
     unDo : () ->
         # if there is an action to undo
@@ -2666,7 +2674,7 @@
             # 2 - restore selection
             savedSel = @_history.historySelect[@_history.index+1]
             savedSel.restored = false
-            rangy.restoreSelection(savedSel)
+            rangy.deserializeSelection(savedSel, @linesDiv)
             # 3 - restore scrollbar position
             xcoord = @_history.historyScroll[@_history.index+1].xcoord
             ycoord = @_history.historyScroll[@_history.index+1].ycoord


Unlock game-dev-hoc levels based on progression

Level progress/unlocking between dungeon and game-dev-hoc campaigns
seems to play nicely together if a user jumps between them.

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -299,7 +299,6 @@
       level.locked = false if @editorMode
       level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro']
       level.locked = false if me.isInGodMode()
-      level.locked = false if @campaign?.get('slug') is 'game-dev-hoc'
       level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
       level.disabled = false if me.isInGodMode()
       level.color = 'rgb(255, 80, 60)'
@@ -365,6 +364,12 @@
       for level in orderedLevels
         if @levelStatusMap[level.slug] isnt 'complete'
           level.next = true
+          # Unlock and re-annotate this level
+          # May not be unlocked/awarded due to different game-dev-hoc progression using mostly shared levels
+          level.locked = false
+          level.hidden = level.locked
+          level.disabled = false
+          level.color = 'rgb(255, 80, 60)'
           return

     findNextLevel = (nextLevels, practiceOnly) =>


Show levels,bg with undefined page number on first campaign page (#161)

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -862,8 +862,8 @@
   applyCampaignStyles: ->
     return unless @campaign?.loaded
     if (backgrounds = @campaign.get 'backgroundImage') and backgrounds.length
-      if backgrounds.filter((b) => b.pageNumber).length == 0
-        background = backgrounds[0]
+      if @campaignPage == 1
+        background = backgrounds.find((b) => !b.pageNumber || b.pageNumber == @campaignPage) || {}
       else
         background = backgrounds.find((b) => b.pageNumber == @campaignPage) || {}
       rule = "#campaign-view .map-background { background-image: url(/file/#{background.image}); }"
@@ -1349,8 +1349,10 @@
     return @courseLevelsFake if @courseLevels?
     currentLevels = @campaign?.get('levels')
     if currentLevels
-      # Show levels without page number / having same page number as in url
-      currentLevelsKeys = Object.keys(currentLevels).filter((l) => !currentLevels[l].pageNumber || currentLevels[l].pageNumber == @campaignPage)
+      if @campaignPage == 1
+        currentLevelsKeys = Object.keys(currentLevels).filter((l) => !currentLevels[l].pageNumber || currentLevels[l].pageNumber == @campaignPage)
+      else
+        currentLevelsKeys = Object.keys(currentLevels).filter((l) => currentLevels[l].pageNumber == @campaignPage)
       return _.pick(currentLevels, currentLevelsKeys)
     return currentLevels


Fix for IE11 issue-all levels unlocked sporadically (#5006)

* Fix for IE11 issue-all levels unlocked sporadically

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -193,6 +193,8 @@
           @classroom = new Classroom(_id: classroomID)
           @supermodel.trackRequest @classroom.fetch()
           @listenToOnce @classroom, 'sync', =>
+            @updateClassroomSessions()
+            @render()
             @courseInstance.sessions = new CocoCollection([], {
               url: @courseInstance.url() + '/course-level-sessions/' + me.id,
               model: LevelSession
@@ -203,6 +205,7 @@
             @courseInstance.sessions.comparator = 'changed'
             @listenToOnce @courseInstance.sessions, 'sync', =>
               @courseStats = @classroom.statsForSessions(@courseInstance.sessions, @course.id)
+              @render()
             @courseLevels = new Levels()
             @supermodel.trackRequest @courseLevels.fetchForClassroomAndCourse(classroomID, courseID, {
               data: { project: 'concepts,practice,assessment,primerLanguage,type,slug,name,original,description,shareable,i18n' }
@@ -226,6 +229,7 @@
                     @courseLevelsFake[idx].position = levelPositions[idx]
                 @courseLevelsFake[idx].courseIdx = parseInt(k)
                 @courseLevelsFake[idx].requiresSubscription = false
+                @render()

               # Fill in missing positions, for courses which have levels that no longer exist in campaigns
               for k,v of courseLevels
@@ -337,23 +341,14 @@
     levelPlayCountsRequest.load()

   onLoaded: ->
-    if @classroom
-      classroomLevels = @classroom.getLevels()
-      @classroomLevelMap = _.zipObject(classroomLevels.map((l) -> l.get('original')), classroomLevels.models)
-      defaultLanguage = @classroom.get('aceConfig').language
-      for session in @sessions.slice()
-        classroomLevel = @classroomLevelMap[session.get('level').original]
-        if not classroomLevel
-          continue
-        expectedLanguage = classroomLevel.get('primerLanguage') or defaultLanguage
-        if session.get('codeLanguage') isnt expectedLanguage
-          @sessions.remove(session)
-          continue
-    unless @editorMode
-      for session in @sessions.models
-        unless @levelStatusMap[session.get('levelID')] is 'complete'  # Don't overwrite a complete session with an incomplete one
-          @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
-        @levelDifficultyMap[session.get('levelID')] = session.get('state').difficulty if session.get('state')?.difficulty
+    if @isClassroom()
+      @updateClassroomSessions()
+    else
+      unless @editorMode
+        for session in @sessions.models
+          unless @levelStatusMap[session.get('levelID')] is 'complete'  # Don't overwrite a complete session with an incomplete one
+            @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
+          @levelDifficultyMap[session.get('levelID')] = session.get('state').difficulty if session.get('state')?.difficulty

     @buildLevelScoreMap() unless @editorMode
     # HoC: Fake us up a "mode" for HeroVictoryModal to return hero without levels realizing they're in a copycat campaign, or clear it if we started playing.
@@ -383,6 +378,27 @@
     # Minecraft Modal:
     #@maybeShowMinecraftModal() # Disable for now

+
+  updateClassroomSessions: ->
+    if @classroom
+      classroomLevels = @classroom.getLevels()
+      @classroomLevelMap = _.zipObject(classroomLevels.map((l) -> l.get('original')), classroomLevels.models)
+      defaultLanguage = @classroom.get('aceConfig').language
+      for session in @sessions.slice()
+        classroomLevel = @classroomLevelMap[session.get('level').original]
+        if not classroomLevel
+          continue
+        expectedLanguage = classroomLevel.get('primerLanguage') or defaultLanguage
+        if session.get('codeLanguage') isnt expectedLanguage
+          # console.log("Inside remove session")
+          @sessions.remove(session)
+          continue
+      unless @editorMode
+        for session in @sessions.models
+          unless @levelStatusMap[session.get('levelID')] is 'complete'  # Don't overwrite a complete session with an incomplete one
+            @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
+          @levelDifficultyMap[session.get('levelID')] = session.get('state').difficulty if session.get('state')?.difficulty
+
   buildLevelScoreMap: ->
     for session in @sessions.models
       levels = @getLevels()
@@ -613,7 +629,7 @@
     false

   annotateLevels: (orderedLevels) ->
-    return if @course?
+    return if @isClassroom()

     for level, levelIndex in orderedLevels
       level.position ?= { x: 10, y: 10 }
@@ -622,13 +638,13 @@
       level.locked = true if level.requiresSubscription and @requiresSubscription and me.get('hourOfCode')
       level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
       level.locked = false if @editorMode
-      level.locked = false if not @course? and @campaign?.get('name') in ['Auditions', 'Intro']
+      level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro']
       level.locked = false if me.isInGodMode()
       level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
       level.disabled = false if me.isInGodMode()

       level.color = 'rgb(255, 80, 60)'
-      unless @course? or @campaign?.get('type') is 'hoc'
+      unless @isClassroom() or @campaign?.get('type') is 'hoc'
         level.color = 'rgb(80, 130, 200)' if level.requiresSubscription and not features.codePlay
         level.color = 'rgb(200, 80, 200)' if level.adventurer

@@ -692,9 +708,11 @@
     leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
     @openModalView leaderboardModal

+  isClassroom: -> @courseInstanceID?
+
   determineNextLevel: (orderedLevels) ->
-    if @courseStats?
-      @applyCourseLogicToLevels(orderedLevels)
+    if @isClassroom()
+      @applyCourseLogicToLevels(orderedLevels) if @courseStats?
       return true


Fix the settings page - it thought it was always overridden

--- js/inner.coffee
+++ js/inner.coffee
@@ -228,12 +228,17 @@
       @project_name()

     @is_inferred = @komp =>
-      full_spec = @setup
-      full_spec += @dependencies
-      full_spec += @compile
-      full_spec += @test
-      full_spec += @extra
+      full_spec = @setup()
+      full_spec += @dependencies()
+      full_spec += @compile()
+      full_spec += @test()
+      full_spec += @extra()
       "" == full_spec
+
+  submit_hipchat: () =>
+    $.put("/api/v1/project/#{username}/#{project_name}/hipchat")
+
+

 class User extends Base


adjust button to 10 min timeout for jobs

--- src/components/buttons/async-button.cjsx
+++ src/components/buttons/async-button.cjsx
@@ -13,7 +13,7 @@
     {isWaiting, isJob} = @props
     {isTimedout} = @state

-    timeout = if isJob then 300000 else 30000
+    timeout = if isJob then 600000 else 30000

     if isWaiting and not isTimedout
       _.delay =>


updated incorrect coments

updated incorrect coments

--- app/scripts/controllers/compare.coffee
+++ app/scripts/controllers/compare.coffee
@@ -7,9 +7,9 @@
 ######################

     # these were defined in AppCtrl and $scope will delegate to $rootScope
-      # $rootScope.reps
-      # $rootScope.selected
-      # $rootScope.reps_list
+      # $scope.reps
+      # $scope.selected
+      # $scope.reps_list

     # init local variables
     $scope.comparison =


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/active_bounds.coffee
+++ test/active_bounds.coffee
@@ -1,6 +1,6 @@
 module "ActiveBounds"

-test "should remove objects outside of activeBounds", ->
+test "should remove objects outside of activeBounds", 2, ->
   obj = GameObject
     x: 50
     y: 50


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/active_bounds.coffee
+++ test/active_bounds.coffee
@@ -9,7 +9,10 @@
   obj = GameObject
     x: 50
     y: 50
-    width:
+    width: 32
+    height: 32
+
+

 test "should remove objects outside of activeBounds", 2, ->
   obj = GameObject


Add xp drops for dummy content

Add xp drops for dummy content

--- app/scripts/lazerscripts/stage1.coffee
+++ app/scripts/lazerscripts/stage1.coffee
@@ -307,9 +307,11 @@
       @placeSquad Game.Scripts.ScraperFlyer,
         amount: 8
         delay: 750
+        drop: 'xp'
       @placeSquad Game.Scripts.Swirler,
         amount: 8
         delay: 750
+        drop: 'xp'
         options:
           shootOnSight: yes
       @placeSquad Game.Scripts.Stage1BossPopup
@@ -320,9 +322,11 @@
       @placeSquad Game.Scripts.ScraperFlyer,
         amount: 8
         delay: 750
+        drop: 'xp'
       @placeSquad Game.Scripts.Swirler,
         amount: 8
         delay: 750
+        drop: 'xp'
         options:
           shootOnSight: yes
     )


Fixed exception handler

Fixed exception handler

- Passport tends to use err.code and err.message
- Oauth2 stuff uses err.error and err.error_description
- Exception handler now supports both

--- src/server/common/ExceptionHandler.coffee
+++ src/server/common/ExceptionHandler.coffee
@@ -12,8 +12,8 @@
 		body: req.body
 		status: err.status
 		name: err.name
-		code: err.code
-		message: err.message
+		error: err.error || err.code
+		error_description: err.error_description || err.message
 	json =
 		data: prettyjson.render data, opts
 	log =


remove idiocy

remove idiocy

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -74,7 +74,7 @@
     .pipe gulp.dest '/Users/mc/Desktop'

 # this is also ugly and might work...
-gulp.task 'coverage', [test] ->
+gulp.task 'coverage', [ 'test' ] ->
   run 'mocha --compilers coffee:coffee-script/register
     -r blanket -r should
     -R mocha-lcov-reporter', { silent: true }


add coveralls

add coveralls

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -1,11 +1,12 @@
-coffee  = require 'gulp-coffee'
-gulp    = require 'gulp'
-gutil   = require 'gulp-util'
-istanbul= require 'gulp-istanbul'
-mocha   = require 'gulp-mocha'
-path    = require 'path'
-uglify  = require 'gulp-uglifyjs'
-verb    = require 'gulp-verb'
+coffee    = require 'gulp-coffee'
+coveralls = require 'gulp-coveralls'
+gulp      = require 'gulp'
+gutil     = require 'gulp-util'
+istanbul  = require 'gulp-istanbul'
+mocha     = require 'gulp-mocha'
+path      = require 'path'
+uglify    = require 'gulp-uglifyjs'
+verb      = require 'gulp-verb'

 destDir = path.dirname require('./package.json').main

@@ -23,6 +24,10 @@
       reporter: 'spec'
     .pipe istanbul.writeReports()

+gulp.task 'coveralls', ['test'], ->
+  gulp.src 'coverage/**/lcov.info'
+  .pipe coveralls()
+
 gulp.task 'compile',->
   gulp.src './src/*.{coffee,litcoffee}'
     .pipe coffee bare:false
@@ -31,4 +36,4 @@
     .pipe uglify(path.basename(require('./package.json').main).replace('.js','.min.js'))
     .pipe gulp.dest destDir

-gulp.task 'default', ['compile','test', 'docs']
+gulp.task 'default', ['compile','test', 'coveralls', 'docs']


Travis needs some more time

Travis needs some more time

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -25,7 +25,7 @@
   gulp.src(
     "tests/spec/*.coffee"
   ).pipe(mocha(
-    "timeout": 10000
+    "timeout": 30000
   ))

 gulp.task "compile", ["stylecheck"], ->


remove uncesary rename in coverage task

remove uncesary rename in coverage task

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -74,7 +74,6 @@
     -r test/register-coffee-coverage -r should
     -R mocha-lcov-reporter', { silent: true }
     .exec()
-    #.pipe rename 'lcov.info'
     .pipe streamify coveralls()

 gulp.task 'testwatch', ['test' ], ->


deleting obsolete readline example

deleting obsolete readline example

--- apps/client-cli/cli-example.coffee
+++ apps/client-cli/cli-example.coffee
@@ -1,14 +0,0 @@
-readline = require('readline')
-rl = readline.createInterface process.stdin, process.stdout
-
-rl.setPrompt 'OHAI> '
-rl.prompt()
-
-rl.on 'line', (line) ->
-  switch line.trim()
-    when 'hello' then console.log 'world!'
-    else console.log 'Say what? I might have heard `' + line.trim() + '`'
-  rl.prompt()
-rl.on 'close', ->
-  console.log 'Have a great day!'
-  process.exit 0


Rename 'editor-path-change' to 'editor:path-changed'

--- src/status-bar.coffee
+++ src/status-bar.coffee
@@ -29,7 +29,7 @@

   initialize: (@rootView, @editor) ->
     @updatePathText()
-    @editor.on 'editor-path-change', =>
+    @editor.on 'editor:path-changed', =>
       @subscribeToBuffer()
       @updatePathText()


AppController: add more checks to ping mechanism

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1889,28 +1889,59 @@

     pingInterval = 1000 * 5
     pongInterval = 1000 * 15
-    diffInterval = KD.config.collaboration.timeout
-
-    if @amIHost
-      @pingInterval = KD.utils.repeat pingInterval, =>
-        @pingTime.setText Date.now().toString()
-    else
-      @pingInterval = KD.utils.repeat pongInterval, =>
-        lastPing = @pingTime.getText()
-
-        return  if Date.now() - lastPing < diffInterval
-
-        KD.remote.api.Collaboration.stop @rtmFileId, @workspaceData, (err) =>
-          if err
-          then console.warn err
-          else
-            KD.utils.killRepeat @pingInterval
-            @stopCollaborationSession =>
-              @quit()
-
-              new KDNotificationView
-                title    : "@#{@collaborationHost} has left the session."
-                duration : 3000
+
+    @pingInterval = if @amIHost
+    then KD.utils.repeat pingInterval, @bound 'sendPing'
+    else KD.utils.repeat pongInterval, @bound 'checkPing'
+
+
+  sendPing: -> @pingTime.setText Date.now().toString()
+
+
+  forceQuitCollaboration = ->
+
+    { Collaboration } = KD.remote.api
+    Collaboration.stop @rtmFileId, @workspaceData, (err) =>
+
+      return warn err  if err
+
+      KD.utils.killRepeat @pingInterval
+
+      @stopCollaborationSession =>
+        @quit()
+
+        new KDNotificationView
+          title    : "@#{@collaborationHost} has left the session."
+          duration : 3000
+
+
+  checkPing: do ->
+
+    lastCheckedAt = null
+    lastPing      = null
+    errorTryCount = 3
+    diffInterval  = KD.config.collaboration.timeout
+
+    return ->
+
+      ping = @pingTime.getText()
+
+      # kill session if `errorTryCount`
+      # limit is passed.
+      if errorTryCount <= 0
+        forceQuitCollaboration.call this
+      # update the lastChecked at if the last ping
+      # is still the same, decrease the error try count.
+      else if ping is lastPing and errorTryCount > 0
+        errorTryCount -= 1
+        lastCheckedAt = Date.now()
+        return
+      # this is the happy path.
+      else if lastPing - ping < diffInterval
+        lastCheckedAt = Date.now()
+        lastPing      = ping
+        errorTryCount = 3
+        return

   removeParticipantCursorWidget: (targetUser) ->


Collaboration: Handle changes for drawing board to redraw it.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -889,11 +889,16 @@
       return unless context

       if change.type is 'ContentChange'
-        string = @rtm.getFromModel @realTimeDoc, change.context.file.path
+        string = @rtm.getFromModel @realTimeDoc, context.file.path

         @forEachSubViewInIDEViews_ 'editor', (editorPane) =>
-          if editorPane.getFile().path is change.context.file.path
+          if editorPane.getFile().path is context.file.path
             editorPane.setContent string.getText(), no
+
+      if change.type is 'DrawingBoardUpdated'
+        @forEachSubViewInIDEViews_ 'drawing', (drawingPane) =>
+          if drawingPane.hash is context.paneHash
+            drawingPane.setCanvasData change.context.data

   createPaneFromChange: (change) ->


Copy over all other fields from publish context.

--- server.coffee
+++ server.coffee
@@ -20,7 +20,9 @@
     @_publishStop = @publish.stop.bind @publish
     @_publishError = @publish.error.bind @publish

-    @userId = @publish.userId
+    # We copy other fields as they are
+    for own key, value of @publish when key not in ['added', 'changed', 'removed', 'ready', 'stop', 'error']
+      @[key] = value

   added: (args...) =>
     @_publishAdded args...
@@ -39,18 +41,6 @@

   error: (args...) =>
     @_publishError args...
-
-  # The following methods we do not override, so
-  # we can access them directly here.
-
-  onStop: (args...) =>
-    @publish.onStop args...
-
-  connection: (args...) =>
-    @publish.connection args...
-
-  params: (args...) =>
-    @publish.params()

 class globals.PublishEndpoint
   constructor: (@options, @publishFunction) ->


Count updateAll migrations better.

Count updateAll migrations better.

--- server.coffee
+++ server.coffee
@@ -455,11 +455,6 @@
     updateAll: (document, collection, currentSchema, intoSchema) =>
       @_updateAll = true

-      count = collection.update {_schema: currentSchema}, {$set: _schema: intoSchema}, {multi: true}
-
-      migrated: count
-      all: count
-
     forward: (document, collection, currentSchema, newSchema) =>
       migrated: 0
       all: collection.update {_schema: currentSchema}, {$set: _schema: newSchema}, {multi: true}
@@ -478,15 +473,31 @@
     forward: (document, collection, currentSchema, newSchema) =>
       @updateAll document, collection, currentSchema, newSchema

+      counts = super
+      counts.migrated = counts.all
+      counts
+
     backward: (document, collection, currentSchema, oldSchema) =>
       @updateAll document, collection, currentSchema, oldSchema
+
+      counts = super
+      counts.migrated = counts.all
+      counts

   @UpdateAllMajorMigration: class extends @MajorMigration
     forward: (document, collection, currentSchema, newSchema) =>
       @updateAll document, collection, currentSchema, newSchema

+      counts = super
+      counts.migrated = counts.all
+      counts
+
     backward: (document, collection, currentSchema, oldSchema) =>
       @updateAll document, collection, currentSchema, oldSchema
+
+      counts = super
+      counts.migrated = counts.all
+      counts

   @AddOptionalFieldsMigration: class extends @MinorMigration
     # Fields is an array


make auth calls

make auth calls

--- server.coffee
+++ server.coffee
@@ -14,7 +14,7 @@
 gitty   = require "gitty"

 GitHubApi = require "github"
-ghRepos   = new GitHubApi({version: "3.0.0"}).repos
+githubApi = new GitHubApi({version: "3.0.0"})

 redis = require "redis"
@@ -45,8 +45,13 @@
 }).login ["user", "repo", "gist"], (err, id, token) ->
   if err or not token
     throw new Error("Cannot connect to GitHub")
+  console.log "github auth token was set"
   nconf.set "GIHUB_ADMIN_TOKEN", token

+
+ghRepos = ->
+  githubApi.authenticate {type: "oauth", token: nconf.get("GIHUB_ADMIN_TOKEN")}
+  githubApi.repos

 createRepo = (repo, username, callback) ->
@@ -217,7 +222,7 @@
 getCommunities = (callback) ->
   rc.hgetall "communities", (err, hash) ->
     if err or not hash or Object.keys(hash) == 0
-      ghRepos.getFromOrg {org: "communities"}, (err, repos) ->
+      ghRepos().getFromOrg {org: "communities"}, (err, repos) ->
         if err
           callback err
           return
@@ -258,7 +263,7 @@

 getCommunity = (community, callback) ->
-  ghRepos.get {user: "communities", repo: community}, (err, repo) ->
+  ghRepos().get {user: "communities", repo: community}, (err, repo) ->
     if err
       callback err
       return
@@ -280,7 +285,7 @@
       callback undefined, repo

 getTopics = (community, callback) ->
-  ghRepos.getBranches {user: "communities", repo: community}, (err, branches) ->
+  ghRepos().getBranches {user: "communities", repo: community}, (err, branches) ->
     if err
       callback err
       return


Add automaticaly livereload when using the developpment server.

--- server.coffee
+++ server.coffee
@@ -59,7 +59,14 @@
   if request
     proxy.web(req, res)
   else if fs.existsSync(link)
-    file = fs.readFileSync(link)
+    file = fs.readFileSync(link, {
+      encoding: 'utf-8'
+    })
+    # Livereload
+    if link.match(/.*index.html$/)
+      localip = require('ip').address()
+      file = file.replace('', '')
+
     res.writeHead(200, {
       "Content-Length": file.length
       "Content-Encoding": 'utf-8'


fixes

fixes

--- server.coffee
+++ server.coffee
@@ -495,6 +495,9 @@
       params.topicsCount = _.reduce communities, topicsSumFunc, 0
       membersSumFunc = (memo, community) -> return memo + community.members_count
       params.membersCount = _.reduce communities, membersSumFunc, 0
+    else
+      params.topicsCount = 0
+      params.membersCount = 0
     res.render "index", params

 app.get "/", renderIndexPage


need to get a style guide, linter setup for each lang and apply across projects

--- server.coffee
+++ server.coffee
@@ -30,7 +30,7 @@
 tof = require 'toffee'

 cfg = {
-	port: 4747
+	port : 4747
 }

 rdr.block_state = false
@@ -50,15 +50,15 @@
 	head

 mkd_opt = {
-	breaks: false
-	gfm: true
-	highlight: (code) -> hlt.highlightAuto(code).value
-	pedantic: false
-	renderer: rdr
-	sanitize: true
-	smartLists: true
-	smartypants: false
-	tables: true
+	breaks      : false
+	gfm         : true
+	highlight   : (code) -> hlt.highlightAuto(code).value
+	pedantic    : false
+	renderer    : rdr
+	sanitize    : true
+	smartLists  : true
+	smartypants : false
+	tables      : true
 }

 log = console.log
@@ -71,9 +71,9 @@
 			file?
 		.map (file) ->
 			out = {
-				date: file[1]
-				title: hmz.titleCase file[2].replace /-/g, ' '
-				path: [file[1],file[2]].join('-')
+				date  : file[1]
+				title : hmz.titleCase file[2].replace /-/g, ' '
+				path  : [file[1],file[2]].join('-')
 			}

 			if !load then return out
@@ -107,11 +107,11 @@

 render_page = (res,body,page) ->
 	res.render 'scaffold', {
-		blog: body.blog ? ''
-		posts: body.posts ? []
-		content: body.content ? body
-		page: page
-		title: make_title page
+		blog    : body.blog ? ''
+		posts   : body.posts ? []
+		content : body.content ? body
+		page    : page
+		title   : make_title page
 	}

@@ -127,7 +127,6 @@

 app.use '/static', exp.static pth.join __dirname, 'static'

-# app.use '/content', page_renderer (toffee, requested bit in middle)
 app.use '/(:page)?', (req,res) ->
 	page = req.params.page ? 'home'

@@ -137,16 +136,16 @@
 				when 'home'
 					return blog_posts 0,3
 					.then (posts) -> {
-						content: body
-						posts: posts
-						blog: 'roll'
+						content : body
+						posts   : posts
+						blog    : 'roll'
 					}
 				when 'archive'
 					return blog_load false
 					.then (posts) -> {
-						content: body
-						posts: posts
-						blog: 'list'
+						content : body
+						posts   : posts
+						blog    : 'list'
 					}
 				else
 					body
@@ -155,3 +154,4 @@

 app.listen cfg.port, () ->
 	log "Webserver started [express, port #{cfg.port}]"
+


application.getAll: Use the my_application endpoint

With this change the application.getAll() will use the my_application
endpoint instead of defining pine filters. This will also stops it from
returning dependent applications.

Change-type: major
Signed-off-by: Thodoris Greasidis <5791bdc4541e79bd43d586e3c3eff1e39c16665f@balena.io>

--- lib/models/application.coffee
+++ lib/models/application.coffee
@@ -117,16 +117,10 @@
 		auth.getUserId()
 		.then (userId) ->
 			return pine.get
-				resource: 'application'
+				resource: 'my_application'
 				options:
 					mergePineOptions
 						$orderby: 'app_name asc'
-						$filter:
-							$or:
-								user: userId
-								includes__user: $any:
-									$alias: 'x'
-									$expr: x: user: userId
 					, options

 		.map (application) ->


Improve headless test stdout messages

Improve headless test stdout messages

--- test/headless-test.coffee
+++ test/headless-test.coffee
@@ -5,7 +5,7 @@
   if stacktrace?.length
     stack = for t in stacktrace
       ' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (if t.function then ' (in function ' + t.function + ')' else '')
-    console.log "ERROR: #{message}\n" + stack.join '\n'
+    console.log "PHANTOM: *** ERROR *** #{message}\n" + stack.join '\n'
     phantom.exit 1

 hostname = system.args[1] ? 'localhost:54321'
@@ -13,24 +13,24 @@
 page = webpage.create()

 page.onResourceError = ({ url, errorString }) ->
-  console.log "ERROR: #{url}: #{errorString}"
+  console.log "BROWSER: *** RESOURCE ERROR *** #{url}: #{errorString}"

 page.onConsoleMessage = (message) ->
-  console.log message
+  console.log "BROWSER: #{message}"

 waitFor = (test, onReady, timeout=3600000) ->
   startTime = new Date().getTime()
   condition = false
   retest = ->
     if (new Date().getTime() - startTime < timeout) and not condition
-      console.log 'Polling...'
+      console.log 'PHANTOM: PING'
       condition = test()
     else
       if condition
         onReady()
         clearInterval interval
       else
-        console.log 'ERROR: Timeout exceeded'
+        console.log 'PHANTOM: *** ERROR *** Timeout Exceeded'
         phantom.exit 1

   interval = setInterval retest, 2000
@@ -47,7 +47,7 @@
             console.log 'Fetching packs...'
             context.requestPacks (error, packNames) ->
               if error
-                console.log 'Failed fetching packs'
+                console.log '*** ERROR *** Failed fetching packs'
                 go error
               else
                 console.log 'Processing packs...'
@@ -59,7 +59,7 @@
             console.log "Fetching pack: #{packName}"
             context.requestPack packName, (error, flowNames) ->
               if error
-                console.log 'Failed fetching pack'
+                console.log '*** ERROR *** Failed fetching pack'
                 go error
               else
                 console.log 'Processing pack...'
@@ -68,10 +68,10 @@
                 (Flow.Async.iterate tasks) go

           runFlow = (packName, flowName, go) ->
-            console.log "Fetching flow: #{packName} - #{flowName}"
+            console.log "Fetching flow document: #{packName} - #{flowName}"
             context.requestFlow packName, flowName, (error, flow) ->
               if error
-                console.log 'Failed fetching flow'
+                console.log '*** ERROR *** Failed fetching flow document'
                 go error
               else
                 flowTitle = "#{packName} - #{flowName}"
@@ -84,10 +84,10 @@

                 waitForFlow = ->
                   if window._phantom_running_
-                    console.log 'Flow still running...'
+                    console.log 'ACK'
                     setTimeout waitForFlow, 2000
                   else
-                    console.log 'Flow completed'
+                    console.log 'Flow completed!'
                     errors = window._phantom_errors_
                     # delete all keys from the k/v store
                     context.requestRemoveAll ->
@@ -107,24 +107,22 @@
           window._phantom_started_ = yes
           runPacks (error) ->
             if error
-              console.log 'Error running packs'
-              console.log JSON.stringify error, null, 2
+              console.log '*** ERROR *** Error running packs: ' + JSON.stringify error
             else
-              console.log 'Finished running all packs'
+              console.log 'Finished running all packs!'
             window._phantom_exit_ = yes
           no

     waitFor test, ->
       errors = page.evaluate -> window._phantom_errors_
       if errors
-        console.log JSON.stringify errors, null, 2
-        console.log 'ERROR: One or more flows failed to complete'
+        console.log 'PHANTOM: *** ERROR *** One or more flows failed to complete: ' + JSON.stringify errors
         phantom.exit 1
       else
-        console.log 'Success! All flows ran to completion!'
+        console.log 'PHANTOM: Success! All flows ran to completion!'
         phantom.exit 0
   else
-    console.log 'ERROR: Unable to access network.'
+    console.log 'PHANTOM: *** ERROR *** Unable to access network.'
     phantom.exit 1


Save loop fix - check if relations have changed

--- lib/relations/relation.coffee
+++ lib/relations/relation.coffee
@@ -75,9 +75,11 @@
           for related_json in changes.removed
             do (related_json) => queue.defer (callback) => @_clearAndSaveRelatedBacklink(model, new @reverse_model_type(related_json), callback)

-        # add new
+        # add new, if they have changed
         for added_id in added_ids
           related_model = _.find(related_models, (test) -> test.id is added_id)
+          continue unless @reverse_relation._hasChanged(related_model) # related has not changed
+
           do (related_model) => queue.defer (callback) =>
             related_model.save {}, Utils.bbCallback (err, saved_model) =>
               cache.set(saved_model.id, saved_model) if not err and cache = @reverse_model_type.cache


Fix lint warning

Fix lint warning

--- lib/root-drag-and-drop.coffee
+++ lib/root-drag-and-drop.coffee
@@ -49,7 +49,7 @@
     catch error
       false

-  onDragEnter: (e) =>
+  onDragEnter: (e) ->
     e.stopPropagation()

   onDragLeave: (e) =>


Add parameterized tests for INSERT, UPDATE and DELETE query builders

--- test/postgres.test.coffee
+++ test/postgres.test.coffee
@@ -49,9 +49,14 @@
         assert.same @inst.toString(), 'INSERT INTO table (field) VALUES (1) RETURNING id'

     '>> into(table).set(field, 1).with(alias, table)':
-      beforeEach: -> @inst.into('table').set('field', 1).with('alias', squel.select().from('table'))
-      toString: ->
-        assert.same @inst.toString(), 'WITH alias AS (SELECT * FROM table) INSERT INTO table (field) VALUES (1)'
+      beforeEach: -> @inst.into('table').set('field', 1).with('alias', squel.select().from('table').where('field = ?', 2))
+      toString: ->
+        assert.same @inst.toString(), 'WITH alias AS (SELECT * FROM table WHERE (field = 2)) INSERT INTO table (field) VALUES (1)'
+      toParam: ->
+        assert.same @inst.toParam(), {
+          "text": 'WITH alias AS (SELECT * FROM table WHERE (field = $1)) INSERT INTO table (field) VALUES ($2)',
+          "values": [2, 1]
+        }

   'UPDATE builder':
     beforeEach: -> @upd = squel.update()
@@ -72,9 +77,14 @@
         assert.same @upd.toString(), 'UPDATE table SET field = 1 FROM table2'

     '>> table(table).set(field, 1).with(alias, table)':
-      beforeEach: -> @upd.table('table').set('field', 1).with('alias', squel.select().from('table'))
-      toString: ->
-        assert.same @upd.toString(), 'WITH alias AS (SELECT * FROM table) UPDATE table SET field = 1'
+      beforeEach: -> @upd.table('table').set('field', 1).with('alias', squel.select().from('table').where('field = ?', 2))
+      toString: ->
+        assert.same @upd.toString(), 'WITH alias AS (SELECT * FROM table WHERE (field = 2)) UPDATE table SET field = 1'
+      toParam: ->
+        assert.same @upd.toParam(), {
+          "text": 'WITH alias AS (SELECT * FROM table WHERE (field = $1)) UPDATE table SET field = $2',
+          "values": [2, 1]
+        }

   'DELETE builder':
     beforeEach: -> @del = squel.delete()
@@ -90,9 +100,14 @@
         assert.same @del.toString(), 'DELETE FROM table WHERE (field = 1) RETURNING field'

     '>> from(table).where(field = 1).with(alias, table)':
-      beforeEach: -> @del.from('table').where('field = 1').with('alias', squel.select().from('table'))
-      toString: ->
-        assert.same @del.toString(), 'WITH alias AS (SELECT * FROM table) DELETE FROM table WHERE (field = 1)'
+      beforeEach: -> @del.from('table').where('field = ?', 1).with('alias', squel.select().from('table').where('field = ?', 2))
+      toString: ->
+        assert.same @del.toString(), 'WITH alias AS (SELECT * FROM table WHERE (field = 2)) DELETE FROM table WHERE (field = 1)'
+      toParam: ->
+        assert.same @del.toParam(), {
+          "text": 'WITH alias AS (SELECT * FROM table WHERE (field = $1)) DELETE FROM table WHERE (field = $2)',
+          "values": [2, 1]
+        }

   'SELECT builder':
     beforeEach: ->


remove some logging.

remove some logging.

--- src/code/models/palette-manager.coffee
+++ src/code/models/palette-manager.coffee
@@ -48,7 +48,6 @@
           @props.linkManager.setImageMetadata node.image, node.metada

   onAddToPallete: (node) ->
-    @info "Adding to Palette: #{JSON.stringify(node)}"
     @_addToPallete(node)
     @_updateChanges()


add help commands test

add help commands test

--- test/scripts/hello.coffee
+++ test/scripts/hello.coffee
@@ -91,3 +91,9 @@
       it 'send "hello!"', ->
         assert @send.callCount is 1
         assert @send.firstCall.args[0] is 'hello!'
+
+  describe 'robot.helpCommands()', ->
+    it 'should be ["hubot XXX [<args>] - DESCRIPTION"]', ->
+      assert.deepEqual @robot.helpCommands(), [
+        "hubot XXX [<args>] - DESCRIPTION"
+      ]


Removes unneeded use of pem.getFingerprint.

--- src/middleware/tlsAuthentication.coffee
+++ src/middleware/tlsAuthentication.coffee
@@ -98,12 +98,8 @@
                   return deferred.reject err

                 if info.commonName is issuerCN
-                  pem.getFingerprint cert.data, (err, issuerFingerprint) ->
-                    if err?
-                      logger.error "Could not read issuer fingerprint while resolving in-chain authentication: #{err}"
-                      return deferred.resolve null
-                    promise = clientLookup issuerFingerprint.fingerprint, info.commonName, info.issuer.commonName
-                    promise.then (result) -> deferred.resolve result
+                  promise = clientLookup cert.fingerprint, info.commonName, info.issuer.commonName
+                  promise.then (result) -> deferred.resolve result
                 else
                   missedMatches++


index: Documentation fix.

index: Documentation fix.

--- client/index.coffee
+++ client/index.coffee
@@ -27,7 +27,7 @@
 #   @return [Array<any>] Return values of `f`.
 #
 # @method #with_K( v )
-#   Call f(o) for each object `o` with value `v` of key `K`.
+#   All items with value `v` of key `K`.
 #   @param [String] v Value of key `K` to look for keys of.
 #   @return [Array<any>] Objects with value `v` of key `K`.
 #


Support anchor fragments in URLs (e.g. /foo#bar)

--- client/views/voltagePage.coffee
+++ client/views/voltagePage.coffee
@@ -46,6 +46,21 @@

   editor.init(options).renderData()

+  # anchor fragment support
+  hash = document.location.hash.substr(1)
+  if hash && !Template.voltagePage.scrolled
+    scroller = ->
+      $("html, body").stop()
+
+    Meteor.setTimeout(->
+      elem = $('#' + hash)
+      if elem.length
+        scroller().scrollTop(elem.offset().top)
+        Template.voltagePage.scrolled = true
+    , 0)
+
+Template.voltagePage.destroyed = ->
+  delete Template.voltagePage.scrolled

 Template.voltageEditor.rendered = ->
   editor.renderEditor()


Remove description from registry entry for key-state mapping.

We do not need the registryEntry's decription in the content scripts, so
remove it (saving a marginal amount of memory and time).

--- background_scripts/commands.coffee
+++ background_scripts/commands.coffee
@@ -127,8 +127,9 @@
         else if index < registryEntry.keySequence.length - 1
           currentMapping = currentMapping[key] ?= {}
         else
-          delete registryEntry.keySequence # We don't need this any more.
-          currentMapping[key] = registryEntry
+          currentMapping[key] = extend {}, registryEntry
+          # We don't need these properties in the content scripts.
+          delete registryEntry[prop] for prop in ["keySequence", "description"]
     chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping

   # An ordered listing of all available commands, grouped by type. This is the order they will


build help link, pass to exercise

build help link, pass to exercise

--- src/components/task-step/exercise.cjsx
+++ src/components/task-step/exercise.cjsx
@@ -7,11 +7,16 @@
 {Exercise} = require 'openstax-react-components'
 StepFooter = require './step-footer'

+{ChapterSectionMixin} = require 'openstax-react-components'
+BrowseTheBook = require '../buttons/browse-the-book'
+
 module.exports = React.createClass
   displayName: 'ExerciseShell'
   propTypes:
     id: React.PropTypes.string.isRequired
     taskId: React.PropTypes.string.isRequired
+
+  mixins: [ ChapterSectionMixin ]

   updateFreeResponse: (freeResponse) -> TaskStepActions.updateTempFreeResponse(@props.id, freeResponse)

@@ -21,6 +26,21 @@
       .difference(['clickContinue'])
       .isEmpty()
       .value()
+
+  renderHelpLink: (sections) ->
+    sections = _.map sections, (section) =>
+      combined = @sectionFormat(section.chapter_section)
+      <BrowseTheBook unstyled key={combined} section={combined} onlyShowBrowsable={false}>
+        {combined} {section.title} <i className='fa fa-external-link' />
+      </BrowseTheBook>
+
+    helpLink =
+      <div key='task-help-links' className='task-help-links'>
+        Comes from&nbsp&nbsp{sections}
+      </div>
+
+    if sections.length > 0 then helpLink
+

   render: ->
     {id, taskId} = @props
@@ -49,6 +69,7 @@
         controlText={controlText}
         step={step}
         footer={<StepFooter/>}
+        helpLink={@renderHelpLink(step.related_content)}
         waitingText={waitingText}

         canTryAnother={TaskStepStore.canTryAnother(id, task)}


KataPokerHands: 4 tests added

KataPokerHands: 4 tests added

--- KataPokerHands_tests.coffee
+++ KataPokerHands_tests.coffee
@@ -1,3 +1,34 @@
 KataPokerHands = require('./KataPokerHands')
+assert         = require('assert')

-# tests …
+tests = [   # [input, expected output]
+
+            # - given tests -
+
+            ['Black: 2H 3D 5S 9C KD  White: 2C 3H 4S 8C AH',
+                'White wins. - with high card: Ace'],
+
+            ['Black: 2H 4S 4C 2D 4H  White: 2S 8S AS QS 3S',
+                'White wins. - with flash'],
+
+            ['Black: 2H 3D 5S 9C KD  White: 2C 3H 4S 8C KH',
+                'Black wins. - with high card: 9'],
+
+            ['Black: 2H 3D 5S 9C KD  White: 2D 3H 5C 9S KH',
+                'Tie.']
+]
+
+len = tests.length
+fails = 0
+
+console.log("Running #{len} tests…")
+
+for [input,expected_output] in tests
+    output = KataPokerHands.compare_hands(input)
+    try
+        assert.equal(output, expected_output)
+    catch AssertionError
+        console.log("'#{input}' -> '#{output}', expected '#{expected_output}'")
+        fails++
+
+console.log "#{len} tests done.\n#{((len-fails)/len*100)}% OK\n#{(fails/len*100)}% failed"


fix sendToUsers

fix sendToUsers

--- websockets/websockets.coffee
+++ websockets/websockets.coffee
@@ -59,13 +59,14 @@
     sendToTeam: (team, action, datas...) =>
         for id, socket of @teams[team.id]
             socket.emit action, datas...
-
+
     # send a message to all users, except the sender
-    sendToUsers: (user, action, datas...) =>
-        for usr in @users
-            if usr.id != user.id
-                for socket of usr
+    sendToUsers: (sender, action, datas...) =>
+        for user of @users
+            if parseInt(user) != parseInt(sender.id)
+                for id, socket of @users[user]
                     socket.emit action, datas...
+

 # export
 module.exports = new WebSockets


Rename config_keys_map to configKeysMap

Rename config_keys_map to configKeysMap

--- assets/scripts/app/utils/helpers.coffee
+++ assets/scripts/app/utils/helpers.coffee
@@ -1,6 +1,6 @@
 require 'config/emoij'

-config_keys_map = Travis.CONFIG_KEYS_MAP
+configKeysMap = Travis.configKeysMap
 config = ENV.config
 githubCommitUrl = Travis.Urls.githubCommit
 timeago = $.timeago
@@ -54,12 +54,12 @@
   (sha || '').substr(0, 7)

 formatConfig = (config) ->
-  config = only config, Object.keys(config_keys_map)
+  config = only config, Object.keys(configKeysMap)
   values = mapObject config, (value, key) ->
     value = (if value && value.join then value.join(', ') else value) || ''
     if key == 'rvm' && "#{value}".match(/^\d+$/)
       value = "#{value}.0"
-    '%@: %@'.fmt config_keys_map[key], value
+    '%@: %@'.fmt configKeysMap[key], value
   if values.length == 0 then '-' else values.join(', ')

 formatMessage = (message, options) ->
@@ -154,7 +154,7 @@

 configKeys = (config) ->
   return [] unless config
-  intersect(Object.keys(config), Object.keys(config_keys_map))
+  intersect(Object.keys(config), Object.keys(configKeysMap))

 pathFrom = (url) ->
   (url || '').split('/').pop()


add some signature enforcers to the kernel accquire method

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -154,6 +154,10 @@
     # it returns an instance to the invoker. If not, however, it
     # throws an assertion error about being unable to accquire.
     accquire: (kinded, silent=no) ->
+        usage = "method has been used incrorrectly"
+        sign = "should be called with the class arg"
+        assert (arguments.length or NaN) >= 1, usage
+        assert _.isObject(kinded or undefined), sign
         assert ident = try kinded.identify() or null
         error = "could not find a %s in the registry"
         noKinded = "the supplied arg has to be class"


Minor fix in the kernel bootstrap

Minor fix in the kernel bootstrap

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -102,7 +102,7 @@
     # that are necessary, do the configuration on the kernel, then
     # boot it up, using the hostname and port parameters from config.
     # Please use this static method instead of manually launching up.
-    @bootstrap: -> new Kernel ->
+    @bootstrap: -> new this ->
         @setupRoutableServices()
         @setupConnectPipeline()
         @setupListeningServers()


minor code reordering

minor code reordering

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -107,21 +107,6 @@
         assert _.isString(identica), noIdentica
         assert @$identica = identica.toString()
         return @emit? "identica", arguments...
-
-    # Create and wire in an appropriate Connext middleware that will
-    # serve the specified directory as the directory with a static
-    # content. That is, it will expose it to the world (not list it).
-    # The serving aspects can be configured via a passed in options.
-    serveStaticDirectory: (directory, options={}) ->
-        assert cwd = try process.cwd().toString()
-        solved = try paths.relative cwd, directory
-        serving = "Serving %s as static assets dir"
-        notExist = "The assets dir %s does not exist"
-        fail = -> logger.warn notExist, solved.underline
-        return fail() unless fs.existsSync directory
-        middleware = connect.static directory, options
-        logger.info serving.cyan, solved.underline
-        @connect.use middleware; return this

     # An embedded system for adding ad-hoc configuration routines.
     # Supply the reasoning and the routine and this method will add
@@ -470,6 +455,21 @@
         @serveStaticDirectory established, Object()
         @serveStaticDirectory pub(); return this

+    # Create and wire in an appropriate Connext middleware that will
+    # serve the specified directory as the directory with a static
+    # content. That is, it will expose it to the world (not list it).
+    # The serving aspects can be configured via a passed in options.
+    serveStaticDirectory: (directory, options={}) ->
+        assert cwd = try process.cwd().toString()
+        solved = try paths.relative cwd, directory
+        serving = "Serving %s as static assets dir"
+        notExist = "The assets dir %s does not exist"
+        fail = -> logger.warn notExist, solved.underline
+        return fail() unless fs.existsSync directory
+        middleware = connect.static directory, options
+        logger.info serving.cyan, solved.underline
+        @connect.use middleware; return this
+
     # This method sets up the necessary internal toolkits, such as
     # the determined scope and the router, which is then are wired
     # in with the located and instantiated services. Please refer


Minor changes to the zombie

Minor changes to the zombie

--- library/nucleus/zombie.coffee
+++ library/nucleus/zombie.coffee
@@ -87,7 +87,8 @@
         assert upstream = @instance.upstreamAsync
         assert upstream = upstream.bind @instance
         assert singleton = upstream "singleton", ->
-            callback? @instance, kernel
+            return unless _.isFunction callback
+            callback.call this, @instance, kernel
         singleton kernel, @instance; @instance

     # An important method whose responsibility is to create a new
@@ -103,5 +104,6 @@
         assert upstream = service.upstreamAsync
         assert upstream = upstream.bind service
         assert instance = upstream "instance", ->
-            callback? service, kernel
+            return unless _.isFunction callback
+            callback.call this, service, kernel
         instance kernel, service; service


Instrument the element with correct EE invocations

--- library/visualize/tree.coffee
+++ library/visualize/tree.coffee
@@ -89,7 +89,7 @@
         return @$name unless name?
         invalid = "Name is not a string"
         throw new Error invalid unless correct
-        @emit("name", this, @$name, name)
+        @emit "name", this, @$name, name
         @$name = name.toString()

     # Either get or set the identification prefix string for this
@@ -101,7 +101,7 @@
         return @$prefix unless prefix?
         invalid = "Prefix is not a string"
         throw new Error invalid unless correct
-        @emit("prefix", this, @$prefix, prefix)
+        @emit "prefix", this, @$prefix, prefix
         @$prefix = prefix.toString()

     # Resolve the specific node by its tag. The method is recursive
@@ -130,6 +130,7 @@
         throw new Error invalid unless correct
         throw new Error abstract if node.abstract()
         index = _.findIndex @children or [], isEqual
+        @emit "remove", this, node, index if index?
         (@children ?= []).splice index, 1 if index?

     # Prepend the supplied node to the array of children nodes of
@@ -144,6 +145,7 @@
         throw new Error invalid unless correct
         throw new Error abstract if node.abstract()
         exists = _.find @children or [], isEqual
+        @emit "prepend", this, node unless exists?
         (@children ?= []).unshift node unless exists

     # Append the supplied node to the array of children nodes of
@@ -158,4 +160,5 @@
         throw new Error invalid unless correct
         throw new Error abstract if node.abstract()
         exists = _.find @children or [], isEqual
+        @emit "append", this, node unless exists?
         (@children ?= []).push node unless exists


Hide pagination by default closes #355

Hide pagination by default closes #355

--- wzk/ui/grid/Paginator.coffee
+++ wzk/ui/grid/Paginator.coffee
@@ -6,6 +6,7 @@
 goog.require 'goog.style'
 goog.require 'wzk.num'

+
 class wzk.ui.grid.Paginator extends wzk.ui.Component

   ###*
@@ -13,6 +14,7 @@
   ###
   @DATA:
     BASE: 'base'
+    FORCE_DISPLAY: 'forceDisplay'

   ###*
     @enum {string}
@@ -51,6 +53,7 @@
     @switcher = null
     @bases = null
     @defBases = [10, 25, 50, 100, 500, 1000]
+    @forceDisplay = false

   ###*
     @return {number}
@@ -139,6 +142,7 @@
       @dom.replaceNode newClone, oldClone
       newClones.push newClone
     @clones = newClones
+    @show true

   ###*
     @protected
@@ -161,12 +165,14 @@
     @override
   ###
   decorateInternal: (el) ->
+    @forceDisplay = goog.dom.dataset.get(el, wzk.ui.grid.Paginator.DATA.FORCE_DISPLAY) is 'true'
     unless @bases
       switcherEl = el.querySelector '.' + wzk.ui.grid.PaginatorRenderer.CLASSES.BASE_SWITCHER
       @parseBases switcherEl

     @renderer.decorate @, el
     @setElementInternal el
+    @showInternal false

   ###*
     @protected
@@ -252,8 +258,24 @@
     @listeners = []

   ###*
+    @protected
+    @return {boolean}
+  ###
+  canHide: ->
+    @pageCount < 2 and not @forceDisplay
+
+  ###*
     @param {boolean} visible
-  ###
-  show: (visible) ->
+    @param {boolean=} force default is false
+  ###
+  show: (visible, force = false) ->
+    return if not force and @canHide()
+    @showInternal visible
+
+  ###*
+    @protected
+    @param {boolean} visible
+  ###
+  showInternal: (visible) ->
     visibility = if visible then 'visible' else 'hidden'
-    goog.style.setStyle el, 'visibility', visibility for el in [@getElement()].concat @clones
+    goog.style.setStyle(el, 'visibility', visibility) for el in [@getElement()].concat @clones


Remove .java from model download endpoint

--- src/ext/components/model-output.coffee
+++ src/ext/components/model-output.coffee
@@ -602,7 +602,7 @@
         _pojoPreview "<pre>#{Flow.Util.highlight result, 'java'}</pre>"

   downloadPojo = ->
-    window.open "/3/Models.java/#{encodeURIComponent _model.model_id.name}.java", '_blank'
+    window.open "/3/Models.java/#{encodeURIComponent _model.model_id.name}", '_blank'

   exportModel = ->
     _.insertAndExecuteCell 'cs', "exportModel #{stringify _model.model_id.name}"


seattle content

seattle content

Former-commit-id: a15ffef4abb315dcb2bab27b5dfe9645542a5a6d [formerly e279f4d2004c064a039c8e34c09b5c7abc5f5574] [formerly daff31b2054228e256d22af58258313b83d1acfc [formerly 594b76c5f7a12d69eb414f13e00f0cb94afe6adf]] [formerly d938c761b2a98fc3388f365e189aed31fd8a2671 [formerly c477b4b92218de235084609f5f30b29597f23eff] [formerly ffff2daf7092ec48c9af5b8adf67e52399ea77f8 [formerly ffff2daf7092ec48c9af5b8adf67e52399ea77f8 [formerly 01700a4476aaf30c323b4fae73f019a1277f0f9c]]]]
Former-commit-id: c50198dcd47b8e13e60752e81c09ecca0c944e02 [formerly a4e4d85356dbcb8fd4faf5337c3c1285aa5ed012] [formerly 6c80572731d42b275053b316987072ca19c24d86 [formerly 2b97c1914bf5fcb66e48993f20c38f10d1876d56]]
Former-commit-id: ec9ea87039156a45920c6122cbc730a30b09bea6 [formerly 0e4fe43d86eef7486cfb94f8f5ca8cba4e260ce0]
Former-commit-id: 5fa2ee5345211b895a90eb79f08b27bac8a6e021
Former-commit-id: 206e321741c29bee73dbe29a50b5dd9a711279d5

--- @client/customizations.coffee
+++ @client/customizations.coffee
@@ -643,35 +643,95 @@
               width: SIMPLE_HOMEPAGE_WIDTH()
               margin: 'auto'

+
             DIV
               style:
                 color: seattle2035_pink
                 fontSize: 48
                 fontWeight: 600
-
-              'How should Seattle grow? You tell us!'
+                margin: '10px 0'
+
+              'Seattle 2035 Draft Plan Conversation – a place to consider Big Changes.'

             DIV
               style:
-                fontSize: 24
-
-              "What do you think of the draft "
-              A
-                target: '_blank'
-                href: 'http://www.seattle.gov/dpd/cs/groups/pan/@pan/documents/web_informational/p2273574.pdf'
+                fontSize: 22
+
+
+              P
                 style:
-                  textDecoration: 'underline'
-                "Seattle 2035 comprehensive plan"
-              "? "
-
-              "We've listed pivotal proposals from the draft below for your consideration. Email us at "
-              A
-                href: "mailto:[email]@seattle.gov"
+                  marginTop: 20
+
+                """
+                Seattle is one of the fastest growing cities in America, and more growth is
+                coming– 120,000 more people and 115,000 more jobs by 2035. The time to plan
+                for how and where that growth occurs is now. After months of working with city
+                departments and the Seattle Planning Commission, we are pleased to present
+                a Draft Plan for public discussion. The full Draft Plan can be found """
+                A
+                  target: '_blank'
+                  href: 'http://www.seattle.gov/dpd/cs/groups/pan/@pan/documents/web_informational/p2273574.pdf'
+                  style:
+                    textDecoration: 'underline'
+
+                  'here'
+                "."
+
+              P
                 style:
-                  textDecoration: 'underline'
-
-                "[email]@seattle.gov"
-              " to suggest other parts of the draft plan to post here."
+                  marginTop: 20
+
+                """
+                The Draft Plan contains hundreds of goals and policies. Many are not much
+                different than our current plan. Over the last year, people have suggested
+                bigger changes in the direction of planning for our city. The Draft Plan is
+                an opportunity to discuss the pros and cons of these directions, and
+                determine if they make sense for Seattle over the coming twenty years."""
+
+              P
+                style:
+                  marginTop: 20
+
+                """
+                Is there another Big Change you would like to add to the Draft Plan
+                Conversation? Email us at """
+                A
+                  href: "mailto:[email]@seattle.gov"
+                  style:
+                    textDecoration: 'underline'
+
+                  "[email]@seattle.gov"
+
+
+            # DIV
+            #   style:
+            #     color: seattle2035_pink
+            #     fontSize: 48
+            #     fontWeight: 600
+
+            #   'How should Seattle grow? You tell us!'
+
+            # DIV
+            #   style:
+            #     fontSize: 24
+
+            #   "What do you think of the draft "
+            #   A
+            #     target: '_blank'
+            #     href: 'http://www.seattle.gov/dpd/cs/groups/pan/@pan/documents/web_informational/p2273574.pdf'
+            #     style:
+            #       textDecoration: 'underline'
+            #     "Seattle 2035 comprehensive plan"
+            #   "? "
+
+            #   "We've listed pivotal proposals from the draft below for your consideration. Email us at "
+            #   A
+            #     href: "mailto:[email]@seattle.gov"
+            #     style:
+            #       textDecoration: 'underline'
+
+            #     "[email]@seattle.gov"
+            #   " to suggest other parts of the draft plan to post here."

             DIV
               style:


foodcorps update

foodcorps update

Former-commit-id: 773bf5595901d6a930b04d7e5c3509361449d0b7 [formerly 6cd2f2dc27c3cfde746cb47178acc504cefaea85] [formerly c923b038af77c0359e7cb45edc0c9851694d2ded [formerly dd8f6a58b06cc57a9542db87c960e7c4da0a2112]] [formerly e4a7ad590e27680046e9d573807922cc155aa552 [formerly 5b5c8f5398916723527e3abed7c4ed952885d88f] [formerly 246ea843c004cccae89bebe1d4571bee44ac4cb4 [formerly 246ea843c004cccae89bebe1d4571bee44ac4cb4 [formerly 2fc4597cff91a71332fb95ac32295b33b9263485]]]]
Former-commit-id: f41a420d0e4eda6b7fe4454ed11232cfbe19ce0f [formerly efa1b40c71cd68a6d4f22c3b8963362919c7e069] [formerly e8b991c521f238603184a38232f1bca68d93b35d [formerly 7b87a7f1d2201ad4bcd7a2d8d8ce95da610cf812]]
Former-commit-id: 894d4042e1c668442d8fb936935dab97d48db351 [formerly 35016be34e87513c9cabe2c724ca1a5712874440]
Former-commit-id: 6960273b40f0aa721fed38be36e240624c531483
Former-commit-id: bceb71166803d039621444e5b779c709749e1156

--- @client/customizations.coffee
+++ @client/customizations.coffee
@@ -139,7 +139,7 @@
   pros: 'strengths'
   con: 'weakness'
   cons: 'weaknesses'
-  your_header: "--valences-- you observe"
+  your_header: "--valences--"
   other_header: "--valences-- observed"
   top_header: "Foremost --valences--"

@@ -913,7 +913,7 @@

 customizations['foodcorps'] =
   point_labels : strengths_weaknesses
-  slider_pole_labels : yes_no
+  slider_pole_labels : ready_not_ready
   show_slider_feedback: false

   HomepageHeader : FoodcorpsHeader
@@ -1926,7 +1926,7 @@
           DIV
             style:
               backgroundColor: LVG_green
-              paddingTop: 20
+              paddingTop: 5

   Footer : ReactiveComponent


It was fixed a bug where the class 'cui-input-has-check-input' were not added when regex were used in CUI.Input class. See #47338

--- src/elements/Input/Input.coffee
+++ src/elements/Input/Input.coffee
@@ -25,11 +25,13 @@
 			@addClass("cui-input-required")

 		if @_checkInput
-			@addClass("cui-input-has-check-input")
 			@__checkInput = @_checkInput

 		if @_prevent_invalid_input
 			@addClass("cui-input-has-prevent-invalid-input")
+
+		if @__checkInput
+			@addClass("cui-input-has-check-input")

 		@__inputHints = {}
 		@__inputHintTexts = {}


fix https://github.com/sagemathinc/smc/issues/1935 -- "keyboard shortcuts firefox"

--- src/smc-webapp/jupyter/top-menubar.cjsx
+++ src/smc-webapp/jupyter/top-menubar.cjsx
@@ -230,7 +230,7 @@
                 disabled = {disabled}
                 >
                 <span style={style}>
-                    {display ? obj.m ? name} {s}
+                    {s} {display ? obj.m ? name}   {# shortcut must be first! -- https://github.com/sagemathinc/smc/issues/1935 }
                 </span>
             </MenuItem>
         else


return 'a guest' by default for guest names

--- client/Framework/core/utils.coffee
+++ client/Framework/core/utils.coffee
@@ -310,12 +310,12 @@
   getFullnameFromAccount:(account, justName=no)->
     account or= KD.whoami()
     if account.type is 'unregistered'
-      name = account.profile.nickname.capitalize()
+      name = "a guest"
     else if justName
       name = account.profile.firstName
     else
       name = "#{account.profile.firstName} #{account.profile.lastName}"
-    return Encoder.htmlEncode name
+    return Encoder.htmlEncode name or 'a Koding user'

   getNameFromFullname :(fullname)->
     fullname.split(' ')[0]


Add cache to store grid's triangles

Add cache to store grid's triangles

--- src/geom/triangular_grid.coffee
+++ src/geom/triangular_grid.coffee
@@ -4,6 +4,7 @@
   constructor: (@triangleSize=10) ->
     @halfSize = @triangleSize / 2
     @rowHeight = Math.sqrt(3) / 2 * @triangleSize
+    @trianglesCache = {}

   gridSizeForScreen: (width, height) ->
     width: Math.ceil(width / @triangleSize)
@@ -146,10 +147,13 @@
   compareCoordinates: (a,b) -> a.x is b.x and a.y is b.y

   triangleAtPosition: (pos) ->
+    key = "#{pos.x};#{pos.y}"
+    return @trianglesCache[key] if @trianglesCache[key]?
+
     [a,b,c] = []
     yStep = pos.y % 4
     yStep = 3 + yStep if yStep < 0
-
+
     switch yStep
       when 0
         xRef = pos.x * @triangleSize
@@ -176,8 +180,4 @@
         b = new agt.geom.Point(xRef + @halfSize, yRef + @rowHeight)
         c = new agt.geom.Point(xRef - @halfSize, yRef + @rowHeight)

-    try
-      new agt.geom.Triangle(a,b,c)
-    catch e
-      console.log pos
-      throw e
+    @trianglesCache[key] = new agt.geom.Triangle(a,b,c)


Tune sunrise timing

Tune sunrise timing

--- src/lazerscripts/stage1/sunrise.coffee
+++ src/lazerscripts/stage1/sunrise.coffee
@@ -100,7 +100,7 @@
     speed = @options.speed ? 1
     @options.skipTo ?= 0

-    colorDuration = (60000 / speed)
+    colorDuration = (120000 / speed)
     @sequence(
       @setLocation x: .75, y: .61
       @parallel(
@@ -112,7 +112,7 @@
         )
         @movePath [
           [.71, .45]
-        ], rotate: no, skip: @options.skipTo, speed: speed * 4
+        ], rotate: no, skip: @options.skipTo, speed: speed * 2
       )
     )


client: home: subclassed views used

client: home: subclassed views used

--- client/home/lib/homeappview.coffee
+++ client/home/lib/homeappview.coffee
@@ -1,7 +1,8 @@
 kd                = require 'kd'
 globals           = require 'globals'
 HomeAppAvatarView = require './commons/homeappavatarview'
-
+# HomeTabView       = require './commons/hometabview'
+HomeTabHandle     = require './commons/hometabhandle'

 module.exports = class HomeAppView extends kd.ModalView

@@ -9,7 +10,7 @@

     options.testPath         = 'dashboard'
     options.checkRoles      ?= yes
-    options.cssClass       or= kd.utils.curry '.HomeAppView', options.cssClass
+    options.cssClass       or= kd.utils.curry 'HomeAppView', options.cssClass
     options.width           ?= 1000
     options.height          ?= '90%'
     options.overlay         ?= yes
@@ -17,7 +18,6 @@
     super options, data

     @addSubView @nav = new kd.TabHandleContainer { cssClass: 'HomeAppView-Nav' }
-

     @avatarArea = new HomeAppAvatarView
     @title      = new kd.CustomHTMLView
@@ -31,6 +31,7 @@
       maxHandleWidth     : 245
       cssClass           : 'HomeAppView-TabView'
       tabHandleContainer : @nav
+      tabHandleClass     : HomeTabHandle
     , data

     @tabs.unsetClass 'kdscrollview'


add per-user compilation in client

add per-user compilation in client

enabled only when query string includes isolated=true

--- services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -5,6 +5,8 @@
 ], (App, LogParser, BibLogParser) ->
 	App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->

+		# enable per-user containers if querystring includes isolated=true
+		perUserCompile = window.location?.search?.match(/isolated=true/)? or undefined
 		autoCompile = true

 		# pdf.view = uncompiled | pdf | errors
@@ -28,13 +30,16 @@

 		sendCompileRequest = (options = {}) ->
 			url = "/project/#{$scope.project_id}/compile"
+			params = {}
 			if options.isAutoCompile
-				url += "?auto_compile=true"
+				params["auto_compile"]=true
+			if perUserCompile # send ?isolated=true for per-user compiles
+				params["isolated"] = true
 			return $http.post url, {
 				rootDoc_id: options.rootDocOverride_id or null
 				draft: $scope.draft
 				_csrf: window.csrfToken
-			}
+			}, {params: params}

 		parseCompileResponse = (response) ->

@@ -241,6 +246,7 @@
 				method: "DELETE"
 				params:
 					clsiserverid:ide.clsiServerId
+					isolated: perUserCompile
 				headers:
 					"X-Csrf-Token": window.csrfToken
 			}
@@ -324,6 +330,7 @@
 							line: row + 1
 							column: column
 							clsiserverid:ide.clsiServerId
+							isolated: perUserCompile
 						}
 					})
 					.success (data) ->
@@ -352,6 +359,7 @@
 							h: position.offset.left.toFixed(2)
 							v: position.offset.top.toFixed(2)
 							clsiserverid:ide.clsiServerId
+							isolated: perUserCompile
 						}
 					})
 					.success (data) ->


S70911: fix TreeGridAppSpec

S70911: fix TreeGridAppSpec

--- test/spec/treegrid/TreeGridAppSpec.coffee
+++ test/spec/treegrid/TreeGridAppSpec.coffee
@@ -27,7 +27,9 @@
     expect(Ext.isDefined(treeGridApp)).toBeTruthy()

   it 'should use the row expansion plugin', ->
-    appCfg = _.extend @getTreeGridAppConfig(true)
+    appCfg = _.extend @getTreeGridAppConfig(true),
+      defaultSettings:
+        modelNames: 'hierarchicalrequirement,defect'
     treeGridApp = Ext.create 'Rally.apps.treegrid.TreeGridApp', appCfg
     plugins = treeGridApp.down('#gridBoard').gridConfig.plugins


only move to the add_containers subpage if we're on the choose_plan page

--- js/inner/billing.coffee
+++ js/inner/billing.coffee
@@ -185,8 +185,9 @@
       data: JSON.stringify(changed_attributes)
       success: (data) =>
         @loadPlanData(data)
-        $('#confirmForm').modal('hide') # TODO: eww
-        VM.org().subpage('add-containers')
+        if VM.org().subpage() is 'choose_plan'
+          $('#confirmForm').modal('hide') # TODO: eww
+          VM.org().subpage('add_containers')

   newPlan: (plan, event) =>


making expanding urls optional

making expanding urls optional

--- lib/bot.coffee
+++ lib/bot.coffee
@@ -330,6 +330,8 @@
 checkMessage = (bot, message) ->
   return if !message.text || message.subtype == "bot_message"

+  return if process.env.LOOKER_SLACKBOT_EXPAND_URLS == "false"
+
   # URL Expansion
   for url in getUrls(message.text).map((url) -> url.replace("%3E", ""))


allow hidden commands

allow hidden commands

closes #11

--- lib/bot.coffee
+++ lib/bot.coffee
@@ -73,6 +73,8 @@
             looker: looker
             category: category

+          command.hidden = category.toLowerCase().indexOf("[hidden]") != -1 || command.name.indexOf("[hidden]") != -1
+
           command.helptext = (dashboard.filters || "").map((f) ->
             "<#{f.title.toLowerCase()}>"
           ).join(", ")
@@ -192,17 +194,19 @@
       for groupName, groupCommmands of groups
         groupText = ""
         for command in _.sortBy(_.values(groupCommmands), "name")
-          groupText += "• *<#{command.looker.url}/dashboards/#{command.dashboard.id}|#{command.name}>* #{command.helptext}"
-          if command.description
-            groupText += " — _#{command.description}_"
-          groupText += "\n"
-
-        helpAttachments.push(
-          title: groupName
-          text: groupText
-          color: "#64518A"
-          mrkdwn_in: ["text"]
-        )
+          unless command.hidden
+            groupText += "• *<#{command.looker.url}/dashboards/#{command.dashboard.id}|#{command.name}>* #{command.helptext}"
+            if command.description
+              groupText += " — _#{command.description}_"
+            groupText += "\n"
+
+        if groupText
+          helpAttachments.push(
+            title: groupName
+            text: groupText
+            color: "#64518A"
+            mrkdwn_in: ["text"]
+          )

       defaultText = """
       • *find* <look search term> — _Shows the top five Looks matching the search._


fix help message

fix help message

--- lib/clone.iced
+++ lib/clone.iced
@@ -15,9 +15,9 @@
               Clone a RethinkDB database on the same host or between remote hosts.

               Usage:
-                thinker [options]
-                thinker --sh host[:port] --th host[:port] --sd dbName --td newDbName
-                thinker -h | --help
+                thinker clone [options]
+                thinker clone --sh host[:port] --th host[:port] --sd dbName --td newDbName
+                thinker clone -h | --help

               Options:
                 --sh, --sHost=<host[:port]>     Source host, defaults to 'localhost:21015'


fix unit tests

fix unit tests

--- test/unit/coffee/DocArchiveManager.coffee
+++ test/unit/coffee/DocArchiveManager.coffee
@@ -7,6 +7,7 @@
 SandboxedModule = require('sandboxed-module')
 ObjectId = require("mongojs").ObjectId
 Errors = require "../../../app/js/Errors"
+crypto = require("crypto")

 describe "DocArchiveManager", ->

@@ -58,12 +59,12 @@

 		@error = "my errror"
 		@project_id = ObjectId().toString()
-		@stubbedError = new Errors.NotFoundError("blew up")
+		@stubbedError = new Errors.NotFoundError("Error in S3 request")

 	describe "archiveDoc", ->

 		it "should use correct options", (done)->
-			@request.put = sinon.stub().callsArgWith(1,  null, statusCode:200)
+			@request.put = sinon.stub().callsArgWith(1,  null, {statusCode:200,headers:{etag:""}})
 			@requires["request"] = @request
 			@DocArchiveManager = SandboxedModule.require modulePath, requires: @requires

@@ -75,13 +76,23 @@
 				opts.uri.should.equal "https://#{@settings.filestore.stores.user_files}.s3.amazonaws.com/#{@project_id}/#{@docs[0]._id}"
 				done()

-		it "should return the error", (done)->
-			@request.put = sinon.stub().callsArgWith(1, @error, {})
+		it "should return no md5 error", (done)->
+			@md5 = crypto.createHash("md5").update(JSON.stringify(@docs[0].lines)).digest("hex")
+			@request.put = sinon.stub().callsArgWith(1,  null, {statusCode:200,headers:{etag:@md5}})
 			@requires["request"] = @request
 			@DocArchiveManager = SandboxedModule.require modulePath, requires: @requires

 			@DocArchiveManager.archiveDoc @project_id, @docs[0], (err)=>
-				err.should.equal @error
+				should.not.exist err
+				done()
+
+		it "should return the error", (done)->
+			@request.put = sinon.stub().callsArgWith(1, @stubbedError, {statusCode:400,headers:{etag:""}})
+			@requires["request"] = @request
+			@DocArchiveManager = SandboxedModule.require modulePath, requires: @requires
+
+			@DocArchiveManager.archiveDoc @project_id, @docs[0], (err)=>
+				should.exist err
 				done()

 	describe "unarchiveDoc", ->
@@ -100,12 +111,12 @@
 				done()

 		it "should return the error", (done)->
-			@request.get = sinon.stub().callsArgWith(1, @error, {}, {})
+			@request.get = sinon.stub().callsArgWith(1, @stubbedError, {}, {})
 			@requires["request"] = @request
 			@DocArchiveManager = SandboxedModule.require modulePath, requires: @requires

 			@DocArchiveManager.unarchiveDoc @project_id, @docs[0], (err)=>
-				err.should.equal @error
+				should.exist err
 				done()

 	describe "archiveAllDocs", ->


Add a unit test for the analytics events sent from project-creation

--- services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
+++ services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
@@ -52,7 +52,7 @@

 		@Settings = apis: { project_history: {} }

-		@AnalyticsManger = recordEvent: sinon.stub()
+		@AnalyticsManager = recordEvent: sinon.stub()

 		@handler = SandboxedModule.require modulePath, requires:
 			'../../models/User': User:@User
@@ -62,7 +62,7 @@
 			'./ProjectEntityUpdateHandler':@ProjectEntityUpdateHandler
 			"./ProjectDetailsHandler":@ProjectDetailsHandler
 			"settings-sharelatex": @Settings
-			"../Analytics/AnalyticsManager": @AnalyticsManger
+			"../Analytics/AnalyticsManager": @AnalyticsManager
 			'logger-sharelatex': {log:->}
 			"metrics-sharelatex": {
 				inc: ()->,
@@ -130,6 +130,23 @@
 				@handler.createBlankProject ownerId, projectName, (err, project)=>
 					expect(project.overleaf.history.display).to.equal true
 					done()
+
+			it "should send a project-created event to analytics", (done) ->
+				@handler.createBlankProject ownerId, projectName, (err, project) =>
+					expect(@AnalyticsManager.recordEvent.callCount).to.equal 1
+					expect(
+						@AnalyticsManager.recordEvent.calledWith(ownerId, 'project-created')
+					).to.equal true
+					done()
+
+			it "should send a project-imported event when importing a project", (done) ->
+				@handler.createBlankProject ownerId, projectName, 1234, (err, project) =>
+					expect(@AnalyticsManager.recordEvent.callCount).to.equal 1
+					expect(
+						@AnalyticsManager.recordEvent.calledWith(ownerId, 'project-imported')
+					).to.equal true
+					done()
+

 		describe "with an error", ->
 			beforeEach ->