remove trial more refs

remove trial more refs

--- angular/core/components/group_page/group_page_controller.coffee
+++ angular/core/components/group_page/group_page_controller.coffee
@@ -1,4 +1,4 @@
-angular.module('loomioApp').controller 'GroupPageController', ($rootScope, $location, $routeParams, Records, CurrentUser, MessageChannelService, AbilityService, AppConfig, LmoUrlService, PaginationService, ModalService, SubscriptionSuccessModal, GroupWelcomeModal, LegacyTrialExpiredModal) ->
+angular.module('loomioApp').controller 'GroupPageController', ($rootScope, $location, $routeParams, Records, CurrentUser, MessageChannelService, AbilityService, AppConfig, LmoUrlService, PaginationService, ModalService, SubscriptionSuccessModal, GroupWelcomeModal) ->
   $rootScope.$broadcast 'currentComponent', {page: 'groupPage'}

   # allow for chargify reference, which comes back #{groupKey}|#{timestamp}
@@ -7,12 +7,10 @@
     @group = group

     if AbilityService.isLoggedIn()
-      $rootScope.$broadcast 'trialIsOverdue', @group if @group.trialIsOverdue()
       MessageChannelService.subscribeToGroup(@group)
       Records.drafts.fetchFor(@group)
       @handleSubscriptionSuccess()
       @handleWelcomeModal()
-      LegacyTrialExpiredModal.showIfAppropriate(@group, CurrentUser)

     @pageWindow = PaginationService.windowFor
       current:  parseInt($location.search().from or 0)
@@ -60,13 +58,12 @@
     @group.isParent() and
     AbilityService.isCreatorOf(@group) and
     @group.noInvitationsSent() and
-    !@group.trialIsOverdue() and
     !@subscriptionSuccess and
-    !GroupWelcomeModal.shownToGroup[@group.id]
+    GroupWelcomeModal.shownToGroup[@group.id]

   @handleWelcomeModal = =>
     if @showWelcomeModel()
       GroupWelcomeModal.shownToGroup[@group.id] = true
-    #  ModalService.open GroupWelcomeModal
+      ModalService.open GroupWelcomeModal

   return


hide blocks when drop

hide blocks when drop

--- app/assets/javascripts/chaskiq/manage/campaign_wizard.js.coffee
+++ app/assets/javascripts/chaskiq/manage/campaign_wizard.js.coffee
@@ -84,10 +84,14 @@
     ev.preventDefault()

   setDraggedEl: (e)->
-    #debugger
     crt = $(e.currentTarget).parents(".tpl-block")[0].cloneNode(true) #.clone()
-    crt.style.backgroundColor = "red";
-    crt.style.position = "absolute"; crt.style.top = "0px"; crt.style.right = "0px";
+    crt.style.backgroundColor = "white";
+    crt.style.position = "absolute";
+    crt.style.border = "1px solid #666";
+    crt.style.boxShadow = "1px 2px 2px #adadad"
+    crt.style.top = "0px";
+    crt.style.right = "0px";
+    crt.style.opacity = 0.9;
     document.body.appendChild(crt);
     e.dataTransfer = e.originalEvent.dataTransfer;
     e.dataTransfer.setDragImage(crt, 0, 0);
@@ -106,7 +110,6 @@
     _.each $('.tpl-container'), (n)->
       $(n).find(".legend").remove()

-
   drag: (ev)->
     #console.log(ev.currentTarget)
     @dragged = $(ev.currentTarget)
@@ -114,6 +117,14 @@
   allowDrop: (ev)->
     #console.log("allos drop")
     ev.preventDefault()
+
+  displayEmptyBlocks: ->
+    _.each $('.tpl-container'), (container)->
+      empty_message = $(container).find('.mojoMcContainerEmptyMessage')
+      if $(container).find(".tpl-block").length > 0
+        empty_message.hide()
+      else
+        empty_message.show()

   drop: (ev)->
     ev.preventDefault()
@@ -124,7 +135,6 @@
     #ev.target.appendChild(data);
     container = $(ev.currentTarget)

-    #debugger
     if @dragged.attr('data-block')
       tmpl = @handleBlock @dragged.data('block')
       @dropBlock(container, tmpl)
@@ -136,13 +146,16 @@

     @releaseBeforeItem()

+    @displayEmptyBlocks()
+
   dropBlock: (container, tmpl)->
     #drop on before item or in tpl-container
     if $(".tpl-block.dojoDndItemBefore").length > 0
       container.find(".tpl-block.dojoDndItemBefore").before(tmpl)
+      container.find('.mojoMcContainerEmptyMessage').hide()
     else
       container.find(".tpl-container").append(tmpl)
-      container.find('.mojoMcContainerEmptyMessage').hide()
+      #container.find('.mojoMcContainerEmptyMessage').hide()

   releaseBeforeItem: ()->
     $(".tpl-block").removeClass("dojoDndItemBefore")


another thing that worked locally

another thing that worked locally

--- app/assets/javascripts/staffplans/show/models/project.js.coffee
+++ app/assets/javascripts/staffplans/show/models/project.js.coffee
@@ -5,7 +5,7 @@
       model: @
       id: "project_#{@id}"

-  urlRoot: "/views"
+  urlRoot: "/projects"

 class ProjectList extends Backbone.Collection
   model: Project


Init single fileupload function

Init single fileupload function

--- app/assets/javascripts/superb_text_constructor/blocks.js.coffee
+++ app/assets/javascripts/superb_text_constructor/blocks.js.coffee
@@ -1,3 +1,21 @@
+window.initSingleFileupload = ->
+  $('.single-fileupload').each ->
+    $(this).fileupload
+      type: 'PATCH'
+      dropZone: $(this).closest('.dropzone')
+      add: (e, data) ->
+        data.submit()
+      done: (e, data) ->
+        $(this).closest('.block').replaceWith(data.result.html)
+        $block = $("##{data.result.block.id}")
+        $form = $block.find('.form')
+        if $form.length
+          $form.show()
+          $block.find('.content').hide()
+        initSingleFileupload()
+      fail: (e, data) ->
+        alert "Не удалось загрузить файл #{file.name}"
+
 $ ->
   $('.blocks').sortable
     placeholder: "ui-state-highlight"
@@ -60,22 +78,4 @@
     else
       $(this).closest('.block').fadeOut()

-  initSingleFileupload = ->
-    $('.single-fileupload').each ->
-      $(this).fileupload
-        type: 'PATCH'
-        dropZone: $(this).closest('.dropzone')
-        add: (e, data) ->
-          data.submit()
-        done: (e, data) ->
-          $(this).closest('.block').replaceWith(data.result.html)
-          $block = $("##{data.result.block.id}")
-          $form = $block.find('.form')
-          if $form.length
-            $form.show()
-            $block.find('.content').hide()
-          initSingleFileupload()
-        fail: (e, data) ->
-          alert "Не удалось загрузить файл #{file.name}"
-
   initSingleFileupload()


:bug: dashboard no longer caches events

:bug: dashboard no longer caches events

--- src/flux/student-dashboard.coffee
+++ src/flux/student-dashboard.coffee
@@ -13,12 +13,10 @@

   _reset: ->
     CrudConfig.reset.call(this)
-    delete @_weeks

   exports:

     eventsByWeek: (courseId) ->
-      return @_weeks if @_weeks
       data = @_get(courseId)
       tasks = data.tasks or []
       weeks = _.groupBy tasks, (event) ->
@@ -26,7 +24,7 @@
       sorted = {}
       for weekId, events of weeks
         sorted[weekId] = _.sortBy(events, 'due_at')
-      @_weeks = sorted
+      sorted

     pastEventsByWeek: (courseId) ->
       weeks = this.exports.eventsByWeek.call(this, courseId)


Closer to working sentence selection

Closer to working sentence selection

--- annotran/static/scripts/annotator/plugin/sentenceselection.coffee
+++ annotran/static/scripts/annotator/plugin/sentenceselection.coffee
@@ -51,16 +51,24 @@

     return target

-  packageData: (initialTarget, endTarget, endIndex) ->
+  packageData: (initialTarget, endTarget, endIndex, startIndexOffset = 0) ->
     full_xpath = Util.xpathFromNode($(initialTarget), document)
     end_xpath = Util.xpathFromNode($(endTarget), document)

-    data = {
-      start: full_xpath
-      startOffset: this.currentIndex
-      end: end_xpath
-      endOffset: endIndex + 1
-    }
+    if startIndexOffset != 0
+      data = {
+        start: full_xpath
+        startOffset: startIndexOffset
+        end: end_xpath
+        endOffset: endIndex + 1
+      }
+    else
+      data = {
+        start: full_xpath
+        startOffset: this.currentIndex
+        end: end_xpath
+        endOffset: endIndex + 1
+      }

     return data

@@ -83,7 +91,7 @@
     for sentence in desiredText
       if counter == this.currentSentence
         break
-      offset_to_use = offset_to_use + sentence.length
+      offset_to_use = offset_to_use + sentence.length + 1
       counter = counter + 1

     if desiredText.length == 1
@@ -91,25 +99,36 @@
     else
       desiredText = desiredText[this.currentSentence]

-    endIndex = offset_to_use + desiredText.length + this.currentSentence + 1
+    endIndex = offset_to_use + desiredText.length + 1

     if endIndex > $(currentTarget).text().length - 1
       endIndex = $(currentTarget).text().length - 1

     # test if we have found a sentence marker or if we are forcing this through anyway
-    if match or force
-      data = this.packageData(initialTarget, currentTarget, endIndex)
+    if (match or force) and (offset_to_use < endIndex)
+      console.log("Start:" + offset_to_use)
+      console.log("End:" + endIndex)
+      data = this.packageData(initialTarget, currentTarget, endIndex, offset_to_use)
       this.anchorToPage(data)
     else
       # here want to test:
       # 1. is there a sibling element?
       # 2. is there a parent element with a next sibling?
+      this.currentIndex = 0
+      this.currentSentence = 0
+
       nextSibling = $(currentTarget).next()
+
+      if offset_to_use == endIndex + 1
+        initialTarget = nextSibling

       if nextSibling != undefined and nextSibling.length != 0
         this.selectSentence initialTarget, nextSibling
       else
         nextSibling = this.findNextJumpNode(currentTarget)
+
+        if offset_to_use == endIndex + 1
+          initialTarget = nextSibling

         if nextSibling != undefined and nextSibling.length != 0
           this.selectSentence initialTarget, currentTarget, true


Fix does not show skip pattern for question without page refresh bug

--- app/assets/javascripts/angular/controllers/options_ctrl.js.coffee
+++ app/assets/javascripts/angular/controllers/options_ctrl.js.coffee
@@ -4,7 +4,7 @@
     $scope.project_id = project_id
     $scope.instrument_id = instrument_id
     $scope.question_id = question_id
-    if instrument_id and question_id
+    if $scope.instrument_id and $scope.question_id
       $scope.options = $scope.queryOptions()

   $scope.queryOptions = ->
