account/test: add case for shared vm and its deletion check

--- workers/social/lib/social/models/account.test.coffee
+++ workers/social/lib/social/models/account.test.coffee
@@ -4,9 +4,13 @@
   withDummyClient
   withConvertedUser
   generateDummyClient
+  generateUserInfo
+  createCustomClient
   generateRandomString
   generateRandomEmail
   checkBongoConnectivity } = require '../../../testhelper'
+
+{ createUserAndMachine } = require '../../../testhelper/models/computeproviders/machinehelper'

 { generateMetaData } = require \
   '../../../testhelper/models/computeproviders/credentialhelper'
@@ -21,11 +25,12 @@
 JInvitation      = require './invitation'
 JApiToken        = require './apitoken'
 JComputeStack    = require './stack'
-
+JMachine         = require './computeproviders/machine'
 JStackTemplate   = require './computeproviders/stacktemplate'
 JCredential      = require './computeproviders/credential'

-{ generateStackTemplateData } = require \
+{ generateStackTemplateData
+  generateStackMachineData } = require \
   '../../../testhelper/models/computeproviders/stacktemplatehelper'

@@ -200,10 +205,10 @@

     describe 'create groups as owner and admin', ->

-      groupData1         =
-        slug           : generateRandomString()
-        title          : generateRandomString()
-        visibility     : 'visible'
+      groupData1 =
+        slug       : generateRandomString()
+        title      : generateRandomString()
+        visibility : 'visible'

       before (done) ->

@@ -227,42 +232,30 @@

             (next) ->
               # create a session for AdminAccount to be able to create resources
-              JSession = require './session'
-              sessionOptions =
-                username  : account.getAt 'profile.nickname'
-                groupName : group2Slug
-
-              JSession.createNewSession sessionOptions, (err, session) ->
-                expect(err).to.not.exist
-
-                adminClient =
-                  sessionToken : session.clientId
-                  context      : { group: group2Slug }
-                  clientIP     : '127.0.0.1'
-                  connection   :
-                    delegate   : account
-
+              createCustomClient group2Slug, account, (err, client_) ->
+                adminClient = client_
+                expect(err).to.not.exist
                 next()

             (next) ->
               # creating resources for group2 for adminAccount
               createResourcesQueue = [
                 (next) ->
+
                   provider = 'aws'
-
-                  options =
+                  options  =
                     meta     : generateMetaData provider
                     title    : 'someCredentialTitle'
                     provider : provider

-                  options.meta.foobar = '  trim this white space   '
                   JCredential.create adminClient, options, (err, credential) ->
                     expect(err).to.not.exist
                     expect(credential).to.exist
                     next()

                 (next) ->
-                  stackTemplateData = generateStackTemplateData adminClient
+                  data = { machines: generateStackMachineData 1 }
+                  stackTemplateData = generateStackTemplateData adminClient, data
                   JStackTemplate.create adminClient, stackTemplateData, (err, template) ->
                     expect(err).to.not.exist
                     expect(template).to.exist
@@ -276,10 +269,12 @@
                     next()

                 (next) ->
-                  stackTemplate.generateStack adminClient, (err, res) ->
+                  stackTemplate.generateStack adminClient, {}, (err, res) ->
                     { stack, results: { machines } } = res
                     expect(err).to.not.exist
                     expect(machines).to.exist
+                    expect(machines[0].err).to.not.exist
+                    expect(machines[0].obj).to.exist
                     expect(stack).to.exist
                     next()

@@ -296,6 +291,17 @@
                       expect(err).to.not.exist
                       expect(apiToken).to.exist
                       next()
+
+                (next) ->
+                  # create a user with machine and share it with Admin Client
+                  userInfo = generateUserInfo()
+                  createUserAndMachine userInfo, (err, data) ->
+                    expect(err).to.not.exist
+                    { machine } = data
+                    params = { target : [ account.getAt 'profile.nickname' ], asUser : yes }
+                    machine.shareWith params, (err) ->
+                      expect(err).to.not.exist
+                      next()
               ]

               async.series createResourcesQueue, next
@@ -332,6 +338,12 @@
               next()

           (next) ->
+            JMachine.some { 'users.username': account.getAt('profile.nickname') }, {}, (err, machines) ->
+              expect(err).to.not.exist
+              expect(machines.length).to.be.equal 3
+              next()
+
+          (next) ->
             JApiToken.some { originId: account.getId() }, {}, (err, apiTokens) ->
               expect(err).to.not.exist
               expect(apiTokens.length).to.be.equal 1
@@ -356,7 +368,7 @@
         async.series queue, done

-      it 'should not be able to delete account when there more than one ownership', (done) ->
+      it 'should not allow to delete account when there more than one ownership', (done) ->

         account.destroy client, (err) ->
           expect(err).to.exist
@@ -394,6 +406,12 @@
               expect(creds.length).to.be.equal 0
               next()

+          (next) ->
+            selector = { 'users.username' : account.getAt('profile.nickname') }
+            JMachine.some selector, {}, (err, machines) ->
+              expect(err).to.not.exist
+              expect(machines.length).to.be.equal 0
+              next()

           (next) ->
             JStackTemplate.some { originId: account.getId() }, {}, (err, stackTemplates) ->
@@ -413,9 +431,10 @@

       it 'should ensure that account and group are deleted', (done) ->

+        username = account.getAt 'profile.nickname'
+
         queue = [
           (next) ->
-            username = account.profile.nickname
             JAccount.one { 'profile.nickname' : username }, (err, account_) ->
               expect(err).to.not.exist
               expect(account_).to.not.exist


wip notify

wip notify

--- workers/social/lib/social/models/systemstatus.coffee
+++ workers/social/lib/social/models/systemstatus.coffee
@@ -2,12 +2,6 @@

 module.exports = class JSystemStatus extends Model

-  # @set
-  #   sharedMethods :
-  #     static      : ['monitorStatus','scheduleRestart']
-  #   schema        :
-  #     alias       : String
-  #     target      : String
   @setSharedMethods
     static: ['monitorStatus','scheduleRestart']

@@ -18,16 +12,17 @@
     restartTitle     : null
     restartContent   : null

-  callbacks = []
+  # callbacks = []

   {log} = console

   @monitorStatus =(callback)->
     log 'monitorStatus called'
     callback restartScheduled  if restartData.restartScheduled?
-    callbacks.push callback
+    # callbacks.push callback

   @scheduleRestart =(data)->
     log 'scheduleRestart called',data
     restartData = data
-    callback restartData  for callback in callbacks+    # callback restartData  for callback in callbacks
+    @emit 'restartScheduled', data


Fix crash when an app is not specified for build command

This is a regression introduced in #818

Change-Type: patch

--- lib/actions/build.coffee
+++ lib/actions/build.coffee
@@ -7,7 +7,7 @@
 ###
 Opts must be an object with the following keys:

-	app: the app this build is for
+	app: the app this build is for (optional)
 	arch: the architecture to build for
 	deviceType: the device type to build for
 	buildEmulated
@@ -20,7 +20,8 @@
 		composeOpts.projectName
 	)
 	.then (project) ->
