Fix tests

Fix tests

--- server/swf.coffee
+++ server/swf.coffee
@@ -18,7 +18,6 @@
 Commands.before.insert (userId, command) ->
   step = Steps.findOne(command.stepId, {transform: Transformations.Step})
   _.extend command, step.insertCommandData() # an old version of client-side code might insert a Command in old format, so we need to override the Command data in server-side code
-  return true if command.isDryRunWorkflowExecution
   Users.update(userId, {$inc: {executions: 1}})
   try
     user = Users.findOne(userId, {transform: Transformations.User})
@@ -41,23 +40,24 @@
         step.userId
       ]
       input: JSON.stringify(input)
-    data = startWorkflowExecutionSync(params)
-    command.runId = data.runId
+    if not command.isDryRunWorkflowExecution
+      data = startWorkflowExecutionSync(params)
+      command.runId = data.runId
   catch error
     Users.update(userId, {$inc: {executions: -1}}) # revert the update; this is better than fetch-and-check, because it prevents race conditions
     throw error
   true

 Commands.before.remove (userId, command) ->
-  return true if command.isDryRunWorkflowExecution
+  if not command.isCompleted and not command.isFailed # cancelled by user; reimburse trial if command is cancelled by user
+    Users.update(userId, {$inc: {executions: -1}})
   step = Steps.findOne(command.stepId, {transform: Transformations.Step})
   params =
     domain: step.domain()
     workflowId: command._id
   try
-    requestCancelWorkflowExecutionSync(params)
+    if not command.isDryRunWorkflowExecution
+      requestCancelWorkflowExecutionSync(params)
   catch error
     throw error if error.code isnt "UnknownResourceFault" # Workflow execution may have already been terminated
-  if not command.isCompleted and not command.isFailed # cancelled by user; reimburse trial if command is cancelled by user
-    Users.update(userId, {$inc: {executions: -1}})
   true


Bugfix.

Bugfix.

--- server/pdf.coffee
+++ server/pdf.coffee
@@ -11,22 +11,8 @@

     processPDF = (finalCallback) -> (pdf) ->
       counter = pdf.numPages
-      finalError = null
-
       for pageNumber in [1..pdf.numPages]
-        if finalError
-          # We call finalCallback only after all pages have been processed and thus callbacks called
-          counter--
-          finalCallback finalError if counter == 0
-          return
-
         pdf.getPage(pageNumber).then (page) ->
-          if finalError
-            # We call finalCallback only after all pages have been processed and thus callbacks called
-            counter--
-            finalCallback finalError if counter == 0
-            return
-
           # pageNumber is not necessary current page number once promise is resolved, use page.pageNumber instead
           progressCallback (page.pageNumber - 1) / pdf.numPages

@@ -51,7 +37,7 @@
                 counter--
                 if counter == 0
                   progressCallback 1.0
-                  finalCallback finalError
+                  finalCallback()

               appendText: (geom) ->
                 width = geom.canvasWidth * geom.hScale
@@ -93,11 +79,8 @@
             page.render(renderContext).then ->
               return # Do nothing
             , (error) ->
-              finalError = error: "PDF Error page #{ page.pageNumber }", message: error
-
-              # We call finalCallback only after all pages have been processed and thus callbacks called
-              counter--
-              finalCallback finalError if counter == 0
+              # TODO: This exception is not nicely displayed in the console, future.wrap seems to remove payload
+              throw new Error error

         pdf.getMetadata(pageNumber).then (metadata) ->
           # TODO: If we will process metadata, too, we have to make sure finalCallback is called after only once after everything is finished
@@ -106,7 +89,10 @@
       return # So that we do not return the results of the for-loop

     processError = (finalCallback) -> (message, exception) ->
-      finalCallback error: "PDF Error", message: message, exception: exception
+      # TODO: This exception is not nicely displayed in the console, future.wrap seems to remove payload
+      error = new Error message
+      error.exception = exception
+      throw error

     # "finalCallback" has to be called only once to unblock
     processAll = future.wrap (finalCallback) ->


Remove debug statement.

Remove debug statement.

--- services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
+++ services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
@@ -585,7 +585,7 @@

 		$scope.toggleFullTCStateCollapse = () ->
 			if $scope.project.features.trackChanges
-				if true or $scope.reviewPanel.showPerUserTCNotice
+				if $scope.reviewPanel.showPerUserTCNotice
 					$scope.openPerUserTCNoticeModal()
 				$scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed
 			else


add a delay to announcing user search results in admin tools

fixes CNVS-39380

test plan:
- go to admin tools
- view notifications
- Type in 2 letters
- note that no results populated yet
- type in the 3rd letter
- note that you can hear the number of results

Change-Id: I0ec77337cd83bee3878bcfa853cf5ea6286443ad
Reviewed-on: https://gerrit.instructure.com/126724
Reviewed-by: Ed Schiebel <0ca8a83847a5889cf05cb219c0ab09f6b646243b@instructure.com>
Tested-by: Jenkins
QA-Review: Deepeeca Soundarrajan <5bcd4b623f00b7fc2942cd1c65c245b30a48a61f@instructure.com>
Product-Review: Felix Milea-Ciobanu <9384aa226ac9d3237ed0bc52e18ffcb251ee0bf7@instructure.com>

--- app/coffeescripts/views/accounts/admin_tools/UserDateRangeSearchFormView.coffee
+++ app/coffeescripts/views/accounts/admin_tools/UserDateRangeSearchFormView.coffee
@@ -69,7 +69,9 @@
       @collection.on 'sync', @notificationsFound

     resultsFound: =>
-      $.screenReaderFlashMessage(I18n.t('%{length} results found', { length: @usersView.collection.length }))
+      setTimeout(() =>
+        $.screenReaderFlashMessageExclusive(I18n.t('%{length} results found', { length: @usersView.collection.length }))
+      , 500)

     notificationsFound: =>
       $.screenReaderFlashMessage(I18n.t('%{length} notifications found', { length: @collection.length }))


#16, Closes #19. Changed u/d to scroll half a page, added c-f/c-b to scroll full page

--- extension/packages/commands.coffee
+++ extension/packages/commands.coffee
@@ -78,12 +78,20 @@
 command_l = (vim) ->
   vim.window.scrollBy(getPref 'scroll_step', 0)

-# Scroll down a page
+# Scroll down half a page
 command_d = (vim) ->
+  vim.window.scrollBy(0, vim.window.innerHeight / 2)
+
+# Scroll up half a page
+command_u = (vim) ->
+  vim.window.scrollBy(0, -vim.window.innerHeight / 2)
+  #
+# Scroll down full a page
+command_cf = (vim) ->
   vim.window.scrollBy(0, vim.window.innerHeight)

-# Scroll up a page
-command_u = (vim) ->
+# Scroll up full a page
+command_cb = (vim) ->
   vim.window.scrollBy(0, -vim.window.innerHeight)

 # Activate previous tab
@@ -206,8 +214,10 @@
     'k|c-y':    [ command_k_cy,   "Scroll Up" ]
     'h':        [ command_h,      "Scroll Left" ]
     'l':        [ command_l ,     "Scroll Right" ]
-    'd':        [ command_d,      "Scroll a Page Down" ]
-    'u':        [ command_u,      "Scroll a Page Up" ]
+    'd':        [ command_d,      "Scroll half a Page Down" ]
+    'u':        [ command_u,      "Scroll half a Page Up" ]
+    'c-f':      [ command_cf,     "Scroll a full Page Down" ]
+    'c-b':      [ command_cb,     "Scroll a full Page Up" ]
   'tabs':
     't':        [ command_t,      "Open New Blank tab" ]
     'J|g,T':    [ command_J_gT,   "Go to the Previous tab" ]


FIgure out which class defines a template, so we can figure out which class <super> refers to.

--- elements/QuetzalElement.coffee
+++ elements/QuetzalElement.coffee
@@ -77,11 +77,15 @@
     #     style.innerHTML = elementClass::style
     #   root.appendChild style

-    if elementClass::hasOwnProperty "template"
+    if @template?
       root = @webkitCreateShadowRoot()
-      root.innerHTML = elementClass::template
-      for superElement in root.querySelectorAll "super"
-        baseClass = elementClass.__super__.constructor
+      root.innerHTML = @template
+      superElement = root.querySelector "super"
+      if superElement?
+        classDefiningTemplate = @_classDefiningTemplate elementClass
+        baseClass = classDefiningTemplate.__super__.constructor
+        unless baseClass?
+          throw "Used <super> in #{classDefiningTemplate.name}, but couldn't find superclass."
         superInstance = new baseClass()
         superInstance.innerHTML = superElement.innerHTML
         superElement.parentNode.replaceChild superInstance, superElement
@@ -89,6 +93,9 @@
           @[ name ] = value
       for subelement in root.querySelectorAll "[id]"
         @$[ subelement.id ] = subelement
+    # else if elementClass isnt QuetzalElement
+    #   root = @webkitCreateShadowRoot()
+    #   root.appendChild document.createElement "shadow"

     # @_wrappers[ elementClass.name ] = wrapper

@@ -101,6 +108,15 @@

   wrapper: ( elementClass ) ->
     @_wrappers[ elementClass.name ]
+
+  # Figure out which class in the hierarchy defines a template, so we can figure
+  # out which class <super> refers to.
+  _classDefiningTemplate: ( elementClass ) ->
+    while elementClass?
+      if elementClass::hasOwnProperty "template"
+        return elementClass
+      elementClass = elementClass.__super__?.constructor
+    null

   # Holds "private" properties referenced by @property.
   _properties: {}


Rename charts to axes and initLoad to reRenderChart.

--- app/javascripts/controllers.coffee
+++ app/javascripts/controllers.coffee
@@ -104,34 +104,46 @@
         # Put a copy of the default charts (X and Y axes) in the scope.
         # This charts object changes when the user selects a different
         # axis to chart.
-        $scope.charts = _.clone(Correlation.DEFAULT_CHARTS[collection])
+        $scope.axes = _.clone(Correlation.DEFAULT_AXES[collection])
         # Place the chart configuration object in the scope.
         $scope.config =
           data: data
           padding: padding
-          charts: $scope.charts
-        # The initial load flag.
-        $scope.initLoad = true
-
-        # If any X or Y axis selection is changed, re-render the charts and set
-        # the initial load flag to false. The flag prevents a chart rendering
-        # call from taking place while the initial collection detail page
-        # load is taking place and causing the rendering to fail. The page must
-        # first fully load, at which point the first chart rendering will be
-        # triggered by the collection detail directive.
+          axes: $scope.axes
+        # The re-render flag.
+        $scope.reRenderChart = false
+
+        # If the user changes any X or Y axis selection, then re-render the charts.
+        # The re-render flag prevents a chart rendering call from taking place
+        # on the initial collection detail page digest, since that would
+        # cause the rendering to fail. The page must first be fully digested,
+        # at which point the first chart rendering will be triggered by the
+        # collection detail directive. Any subsequent change by the user to the
+        # axes activates the listener, which re-renders the charts.
         #
-        # TODO - how could the charts object change before the page is loaded?
+        # TODO - how could the axes object change before the page is loaded?
+        #   Does Angular always invoke the watcher body on the first digest?
+        #   I wouldn't expect that to be true.
         # TODO - how often is the watch (and therefore equals test) performed?
+        # TODO - this watch is called on every $digest. Since the watch does a
+        #   deep equality test, this might be expensive. How often is the
+        #   watch expression evaluated, i.e., how often is $digest performed
+        #   on this page? I doubt if the equality test is a problem, but I'm
+        #   curious how often in general redigest occurs.
         watcher = ->
-          if not $scope.initLoad
+          # If the re-render flag is set, then render the charts and leave the
+          # flag set to true. Otherwise, set the flag to true so that each
+          # subsequent charts change triggers a re-render.
+          if $scope.reRenderChart
             Correlation.renderCharts($scope.config)
           else
-            $scope.initLoad = false
+            $scope.reRenderChart = true
+
         # Since charts is an object, the objectEquality flag is set to true.
         # AngularJS then copies the object for later comparison and uses
         # angular.equals to recursively compare the object properties rather
         # than a simple === test, which is the default.
-        $scope.$watch('charts', watcher, true)
+        $scope.$watch('axes', watcher, true)

         # If the project is the default, then remove it from the URL.
         ControllerHelper.cleanBrowserUrl($rootScope.project)


readonly and timeout fixes

readonly and timeout fixes

--- client/landing/app/Applications/Activity.kdapplication/views/activityactions.coffee
+++ client/landing/app/Applications/Activity.kdapplication/views/activityactions.coffee
@@ -10,6 +10,8 @@
       cssClass      : "share-input"
       type          : "text"
       placeholder   : "shortening..."