@@ -18,8 +18,8 @@

   $scope.$on('SAVE_QUESTION', (event, id) ->
     if ($scope.question_id == id or !$scope.question_id)
+      $scope.question_id = id
       angular.forEach $scope.options, (option, index) ->
-        $scope.question_id = id
         option.number_in_question = index + 1
         option.project_id = $scope.project_id
         option.instrument_id = $scope.instrument_id
@@ -32,9 +32,8 @@
         else
           option.$save({},
             (data, headers) -> $scope.options = $scope.queryOptions(),
-            (result, headers) -> alert "Error updating option"
+            (result, headers) -> alert "Error saving option"
           )
-      $scope.options = $scope.queryOptions()
   )

   $scope.$on('CANCEL_QUESTION', ->


fix content resizing in some browsers

fix content resizing in some browsers

--- app/assets/javascripts/darkswarm/directives/body_scroll.js.coffee
+++ app/assets/javascripts/darkswarm/directives/body_scroll.js.coffee
@@ -7,9 +7,11 @@
         document.body.style.top = "-#{window.scrollY}px"
         document.body.style.position = 'fixed'
         document.body.style.overflowY = 'scroll'
+        document.body.style.width = '100%'
       else
         scrollY = parseInt(document.body.style.top)
         document.body.style.position = ''
         document.body.style.top = ''
         document.body.style.overflowY = ''
+        document.body.style.width = ''
         window.scrollTo(0, scrollY * -1) if scrollY


trailing whitespace

trailing whitespace

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -574,14 +574,14 @@

   return Zotero.DB.columnQuery("
     with recursive recursivecollections as (
-      select collectionID, parentCollectionID
-      from collections
+      select collectionID, parentCollectionID
+      from collections
       where collectionID in #{Zotero.BetterBibTeX.SQLSet(collections)}

       union all

       select p.collectionID, p.parentCollectionID
-      from collections p
+      from collections p
       join recursivecollections as c on c.parentCollectionID = p.collectionID
     ) select distinct collectionID from recursivecollections")


Fix indentation

Fix indentation

--- client/app/MainApp/filetree/controllers/findercontroller.coffee
+++ client/app/MainApp/filetree/controllers/findercontroller.coffee
@@ -210,60 +210,60 @@

 class VMMountStateWidget extends JView

-    constructor:->
-      super cssClass : 'no-vm-found-widget'
-
-      @loader = new KDLoaderView
-        size          : width : 20
-        loaderOptions :
-          speed       : 0.7
-          FPS         : 24
-
-      @warning = new KDCustomHTMLView
-        partial : "There is no attached VM"
-
-    pistachio:->
-      """
-      {{> @loader}}
-      {{> @warning}}
-      """
-
-    showMessage:(message)->
-      message or= """There is no VM attached to filetree, you can
-                     attach or create one from environment menu below."""
-
-      @warning.updatePartial message
-      @warning.show()
-
-      @loader.hide()
-
-    show:->
-      @setClass 'visible'
-      @warning.hide()
-      @loader.show()
-
-      if KD.getSingleton("groupsController").getGroupSlug() is 'koding'
-        @showMessage()
-
-      # Not sure about it I guess only owners can create GroupVM?
-      else if ("admin" in KD.config.roles) or ("owner" in KD.config.roles)
-        group = KD.getSingleton("groupsController").getCurrentGroup()
-        group.checkPayment (err, payments)=>
-          warn err  if err
-          if payments.length is 0
-            @showMessage """There is no VM attached for this group, you can
-                            attach one or you can pay and create
-                            a new one from environment menu below."""
-          else
-            @showMessage """There is no VM attached for this group, you can
-                            attach one or you can create a new one from
-                            environment menu below."""
-
-      else
-        @showMessage """There is no VM for this group or not attached to
-                        filetree yet, you can attach one from environment
-                        menu below."""
-
-    hide:->
-      @unsetClass 'visible'
-      @loader.hide()+  constructor:->
+    super cssClass : 'no-vm-found-widget'
+
+    @loader = new KDLoaderView
+      size          : width : 20
+      loaderOptions :
+        speed       : 0.7
+        FPS         : 24
+
+    @warning = new KDCustomHTMLView
+      partial : "There is no attached VM"
+
+  pistachio:->
+    """
+    {{> @loader}}
+    {{> @warning}}
+    """
+
+  showMessage:(message)->
+    message or= """There is no VM attached to filetree, you can
+                   attach or create one from environment menu below."""
+
+    @warning.updatePartial message
+    @warning.show()
+
+    @loader.hide()
+
+  show:->
+    @setClass 'visible'
+    @warning.hide()
+    @loader.show()
+
+    if KD.getSingleton("groupsController").getGroupSlug() is 'koding'
+      @showMessage()
+
+    # Not sure about it I guess only owners can create GroupVM?
+    else if ("admin" in KD.config.roles) or ("owner" in KD.config.roles)
+      group = KD.getSingleton("groupsController").getCurrentGroup()
+      group.checkPayment (err, payments)=>
+        warn err  if err
+        if payments.length is 0
+          @showMessage """There is no VM attached for this group, you can
+                          attach one or you can pay and create
+                          a new one from environment menu below."""
+        else
+          @showMessage """There is no VM attached for this group, you can
+                          attach one or you can create a new one from
+                          environment menu below."""
+
+    else
+      @showMessage """There is no VM for this group or not attached to
+                      filetree yet, you can attach one from environment
+                      menu below."""
+
+  hide:->
+    @unsetClass 'visible'
+    @loader.hide()


Fix user's name not being stored

Fix user's name not being stored

--- Web/js/classes/rohbot-api.coffee
+++ Web/js/classes/rohbot-api.coffee
@@ -74,7 +74,7 @@
 	_onMessage: (data) ->
 		switch data.Type
 			when "authResponse"
-				@name = data.name
+				@name = data.Name
 				@trigger "login", data
 			when "chatHistory"
 				@trigger "chathistory", data
@@ -95,4 +95,4 @@
 	legacy "ChatHistory"
 	legacy "SysMessage"
 	legacy "Message"
-	legacy "UserList"+	legacy "UserList"


About: team file organized

About: team file organized

--- client/landing/About/team.coffee
+++ client/landing/About/team.coffee
@@ -66,6 +66,9 @@
       ,
         username  : 'alice'
         title     : 'Chef de Cuisine'
+      ]
+
+    # suspended     : []
       # ,
       #   username  : 'cenk'
       #   title     : 'Software Engineer'
@@ -85,14 +88,11 @@
       #   username  : 'pablostanley'
       #   title     : 'Illustrator'
       # ,
-      #   username  : 'aleksey-m'
-      #   title     : 'System Administrator'
-      ]
-
-    # suspended     : []
-
       #   username  : 'kiwigeraint'
       #   title     : 'Director of Infrastructure'
+      # ,
+      #   username  : 'leventyalcin'
+      #   title     : 'DevOps'
       # ,
       #   username  : 'aleksey-m'
       #   title     : 'Sys Admin & node.js Developer'


include domain in compose project name

include domain in compose project name

--- src/coffee/compose/actions.coffee
+++ src/coffee/compose/actions.coffee
@@ -10,10 +10,13 @@
   buildScriptPaths = (instance) ->
     [scriptDir = "#{config.compose.scriptBaseDir}/#{config.domain}/#{instance}", "#{scriptDir}/docker-compose.yml"]

+  composeProject = (instance) -> "#{config.domain}-#{instance}"
+
   start: (instance, composition, data) ->
     eventEmitter = new events.EventEmitter()
     compose = yaml.safeDump composition
     [scriptDir, scriptPath] = buildScriptPaths instance
+    composeProjectName = composeProject instance

     pullCb = (data) ->
       data = data.toString()
@@ -29,20 +32,31 @@

     ensureMkdir scriptDir, ->
       writeFile scriptPath, compose, ->
-        runCmd 'docker-compose', ['-f', scriptPath, '-p', instance, 'pull'], env, {stdout: pullCb, stderr: emitLogCb}, ->
-          runCmd 'docker-compose', ['-f', scriptPath, '-p', instance, 'up', '-d', '--remove-orphans'], env, {stderr: emitLogCb}, ->
-            console.log 'Done, started', instance
+        runCmd 'docker-compose', ['-f', scriptPath, '-p', composeProjectName, 'pull'], env, {stdout: pullCb, stderr: emitLogCb}, ->
+          runCmd 'docker-compose', ['-f', scriptPath, '-p', composeProjectName, 'up', '-d', '--remove-orphans'], env, {stderr: emitLogCb}, ->
+            console.log 'Done, started', composeProjectName
     eventEmitter

   stop: (instance, data) ->
     eventEmitter = new events.EventEmitter()
     [scriptDir, scriptPath] = buildScriptPaths instance
+    composeProjectName = composeProject instance

     env = buildEnv config, data
-    emitLogCb = (data) -> eventEmitter.emit 'teardown-log', data.toString()
+    emitCbCalled = false
+    emitLogCb = (data) ->
+      emitCbCalled = true
+      eventEmitter.emit 'teardown-log', data.toString()

-    runCmd 'docker-compose', ['-f', scriptPath, '-p', instance, 'down', '--remove-orphans'], env, {stderr: emitLogCb}, ->
-      console.log 'Done, stopped', instance
+    runCmd 'docker-compose', ['-f', scriptPath, '-p', composeProjectName, 'down', '--remove-orphans'], env, {stderr: emitLogCb}, ->
+      if emitCbCalled
+        console.log 'Done, stopped', composeProjectName
+      else
+        # TODO: this fallback mechanism should be removed in future versions (e.g. v + 10), current(v)=2.0.1
+        console.log "#{composeProjectName} did not stop, falling back on old stop behavior based on instance name only"
+        runCmd 'docker-compose', ['-f', scriptPath, '-p', instance, 'down', '--remove-orphans'], env, {stderr: emitLogCb}, ->
+          console.log 'Done, stopped', composeProjectName
+
     eventEmitter

 #
@@ -65,7 +79,7 @@
     console.log 'success', cmd, args
     spawned.stdout.on 'data', callbacks.stdout if callbacks.stdout
     spawned.stderr.on 'data', callbacks.stderr if callbacks.stderr
-    spawned.stdout.on 'end', exitCb
+    spawned.on 'close', exitCb

 ensureMkdir = (scriptDir, success) ->
   mkdirp scriptDir, (err) ->


Fix typo.

Fix typo.

--- src/coffee/retailer2master.coffee
+++ src/coffee/retailer2master.coffee
@@ -11,12 +11,12 @@

     masterOpts = _.clone options.baseConfig
     masterOpts.config = options.master
-    retialerOpts = _.clone options.baseConfig
-    retialerOpts.config = options.retailer
+    retailerOpts = _.clone options.baseConfig
+    retailerOpts.config = options.retailer

     super masterOpts
     @masterRest = @rest
-    @retailerRest = new Rest retialerOpts
+    @retailerRest = new Rest retailerOpts

     @logger = options.baseConfig.logConfig.logger
     @retailerProjectKey = options.retailer.project_key


don't show broken links for missing avatars

--- src/scripts/controllers/mapController.coffee
+++ src/scripts/controllers/mapController.coffee
@@ -237,12 +237,17 @@
       angular.extend(marker.payload, payload)
       p = marker.payload
       if p.userName? && p.id? && p.summaryText?