-		if project.descriptors.length > 1 and not opts.app.application_type?[0]?.supports_multicontainer
+		appType = opts.app?.application_type?[0]
+		if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer
 			logger.logWarn(
 				'Target application does not support multiple containers.\n' +
 				'Continuing with build, but you will not be able to deploy.'


Fixes to CS

Fixes to CS

--- lib/auto-id-class.coffee
+++ lib/auto-id-class.coffee
@@ -1,34 +1,38 @@
 {CompositeDisposable} = require 'atom'

 module.exports = AutoIdClass =
+
   subscriptions: null

   activate: (state) ->
     @subscriptions = new CompositeDisposable
     @subscriptions.add atom.commands.add 'atom-text-editor', 'auto-id-class:insert_id_attribute': => @insert_id_attribute()
-    @subscriptions.add atom.commands.add 'atom-text-editor', 'auto-id-class:insert_class_attribute': (event) ->
-        if false
-          @insert_attribute('class')
-        else
-          event.abortKeyBinding()
+    @subscriptions.add atom.commands.add 'atom-text-editor', 'auto-id-class:insert_class_attribute': => @insert_class_attribute()
+
+  insert_id_attribute: ->
+    if @cursor_inside_html_tag()
+      @insert_attribute('id')
+    else
+      event.abortKeyBinding()
+
+  insert_class_attribute: ->
+    if @cursor_inside_html_tag()
+      @insert_attribute('class')
+    else
+      event.abortKeyBinding()

   deactivate: ->
     @subscriptions.dispose()

   cursor_inside_html_tag: ->
-    return true
-
-    # Get contents of current line to evaluate if within HTML tag.
-    # TODO: support multiple cursors.
     cursor = atom.workspace.getActiveTextEditor().cursors[0]
-    console.log(cursor)
     currentLine = cursor.getCurrentBufferLine()
     console.log(currentLine)
     bufferPos = cursor.getBufferPosition
     console.log(bufferPos)
     return true

-  insert_attribute: (type) ->
+  insert_attribute: (attrType) ->
+    console.log '@insert attribute ' + attrType
     editor = atom.workspace.getActiveTextEditor()
-    # editor.insertText(' id=""')
-    editor.insertText(' class=""')
+    editor.insertText ' ' + attrType + '=""'


fix project toolbar config deleted

fix project toolbar config deleted

--- lib/flex-tool-bar.coffee
+++ lib/flex-tool-bar.coffee
@@ -106,7 +106,7 @@

   resolveProjectConfigPath: ->
     persistent = atom.config.get 'flex-tool-bar.persistentProjectToolBar'
-    @projectToolbarConfigPath = null unless persistent
+    @projectToolbarConfigPath = null unless persistent and fs.isFileSync(@projectToolbarConfigPath)
     relativeProjectConfigPath = atom.config.get 'flex-tool-bar.toolBarProjectConfigurationFilePath'
     editor = atom.workspace.getActivePaneItem()
     file = editor?.buffer?.file or editor?.file


Update, fix tests.

Update, fix tests.

Now that the test database has a password item called my-password,
using my-password as an indication of printing details is not
ideal. Use my-username instead.

--- test/one_pass/app_test.coffee
+++ test/one_pass/app_test.coffee
@@ -51,13 +51,13 @@
     app = makeApp(['-d', keychainPath, 'show', 'my-login'], 'master-password')
     app.run (status) ->
       assert.ok(app.output.contains('my-login'))
-      assert.ok(app.output.contains('my-password'))
+      assert.ok(app.output.contains('my-username'))

   "App#run show with -r prints raw representations": (beforeExit, assert) ->
     app = makeApp(['-d', keychainPath, '-r', 'show', 'my-login'], 'master-password')
     app.run (status) ->
-      assert.ok(app.output.contains('my-password'))
-      assert.ok(!app.output.contains('Password:'))
+      assert.ok(app.output.contains('my-username'))
+      assert.ok(!app.output.contains('Username:'))

   "App#run list returns 0 without asking for a password": (beforeExit, assert) ->
     app = makeApp(['-d', keychainPath, 'list', 'my-login'])
@@ -68,7 +68,7 @@
     app = makeApp(['-d', keychainPath, 'list'])
     app.run (status) ->
       assert.ok(app.output.contains('my-login'))
-      assert.ok(!app.output.contains('my-password'))
+      assert.ok(!app.output.contains('my-username'))

   "App#run list prints all items by default": (beforeExit, assert) ->
     app = makeApp(['-d', keychainPath, 'list'])
@@ -86,7 +86,7 @@
     app = makeApp(['-d', keychainPath, 'my-login'], 'master-password')
     app.run (status) ->
       assert.ok(app.output.contains('my-login'))
-      assert.ok(app.output.contains('my-password'))
+      assert.ok(app.output.contains('my-username'))

   "App#run, when a database path is specified and does not exist, exits with an error": (beforeExit, assert) ->
     app = makeApp(['-d', 'invalid.agilekeychain', 'list'])


error handling [#3]

error handling [#3]

--- lib/jedi-provider.coffee
+++ lib/jedi-provider.coffee
@@ -32,6 +32,8 @@

       exec command, (error, stdout, stderr) ->
         resolve(suggestions) unless stdout != ""
+        resolve(suggestions) unless stderr != ""
+        resolve(suggestions) unless error != null

         jediResponse = JSON.parse stdout


Revert "Update signature in EC2 test"

Revert "Update signature in EC2 test"

This reverts commit f99ecca600be05710b0f77b720dc4d3496f5a37b.

--- test/services/ec2.spec.coffee
+++ test/services/ec2.spec.coffee
@@ -30,7 +30,7 @@
             "%26X-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%3Dakid%252F19700101" +
             "%252Fsrc-region%252Fec2%252Faws4_request%26X-Amz-Date%3D19700101T000000Z" +
             "%26X-Amz-Expires%3D3600%26X-Amz-Security-Token%3Dsession" +
-            "%26X-Amz-Signature%3D2a09621aaae799a2a4f68d40e14d4d57f73227307c992615917acc097cb7400c" +
+            "%26X-Amz-Signature%3D014459362191e87b3b175e1e16d007c0c9aaa1fdb2743f8cebdfd8dea0fb0cc2" +
             "%26X-Amz-SignedHeaders%3Dhost",
           'SourceRegion=src-region',
           'SourceSnapshotId=snap-123456789'


we can require() any files we want! non-js files come back as plaintext.

this commit also fixes reloading of html files, since require() will
look in your dev paths by way of require.paths :zap:

--- plugins/project/project.coffee
+++ plugins/project/project.coffee
@@ -7,7 +7,7 @@
 {bindKey} = require 'editor'

 exports.init = ->
-  @html = File.read Chrome.appRoot() + "/plugins/project/project.html"
+  @html = require "project/project.html"

   bindKey 'toggleProjectDrawer', 'Command-Ctrl-N', (env) =>
     @toggle()


Parser can identify current symbol

Parser can identify current symbol

--- lib/symbol-finder.coffee
+++ lib/symbol-finder.coffee
@@ -17,8 +17,20 @@
       editor = atom.workspace.getActiveTextEditor()
       pos = editor.getCursorBufferPosition()
       text = editor.lineTextForBufferRow(pos.row)
-      left = text.substring 0, pos.column
-      right = text.substring pos.column
+      leftText = text.substring 0, pos.column
+      rightText = text.substring pos.column

-      console.log left
-      console.log right
+      leftRegex = /\s*([A-Za-z0-9_$]+)$/g
+      rightRegex = /^([A-Za-z0-9_$]+)\s*/g
+
+      leftWord = leftRegex.exec(leftText)
+      rightWord = rightRegex.exec(rightText)
+
+      leftWord = if leftWord then leftWord[1] else ''
+      rightWord = if rightWord then rightWord[1] else ''
+
+      word = leftWord + rightWord
+
+      console.log 'Current symbol: ' + word
+
+      return word


Add install date to logging page; remove console.log.

--- pages/logging.coffee
+++ pages/logging.coffee
@@ -3,8 +3,5 @@
 document.addEventListener "DOMContentLoaded", ->
   $("vimiumVersion").innerText = Utils.getCurrentVersion()
   chrome.storage.local.get "installDate", (items) ->
-    console.log new Date
-    console.log items
-    console.log items.installDate, items.installDate.toString()
     $("installDate").innerText = items.installDate.toString()


updated coffeelint path

updated coffeelint path

--- grunt/coffeelint.coffee
+++ grunt/coffeelint.coffee
@@ -1,7 +1,7 @@
 module.exports = (grunt) ->
   grunt.config 'coffeelint',
     all: [
-      'assets/js/**/*.coffee'
+      'assets/coffee/**/*.coffee'
       'grunt/*.coffee'
       'test/**/*.coffee'
       '*.coffee'


added route icons

added route icons

--- example/server/init_routes.coffee
+++ example/server/init_routes.coffee
@@ -22,7 +22,7 @@
       external: true
       nav:
         priority: 1001
-        icon: 'icon-github'
+        icon: 'icon-code'
     },{
       route: 'build'
       path: "https://travis-ci.org/LumaPictures/luma-ui"
@@ -30,7 +30,7 @@
       external: true
       nav:
         priority: 1002
-        icon: 'icon-github'
+        icon: 'icon-cogs'
     },{
       route: 'errorPages'
       path: "/error-pages"


Add specs for $.containingView

Add specs for $.containingView

--- spec/jquery-extensions-spec.coffee
+++ spec/jquery-extensions-spec.coffee
@@ -181,10 +181,20 @@
       view = new TestView
       expect(view.find('h1').view()).toBe view

+  describe "$.fn.containingView()", ->
+    it "returns the containing view", ->
+      class TestView extends View
+        @content: (params={}, otherArg) ->
+          @div =>
+            @h1 "Hello"
+
+      view = new TestView
+      expect(view.find('h1').containingView()).toBe view
+
       # also works for non-jQuery DOM nodes
       node = document.createElement('div')
       view.find('h1')[0].appendChild(node)
-      expect($(node).view()).toBe view
+      expect($(node).containingView()).toBe view

   if document.hasFocus() # It's not currently possible to run this spec in phantomjs
     describe "$.fn.hasFocus()", ->


Add spec for buffer sharing

Add spec for buffer sharing

--- spec/project-find-view-spec.coffee
+++ spec/project-find-view-spec.coffee
@@ -12,7 +12,7 @@
 waitsForPromise = (fn) -> window.waitsForPromise timeout: 30000, fn

 describe 'ProjectFindView', ->
-  [activationPromise, editor, editorView, projectFindView, searchPromise, resultsPane, workspaceElement] = []
+  [activationPromise, editor, editorView, projectFindView, searchPromise, resultsPane, workspaceElement, mainModule] = []

   getAtomPanel = ->
     workspaceElement.querySelector('.project-find').parentNode
@@ -28,7 +28,8 @@
     jasmine.attachToDOM(workspaceElement)

     atom.config.set('find-and-replace.openProjectFindResultsInRightPane', false)
-    activationPromise = atom.packages.activatePackage("find-and-replace").then ({mainModule}) ->
+    activationPromise = atom.packages.activatePackage("find-and-replace").then (options) ->
+      mainModule = options.mainModule
       mainModule.createViews()
       {projectFindView} = mainModule
       spy = spyOn(projectFindView, 'confirm').andCallFake ->
@@ -824,6 +825,42 @@
           for decoration in decorations
             resultDecorations.push decoration if decoration.getProperties().class is className
         resultDecorations
+
+      it "shares the buffers and history cyclers between both buffer and project views", ->
+        {findView} = mainModule
+
+        projectFindView.findEditor.setText('findme')
+        projectFindView.replaceEditor.setText('replaceme')
+
+        atom.commands.dispatch editorView, 'find-and-replace:show'
+        expect(findView.findEditor.getText()).toBe 'findme'
+        expect(findView.replaceEditor.getText()).toBe 'replaceme'
+
+        # add some things to the history
+        atom.commands.dispatch(findView.findEditor.element, 'core:confirm')
+        findView.findEditor.setText('findme1')
+        atom.commands.dispatch(findView.findEditor.element, 'core:confirm')
+        findView.findEditor.setText('')
+
+        atom.commands.dispatch(findView.replaceEditor.element, 'core:confirm')
+        findView.replaceEditor.setText('replaceme1')
+        atom.commands.dispatch(findView.replaceEditor.element, 'core:confirm')
+        findView.replaceEditor.setText('')
+
+        # Back to the project view to make sure we're using the same cycler
+        atom.commands.dispatch editorView, 'project-find:show'
+
+        expect(projectFindView.findEditor.getText()).toBe ''
+        atom.commands.dispatch(projectFindView.findEditor.element, 'core:move-up')
+        expect(projectFindView.findEditor.getText()).toBe 'findme1'
+        atom.commands.dispatch(projectFindView.findEditor.element, 'core:move-up')
+        expect(projectFindView.findEditor.getText()).toBe 'findme'
+
+        expect(projectFindView.replaceEditor.getText()).toBe ''
+        atom.commands.dispatch(projectFindView.replaceEditor.element, 'core:move-up')
+        expect(projectFindView.replaceEditor.getText()).toBe 'replaceme1'
+        atom.commands.dispatch(projectFindView.replaceEditor.element, 'core:move-up')
+        expect(projectFindView.replaceEditor.getText()).toBe 'replaceme'

       it 'highlights the search results in the selected file', ->
         # Process here is to


confirm before deleting comment/thread

confirm before deleting comment/thread

--- lms/static/coffee/src/discussion/content.coffee
+++ lms/static/coffee/src/discussion/content.coffee
@@ -226,9 +226,12 @@
     handleDelete = (elem) ->
       if $content.hasClass("thread")
         url = Discussion.urlFor('delete_thread', id)
+        c = confirm "Are you sure to delete thread \"" + $content.find("a.thread-title").text() + "\"?"
       else
         url = Discussion.urlFor('delete_comment', id)
-
+        c = confirm "Are you sure to delete this comment? "
+      if c != true
+        return
       Discussion.safeAjax
         $elem: $(elem)
         url: url


Ensure that /usr/local/bin is not duplicated in PATH

--- atom/init.coffee
+++ atom/init.coffee
@@ -1,7 +1,11 @@
 # Your init script

-# Make environment variables available to Atom
-process.env.PATH = ['/usr/local/bin', process.env.PATH].join(':')
+# If PATH does not already contain /usr/local/bin
+if process.env.PATH.indexOf('/usr/local/bin:') == -1
+
+  # Make environment variables available to Atom
+  process.env.PATH = ['/usr/local/bin', process.env.PATH].join(':')
+

 # Ensure that Python correctly outputs Unicode characters
 process.env.PYTHONIOENCODING = 'utf_8'


Split selection into lines

Split selection into lines

Former-commit-id: 68fa9a362177a4a36bf34c4a868545159dcb4c21 [formerly b651a8bed2aa1bb7aaf26b4bb233bb4d86e2eace]
Former-commit-id: 818912f7fd22afacd602b3bbffa18151a1c12feb

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -13,7 +13,7 @@
 # 'atom-text-editor':
 #   'enter': 'editor:newline'
 #
-#
+# 'atom-workspace':
 #   'ctrl-shift-p': 'core:move-up'
 #   'ctrl-p': 'core:move-down'
 #
@@ -28,3 +28,6 @@

 'atom-workspace':
   'alt': 'null'
+
+'atom-text-editor':
+  'alt-shift-l': 'editor:split-selections-into-lines'


Add key binding for fixing file per editorconfig

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -95,6 +95,9 @@
   # Swaps selections
   'alt-cmd-t': 'atom-transpose:transpose'

+  # Reformat a file according to the applicable editorconfig settings
+  'alt-cmd-e alt-cmd-f': 'EditorConfig:fix-file'
+
 # Toggles match by whole word in Find and Replace / Project Find panels
 '.platform-darwin .find-and-replace':
   'alt-cmd-b': 'find-and-replace:toggle-whole-word-option'


Set keymap (vim mode) for spellcheck

Set keymap (vim mode) for spellcheck

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -50,8 +50,11 @@
   'space l': 'pane:show-next-item'
   'space j': 'pane:show-previous-item'

-  #  Close pane
+  # Close pane
   'space q': 'tab:close'
+
+  # Show spellcheck
+  'z =': 'spell-check:correct-misspelling'

 'atom-text-editor[mini]':
   'ctrl-u': 'editor:delete-to-beginning-of-line'


Add new split pane keymaps for Atom 1.6

Add new split pane keymaps for Atom 1.6

http://blog.atom.io/2016/03/17/atom-1-6-and-1-7-beta.html

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -11,3 +11,16 @@
 'body':
   'alt-<': 'core:move-to-top'
   'alt->': 'core:move-to-bottom'
+
+'.platform-darwin':
+  # Create a split pane with the current pane item moved into it
+  'cmd-k m left':  'pane:split-left-and-move-active-item'
+  'cmd-k m right': 'pane:split-right-and-move-active-item'
+  'cmd-k m up':    'pane:split-up-and-move-active-item'
+  'cmd-k m down':  'pane:split-down-and-move-active-item'
+
+  # Move the current pane item to the target split pane
+  'cmd-ctrl-shift-left':  'window:move-active-item-to-pane-on-left'
+  'cmd-ctrl-shift-right': 'window:move-active-item-to-pane-on-right'
+  'cmd-ctrl-shift-up':    'window:move-active-item-to-pane-above'
+  'cmd-ctrl-shift-down':  'window:move-active-item-to-pane-below'


Fix bug with the smileys

Fix bug with the smileys


Reversed the x axis to read left-to-right better

--- metrics-ui/ui/js/components/linechart-d3.coffee
+++ metrics-ui/ui/js/components/linechart-d3.coffee
@@ -40,7 +40,7 @@

   scales: (state) ->
     xscale: d3.scale.ordinal()
-      .rangePoints([0, @plotwidth()])
+      .rangePoints([@plotwidth(), 0])
       .domain(state.data.map (d) -> "#{d.day}")
     yscale: d3.scale.linear()
       .range([@plotheight(), 0])


chat: fix merge #2

chat: fix merge #2

--- public/assets/chat/chat.coffee
+++ public/assets/chat/chat.coffee
@@ -303,7 +303,7 @@

       # delay initial queue position, show connecting first
       show = =>
-        @onQueue data.position
+        @onQueue data
       if @initialQueueDelay && !@onInitialQueueDelayId
         @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
         return


better error, arg passing

better error, arg passing

--- lib/clang-format.coffee
+++ lib/clang-format.coffee
@@ -54,17 +54,27 @@
     # Don't catch errors to make them visible to users via atom's UI
     # We need to explicitly ignore stderr since there is no parent stderr on
     # windows and node.js will try to write to it - whether it's there or not
-    args = ("-#{k}=#{v}" for k, v of options).join ' '
+    args = ("-#{k}=\"#{v}\"" for k, v of options).join ' '
     options = input: editor.getText(), stdio: ['pipe', 'pipe', 'ignore']
-    stdout = execSync("#{exe} #{args}", options).toString()

-    # Update buffer with formatted text. setTextViaDiff minimizes re-rendering
-    buffer.setTextViaDiff @getReturnedFormattedText(stdout)
+    try
+      stdout = execSync("#{exe} #{args}", options).toString()
+      # Update buffer with formatted text. setTextViaDiff minimizes re-rendering
+      buffer.setTextViaDiff @getReturnedFormattedText(stdout)
+      # Restore cursor position
+      returnedCursorPos = @getReturnedCursorPosition(stdout)
+      convertedCursorPos = buffer.positionForCharacterIndex(returnedCursorPos)
+      editor.setCursorBufferPosition(convertedCursorPos)

-    # Restore cursor position
-    returnedCursorPos = @getReturnedCursorPosition(stdout)
-    convertedCursorPos = buffer.positionForCharacterIndex(returnedCursorPos)
-    editor.setCursorBufferPosition(convertedCursorPos)
+    catch error
+      atom.confirm
+        message: "Something Went Wrong With ClangFormat"
+        detailedMessage: "This error is most often caused by not having
+                          clang-format installed and on your path. If you do
+                          please create an issue on our github page."
+        buttons:
+          Okay: (->)
+

   shouldFormatOnSaveForScope: (scope) ->
     if atom.config.get('clang-format.formatCPlusPlusOnSave') and scope in ['source.c++', 'source.cpp']


Fix Buffer comparison

Fix Buffer comparison

--- avstreams.coffee
+++ avstreams.coffee
@@ -77,8 +77,7 @@

   # nal_unit_type 7
   updateSPS: (nalUnit) ->
-    if nalUnit isnt @spsNALUnit
-      logger.debug "[stream:#{@id}] updated SPS"
+    if (not @spsNALUnit?) or (nalUnit.compare(@spsNALUnit) isnt 0)
       @spsNALUnit = nalUnit
       @updateSpropParam nalUnit
       try
@@ -106,11 +105,12 @@
         logger.debug "[stream:#{@id}] video avcprofile: #{@videoAVCProfile}"
         isConfigUpdated = true
       if isConfigUpdated
+        logger.debug "[stream:#{@id}] updated SPS"
         @emit 'updateConfig'

   # nal_unit_type 8
   updatePPS: (nalUnit) ->
-    if nalUnit isnt @ppsNALUnit
+    if (not @ppsNALUnit?) or (nalUnit.compare(@ppsNALUnit) isnt 0)
       logger.debug "[stream:#{@id}] updated PPS"
       @ppsNALUnit = nalUnit
       @updateSpropParam nalUnit


Fix HSL overflow issue. Fixes #209

Fix HSL overflow issue. Fixes #209


use ink command for this

use ink command for this

--- lib/julia-client.coffee
+++ lib/julia-client.coffee
@@ -78,7 +78,6 @@
     subs.add atom.commands.add 'atom-text-editor[data-grammar="source julia"]:not([mini])',
       'julia-client:set-working-module': => modules.chooseModule()
       'julia-client:reset-working-module': => modules.resetModule()
-      'julia-client:clear-inline-results': => @ink?.results.removeAll()

     utils.commands subs


this didn't work for some reason

this didn't work for some reason

--- lib/julia-client.coffee
+++ lib/julia-client.coffee
@@ -31,6 +31,4 @@
     editor = atom.workspace.getActiveTextEditor()
     for cursor in editor.getCursors()
       {row, column} = cursor.getScreenPosition()
-      comm.msg 'eval-block', {row: row+1,
-                              column: column+1,
-                              code: editor.getText()}
+      comm.msg 'eval-block', {row: row+1, column: column+1, code: editor.getText()}


JInvitation: only admins can remove an invitation

--- workers/social/lib/social/models/invitation.coffee
+++ workers/social/lib/social/models/invitation.coffee
@@ -114,7 +114,7 @@
   isValid: -> @status in @validTypes

   # remove deletes an invitation from database
-  remove$: permit 'send invitations',
+  remove$: permit 'remove invitation',
     success: (client, callback) ->
       @remove callback


app-settings-menu: Added menu options to internal app settings.

--- client/Framework/core/KD.coffee
+++ client/Framework/core/KD.coffee
@@ -171,6 +171,7 @@
     options.openWith     or= "lastActive" # a String "lastActive","forceNew" or "prompt"
     options.behavior     or= ""           # a String "application", "hideTabs", or ""
     options.thirdParty    ?= no           # a Boolean
+    options.menu         or= null         # {Array.<Object{{title: string, eventName: string, shortcut: string}}>}

     Object.defineProperty KD.appClasses, options.name,
       configurable  : yes


Minor: notification placeholder for referrals

--- client/Main/kodingrouter.coffee
+++ client/Main/kodingrouter.coffee
@@ -281,6 +281,7 @@
       '/Landing/:page'         : noop
       '/R/:username'           : ({params:{username}})->
         KD.mixpanel "Visit referrer url, success", {username}
+        # give a notification to tell that this is a referral link here - SY
         handleRoot.call this

       '/:name?/Logout'         : ({params:{name}})-> requireLogin -> mainController.doLogout()


Troubleshoot: Pinger is renamed as HealthChecker and callback is passed as parameter

--- client/Main/troubleshoot.coffee
+++ client/Main/troubleshoot.coffee
@@ -78,29 +78,31 @@
       @result[name] = new TroubleshootResult name, item
       @result[name].on "completed", =>
         waitingResponse -= 1
+        # we can also send each response to user
         unless waitingResponse
           @emit "pingCompleted"

 class TroubleshootResult extends KDObject

-  constructor: (name, pinger) ->
+  constructor: (name, healthChecker) ->
     @name    = name
-    @pinger  = pinger
+    @healthChecker  = healthChecker
     @status  = "failed"
     super

-    pinger.once "finish", =>
+    healthChecker.once "finish", =>
       @status = "ok"
       @responseTime = @getResponseTime()
       @emit "completed"

-    pinger.once "failed", =>
-      @status = "failed"
+    healthChecker.once "failed", =>
+      @status = "down"
       @emit "completed"

   getResponseTime: ->
-    @pinger.getResponseTime()
+    @healthChecker.getResponseTime()
+

 class ConnectionChecker extends KDObject

@@ -122,13 +124,12 @@
       error   : ->

-class ServicePinger extends KDObject
+class HealthCheck extends KDObject
   [NOTSTARTED, WAITING, SUCCESS, FAILED] = [1..4]

-  constructor: (item, options={}) ->
+  constructor: (options={}, @item, @cb) ->
     super options

-    @item = item
     @identifier = options.identifier or Date.now()
     @status = NOTSTARTED

@@ -136,22 +137,20 @@
     @status = WAITING
     @startTime = Date.now()
     @setPingTimeout()
-    # unless @item.ping is typeof "function"
-    #   throw new Error "ping is not defined"
-    @item.ping(@finish.bind(this))
+    @cb.call @item, @finish.bind(this)

   setPingTimeout: ->
     @pingTimeout = setTimeout =>
       @status = FAILED
-      @emit "failed", @item, @name
+      @emit "failed"
     , 5000

-  finish: ->
+  finish: (data)->
     @status = SUCCESS
     @finishTime = Date.now()
     clearTimeout @pingTimeout
     @pingTimeout = null
-    @emit "finish", @item
+    @emit "finish"

   getResponseTime: ->
     status = switch @status
@@ -161,7 +160,7 @@
         "failed"
       when SUCCESS
         @finishTime - @startTime
-      when "WAITING"
+      when WAITING
         "waiting"

     return status


rename mouseDown & mouseWheel event handlers

--- lib/minimap-view.coffee
+++ lib/minimap-view.coffee
@@ -34,8 +34,8 @@
     @minimapScroll = 0

   initialize: ->
-    @on 'mousewheel', @mouseWheel
-    @on 'mousedown', @mouseDown
+    @on 'mousewheel', @onMouseWheel
+    @on 'mousedown', @onMouseDown

     @subscribe @paneView.model.$activeItem, @onActiveItemChanged
     @subscribe @miniEditorView, 'minimap:updated', @updateMinimapView
@@ -101,8 +101,7 @@

   setMinimapEditorView: ->
     # update minimap-editor
-    setImmediate =>
-      @miniEditorView.setEditorView(@editorView)
+    setImmediate => @miniEditorView.setEditorView(@editorView)

   # UPDATE METHODS

@@ -163,7 +162,7 @@
       @deactivatePaneViewMinimap()
       @log 'minimap is not supported by the current tab'

-  mouseWheel: (e) =>
+  onMouseWheel: (e) =>
     return if @isClicked
     {wheelDeltaX, wheelDeltaY} = e.originalEvent
     if wheelDeltaX
@@ -171,7 +170,7 @@
     if wheelDeltaY
       @editorView.scrollTop(@editorView.scrollTop() - wheelDeltaY)

-  mouseDown: (e) =>
+  onMouseDown: (e) =>
     @isClicked = true
     e.preventDefault()
     e.stopPropagation()


Show amount of remaining submits in review

--- client/components/Review.coffee
+++ client/components/Review.coffee
@@ -22,7 +22,7 @@

     render: () ->

-            <Button onClick={@props.handleReload}>Обновить</Button>
+            ={@state.results.length} <Button onClick={@props.handleReload}>Обновить</Button>
             {
             if @state.results.length == 0

Ревьювить больше нечего, обновите страницу


AppController: sync up ndpane when splitting

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -187,6 +187,13 @@
     ideView        = @activeTabView.parent
     ideParent      = ideView.parent
     newIDEView     = new IDE.IDEView ideViewOptions
+
+    splitViewPanel = @activeTabView.parent.parent
+    if splitViewPanel instanceof KDSplitViewPanel
+      layout = SplitViewPanel._layout
+    else
+      layout = @layout
+
     @activeTabView = null

     ideView.detach()
@@ -194,6 +201,8 @@
     splitView   = new KDSplitView
       type      : type
       views     : [ null, newIDEView ]
+
+    layout.split(type is 'vertical')

     @registerIDEView newIDEView


Collaboration: Write participant name and hash to Realtime API.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -747,13 +747,13 @@

   addParticipant: (initalizeList = no) ->
-    nickname = KD.nick()
+    {hash, nickname} = KD.whoami().profile

     if initalizeList
       @rtm.create 'list', @realTimeDoc, 'participants', []

     @participants = @rtm.getFromModel @realTimeDoc, 'participants'
-    @participants.push nickname
+    @participants.push { nickname, hash }

     @rtm.create 'map', @realTimeDoc, nickname, @createWorkspaceSnapshot()


AppController: Make machine running check a separate method.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -269,18 +269,26 @@
     filesPane.emit 'MachineUnmountRequested', machineData

+  isMachineNotRunning: ->
+
+    states    = [ 'Stopped', 'NotInitialized', 'Terminated', 'Starting', 'Building' ]
+    { state } = @mountedMachine.status
+
+    return states.indexOf(state) > -1
+
+
   createInitialView: ->

     KD.utils.defer =>
       @splitTabView 'horizontal', createNewEditor: no
       @getMountedMachine (err, machine) =>
+
         return unless machine
-        {state} = machine.status

         for ideView in @ideViews
           ideView.mountedMachine = @mountedMachine

-        if state in [ 'Stopped', 'NotInitialized', 'Terminated', 'Starting', 'Building' ]
+        if @isMachineNotRunning()
           nickname     = KD.nick()
           machineLabel = machine.slug or machine.label
           splashs      = IDE.splashMarkups


improve GUI.

improve GUI.

--- client/audio.coffee
+++ client/audio.coffee
@@ -11,7 +11,7 @@
   data[index + 2] = color.b unless isNaN(color.b)
   data[index + 3] = color.a unless isNaN(color.a)

-drawWave = (buffer) ->
+drawWave = (buffer, note) ->
   canvasContext.save()
   canvasContext.fillStyle = "rgb(30, 30, 30)"
   canvasContext.fillRect 0, 0, canvas.width, canvas.height
@@ -23,12 +23,23 @@
     b: 200
     a: 255

+  red =
+    r: 200
+    g: 0
+    b: 0
+    a: 255
+
   width = imageData.width
   height = imageData.height

   for x in [0...width]
     y = Math.floor(height/2+buffer[x*2]*height)
     setPixel imageData, x, y, color
+
+  x = Math.round(width/2 + width * note.diff())
+  for y in [0...height]
+    setPixel imageData, x, y, color
+    setPixel imageData, width/2, y, red

   canvasContext.putImageData imageData, 0, 0

@@ -42,10 +53,10 @@
   recorder = audioContext.createScriptProcessor(bufferSize, 2, 2)
   recorder.onaudioprocess = (e) ->
     left = e.inputBuffer.getChannelData(0)
-    drawWave(left)
     hz = Pitcher.pitch(left, audioContext.sampleRate)
+    note = new Note(hz)
+    drawWave(left, note)
     return unless hz >= 30
-    note = new Note(hz)
     hzElement.innerHTML = 'hz = ' + hz
     noteElement.innerHTML = 'note = ' + note.name()


re-ad unpublish event

re-ad unpublish event

--- client/views/admin/admin.coffee
+++ client/views/admin/admin.coffee
@@ -55,8 +55,8 @@
   'click .for-unpublish': (e, tpl) ->
     e.preventDefault()
     @update
-    published: false
-    publishedAt: null
+      published: false
+      publishedAt: null

 Template.blogAdminDeleteColumn.events


expose modelTypeIdentifier

expose modelTypeIdentifier

--- client/lanes/models/Base.coffee
+++ client/lanes/models/Base.coffee
@@ -52,9 +52,11 @@
         this.trigger('remote-update', triggered, change)
         @parent?.triggerChangeSet?(triggered, change)

+    modelTypeIdentifier: ->
+        _.dasherize(_.last(@FILE.path))
+
     api_path: ->
-        path = if this.FILE then _.last(this.FILE.path) else ''
-        _.pluralize(_.dasherize(path))
+        @FILE.extension.identifier + '/' + _.pluralize(@modelTypeIdentifier())

     urlRoot: ->
         Lanes.config.api_path + '/' + _.result(this, 'api_path')


Improve turn detection

Improve turn detection

--- components/DetectScratch.coffee
+++ components/DetectScratch.coffee
@@ -59,9 +59,9 @@
       return

     turn = Math.abs @angleChange @prevAngle, angle
-    return unless turn > 130
     @prevPoint = touch.movepoint
     @prevAngle = angle
+    return unless turn > 130
     @prevTime = time
     @turns++
     if @turns >= @minturns


ListenNetwork: make the component actually work

--- components/ListenNetwork.coffee
+++ components/ListenNetwork.coffee
@@ -8,34 +8,36 @@
     datatype: 'object'
     description: 'Runtime to listen from'
     process: (event, payload) ->
-      return unless payload is 'data'
-      c.updateListeners payload, c.network
+      return unless event is 'data'
+      c.updateListeners payload, c.graph
   c.inPorts.add 'graph',
     datatype: 'object'
     description: 'Graph to listen to'
     process: (event, payload) ->
-      return unless payload is 'data'
+      return unless event is 'data'
       c.updateListeners c.runtime, payload

   c.outPorts.add 'packet',
     datatype: 'object'

-  c.updateListeners = (runtime, network) ->
-    @runtime.removeListeners 'network', @onNetworkPacket if @runtime
+  c.updateListeners = (runtime, graph) ->
+    @runtime.removeListener 'network', @onNetworkPacket if @runtime?
     @runtime = runtime
     @graph = graph
+    return unless @graph? and @runtime?
+    @graphId = if @graph.properties.library? then "#{@graph.properties.library}/#{@graph.properties.id}" else @graph.properties.id
     @runtime.on 'network', @onNetworkPacket if @runtime

   c.onNetworkPacket = ({command, payload}) =>
-    return unless command is 'network'
-    return unless payload.id and @graph and payload.id == @graph.id
-    @outPorts.packet.send
+    return unless payload.graph
+    return unless payload.graph == c.graphId
+    c.outPorts.packet.send
       edge: payload.id
       type: command
       group: if payload.group? then payload.group else ''
       data: if payload.data? then payload.data else ''
       subgraph: if payload.subgraph? then payload.subgraph else ''
-      runtime: @runtime.definition.id
+      runtime: c.runtime.definition.id

   c.shutdown = () ->
     @updateListeners null, null


Memoize roles, for (not For) over roles

Memoize roles, for (not For) over roles

--- client/users.coffee
+++ client/users.coffee
@@ -119,6 +119,13 @@
       Messages.findOne(id) ?
         _id: id
         title: '(loading)'
+  roles = createMemo ->
+    message = props.user.rolesPartial?[escapedGroup()]?[props.messageID] ? [] \
+      if props.messageID?
+    group = props.user.roles?[escapedGroup()] ? []
+    global = props.user.roles?[wildGroup] ? [] if props.group != wildGroup
+    first = message ? group
+    {message, group, global, first}
   authorLink = -> pathFor 'author',
     group: props.group
     author: props.user.username
@@ -144,22 +151,20 @@
           joined {formatDate props.user.createdAt}

-      <For each={allRoles}>{(role) ->
-        levels = createMemo ->
-          l = []
-          if props.messageID?
-            l.push role in (props.user.rolesPartial?[escapedGroup()]?[props.messageID] ? [])
-          l.push role in (props.user.roles?[escapedGroup()] ? [])
-          if props.group != wildGroup
-            l.push role in (props.user.roles?[wildGroup] ? [])
-          l.pop() until l[l.length-1] or l.length == 1
-          l
+      {
+      r = roles()
+      for role in allRoles
+        levels = []
+        levels.push role in r.message if r.message?
+        levels.push role in r.group if r.group?
+        levels.push role in r.global if r.global?
+        levels.pop() until levels.length == 1 or levels[levels.length-1]
         showLevel = (i) ->
-          return if i >= levels().length
-          level = if levels()[i] then 'YES' else 'NO'
+          return if i >= levels.length
+          level = if levels[i] then 'YES' else 'NO'
           level += '*' if i == 1 + props.messageID?  # global override
           space = if i == 0 then '' else 'space'
-          if i == levels().length - 1  # last level
+          if i == levels.length - 1  # last level
             if i == 0
               level
             else
@@ -168,8 +173,8 @@
             {level}

           {if props.admin.group
-            <button className="roleButton btn #{if levels()[0] then 'btn-success' else 'btn-danger'}"
-            onClick={[onRole, props]}>
+            <button className="roleButton btn #{if levels[0] then 'btn-success' else 'btn-danger'}"
+             onClick={[onRole, props]}>
               {showLevel 0}
             </button>
           else
@@ -178,7 +183,7 @@
           {showLevel 1}
           {showLevel 2}

-      }</For>
+      }

     <Show when={partialMember()?.length}>
       {### Extra row to get parity right ###}


upgrade to convert to jsx

upgrade to convert to jsx

--- tutor/src/components/app.cjsx
+++ tutor/src/components/app.cjsx
@@ -3,7 +3,7 @@

 Router = require '../helpers/router'
 Analytics = require '../helpers/analytics'
-Navbar = require './navbar'
+{default: Navbar} = require './navbar'
 MatchForTutor = require './match-for-tutor'

 {DragDropContext} = require 'react-dnd'


First draft of fixing shorthand objects mixed with regulars within arrays.

--- nodes.coffee
+++ nodes.coffee
@@ -499,28 +499,30 @@
       ifn = unfoldSoak o, call, 'variable'
     ifn

+  # Walk through the objects in the arguments, moving over simple values.
+  # This allows syntax like `call a: b, c` into `call({a: b}, c);`
+  filterImplicitObjects: (list) ->
+    nodes = []
+    for node in list
+      unless node.isObject?() and node.base.generated
+        nodes.push node
+        continue
+      obj = null
+      for prop in node.base.properties
+        if prop instanceof Assign
+          nodes.push obj = new Obj properties = [], true if not obj
+          properties.push prop
+        else
+          nodes.push prop
+          obj = null
+    nodes
+
   # Compile a vanilla function call.
   compileNode: (o) ->
     @variable?.front = @front
     if code = Splat.compileSplattedArray o, @args, true
       return @compileSplat o, code
-
-    # Walk through the objects in the arguments, moving over simple values.
-    # This allows syntax like `call a: b, c` into `call({a: b}, c);`
-    args = []
-    for arg in @args
-      unless arg.isObject?() and arg.base.generated
-        args.push arg
-        continue
-      obj = null
-      for prop in arg.base.properties
-        if prop instanceof Assign
-          args.push obj = new Obj properties = [], true if not obj
-          properties.push prop
-        else
-          args.push prop
-          obj = null
-
+    args = @filterImplicitObjects @args
     args = (arg.compile o, LEVEL_LIST for arg in args).join ', '
     if @isSuper
       @superReference(o) + ".call(this#{ args and ', ' + args })"
@@ -755,11 +757,14 @@

   children: ['objects']

+  filterImplicitObjects: Call::filterImplicitObjects
+
   compileNode: (o) ->
     return '[]' unless @objects.length
     o.indent += TAB
-    return code if code = Splat.compileSplattedArray o, @objects
-    code = (obj.compile o, LEVEL_LIST for obj in @objects).join ', '
+    objs = @filterImplicitObjects @objects
+    return code if code = Splat.compileSplattedArray o, objs
+    code = (obj.compile o, LEVEL_LIST for obj in objs).join ', '
     if code.indexOf('\n') >= 0
       "[\n#{o.indent}#{code}\n#{@tab}]"
     else


fixed a bug where `in` malfunctioned in commaed lists

--- nodes.coffee
+++ nodes.coffee
@@ -1273,11 +1273,9 @@

   compileLoopTest: (o) ->
     [sub, ref] = @object.compileReference merge(o, top: yes), precompile: yes
-    code  = utility('indexOf') + ".call(#{ @array.compile o }, #{ref}) "
-    code += (if @negated then '< 0' else '>= 0')
-    return code if sub is ref
-    code = sub + ', ' + code
-    if @parenthetical then code else "(#{code})"
+    code = utility('indexOf') + ".call(#{ @array.compile o }, #{ref}) " +
+           if @negated then '< 0' else '>= 0'
+    if sub is ref then code else "(#{sub}, #{code})"

   toString: (idt) ->
     super idt, @constructor.name + if @negated then '!' else ''


self-compiling pattern matching correctly

--- nodes.coffee
+++ nodes.coffee
@@ -600,6 +600,7 @@
       if obj instanceof SplatNode
         val: new LiteralNode(obj.compile_value(o, val_var, @variable.base.objects.indexOf(obj)))
       else
+        idx: new LiteralNode(idx) unless typeof idx is 'object'
         val: new ValueNode(new LiteralNode(val_var), [new access_class(idx)])
       assigns.push(new AssignNode(obj, val).compile(o))
     assigns.join("\n")
@@ -664,11 +665,12 @@
     if @index then @compile_param(o) else @name.compile(o)

   compile_param: (o) ->
-    o.scope.find @name
-    @name + ' = Array.prototype.slice.call(arguments, ' + @index + ')'
+    name: @name.compile(o)
+    o.scope.find name
+    name + ' = Array.prototype.slice.call(arguments, ' + @index + ')'

   compile_value: (o, name, index) ->
-    "Array.prototype.slice.call(" + @name + ', ' + @index + ')'
+    "Array.prototype.slice.call(" + name + ', ' + index + ')'

 }


log user requests for production

log user requests for production

--- config/requestloggerfile.coffee
+++ config/requestloggerfile.coffee
@@ -2,4 +2,4 @@
 	requestloggerfile:
 		format:			'[:date[clf]] :method :url :status :response-time ms'
 		logLocation:	'file'
-		fileLocation:	'access.log'+		inProduction:	true


Put a guard on sentry dsn

Put a guard on sentry dsn

--- config/settings.defaults.coffee
+++ config/settings.defaults.coffee
@@ -39,6 +39,7 @@
 	parallelSqlQueryLimit:1
 	filestoreDomainOveride: process.env["FILESTORE_DOMAIN_OVERRIDE"]
 	texliveImageNameOveride: process.env["TEX_LIVE_IMAGE_NAME_OVERRIDE"]
+if process.env['SENTRY_DSN']
 	sentry:
 		dsn: process.env['SENTRY_DSN']


Smooth scroll; initial handle keyboard repeat.

--- content_scripts/scroller.coffee
+++ content_scripts/scroller.coffee
@@ -2,6 +2,7 @@
 # activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for
 # input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document.
 #
+root = exports ? window
 activatedElement = null
 settings = null

@@ -78,44 +79,81 @@
   element[axisName] - before

 # Scroll by a relative amount (a number) in some direction, possibly smoothly.
-doScrollBy = (element, direction, amount, wantSmooth) ->
-  axisName = scrollProperties[direction].axisName
+doScrollBy = do ->
+  time = 0
+  lastActivationId = -1
+  keyupHandler = null

-  unless wantSmooth and settings.get "smoothScroll"
-    return performScroll element, axisName, amount
+  # When the keyboard is repeating and the key is released, sometimes the last key event is delivered *after*
+  # the keyup event.  This could cause indefinite scrolling with no key pressed, because no further keyup
+  # event is delivered (we behave as if the final key is still pressed).  To get around this, we briefly hold
+  # a lock to prevent a new smooth scrolling event from beginning too soon after the end of the previous
+  # scroll.  This is a hack.
+  keyLock = false

-  duration = 100 # Duration in ms.
-  fudgeFactor = 25
+  (element, direction, amount, wantSmooth) ->
+    axisName = scrollProperties[direction].axisName

-  # Allow a bit longer for longer scrolls.
-  duration += fudgeFactor * Math.log Math.abs amount
+    unless wantSmooth and settings.get "smoothScroll"
+      return performScroll element, axisName, amount

-  roundOut = if 0 <= amount then Math.ceil else Math.floor
+    # Assumption.  If animator A is started before animator B, then A will also finish before B.  We need to
+    # verify this - it may not always be true.

-  # Round away from 0, so that we don't leave any scroll amount unscrolled.
-  delta = roundOut(amount / duration)
+    console.log("locked") if keyLock
+    if lastActivationId == time or keyLock
+      # This is a keyboard repeat, and the most-recently activated animator will handle it.
+      return

-  animatorId = null
-  start = null
-  lastTime = null
-  scrolledAmount = 0
+    if not keyupHandler
+      keyupHandler = root.handlerStack.push keyup: ->
+        time += 1
+        keyLock = true
+        setTimeout((-> keyLock = false), 10)
+        console.log "keyup", time, lastActivationId
+        true # Do not prevent propagation.

-  animate = (timestamp) ->
-    start ?= timestamp
+    lastActivationId = activationId = ++time
+    console.log activationId

-    progress = Math.min(timestamp - start, duration)
-    scrollDelta = roundOut(delta * progress) - scrolledAmount
-    scrolledAmount += scrollDelta
+    duration = 100 # Duration in ms.
+    fudgeFactor = 25

-    if performScroll(element, axisName, scrollDelta, false) != scrollDelta or
-       progress >= duration
-      # One final call of performScroll to check the visibility of the activated element.
-      performScroll(element, axisName, 0, true)
-      window.cancelAnimationFrame(animatorId)
-    else
-      animatorId = window.requestAnimationFrame(animate)
+    # Allow a bit longer for longer scrolls.
+    duration += fudgeFactor * Math.log Math.abs amount

-  animatorId = window.requestAnimationFrame(animate)
+    # Round away from 0, so that we don't leave any scroll amount unscrolled.
+    roundOut = if 0 <= amount then Math.ceil else Math.floor
+    delta = roundOut(amount / duration)
+
+    animatorId = null
+    lastTime = null
+    start = null
+    scrolledAmount = 0
+
+    stopScrolling = (progress) ->
+      if activationId == time
+        # This is the most recent animator, and the key is still pressed.
+        false
+      else
+        duration <= progress
+
+    animate = (timestamp) ->
+      start ?= timestamp
+
+      progress = timestamp - start
+      scrollDelta = roundOut(delta * progress) - scrolledAmount
+      scrolledAmount += scrollDelta
+
+      if performScroll(element, axisName, scrollDelta, false) != scrollDelta or stopScrolling progress
+          # One final call of performScroll to check the visibility of the activated element.
+          performScroll(element, axisName, 0, true)
+          window.cancelAnimationFrame(animatorId)
+          time += 1 if activationId == time
+      else
+        animatorId = window.requestAnimationFrame(animate)
+
+    animatorId = window.requestAnimationFrame(animate)

 Scroller =
   init: (frontendSettings) ->
@@ -148,5 +186,4 @@
     amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]
     doScrollBy element, direction, amount, wantSmooth

-root = exports ? window
 root.Scroller = Scroller


URL decode bookmarklets

URL decode bookmarklets

--- content_scripts/vomnibar.coffee
+++ content_scripts/vomnibar.coffee
@@ -203,7 +203,7 @@
     navigateToUrl: (url, openInNewTab) ->
       # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
       if url.startsWith "javascript:"
-        eval url["javascript:".length..]
+        eval decodeURIComponent(url["javascript:".length..])
       else
         chrome.extension.sendRequest(
           handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"


react-based course: links to grade each student; buttons to assign to one student, collect from one, etc.

--- salvus/page/editor_course.cjsx
+++ salvus/page/editor_course.cjsx
@@ -41,10 +41,11 @@
 - [x] (1:00?) return all... button
 - [x] (0:45?) (0:20)  counter for each page heading (num students, num assignments)

-- [ ] (1:00?) link to grade each student
-- [ ] (1:00?) buttons to assign to one student, collect from one, etc.
+- [x] (2:00?) (0:59) links to grade each student; buttons to assign to one student, collect from one, etc.
+- [ ] (1:00?) fix bug in that opening directories in different projects doesn't seem to work (with open_directory/open_file, etc. actions in project store.)

 ---
+- [ ] (1:30?) display extra info about each student when they are clicked on (in students page)
 - [ ] (0:30?) when adding assignments filter out folders contained in existing assignment folders

 - [ ] (0:45?) delete student; show deleted students; permanently delete students
@@ -384,7 +385,7 @@
                         last_return_graded = syncdb.select_one(where:where).last_return_graded ? {}
                         last_return_graded[student.get('student_id')] = {time: misc.mswalltime(), error:err}
                         @_update(set:{last_return_graded:last_return_graded}, where:where)
-                        cb(err)
+                        cb?(err)

         # Copy the given assignment to all non-deleted students, doing 10 copies in parallel at once.
         return_assignment_to_all_students: (assignment, cb) =>
@@ -482,6 +483,44 @@
                     @set_activity(id:id)
                     cb?()

+        copy_assignment: (type, assignment_id, student_id) =>
+            # type = assigned, collected, graded
+            switch type
+                when 'assigned'
+                    @copy_assignment_to_student(assignment_id, student_id)
+                when 'collected'
+                    @copy_assignment_from_student(assignment_id, student_id)
+                when 'graded'
+                    @return_assignment_to_student(assignment_id, student_id)
+                else
+                    @set_error("copy_assignment -- unknown type: #{type}")
+
+        open_assignment: (type, assignment_id, student_id) =>
+            # type = assigned, collected, graded
+            assignment = store.get_assignment(assignment_id)
+            student    = store.get_student(student_id)
+            student_project_id = student.get('project_id')
+            if not student_project_id?
+                @set_error("open_assignment: student project not yet created")
+                return
+            # Figure out what to open
+            switch type
+                when 'assigned' # where project was copied in the student's project.
+                    path = assignment.get('target_path')
+                    proj = student_project_id
+                when 'collected'   # where collected locally
+                    path = assignment.get('collect_path') + '/' + student.get('student_id')  # TODO: refactor
+                    proj = project_id
+                when 'graded'  # where project returned
+                    path = assignment.get('graded_path')  # refactor
+                    proj = student_project_id
+                else
+                    @set_error("open_assignment -- unknown type: #{type}")
+            if not proj?
+                @set_error("no such project")
+                return
+            # Now open it
+            flux.getProjectActions(proj).open_directory(path)

     actions = flux.createActions(name, CourseActions)

@@ -833,31 +872,43 @@
 StudentList = rclass
     displayName : "CourseEditor-StudentList"
     propTypes:
-        students   : rtypes.object.isRequired
-        user_map   : rtypes.object.isRequired
-        info       : rtypes.func.isRequired
-
-    render_last: (name, obj) ->
+        name     : rtypes.string.isRequired
+        flux     : rtypes.object.isRequired
+        students : rtypes.object.isRequired
+        user_map : rtypes.object.isRequired
+        info     : rtypes.func.isRequired
+
+    open: (type, assignment_id, student_id) ->
+        @props.flux.getActions(@props.name).open_assignment(type, assignment_id, student_id)
+
+    copy: (type, assignment_id, student_id) ->
+        @props.flux.getActions(@props.name).copy_assignment(type, assignment_id, student_id)
+
+    render_last: (name, obj, type, info) ->
+        open = => @open(type, info.assignment_id, info.student_id)
+        copy = => @copy(type, info.assignment_id, info.student_id)
         v = [{name+': '}]
         if obj?
             if obj.time?
                 v.push({new Date(obj.time).toLocaleString()})
+                v.push(e.preventDefault();open()}>(open))
             if obj.error
                 v.push({obj.error})
+            v.push(e.preventDefault();copy()}>(re-copy))
         else
-            v.push("(not yet)") # TODO: put a button on it
+            v.push(e.preventDefault();copy()}>(copy))
         return v

     render_student_info: (info) ->
         <Row >
             <Col md=4 key='last_assignment'>
-                {@render_last('Assigned', info.last_assignment)}
+                {@render_last('Assigned', info.last_assignment, 'assigned', info)}
             </Col>
             <Col md=4 key='collect'>
-                {@render_last('Collected', info.last_collect)}
+                {@render_last('Collected', info.last_collect, 'collected', info)}
             </Col>
             <Col md=4 key='return_graded'>
-                {@render_last('Returned', info.last_return_graded)}
+                {@render_last('Returned', info.last_return_graded, 'graded', info)}
             </Col>
         </Row>

@@ -895,6 +946,7 @@
 AssignmentStudents = rclass
     displayName : "CourseEditor-AssignmentStudents"
     propTypes:
+        name       : rtypes.string.isRequired
         flux       : rtypes.object.isRequired
         assignment : rtypes.object.isRequired
         students   : rtypes.object.isRequired
@@ -906,6 +958,7 @@
         last_assignment    = assignment.get('last_assignment')?.toJS() ? {}
         last_collect       = assignment.get('last_collect')?.toJS() ? {}
         last_return_graded = assignment.get('last_return_graded')?.toJS() ? {}
+        assignment_id = assignment.get('assignment_id')
         info = (student_id) ->
             # determine following info:
             #   - if (when) assignment assigned to this student
@@ -917,9 +970,12 @@
                 last_assignment    : last_assignment[student_id]
                 last_collect       : last_collect[student_id]
                 last_return_graded : last_return_graded[student_id]
+                student_id         : student_id
+                assignment_id      : assignment_id
             return x

-        <StudentList info={info} students={@props.students} user_map={@props.user_map} />
+        <StudentList info={info} name={@props.name} flux={@props.flux}
+                     students={@props.students} user_map={@props.user_map} />

     render: ->

@@ -962,7 +1018,7 @@
         <Row  style={entry_style}>
             <Col sm=12>
                 <Panel header={@render_more_header()}>
-                    <AssignmentStudents flux={@props.flux} assignment={@props.assignment}
+                    <AssignmentStudents flux={@props.flux} name={@props.name} assignment={@props.assignment}
                                         students={@props.students} user_map={@props.user_map} />
                 </Panel>
             </Col>


CSS TextInput keys.focus support added

CSS TextInput keys.focus support added

--- impl/css/level0/text/input.coffee
+++ impl/css/level0/text/input.coffee
@@ -43,6 +43,11 @@
 				e.stopPropagation()
 			return

+		textElem.addEventListener 'focus', ->
+			self.keys.focus = true
+		textElem.addEventListener 'blur', ->
+			self.keys.focus = false
+
 		@onTextChange ->
 			textElem.value = @text
 		textElem.addEventListener 'input', ->


CSS Scrollable impl bug with minus contentY on wheel event fixed

--- impl/css/level2/scrollable.coffee
+++ impl/css/level2/scrollable.coffee
@@ -40,10 +40,10 @@

 		setContentY = (val) ->
 			max = self._impl.contentItem?._height - self._height or 0
+			if val > max
+				val = max
 			if val < 0
 				val = 0
-			if val > max
-				val = max

 			oldVal = self.contentY
 			if val isnt oldVal


stubs syncAuth

stubs syncAuth

--- components/layout/client.coffee
+++ components/layout/client.coffee
@@ -19,6 +19,7 @@
   setupViews()
   setupAjaxHeaders()
   setupAnalytics()
+  # syncAuth()
   # setupFastClick()
   initShortCuts()
   showBetaMessage()
@@ -69,11 +70,24 @@

   mediator.shared.pusher = new Pusher(sd.PUSHER_KEY) if Pusher?

+syncAuth = module.exports.syncAuth = ->
+  # Log out of Force if you're not logged in to Gravity
+  if sd.CURRENT_USER
+    $.ajax
+      url: "#{sd.API_URL}/accounts"
+      success: ensureFreshUser
+      error: ->
+        $.ajax
+          method: 'GET'
+          url: '/me/sign_out'
+          complete: ->
+            window.location.reload()
+
 ensureFreshUser = (data) ->
-  return unless sd.CURRENT_USER
+  return unless sd.CURRENT_USER and sd.CURRENT_USER.id is 15
   for attr in ['id', 'authentication_token', 'avatar_image', 'email', 'first_name', 'id',
                'last_name', 'manifest', 'notification_count', 'shortcuts_id', 'slug', 'username']
-    if not _.isEqual data[attr], sd.CURRENT_USER[attr]
+    if not _.isEqual data.user[attr], sd.CURRENT_USER[attr]
       return $.ajax('/me/refresh').then -> mediator.trigger 'current_user:refreshed'

 setupAjaxHeaders = ->


Update Landing Page

Update Landing Page

text

--- src/components/LandingPage.coffee
+++ src/components/LandingPage.coffee
@@ -116,7 +116,7 @@

               when V.DONATE
                 div null,
-                  h2 null, "Hier kannst du doppelte Wirkung zeigen."
+                  h2 null, "hier kannst du doppelte Wirkung zeigen."
                   p null,
                     """
                     Unsere Crowdfunding Kampagne auf www.betterplace.org


Shrink padding for blog

Shrink padding for blog

--- examples/blog/pages/app.cjsx
+++ examples/blog/pages/app.cjsx
@@ -48,7 +48,7 @@
     <Container
       style={{
         maxWidth: rhythm(24)
-        padding: "#{rhythm(2)} #{rhythm(1)}"
+        padding: "#{rhythm(2)} #{rhythm(1/2)}"
       }}
     >
       {header}


refactor(app generator): make waiting for async stuff purely promise based

--- generators/app/templates/src/app.coffee
+++ generators/app/templates/src/app.coffee
@@ -227,40 +227,32 @@
 ##  [App]
 ##

-router          = require( './router.coffee' )<% if ( i18n ) { %>
-
-##  Initialize locale passing Handlebars runtime and default locale.
-##  It's loading the locale file async so wait starting the app until that's done.
-##
-locale.initialize( Handlebars, 'en_GB' ).done(
+router          = require( './router.coffee' )
+
+initialized = Q.all(
+    [
+        ##  Wait until the DOM is ready.
+        ##
+        new Q.Promise( ( resolve ) -> $( resolve ); return; )<% if ( i18n ) { %>
+
+        ##  Initialize locale passing Handlebars runtime and default locale.
+        ##  Wait until it's been loaded.
+        ##
+        locale.initialize( Handlebars, 'en_GB' )<% } %>
+    ]
+)
+
+initialized.done(

     () ->

-        ##  Start the app when the DOM is ready.
-        ##
-        $( () ->
-
-            router.startApp()
-
-            return
-
-        )
+        router.startApp()

         return

     () ->
-        console.error( 'Failed to retrieve default locale file.' )
+        console.error( 'Failed to initialize' )

         return

-)<% } else { %>
-
-##  Start the app when the DOM is ready.
-##
-$( () ->
-
-    router.startApp()
-
-    return
-
-)<% } %>
+)


Allow anonymous use of the editor.

Allow anonymous use of the editor.

A previous change caused no github client to be created until credentials
were set on the session, but for anonymous access, such credentials are never
set.

--- scripts/gh-book/session.coffee
+++ scripts/gh-book/session.coffee
@@ -60,7 +60,11 @@
           @set('canCollaborate', canCollaborate)

     getClient: () ->
-      return @_client or throw 'BUG: Client was not loaded yet'
+      if not @_client
+        @_client = new Github({})
+        @set('canCollaborate', false)
+        console and console.warn('Using anonymous access for the GithUb API')
+      return @_client

     getRepo: () ->
       repoUser = @get('repoUser')


Shows docpad path when running "docpad --version"

Works for local and global instances

--- src/lib/interfaces/console.coffee
+++ src/lib/interfaces/console.coffee
@@ -24,7 +24,7 @@

 		commander
 			.version(
-				docpad.getVersionString()
+				docpad.getVersionString() + ' ' + pathUtil.join(__dirname, "../../..")
 			)
 			.option(
 				'-o, --out <outPath>'


Fix unlimited (edit) spreadsheet view

Fix unlimited (edit) spreadsheet view

--- web/volume/spreadsheet.coffee
+++ web/volume/spreadsheet.coffee
@@ -13,7 +13,7 @@

         Editing = $scope.editing = $attrs.edit != undefined
         ID = $scope.id = $attrs.id ? 'ss'
-        Limit = $attrs.limit
+        Limit = $attrs.limit || Infinity
         Key = undefined
         Filter = () -> true

@@ -1168,7 +1168,7 @@
           false

         $scope.unlimit = ->
-          Limit = undefined
+          Limit = Infinity
           fill()

         if Editing


change whole volume in global record cell to include 'only displayed in volume description'

--- web/volume/spreadsheet.coffee
+++ web/volume/spreadsheet.coffee
@@ -372,7 +372,7 @@
             release: s.release+''
           if s == volume.top
             d.global = true
-            d.summary = "Whole volume"
+            d.summary = "Whole volume (only displayed in volume description)"
           d

         populateRecordData = (r) ->


Groups: Registration completed message reformatted

--- client/Login/AppView.coffee
+++ client/Login/AppView.coffee
@@ -342,9 +342,9 @@
         $.cookie 'clientId', replacementToken
         KD.getSingleton('mainController').accountChanged account

-        title = ''
-        title += unless err then 'Good to go, Enjoy!' \
-                else 'Quota exceeded and could not join to the group. Please contact with group admin'
+        titleText = unless err then 'Good to go, Enjoy!' \
+                    else 'Quota exceeded and could not join to the group. Please contact with group admin'
+        title = '#{titleText}'

         new KDNotificationView
           cssClass  : "login"


Login: reload ontime after registration

Login: reload ontime after registration

--- client/Login/AppView.coffee
+++ client/Login/AppView.coffee
@@ -375,14 +375,9 @@
         return location.reload()  unless KD.remote.isConnected()

         @headBanner.hide()
-        #could not joined to the group. Directing to Koding
-        window.location.href= "/" if err
-
-        KD.utils.wait 1000, =>
-          @registerForm.reset()
-          @registerForm.button.hideLoader()
-          @hide()
-          location.replace('/')
+
+        window.location.href= '/'
+

   doFinishRegistration: (formData) ->
     (KD.getSingleton 'mainController').handleFinishRegistration formData, @bound 'afterLoginCallback'


Client: More verbose debugging information

--- client/app/js/client.coffee
+++ client/app/js/client.coffee
@@ -16,6 +16,8 @@
       @connectionAttempts = 0

     @socket.onmessage = (data) =>
+      console.log "Received data: #{data}"
+
       # todo: error handling. If there's a syntax error here, its because of Json.parse
       data = JSON.parse(data.data)
       @handleEvent(data.messageType, data.data)
@@ -25,9 +27,8 @@
       @closed = true

   handleEvent: (type, data) =>
-    console.log "Received event #{type}"
+    data ?= []

-    data ?= []
     for callback in (@callbacks[type] || [])
       callback.apply(this, [this].concat(data))


IDE: Fix fat/slim arrow.

IDE: Fix fat/slim arrow.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -89,7 +89,7 @@
     {windowController} = kd.singletons
     windowController.addFocusListener @bound 'setActivePaneFocus'

-    @workspace.once 'ready', -> @getView().addSubView @workspace.getView()
+    @workspace.once 'ready', => @getView().addSubView @workspace.getView()

     kd.singletons.appManager.on 'AppIsBeingShown', (app) =>


reviveTabs: stop if a pane has no view

reviveTabs: stop if a pane has no view

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -663,12 +663,10 @@

     for ideView in @ideViews
       for pane in ideView.tabView.panes when pane
-        view = pane.getSubViews().first
+        return  unless view = pane.getSubViews().first
         if paneType
-          if view.getOptions().paneType is paneType
-            callback view
-        else
-          callback view
+        then callback view  if view.getOptions().paneType is paneType
+        else callback view

   updateSettings: (component, key, value) ->


Closes #12. Implement customizable download directories for uTorrent

--- server/clients/uTorrent.coffee
+++ server/clients/uTorrent.coffee
@@ -16,8 +16,6 @@
   list: (callback) ->
     @utorrent.call("list", (err, data) ->
         return callback(err, null) if err
-
-        # console.log(data)
         results = {
             torrents: []
         }
@@ -28,7 +26,6 @@
                 eta: datum[10]
             }
             results.torrents.push(torrent)
-        # console.log(results)
         return callback err, results
     )

@@ -37,31 +34,29 @@
     # Check if custom download-dir
     downloadDir = @getDownloadDirForTorrent(torrent)
     if downloadDir?
-      options['download-dir'] = downloadDir
+      options['download_dir'] = downloadDir.basePath
+      options['path'] = downloadDir.subPath
     # Check for missing Torrent URL
     unless torrent.torrentUrl
       callback new Error("Missing Torrent URL field."), null
     else
-
-      @utorrent.call('add-url',
-        {
-            's': torrent.torrentUrl,
-            'download_dir': 0,
-            'path': '' # TODO: allow for customizing download directory
-        },
-        (err, result) =>
-            # console.log("add-file", err, result)
-            return callback err, result
+      options['s'] = torrent.torrentUrl
+      @utorrent.call('add-url', options, (err, result) =>
+        # console.log("add-file", err, result)
+        return callback err, result
       )

   getDownloadDirForTorrent: (torrent) ->
     downloadDirs = config.downloadDirs
+    def = { # Default
+        "basePath": 0,
+        "subPath": ""
+      }
     if downloadDirs
       downloadDir = downloadDirs[torrent.category]
-      if typeof downloadDir is "object"
+      # console.log(typeof downloadDir, torrent.category)
+      renderPathForTorrent = (torrent, template, defDir) ->
         title = torrent.title
-        template = downloadDir.template || null
-        defDir = downloadDir.default || null
         e = episode(title)
         # Check if it was properly parse
         if e.matches.length > 0
@@ -73,7 +68,33 @@
           return downloadDir
         else
           return defDir
+
+      if typeof downloadDir is "object"
+        basePath = downloadDir.basePath || 0
+        template = downloadDir.template || ""
+        defDir = downloadDir.default || ""
+        subPath = renderPathForTorrent(torrent, template, defDir)
+        return {
+            "basePath": basePath
+            "subPath": subPath
+        }
+      else if typeof downloadDir is "number"
+        # Specifying Base-Path
+        return {
+            "basePath": downloadDir
+            "subPath": ""
+        }
+      else if typeof downloadDir is "string"
+        # Specifiying Sub-Path
+        basePath = 0 # Default
+        subPath = renderPathForTorrent(torrent, downloadDir, "")
+        return {
+            "basePath": basePath,
+            "subPath": subPath
+        }
       else
-        return downloadDir
+        # Unsupported
+        console.warn("Unsupported 'downloadDirs' configuration for category #{torrent.category}. Please check that.")
+        return def
     else
-      return null
+      return def


Session: validate clientId

Session: validate clientId

--- server/lib/server/index.coffee
+++ server/lib/server/index.coffee
@@ -109,8 +109,7 @@
 app.use (req, res, next) ->
   {JSession} = koding.models
   {clientId} = req.cookies
-  return next() if clientId
-  JSession.createSession (err, session, account)->
+  JSession.fetchSession clientId, (err, session)->
     return next() if err or not session
     res.cookie "clientId", session.clientId, {}
     next()


StaticPage: Add slug parameter to path

StaticPage: Add slug parameter to path

--- server/lib/server/index.coffee
+++ server/lib/server/index.coffee
@@ -331,15 +331,16 @@
 # Handles all internal pages
 # /USER || /SECTION || /GROUP[/SECTION] || /APP
 #
-app.all '/:name/:section?*', (req, res, next)->
-
+app.all '/:name/:section?/:slug?*', (req, res, next)->
   {JName, JGroup} = koding.models
-  {name, section} = req.params
+  {name, section, slug} = req.params
   isCustomPreview = req.cookies["custom-partials-preview-mode"]
-  path            = if section then "#{name}/#{section}" else name
+
+  path = name
+  path = "#{path}/#{section}"  if section
+  path = "#{path}/#{slug}"     if slug

   return res.redirect 301, req.url.substring 7  if name in ['koding', 'guests']
-
   # Checks if its an internal request like /Activity, /Terminal ...
   #
   bongoModels = koding.models


stylefix: decreased indent

stylefix: decreased indent

--- coffee/src/util/test.coffee
+++ coffee/src/util/test.coffee
@@ -2,124 +2,120 @@

 goog.require 'acorn.util'

+test = acorn.util.test

-acorn.util.test =
+# call pattern: Module [, modelOptions] [, tests]
+test.describeShellModule = (Module, modelOptions, tests) =>
+
+  # cycle arguments if necessary
+  unless athena.lib.util.isStrictObject modelOptions
+    tests = modelOptions
+    modelOptions = {}
+
+  derives = athena.lib.util.derives
+  Shell = acorn.shells.Shell
+
+  acorn.util.assert Shell, 'goog.require \'acorn.shells.Shell\' before ' +
+    'calling acorn.util.test.describeShellModule'
+
+  describeView = athena.lib.util.test.describeView
+
+  isOrDerives = (Subclass, Superclass) =>
+    Subclass == Superclass or derives Subclass, Superclass
+
+  describe "#{Module.id} shell module", ->
+
+    it 'should contain required properties', ->
+      requiredProperties = [ 'id', 'title', 'description', 'icon' ]
+      _.each requiredProperties, (property) ->
+        expect(Module[property]).toBeDefined()
+        expect(_.isString Module[property]).toBe true
+
+    it 'should contain required Model and View classes', ->
+      requiredClasses = [ 'Model', 'MediaView', 'RemixView' ]
+      _.each requiredClasses, (property) ->
+        expect(Module[property]).toBeDefined()
+        expect(_.isFunction Module[property]).toBe true
+
+    viewOptions = -> model: new Module.Model modelOptions
+
+    describe "#{Module.id}.Model", ->
+      Model = Module.Model
+
+      it 'should derive from athena.lib.Model', ->
+        expect(derives Model, athena.lib.Model).toBe true
+
+      it 'should derive from (or be) Shell.Model', ->
+        expect(isOrDerives Model, Shell.Model).toBe true
+
+      it 'should correctly assign attributes', ->
+        m = new Model _.extend modelOptions, {a: 1, b:2}
+        expect(m.get 'a').toBe 1
+        expect(m.get 'b').toBe 2
+
+      it 'should correctly support setting of attributes', ->
+        m = new Model _.extend modelOptions, {a: 1, b:2}
+        m.set 'a', 2
+        m.set 'b', 3
+        expect(m.get 'a').toBe 2
+        expect(m.get 'b').toBe 3
+
+      it 'should throw an exception on save/sync', ->
+        m = new Model _.extend modelOptions, {a: 1, b:2}
+        expect(m.save).toThrow()
+        expect(m.sync).toThrow()
+
+      it 'should be clonable (with deep-copies)', ->
+        deep = {'a': 5}
+        m = new Model _.extend modelOptions, {shellid:'Shell', a: b: c: deep}
+        expect(m.clone().attributes.a.b.c).toEqual deep
+        expect(m.clone().attributes.a.b.c).not.toBe deep
+
+        # ensure the same thing breaks on Backbone's non-deep copy
+        b = new Backbone.Model {shellid:'Shell', a: b: c: deep}
+        expect(b.clone().attributes.a.b.c).toEqual deep
+        expect(b.clone().attributes.a.b.c).toBe deep
+
+      it 'should have a toJSONString function', ->
+        expect(typeof Model::toJSONString).toBe 'function'
+        m = new Model _.extend modelOptions,
+            {shellid:'Shell', a: b: c: {'a': 5}}
+        s = m.toJSONString()
+        expect(JSON.stringify m.attributes).toEqual s

-  # call pattern: Module [, modelOptions] [, tests]
-  describeShellModule: (Module, modelOptions, tests) =>
+    describeView Module.MediaView, athena.lib.View, viewOptions(), ->

-    # cycle arguments if necessary
-    unless athena.lib.util.isStrictObject modelOptions
-      tests = modelOptions
-      modelOptions = {}
+      MediaView = Module.MediaView

-    derives = athena.lib.util.derives
-    Shell = acorn.shells.Shell
+      it 'should derive from athena.lib.View', ->
+        expect(derives MediaView, athena.lib.View).toBe true

-    acorn.util.assert Shell, 'goog.require \'acorn.shells.Shell\' before ' +
-      'calling acorn.util.test.describeShellModule'
+      it 'should derive from (or be) Shell.MediaView', ->
+        expect(isOrDerives MediaView, Shell.MediaView).toBe true

-    describeView = athena.lib.util.test.describeView
-
-    isOrDerives = (Subclass, Superclass) =>
-      Subclass == Superclass or derives Subclass, Superclass
-
-    describe "#{Module.id} shell module", ->
-
-      it 'should contain required properties', ->
-        requiredProperties = [ 'id', 'title', 'description', 'icon' ]
-        _.each requiredProperties, (property) ->
-          expect(Module[property]).toBeDefined()
-          expect(_.isString Module[property]).toBe true
-
-      it 'should contain required Model and View classes', ->
-        requiredClasses = [ 'Model', 'MediaView', 'RemixView' ]
-        _.each requiredClasses, (property) ->
-          expect(Module[property]).toBeDefined()
-          expect(_.isFunction Module[property]).toBe true
-
-      viewOptions = -> model: new Module.Model modelOptions
-
-      describe "#{Module.id}.Model", ->
-        Model = Module.Model
-
-        it 'should derive from athena.lib.Model', ->
-          expect(derives Model, athena.lib.Model).toBe true
-
-        it 'should derive from (or be) Shell.Model', ->
-          expect(isOrDerives Model, Shell.Model).toBe true
-
-        it 'should correctly assign attributes', ->
-          m = new Model _.extend modelOptions, {a: 1, b:2}
-          expect(m.get 'a').toBe 1
-          expect(m.get 'b').toBe 2
-
-        it 'should correctly support setting of attributes', ->
-          m = new Model _.extend modelOptions, {a: 1, b:2}
-          m.set 'a', 2
-          m.set 'b', 3
-          expect(m.get 'a').toBe 2
-          expect(m.get 'b').toBe 3
-
-        it 'should throw an exception on save/sync', ->
-          m = new Model _.extend modelOptions, {a: 1, b:2}
-          expect(m.save).toThrow()
-          expect(m.sync).toThrow()
-
-        it 'should be clonable (with deep-copies)', ->
-          deep = {'a': 5}
-          m = new Model _.extend modelOptions, {shellid:'Shell', a: b: c: deep}
-          expect(m.clone().attributes.a.b.c).toEqual deep
-          expect(m.clone().attributes.a.b.c).not.toBe deep
-
-          # ensure the same thing breaks on Backbone's non-deep copy
-          b = new Backbone.Model {shellid:'Shell', a: b: c: deep}
-          expect(b.clone().attributes.a.b.c).toEqual deep
-          expect(b.clone().attributes.a.b.c).toBe deep
-
-        it 'should have a toJSONString function', ->
-          expect(typeof Model::toJSONString).toBe 'function'
-          m = new Model _.extend modelOptions,
-              {shellid:'Shell', a: b: c: {'a': 5}}
-          s = m.toJSONString()
-          expect(JSON.stringify m.attributes).toEqual s
+      it 'should require `model` parameter', ->
+        throwOptions = viewOptions()
+        delete throwOptions.model
+        expect(-> new MediaView viewOptions()).not.toThrow()
+        expect(-> new MediaView throwOptions).toThrow()

+    describeView Module.RemixView, athena.lib.View, viewOptions(), ->

+      RemixView = Module.RemixView

-      describeView Module.MediaView, athena.lib.View, viewOptions(), ->
+      it 'should derive from athena.lib.View', ->
+        expect(derives RemixView, athena.lib.View).toBe true

-        MediaView = Module.MediaView
+      it 'should derive from (or be) Shell.RemixView', ->
+        expect(isOrDerives RemixView, Shell.RemixView).toBe true

-        it 'should derive from athena.lib.View', ->
-          expect(derives MediaView, athena.lib.View).toBe true
+      it 'should require `model` parameter', ->
+        throwOptions = viewOptions()
+        delete throwOptions.model
+        expect(-> new RemixView viewOptions()).not.toThrow()
+        expect(-> new RemixView throwOptions).toThrow()

-        it 'should derive from (or be) Shell.MediaView', ->
-          expect(isOrDerives MediaView, Shell.MediaView).toBe true
-
-        it 'should require `model` parameter', ->
-          throwOptions = viewOptions()
-          delete throwOptions.model
-          expect(-> new MediaView viewOptions()).not.toThrow()
-          expect(-> new MediaView throwOptions).toThrow()
-
-      describeView Module.RemixView, athena.lib.View, viewOptions(), ->
-
-        RemixView = Module.RemixView
-
-        it 'should derive from athena.lib.View', ->
-          expect(derives RemixView, athena.lib.View).toBe true
-
-        it 'should derive from (or be) Shell.RemixView', ->
-          expect(isOrDerives RemixView, Shell.RemixView).toBe true
-
-        it 'should require `model` parameter', ->
-          throwOptions = viewOptions()
-          delete throwOptions.model
-          expect(-> new RemixView viewOptions()).not.toThrow()
-          expect(-> new RemixView throwOptions).toThrow()
-
-      tests?()
-
+    tests?()


allow setting onClick for links

allow setting onClick for links

--- shared/src/factories/link.cjsx
+++ shared/src/factories/link.cjsx
@@ -8,7 +8,7 @@
 PASSABLE_PROPS = [
   'className', 'id', 'children', 'target', 'activeOnlyWhenExact',
   'activeStyle', 'activeClassName', 'isActive', 'location', 'ref',
-  'tabIndex', 'alt', 'title'
+  'tabIndex', 'alt', 'title', 'onClick'
 ]

 makeLink = (router, name = 'OpenStax') ->


modify existing instead of replacing

modify existing instead of replacing

--- src/chinchilla/action_operation.coffee
+++ src/chinchilla/action_operation.coffee
@@ -56,9 +56,9 @@
         @$data = response.data

         if response.data.members
-          @$arr = response.data.members
+          _.each response.data.members, (member) => @$arr.push(member)
         else
-          @$obj = response.data
+          _.merge @$obj, response.data

         @$deferred.resolve(@)
       error = =>


Fix to restore selected Project.

Fix to restore selected Project.

--- src/coffee/controllers/mainCtrl.coffee
+++ src/coffee/controllers/mainCtrl.coffee
@@ -153,6 +153,7 @@
     obj = {
       url: selected.url
       id: selected.id
+      type: selected.type
     }
     Platform.save(Platform.SELECTED_PROJECT, obj)


fix(inventory): fix update error in code

--- src/coffee/sync/utils/inventory.coffee
+++ src/coffee/sync/utils/inventory.coffee
@@ -52,20 +52,15 @@
   actionsMapRestockableInDays: (diff) ->
     actions = []
     if diff.restockableInDays
-      if _.isArray(diff.restockableInDays)
-        size = _.size(diff.restockableInDays)
-        a =
-          action: 'setRestockableInDays'
-        if size is 1
-          a.restockableInDays = diff.restockableInDays[0]
-        else if size is 2
-          oldVal = diff.restockableInDays[0]
-          newVal = diff.restockableInDays[1]
-          diffVal = newVal - oldVal
-          if diffVal
-            a.restockableInDays = newVal
-        # Delete case (size is 3) - we do not set any restockableInDays
-        actions.push a
+      if _.isArray(diff.restockableInDays) and _.size(diff.restockableInDays) is 2
+        oldVal = diff.restockableInDays[0]
+        newVal = diff.restockableInDays[1]
+        diffVal = newVal - oldVal
+        if diffVal
+          a =
+            action: 'setRestockableInDays'
+            restockableInDays: newVal
+          actions.push a
     actions

   # Private: map inventory custom type and fields


Extended fullscreen timeout

Extended fullscreen timeout

--- src/controllers/main-controller.coffee
+++ src/controllers/main-controller.coffee
@@ -279,7 +279,7 @@

     @scope.$watch 'fullscreen', =>
       return clearTimeout @fullScreenTimeout unless @scope.fullscreen?
-      @fullScreenTimeout = @timeout @clearFullscreen, 10000
+      @fullScreenTimeout = @timeout @clearFullscreen, 30000

 angular.module 'gateblu-ui'
   .controller 'MainController', ($rootScope, $scope, $timeout, $interval, GatebluServiceManager, LogService, DeviceLogService, UpdateService, GatebluBackendInstallerService, GatebluService, DeviceManagerService, $mdDialog) ->


Added constant for max number of pages

Added constant for max number of pages

--- src/code/utils/open-clipart.coffee
+++ src/code/utils/open-clipart.coffee
@@ -1,4 +1,4 @@
-initialResultSize = 40
+MAX_NUMBER_OF_PAGES = 20

 module.exports = OpenClipArt =

@@ -11,8 +11,8 @@
     url = "https://openclipart.org/search/json/?query=#{encodeURIComponent query}&sort=downloads&page=#{options.page}&amount=24"
     OpenClipArt.jqXHR = $.getJSON url, (data) ->
       results = []
-      page = Math.min(data?.info?.current_page or 0, 20)
-      numPages = Math.min(data?.info?.pages or 0, 20)
+      page = Math.min(data?.info?.current_page or 0, MAX_NUMBER_OF_PAGES)
+      numPages = Math.min(data?.info?.pages or 0, MAX_NUMBER_OF_PAGES)
       for item in data?.payload
         results.push
           image: item.svg.png_thumb


renamed attempts

renamed attempts

--- src/coffee/helpers/repeater.coffee
+++ src/coffee/helpers/repeater.coffee
@@ -25,8 +25,8 @@
    *   recoverableError - Error => Boolean - function that decides, whether an error can be recovered by repeating the task execution
   ###
   execute: (options) ->
-    throw new Error '`task` function is undefined' unless _.isFunction(task)
-    throw new Error '`recoverableError` function is undefined' unless _.isFunction(recoverableError)
+    throw new Error '`task` function is undefined' unless _.isFunction(options.task)
+    throw new Error '`recoverableError` function is undefined' unless _.isFunction(options.recoverableError)

     d = Q.defer()


updated AppController to be compatible with recent changes (navItem properties) and fixed some indentation

--- client/app/Applications/Inbox.kdapplication/AppController.coffee
+++ client/app/Applications/Inbox.kdapplication/AppController.coffee
@@ -4,6 +4,9 @@
     name         : "Inbox"
     route        : "/:name?/Inbox"
     hiddenHandle : yes
+    navItem      :
+      title      : "Inbox"
+      path       : "/Inbox"

   {race} = Bongo

@@ -134,27 +137,27 @@
       @prepareMessage formOutput, callback, newMessageBar

     newMessageBar.on 'MessageShouldBeDisowned', do =>
-        if not @selection
+      if not @selection
+        newMessageBar.disableMessageActionButtons()
+        modal.destroy()
+        return
+      disownAll = (items, callback)->
+        disownItem = race (i, item, fin)->
+          item.data.disown (err)->
+            if err
+              fin err
+            else
+              fin()
+        , callback
+        disownItem item for own id, item of items
+      (modal) =>
+        disownAll @selection, =>
+          for own id, {item, paneView} of @selection
+            item.destroy()
+            paneView.destroy()
+            @deselectMessages()
+          modal.destroy()
           newMessageBar.disableMessageActionButtons()
-          modal.destroy()
-          return
-        disownAll = (items, callback)->
-          disownItem = race (i, item, fin)->
-            item.data.disown (err)->
-              if err
-                fin err
-              else
-                fin()
-          , callback
-          disownItem item for own id, item of items
-        (modal) =>
-          disownAll @selection, =>
-            for own id, {item, paneView} of @selection
-              item.destroy()
-              paneView.destroy()
-              @deselectMessages()
-            modal.destroy()
-            newMessageBar.disableMessageActionButtons()

     newMessageBar.on 'MessageShouldBeMarkedAsUnread', =>
       for own id, {item, data} of @selection


markdowneditor view has scrollbar

markdowneditor view has scrollbar

--- client/stacks/lib/views/stacks/editors/markdowneditorview.coffee
+++ client/stacks/lib/views/stacks/editors/markdowneditorview.coffee
@@ -26,13 +26,23 @@
   handlePreview: ->

     { title } = @getOptions()
-    content   = @getContent()
+    content = @getContent()
+
+    scrollView = new kd.CustomScrollView { cssClass : 'readme-scroll' }
+
+    markdown = applyMarkdown content, { breaks: false }
+
+    scrollView.wrapper.addSubView markdown_content = new kd.CustomHTMLView
+      cssClass : 'markdown-content'
+      partial : markdown
+

     new ContentModal
       width : 600
       overlay : yes
+      cssClass : 'readme-preview has-markdown content-modal'
       attributes     : { testpath: 'ReadmePreviewModal' }
       overlayOptions : { cssClass : 'second-overlay' }
       title          : title or 'Readme Preview'
-      cssClass       : 'readme-preview has-markdown content-modal'
-      content        : "
#{applyMarkdown content}
"
+      content        : scrollView
+


hide url

hide url

--- app/assets/javascripts/effective_datatables/download.js.coffee
+++ app/assets/javascripts/effective_datatables/download.js.coffee
@@ -5,4 +5,6 @@
   url = $table.data('source').replace('.json', '/download.csv')
   attributes = 'attributes=' + encodeURIComponent($table.data('attributes'))

-  $(event.currentTarget).attr('href', url + '?' + attributes)
+  $button.attr('href', url + '?' + attributes)
+
+  setTimeout (=> $button.attr('href', 'download.csv')), 0


All query functions in coffeescript file now using micro data

--- app/assets/javascripts/hot_date_rails/hot_date_rails.js.coffee
+++ app/assets/javascripts/hot_date_rails/hot_date_rails.js.coffee
@@ -1,22 +1,24 @@
 $(document).on 'ready page:load', ->
   $("input.datepicker").each (i) ->
-    $(this).datepicker
-      showTimepicker: false
-      altFormat: "yy-mm-dd"
-      dateFormat: this.getAttribute("data-date-format") #"mm/dd/yy"
-      altField: $(this).next()
-      changeYear: true
-      changeMonth: true
+    defOpts = {
+    #showTimepicker: false
+    altFormat: "yy-mm-dd"
+    #dateFormat: this.getAttribute("data-date-format") #"mm/dd/yy"
+    altField: $(this).next()
+    changeYear: true
+    changeMonth: true}
+    setDatePickerOptions( this, defOpts )

 $(document).on 'ready page:load', ->
   $("input.datetimepicker").each (i) ->
-    $(this).datetimepicker
-      altField: $(this).next()
-      altFieldTimeOnly: false
-      altFormat: "yy-mm-dd"
-      altTimeFormat: "HH:mm"
-      dateFormat: "mm/dd/yy"
-      timeFormat: "HH:mm"
+    #$(this).datetimepicker
+    defOpts = {altField: $(this).next()
+    altFieldTimeOnly: false
+    altFormat: "yy-mm-dd"
+    altTimeFormat: "HH:mm"
+    dateFormat: "mm/dd/yy"
+    timeFormat: "HH:mm"}
+    setPickerOptions( this, defOpts )

 $(document).on 'ready page:load', ->
   $("input.timepicker").each (i) ->
@@ -32,6 +34,11 @@
   userOpts = $(input).data()
   allOpts = $.extend({}, defOpts, userOpts)
   $(input).datetimepicker(allOpts)
+
+setDatePickerOptions = (input, defOpts) ->
+  userOpts = $(input).data()
+  allOpts = $.extend({}, defOpts, userOpts)
+  $(input).datepicker(allOpts)


Lazy-loading entries with relative URLs now also works when the "open all entries by default" config option is enabled.

--- app/assets/javascripts/ng-services/ng-LazyLoadingSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-LazyLoadingSvc.js.coffee
@@ -51,7 +51,9 @@
     #---------------------------------------------
     load_viewport_images: ->
       $('.entry .entry-content img[data-src]').withinViewportBottom({bottom: -600}).each ->
-        load_image $(this)
+        entry_id = $(this).closest('.entry').children('.open-entry-link').attr 'data-entry-id'
+        entry = findSvc.find_entry entry_id
+        load_image $(this), entry

   return service
 ]


Fix syncing of min and max values from regex

--- app/assets/javascripts/rglossa/components/cwb-multiword.coffee
+++ app/assets/javascripts/rglossa/components/cwb-multiword.coffee
@@ -57,6 +57,8 @@
         dq.push App.CwbMultiwordTerm.create
           word:     word
           features: []
+          min:      min
+          max:      max
           isStart:  isStart
           isEnd:    isEnd

@@ -128,7 +130,7 @@

   _splitQueryTerms: ->
     query = @get('query')
-    query.match(/"[^"\s]+"|\[[^\]]+\]/g) or ['']
+    query.match(/\[\]\{(.+)\}|"[^"\s]+"|\[[^\]]+\]/g) or ['']

   _handleIntervalSpecification: (m) ->


beta message appears on rank and score questions

--- jsapp/xlform_model_view/view.row.coffee
+++ jsapp/xlform_model_view/view.row.coffee
@@ -262,6 +262,7 @@

       extra_score_contents = $viewTemplates.$$render('row.scoreView', template_args)
       @$('.card--selectquestion__expansion').eq(0).append(extra_score_contents)
+      @$('.card').eq(0).append(beta_elem)
       $rows = @$('.score__contents--rows').eq(0)
       $choices = @$('.score__contents--choices').eq(0)

@@ -323,7 +324,6 @@
                     class: 'scorerank-beta-warning'
                     text: 'Note: Rank and Score question types are currently in beta.'
                     })
-      @$el.append(beta_elem)
       template_args = {}
       template_args.rank_constraint_msg = @model.get('kobo--rank-constraint-message')?.get('value')

@@ -348,6 +348,7 @@
         cid: model.cid
       template_args.rank_rows = rank_rows
       extra_score_contents = $viewTemplates.$$render('row.rankView', @, template_args)
+      @$('.card').append(beta_elem)
       @$('.card--selectquestion__expansion').eq(0).append(extra_score_contents)
       @editRanks()
     editRanks: ->


Generalize functions

Generalize functions

--- components/SCDDetect.coffee
+++ components/SCDDetect.coffee
@@ -7,10 +7,7 @@
 # @runtime noflo-nodejs
 # @name SCDDetect

-compute = (canvas, cascade, callback) ->
-  ctx = canvas.getContext '2d'
-  imageData = ctx.getImageData 0, 0, canvas.width, canvas.height
-  data = imageData.data
+writeCanvasTempFile = (canvas, callback) ->
   tmpFile = new temporary.File

   rs = canvas.pngStream()
@@ -31,13 +28,13 @@
     ws.once 'close', ->
       fs.fsync fd, ->
         try
-          onEnd tmpFile, cascade, callback
+          callback null, tmpFile
         catch error
           callback error
           tmpFile.unlink()
   rs.pipe ws

-onEnd = (tmpFile, cascade, callback) ->
+runScdDetect = (tmpFile, cascade, callback) ->
   bin = path.join __dirname, '../build/Release/scddetect'
   exec "#{bin} #{tmpFile.path} #{cascade}", (err, stdout, stderr) ->
     if err
@@ -76,13 +73,16 @@
     out: 'out'
     forwardGroups: true
     async: true
-  , (payload, groups, out, callback) ->
+  , (canvas, groups, out, callback) ->
     if not c.params.cascade
       cascade = path.join __dirname, '../cascades/face.sqlite3'
     else
       cascade = c.params.cascade
-    compute payload, cascade, (err, val) ->
+    writeCanvasTempFile canvas, (err, tmpFile) ->
       return callback err if err
-      out.send val
-      do callback
+      runScdDetect tmpFile, cascade, (err, val) ->
+        return callback err if err
+        out.send val
+        do callback
+
   c