+      attributes    :
+        readonly    : yes
       width         : 50

     unless @getDelegate()._shorten


Clean up loader_common

Clean up loader_common

- name iframe/doc variables according to meaning, not type
 - don't unnecessarily pass around config by json-ing and then decoding
again.

--- app/js/loader/loader_common.coffee
+++ app/js/loader/loader_common.coffee
@@ -1,60 +1,54 @@
+mkWrapDiv = (id, domContent) ->
+  wrapper = document.createElement('div')
+  wrapper.id = id
+  wrapper.appendChild(domContent)
+  wrapper
+
 window.FACTLINK_START_LOADER = ->
   if window.FACTLINK_LOADED
     console.error 'FACTLINK already loaded!'
     return
   window.FACTLINK_LOADED = true

-  #### Create iframe
-  iframe = document.createElement('iframe')
-  iframe.style.display = 'block'
-  iframe.style.border = '0px solid transparent'
-  iframe.id = 'factlink-iframe'
+  #### Create iframe so jslib's namespace (window) doesn't collide with any content window.
+  jslib_jail_iframe = document.createElement('iframe')
+  jslib_jail_iframe.style.display = 'block'
+  jslib_jail_iframe.style.border = '0px solid transparent'
+  jslib_jail_iframe.id = 'factlink-iframe'

   # Wrappers for increased CSS specificity
-  div = document.createElement('div')
-  wrapper1 = document.createElement('div')
-  wrapper2 = document.createElement('div')
-  wrapper3 = document.createElement('div')
+  outerWrapperEl =
+    mkWrapDiv 'fl-wrapper-1',
+      mkWrapDiv 'fl-wrapper-2',
+        mkWrapDiv 'fl-wrapper-3',
+          mkWrapDiv 'fl', jslib_jail_iframe

   # Keep in sync with #fl-wrapper-1 in basic.less
-  wrapper1.style.visibility = 'hidden'; # Prevent showing stuff when css is not loaded yet
-  wrapper1.style.position = 'absolute'; # Prevent reflowing of the page
+  outerWrapperEl.style.visibility = 'hidden'; # Prevent showing stuff when css is not loaded yet
+  outerWrapperEl.style.position = 'absolute'; # Prevent reflowing of the page

-  wrapper1.id = 'fl-wrapper-1'
-  wrapper2.id = 'fl-wrapper-2'
-  wrapper3.id = 'fl-wrapper-3'
-  div.id = 'fl'
-
-  wrapper1.appendChild wrapper2
-  wrapper2.appendChild wrapper3
-  wrapper3.appendChild div
-  document.body.appendChild wrapper1
-
-  div.insertBefore iframe, div.firstChild
-
+  document.body.appendChild outerWrapperEl
   #### Create

   """
-  iframeDoc.close()
+  jslib_jail_doc.close()


updated external link help test

updated external link help test

updated list of autogenerated tabs now on the landing page and also included directions on how one reorders the links

--- app/pages/lab/project-details.cjsx
+++ app/pages/lab/project-details.cjsx
@@ -158,7 +158,7 @@

             External links

-            Adding an external link will make it appear as a new tab alongside the science, classify, and talk tabs.
+            Adding an external link will make it appear as a new tab alongside the about, classify, talk, and collect tabs. You can rearrange the displayed order by clicking and dragging on the left gray tab next to each link below.
             <ExternalLinksEditor project={@props.project} />


Run grunt install inside service directory

--- server-ce/Gruntfile.coffee
+++ server-ce/Gruntfile.coffee
@@ -241,7 +241,8 @@
 				grunt.log.writeln "Config file already exists. Skipping."
 				callback()

-		runGruntInstall: (dir, callback = (error) ->) ->
+		runGruntInstall: (service, callback = (error) ->) ->
+			dir = service.name
 			proc = spawn "grunt", ["install"], stdio: "inherit", cwd: dir
 			proc.on "close", () ->
 				callback()


Reset year for all except paid students and admin

--- server/api/setupApi.coffee
+++ server/api/setupApi.coffee
@@ -540,17 +540,18 @@
         if not req.user?.admin
             res.status(403).send('No permissions')
             return
-        adminUser = await InformaticsUser.findAdmin()

         runForUser = (user) ->
-            await groups.moveUserToGroup(adminUser, user._id, "unknown")
+            userPrivate = await UserPrivate.findById(user._id)
+            registeredUser = await RegisteredUser.findByKey(user._id)
+            if registeredUser.admin or (user.userList == "stud" and userPrivate.paidTill > new Date())
+                logger.info("Will not move user #{user._id} to unknown group")
+                return
             await user.setUserList("unknown")
             logger.info("Moved user #{user._id} to unknown group")

         users = await User.findAll()
         for user in users
-            #if user.userList == "stud"
-            #    continue
             runForUser(user)
         res.send('OK')


Prevent add users as admins

Prevent add users as admins

--- server/lib/accounts.coffee
+++ server/lib/accounts.coffee
@@ -83,7 +83,7 @@

 	if roles.length is 0
 		# when inserting first user give them admin privileges otherwise make a regular user
-		hasAdmin = RocketChat.models.Users.findOne({ roles: 'admin' }, {fields: {_id: 1}})
+		hasAdmin = RocketChat.models.Users.findOne({ roles: { $in: 'admin' } }, {fields: {_id: 1}})
 		if hasAdmin?
 			roles.push 'user'
 		else


Update publication.coffee

Update publication.coffee

--- server/publications.coffee
+++ server/publications.coffee
@@ -10,13 +10,11 @@
 Meteor.publish "flowers", ->
   Flowers.find()

-<<<<<<< HEAD
 Meteor.publish "friends", ->
   if @userId
     friendList = Meteor.users.findOne(@userId).profile.friendList
     if friendList
       friendsCursor = Meteor.users.find({_id: {$in: friendList}}, {fields: {services: 0}})
-=======
+
 Meteor.publish "locations", ->
-  Locations.find()
->>>>>>> 0944381c1ae56da619563fa4cad47d55dace84a3
+  Locations.find()


centering avatarview's tooltip

centering avatarview's tooltip

--- client/app/Applications/Members.kdapplication/ContentDisplays/profileview.coffee
+++ client/app/Applications/Members.kdapplication/ContentDisplays/profileview.coffee
@@ -353,7 +353,7 @@

     if KD.isMine @memberData
       avatarOptions.tooltip =
-        offset      : top: 0, left: -3
+        # offset      : top: 0, left: -3
         title       : "
Click avatar to edit
"
         placement   : "below"
         arrow       :


Show a book icon for `*.bib` files.

Show a book icon for `*.bib` files.

--- services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee
+++ services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee
@@ -60,6 +60,8 @@
 				return "table"
 			else if ext in ["py", "r"]
 				return "file-text"
+			else if ext in ['bib']
+				return 'book'
 			else
 				return "file"
 	]


better add server example

better add server example

--- salvus/compute.coffee
+++ salvus/compute.coffee
@@ -151,7 +151,7 @@

 require('compute').compute_server(db_hosts:['smc0-us-central1-c'],cb:(e,s)->console.log(e);s.add_server(experimental:true, host:'compute0-amath-us', cb:(e)->console.log("done",e)))

-         require('compute').compute_server(keyspace:'devel',cb:(e,s)->console.log(e);s.add_server(host:'localhost', cb:(e)->console.log("done",e)))
+         require('compute').compute_server(cb:(e,s)->console.log(e);s.add_server(host:'localhost', cb:(e)->console.log("done",e)))
     ###
     add_server: (opts) =>
         opts = defaults opts,


Clean up emulatore

Clean up emulatore

--- framer/Extras/TouchEmulator.coffee
+++ framer/Extras/TouchEmulator.coffee
@@ -42,12 +42,12 @@
 		if not @isHammerTouchSupported()
 			throw new Error "Touch emulation for hammer is not supported"

-		@mouseCursorImage = "framer/images/cursor@2x.png"
-		@mouseCursorImageActive = "framer/images/cursor-active@2x.png"
-		@mouseCursorImageSize = 64
-		@mouseCursorInitialOffset = {x:0, y:0}
-
-		@keyPinchCode = 16 # Shift
+		@touchPointerImage = "framer/images/cursor@2x.png"
+		@touchPointerImageActive = "framer/images/cursor-active@2x.png"
+		@touchPointerImageSize = 64
+		@touchPointerInitialOffset = {x:0, y:0}
+
+		@keyPinchCode = 18 # Alt
 		@keyPanCode = 91 # Command

 		@context = new Framer.Context name:"TouchEmulator"
@@ -62,32 +62,32 @@
 		@isPinchKeyDown = false
 		@isPanKeyDown = false

-		mouseCursorInitialOffset = @mouseCursorInitialOffset
+		touchPointerInitialOffset = @touchPointerInitialOffset

 		@context.run =>
-			@imageLayer = new Layer
-				width: @mouseCursorImageSize
-				height: @mouseCursorImageSize
-				image: @mouseCursorImage
+			@touchPointLayer = new Layer
+				width: @touchPointerImageSize
+				height: @touchPointerImageSize
+				image: @touchPointerImage
 				opacity: 0

 	showTouchCursor: ->
-		@imageLayer.animateStop()
-		@imageLayer.midX = @point.x
-		@imageLayer.midY = @point.y
-		@imageLayer.scale = 1.8
-		@imageLayer.animate
+		@touchPointLayer.animateStop()
+		@touchPointLayer.midX = @point.x
+		@touchPointLayer.midY = @point.y
+		@touchPointLayer.scale = 1.8
+		@touchPointLayer.animate
 			properties:
 				opacity: 1
 				scale: 1
-				# midX: @point.x + @mouseCursorInitialOffset.x
-				# midY: @point.y + @mouseCursorInitialOffset.y
+				# midX: @point.x + @touchPointerInitialOffset.x
+				# midY: @point.y + @touchPointerInitialOffset.y
 			time: 0.1
 			curve: "ease-out"

 	hideTouchCursor: ->
-		@imageLayer.animateStop()
-		@imageLayer.animate
+		@touchPointLayer.animateStop()
+		@touchPointLayer.animate
 			properties:
 				opacity: 0
 				scale: 1.2
@@ -97,13 +97,13 @@
 		window.ontouchstart is null

 	keydown: (event) =>
-
+
 		if event.keyCode is @keyPinchCode
 			@isPinchKeyDown = true
 			@startPoint = @centerPoint = null
 			@showTouchCursor()
-			@imageLayer.midX = @point.x
-			@imageLayer.midY = @point.y
+			@touchPointLayer.midX = @point.x
+			@touchPointLayer.midY = @point.y
 			@wrap(document).addEventListener("keyup", @keyup, true)
 			@wrap(document).addEventListener("mousemove", @mousemove, true)

@@ -141,7 +141,7 @@
 		else
 			dispatchTouchEvent("touchstart", @target, event)

-		@imageLayer.image = @mouseCursorImageActive
+		@touchPointLayer.image = @touchPointerImageActive

 	mousemovePosition: (event) =>
 		@point =
@@ -159,16 +159,16 @@
 		@centerPoint ?= @startPoint

 		if @isPinchKeyDown and not @isPanKeyDown
-			@touchPoint = Utils.pointAdd(@mouseCursorInitialOffset, @pinchPoint(@point, @centerPoint))
+			@touchPoint = Utils.pointAdd(@touchPointerInitialOffset, @pinchPoint(@point, @centerPoint))
 			@touchPointDelta = Utils.pointSubtract(@point, @touchPoint)

 		if @isPinchKeyDown and @isPanKeyDown
 			@touchPoint = @panPoint(@point, @touchPointDelta)

 		if @isPinchKeyDown or @isPanKeyDown
-			@imageLayer.visible = true
-			@imageLayer.midX = @touchPoint.x
-			@imageLayer.midY = @touchPoint.y
+			@touchPointLayer.visible = true
+			@touchPointLayer.midX = @touchPoint.x
+			@touchPointLayer.midY = @touchPoint.y

 		if @isPinchKeyDown or @isPanKeyDown
 			dispatchTouchEvent("touchmove", @target, event, @touchPointDelta)
@@ -189,7 +189,7 @@
 		else
 			dispatchTouchEvent("touchend", @target, event)

-		@imageLayer.image = @mouseCursorImage
+		@touchPointLayer.image = @touchPointerImage
 		@hideTouchCursor()

 	pinchPoint: (point, centerPoint) ->


Taking into account wrong answer when calculate difficalty index

--- app/assets/javascripts/langtrainer_frontend_backbone/models/step.js.coffee
+++ app/assets/javascripts/langtrainer_frontend_backbone/models/step.js.coffee
@@ -5,13 +5,14 @@
   resetState: ->
     @wordsHelped = 0
     @stepsHelped = 0
+    @wrongAnsers = 0

   difficultyIndex: (answer) ->
     wordsNumber = answer.split(' ').length
     if wordsNumber == 0
       return 1

-    return @stepsHelped + @wordsHelped/wordsNumber
+    return @stepsHelped + @wrongAnsers/2 + @wordsHelped/wordsNumber

   baseParams: ->
     result = '?token=' + Langtrainer.LangtrainerApp.currentUser.readAttribute('token')
@@ -121,13 +122,14 @@
       url: @verifyAnswerUrl(@sanitizeText(answer))
       dataType: 'json'
       success: (response) ->
-        that.resetState()
         if response
           that.set response
           that.trigger('change', that)
           that.trigger('verify:right')
+          that.resetState()
         else
           that.trigger('verify:wrong')
+          that.wrongAnsers += 1
       error: ->
         that.trigger('verify:error')


fix new_dataset test to work with new tool menu

--- test/integration/new_dataset.coffee
+++ test/integration/new_dataset.coffee
@@ -13,6 +13,7 @@
     browser.waitForElementByCss '.dataset-list', 4000, done

   context 'when I click the "new dataset" button', ->
+    iframeUrl = null
     before (done) ->
       wd40.click '.new-dataset', ->
         browser.waitForElementByCss '#chooser .tool', 4000, done
@@ -25,32 +26,45 @@
               @currentUrl = url
               done err

+      before (done) ->
+        # wait for the tool menu toggle to load
+        setTimeout done, 1000
+
       it 'takes me to the dataset settings page', ->
         @currentUrl.should.match new RegExp("#{home_url}/dataset/[^/]+/settings")

+      it 'and shows that the "Code a dataset" tool is active', (done) ->
+        wd40.elementByCss '#dataset-tools-toggle', (err, link) ->
+          link.text (err, text) ->
+            text.toLowerCase().should.include 'code a dataset'
+            done()
+
+      it 'and shows me the "Code in a dataset" tool contents', (done) ->
+        wd40.switchToBottomFrame ->
+          wd40.trueURL (err, url) ->
+            iframeUrl = url
+            done()
+
     context 'when I go back to the dataset page', ->
       before (done) ->
+        # wait for the data tables tool to be installed in the background
         setTimeout done, 5000

       before (done) ->
         browser.get @currentUrl.replace(/\/settings$/, ''), done

       before (done) ->
-        browser.waitForElementByPartialLinkText 'Tools', 4000, done
+        # wait for the tool menu toggle to load (just in case)
+        setTimeout done, 1000

-      before (done) ->
-        wd40.elementByPartialLinkText 'Tools', (err, link) ->
-          link.click done
-
-      before (done) ->
-        browser.waitForElementByCss '.active', 4000, done
-
-      it 'has the datatables view selected', (done) ->
-        browser.elementByCss '.active', (err, link) ->
+      it 'shows that the "View in a table" tool is active', (done) ->
+        wd40.elementByCss '#dataset-tools-toggle', (err, link) ->
           link.text (err, text) ->
-            text.should.include 'View in a table'
+            text.toLowerCase().should.include 'view in a table'
             done()

-      it 'has the datatables view installed', (done) ->
+      it 'and shows me the "View in a table" tool contents', (done) ->
         wd40.switchToBottomFrame ->
-          wd40.waitForText 'database file does not exist', done
+          wd40.trueURL (err, url) ->
+            url.should.not.equal iframeUrl
+            done()


Enable all tests

Enable all tests

--- test/tests/FlowComponentTest.coffee
+++ test/tests/FlowComponentTest.coffee
@@ -35,7 +35,7 @@
 		nav.height.should.equal 100
 		nav.current.should.equal cardA

-	describe.only "Header Footer", ->
+	describe "Header Footer", ->

 		flowSize =
 			width: 300


refactor

refactor

--- test/unit/DebouncedCall.spec.coffee
+++ test/unit/DebouncedCall.spec.coffee
@@ -1,9 +1,11 @@
 DebouncedCall = require('../../src/DebouncedCall').default

 describe 'Unit. DebouncedCall.', ->
+  beforeEach () ->
+    this.debounced = new DebouncedCall(300);
+
   describe 'Simple callback.', ->
     beforeEach () ->
-      this.debounced = new DebouncedCall(300);
       this.simpleValue = undefined;
       this.simpleHandler = (value) =>
         this.simpleValue = value;
@@ -37,7 +39,6 @@

   describe 'Promises.', ->
     beforeEach () ->
-      this.debounced = new DebouncedCall(300);
       this.promisedHandler = undefined;
       this.firstPromiseResolve = undefined;
       this.firstPromiseReject = undefined;
@@ -83,7 +84,6 @@

   describe 'Collisions with delay.', ->
     beforeEach () ->
-      this.debounced = new DebouncedCall(300);
       this.firstHandler = sinon.spy()
       this.secondHandler = sinon.spy()

@@ -113,7 +113,6 @@

   describe 'Collisions with promise pending.', ->
     beforeEach () ->
-      this.debounced = new DebouncedCall(300);
       this.promisedHandler = () =>
         return new Promise (resolve, reject) =>
           this.firstPromiseResolve = resolve
@@ -138,6 +137,7 @@
       promise1.then =>
         expect(this.secondHandler).to.have.been.calledOnce

+  describe 'Collisions with promise pending.', ->

 # TODO: cancel - check statuses


fix filters

fix filters

--- app/templates/gulp/gulpfile.coffee
+++ app/templates/gulp/gulpfile.coffee
@@ -96,13 +96,16 @@
       #{file.contents.toString(enc)}
       return output;
     """