+        avatarImg = if p.userAvatarImage?
+          """"""
+        else
+          ""
+
         marker.message = """

-
+                #{avatarImg}


corrects click on tray action

corrects click on tray action

--- src/main.coffee
+++ src/main.coffee
@@ -358,7 +358,7 @@
         tray.currentImage = iconpath

         tray.setToolTip toolTip
-        tray.on 'click', -> action 'togglewindow'
+        tray.on 'click', -> ipcsend 'togglewindow'

         if menu
             # build functions that cannot be sent via ipc


#383 Force quit when closing window.

#383 Force quit when closing window.

--- src/main.coffee
+++ src/main.coffee
@@ -374,7 +374,7 @@
     # For OSX only hides the window if we're not force closing.
     mainWindow.on 'close', (ev) ->
         if process.platform == 'darwin'
-            if mainWindow.isFullScreen()
+            if mainWindow?.isFullScreen()
                 mainWindow.setFullScreen false
             if not global.forceClose
                 ev.preventDefault()


store the top-level object

store the top-level object

--- src/main.coffee
+++ src/main.coffee
@@ -8,7 +8,7 @@
 app.setup()

 setTimeout ( ->
-  inputBar = React.createElement InputElement, {key: "input-bar-1", app: app.reactElement}
+  inputBar = React.createElement InputElement, {key: "input-bar-1", app: app}
   React.render inputBar, document.getElementById("input-bar")

   React.render app.reactElement, document.getElementById("chat-app")


Three working edges down, one to go

Three working edges down, one to go

--- src/main.coffee
+++ src/main.coffee
@@ -84,24 +84,24 @@
 		# Bottom edge
 		zero = (rectangle.width - rectangle.height) / 2
 		one = zero + rectangle.height
+		half = rectangle.height / 2
 		quarter = rectangle.height / 4
-		half = rectangle.height / 2

 		if pieceEdges[i].bottom is 1

 			# Outwards
-			puzzle.lineTo new P.Point one - quarter, H
-			puzzle.quadraticCurveTo new P.Point(one - half, H), new P.Point(one - quarter, H + quarter)
-			puzzle.cubicCurveTo new P.Point(one, H + half), new P.Point(zero, H + half), new P.Point(zero + quarter, H + quarter)
-			puzzle.quadraticCurveTo new P.Point(zero + half, H), new P.Point(zero + quarter, H)
+			puzzle.lineTo new P.Point one - 3 * E, H
+			puzzle.quadraticCurveTo new P.Point(one - half, H), new P.Point(one - 3 * E, H + E)
+			puzzle.cubicCurveTo new P.Point(one - 2 * E, H + 2 * E), new P.Point(zero + 2 * E, H + 2 * E), new P.Point(zero + 3 * E, H + E)
+			puzzle.quadraticCurveTo new P.Point(one - half, H), new P.Point(zero + 3 * E, H)

 		else if pieceEdges[i].bottom is -1

 			# Outwards
-			puzzle.lineTo new P.Point one - quarter, H
-			puzzle.quadraticCurveTo new P.Point(one - half, H), new P.Point(one - quarter, H - quarter)
-			puzzle.cubicCurveTo new P.Point(one, H - half), new P.Point(zero, H - half), new P.Point(zero + quarter, H - quarter)
-			puzzle.quadraticCurveTo new P.Point(zero + half, H), new P.Point(zero + quarter, H)
+			puzzle.lineTo new P.Point one - 3 * E, H
+			puzzle.quadraticCurveTo new P.Point(one - half, H), new P.Point(one - 3 * E, H - E)
+			puzzle.cubicCurveTo new P.Point(one - 2 * E, H - 2 * E), new P.Point(zero + 2 * E, H - 2 * E), new P.Point(zero + 3 * E, H - E)
+			puzzle.quadraticCurveTo new P.Point(one - half, H), new P.Point(zero + 3 * E, H)

 		# Straight
 		puzzle.lineTo new P.Point 0, H


Add tests for partial inventory availability.

--- tests/jasmine/server/integration/rentalProducts.coffee
+++ tests/jasmine/server/integration/rentalProducts.coffee
@@ -166,9 +166,10 @@
     beforeEach ->
       Products.remove {}

-    it 'should return array of available inventory variants', (done) ->
-      product = Factory.create 'rentalProductWithInventory'
-      variant = product.variants[0]
+    it 'should return array of available inventory variant ids', (done) ->
+      product = Factory.create 'rentalProductWithInventory'
+      variant = product.variants[0]
+      quantity = _.random(1,5)
       expect(product.variants.length).toEqual 7
       expect(product.variants[3].parentId).toEqual(product.variants[0]._id)
       inventoryAvailable = Meteor.call(
@@ -179,12 +180,31 @@
           startTime: moment().startOf('day').add(3, 'days').toDate(),
           endTime: moment().startOf('day').add(5, 'days').toDate()
         },
-        1)
-      console.log(inventoryAvailable)
-      expect(inventoryAvailable.length).toEqual 1
-      done()
-
-    it 'should be return empty array if requested dates are booked', (done) ->
+        quantity)
+      expect(inventoryAvailable.length).toEqual quantity
+      done()
+
+    it 'should return array of available inventory variant ids
+      up to the max inventory available', (done) ->
+      product = Factory.create 'rentalProductWithInventory'
+      variant = product.variants[0]
+      quantity = 8
+      expect(product.variants.length).toEqual 7
+      expect(product.variants[3].parentId).toEqual(product.variants[0]._id)
+      inventoryAvailable = Meteor.call(
+        'checkInventoryAvailability',
+        product._id,
+        variant._id,
+        {
+          startTime: moment().startOf('day').add(3, 'days').toDate(),
+          endTime: moment().startOf('day').add(5, 'days').toDate()
+        },
+        8)
+      expect(inventoryAvailable.length).toEqual 5
+      done()
+
+    it 'should be return empty array if requested dates are booked
+      for all inventory variants', (done) ->
       product = Factory.create 'rentalProductWithInventory'
       variant = product.variants[0]
       expect(product.variants.length).toEqual 7
@@ -198,12 +218,12 @@
           endTime: moment().startOf('day').add(16, 'days').toDate()
         },
         1)
-      console.log(inventoryAvailable)
       expect(inventoryAvailable.length).toEqual 0
       done()

     it 'should be return empty array if
-      requested dates are partially booked ', (done) ->
+      requested dates are partially booked
+      for all inventory variants', (done) ->

       product = Factory.create 'rentalProductWithInventory'
       variant = product.variants[0]
@@ -218,6 +238,5 @@
           endTime: moment().startOf('day').add(10, 'days').toDate()
         },
         1)
-      console.log(inventoryAvailable)
       expect(inventoryAvailable.length).toEqual 0
       done()


Equation builder +/- key works Fixes #4707618

--- math/expression_manipulation_spec.coffee
+++ math/expression_manipulation_spec.coffee
@@ -445,6 +445,11 @@
       new_exp = @manip.build_negate_last().perform(exp)
       expect(new_exp.expression().last().value()).toEqual -1

+    it "does nothing when the last element is not a number", ->
+      exp = @exp_pos_builder()
+      new_exp = @manip.build_negate_last().perform(exp)
+      expect(new_exp.expression().last()).toEqual undefined
+
   describe "appending pi", ->
     it_plays_manipulation_role
       subject: ->


updates exponentiation to wrap base and power in expressions

--- math/expression_manipulation_spec.coffee
+++ math/expression_manipulation_spec.coffee
@@ -56,13 +56,14 @@
         expect(@new_exp.first()).toBeInstanceOf @components.exponentiation

       it "provides the exponentiation its base", ->
-        expect(@new_exp.first().base()).toBeAnEqualExpressionTo @exp_builder(10).first()
+        expect(@new_exp.first().base()).toBeAnEqualExpressionTo @exp_builder([10]).first()

     describe "on an expression that currently ends with an operator ", ->
       it "replaces the trailing operator", ->
         exp = @exp_builder(10, '+')
         new_exp = @manip.exponentiate_last.build().invoke(exp)
-        expect(new_exp).toBeAnEqualExpressionTo @exp_builder('^': [10, null])
+        expected = @exp_builder('^': [10, {open_expression: null}])
+        expect(new_exp).toBeAnEqualExpressionTo expected

     describe "on an expression that has a trailing exponent", ->
       it "manipulates expression correctly", ->
@@ -90,15 +91,26 @@
         expected = @exp_builder(open_expression: {open_expression: [8]})
         expect(exp).toBeAnEqualExpressionTo expected

-    describe "when the manipulation to be manipulated has an exponentiation ", ->
+    describe "when the manipulation to be manipulated has an exponentiation as its last element", ->
       describe "with no power", ->
         beforeEach ->
           @exp = @exp_builder('^': [10, null])

         it "inserts the number into the exponentiation", ->
           new_exp = @manip.append_number.build(value: 11).invoke(@exp)
-          expected = @exp_builder({'^': [10,11]})
+
+          expected = @exp_builder('^': [10, {open_expression: 11}])
           expect(new_exp).toBeAnEqualExpressionTo expected
+
+      describe "with a power", ->
+        beforeEach ->
+          @exp = @exp_builder('^': [10, 11])
+
+        it "inserts a multiplication and then the number", ->
+          new_exp = @manip.append_number.build(value: 12).invoke(@exp)
+          expected = @exp_builder({'^': [10,11]}, '*', 12)
+          expect(new_exp).toBeAnEqualExpressionTo expected
+

   describe "opening a new sub expression", ->
     it "adds a sub-expression to the expression", ->


avoid clobbering global _ in loop

avoid clobbering global _ in loop

--- app/coffee/UpdatesManager.coffee
+++ app/coffee/UpdatesManager.coffee
@@ -158,7 +158,7 @@
 				users[update.meta.user_id] = true

 		jobs = []
-		for user_id, _ of users
+		for user_id of users
 			do (user_id) ->
 				jobs.push (callback) ->
 					WebApiManager.getUserInfo user_id, (error, userInfo) ->


remove logs from stats_box.coffee

remove logs from stats_box.coffee

--- app/controllers/stats_box.coffee
+++ app/controllers/stats_box.coffee
@@ -35,11 +35,9 @@
     return total

   update: =>
-    console.log('update')
-    @updateSubjectTotal().then(@setTotal).then(@updateHelper)
+    @updateSubjectTotal().then(@setTotal).then(@updateHelper)

   updateHelper: =>
-    console.log('update helper ')
     project = Project.current
     @completeCount = num(project.complete_count)
     @complete.text @percentComplete() + "%"


add bits of documentation, add some API methods

--- src/experiments/recycle.coffee
+++ src/experiments/recycle.coffee
@@ -27,7 +27,7 @@
 # $async                    = D.remit_async.bind D
 PS                        = require 'pipestreams'
 { $, $async, }            = PS
-new_pushable              = require 'pull-pushable'
+_new_push_source          = require 'pull-pushable'
 assign                    = Object.assign
 jr                        = JSON.stringify
 copy                      = ( P... ) -> assign {}, P...
@@ -74,18 +74,29 @@
 #===========================================================================================================
 # RECYCLING
 #-----------------------------------------------------------------------------------------------------------
+@$unwrap_recycled = -> return $ ( d, send ) => send @unwrap_recycled d
 @unwrap_recycled = ( d ) ->
   ### Given an event, return its value if its a `~recycle` event; otherwise, return the event itself. ###
   return if ( @is_recycling d ) then d.value else d

 #-----------------------------------------------------------------------------------------------------------
-@$unwrap_recycled = ->
-  return $ ( d, send ) => send @unwrap_recycled d
+@$recycle = ( push ) ->
+  ### Stream transform to send events either down the pipeline (using `send`) or
+  to an alternate destination, using the `push` method ( the only argument to
+  this function). Normally, this will be the `push` method of a push source, but
+  it could be any function that accepts a single event as argument. ###
+  return $ ( d, send ) =>
+    if ( @is_recycling d ) then push d else send d
+    return null

 #-----------------------------------------------------------------------------------------------------------
-@$recycle = ( sender ) ->
-  return PS.$watch ( d ) =>
-    if ( @is_recycling d ) then sender d
+@new_push_source = ->
+  ### Return a `pull-streams` `pushable`. Methods `push` and `end` will be bound to the instance
+  so they can be freely passed around. ###
+  R       = _new_push_source()
+  R.push  = R.push.bind R
+  R.end   = R.end.bind R
+  return R

 #===========================================================================================================
@@ -170,7 +181,12 @@
 @new_stop_event     = ( key, value, other...  ) -> @new_event ')', key, value, other...
 @new_system_event   = ( key, value, other...  ) -> @new_event '~', key, value, other...
 @new_end_event      =                           -> @new_system_event 'end'
+@new_text_event     = (      value, other...  ) -> @new_single_event 'text',    value, other...
 @recycling          = ( d )                     -> @new_system_event 'recycle', d
+
+#-----------------------------------------------------------------------------------------------------------
+@new_warning = ( ref, message, d, other...  ) ->
+  @new_system_event 'warning', d, { ref, message, }, other...

 ############################################################################################################


Fixed a bug.

Fixed a bug.

--- app/views/play/level_view.coffee
+++ app/views/play/level_view.coffee
@@ -154,7 +154,7 @@
       return Backbone.Mediator.subscribeOnce 'modal-closed', @onLevelLoaderLoaded, @

     # Save latest level played in local storage
-    localStorage["lastLevel"] = @levelID if localStorage? and not (@level.get('type') in ['ladder', 'ladder-tutorial'])
+    localStorage["lastLevel"] = @levelID if localStorage? and not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
     @grabLevelLoaderData()
     team = @getQueryVariable("team") ? @world.teamForPlayer(0)
     @loadOpponentTeam(team)


Make sure always one chat conversation between same invitees, Return if exists, otherwise create a new one and invite all invitees

--- workers/social/lib/social/models/chatconversation/index.coffee
+++ workers/social/lib/social/models/chatconversation/index.coffee
@@ -76,21 +76,32 @@

     {nickname} = delegate.profile

+    initialInvitees or= []
     initialInvitees.push nickname
-
     initialInvitees = uniq initialInvitees

-    conversation = new this {
-      publicName  : createId()
-      createdBy   : nickname
-    }
+    # Invitees check
+    if (initialInvitees.length is 1) and nickname in initialInvitees
+      return callback new KodingError 'You cannot chat with yourself'

-    @one { invitees: initialInvitees }, (err, conversation)->
+    # Make sure there is only one conversation with same participants
+    selector =
+      $and: [
+        {invitees : $all  : initialInvitees}
+        {invitees : $size : initialInvitees.length}
+      ]
+
+    @one selector, (err, conversation)->
       return callback err  if err
+
+      # If conversation exists return the conversation
+      # and invite all invitees again
       if conversation
         conversation.invite client, invitee  for invitee in initialInvitees
         return callback err, conversation

+      # If conversation not found, just create a new one
+      # and invite all invitees
       conversation = new JChatConversation
         publicName : createId()
         createdBy  : nickname
@@ -102,12 +113,17 @@
           conversation.invite client, invitee  for invitee in initialInvitees

   invite: secure (client, invitee, callback)->
+
+    # Check if user logged in
     {delegate} = client.connection
+    JAccount   = require '../account'
+    unless delegate instanceof JAccount
+      return callback new KodingError 'Access denied.'
+
     {nickname} = delegate.profile
+    delegateCanInvite = nickname in @invitees

-    delegateCanInvite = nickname? and nickname in @invitees
-
-    return callback new KodingError "Access denied!" unless delegateCanInvite
+    return callback new KodingError "Access denied!"  unless delegateCanInvite

     @update {$addToSet: invitees: invitee}, (err)=>
       return console.error err  if err?


reworked discussion fetchTeaser to grab the tags

--- workers/social/lib/social/models/messages/discussion/index.coffee
+++ workers/social/lib/social/models/messages/discussion/index.coffee
@@ -196,6 +196,13 @@
     @beginGraphlet()
       .edges
         query         :
+          sourceName  : 'JDiscussion'
+          targetName  : 'JTag'
+          as          : 'tag'
+        limit         : 5
+      .and()
+      .edges
+        query         :
           targetName  : 'JOpinion'
           as          : 'opinion'
           'data.deletedAt':
@@ -218,13 +225,6 @@
         # limit         : 3
         sort          :
           timestamp   : 1
-      # .nodes()
-      .and()
-      .edges
-        query         :
-          targetName  : 'JTag'
-          as          : 'tag'
-        limit         : 5
       .nodes()
     .endGraphlet()
     .fetchRoot callback


#15 - DRY up sync.create

#15 - DRY up sync.create

--- app/controllers/sync.coffee
+++ app/controllers/sync.coffee
@@ -54,51 +54,74 @@
   #                                   #
   #####################################

-
+  ###
+   * (private) Create Model
+   *
+   * - classname (string)
+   * - id (string)
+   * - model (object)
+   * - [timestamp] (number)
+  ###
+
+  model_create: (classname, model, timestamp) =>
+    id = model.id ?= @createId classname
+    @user.setModel classname, id, model
+    timestamp ?= Date.now()
+    @time.set classname, id, '*', timestamp
+    log "[#{ classname }] [create] created", id
+    return id
+
+
+  ###
+   * Create Task
+   *
+   * - model (object)
+   * - timestamp (number)
+   * > id (number)
+  ###

   task_create: (model, timestamp) =>

+    # Throw away client id
+    delete model.id
+
+    # Check that the list exists
     unless @user.checkModel(LIST, model.listId)
       log 'Trying to add a task to a list that doesn\'t exist'
       return null

+    # Add the task to the list
     list = @user.findModel(LIST, model.listId)

-    id = @createId TASK
-    model.id = id
+    id = @model_create(TASK, model, timestamp)

     @taskAdd id, list
-
-    @user.setModel TASK, id, model
-
-    timestamp ?= Date.now()
-    @time.set TASK, id, '*', timestamp
-
-    log '[task] Created', id, model.name
-
     return id

+
+  ###
+   * Create List
+   *
+   * - model (object)
+   * - timestamp (number)
+   * > id (number)
+  ###
+
   list_create: (model, timestamp) =>

+    # Handle inbox list
     if model.id is INBOX
-      id = INBOX
-      if @user.hasModel(LIST, INBOX) then return null
-      log '[list] [create] made inbox'
+      if @user.hasModel(LIST, INBOX)
+        return null
     else
-      id = @createId LIST
-      model.id = id
-
+      delete model.id
+
+    # Make sure tasks is empty
     model.tasks = []

-    @user.setModel LIST, id, model
-
-    timestamp ?= Date.now()
-    @time.set LIST, id, '*', timestamp
-
-    log '[list] Created', id, model.name
+    id = @model_create(LIST, model, timestamp)

     return id
-

   ####################################
@@ -256,7 +279,7 @@
     # Remove from list
     list = @user.findModel(LIST, model.listId)
     if list.tasks?
-      @taskRemove id, model.listId
+      @taskRemove id, list

     # Replace task with deleted template
     @user.setModel TASK, id,


An actually kinda working sync (notebooks are a bit broken though)

--- app/controllers/sync.coffee
+++ app/controllers/sync.coffee
@@ -66,7 +66,8 @@
       beforeSend: (xhr) ->
         xhr.setRequestHeader "Authorization", "Bearer " + localStorage.Token
     ).done((data) ->
-      result = Sync.merger(JSON.parse(Sync.exportData()), data, JSON.parse(localStorage.Queue))
+      console.log(JSON.parse(Sync.exportData()), data, JSON.parse(localStorage.Queue))
+      result = Sync.merger(JSON.parse(Sync.exportData()), data, Sync.Queue)

       console.log(result)

@@ -173,12 +174,13 @@
             contentType: "application/octet-stream"
             dataType: "json"
             url: "https://api-content.dropbox.com/1/files_put/sandbox/meta.seed"
-            data: Sync.exportData()
+            data: JSON.stringify(result[0])
             beforeSend: (xhr) ->
               xhr.setRequestHeader 'Authorization', 'Bearer ' + localStorage.Token
           ).done (data) ->
             console.log("all done! Delete the queue")
-            localStorage.Queue = '{"Note": {}, "Notebook": {}}'
+            Sync.queue = {"Note": {}, "Notebook": {}}
+            Sync.saveQueue()

     ).error (data) ->
       if data.status is 401
@@ -219,7 +221,8 @@
               xhr.setRequestHeader 'Authorization', 'Bearer ' + localStorage.Token
           ).done (data) ->
             console.log("all done! Delete the queue")
-            localStorage.Queue = '{"Note": {}, "Notebook": {}}'
+            Sync.queue = {"Note": {}, "Notebook": {}}
+            Sync.saveQueue()

   connect: (fn) ->
@@ -271,8 +274,8 @@

   importData: (obj) ->
     input = JSON.parse(obj)
-    Note.refresh(input.Note)
-    Notebook.refresh(input.Notebook)
+    Note.refresh(input.Note, clear: true)
+    Notebook.refresh(input.Notebook, clear: true)

   # Merges the client & server, using a queue
   # Spits out the result, id changes & fs changes


Apparently I have to re-add matchers on each run?

6/10 passing

--- app/js/jasmine-given.coffee
+++ app/js/jasmine-given.coffee
@@ -208,11 +208,10 @@
         func()
         @flow()

-  if jasmine.addMatchers?
-    jasmine.addMatchers(jasmine.matcherWrapper.wrap(jasmine._given.matchers))
-  else
-    beforeEach ->
+  beforeEach ->
+    if jasmine.addMatchers?
+      jasmine.addMatchers(jasmine.matcherWrapper.wrap(jasmine._given.matchers))
+    else
       @addMatchers(jasmine._given.matchers)

-
 ) jasmine


move mark event listeners to window

move mark event listeners to window

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -123,13 +123,11 @@
     @element.addEventListener 'mouseover', (e) => @hovering = true
     @element.addEventListener 'mouseout', (e) => @hovering = false
     @element.addEventListener 'mousedown', (e) => @onMouseDown(e)
-    @element.addEventListener 'mousemove', (e) => @onMouseMove(e)
-    @element.addEventListener 'mouseup', (e) => @onMouseUp(e)
     @element.addEventListener 'click', (e) => @canvasGraph.marks.remove(@) if e.layerY < 15
+    window.addEventListener 'mousemove', (e) => @onMouseMove(e)
+    window.addEventListener 'mouseup', (e) => @onMouseUp(e)

   draw: (e) ->
-    console.log "SP", @startingPoint, "e.layerX", e.pageX
-
     markLeftX = Math.min @startingPoint, (e.pageX-@canvas.getBoundingClientRect().left)
     markRightX = Math.max @startingPoint, (e.pageX-@canvas.getBoundingClientRect().left)

@@ -186,8 +184,11 @@

   onMouseUp: (e) ->
     e.preventDefault()
+    for mark in @canvasGraph.marks.all
+      mark.dragging = false
+      mark.moving = false
+    if (@ in @canvasGraph.marks.all) then @canvasGraph.marks.update(@) else @canvasGraph.marks.add(@)
     @dragging = false
     @moving = false
-    if (@ in @canvasGraph.marks.all) then @canvasGraph.marks.update(@) else @canvasGraph.marks.add(@)

 module?.exports = CanvasGraph: CanvasGraph, Marks: Marks, Mark: Mark


Do not generate an error when a directory to remove is missing.

--- app/coffee/CompileManager.coffee
+++ app/coffee/CompileManager.coffee
@@ -64,7 +64,8 @@
 		compileDir = Path.join(Settings.path.compilesDir, project_id)
 		proc = child_process.spawn "rm", ["-r", compileDir]

-		proc.on "error", callback
+		proc.on "error", (err) ->
+			callback if err.code == "ENOENT" then null else err

 		stderr = ""
 		proc.stderr.on "data", (chunk) -> stderr += chunk.toString()


Sort registrations by key.

Sort registrations by key.

--- app/javascripts/intensity.coffee
+++ app/javascripts/intensity.coffee
@@ -146,13 +146,18 @@
         registration.title = key

         # TODO - Wrap the key in a registration parameters pop-up hyperlink.
+
         # Return the data series format object.
         key: key
         color: COLORS[index % COLORS.length]
         values: coordinates(registration.intensity.intensities)

-        # The registration data series configuration.
-      regData = ((registrationDataSeries(reg, i) for reg, i in scan.registrations))
+      # Sort the scan registrations by key.
+      regs = scan.registrations
+      regKeys = _.keys(regs).sort()
+      regsSorted = (regs[key] for key in regKeys)
+      # Collect the data series specifications.
+      regData = (registrationDataSeries(reg, i) for reg, i in regsSorted)

       # Return the chart configuration.
       data: [scanData].concat(regData)


Adding badges for calbug-expeditions

Adding badges for calbug-expeditions

--- app/lib/badge-definitions.coffee
+++ app/lib/badge-definitions.coffee
@@ -133,6 +133,54 @@
           return false
     collection: 'calbug'
   ,
+    name: 'Egg'
+    url: 'badges/calbug/egg.png'
+    description: 'You earn this badge for transcribing a Calbug record'
+    awardText: 'You earned this badge for transcribing a Calbug record'
+    condition:
+      func: (details) ->
+        if details.user.project.groups?[details.archive.id]?
+          return details.user.project.groups[details.archive.id].classification_count > 0
+        else
+          return false
+    collection: 'calbug-expeditions'
+  ,
+    name: 'Caterpillar'
+    url: 'badges/calbug/caterpillar.png'
+    description: 'You earn this badge for transcribing 25 Calbug records'
+    awardText: 'You earned this badge for transcribing 25 Calbug records'
+    condition:
+      func: (details) ->
+        if details.user.project.groups?[details.archive.id]?
+          return details.user.project.groups[details.archive.id].classification_count > 25
+        else
+          return false
+    collection: 'calbug-expeditions'
+  ,
+    name: 'Butterfly'
+    url: 'badges/calbug/butterfly.png'
+    description: 'You earn this badge for transcribing 100 Calbug records'
+    awardText: 'You earned this badge for transcribing 100 Calbug records'
+    condition:
+      func: (details) ->
+        if details.user.project.groups?[details.archive.id]?
+          return details.user.project.groups[details.archive.id].classification_count > 100
+        else
+          return false
+    collection: 'calbug-expeditions'
+  ,
+    name: 'Butterflies'
+    url: 'badges/calbug/butterflies.png'
+    description: 'You earn this badge for transcribing 500 Calbug records'
+    awardText: 'You earned this badge for transcribing 500 Calbug records'
+    condition:
+      func: (details) ->
+        if details.user.project.groups?[details.archive.id]?
+          return details.user.project.groups[details.archive.id].classification_count > 500
+        else
+          return false
+    collection: 'calbug-expeditions'
+  ,
     name: 'Spore'
     url: 'badges/macrofungi/spore.png'
     description: 'You earn this badge for transcribing 1 Macrofungi record'


Move some stuff around to make coffeescript happy

--- app/lib/badge-definitions.coffee
+++ app/lib/badge-definitions.coffee
@@ -168,43 +168,7 @@
         else
           return false
     collection: 'macrofungi'
-    ,
-  #   name: 'Nest'
-  #   url: 'badges/ornithology/nest.png'
-  #   description: 'You earn this badge for transcribing 1 Ornithological records'
-  #   awardText: 'You earned this badge for transcribing 1 Ornithological records'
-  #   condition:
-  #     func: (details) ->
-  #       if details.user.project.groups?[details.archive.id]?
-  #         return details.user.project.groups[details.archive.id].classification_count > 0
-  #       else
-  #         return false
-  #   collection: 'ornithological'
-  # ,
-  #   name: 'Fledgling'
-  #   url: 'badges/ornithology/fledgling.png'
-  #   description: 'You earn this badge for transcribing 25 Ornithological records'
-  #   awardText: 'You earned this badge for transcribing 25 Ornithological records'
-  #   condition:
-  #     func: (details) ->
-  #       if details.user.project.groups?[details.archive.id]?
-  #         return details.user.project.groups[details.archive.id].classification_count > 25
-  #       else
-  #         return false
-  #   collection: 'ornithological'
-  # ,
-  #   name: 'Adult Bird'
-  #   url: 'badges/ornithology/adult.png'
-  #   description: 'You earn this badge for transcribing 100 Ornithological records'
-  #   awardText: 'You earned this badge for transcribing 100 Ornithological records'
-  #   condition:
-  #     func: (details) ->
-  #       if details.user.project.groups?[details.archive.id]?
-  #         return details.user.project.groups[details.archive.id].classification_count > 100
-  #       else
-  #         return false
-  #   collection: 'ornithological'
-  # ,
+  ,
     name: "Novice Collection's Explorer"
     url: 'badges/multi/beginner.png'
     description: 'You earn this badge for transcribing 1 record from 3 collections'
@@ -221,3 +185,40 @@
       func: (details) ->
         checkWorkSpread.call details.user, 25, 3
 ]
+
+#   name: 'Nest'
+#   url: 'badges/ornithology/nest.png'
+#   description: 'You earn this badge for transcribing 1 Ornithological records'
+#   awardText: 'You earned this badge for transcribing 1 Ornithological records'
+#   condition:
+#     func: (details) ->
+#       if details.user.project.groups?[details.archive.id]?
+#         return details.user.project.groups[details.archive.id].classification_count > 0
+#       else
+#         return false
+#   collection: 'ornithological'
+# ,
+#   name: 'Fledgling'
+#   url: 'badges/ornithology/fledgling.png'
+#   description: 'You earn this badge for transcribing 25 Ornithological records'
+#   awardText: 'You earned this badge for transcribing 25 Ornithological records'
+#   condition:
+#     func: (details) ->
+#       if details.user.project.groups?[details.archive.id]?
+#         return details.user.project.groups[details.archive.id].classification_count > 25
+#       else
+#         return false
+#   collection: 'ornithological'
+# ,
+#   name: 'Adult Bird'
+#   url: 'badges/ornithology/adult.png'
+#   description: 'You earn this badge for transcribing 100 Ornithological records'
+#   awardText: 'You earned this badge for transcribing 100 Ornithological records'
+#   condition:
+#     func: (details) ->
+#       if details.user.project.groups?[details.archive.id]?
+#         return details.user.project.groups[details.archive.id].classification_count > 100
+#       else
+#         return false
+#   collection: 'ornithological'
+# ,


replace "scientists" with "researchers"

replace "scientists" with "researchers"

--- app/pages/lab/collaborators.cjsx
+++ app/pages/lab/collaborators.cjsx
@@ -24,7 +24,7 @@
     description: 'Experts can enter "gold mode" to make authoritative gold standard classifications that will be used to validate data quality.'
   scientist:
     label: 'Researcher'
-    description: 'Members of the research team will be marked as scientists on "Talk"'
+    description: 'Members of the research team will be marked as researchers on "Talk"'
   moderator:
     label: 'Moderator'
     description: 'Moderators have extra privileges in the community discussion area to moderate discussions. They will also be marked as moderators on "Talk".'


Remove cellect-reload button

Remove cellect-reload button

--- app/pages/lab/workflow.cjsx
+++ app/pages/lab/workflow.cjsx
@@ -81,10 +81,6 @@

-          <button type="button" className="minor-button" disabled={@state.reloadCellectInProgress} data-busy={@state.reloadCellectInProgress || null} onClick={@reloadCellect}>Reload Cellect</button>{' '}
-          {if @state.reloadCellectError
-            There was an error reloading cellect}
-
Reload Cellect after you’ve modified an associated subject set. TODO: Call this automatically when adding to a subject set.

Subject retirement <RetirementRulesEditor workflow={@props.workflow} />


remove currentUserId on Session.clear(){

--- app/services/session.coffee
+++ app/services/session.coffee
@@ -8,6 +8,7 @@
       deferred = $q.defer()
       if CurrentUser()
         $auth.signOut().then ->
+          global.cobudgetApp.currentUserId = null
           deferred.resolve()
       else
         deferred.resolve()


Set href on favicon <link> tag second time in a timeout

This somehow fixes chrome inconsistencies when changing favicon

--- app/utils/favicon-manager.coffee
+++ app/utils/favicon-manager.coffee
@@ -16,7 +16,11 @@

   link = @createLinkTag()
   head.appendChild(link)
+
   link.setAttribute('href', href)
+  setTimeout ->
+    link.setAttribute('href', href)
+  , 1

 manager.prototype.getLinkTag = ->
   links = @getHeadTag().getElementsByTagName('link')


Call heartbeat after paper model save

Call heartbeat after paper model save

--- app/assets/javascripts/controllers/paper_edit_controller.js.coffee
+++ app/assets/javascripts/controllers/paper_edit_controller.js.coffee
@@ -1,6 +1,7 @@
 #= require controllers/base_paper_controller
 ETahi.PaperEditController = ETahi.BasePaperController.extend
   visualEditor: null
+  saveState: false

   setupVisualEditor: (->
     @set('visualEditor', ETahi.VisualEditorService.create())
@@ -17,8 +18,8 @@
   ).property('model.body')

   statusMessage: ( ->
-    @get('processingMessage') || @get('userEditingMessage') || @get('saveState')
-  ).property('processingMessage', 'userEditingMessage', 'saveState')
+    @get('processingMessage') || @get('userEditingMessage') || @get('saveStateMessage')
+  ).property('processingMessage', 'userEditingMessage', 'saveStateMessage')

   processingMessage: (->
     if @get('status') is "processing"
@@ -50,26 +51,35 @@

   defaultBody: 'Type your manuscript here'

+  saveStateDidChange: (->
+    if @get('saveState')
+      @setProperties
+        saveStateMessage: "Saved"
+        savedAt: new Date()
+    else
+      @setProperties
+        saveStateMessage: null
+        savedAt: null
+  ).observes('saveState')
+
   actions:
     toggleEditing: ->
-      if @get('lockedBy')
+      if @get('lockedBy') #unlocking
         @set('body', @get('visualEditor.bodyHtml'))
-        @set('lockedBy', null) #unlock for others
+        @set('lockedBy', null)
         @send('stopEditing')
-      else
+        @get('model').save().then (paper) =>
+          @set('saveState', true)
+      else #locking
         @set('lockedBy', @getCurrentUser())
-        @send('startEditing')
-      @get('model').save().then (paper) =>
-        @setProperties
-          saveState: null
-          savedAt: null
+        @get('model').save().then (paper) =>
+          @send('startEditing')
+          @set('saveState', false)

     savePaper: ->
       return unless @get('model.editable')
       @get('model').save().then (paper) =>
-        @setProperties
-          saveState: "Saved"
-          savedAt: new Date()
+        @set('saveState', true)

     updateDocumentBody: (content) ->
       @set('body', content)


Fix typo in documentation

Fix typo in documentation


I give up

I give up

--- vendor/assets/javascripts/fustrate/components/modal.coffee
+++ vendor/assets/javascripts/fustrate/components/modal.coffee
@@ -28,8 +28,8 @@
     @settings.previousModal = $()

     @setTitle @constructor.title, icon: @constructor.icon
-    @setContent content, reload: false
-    @setButtons @constructor.buttons, reload: false
+    @setContent content, false
+    @setButtons @constructor.buttons, false

     @_reloadUIElements()
     @addEventListeners()
@@ -60,14 +60,14 @@
     else
       $('.modal-title span', @modal).html title

-  setContent: (content, { reload } = { reload: true }) =>
+  setContent: (content, reload = true) =>
     $('.modal-content', @modal).html content

     @settings._cachedHeight = undefined

     @_reloadUIElements() if reload

-  setButtons: (buttons, { reload } = { reload: true }) =>
+  setButtons: (buttons, reload = true) =>
     if buttons?.length < 1
       $('.modal-buttons', @modal).empty()


one more fix

one more fix

Signed-off-by: Steven Hoffman <f5bcacb8ce1651b7d1e56f2693431f20e085fdeb@valenciamgmt.com>

--- vendor/assets/javascripts/fustrate/components/modal.coffee
+++ vendor/assets/javascripts/fustrate/components/modal.coffee
@@ -180,7 +180,7 @@
       </div>""").appendTo(@settings.appendTo)

   @_defaultClasses: ->