-    amd_options = _.merge amd_options, Function(rjs_content)()
-    amd_options.paths = _.reduce amd_options.paths, ((memo, v, k)->
-      memo[k] = v.replace /^\.\.\/bower_components\//, "../../app/bower_components/"
-      memo
-    ), {}
-    @push file
-    callback()
+    try
+      amd_options = _.merge amd_options, Function(rjs_content)()
+      amd_options.paths = _.reduce amd_options.paths, ((memo, v, k)->
+        memo[k] = v.replace /^\.\.\/bower_components\//, "../../app/bower_components/"
+        memo
+      ), {}
+      @push file
+      callback()
+    catch e
+      callback e

   linter = []
   gulp.src PROP.path.scripts()
@@ -137,16 +140,12 @@
     .pipe filter_preprocess
     .pipe $.preprocess PROP.preprocess()
     .pipe $.rename basename: "preprocess"
-    .pipe filter_preprocess.restore()
+    .pipe filter_preprocess.restore(end:true)

     .pipe $.if !PROP.isDev, filter_main_js
     .pipe $.if !PROP.isDev, extractAmdOptions
-    .pipe $.if !PROP.isDev, filter_main_js.restore()
-
-    .pipe through2.obj (file, enc, callback)->
-      #ugly hack for filter
-      @push file
-      callback()
+    .pipe $.if !PROP.isDev, filter_main_js.restore(end:true)
+
     .pipe gulp.dest PROP.path.scripts("dest")

 gulp.task "rjs", ["scripts"], ->


Add other required properties for project load.

--- services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
+++ services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
@@ -30,7 +30,14 @@
 			return callback(error) if error?
 			return callback null, members.map (m) -> m.id

-	USER_PROJECTION: { _id: 1, email: 1, features: 1}
+	USER_PROJECTION: {
+		_id: 1,
+		email: 1,
+		features: 1,
+		first_name: 1,
+		last_name: 1,
+		signUpDate: 1
+	}
 	getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
 		CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
 			return callback(error) if error?


Fix JS for new Evil Front

Fix JS for new Evil Front

--- scripts/action.coffee
+++ scripts/action.coffee
@@ -1,3 +1,4 @@
+#= require evil-front/shortcuts
 #= require evil-front/after
 #= require evil-front/tappable
 #= require evil-front/detect-3d


Start material exchange command

Start material exchange command

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -89,6 +89,16 @@
                   --remainingIteration
         , (err) ->
           return
+
+    else if 'material exchange'.startsWith firstCommand
+      vendors = Constants.MATERIAL_VENDORS
+
+      materialExchangeItems = []
+      for vendorHash in vendors
+        getVendorDetails(res, vendorHash).then (vendorDetails) ->
+
+          categories = vendorDetails.saleItemCategories.filter (category) -> category.categoryTitle is 'material exchange'
+          console.log "#{vendorDetails.vendorName}: #{categories}"

     else
       # command not recognized. Lists all available commands


update

update

--- scripts/deploy.coffee
+++ scripts/deploy.coffee
@@ -5,7 +5,7 @@
 exec = require("child_process").exec

 module.exports = (robot) ->
-	robot.on 'github_deployment_event'
+	robot.on 'github_deployment_event', ->
 	robot.respond /deploy/i, (msg) ->

 		unless robot.auth.hasRole(msg.envelope.user, 'deploy')


Fix display of datetime

Fix display of datetime

--- scripts/peatix.coffee
+++ scripts/peatix.coffee
@@ -52,8 +52,11 @@
         buffer += "（#{@pastString()}） "
         buffer += "#{@data.dashboard_uri}"
       else
+        datetime = moment(@data.datetime).format("YYYY年M月D日")
+        weekday = "日月火水木金土".split('')[moment(@data.datetime).day()]
+        datetime += "（#{weekday}）"
         buffer =  "*#{@data.name}*\n"
-        buffer += "開催日：#{@data.datetime} （#{@pastString()}）\n"
+        buffer += "開催日：#{datetime} （#{@pastString()}）\n"
         buffer += "チケット：#{@data.seats_sold} / #{@data.seats_total} \n"
         buffer += "売り上げ：#{@incomeToString()}\n"
         buffer += "管理画面：#{@data.dashboard_uri}\n"


how much time for our thesis?

how much time for our thesis?

--- scripts/thesis.coffee
+++ scripts/thesis.coffee
@@ -48,3 +48,6 @@
     msg.send "Ok, I stop that"
     clearInterval(deadlineIntervalId)
     deadlineIntervalId = null
+
+  robot.respond /how much time for our thesis\?/, (msg) ->
+    printTimeBeforeDeadline(msg)


Enables compression

Enables compression

--- server/app.coffee
+++ server/app.coffee
@@ -27,6 +27,7 @@
 app.use express.favicon client_build + '/images/favicon.ico'
 app.use express.logger('dev')
 app.use express.bodyParser()
+app.use express.compress()
 app.use express.methodOverride()
 app.use express.static client_build
 app.use app.router


Fix allowed urls.

Fix allowed urls.


Improve error handling

Improve error handling

--- services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee
+++ services/web/app/coffee/Features/Subscription/TeamInvitesController.coffee
@@ -11,7 +11,7 @@
 		email   = req.body.email

 		TeamInvitesHandler.createInvite teamManagerId, email, (err, invite) ->
-			next(err) if err?
+			return handleError(err, req, res, next) if err?
 			inviteView = { user:
 				{ email: invite.email, sentAt: invite.sentAt, holdingAccount: true }
 			}
@@ -22,13 +22,13 @@
 		userId = AuthenticationController.getLoggedInUserId(req)

 		TeamInvitesHandler.getInvite token, (err, invite, teamSubscription) ->
-			next(err) if err?
+			return handleError(err, req, res, next) if err?

-			unless invite?
+			if !invite
 				return ErrorController.notFound(req, res, next)

 			SubscriptionLocator.getUsersSubscription userId, (err, personalSubscription) ->
-				return callback(err) if err?
+				return handleError(err, req, res, next) if err?

 				res.render "subscriptions/team/invite",
 					inviterName: invite.inviterName
@@ -36,13 +36,12 @@
 					hasPersonalSubscription: personalSubscription?
 					appName: settings.appName

-
 	acceptInvite: (req, res, next) ->
 		token = req.params.token
 		userId = AuthenticationController.getLoggedInUserId(req)

 		TeamInvitesHandler.acceptInvite token, userId, (err, results) ->
-			next(err) if err?
+			return handleError(err, req, res, next) if err?

 		res.sendStatus 204

@@ -51,6 +50,12 @@
 		teamManagerId = AuthenticationController.getLoggedInUserId(req)

 		TeamInvitesHandler.revokeInvite teamManagerId, email, (err, results) ->
-			next(err) if err?
+			return handleError(err, req, res, next) if err?

 			res.sendStatus 204
+
+handleError = (err, req, res, next) ->
+	if err.teamNotFound or err.inviteNoLongerValid
+		ErrorController.notFound(req, res, next)
+	else
+		next(err)


More better commentary

More better commentary