-    ['modal', @size, @type].filter (klass) klass isnt null
+    ['modal', @size, @type].filter (klass) -> klass isnt null

   @toggleBackground: (visible = true) =>
     @overlay = $ '<div class="modal-overlay">' unless @overlay


Fixes issue #236 - `Event.ImageLoaded` event never triggered in FramerJS

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -551,7 +551,7 @@

 			# As an optimization, we will only use a loader
 			# if something is explicitly listening to the load event
-			if @events?.hasOwnProperty "load" or @events?.hasOwnProperty "error"
+			if @_eventListeners?.hasOwnProperty "load" or @_eventListeners?.hasOwnProperty "error"

 				loader = new Image()
 				loader.name = imageUrl


Fix inspect bug

Fix inspect bug

--- framer/Utils.coffee
+++ framer/Utils.coffee
@@ -174,10 +174,13 @@
 	return obj

 Utils.inspectObjectType = (item) ->
+	# This is a hacky way to get nice object names, it tries to
+	# parse them from the .toString methods for objects.
 	objectType = item.toString().split(" ")[1]
+	return objectType unless objectType
 	objectType[0..objectType.length-2]

-Utils.inspect = (item, l=0, max=5) ->
+Utils.inspect = (item, max=5, l=0) ->

 	return "null" if item is null
 	return "undefined" if item is undefined