--- source/coffee/index.coffee
+++ source/coffee/index.coffee
@@ -112,7 +112,7 @@
             switch jqXHR.status
               when 410 # Gone, cycle transaction
                 updateTransaction null, -> rePoll()
-              when 502 # Bad gateway; proxy failure
+              when 502 # Bad gateway; proxy successfully failed
                 rePoll true
               else # Other errors
                 if textStatus is "timeout"


Added loader, pagination, close button to new member activity.

--- client/landing/app/Applications/Activity.kdapplication/views/newmemberbucket.coffee
+++ client/landing/app/Applications/Activity.kdapplication/views/newmemberbucket.coffee
@@ -22,55 +22,66 @@
     @lastFetchedDate      = null
     @lastFetchedItemCount = 0
     @isUserViewCreated    = no
+    @listViews            = []

     @group.on "moreLinkClicked", =>
+      groupLoader = new KDLoaderView size: width: 24
+      @group.addSubView groupLoader
+      groupLoader.show()
+
+      count = 10
+
       if not @isUserViewCreated
-        @addSubView new KDView
-          cssName : "close-button"
-          click   : =>
+        @createCloseButton()
+        @createShowMore count
         @isUserViewCreated = yes

-      count = 10
-
-      @fetchUsers count, (teasers) =>
+      options =
+        limit: count
+        from : @lastFetchedDate or @getData().createdAtTimestamps[0]
+        to   : @getData().createdAtTimestamps[1]
+
+      appManager.tell "Activity", "fetchQuery", options, (teasers, activities) =>
         @getData().anchors = @getData().anchors.concat teasers.map (item)-> item.anchor
         @group.setData @getData().anchors
-        # @group.visibleCount += 10
-        # @group.render()
-        # @group.loader.hide()
+        @group.loader.hide()

         items = @getData().anchors.slice @lastFetchedItemCount, @lastFetchedItemCount + count

-        @newMembersList = new KDListViewController
+        newMembersList = new KDListViewController
           view        : new NewMemberList
           wrapper     : no
           scrollView  : no
         , items: items

-        @group.addSubView @newMembersList.getView()
+        @listViews.push newMembersList
+        @group.addSubView newMembersList.getView()
         @lastFetchedItemCount = @lastFetchedItemCount + count
-
-  fetchUsers: (count, callback) ->
-    # @group.loader.show()
-
-    selector    =
-      type      : { $in : ['CNewMemberBucketActivity'] }
-      createdAt :
-        $gt     : @lastFetchedDate or @getData().createdAtTimestamps[0]
-        $lt     : @getData().createdAtTimestamps[1]
-
-    options     =
-      limit     : count
-
-    KD.remote.api.CActivity.some selector, options, (err, activities)=>
-      return if err or activities.length is 0
-
-      activities = ActivityAppController.clearQuotes activities
-      @lastFetchedDate = activities.last.createdAt
-      KD.remote.reviveFromSnapshots activities, (err, teasers)=>
-        return warn err if err
-
-        callback? teasers
+        @showMore.hide() if @getData().count <= @lastFetchedItemCount
+        @lastFetchedDate = activities.last.createdAt
+        groupLoader.destroy()
+
+  createCloseButton: ->
+    @addSubView @closeButton = new KDView
+      cssClass : "close-button"
+      partial  : "Close"
+      click    : =>
+        list.getView().destroy() for list in @listViews
+        @closeButton.destroy()
+        @showMore.destroy()
+        @lastFetchedItemCount = 0
+        @isUserViewCreated    = no
+        @lastFetchedDate      = null
+        @listViews            = []
+
+  createShowMore: (count) ->
+    return if @getData().count < @lastFetchedItemCount + count
+    @addSubView @showMore = new KDView
+      cssClass : "show-more-link"
+      partial  : "Show More"
+      click    : =>
+        @group.emit "moreLinkClicked"
+        @showMore.hide() if @getData().count <= @lastFetchedItemCount + count

   pistachio:->
     """


Fixed queryOne helper for wikipage particularity.

--- app/coffee/services/resource.coffee
+++ app/coffee/services/resource.coffee
@@ -59,7 +59,7 @@

             return defered.promise

-        queryOne = (name, id, params, options) ->
+        queryOne = (name, id, params, options, cls) ->
             defauts = {method: "GET", headers:  headers()}
             current = {url: "#{url(name)}#{id}/", params: params or {}}

@@ -68,9 +68,8 @@
             defered = $q.defer()

             promise = $http(httpParams)
-            console.log httpParams
-            promise.success (data, status) ->
-                defered.resolve($model(name, data))
+            promise.success (data, status) ->
+                defered.resolve($model(name, data, cls))

             promise.error (data, status) ->
                 defered.reject()
@@ -274,12 +273,13 @@
             return defered.promise

         service.getWikiPage = (projectId, slug) ->
+            console.log $model.cls
             class WikiModel extends $model.cls
                 getUrl: ->
                     return "#{url(@_name)}#{@_attrs.project}-#{@_attrs.slug}/"

             _id = "#{projectId}-#{slug}"
-            return queryOne("wikipages", _id, {}, cls=WikiModel)
+            return queryOne("wikipages", _id, {project:projectId}, {},  WikiModel)

         service.createWikiPage = (projectId, slug, content) ->
             obj =


Add comments to user model method

Add comments to user model method

--- www/js/models/user.coffee
+++ www/js/models/user.coffee
@@ -55,12 +55,10 @@
         allowNull: true
         default: CONSTANTS.RELATIONSHIP.NONE
         type: "string"
-        # values: [_.filter _.values(CONSTANTS.RELATIONSHIP), (v) -> v]
       type:
         allowNull: true
         default: CONSTANTS.TYPE.USER
         type: "string"
-        # values: [_.filter _.values(CONSTANTS.TYPE), (v) -> v]
       # friends: null
       homeCity: ['string']
       gender: ['string']
@@ -84,7 +82,6 @@

           nameString

-    # otherMethods:
     # Check this user into a venue. Creates a new check-in object added to this
     # user account.
     checkIn: (venue, shout = null) ->
@@ -130,6 +127,8 @@
     isOutdated: ->
       @lastUpdated + window.GLOBALS.HOUR < window.timestamp()

+    # Return the absolute URL to this user's profile photo at a given size.
+    # Defaults to 100px x 100px.
     profilePhoto: (size = 100) ->
       return "" unless @photo
       "#{@photo.prefix}#{size}x#{size}#{@photo.suffix}"


added total number of trees to right menu, fixes #517

--- app/assets/javascripts/oxalis/view/skeletontracing/right-menu/dataset_info_view.coffee
+++ app/assets/javascripts/oxalis/view/skeletontracing/right-menu/dataset_info_view.coffee
@@ -14,6 +14,7 @@

<%= annotationType %>

DataSet: <%= dataSetName %>

Viewport width: <%= chooseUnit(zoomLevel) %>

+
Total number of trees: <%= treeCount %>

   """)

@@ -32,6 +33,9 @@

     @listenTo(@model.flycam3d, "changed", @render)
     @listenTo(@model.flycam, "zoomStepChanged", @render)
+    @listenTo(@model.skeletonTracing, "deleteTree", @render)
+    @listenTo(@model.skeletonTracing, "mergeTree", @render)
+    @listenTo(@model.skeletonTracing, "newTree", @render)

   serializeData : ->
@@ -39,7 +43,8 @@
     return {
       annotationType : @model.get("tracingType")
       zoomLevel : @calculateZoomLevel()
-      dataSetName :@model.get("dataset").get("name")
+      dataSetName : @model.get("dataset").get("name")
+      treeCount : @model.skeletonTracing.trees.length
     }

@@ -61,4 +66,3 @@

     @model.flycam3d.off("changed")
     @model.flycam.off("zoomStepChanged")
-


payment: improve/unify billing method view

--- client/landing/app/Applications/Dashboard.kdapplication/views/billingmethodview.coffee
+++ client/landing/app/Applications/Dashboard.kdapplication/views/billingmethodview.coffee
@@ -20,10 +20,12 @@

   getCardInfoPartial: (billingInfo) ->
     if billingInfo
-      { cardFirstName, cardLastName, cardNumber, cardMonth, cardYear
-        cardType, address1, address2, city, state, zip } = billingInfo
+      { description, cardFirstName, cardLastName, cardNumber, cardMonth
+        cardYear, cardType, address1, address2, city, state, zip } = billingInfo
       address = [address1, address2].filter(Boolean).join ' '
+      description ?= "#{cardFirstName}'s #{cardType}"
       """
+
#{description}

#{cardFirstName} #{cardLastName}

#{cardNumber} - #{cardMonth}/#{cardYear} (#{cardType})

#{address}


Updated to the coffee version.

Updated to the coffee version.

--- caboose-model/examples/user.coffee
+++ caboose-model/examples/user.coffee
@@ -1,14 +1,8 @@
-Model = require 'caboose-model'
-
-User = Model.create('User')
-            .store_in('user')
-            .authenticate_using('email', 'password')
-            .authenticate_with_token('auth_token')
-
-User.static 'find_by_email', (email) ->
-  @where {email: email}
-
-User.instance 'full_name', ->
-  "#{@first_name} #{@last_name}"
-
-module.exports = User.build()
+class User extends Model
+  store_in 'user'
+
+  static 'find_by_email', (email) ->
+    @where {email: email}
+
+  instance 'full_name', ->
+    "#{@first_name} #{@last_name}"


bugfix

bugfix

--- public/js/Ball.coffee
+++ public/js/Ball.coffee
@@ -2,8 +2,8 @@

 class Ball extends Object
   constructor: (@el, @speed) ->
+    @defaultSpeed = x: @speed.x, y: @speed.y
     super @el
-    @defaultSpeed = x: @speed.x, y: @speed.y

   _update: ->
     collision = false


Bump timeout a bit

Bump timeout a bit

--- spec/CanvasToBuffer.coffee
+++ spec/CanvasToBuffer.coffee
@@ -32,7 +32,7 @@
   describe 'when sending a canvas', ->
     unless noflo.isBrowser()
       it 'should return a buffer', (done) ->
-        expected = 'spec/test-80x80.jpg'
+        @timeout 5000
         out.once 'data', (data) ->
           console.log 'data', data
           chai.expect(data).to.be.an 'object'


ComponentTests: Print debug and level, use normal time

--- spec/ComponentTests.coffee
+++ spec/ComponentTests.coffee
@@ -27,7 +27,8 @@
             onReceived = (port, type, data) ->
                 throw new Error 'onReceived not set'
             simulator.library.definition = comps
-            simulator.start 0 # no time increments
+            #simulator.start 0 # no time increments
+            simulator.start 1 # normal time
             simulator.device.on 'response', () ->
                 event = arguments[0]
                 if event == 'SEND'
@@ -37,6 +38,10 @@
                 else if event == 'ERROR'
                     args = Array.prototype.slice.call arguments
                     done new Error args.join ', '
+                else if event == 'DEBUG'
+                    console.log event, arguments[1], arguments[2]
+                else if event == 'DEBUGLEVEL'
+                    console.log event, arguments[1]
             simulator.device.open (err) ->
                 chai.expect(err).to.not.exist
                 simulator.uploadFBP prog, (err) ->
@@ -53,6 +58,7 @@
         tests.cases.forEach (tcase) ->
             describe tcase.name, ->
                 it tcase.assert, (done) ->
+                    @timeout 4000
                     # listen outputs, verify against expected
                     received = {}
                     bracketed = null


make cancel operation spec clearer

make cancel operation spec clearer

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -25,6 +25,8 @@

   describe "cancel operation is robust", ->
     it "can call cancel operation, even if no operator pending", ->
+      # cancel operation pushes an empty input operation
+      # doing this without a pending operation throws an exception
       expect(-> vimState.pushOperations(new Input(''))).toThrow()

       # make sure commandModeInputView is created
@@ -34,7 +36,10 @@

       # now again cancel op, although there is none pending
       expect(vimState.isOperatorPending()).toBe false
-      editor.commandModeInputView.viewModel.cancel()
+
+      # which should not raise an exception
+      expect(-> editor.commandModeInputView.viewModel.cancel()).not.toThrow()
+

   describe "the x keybinding", ->
     describe "on a line with content", ->


Fix the last remaining test failure.

Fix the last remaining test failure.

--- spec/side-view-spec.coffee
+++ spec/side-view-spec.coffee
@@ -15,7 +15,7 @@
     view = new SideView(ours, editorView)

   it 'positions itself over the banner line', ->
-    refBanner = editorView.find('.line:contains(">>>>>>>")').eq 0
+    refBanner = editorView.find('.line:contains("<<<<<<<")').eq 0
     expect(view.offset().top).toEqual(refBanner.offset().top)
     expect(view.height()).toEqual(refBanner.height())


Better test.

Better test.

--- spec/workspace-spec.coffee
+++ spec/workspace-spec.coffee
@@ -1617,17 +1617,21 @@
       runs ->
         expect(pane.getPendingItem()).toBeFalsy()

+  describe "grammar activation", ->
+    beforeEach ->
+      waitsForPromise ->
+        atom.packages.activatePackage('language-javascript')
+
+    it "notifies the workspace of which grammar is used", ->
+      editor = null
+
+      grammarUsed = jasmine.createSpy()
+      atom.workspace.handleGrammarUsed = grammarUsed
+
+      waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
+      waitsFor -> grammarUsed.callCount is 1
+      runs ->
+        expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript'
+
   escapeStringRegex = (str) ->
     str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
-
-  describe "grammar activation", ->
-    it "activates grammars", ->
-      editor = null
-
-      atom.workspace.handleGrammarUsed = jasmine.createSpy()
-
-      waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
-      runs ->
-        atom.grammars.setGrammarOverrideForPath(editor.getPath(), 'source.coffee')
-        editor.reloadGrammar()
-      waitsFor -> atom.workspace.handleGrammarUsed.callCount is 1


Much better and correct version of nickname link regexp

--- client/Framework/core/utils.coffee
+++ client/Framework/core/utils.coffee
@@ -96,7 +96,7 @@

   expandUsernames: (text) ->
     return null unless text
-    text.replace /\ [@]+[A-Za-z0-9-_]+/g, (u) ->
+    text.replace /\B\@([\w\-]+)/gim, (u) ->
       username = u.replace "@", ""
       u.link "#!/member/#{username}"


whitespacing

whitespacing

--- js-library/app/js/jail_iframe/util/modal.coffee
+++ js-library/app/js/jail_iframe/util/modal.coffee
@@ -24,7 +24,6 @@
   closeModal_noAction: ->
     iFrame.fadeOut 'fast', -> FactlinkJailRoot.trigger 'modalClosed'

-
   closeModal_highlightNewFactlink: (fact, id) ->
     FactlinkJailRoot.selectRanges(FactlinkJailRoot.search(fact), id)
     FactlinkJailRoot.trigger 'factlinkAdded'


past events aren't soon

past events aren't soon

--- app/assets/javascripts/modules/pigeon_feet/directives/pigeon_timestamp_directive.coffee
+++ app/assets/javascripts/modules/pigeon_feet/directives/pigeon_timestamp_directive.coffee
@@ -7,7 +7,8 @@
       @details = $scope.time_tracker_output = $attrs.timeTracker.toString()

       isRaceSoon = =>
-        (Date.now() - @details) <= 300000 # 5 mins
+        ref = (Date.now() - @details)
+        ref <= 300000 && ref >= 0

       updateTimestamp = =>
         $elem.addClass('highlighted') if isRaceSoon()


Separates tests for every callpoint

Separates tests for every callpoint

--- spec/index.coffee
+++ spec/index.coffee
@@ -18,53 +18,39 @@
   describe 'the returned function', ->
     beforeEach ->
       @returnValue = {}
-      @original = -> @returnValue
+      @original = sinon.spy( -> @returnValue)
       @maypop = maypop(@original)
     it 'should have a "before" and "after" property', ->
       @maypop.should.contain.keys ['before', 'after']
     it 'should return the same value as the original function', ->
       @maypop().should.equal @original()
-  describe 'adding aspects', ->
+    it 'should pass the parameters to the original function', ->
+      @maypop {}, true, "123"
+      @original.should.have.been.calledWith {}, true, "123"
+
+  describe 'Before', ->
     beforeEach ->
       @returnValue = {in: "credible"}
       @original = sinon.spy(-> @returnValue)
       @maypop = maypop(@original)
     it 'should only accept functions as arguments', ->
       @maypop.before.bind(undefined).should.throw(Error)
-      @maypop.after.bind(undefined).should.throw()
       @maypop.before.bind(undefined, ->).should.not.throw()
-      @maypop.after.bind(undefined, ->).should.not.throw()
     it 'should run the function passed to "before" before the original function', ->
       spy = sinon.spy()
       @maypop.before spy
       @maypop()
       spy.should.have.been.calledBefore(@original)
-    it 'should run the function passed to "after" after the original function', ->
-      spy = sinon.spy()
-      @maypop.after spy
-      @maypop()
-      spy.should.have.been.calledAfter(@original)
-    describe 'arguments of aspects', ->
-      it 'should receive the arguments of the original function on "before"', ->
+    describe 'arguments', ->
+      it 'should receive the arguments of the original function', ->
         beforeSpy = sinon.spy()
         @maypop.before(beforeSpy)
         @maypop {}, true, "123"
         beforeSpy.should.have.been.calledWith {}, true, "123"
-      it 'should pass the parameters to the original function', ->
-        @maypop {}, true, "123"
-        @original.should.have.been.calledWith {}, true, "123"
-      it 'should receive the returned value of the original function on "after"', ->
-        afterSpy = sinon.spy()
-        @maypop.after(afterSpy)
-        @maypop {}, true, "123"
-        afterSpy.should.have.been.calledWith @returnValue
-    xdescribe 'scope of aspects', ->
     describe 'multiple calls to aspects', ->
-      it 'should be possible to call "before" and "after" multiple times', ->
+      it 'should be possible to call "before" multiple times', ->
         @maypop.before(->)
         @maypop.before.bind(undefined, ->).should.not.throw
-        @maypop.after(->)
-        @maypop.after.bind(undefined, ->).should.not.throw
       it 'should execute the functions in the order they are added', ->
         spy1 = sinon.spy()
         spy2 = sinon.spy()
@@ -74,6 +60,35 @@
         spy1.should.have.been.calledBefore spy2
         spy2.should.have.been.calledBefore @original

+  describe 'After', ->
+    beforeEach ->
+      @returnValue = {in: "credible"}
+      @original = sinon.spy(-> @returnValue)
+      @maypop = maypop(@original)
+    it 'should only accept functions as arguments', ->
+      @maypop.after.bind(undefined).should.throw()
+      @maypop.after.bind(undefined, ->).should.not.throw()
+    it 'should run the function passed to "before" before the original function', ->
+      spy = sinon.spy()
+      @maypop.before spy
+      @maypop()
+      spy.should.have.been.calledBefore(@original)
+    it 'should run the function passed after the original function', ->
+      spy = sinon.spy()
+      @maypop.after spy
+      @maypop()
+      spy.should.have.been.calledAfter(@original)
+    describe 'arguments', ->
+      it 'should receive the returned value of the original function on "after"', ->
+        afterSpy = sinon.spy()
+        @maypop.after(afterSpy)
+        @maypop {}, true, "123"
+        afterSpy.should.have.been.calledWith @returnValue
+    describe 'multiple calls to aspects', ->
+      it 'should be possible to call "before" and "after" multiple times', ->
+        @maypop.after(->)
+        @maypop.after.bind(undefined, ->).should.not.throw
+      it 'should execute the functions in the order they are added', ->
         spy1 = sinon.spy()
         spy2 = sinon.spy()
         @maypop.after spy1
@@ -81,8 +96,9 @@
         @maypop()
         @original.should.have.been.calledBefore spy1
         spy1.should.have.been.calledBefore spy2
-    xdescribe "Adding multiple aspects at once", ->
-    xdescribe "exceptions", ->
+
+  xdescribe 'scope of aspects', ->
+  xdescribe "Adding multiple aspects at once", ->
+  xdescribe "exceptions", ->

-


Remove console.log

Remove console.log

--- app/scripts/components/Enemy.coffee
+++ app/scripts/components/Enemy.coffee
@@ -53,7 +53,6 @@
     try
       @flip('X')
       for c in @_children
-        console.log c
         relX = c.x - @x
         c.attr?(
           x: @x + @w - c.w - relX


add snippets : removeEventListener

add snippets : removeEventListener

--- snippets/tialloy.cson
+++ snippets/tialloy.cson
@@ -5,10 +5,16 @@
   'Ti: addEventListener':
     'prefix': 'ael'
     'body': 'addEventListener(\'${1:eventName}\', );'
+  'Ti: removeEventListener function':
+    'prefix': 'relf'
+    'body': 'removeEventListener(\'${1:eventName}\', function (e) {\n\t$2\n});'
+  'Ti: removeEventListener':
+    'prefix': 'rel'
+    'body': 'removeEventListener(\'${1:eventName}\', );'
   'on: event on function':
     'prefix': 'onf'
     'body': 'on(\'${1:eventName}\', function (e) {\n\t$2\n});'
-  'on: event off function':
+  'off: event off function':
     'prefix': 'off'
     'body': 'off(\'${1:eventName}\', function (e) {\n\t$2\n});'


Trigger move-up to reset top scroll to 0

--- src/packages/command-panel/spec/command-panel-spec.coffee
+++ src/packages/command-panel/spec/command-panel-spec.coffee
@@ -408,7 +408,7 @@

         expect(previewList.scrollBottom()).toBeCloseTo previewList.prop('scrollHeight'), -1

-        previewList.trigger 'core:move-down'
+        _.times previewList.getOperations().length, -> previewList.trigger 'core:move-up'
         expect(previewList.scrollTop()).toBe 0

       it "doesn't bubble up the event and the command panel text doesn't change", ->


enabled multicast udp

enabled multicast udp

--- modules/dgram/platforms/node/lib/Context.coffee
+++ modules/dgram/platforms/node/lib/Context.coffee
@@ -12,7 +12,7 @@
             super new floyd.Config

                 data:
-                    port: null
+                    ports: []

             , config

@@ -21,31 +21,43 @@
         ##
         ##
         boot: (done)->
-            if @data.port
-                dgram.createSocket 'udp4', (msg, info)=>
-                    @_handleMessage msg, info
+            @_sockets = []
+
+            @_process @data.ports,
+                each: (conf, next)=>
+                    @_sockets.push sock = dgram.createSocket 'udp4', (msg, info)=>
+                        @_handleMessage sock, msg, info, conf

-                .bind @data.port, @data.host
+                    sock.bind conf.port, conf.host, (err)=>
+                        return next(err) if err
+                        @logger.info 'listening for UDP on %s:%s', (conf.host||'0.0.0.0'), conf.port
+
+                        if conf.multicast
+                            @_process conf.multicast,
+                                each: (addr, fn)=>
+                                    if typeof addr is 'string'
+                                        addr =
+                                            addr: addr
+
+                                    sock.setBroadcast true
+                                    sock.setMulticastTTL addr.ttl||128
+
+                                    sock.addMembership addr.addr, addr.iface
+
+                                    @logger.info '    adding multicast membership: %s@%s', addr.addr, addr.iface||'all'
+
+                                    fn()
+
+                                done: next
+
+                        else next()

-                done()
+                done: done

         ##
         ##
         ##
-        _handleMessage: (msg, info)->
-            console.log 'incomming message:', msg, info
+        _handleMessage: (sock, msg, info, conf)->
+            console.log 'incomming message:', msg, info, conf

-
-        ##
-        ##
-        ##
-        _sendMessage: (msg, port, host, fn)->
-            if typeof host is 'function'
-                fn = host
-                host = '127.0.0.1'
-
-            buff = new Buffer msg
-
-            dgram.createSocket('udp4').send buff, 0, buff.length, port, host, fn
-
-            +


add namespacing to support Meteor package scoping

--- source/helpers.coffee
+++ source/helpers.coffee
@@ -2,7 +2,9 @@
 global = this

 # Define global namespace for the space framework
-@Space = {}
+@Space = {
+  namespaces: {}
+}

 # Resolves a (possibly nested) path to an global object
 # Returns the object or null (if not found)
@@ -14,6 +16,9 @@
     result = result?[key] ? null
     # Take published space modules into account
     # to solve the Meteor package scoping problem
+    if !result? then result = Space.namespaces[key]
     if !result? then result = Space.Module.published[key]
     if !result? then throw new Error "Could not resolve <#{path}>"
   return result
+
+Space.namespace = (id) -> Space.namespaces[id] = {}


#936 - Can't move question out of group

#936 - Can't move question out of group

--- jsapp/xlform_model_view/view.surveyApp.coffee
+++ jsapp/xlform_model_view/view.surveyApp.coffee
@@ -100,7 +100,7 @@
         previous: survey_findRowByCid _prev
         parent: survey_findRowByCid _par
         event: 'sort'
-      ``
+      return

     _getRelatedElIds: ($el)->
       prev = $el.prev('.survey__row').eq(0).data('rowId')
@@ -149,7 +149,7 @@
         m.ordinal = if index >= position then (index + 1) else index
       model.ordinal = position
       @survey.rows.add(model, at: position)
-      ``
+      return

     forceSelectRow: (evt)->
       # forceSelectRow is used to mock the shift key
@@ -166,7 +166,7 @@

     questionSelect: (evt)->
       @activateGroupButton(@selectedRows().length > 0)
-      ``
+      return

     activateGroupButton: (active=true)->
       @$('.btn--group-questions').toggleClass('btn--disabled', !active)
@@ -305,6 +305,8 @@
           activate: sortable_activate_deactivate
           deactivate: sortable_activate_deactivate
           receive: (evt, ui) =>
+            if ui.sender.hasClass('group__rows')
+              return
             item = ui.item.prev()

             @ngScope.add_item @getItemPosition(item)
@@ -329,7 +331,7 @@
           activate: sortable_activate_deactivate
           deactivate: sortable_activate_deactivate
         })
-      ``
+      return
     _preventSortableIfGroupTooSmall: (evt)->
       $ect = $(evt.currentTarget)
       if $ect.siblings('.survey__row').length is 0
@@ -345,7 +347,7 @@
       scsv = @survey.toCSV()
       console?.clear()
       log scsv
-      ``
+      return

     ensureElInView: (row, parentView, $parentEl)->
       view = @getViewForRow(row)
@@ -417,7 +419,7 @@
         @activateSortable()

       # $viewUtils.reorderElemsByData(".xlf-row-view", @$el, "row-index")
-      ``
+      return

     clickRemoveRow: (evt)->
       evt.preventDefault()
@@ -430,7 +432,7 @@
         findMatch = (r)->
           if r.cid is rowId
             matchingRow = r
-          ``
+          return

         @survey.forEachRow findMatch, {
           includeGroups: false
@@ -485,7 +487,7 @@
           onSuccess: => @onEscapeKeydown = $viewUtils.enketoIframe.close
           onError: (errArgs...)=>
             @alert errArgs
-      ``
+      return

     alert: (message) ->
         $('.alert-modal').text(message).dialog('option', {


fixing tab

fixing tab

--- src/Models/UserKeys.coffee
+++ src/Models/UserKeys.coffee
@@ -5,7 +5,7 @@
     @get "users/#{parseInt userId}/keys", (data) => fn data if fn

   addKey: (userId, title, key, fn = null) =>
-  	params =
+    params =
       title: title
       key: key


mo' expression examples

mo' expression examples

--- test/fixtures/generation/statements_as_expressions.coffee
+++ test/fixtures/generation/statements_as_expressions.coffee
@@ -4,11 +4,13 @@
   go_outside()

 print(3 + try
-    nonexistent.no_way
-  catch error
-    print(error)
-    3
+  nonexistent.no_way
+catch error
+  print(error)
+  3
 )

 func: x =>
-  return throw x+  return throw x
+
+print(x * x for x in [1..100])


Provide JSON directly, no need to parse

Provide JSON directly, no need to parse

--- test/javascripts/integration/download_docx_test.js.coffee
+++ test/javascripts/integration/download_docx_test.js.coffee
@@ -11,12 +11,11 @@
     )
     ETahi.Test = {}
     ETahi.Test.currentPaper = records[0]
-    formats = export_formats: [
+    ETahi.supportedDownloadFormats = export_formats: [
       format: "docx"
       url: "https://tahi.example.com/export/docx"
       description: "This converts from docx to HTML"
     ]
-    ETahi.supportedDownloadFormats = JSON.parse(formats)
     paperPayload = ef.createPayload('paper')
     paperPayload.addRecords(records.concat([fakeUser]))
     paperResponse = paperPayload.toJSON()


some more refactorings

some more refactorings

--- src/airbud.coffee
+++ src/airbud.coffee
@@ -69,8 +69,9 @@
       setTimeout =>
         fs.readFile path, "utf8", (err, buf) =>
           if err
-            return cb new Error "Error while opening #{path}. #{err.message}"
-          return @handleData options, buf, cb
+            returnErr = new Error "Error while opening #{path}. #{err.message}"
+            return cb returnErr
+          return @_handleData options, buf, cb
       , options.testDelay
       return

@@ -82,26 +83,27 @@
         if options.expectedStatus.indexOf(parseInt(res.statusCode, 10)) == -1
           msg = "#{res.statusCode} received when fetching '#{url}'. \n expected"
           msg += " one status of: #{options.expectedStatus.join(', ')}. #{buf}"
-          err = new Error err
+          err = new Error msg
           return cb err

-      return @handleData options, buf, cb
+      return @_handleData options, buf, cb

-  @handleData: (options, buf, cb) ->
+  @_handleData: (options, buf, cb) ->
     data = buf

-    if options.parseJson
-      try
-        data = JSON.parse data
-      catch e
-        return cb e, data
+    if !options.parseJson
+      return cb null, data

-      if options.expectedKey?
-        if !data[options.expectedKey]?
-          return cb new Error(
-            "Invalid body received when fetching '#{options.url}'. \n" +
-            "No key: #{options.expectedKey}. #{buf}"
-          )
+    try
+      data = JSON.parse data
+    catch e
+      return cb e, data
+
+    if options.expectedKey? && !data[options.expectedKey]?
+      msg = "Invalid body received when fetching '#{options.url}'. \n"
+      msg += "No key: #{options.expectedKey}. #{buf}"
+      err = new Error msg
+      return cb err

     cb null, data


Fixed invisible username in "to" area

Fixed invisible username in "to" area

--- client/landing/app/CommonViews/linkviews/autocompleteprofiletextview.coffee
+++ client/landing/app/CommonViews/linkviews/autocompleteprofiletextview.coffee
@@ -13,7 +13,7 @@
   pistachio:->

     name = KD.utils.getFullnameFromAccount @getData()
-    "{{@highlightMatch name}}" +
+    "#{@highlightMatch name}" +
       if @getOptions().shouldShowNick then """
         <span class='nick'>
           (@{{@highlightMatch #(profile.nickname), yes}})


Fixed typo

Fixed typo

--- src/cachet.coffee
+++ src/cachet.coffee
@@ -12,7 +12,7 @@
 #   hubot cachet component set <component name> <id> - Register a component into my brain
 #   hubot cachet component list - List all registered components into my brain (i.e. Cachet could own more components)
 #   hubot cachet component flushall - Remove all registered components from my brain
-#   hubot incident investigating on <component name>: <incident message> - Declare an incident on a component (or anything else if it cannot be linked to an existing compoenent) when it experiences an issue
+#   hubot incident investigating on <component name>: <incident message> - Declare an incident on a component (or anything else if it cannot be linked to an existing component) when it experiences an issue
 #   hubot incident identified on <component name>: <incident message> - Declare an incident when you find the (root) cause of the current issue
 #   hubot incident watching on <component name>: <incident message> - Declare an incident when you monitor changes due to an outage for instance
 #   hubot incident fixed on <component name>: <incident message> - Declare an incident when things are fixed


cameras update

cameras update

--- src/camera.coffee
+++ src/camera.coffee
@@ -28,7 +28,6 @@
   transformFilters = []

   focusOn = (object, elapsedTime) ->
-    debugger unless elapsedTime
     dampingFactor = 2

     #TODO: Different t90 value inside deadzone?


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -41,9 +41,9 @@
       I.scroll.x = I.scroll.x.approach(centerRect.left, I.maxVelocity.x)

     if objectCenter.y > centerRect.bottom
-      I.scroll.y = I.scroll.y.approach(centerRect.bottom, I.maxVelocity.x)
+      I.scroll.y = I.scroll.y.approach(centerRect.bottom, I.maxVelocity.y)
     else if objectCenter.x < centerRect.left
-      I.scroll.x = I.scroll.x.approach(centerRect.left, I.maxVelocity.x)
+      I.scroll.x = I.scroll.x.approach(centerRect.left, I.maxVelocity.y)

     I.scroll.x = I.scroll.x.clamp(I.cameraBounds.left, I.cameraBounds.right - I.screen.width)
     I.scroll.y = I.scroll.y.clamp(I.cameraBounds.top, I.cameraBounds.bottom - I.screen.height)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -102,7 +102,7 @@
       canvas.clip(0, 0, I.screen.width, I.screen.height)
         objects = objectFilters.pipeline(objects)

-        objects.invoke "o"
+        objects.invoke "overlay", canv

   self.include(Camera.ZSort)
   self.include(Camera.Zoom)


client: remove unused code.

client: remove unused code.

--- src/client.coffee
+++ src/client.coffee
@@ -95,27 +95,11 @@
                     throw "Handle input types: `#{ type_ }`."
             value

-        ###
-        get_value_type: (param) ->
-            TYPES = @value_types
-            value = param
-            if _.isArray value
-                value = value[0]
-            type_ = typeof value
-            if type_ of TYPES
-                return TYPES[type_]
-            throw "Unknown value type: `#{ type_ }`."
-        ###
-
         set_choice_doc: (args...) =>
             choice_doc = _.clone @model.get 'choice_doc'
             choice_doc ?= {}
             for name, parameter of @model.get 'parameters'
-                value = @get_input_value name
-                # XXX dont need to deduce type here if we've rendered the
-                # right inputs in the template.
-                # type_ = @get_value_type parameter
-                choice_doc[name] = value
+                choice_doc[name] = @get_input_value name
             @model.set 'choice_doc', choice_doc

     # The `ChoiceDocView` view shows the current choice document that


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -77,8 +77,6 @@
     return rgbMap.concat(a)

   Color2 = (args...) ->
-    __proto__: Color2::
-
     switch args.length
       when 0
         parsedColor = [0, 0, 0, 1]
@@ -104,6 +102,7 @@

     throw "#{args.join(',')} is an unknown color" unless parsedColor

+    __proto__: Color2::
     r: parsedColor[0]
     g: parsedColor[1]
     b: parsedColor[2]


加入401跳转

加入401跳转

--- src/common.coffee
+++ src/common.coffee
@@ -14,4 +14,8 @@
 #响应406错误
 exports.response406 = (res, message)->
   res.statusCode = 406
-  res.end message+  res.end message
+
+exports.response401 = (res)->
+  res.statusCode = 401
+  res.end()


More verbose error reporting in verbose mode

--- src/common.coffee
+++ src/common.coffee
@@ -69,9 +69,15 @@
   log: (level, msg, meta, callback) ->
     if level == 'error'
       process.stderr.write "\n  error".red + " #{ msg }\n"
-      if @level == 'verbose' && meta?.stack
-        stack = meta.stack.substr meta.stack.indexOf('\n') + 1
-        process.stderr.write stack + "\n\n"
+      if @level == 'verbose' && meta?
+        if meta.stack?
+          stack = meta.stack.substr meta.stack.indexOf('\n') + 1
+          process.stderr.write stack + "\n\n"
+        for key, value of meta
+          if key in ['message', 'stack']
+            continue
+          pval = util.inspect(value, false, 2, true).replace(/\n/g, '\n    ')
+          process.stderr.write "    #{ key }: #{ pval }\n"
       else
         process.stderr.write "\n"
     else if !@quiet


provide lengthInRange vs hours

provide lengthInRange vs hours

--- client/skr/models/TimeEntry.coffee
+++ client/skr/models/TimeEntry.coffee
@@ -26,8 +26,6 @@
             this.range.end.diff(this.range.start, 'hour', true).toFixed(2) +
                 " hours: " + @description

-        hours: deps: ['range'], fn: ->
-            @length('hours')
     events:
         'change:end_at': 'onEndChange'
         'change:start_at': 'onStartChange'
@@ -39,6 +37,11 @@

     length: (duration = 'hour') ->
         @range.end.diff(@range.start, duration, true)
+
+    lengthInRange: (range, duration = 'hour') ->
+        _.moment.min(@range.end, range.end).diff(
+            _.moment.max(@range.start, range.start), duration, true
+        )

     toCalEvent: ->
         attrs = {entry: this, range: @range, content: @content, resizable: {step: 15}}


Tests: Teams: Fixed invite user test.

Tests: Teams: Fixed invite user test.

--- client/test/lib/teams/teams.coffee
+++ client/test/lib/teams/teams.coffee
@@ -135,55 +135,31 @@
       .end()

-  # inviteUser: (browser) ->
+  inviteUser: (browser) ->

-  #   invitationsModalSelector = ".kdmodal-content  .AppModal--admin-tabs .invitations"
-  #   inviteButtonSelector     = "#{invitationsModalSelector} button.invite"
-  #   inviteUserView           = "#{invitationsModalSelector} .invite-view"
-  #   emailInputSelector       = "#{inviteUserView} .invite-inputs input.user-email"
-  #   userEmail                = "#{helpers.getFakeText().split(' ')[0]}@kd.io"
-  #   inviteMemberButton       = "#{invitationsModalSelector} button.invite-members"
-  #   cancelButton             = "#{invitationsModalSelector} button.cancel"
-  #   notificationView         = '.kdnotification'
-  #   pendingMemberView        = "#{invitationsModalSelector} .kdlistitemview-member.pending"
+    invitationsModalSelector = ".kdmodal-content  .AppModal--admin-tabs .invitations"
+    inviteUserView           = "#{invitationsModalSelector} .invite-view"
+    emailInputSelector       = "#{inviteUserView} .invite-inputs input.user-email"
+    userEmail                = "#{helpers.getFakeText().split(' ')[0]}@kd.io"
+    inviteMemberButton       = "#{invitationsModalSelector} button.invite-members"
+    notificationView         = '.kdnotification'
+    pendingMembersTab        = "#{invitationsModalSelector} .kdtabhandle.pending-invitations"
+    pendingMemberView        = "#{invitationsModalSelector} .kdlistitemview-member.pending"

-  #   user = teamsHelpers.loginTeam(browser)
-  #   teamsHelpers.clickTeamSettings(browser)
+    user = teamsHelpers.loginTeam(browser)
+    teamsHelpers.clickTeamSettings(browser)

-  #   teamsHelpers.openInvitationsTab(browser)
+    teamsHelpers.openInvitationsTab(browser)

-  #   browser
-  #     .waitForElementVisible  inviteButtonSelector, 20000
-  #     .click                  inviteButtonSelector
-  #     .waitForElementVisible  inviteUserView, 20000
-  #     .waitForElementVisible  emailInputSelector, 20000
-  #     .setValue               emailInputSelector, userEmail
-  #     .waitForElementVisible  inviteMemberButton, 20000
-  #     .click                  inviteMemberButton
-  #     .waitForElementVisible  notificationView, 20000
-  #     .assert.containsText    notificationView, 'All invites sent'
-  #     .waitForElementVisible  cancelButton, 20000
-  #     .click                  cancelButton
-  #     .waitForElementVisible  pendingMemberView, 20000
-  #     .assert.containsText    pendingMemberView, userEmail
-  #     .end()
-    helpers.beginTest(browser, adminUser)
-    teamsHelpers.createInvitation browser, user, (invitationLink) ->
-      browser.click '.close-icon.closeModal'
-
-      helpers.doLogout(browser)
-
-      browser
-        .url                   invitationLink
-        .waitForElementVisible modalSelector, 20000
-        .waitForElementVisible emailSelector, 20000
-        .waitForElementVisible companyNameSelector, 20000
-        .assert.valueContains  emailSelector, user.email
-        .setValue              companyNameSelector, user.teamSlug
-        .click                 signUpButton
-        .pause                 2500
-
-      teamsHelpers.enterTeamURL(browser)
-      teamsHelpers.fillUsernamePasswordForm(browser, user)
-      browser.end()
-
+    browser
+      .waitForElementVisible  inviteUserView, 20000
+      .waitForElementVisible  emailInputSelector, 20000
+      .setValue               emailInputSelector, userEmail
+      .waitForElementVisible  inviteMemberButton, 20000
+      .click                  inviteMemberButton
+      .waitForElementVisible  notificationView, 20000
+      .assert.containsText    notificationView, 'All invites sent'
+      .click                  pendingMembersTab
+      .waitForElementVisible  pendingMemberView, 20000
+      .assert.containsText    pendingMemberView, userEmail
+      .end()


Fixes scope in the specs

Fixes scope in the specs

--- spec/index.coffee
+++ spec/index.coffee
@@ -18,7 +18,7 @@
   describe 'the returned function', ->
     beforeEach ->
       @returnValue = {}
-      @original = sinon.spy( -> @returnValue)
+      @original = sinon.spy( => @returnValue)
       @maypop = maypop(@original)
     it 'should have a "before" and "after" property', ->
       @maypop.should.contain.keys ['before', 'after']


Write testing comments.

Write testing comments.

--- src/5.test.coffee
+++ src/5.test.coffee
@@ -28,19 +28,23 @@
         @result = null
     set: (fun) ->
         @_fun = fun
-        s = fun.toString()
+        funStr = fun.toString()
         testArgName = "testArg_834942610148628375"
         do =>
-            s = s.replace(/\([^\)]*\)/, "(#{testArgName})") # not global, only replace first "(...)"
+            # not global, only replace first "(...)"
+            funStr = funStr.replace(/\([^\)]*\)/, "(#{testArgName})")
+        # I used to use regex for this parser, but nearly all JS engine cannot execute it well.
+        # Some report errors. Node even hangs up with CPU usage 100%. Very weird.
+        # Maybe it's because this regex is very complicated, and nested. So I gave it up.
         do =>
             positions = []
             quote = null
-            slashQuoteReady = true
+            slashQuoteReady = true # indicates whether a slash is related to regex, or math division
             wordStarted = false
-            dotAffected = false
+            dotAffected = false # this is to do with things like `abc  .   def`
             i = 0
-            while i < s.length
-                c = s[i]
+            while i < funStr.length
+                c = funStr[i]
                 oldSlashQuoteReady = slashQuoteReady
                 if quote == null
                     if "a" <= c <= "z" or "A" <= c <= "Z" or "0" <= c <= "9" or
@@ -80,25 +84,25 @@
                 else if c == "\\" and quote != null
                     i += 2
                 else if quote == null and not oldWordStarted and not oldDotAffected and "a" <= c <= "z"
-                    t = s.substr(i, 10)
-                    if t.indexOf("end") == 0 or t.indexOf("equal") == 0 or t.indexOf("unit") == 0
+                    s = funStr.substr(i, 10) # limit to 10 chars for better performance
+                    if s.indexOf("end") == 0 or s.indexOf("equal") == 0 or s.indexOf("unit") == 0
                         positions.push(i)
                     i++
                 else
                     i++
             positions.forEach((m, index) =>
                 pos = m + (testArgName.length + 1) * index
-                s = s.substr(0, pos) + testArgName + "." + s.substr(pos)
+                funStr = funStr.substr(0, pos) + testArgName + "." + funStr.substr(pos)
             )
         do =>
-            s = s.replace(///
+            funStr = funStr.replace(///
                 #{testArgName}\.unit\s*\(\s*
                 (
                     " (?: [^"\\] | \\. )* " |
                     ' (?: [^'\\] | \\. )* '
                 )
             ///g, (match, p1) =>