@@ -192,13 +195,13 @@
 		return item.toString()
 	if _.isArray(item)
 		return "[...]" if l > max
-		return "[" + _.map(item, (i) -> Utils.inspect(i, l++)).join(", ") + "]"
+		return "[" + _.map(item, (i) -> Utils.inspect(i, max, l++)).join(", ") + "]"
 	if _.isObject(item)
 		objectType = Utils.inspectObjectType(item)
 		if l > max
 			objectInfo = "{...}"
 		else
-			objectInfo = "{" + _.map(item, (v, k) -> "#{k}:#{Utils.inspect(v, l++)}").join(", ") + "}"
+			objectInfo = "{" + _.map(item, (v, k) -> "#{k}:#{Utils.inspect(v, max, l++)}").join(", ") + "}"
 		return objectInfo if objectType is "Object"
 		return "<#{objectType} #{objectInfo}>"


submenu/visibility: Minor cleanup.

submenu/visibility: Minor cleanup.

--- Oghma/submenu/visibility.coffee
+++ Oghma/submenu/visibility.coffee
@@ -43,7 +43,7 @@
     )
   else
     items.push(
-      text: 'Make public.'
+      text: 'Make public'
       handler: -> set( [] )
     )


sitewide: some mysterious indentation discovered and fixed

--- client/landing/app/MainApp/maincontroller/maincontroller.coffee
+++ client/landing/app/MainApp/maincontroller/maincontroller.coffee
@@ -83,29 +83,26 @@

       @ready @emit.bind @, "AccountChanged", account, firstLoad