-                unitStr = eval(p1)
+                unitStr = eval(p1) # If use `JSON.parse` instead, single quotes string cannot be parsed.
                 parsed = null
                 newStr = null
                 do =>
@@ -134,15 +138,15 @@
                                     str1: unitStr.substr(0, i).trim()
                                     str2: unitStr.substr(i + 1).trim()
                                 return
-                # An `eval` string must be enclosed by parentheses, otherwise
-                # an object literal (i.e. wrapped in braces) can't be evaluated.
                 do =>
                     if parsed.type == "equal"
                         newStr = "#{testArgName}.equal(#{parsed.str1}, #{parsed.str2}"
                 newStr
             )
-        console.log(s)
-        @_interpretedFunction = eval("(#{s})")
+        console.log(funStr)
+        # If an `eval` string has leading or trailing braces, then it must be enclosed
+        # by parentheses, otherwise it can't be parsed or evaluated.
+        @_interpretedFunction = eval("(#{funStr})")
         @
     get: ->
         @_fun
@@ -254,44 +258,6 @@
     end: (result) ->
         @result = result ? {type: true}
         @
-    unit: (body, description = "") ->
-        parsed = null
-        do =>
-            quote = null
-            parenthesis = 0
-            bracket = 0
-            brace = 0
-            for i in [0...body.length]
-                if body[i] == "\"" and quote == null
-                    quote = "double"
-                else if body[i] == "'" and quote == null
-                    quote = "single"
-                else if (body[i] == "\"" and quote == "double") or (body[i] == "'" and quote == "single")
-                    quote = null
-                else if body[i] == "("
-                    parenthesis++
-                else if body[i] == "["
-                    bracket++
-                else if body[i] == "{"
-                    brace++
-                else if body[i] == ")"
-                    parenthesis--
-                else if body[i] == "]"
-                    bracket--
-                else if body[i] == "}"
-                    brace--
-                else if quote == null and parenthesis == bracket == brace == 0
-                    if body[i] == "="
-                        parsed =
-                            type: "equal"
-                            str1: body.substr(0, i).trim()
-                            str2: body.substr(i + 1).trim()
-                        return
-        # An `eval` string must be enclosed by parentheses, otherwise
-        # an object literal (i.e. wrapped in braces) can't be evaluated.
-        do =>
-            if parsed.type == "equal"
-                @equal(eval("(#{parsed.str1})"), eval("(#{parsed.str2})"), body)
     equal: (actual, expected, description = "") ->
         determine = (actual, expected) =>
             if Array.isArray(actual) and Array.isArray(expected)


Update keybase key

Update keybase key


add section detail

add section detail

--- routes/section.coffee
+++ routes/section.coffee
@@ -7,10 +7,7 @@
   host: config.redis.host, port: config.redis.port
 router = express.Router()

-router.get "/"
-, (req, res, next) ->
-  cache.route(req.originalUrl, 60*5)(req, res, next)
-, (req, res) ->
+router.get "/", cache.route(), (req, res) ->
   Section.all (err, sectionsArr)->
     return res.status(500).send err.message if err
     async.map sectionsArr, (section, callback)->
@@ -34,4 +31,43 @@
         storys: sectionsArr_new
         time: time

+router.get "/:title", cache.route(), (req, res) ->
+  Section.get req.params.title, (err, sectionObj)->
+    return res.status(500).send err.message if err
+    stories = bubbleSort sectionObj.stories
+    async.map stories, (story, callback)->
+      Daily.getStoryById story._id, (err, storyObj)->
+        if err
+          callback()
+        else
+          callback null,
+            url: "/story/" + storyObj.id
+            image: storyObj.image
+            title: storyObj.title
+    , (err, sectionsArr)->
+      return res.status(500).send err.message if err
+      sectionsArr_new = []
+      for section in sectionsArr
+        if section
+          sectionsArr_new.push section
+      time = Date.now() - res.socket._idleStart
+      res.render "day",
+        css: "day"
+        date: sectionObj.title
+        storys: sectionsArr_new
+        time: time
+
+bubbleSort = (arr) ->
+  i = arr.length
+  while i > 0
+    j = 0
+    while j < i - 1
+      if arr[j].date.valueOf() > arr[j + 1].date.valueOf() or arr[j].date.valueOf() == arr[j + 1].date.valueOf() and arr[j].date.index > arr[j + 1].date.index
+        tempVal = arr[j]
+        arr[j] = arr[j + 1]
+        arr[j + 1] = tempVal
+      j++
+    i--
+  return arr.reverse()
+
 module.exports = router


Fixes #1116

Fixes #1116

--- app/views/editor/patch_modal.coffee
+++ app/views/editor/patch_modal.coffee
@@ -8,7 +8,7 @@
   template: template
   plain: true
   modalWidthPercent: 60
-  @DOC_SKIP_PATHS = ['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index', '__v', 'patches', 'creator']
+  @DOC_SKIP_PATHS = ['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index', '__v', 'patches', 'creator', 'js']

   events:
     'click #withdraw-button': 'withdrawPatch'


some more passing tests. now problem with sampling

--- spec/BaconSpec.coffee
+++ spec/BaconSpec.coffee
@@ -1330,8 +1330,8 @@
     describe "series 3", ->
       expectPropertyEvents((-> s.take(1)), ["foo"])

-describe.skip "Property.changes", ->
-  describe.skip "sends property change events", ->
+describe "Property.changes", ->
+  describe "sends property change events", ->
     expectStreamEvents(
       ->
         s = new Bacon.Bus()
@@ -1342,20 +1342,20 @@
           s.end()
         p
       ["b", error()])
- describe.skip "works with synchronous source", ->
+describe.skip "works with synchronous source", ->
    expectStreamEvents(
      -> Bacon.fromArray([1,2,3]).toProperty(0).changes()
      [1,2,3])

-describe.skip "Property.combine", ->
-  describe.skip "combines latest values of two properties, with given combinator function, passing through errors", ->
+describe "Property.combine", ->
+  describe "combines latest values of two properties, with given combinator function, passing through errors", ->
     expectPropertyEvents(
       ->
         left = series(2, [1, error(), 2, 3]).toProperty()
         right = series(2, [4, error(), 5, 6]).delay(t(1)).toProperty()
         left.combine(right, add)
       [5, error(), error(), 6, 7, 8, 9])