-      unless @mainViewController
-        @loginScreen = new LoginView
-        KDView.appendToDOMBody @loginScreen
-
-        @mainViewController  = new MainViewController
-          view    : mainView = new MainView
-            domId : "kdmaincontainer"
-
-        KDView.appendToDOMBody mainView
-
-        @decorateBodyTag()
-        @emit 'ready'
-
-        eventPrefix = if firstLoad then "pageLoaded.as" else "accountChanged.to"
-        eventSuffix = if @isUserLoggedIn() then "loggedIn" else "loggedOut"
-
-        # this emits following events
-        # -> "pageLoaded.as.loggedIn"
-        # -> "pageLoaded.as.loggedOut"
-        # -> "accountChanged.to.loggedIn"
-        # -> "accountChanged.to.loggedOut"
-
-        @emit "#{eventPrefix}.#{eventSuffix}", account, connectedState, firstLoad
+      @createMainViewController()  unless @mainViewController
+
+      @decorateBodyTag()
+      @emit 'ready'
+      # this emits following events
+      # -> "pageLoaded.as.loggedIn"
+      # -> "pageLoaded.as.loggedOut"
+      # -> "accountChanged.to.loggedIn"
+      # -> "accountChanged.to.loggedOut"
+      eventPrefix = if firstLoad then "pageLoaded.as" else "accountChanged.to"
+      eventSuffix = if @isUserLoggedIn() then "loggedIn" else "loggedOut"
+      @emit "#{eventPrefix}.#{eventSuffix}", account, connectedState, firstLoad
+
+  createMainViewController:->
+    @loginScreen = new LoginView
+    KDView.appendToDOMBody @loginScreen
+    @mainViewController  = new MainViewController
+      view    : mainView = new MainView
+        domId : "kdmaincontainer"
+    KDView.appendToDOMBody mainView

   doLogout:->


Log local error variable vs. non-existent instance variable

--- src/tail.coffee
+++ src/tail.coffee
@@ -49,8 +49,8 @@
     try
       stats =  fs.statSync(@filename)
     catch err
-      @logger.error("watch for #{@filename} failed: #{@err}") if @logger
-      @emit("error", "watch for #{@filename} failed: #{@err}")
+      @logger.error("watch for #{@filename} failed: #{err}") if @logger
+      @emit("error", "watch for #{@filename} failed: #{err}")
       return
     @pos = if pos? then pos else stats.size

@@ -70,8 +70,8 @@
       try
         stats = fs.statSync(@filename)
       catch err
-        @logger.error("'change' event for #{@filename}. #{@err}") if @logger
-        @emit("error", "'change' event for #{@filename}. #{@err}")
+        @logger.error("'change' event for #{@filename}. #{err}") if @logger
+        @emit("error", "'change' event for #{@filename}. #{err}")
         return
       @pos = stats.size if stats.size < @pos #scenario where texts is not appended but it's actually a w+
       if stats.size > @pos


Improved Tempo class to work with a user defined time grid. Events for 1/32 to 1/2 notes.

--- src/time.coffee
+++ src/time.coffee
@@ -18,38 +18,68 @@

 # keeps track of rhythm, converts between beats, bars, time, tempo etc
 class Tempo
+    # define the tempo using a pace in BPM and a time signature
     constructor: (beatsPerMinute=120, @sigNum=4, @sigDenom=4) ->
         @bpm = beatsPerMinute
-        @timer = new Timer()
+
+        # smallest unit in the temporal grid, how often to update in 1/n bars
+        @resolution = 32
+
         @reset()

     reset: ->
-        @timer.reset()
+        # beats per millisecond
+        @beatInterval = 1000 / (@bpm / 60)

-        @beat = @beatAbsolute = @bar = 0
-        @prevBeat = -1
+        # user-specified grid units per millisecond
+        @gridInterval = @beatInterval * @sigNum / @resolution
+
+        # reset properties
+        @time = @prevEvent = 0
+
+        # absolute number of beats so far
+        @beats = 0
+
+        # current beat within a bar
+        @beat = @bar = 0
+
+        # booleans wether the current timestep is on that particular note
         @onBeat = @onBar = false
+        @on32 = @on16 = @on8 = @on4 = @on2 = false
+

-        @beatsPerMilliSecond = 1000 / (@bpm / 60)

-    update: ->
-        time = @timer.elapsed()
+    # call update continously
+    update: (dt) ->
+        @time += dt

-        @beatAbsolute = Math.floor(time / @beatsPerMilliSecond)
+        if @time - @prevEvent >= @gridInterval
+            @prevEvent = @time

-        if @beatAbsolute != @prevBeat
-            @prevBeat = @beatAbsolute
+            gridUnits = Math.floor @time / @gridInterval

-            @beat = @beatAbsolute % @sigDenom + 1
-            @bar = @beatAbsolute / @sigDenom + 1
+            u = gridUnits
+            r = @resolution

-            @onBeat = true
-            @onBar = @beat == @sigNum
+            @beats = Math.floor @time / @beatInterval
+            @beat = @beats % @sigNum
+
+            @onBeat = u % (r / @sigNum) == 0
+            @onBar = u %
+
+            @on32 = u % (r / 32) == 0
+            @on16 = u % (r / 16) == 0
+            @on8 = u % (r / 8) == 0
+            @on4 = u % (r / 4) == 0
+            @on2 = u % (r / 2) == 0
+
+            # console.log "time #{@time} beat #{@beat} / 32: #{@on32} 16: #{@on16} / 8: #{@on8}"
+
         else
             @onBeat = @onBar = false
+            @on32 = @on16 = @on8 = @on4 = @on2 = false

-        # console.log "time #{time} / beat #{@beatAbsolute} / on beat #{@onBeat} "
-        @beatAbsolute
+        @beat

 module.exports =


added isError

added isError

--- src/type.coffee
+++ src/type.coffee
@@ -30,6 +30,8 @@

 isArray = isType Array

+isError = isType Error
+
 isDefined = (x) -> x?

 GeneratorFunction = (-> yield null).constructor


Improve exec in debug mode

Improve exec in debug mode

--- src/util.coffee
+++ src/util.coffee
@@ -107,7 +107,7 @@

     execute: (command, cb) ->
         debug 'execute', command
-        return if debugMode
+        return cb null, '', '' if debugMode
         @queue.push(
             command: command,
             callback: cb


Ok Send negative energy in API

Ok Send negative energy in API

--- api/points.coffee
+++ api/points.coffee
@@ -56,7 +56,6 @@

 formatGamePoint = (game_point) ->
     result = game_point
-    result.energy = Math.abs(result.energy)
     return result

 selectGamePointFromReq = (pointId, gameId, callback) ->


write stubbed love.math tests

write stubbed love.math tests

--- test/src/math.coffee
+++ test/src/math.coffee
@@ -4,8 +4,10 @@

   describe '.noise', ->
     noise = null
+    math = null
     beforeEach ->
-      noise = new Love().math.noise
+      math = new Love().math
+      noise = math.noise

     describe 'when called with one argument', ->
       it 'should return the same value for the same input', ->
@@ -15,36 +17,90 @@
         expect(noise(0.1)).to.not.equal(noise(0.2))

       it 'should return values between -1 and 1', ->
-        for i in [0..10]
-          expect(noise(i)).to.be.above(-1).and.below(1)
+        for x in [0..10]
+          expect(noise(x)).to.be.above(-1).and.below(1)

       it 'should return similar values for similar inputs', ->
-        expect(Math.abs(noise(0.1) - noise(0.101))).to.be.below(0.1)
+        expect(Math.abs(noise(0.1) - noise(0.101))).to.be.below(0.1).and.above(0)
+
+      it 'should invoke the correct function in the underlying library', ->
+        noise1D = sinon.spy(math.simplex, "noise1D")
+        noise(0.1)
+        expect(noise1D).to.have.been.called

     describe 'when called with two arguments', ->
-      it 'should return the same value for the same input'
-      it 'should return a different value for a different input'
-      it 'should return values between -1 and 1'
-      it 'should return similar values for similar inputs'
+      it 'should return the same value for the same input', ->
+        expect(noise(0.1, 0.2)).to.equal(noise(0.1, 0.2))
+
+      it 'should return a different value for a different input', ->
+        expect(noise(0.1, 0.1)).to.not.equal(noise(0.2, 0.2))
+
+      it 'should return values between -1 and 1', ->
+        for x in [0..10]
+          for y in [0..10]
+            expect(noise(x, y)).to.be.above(-1).and.below(1)
+
+      it 'should return similar values for similar inputs', ->
+        expect(Math.abs(noise(0.1, 0.2) - noise(0.101, 0.201))).to.be.below(0.1).and.above(0)
+
+      it 'should invoke the correct function in the underlying library', ->
+        noise2D = sinon.spy(math.simplex, "noise2D")
+        noise(0.1, 0.2)
+        expect(noise2D).to.have.been.called

     describe 'when called with three arguments', ->
-      it 'should return the same value for the same input'
-      it 'should return a different value for a different input'
-      it 'should return values between -1 and 1'
-      it 'should return similar values for similar inputs'
+      it 'should return the same value for the same input', ->
+        expect(noise(0.1, 0.2, 0.3)).to.equal(noise(0.1, 0.2, 0.3))
+
+      it 'should return a different value for a different input', ->
+        expect(noise(0.1, 0.1, 0.1)).to.not.equal(noise(0.2, 0.2, 0.2))
+
+      it 'should return values between -1 and 1', ->
+        for i in [0..10]
+          for j in [0..10]
+            expect(noise(i / 5, j / 5, i + j)).to.be.above(-1).and.below(1)
+
+      it 'should return similar values for similar inputs', ->
+        expect(Math.abs(noise(0.1, 0.2, 0.3) - noise(0.101, 0.201, 0.301))).to.be.below(0.1).and.above(0)
+
+      it 'should invoke the correct function in the underlying library', ->
+        noise3D = sinon.spy(math.simplex, "noise3D")
+        noise(0.1, 0.2, 0.3)
+        expect(noise3D).to.have.been.called

     describe 'when called with four arguments', ->
-      it 'should return the same value for the same input'
-      it 'should return a different value for a different input'
-      it 'should return values between -1 and 1'
-      it 'should return similar values for similar inputs'
+      it 'should return the same value for the same input', ->
+        expect(noise(0.1, 0.2, 0.3, 0.4)).to.equal(noise(0.1, 0.2, 0.3, 0.4))
+
+      it 'should return a different value for a different input', ->
+        expect(noise(0.1, 0.1, 0.1, 0.1)).to.not.equal(noise(0.2, 0.2, 0.2, 0.2))
+
+      it 'should return values between -1 and 1', ->
+        for i in [0..10]
+          for j in [0..10]
+            expect(noise(i / 5, j / 5, i + j, i - j)).to.be.above(-1).and.below(1)
+
+      it 'should return similar values for similar inputs', ->
+        expect(Math.abs(noise(0.1, 0.2, 0.3, 0.4) - noise(0.101, 0.201, 0.301, 0.401))).to.be.below(0.1).and.above(0)
+
+      it 'should invoke the correct function in the underlying library', ->
+        noise4D = sinon.spy(math.simplex, "noise4D")
+        noise(0.1, 0.2, 0.3, 0.4)
+        expect(noise4D).to.have.been.called

   describe '.random', ->
+    random = null
+    beforeEach ->
+      random = new Love().math.random
+
     describe 'when called with no arguments', ->
-      it 'should return a value between 0 and 1'
+      it 'should return a value between 0 and 1', ->
+        expect(random()).to.be.above(0).and.below(1)

     describe 'when called with one argument', ->
-      it 'should return a value between 0 and the argument'
+      it 'should return a value between 0 and the argument', ->
+        expect(random(10)).to.be.above(0).and.below(10)

     describe 'when called with two arguments', ->
-      it 'should return a value between the two arguments'
+      it 'should return a value between the two arguments', ->
+        expect(random(5, 10)).to.be.above(5).and.below(10)


QUnit.asyncTest() is deprecated. Prefer assert.async()

--- test/src/test.coffee
+++ test/src/test.coffee
@@ -12,11 +12,12 @@
       fixture.removeChild(element)

 browserSessionTest = (name, callback) ->
-  QUnit.asyncTest name, ->
-    withBrowserSession (session, done) ->
+  QUnit.test name, (assert) ->
+    done = assert.async()
+    withBrowserSession (session, teardown) ->
       callback session, ->
+        teardown()
         done()
-        QUnit.start()

 do ->
   {equal} = QUnit


Added new references of components

Added new references of components

--- components/index.coffee
+++ components/index.coffee
@@ -2,10 +2,13 @@

 module.exports =
   # -- Components
+  Aside         : require "./aside"
   Autocomplete  : require "./autocomplete"
   Button        : require "./button"
+  Card          : require "./card"
   Dialog        : require "./dialog"
   Dropdown      : require "./dropdown"
+  Fieldset      : require "./fieldset"
   FontIcon      : require "./font_icon"
   Form          : require "./form"
   Input         : require "./input"
@@ -13,6 +16,10 @@
   List          : require "./list"
   Loading       : require "./loading"
   Navigation    : require "./navigation"
+  ProgressBar   : require "./progress_bar"
   Ripple        : require "./ripple"
-  ProgressBar   : require "./progress_bar"
+  Slider        : require "./slider"
+  Switch        : require "./switch"
+  Tab           : require "./tabs/tab"
+  Tabs          : require "./tabs/tabs"
   # -- Tools


:wrench: Updated atom config

:wrench: Updated atom config

--- config/atom/config.cson
+++ config/atom/config.cson
@@ -20,8 +20,7 @@
     ui:
       panelShadows: true
   "atom-typescript": {}
-  "autocomplete-modules":
-    babelPluginModuleResolver: true
+  "autocomplete-modules": {}
   "autocomplete-plus":
     suppressActivationForEditorClasses: [
       "vim-mode-plus.insert-mode.replace"


Ensure a comment has focus when edit is clicked

--- app/talk/comment.cjsx
+++ app/talk/comment.cjsx
@@ -61,6 +61,7 @@
     @toggleComponent('report')

   onClickEdit: (e) ->
+    React.findDOMNode(@).scrollIntoView()
     @setState editing: true
     @removeFeedback()


make patch archiving more robust

make patch archiving more robust

--- src/smc-hub/postgres-blobs.coffee
+++ src/smc-hub/postgres-blobs.coffee
@@ -586,6 +586,8 @@
                 opts.cb?()
         )

+    # Offlines and archives the patch, unless the string is active very recently, in
+    # which case this is a no-op.
     archive_patches: (opts) =>
         opts = defaults opts,
             string_id : required
@@ -593,13 +595,14 @@
             level     : -1   # the default
             cb        : undefined
         dbg = @_dbg("archive_patches(string_id='#{opts.string_id}')")
-        syncstring = patches = blob_uuid = project_id = undefined
+        syncstring = patches = blob_uuid = project_id = last_active =undefined
+        cutoff = misc.minutes_ago(30)
         where = {"string_id = $::CHAR(40)" : opts.string_id}
         async.series([
             (cb) =>
                 dbg("get project_id")
                 @_query
-                    query : "SELECT project_id, archived FROM syncstrings"
+                    query : "SELECT project_id, archived, last_active FROM syncstrings"
                     where : where
                     cb    : one_result (err, x) =>
                         if err
@@ -610,8 +613,11 @@
                             cb("already archived")
                         else
                             project_id = x.project_id
+                            last_active = x.last_active
                             cb()
             (cb) =>
+                if last_active? and last_active >= cutoff
+                    cb(); return
                 dbg("get patches")
                 @_query
                     query : "SELECT extract(epoch from time) as epoch, * FROM patches"
@@ -623,8 +629,15 @@
                             delete p.epoch
                         cb(err)
             (cb) =>
+                if last_active? and last_active >= cutoff
+                    cb(); return
                 dbg("create blob from patches")
-                blob = new Buffer(JSON.stringify(patches))
+                try
+                    blob = new Buffer(JSON.stringify(patches))
+                catch err
+                    # TODO: This *will* happen if the total length of all patches is too big.
+                    cb(err)
+                    return
                 dbg('save blob')
                 blob_uuid = misc_node.uuidsha1(blob)
                 @save_blob
@@ -635,6 +648,8 @@
                     level      : opts.level
                     cb         : cb
             (cb) =>
+                if last_active? and last_active >= cutoff
+                    cb(); return
                 dbg("update syncstring to indicate patches have been archived in a blob")
                 @_query
                     query : "UPDATE syncstrings"
@@ -642,6 +657,8 @@
                     where : where
                     cb    : cb
             (cb) =>
+                if last_active? and last_active >= cutoff
+                    cb(); return
                 dbg("actually delete patches")
                 @_query
                     query : "DELETE FROM patches"
@@ -701,11 +718,16 @@
                                 v.push(patch)
                             patches = v
                         dbg("insert patches into patches table")
-                        @_query
-                            query    : 'INSERT INTO patches'
-                            values   : patches
-                            conflict : 'ON CONFLICT DO NOTHING'  # in case multiple servers (or this server) are doing this unarchive at once -- this can and does happen sometimes.
-                            cb       : cb
+                        # We break into blocks since there is limit (about 65K) on
+                        # number of params that can be inserted in a single query.
+                        insert_block_size = 1000
+                        f = (i, cb) =>
+                            @_query
+                                query    : 'INSERT INTO patches'
+                                values   : patches.slice(insert_block_size*i, insert_block_size*(i+1))
+                                conflict : 'ON CONFLICT DO NOTHING'  # in case multiple servers (or this server) are doing this unarchive at once -- this can and does happen sometimes.
+                                cb       : cb
+                        async.mapSeries([0...patches.length/insert_block_size], f, cb)
                     (cb) =>
                         async.parallel([
                             (cb) =>


webapp/course/help box: fix #407 and general cleanup

--- src/smc-webapp/editor_course.cjsx
+++ src/smc-webapp/editor_course.cjsx
@@ -2610,14 +2610,21 @@

-                            <Icon name='external-link'/> Grading in SageMathCloud
+                            Grading Courses <Icon name='external-link'/> (by Rob Beezer)
+

+

+
+                            Course Plans and teaching experiences <Icon name='external-link'/> (by Rob Beezer)

-                            <Icon name='external-link'/> Course Management and Collaborative Jupyter Notebooks via SageMathCloud
+                            Course Management and collaborative Jupyter Notebooks <Icon name='external-link'/> (by Tony Hirst)

-                        <Icon name='external-link'/> How H. Ulfarsson creates and grades homework: part 1, part 2, part 3
+                        How H. Ulfarsson creates and grades homework:{' '}
+                        part 1 <Icon name='external-link'/>,{' '}
+                        part 2 <Icon name='external-link'/>,{' '}
+                        part 3 <Icon name='external-link'/>


adding r u mine

adding r u mine


More Atom config updates

More Atom config updates

--- atom/atom/config.cson
+++ atom/atom/config.cson
@@ -57,13 +57,18 @@
     ]
     projectHome: "/Users/Felix/Development"
     telemetryConsent: "limited"
+    themes: [
+      "dracula-ui"
+      "dracula-theme"
+    ]
     useCustomTitleBar: true
   docblockr: {}
   editor:
     autoIndentOnPaste: false
     fontFamily: "Menlo"
-    fontSize: 12
-    invisibles: {}
+    invisibles:
+      space: "•"
+      tab: "›"
     showInvisibles: true
     tabType: "soft"
     useHardwareAcceleration: true
@@ -75,6 +80,11 @@
   "filetype-color":
     enabled: "false"
   "flex-tool-bar": {}
+  "genesis-ui":
+    distractionFree:
+      hideBottom: false
+      hideFiles: false
+      hideTabs: false
   "git-diff": {}
   "jscs-fixer":
     defaultPreset: "airbnb"
@@ -141,6 +151,13 @@
     showOnRightSide: true
   "vim-mode":
     useClipboardAsDefaultRegister: true
+  "vim-mode-plus":
+    smoothScrollOnFullScrollMotion: true
+    smoothScrollOnHalfScrollMotion: true
+    stayOnDelete: true
+    stayOnYank: true
+    useClipboardAsDefaultRegister: true
+    useSmartcaseForSearch: true
   "vim-surround":
     surroundKey: "S"
   welcome:


Adding test to cover 'states' next() function

--- test/tests/LayerStatesTest.coffee
+++ test/tests/LayerStatesTest.coffee
@@ -73,6 +73,36 @@

 	describe "Properties", ->

+		it "should bring back the 'default' state values when using 'next'", (done) ->
+
+			layer = new Layer
+			layer.states.add
+				stateA: {x:100, rotation: 90}
+				stateB: {x:200, rotation: 180}
+			layer.states.animationOptions =
+				curve: "linear"
+				time: 0.05
+
+			layer.x.should.equal 0
+
+			ready = (animation, layer) ->
+				switch layer.states.current
+					when "stateA"
+						layer.x.should.equal 100
+						layer.rotation.should.equal 90
+						layer.states.next()
+					when "stateB"
+						layer.x.should.equal 200
+						layer.rotation.should.equal 180
+						layer.states.next()
+					when "default"
+						layer.x.should.equal 0
+						layer.rotation.should.equal 0
+						done()
+
+			layer.on "end", ready
+			layer.states.next()
+
 		it "should set scroll property", ->

 			layer = new Layer


Cleaned up message store test. Perhaps it will work on Travis now?

--- test/unit/messageStoreTest.coffee
+++ test/unit/messageStoreTest.coffee
@@ -2,26 +2,17 @@
 sinon = require "sinon"
 http = require "http"
 messageStore = require "../../lib/middleware/messageStore"
-MongoClient = require('mongodb').MongoClient
-config = require "../../lib/config/config"
-transactions = require "../../lib/model/transactions"
+Transaction = require("../../lib/model/transactions").Transaction

-collection = null
 transactionId = null
-db = null

 beforeEach (done) ->
-	MongoClient.connect config.mongo.url, {native_parser:true}, (error,db) ->
-		if error
-			return done error
-		root = exports ? that
-		db.collection "transactions", (err, coll) ->
-			coll.remove (err, doc) ->
-			collection = coll
-			done()
+	Transaction.remove {}, ->
+		done();
+
 afterEach (done)->
-	collection.remove (err, doc) ->
-		done()
+	Transaction.remove {}, ->
+		done();

 describe "MessageStore", ->
 	req = new Object()
@@ -86,7 +77,7 @@
 		it "should be able to save the transaction in the db", (done) ->
 			messageStore.storeTransaction ctx, (error, result) ->
 				should.not.exist(error)
-				transactions.Transaction.findOne { '_id': result._id }, (error, trans) ->
+				Transaction.findOne { '_id': result._id }, (error, trans) ->
 					should.not.exist(error)
 					(trans != null).should.be.true
 					trans.clientID.should.equal "Master_OpenMRS_Instance"
@@ -111,7 +102,7 @@
 				ctx.transactionId = storedTrans._id
 				messageStore.storeResponse ctx, (err2) ->
 					should.not.exist(err2)
-					transactions.Transaction.findOne { '_id': storedTrans._id }, (err3, trans) ->
+					Transaction.findOne { '_id': storedTrans._id }, (err3, trans) ->
 						should.not.exist(err3)
 						(trans != null).should.true
 						trans.response.status.should.equal 201


Revert "resurrection: remove active content display from main tab view when showing another display"

This reverts commit 2375cfd9e199fca530f1a6b97f422a1ec21165c1.

Conflicts:
	client/Main/ContentDisplay/ContentDisplayController.coffee

--- client/Main/ContentDisplay/ContentDisplayController.coffee
+++ client/Main/ContentDisplay/ContentDisplayController.coffee
@@ -31,7 +31,6 @@

     {@mainTabView} = KD.singleton "mainView"
     activePane = @mainTabView.getActivePane()
-    @hideDisplay activePane.getMainView()  if activePane
     @mainTabView.addPane tabPane

     KD.singleton('dock').navController.selectItemByName 'Activity'


FlexSplit: Storage: keepExpandStatuses, wait 500ms before resurrect

--- client/new-stack-editor/lib/views/flexsplit/storage.coffee
+++ client/new-stack-editor/lib/views/flexsplit/storage.coffee
@@ -20,7 +20,7 @@
   addView: (view, identifier, options = {}) ->

     options.restore          ?= yes
-    options.keepExpandStatus ?= no # Experimental
+    options.keepExpandStatus ?= yes # Experimental

     view.on [ Flex.EVENT_RESIZED, Flex.EVENT_HIDDEN ], (fractions) =>
       @set identifier, fractions
@@ -41,14 +41,16 @@

     @viewCount++

-    @get identifier, (fractions) =>
+    kd.utils.wait 500, =>

-      sizes = view.getOption 'sizes'
-      fractions ?= sizes
+      @get identifier, (fractions) =>

-      if options.restore and fractions
-        view.setFractions fractions
-        @set identifier, fractions
+        sizes = view.getOption 'sizes'
+        fractions ?= sizes
+
+        if options.restore and fractions
+          view.setFractions fractions, { initialFractions: sizes }
+          @set identifier, fractions

   get: (identifier, callback) ->


Move remaining fetch functions to beforeEach Similar to what was done at d43da535c53f1574dffd8ec3ed7427cbce206030 with the scheduleData variable

--- test/api/test-departure.coffee
+++ test/api/test-departure.coffee
@@ -11,87 +11,19 @@

 # stub dependencies
 userData =
-  fetch: (authToken) ->
-    user =
-      if authToken is 'foo-token'
-        id: 'foo'
-        displayName: 'Foo User'
-      else null
-
-    Q user
-
+  '@noCallThru': true
+
+scheduleData =
+  '@noCallThru': true
+
+routeData =
+  '@noCallThru': true
+
+departureData =
   '@noCallThru': true

 security = proxyquire '../../lib/security',
   '../data/user': userData
-
-scheduleData =
-  '@noCallThru': true
-
-routeData =
-  fetchDetail: (routeId) ->
-    route =
-      if routeId is '123'
-        id: '123'
-        description: 'Route 123'
-        directions: [
-          {
-            id: '2'
-            description: 'Eastbound'
-            stops: [
-              { id: 'STP1A', description: 'Stop 1-A' },
-              { id: 'STP2A', description: 'Stop 2-A' },
-              { id: 'STP3A', description: 'Stop 3-A' },
-              { id: 'STP4A', description: 'Stop 4-A' },
-              { id: 'STP5A', description: 'Stop 5-A' }
-            ]
-          },
-          {
-            id: '3'
-            description: 'Westbound'
-            stops: [
-              { id: 'STP5A', description: 'Stop 5-A' },
-              { id: 'STP4A', description: 'Stop 4-A' },
-              { id: 'STP3A', description: 'Stop 3-A' },
-              { id: 'STP2A', description: 'Stop 2-A' },
-              { id: 'STP1A', description: 'Stop 1-A' }
-            ]
-          }
-        ]
-      else if routeId is '456'
-        id: '456'
-        description: 'Route 456'
-        directions: [
-          {
-            id: '1'
-            description: 'Northbound'
-            stops: [
-              { id: 'STP1B', description: 'Stop 1-B' },
-              { id: 'STP2B', description: 'Stop 2-B' },
-              { id: 'STP3B', description: 'Stop 3-B' },
-              { id: 'STP4B', description: 'Stop 4-B' },
-              { id: 'STP5B', description: 'Stop 5-B' }
-            ]
-          },
-          {
-            id: '4'
-            description: 'Southbound'
-            stops: [
-              { id: 'STP5B', description: 'Stop 5-B' },
-              { id: 'STP4B', description: 'Stop 4-B' },
-              { id: 'STP3B', description: 'Stop 3-B' },
-              { id: 'STP2B', description: 'Stop 2-B' },
-              { id: 'STP1B', description: 'Stop 1-B' }
-            ]
-          }
-        ]
-      else null
-    Q route
-
-  '@noCallThru': true
-
-departureData =
-  '@noCallThru': true

 nowMoment = {}
 momentStub = ->
@@ -114,6 +46,15 @@
   beforeEach ->
     process.env.RADBUS_TIMEZONE = 'America/Chicago'
     process.env.RADBUS_FUTURE_MINUTES = '60'
+
+    userData.fetch = (authToken) ->
+      user =
+        if authToken is 'foo-token'
+          id: 'foo'
+          displayName: 'Foo User'
+        else null
+
+      Q user

     scheduleData.fetch = (userId) ->
       schedule =
@@ -144,7 +85,68 @@

       Q schedule

+    routeData.fetchDetail = (routeId) ->
+      route =
+        if routeId is '123'
+          id: '123'
+          description: 'Route 123'
+          directions: [
+            {
+              id: '2'
+              description: 'Eastbound'
+              stops: [
+                { id: 'STP1A', description: 'Stop 1-A' },
+                { id: 'STP2A', description: 'Stop 2-A' },
+                { id: 'STP3A', description: 'Stop 3-A' },
+                { id: 'STP4A', description: 'Stop 4-A' },
+                { id: 'STP5A', description: 'Stop 5-A' }
+              ]
+            },
+            {
+              id: '3'
+              description: 'Westbound'
+              stops: [
+                { id: 'STP5A', description: 'Stop 5-A' },
+                { id: 'STP4A', description: 'Stop 4-A' },
+                { id: 'STP3A', description: 'Stop 3-A' },
+                { id: 'STP2A', description: 'Stop 2-A' },
+                { id: 'STP1A', description: 'Stop 1-A' }
+              ]
+            }
+          ]
+        else if routeId is '456'
+          id: '456'
+          description: 'Route 456'
+          directions: [
+            {
+              id: '1'
+              description: 'Northbound'
+              stops: [
+                { id: 'STP1B', description: 'Stop 1-B' },
+                { id: 'STP2B', description: 'Stop 2-B' },
+                { id: 'STP3B', description: 'Stop 3-B' },
+                { id: 'STP4B', description: 'Stop 4-B' },
+                { id: 'STP5B', description: 'Stop 5-B' }
+              ]
+            },
+            {
+              id: '4'
+              description: 'Southbound'
+              stops: [
+                { id: 'STP5B', description: 'Stop 5-B' },
+                { id: 'STP4B', description: 'Stop 4-B' },
+                { id: 'STP3B', description: 'Stop 3-B' },
+                { id: 'STP2B', description: 'Stop 2-B' },
+                { id: 'STP1B', description: 'Stop 1-B' }
+              ]
+            }
+          ]
+        else null
+
+      Q route
+
   afterEach ->
+    # delete enviroment variables so they don't affect other tests
     delete process.env.RADBUS_TIMEZONE
     delete process.env.RADBUS_FUTURE_MINUTES


Update LimitationsManager unit tests

Update LimitationsManager unit tests


got a really nice --watch flag for bin/node_coffee going, thanks to Node.js' process.watchFile. Can be used with --print, --lint, --tokens, --run, or whatever your needs may be

--- command_line.coffee
+++ command_line.coffee
@@ -14,7 +14,7 @@
   ['-i', '--interactive',   'run an interactive CoffeeScript REPL']
   ['-r', '--run',           'compile and run a CoffeeScript']
   ['-o', '--output [DIR]',  'set the directory for compiled JavaScript']
-  # ['-w', '--watch',         'watch scripts for changes, and recompile']
+  ['-w', '--watch',         'watch scripts for changes, and recompile']
   ['-p', '--print',         'print the compiled JavaScript to stdout']
   ['-l', '--lint',          'pipe the compiled JavaScript through JSLint']
   ['-e', '--eval',          'compile a string from the command line']
@@ -23,8 +23,6 @@
   ['-v', '--version',       'display CoffeeScript version']
   ['-h', '--help',          'display this help message']
 ]
-
-WATCH_INTERVAL: 0.5

 options: {}
 sources: []
@@ -42,6 +40,7 @@
     flags: sources[(separator + 1)...sources.length]
     sources: sources[0...separator]
   process.ARGV = flags
+  watch_scripts() if options.watch
   compile_scripts()
   this

@@ -72,8 +71,15 @@
 # or JSLint results.
 compile_scripts: ->
   return unless source: sources.shift()
+  fs.cat(source).addCallback (code) ->
+    compile_script(source, code)
+    compile_scripts()
+
+# Compile a single source script, containing the given code, according to the
+# requested options. Both compile_scripts and watch_scripts share this method.
+compile_script: (source, code) ->
   opts: options
-  fs.cat(source).addCallback (code) ->
+  try
     if      opts.tokens   then coffee.print_tokens coffee.tokenize code
     else if opts.tree     then puts coffee.tree(code).toString()
     else
@@ -82,7 +88,17 @@
       else if opts.print  then puts js
       else if opts.lint   then lint js
       else                     write_js source, coffee.compile code
-    compile_scripts()
+  catch err
+    if opts.watch then puts err.message else throw err
+
+# Watch a list of source CoffeeScript files, recompiling them every time the
+# files are updated.
+watch_scripts: ->
+  for source in sources
+    process.watchFile source, {persistent: true, interval: 500}, (curr, prev) ->
+      return if curr.mtime.getTime() is prev.mtime.getTime()
+      fs.cat(source).addCallback (code) -> compile_script(source, code)
+

 # Write out a JavaScript source file with the compiled code.
 write_js: (source, js) ->
@@ -122,3 +138,4 @@

   paths: oparser.parse(process.ARGV)
   sources: paths[2...paths.length]
+


config: init kite in collaboration worker

--- config/main.prod.coffee
+++ config/main.prod.coffee
@@ -480,7 +480,7 @@
     collaboration :
       group             : "socialapi"
       supervisord       :
-        command         : "#{GOBIN}/collaboration -c #{socialapi.configFilePath}"
+        command         : "#{GOBIN}/collaboration -kite-init -c #{socialapi.configFilePath}"

     gatekeeper          :
       group             : "socialapi"


default TESTS to all tests

default TESTS to all tests

--- test/integration_runner.coffee
+++ test/integration_runner.coffee
@@ -27,8 +27,10 @@
   mkdirp.sync path

 runTests = (callback) ->
-  tests = APP_DIRECTORY + '/' + process.env.TESTS
-  tests ?= TEST_DIRECTORY
+  tests = if process.env.TESTS?
+    APP_DIRECTORY + '/' + process.env.TESTS
+  else
+    TEST_DIRECTORY

   testBrowser = (browser) -> (browserTested) ->
     console.log "\nTesting against: #{browser}\n"


code cleaning

code cleaning

--- proxy.coffee
+++ proxy.coffee
@@ -88,12 +88,12 @@
         @app.use passport.initialize()
         @app.use passport.session()

-        @app.use connect.logger('
+        @app.use connect.logger '
             \\n \\033[33;22m :date \\033[0m
             \\n \\033[37;1m :method \\033[0m \\033[30;1m :url \\033[0m
             \\n  >>> perform
             \\n  Send to client: :status
-            \\n  <<<  [:response-time ms] ')
+            \\n  <<<  [:response-time ms]'
         @app.use connect.static 'public'

         @app.use (err, req, res, next) ->


move fullscreen up a little.

move fullscreen up a little.

--- src/smc-webapp/app_shared.cjsx
+++ src/smc-webapp/app_shared.cjsx
@@ -409,7 +409,7 @@

     render: ->
         icon = if @props.fullscreen then 'compress' else 'expand'
-        top_px = if @props.show_global_info then "#{announce_bar_offset + 1}px" else '1px'
+        top_px = '-1px'

         tip_style =
             position     : 'fixed'
@@ -418,10 +418,9 @@
             top          : top_px
             borderRadius : '3px'

-
         icon_style =
             fontSize   : '13pt'
-            padding    : 4
+            padding    : 2
             color      : COLORS.GRAY
             cursor     : 'pointer'


fix bug in tasks timetravel (the old one)

--- src/smc-webapp/tasks/list.cjsx
+++ src/smc-webapp/tasks/list.cjsx
@@ -123,7 +123,7 @@
           row_count={@props.visible.size+1}
           row_renderer={(obj) => @render_task(obj.index, obj.key)}
           row_key={(index) => @props.visible.get(index) ? 'filler'}
-          cache_id={@props.actions.name}
+          cache_id={@props.actions?.name}
           scroll_top={@props.scroll?.get('scrollTop')}
           hide_resize={false}
         />


dont transmit if counter is 0

dont transmit if counter is 0

--- app/report.coffee
+++ app/report.coffee
@@ -23,8 +23,9 @@
     beaconData = (for id, beacon of beacons
       { uuid: beacon.id(),
       exits: beacon.counter,
-      battery: beacon.batteryLevel })
+      battery: beacon.batteryLevel } if beacon.counter > 0 )

+    console.log beaconData
     { mac: ID,
     timestamp: (new Date()),
     beacons: beaconData }


fix get_data

fix get_data

--- app/server.coffee
+++ app/server.coffee
@@ -184,7 +184,7 @@
     Context.value {promise, clicks, component}

   fn 'get_data', 'Fetches metric data', (ctx, args...) ->
-    Context.value server.get_data server.args_to_server_params {args, default_options: ctx.options()}
+    Context.value server.get_data args_to_server_params ctx, args

   init: ->
     if settings.get('type') == 'lead'


fix(coverage): create lcov.info

fix(coverage): create lcov.info

--- src/steps/create-report.coffee
+++ src/steps/create-report.coffee
@@ -12,7 +12,7 @@
       coverageFile = 'coverage/coverage-coffee.json'

       Report = istanbul.Report
-      report = Report.create 'html', { dir: 'coverage/html-report' }
+      report = Report.create 'lcov', { dir: 'coverage' }
       Collector = istanbul.Collector
       collector = new Collector()