-  describe.skip "also accepts a field name instead of combinator function", ->
+  describe "also accepts a field name instead of combinator function", ->
     expectPropertyEvents(
       ->
         left = series(1, [[1]]).toProperty()
@@ -1363,7 +1363,7 @@
         left.combine(right, ".concat")
       [[1, 2]])

-  describe.skip "combines with null values", ->
+  describe "combines with null values", ->
     expectPropertyEvents(
       ->
         left = series(1, [null]).toProperty()
@@ -1382,15 +1382,15 @@

     bus.push(["fail whale"])
     expect(calls).to.equal 0
-  describe.skip "does not duplicate same error from two streams", ->
+  describe "does not duplicate same error from two streams", ->
     expectPropertyEvents(
       ->
         src = series(1, ["same", error()])
         Bacon.combineAsArray(src, src)
       [["same", "same"], error()])

-describe.skip "EventStream.combine", ->
-  describe.skip "converts stream to Property, then combines", ->
+describe "EventStream.combine", ->
+  describe "converts stream to Property, then combines", ->
     expectPropertyEvents(
       ->
         left = series(2, [1, error(), 2, 3])
@@ -1470,20 +1470,20 @@
       ->
         Bacon.combineAsArray([Bacon.constant([]), Bacon.constant([1])])
     ([[[], [1]]]))
-  describe.skip "works with arrays as values, with first array being non-empty (bug fix)", ->
+  describe "works with arrays as values, with first array being non-empty (bug fix)", ->
     expectPropertyEvents(
       ->
         Bacon.combineAsArray([Bacon.constant([1]), Bacon.constant([2])])
     ([[[1], [2]]]))
-  describe.skip "works with empty array", ->
+  describe "works with empty array", ->
     expectPropertyEvents(
       -> Bacon.combineAsArray([])
       [[]])
-  describe.skip "works with empty args list", ->
+  describe "works with empty args list", ->
     expectPropertyEvents(
       -> Bacon.combineAsArray()
       [[]])
-  describe.skip "accepts constant values instead of Observables", ->
+  describe "accepts constant values instead of Observables", ->
     expectPropertyEvents(
       -> Bacon.combineAsArray(Bacon.constant(1), 2, 3)
     [[1,2,3]])
@@ -1495,37 +1495,37 @@
     Bacon.combineAsArray(Bacon.fromArray([1,2,3,4,5]).map(id)).skip(4).onValue()
     expect(calls).to.equal(1)

-describe.skip "Bacon.combineWith", ->
-  describe.skip "combines n properties, streams and constants using an n-ary function", ->
+describe "Bacon.combineWith", ->
+  describe "combines n properties, streams and constants using an n-ary function", ->
     expectPropertyEvents(
       ->
         stream = series(1, [1, 2])
         f = (x, y, z) -> x + y + z
         Bacon.combineWith(f, stream, Bacon.constant(10), 100)
       [111, 112])
-  describe.skip "works with single input", ->
+  describe "works with single input", ->
     expectPropertyEvents(
       ->
         stream = series(1, [1, 2])
         f = (x) -> x * 2
         Bacon.combineWith(f, stream)
       [2, 4])
-  describe.skip "works with 0 inputs (results to a constant)", ->
+  describe "works with 0 inputs (results to a constant)", ->
     expectPropertyEvents(
       ->
         Bacon.combineWith(-> 1)
       [1])

-describe.skip "Boolean logic", ->
-  describe.skip "combines Properties with and()", ->
+describe "Boolean logic", ->
+  describe "combines Properties with and()", ->
     expectPropertyEvents(
       -> Bacon.constant(true).and(Bacon.constant(false))
       [false])
-  describe.skip "combines Properties with or()", ->
+  describe "combines Properties with or()", ->
     expectPropertyEvents(
       -> Bacon.constant(true).or(Bacon.constant(false))
       [true])
-  describe.skip "inverts property with not()", ->
+  describe "inverts property with not()", ->
     expectPropertyEvents(
       -> Bacon.constant(true).not()
       [false])
@@ -1540,8 +1540,8 @@
       -> Bacon.constant(true).or(false)
       [true])

-describe.skip "Bacon.mergeAll", ->
-  describe.skip ("merges all given streams"), ->
+describe "Bacon.mergeAll", ->
+  describe ("merges all given streams"), ->
     expectStreamEvents(
       ->
         Bacon.mergeAll([
@@ -1549,7 +1549,7 @@
           series(3, [3, 4]).delay(t(1))
           series(3, [5, 6]).delay(t(2))])
       [1, 3, 5, 2, 4, 6], unstable)
-  describe.skip ("supports n-ary syntax"), ->
+  describe ("supports n-ary syntax"), ->
     expectStreamEvents(
       ->
         Bacon.mergeAll(
@@ -1557,14 +1557,14 @@
           series(3, [3, 4]).delay(t(1))
           series(3, [5, 6]).delay(t(2)))
       [1, 3, 5, 2, 4, 6], unstable)
-  describe.skip "works with a single stream", ->
+  describe "works with a single stream", ->
     expectStreamEvents(
       -> Bacon.mergeAll([Bacon.once(1)])
       [1])
     expectStreamEvents(
       -> Bacon.mergeAll(Bacon.once(1))
       [1])
-  describe.skip "returns empty stream for zero input", ->
+  describe "returns empty stream for zero input", ->
     expectStreamEvents(
       -> Bacon.mergeAll([])
       [])
@@ -1572,41 +1572,41 @@
       -> Bacon.mergeAll()
       [])

-describe.skip "Property.sampledBy(stream)", ->
-  describe.skip "samples property at events, resulting to EventStream", ->
+describe "Property.sampledBy(stream)", ->
+  describe "samples property at events, resulting to EventStream", ->
     expectStreamEvents(
       ->
         prop = series(2, [1, 2]).toProperty()
         stream = repeat(3, ["troll"]).take(4)
         prop.sampledBy(stream)
       [1, 2, 2, 2])
-  describe.skip "includes errors from both Property and EventStream", ->
+  describe "includes errors from both Property and EventStream", ->
     expectStreamEvents(
       ->
         prop = series(2, [error(), 2]).toProperty()
         stream = series(3, [error(), "troll"])
         prop.sampledBy(stream)
       [error(), error(), 2])
-  describe.skip "ends when sampling stream ends", ->
+  describe "ends when sampling stream ends", ->
     expectStreamEvents(
       ->
         prop = repeat(2, [1, 2]).toProperty()
         stream = repeat(2, [""]).delay(t(1)).take(4)
         prop.sampledBy(stream)
       [1, 2, 1, 2])
-  describe.skip "accepts optional combinator function f(Vp, Vs)", ->
+  describe "accepts optional combinator function f(Vp, Vs)", ->
     expectStreamEvents(
       ->
         prop = series(2, ["a", "b"]).toProperty()
         stream = series(2, ["1", "2", "1", "2"]).delay(t(1))
         prop.sampledBy(stream, add)
       ["a1", "b2", "b1", "b2"])
-  describe.skip "allows method name instead of function too", ->
+  describe "allows method name instead of function too", ->
     expectStreamEvents(
       ->
         Bacon.constant([1]).sampledBy(Bacon.once([2]), ".concat")
       [[1, 2]])
-  describe.skip "works with same origin", ->
+  describe "works with same origin", ->
     expectStreamEvents(
       ->
         src = series(2, [1, 2])
@@ -1617,7 +1617,7 @@
         src = series(2, [1, 2])
         src.toProperty().sampledBy(src.map(times, 2))
       [1, 2])
-  describe.skip "uses updated property after combine", ->
+  describe "uses updated property after combine", ->
     latter = (a, b) -> b
     expectPropertyEvents(
       ->
@@ -1625,7 +1625,7 @@
         combined = Bacon.constant().combine(src, latter)
         src.sampledBy(combined, add)
       ["aa", "bb", "cc"])
-  describe.skip "uses updated property after combine with subscriber", ->
+  describe "uses updated property after combine with subscriber", ->
     latter = (a, b) -> b
     expectPropertyEvents(
       ->
@@ -1634,7 +1634,7 @@
         combined.onValue(->)
         src.sampledBy(combined, add)
       ["aa", "bb", "cc"])
-  describe.skip "skips samplings that occur before the property gets its first value", ->
+  describe "skips samplings that occur before the property gets its first value", ->
     expectStreamEvents(
       ->
         p = series(5, [1]).toProperty()
@@ -1650,14 +1650,14 @@
         p = series(5, [1, 2]).toProperty()
         p.sampledBy(series(3, [0, 0, 0, 0]).toProperty())
       [1, 1, 2], unstable)
-  describe.skip "works with stream of functions", ->
+  describe "works with stream of functions", ->
     f = ->
     expectStreamEvents(
       ->
         p = series(1, [f]).toProperty()
         p.sampledBy(series(1, [1, 2, 3]))
       [f, f, f])
-  describe.skip "works with synchronous sampler stream", ->
+  describe "works with synchronous sampler stream", ->
     expectStreamEvents(
       -> Bacon.constant(1).sampledBy(Bacon.fromArray([1,2,3]))
       [1,1,1], unstable)


Changed perspective to orthographic cam

Changed perspective to orthographic cam

--- app/assets/javascripts/view.coffee
+++ app/assets/javascripts/view.coffee
@@ -24,20 +24,30 @@
     @camera = new THREE.PerspectiveCamera(90, WIDTH / HEIGHT, 0.1, 10000)
     @scene = new THREE.Scene()

-    @rendereryz = new THREE.WebGLRenderer({ clearColor: 0xff0000, antialias: true })
+    @rendereryz = new THREE.WebGLRenderer({ clearColor: 0x0000ff, antialias: true })
     @camerayz = new THREE.PerspectiveCamera(90, WIDTH / HEIGHT, 0.1, 10000)
     @sceneyz = new THREE.Scene()

-    @rendererxz = new THREE.WebGLRenderer({ clearColor: 0xff0000, antialias: true })
-    @cameraxz = new THREE.PerspectiveCamera(90, WIDTH / HEIGHT, 0.1, 10000)
+    @rendererxz = new THREE.WebGLRenderer({ clearColor: 0x0000ff, antialias: true })
+    @cameraxz = new THREE.OrthographicCamera(WIDTH / -2, WIDTH / 2, HEIGHT / -2, HEIGHT / 2, 0.1, 10000)
     @scenexz = new THREE.Scene()

     #experimental sphere
     sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xccff00 })
     sphereyz = new THREE.Mesh(new THREE.SphereGeometry(50, 16, 16), sphereMaterial)
-
-    sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xcc0000 })
+    sphereyz.position.x = 100
+
+    sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xccff00 })
+    sphereyz2 = new THREE.Mesh(new THREE.SphereGeometry(50, 16, 16), sphereMaterial)
+    sphereyz2.position.x = -100
+
+    sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xccff00 })
     spherexz = new THREE.Mesh(new THREE.SphereGeometry(50, 16, 16), sphereMaterial)
+    spherexz.position.x = 100
+
+    sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xccff00 })
+    spherexz2 = new THREE.Mesh(new THREE.SphereGeometry(50, 16, 16), sphereMaterial)
+    spherexz2.position.x =- 100

     # Let's set up a camera
     # The camera is never "moved". It only looks at the scene
@@ -48,11 +58,13 @@

     @sceneyz.add(@camerayz)
     @sceneyz.add(sphereyz)
+    @sceneyz.add(sphereyz2)
     @camerayz.position.z = CAM_DISTANCE
     @camerayz.lookAt(new THREE.Vector3( 0, 0, 0 ))

     @scenexz.add(@cameraxz)
     @scenexz.add(spherexz)
+    @scenexz.add(spherexz2)
     @cameraxz.position.z = CAM_DISTANCE
     @cameraxz.lookAt(new THREE.Vector3( 0, 0, 0 ))

@@ -201,12 +213,18 @@
   resize : ->
     #FIXME: Is really the window's width or rather the DIV's?
     container = $("#render")
-    WIDTH = container.width()
-    HEIGHT = container.height()
+    WIDTH = container.width()/2
+    HEIGHT = container.height()/2

     @renderer.setSize( WIDTH, HEIGHT )
+    @rendereryz.setSize(WIDTH, HEIGHT)
+    @rendererxz.setSize(WIDTH, HEIGHT)
     @camera.aspect  = WIDTH / HEIGHT
     @camera.updateProjectionMatrix()
+    @camerayz.aspect  = WIDTH / HEIGHT
+    @camerayz.updateProjectionMatrix()
+    @cameraxz.aspect  = WIDTH / HEIGHT
+    @cameraxz.updateProjectionMatrix()
     @draw()

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