Move some of the files in the dist copy arrays to common locations

We were previously doing a LOT of duplicate copying for the builds on
different platforms.  With this change, a lot of those files will only
be specified once

This also fixes an error with a missing file in cordova as well as an
extra file that should have been ignored.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -138,6 +138,60 @@
   'uproxy-lib/loggingprovider'
   'uproxy-lib/churn-pipe'
   'uproxy-lib/cloud/social'
+]
+
+getWithBasePath = (files, base = '') ->
+  for file in files
+    if file[0] == '!'
+      '!' + path.join(base, file[1..])
+    else
+      path.join(base, file)
+
+uiDistFiles = [
+  'generic_ui/*.html'
+  'generic_ui/style/*.css'
+  'generic_ui/polymer/vulcanized*.{html,js}'
+  'generic_ui/fonts/*'
+  'generic_ui/icons/*'
+]
+
+coreDistFiles = [
+  'fonts/*'
+  '*.html' # technically does not exist in Firefox
+
+  'freedomjs-anonymized-metrics/anonmetrics.json'
+  'freedomjs-anonymized-metrics/metric.js'
+  'freedom-social-github/social.github.json'
+  'freedom-social-github/github-social-provider.js'
+  'freedom-social-firebase/social.firebase-facebook.json'
+  'freedom-social-firebase/social.firebase-google.json'
+  'freedom-social-firebase/firebase-shims.js'
+  'freedom-social-firebase/firebase.js'
+  'freedom-social-firebase/firebase-social-provider.js'
+  'freedom-social-firebase/facebook-social-provider.js'
+  'freedom-social-firebase/google-social-provider.js'
+  'freedom-social-firebase/google-auth.js'
+  'freedom-port-control/port-control.js'
+  'freedom-port-control/port-control.json'
+  'freedom-pgp-e2e/end-to-end.compiled.js'
+  'freedom-pgp-e2e/googstorage.js'
+  'freedom-pgp-e2e/e2e.js'
+  'freedom-pgp-e2e/pgpapi.json'
+
+  '**/freedom-module.json'
+  '**/*.static.js'
+]
+
+# this should always be added to the array last
+universalDistFiles = [
+  'icons/*'
+  'bower/webcomponentsjs/webcomponents.min.js'
+  'bower/polymer/polymer.js'
+
+  '!generic_core/freedom-module.json' # not actually needed for the UI builds
+  '!generic_ui/polymer/vulcanized*inline.html'
+  '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
+  '!**/*spec*'
 ]

 gruntConfig = {
@@ -308,29 +362,14 @@
           cwd: chromeExtDevPath
           src: [
             'manifest.json'
-
-            'bower/webcomponentsjs/webcomponents.min.js'
-            'bower/polymer/polymer.js'
-
-            'generic_ui/*.html'
-            'generic_ui/polymer/vulcanized*.{html,js}'
-            '!generic_ui/polymer/vulcanized*inline.html'
-            '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
+            '_locales/**'

             'generic_ui/scripts/copypaste.js'
             'generic_ui/scripts/get_logs.js'
             'scripts/context.static.js'
             'scripts/background.static.js'
-            '!**/*spec*'
-
-            'generic_ui/style/*.css'
-
-            # extra components we use
-            'generic_ui/fonts/*'
-            'generic_ui/icons/*'
-            'icons/*'
-            '_locales/**'
-          ]
+          ].concat uiDistFiles
+            .concat universalDistFiles
           dest: 'build/dist/chrome/extension'
         }
         { # Chrome app
@@ -338,45 +377,15 @@
           cwd: chromeAppDevPath
           src: [
             'manifest.json'
-            '*.html'
-
-            'bower/webcomponentsjs/webcomponents.min.js'
-            'bower/polymer/polymer.js'
+            '_locales/**'

             # UI for not-connected
             # This is not browserified so we use .js instead of .static.js
             'polymer/vulcanized.{html,js}'

-            # actual scripts that run things
-            'freedomjs-anonymized-metrics/anonmetrics.json'
-            'freedomjs-anonymized-metrics/metric.js'
             'freedom-for-chrome/freedom-for-chrome.js'
-            'freedom-social-github/social.github.json'
-            'freedom-social-github/github-social-provider.js'
-            'freedom-social-firebase/social.firebase-facebook.json'
-            'freedom-social-firebase/social.firebase-google.json'
-            'freedom-social-firebase/firebase-shims.js'
-            'freedom-social-firebase/firebase.js'
-            'freedom-social-firebase/firebase-social-provider.js'
-            'freedom-social-firebase/facebook-social-provider.js'
-            'freedom-social-firebase/google-social-provider.js'
-            'freedom-social-firebase/google-auth.js'
-            'freedom-port-control/port-control.js'
-            'freedom-port-control/port-control.json'
-            'freedom-pgp-e2e/end-to-end.compiled.js'
-            'freedom-pgp-e2e/googstorage.js'
-            'freedom-pgp-e2e/e2e.js'
-            'freedom-pgp-e2e/pgpapi.json'
-
-            '**/freedom-module.json'
-            '!generic_core/freedom-module.json'
-            '**/*.static.js'
-            '!**/*spec*'
-
-            'icons/*'
-            'fonts/*'
-            '_locales/**'
-          ]
+          ].concat coreDistFiles
+            .concat universalDistFiles
           dest: 'build/dist/chrome/app'
         }
         { # Chrome app freedom-module
@@ -394,48 +403,13 @@
             # addon sdk scripts
             'lib/**/*.js'

-            'data/freedomjs-anonymized-metrics/anonmetrics.json'
-            'data/freedomjs-anonymized-metrics/metric.js'
-            'data/freedom-for-firefox/freedom-for-firefox.jsm'
-            'data/freedom-social-github/social.github.json'
-            'data/freedom-social-github/github-social-provider.js'
-            'data/freedom-social-firebase/social.firebase-facebook.json'
-            'data/freedom-social-firebase/social.firebase-google.json'
-            'data/freedom-social-firebase/firebase-shims.js'
-            'data/freedom-social-firebase/firebase.js'
-            'data/freedom-social-firebase/firebase-social-provider.js'
-            'data/freedom-social-firebase/facebook-social-provider.js'
-            'data/freedom-social-firebase/google-social-provider.js'
-            'data/freedom-social-firebase/google-auth.js'
-            'data/freedom-port-control/port-control.js'
-            'data/freedom-port-control/port-control.json'
-            'data/freedom-pgp-e2e/end-to-end.compiled.js'
-            'data/freedom-pgp-e2e/googstorage.js'
-            'data/freedom-pgp-e2e/e2e.js'
-            'data/freedom-pgp-e2e/pgpapi.json'
-
-            'data/**/freedom-module.json'
-            '!generic_core/freedom-module.json'
-            'data/**/*.static.js'
             'data/generic_ui/scripts/get_logs.js'
             'data/scripts/content-proxy.js'
-            '!**/*spec*'
-
-            'data/bower/webcomponentsjs/webcomponents.min.js'
-            'data/bower/polymer/polymer.js'
-
-            'data/generic_ui/*.html'
-            'data/generic_ui/polymer/vulcanized*.{html,js}'
-            '!data/generic_ui/polymer/vulcanized*inline.html'
-            '!data/generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
-
-            'data/generic_ui/style/*.css'
-
-            'data/fonts/*'
-            'data/icons/*'
-            'data/generic_ui/fonts/*'
-            'data/generic_ui/icons/*'
-          ]
+
+            'data/freedom-for-firefox/freedom-for-firefox.jsm'
+          ].concat getWithBasePath(uiDistFiles, 'data')
+            .concat getWithBasePath(coreDistFiles, 'data')
+            .concat getWithBasePath(universalDistFiles, 'data')
           dest: 'build/dist/firefox'
         }
         { # Firefox freedom-module
@@ -450,60 +424,21 @@
           src: [
             'manifest.json'
             'config.xml'
-            '*.html'
-
-            'bower/webcomponentsjs/webcomponents.min.js'
-            'bower/polymer/polymer.js'
-
-            'generic_ui/*.html'
-            'generic_ui/polymer/vulcanized*.{html,js}'
-            '!generic_ui/polymer/vulcanized*inline.html'
-            '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
+
+            # This is not browserified so we use .js instead of .static.js
+            'polymer/vulcanized.{html,js}'

             'generic_ui/scripts/copypaste.js'
             'generic_ui/scripts/get_logs.js'
-            '!**/*spec*'
-
-            'generic_ui/style/*.css'
-
-            # extra components we use
-            'generic_ui/fonts/*'
-            'generic_ui/icons/*'
-
-            # This is not browserified so we use .js instead of .static.js
-            'polymer/vulcanized.{html,js}'
-
-            # actual scripts that run things
-            # Only the Gmail and Quiver social providers seem to work on Android, so the
-            # others have been removed from this list.
-            'freedomjs-anonymized-metrics/anonmetrics.json'
-            'freedomjs-anonymized-metrics/metric.js'
-            'freedom-for-chrome/freedom-for-chrome.js'
-            'freedom-social-github/social.github.json'
-            'freedom-social-github/github-social-provider.js'
-            'freedom-social-firebase/social.firebase-facebook.json'
-            'freedom-social-firebase/social.firebase-google.json'
-            'freedom-social-firebase/firebase-shims.js'
-            'freedom-social-firebase/firebase.js'
-            'freedom-social-firebase/firebase-social-provider.js'
-            'freedom-social-firebase/google-social-provider.js'
-            'freedom-social-firebase/google-auth.js'
+
+            # TODO move to universalDistFiles once supported on other platforms
             'freedom-social-quiver/socketio.quiver.json'
             'freedom-social-quiver/socketio.quiver.js'
-            'freedom-port-control/port-control.js'
-            'freedom-port-control/port-control.json'
-            'freedom-pgp-e2e/end-to-end.compiled.js'
-            'freedom-pgp-e2e/googstorage.js'
-            'freedom-pgp-e2e/e2e.js'
-            'freedom-pgp-e2e/pgpapi.json'
-
-            '**/freedom-module.json'
-            '!generic_core/freedom-module.json'
-            '**/*.static.js'
-
-            'icons/*'
-            'fonts/*'
-          ]
+
+            'freedom-for-chrome/freedom-for-chrome.js'
+          ].concat uiDistFiles
+            .concat coreDistFiles
+            .concat universalDistFiles
           dest: ccaDistPath
         }
         { # CCA dist freedom-module.json


Switch to grunt-mocha-test

Switch to grunt-mocha-test

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -28,11 +28,13 @@
           'browser/noflo-facebook.js': ['component.json']

     # BDD tests on Node.js
-    cafemocha:
+    mochaTest:
       nodejs:
         src: ['spec/*.coffee']
         options:
           reporter: 'spec'
+          require: 'coffee-script/register'
+          grep: process.env.TESTS

     # BDD tests on browser
     mocha_phantomjs:
@@ -57,7 +59,7 @@
   @loadNpmTasks 'grunt-contrib-coffee'

   # Grunt plugins used for testing
-  @loadNpmTasks 'grunt-cafe-mocha'
+  @loadNpmTasks 'grunt-mocha-test'
   @loadNpmTasks 'grunt-mocha-phantomjs'
   @loadNpmTasks 'grunt-coffeelint'

@@ -73,7 +75,7 @@
     @task.run 'coffee'
     @task.run 'noflo_manifest'
     if target is 'all' or target is 'nodejs'
-      @task.run 'cafemocha'
+      @task.run 'mochaTest'
     if target is 'all' or target is 'browser'
       @task.run 'noflo_browser'
       @task.run 'mocha_phantomjs'


Remove livereload functionality for now.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -33,7 +33,6 @@
       sass:
         files: './avalonstar/assets/stylesheets/*.scss'
         tasks: ['sass', 'autoprefixer']
-      options: {livereload: true}

   # Imports.
   grunt.loadNpmTasks 'grunt-autoprefixer'


Grunt: Consume less memory while watching files.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -72,7 +72,7 @@
         files: ['client/vendor/css/**/*.css']
         tasks: 'cssmin'
       json:
-        files: ['**/*.json']
+        files: ['**/*.json','!**/node_modules/**']
         tasks: 'compile:json'
     nodemon:
       development:


Mocha now tests from Coffee by default

Mocha now tests from Coffee by default

In order to improve test failures, normal debug testing tests on the
project source. The `test:build` flag will compile and test the compiled
source.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -36,17 +36,20 @@
         expand  : true
         cwd     : './'
         dest    : 'build'
-        src     : [
-          'bin/**/*.js'
-          'test/stubs/**/*.*'
-        ]
+        src     : ['bin/**/*.js']
+      stubs:
+        expand  : true
+        cwd     : './'
+        dest    : 'build'
+        src     : ['test/stubs/**/*.*']

     mochacli:
       options:
-        require : ['should']
+        require : ['should', 'coffee-script']
         reporter: 'nyan'
         bail    : true
-      all: ['build/test/index.js']
+      build   : ['build/test/index.js']
+      source  : ['test/index.coffee']

     replace:
       main:
@@ -63,9 +66,12 @@
   grunt.loadNpmTasks 'grunt-mocha-cli'
   grunt.loadNpmTasks 'grunt-replace'

-  grunt.registerTask 'build', ['clean', 'coffee', 'copy', 'replace']
-  grunt.registerTask 'test:nobuild', ['mochacli']
-  grunt.registerTask 'test', ['build', 'test:nobuild']
-  grunt.registerTask 'prepublish', ['test']
+
+  grunt.registerTask 'build', ['clean', 'coffee', 'copy:bin', 'replace']
+
+  grunt.registerTask 'test', ['copy:stubs', 'mochacli:source']
+  grunt.registerTask 'test:build', ['build', 'mochacli:build']
+
+  grunt.registerTask 'prepublish', ['test:build']
   grunt.registerTask 'default', ['prepublish']


Include should in the browserification of specs

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -176,6 +176,7 @@
           'dist/finitio.tests.js': ['build/specs/**/*.js']
         options:
           extensions: ['.js']
+          alias: ['node_modules/should/index.js:should']

     # Minify distributed library
     uglify:


Use compressed styling for unminified css

Gets rid of build comments, substantially lowering file size

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -149,7 +149,7 @@
       dist:
         options:
           generatedImagesDir: "<%= yeoman.dist %>/images/generated"
-
+          outputStyle: "compressed"
       server:
         options:
           debugInfo: true


Make patch releases by default.

Make patch releases by default.

Signed-off-by: Carlos Escribano <ab5e2bca84933118bbc9d48ffaccce3bac4eeb64@doofinder.com>

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -137,15 +137,15 @@
   # Be careful, these tasks change version numbers on each execution!!!

   grunt.registerTask "release", [
-    "version:project:minor",
+    "version:project:patch",
     "version:library",
     "version:bower",
     "compile",
     "test"
   ]

-  grunt.registerTask "release:patch", [
-    "version:project:patch",
+  grunt.registerTask "release:minor", [
+    "version:project:minor",
     "version:library",
     "version:bower",
     "compile",


grunt travisCI is ready and waiting

grunt travisCI is ready and waiting

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -78,7 +78,7 @@
       gruntfile: ['Gruntfile.js']
       server: ['app.js']
       client: ['client/**/*.js', '!**/models/**', '!**/lib/**']
-      tests: ['tests/**/*.js']
+      tests: ['tests/**/*.js', '!tests/oculus-testing-playground/**/*.js']

     stylus:
       compile:
@@ -149,9 +149,13 @@
   # REGISTER
   # =================

+  grunt.registerTask 'travisCI', ['jshint:server',
+                                  'jshint:client',
+                                  'jshint:tests',
+                                  'karma:travis',
+                                  'mocha-chai-sinon']
+  grunt.registerTask 'test', ['karma:unit:start', 'watch:test']
   grunt.registerTask 'server', ['jshint:server']
   grunt.registerTask 'client', ['jshint:client', 'copy:client', 'concat', 'stylus']
   grunt.registerTask 'client-prod', ['client', 'uglify']
-  grunt.registerTask 'test', ['karma:unit:start', 'watch:test']
-  grunt.registerTask 'travisCI', ['jshint:server', 'jshint:client', 'jshint:tests', 'karma:travis']
   grunt.registerTask 'default', ['server', 'client', 'karma:unit:start', 'concurrent']


[dev] New rule for lint

[dev] New rule for lint

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -32,6 +32,8 @@
             level: 'error'
           space_operators:
             level: 'error'
+          spacing_after_comma:
+            level: 'error'
     mochaTest:
       app:
         src: [


feat(grunt): remove deprecated `leading_quotes` support

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -85,7 +85,6 @@
         files: ["<%= config.app %>/**/*", "!_*", "_config*.yml"]
         tasks: [
           "jekyll:serve"
-          "newer:leading_quotes"
         ]

     uglify:
@@ -262,21 +261,6 @@
         htmlroot: "<%= config.dist %>"

       dist:
-        files: [
-          expand: true
-          cwd: "<%= config.dist %>"
-          src: "**/*.html"
-          dest: "<%= config.dist %>"
-        ]
-
-    leading_quotes:
-      options:
-        elements: "p, h1, h2, h3, h4, h5, h6"
-        regex: /「|『|“|‘|（/
-        class: "leading-indent-fix"
-        verbose: true
-
-      main:
         files: [
           expand: true
           cwd: "<%= config.dist %>"
@@ -686,7 +670,6 @@
     "less:serve"
     "postcss:serve"
     "jekyll:serve"
-    "leading_quotes"
     "browserSync"
     "watch"
   ]
@@ -711,7 +694,6 @@
     "postcss:dist"
     "csscomb"
     "jekyll:dist"
-    "leading_quotes"
     "cssmin"
     "assets_inline"
     "uncss_inline"


Fix clean path

Fix clean path

Use the devBuildPath and distBuildPath variables in clean instead of
manually specifying the paths.  This fixes the issue wher clean misses
build/src

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -222,7 +222,7 @@
       freedomwechat: readJSONFile('node_modules/freedom-social-wechat/package.json')
       freedomquiver: readJSONFile('node_modules/freedom-social-quiver/package.json')

-    clean: ['build/dev', 'build/dist', '.tscache']
+    clean: [devBuildPath, distBuildPath, '.tscache']

     #-------------------------------------------------------------------------
     # Import global names into config name space


room name

room name

this allow to set the room name, which hubot reflects in the message objects (currently it shows up as `undefined`)

--- src/index.coffee
+++ src/index.coffee
@@ -30,7 +30,7 @@
   receive: (userName, message) ->
     new Promise (resolve) =>
       @messages.push [userName, message]
-      user = new Hubot.User(userName)
+      user = new Hubot.User(userName, { room: @name })
       @robot.receive(new Hubot.TextMessage(user, message), resolve)

   destroy: ->
@@ -67,6 +67,7 @@

     robot.brain.emit 'loaded'

+    robot.adapter.name = options.name or 'room1'
     robot.adapter

 module.exports = Helper


Change test task options

Change test task options

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,9 +1,10 @@
 module.exports = (grunt)->
   grunt.loadTasks "tasks"

+  # run test
   grunt.initConfig
     redirect:
       dist:
         files:
-          "test.txt": "gcc --version"
+          "tmp/test.txt": "gcc --version"


LiveReload should be the first middleware function

As stated by the connect-livereload repo:
For use as middleware (...) simply add the following to the **top** of your array of middleware.

This fixes the following use-case:

I already have a middleware function defined for fixing the HTML5 push-state feature in the Angular router:
```
middleware: function (connect, options) {
      var optBase = (typeof options.base === 'string') ? [options.base] : options.base;
      return [modRewrite(['!(\\..+)$ /example/ [L]'])].concat(
        optBase.map(function(path){ return connect.static(path); }));
    }
```

Middleware.push adds the livereload middleware to the bottom of the array causing it not to load for some reason.
Unshifting the array and adding the livereload middleware to the top of the array however fixes this.

--- src/index.coffee
+++ src/index.coffee
@@ -82,7 +82,7 @@
     if opt.livereload
       opt.livereload = {}  if typeof opt.livereload is "boolean"
       opt.livereload.port = 35729  unless opt.livereload.port
-      middleware.push liveReload(port: opt.livereload.port)
+      middleware.unshift liveReload(port: opt.livereload.port)
     if typeof opt.root == "object"
       opt.root.forEach (path) ->
         middleware.push connect.static(path)


decodeURI instead of unescape

decodeURI instead of unescape

--- src/index.coffee
+++ src/index.coffee
@@ -45,7 +45,7 @@
 		{w, h, g} = req.query
 		w = parseInt(w, 10) if w
 		h = parseInt(h, 10) if h
-		path = unescape("#{opts.root}#{req.path[opts.prefix.length ..]}")
+		path = decodeURI("#{opts.root}#{req.path[opts.prefix.length ..]}")
 		type = mime.lookup(path)

 		return next() unless type in opts.contentTypes


Removed not used event binding.

Removed not used event binding.

--- app/assets/javascripts/app/controllers/navigation.js.coffee
+++ app/assets/javascripts/app/controllers/navigation.js.coffee
@@ -1,9 +1,6 @@
 $ = jQuery.sub()

 class App.Navigation extends App.Controller
-  events:
-    'focusin [data-type=edit]':     'edit_in'
-
   constructor: ->
     super
     @log 'nav...'


:skull: wrapping all @get method in a try catch

--- app/helpers/torrent.coffee
+++ app/helpers/torrent.coffee
@@ -87,23 +87,23 @@
         torrent_engine = TorrentStream torrent_id,
           tmp     : cfg.sultanna_path
           trackers: cfg.torrent_trackers
+
+        torrent_engine.once 'ready', ->
+          if timeout?
+            clearTimeout timeout
+            callback null, torrent_engine
+
+        timeout = setTimeout ->
+          clearTimeout timeout
+          timeout = undefined
+
+          torrent_engine.removeAllListeners 'ready'
+          torrent_engine.destroy ->
+            callback new Error('blacksam.torrent.getTimeout')
+        , cfg.timeout
       catch e
         return async.nextTick ->
           callback e
-
-      torrent_engine.once 'ready', ->
-        if timeout?
-          clearTimeout timeout
-          callback null, torrent_engine
-
-      timeout = setTimeout ->
-        clearTimeout timeout
-        timeout = undefined
-
-        torrent_engine.removeAllListeners 'ready'
-        torrent_engine.destroy ->
-          callback new Error('blacksam.torrent.getTimeout')
-      , cfg.timeout

     @scrape = (tracker_urls..., torrent_engine, callback) ->
       try


Fixed online notifications - no shown because in undefined refresh items.

--- app/assets/javascripts/app/models/_application_model.coffee
+++ app/assets/javascripts/app/models/_application_model.coffee
@@ -546,7 +546,7 @@

           # in case of no record_ids are there, no inital render is fired
           if _.isEmpty(data.record_ids)
-            App[@className].trigger('refresh')
+            App[@className].trigger('refresh', [])

         # find / load object
         else


EDSC-619: Fix issue with ECHO Forms inappropriately reading from the rawModel field

--- app/assets/javascripts/models/knockout_extensions.js.coffee
+++ app/assets/javascripts/models/knockout_extensions.js.coffee
@@ -99,6 +99,7 @@
         $el.off('echoforms:modelchange')

       options = ko.unwrap(valueAccessor())
+
       methodName = options.method()
       if methodName?
         method = null
@@ -109,7 +110,7 @@
         if available?.form?
           form = available.form
           model = options.rawModel
-          if model?
+          if model? && !options.isReadFromDefaults
             form = form.replace(/(?:<instance>)(?:.|\n)*(?:<\/instance>)/, "<instance>\n#{model}\n</instance>")
           $el.echoforms(form: form)
           syncModel = ->
@@ -126,6 +127,7 @@
           syncModel.call($el)
         else
           options.isValid(true)
+        options.isReadFromDefaults = true
       null

   ko.bindingHandlers.mapdisplay =


change experiment to version 3

change experiment to version 3

--- app/lib/experiments.coffee
+++ app/lib/experiments.coffee
@@ -9,7 +9,7 @@
   Define the active experiment here by using a string which exists in http://experiments.zooniverse.org/active_experiments
   If no experiments should be running right now, set this to null, false or ""
   ###
-  ACTIVE_EXPERIMENT: "SerengetiMessagingExperiment2"
+  ACTIVE_EXPERIMENT: "SerengetiMessagingExperiment3"
   #ACTIVE_EXPERIMENT: "SerengetiBlanksExperiment1"
   ###
   The URL of the experiment server to use


Added more names for #53

Added more names for #53

--- app/lib/world/names.coffee
+++ app/lib/world/names.coffee
@@ -409,9 +409,14 @@
   ]
   'Ninja': [
     'Amara'
+    'Naruto'
+    'Sasuke'
+    'Minato'
   ]
   'Sorcerer': [
     'Pender'
+    'Gandalf'
+    'Beazer'
   ]
   'Samurai': [
     'Hattori'


Fixed auto-fill of rows.

Fixed auto-fill of rows.

--- app/assets/javascripts/views/staffplans/work_week.js.coffee
+++ app/assets/javascripts/views/staffplans/work_week.js.coffee
@@ -117,7 +117,7 @@
     event.preventDefault()
     event.stopPropagation()
     @_lastFocused.focus()
-    $(@_lastFocused).parent().nextAll().find('input[data-work-week-input]')
+    $(@_lastFocused).nextAll('input[data-work-week-input]')
       .val(@_lastFocused.value)
       .trigger('change')


Fixes focusing a new folder in IE

Fixes focusing a new folder in IE

For New Files, whenever a folder was
created, it wouldn't automatically
focus the new folder.  This fixes
that issue.

fixes CNVS-17026

Test Plan:
* Enable "Better File Browsing"
  feature flag.
* In IE 10/11: Go to any course's
  Files page.
* Click the "+ Folder" button
  to create a new folder.
* That folder's text box should
  be focused, allowing you to
  enter a name.

Change-Id: Ieef503a0259a1c13499f277f9007bc87e278de5e
Reviewed-on: https://gerrit.instructure.com/44755
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Sterling Cobb <7a57d889ea7f266299434459898a90c7f6a1ca4d@instructure.com>
QA-Review: Amber Taniuchi <ef4bfd1e6d278f6bc7d4a478f02be2cb9581a0a5@instructure.com>
Product-Review: Matt Fairbourn <5f353e1c97bb9cc046b9db20beca0b9392f6f1b2@instructure.com>

--- app/coffeescripts/react_files/components/FolderChild.coffee
+++ app/coffeescripts/react_files/components/FolderChild.coffee
@@ -32,15 +32,24 @@
     focusPreviousElement: ->
       @previouslyFocusedElement?.focus()
       return if document.activeElement ==  @previouslyFocusedElement
-      @refs.nameLink.getDOMNode().focus()
+      @focusNameLink()

     focusNameInput: ->
       @previouslyFocusedElement = document.activeElement
-      @refs.newName.getDOMNode().focus()
+      setTimeout () =>
+        @refs.newName?.getDOMNode().focus()
+      , 0
+
+    focusNameLink: ->
+      setTimeout () =>
+        @refs.nameLink?.getDOMNode().focus()
+      , 0

     saveNameEdit: ->
-      @props.model.save(name: @refs.newName.getDOMNode().value)
-      @setState editing: false, => @refs.nameLink.getDOMNode().focus()
+      @setState editing: false, @focusNameLink
+      @props.model.save(name: @refs.newName.getDOMNode().value, {success: =>
+          @focusNameLink()
+      })

     cancelEditingName: ->
       @props.model.collection.remove(@props.model) if @props.model.isNew()


update tests

update tests

--- tests/rescuetime.coffee
+++ tests/rescuetime.coffee
@@ -9,6 +9,7 @@

 start = moment().subtract('years', 10).format 'YYYY-MM-DD'
 end = moment().add('days', 1).format 'YYYY-MM-DD'
+now = moment().format 'YYYY-MM-DD'

 # Mock rescue time server.
@@ -24,8 +25,13 @@
     .reply 200,
         rows: [
           [ '2013-12-22T00:00:00', 7, 1, 'npmjs.org', 'Marketing', 2 ]
+          [ "#{now}T00:00:00", 7, 1, 'npmjs.org', 'Marketing', 2 ]
         ]
-
+    .get(basepath + "&restrict_begin=#{now}&restrict_end=#{end}")
+    .reply 200,
+        rows: [
+          [ "#{now}T00:00:00", 7, 1, 'npmjs.org', 'Marketing', 2 ]
+        ]

 describe "When I fetch rescuetime data", ->
     before (done) ->
@@ -43,11 +49,20 @@
                 activities.length.should.equal 2
                 done()

-    it "The second time it should store one more activity", (done) =>
+    it "The second time it should store two more activities", (done) =>
         connector.fetch apikey: 'testkey', (err) =>
             should.not.exist err

             connector.models.activities.all (err, activities) =>
                 should.not.exist err
-                activities.length.should.equal 3
+                activities.length.should.equal 4
                 done()
+
+    it "The third time it should not store any new activities", (done) =>
+        connector.fetch apikey: 'testkey', (err) =>
+            should.not.exist err
+
+            connector.models.activities.all (err, activities) =>
+                should.not.exist err
+                activities.length.should.equal 4
+                done()


Clean up

Clean up

--- app/pages/about/index.cjsx
+++ app/pages/about/index.cjsx
@@ -13,60 +13,6 @@
       publications: 'Publications'
       ourTeam: 'Our Team'
       careers: 'Careers'
-    pageContent: '''
-      ## What is the Zooniverse?
-
-      The Zooniverse is the world's largest and most popular online platform for
-      people-powered research projects. This research is being conducted in part by
-      volunteers—hundreds of thousands of people around the world who come together to
-      assist professional researchers with their data. Our goal is to enable research
-      that would not be possible (or practical) otherwise. People-powered research results
-      in new discoveries, datasets useful to the wider research community, and [many publications](./#/about/publications).
-
-      ### At the Zooniverse, anyone can be a researcher online.
-
-      You don't need any specialised background, training, or expertise to participate in any
-      Zooniverse projects. We make it easy for anyone to contribute to real academic research,
-      on their own computer, at their own convenience.
-
-      You'll be able to study authentic objects of interest gathered by researchers,
-      like images of faraway galaxies, historical records and diaries, or videos of animals
-      in their natural habitats. By answering simple questions about them, you'll help contribute
-      to our understanding of our world, our history, our universe, and more.
-
-      And with our wide-ranging and ever-expanding suite of projects, covering many disciplines
-      and topics across the sciences and humanities, there's a place for anyone and everyone to
-      explore, learn and have fun in the Zooniverse!
-
-      ### We accelerate important research by working together.
-
-      The major challenge of 21st century research is dealing with the flood of information we can
-      now collect about the world around us. Computers can help, but in many fields the human ability
-      for pattern recognition—and our ability to be surprised—is still superior. With the help of
-      Zooniverse volunteers, researchers can analyze that information more quickly and accurately
-      than would otherwise be possible, saving time and resources, advancing the ability of computers
-      to do the same tasks, and leading to faster research output.
-
-      Our projects apply the philosophy of the "wisdom of crowds." Combining the classifications of
-      many individual users produces accurate and usable data with a measurable estimate of
-      error—exactly what's needed to make progress in these fields of research.
-
-      ### Volunteers and professionals make real discoveries together.
-
-      Zooniverse projects are constructed with the aim of converting volunteers' efforts into
-      measurable results. These projects have produced a large number of [published research papers](./#/about/publications),
-      as well as several open-source sets of analyzed data. In some cases, Zooniverse volunteers have
-      even made completely new and scientifically significant discoveries!
-
-      A significant amount of this research takes place on the Zooniverse discussion boards,
-      where volunteers can work together with each other and with the research teams. These boards
-      are integrated with each project's data to allow for everything from quick hashtagging to in-depth
-      collaborative analysis. There is also a central Zooniverse board for general chat and discussion
-      about Zooniverse-wide matters.
-
-      Many of the most interesting discoveries from Zooniverse projects
-      have come from discussion between volunteers and researchers.
-      We encourage all users to join the conversation on the discussion boards for more in-depth participation.'''

 module.exports = React.createClass
   displayName: 'AboutPage'


Window back to this

Window back to this

--- src/jsonp.coffee
+++ src/jsonp.coffee
@@ -68,4 +68,4 @@

 if define? and define.amd then define -> JSONP
 else if module? and module.exports then module.exports = JSONP
-else window.JSONP = JSONP
+else this.JSONP = JSONP


Add failing spec for paste error notification

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -1412,6 +1412,23 @@

             expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy()
             expect(fs.existsSync(filePath)).toBeFalsy()
+
+      describe "when pasting the files fails", ->
+        it "shows a notification", ->
+          spyOn(fs, 'writeFileSync').andCallFake ->
+            writeError = new Error("ENOENT: no such file or directory, open '#{filePath}'")
+            writeError.code = 'ENOENT'
+            writeError.path = filePath
+            throw writeError
+
+          LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath])
+
+          fileView2.click()
+          atom.notifications.clear()
+          atom.commands.dispatch(treeView.element, "tree-view:paste")
+
+          expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Unable to paste paths'
+          expect(atom.notifications.getNotifications()[0].getDetails()).toContain 'ENOENT: no such file or directory'

     describe "tree-view:add-file", ->
       [addPanel, addDialog] = []


Remove redundant existsSync checks

Remove redundant existsSync checks

These were always followed by an isDirectorySync or isFileSync
assertion which also verifies it exists.

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -1271,7 +1271,6 @@
                 addDialog.trigger 'core:confirm'

               runs ->
-                expect(fs.existsSync(newPath)).toBeTruthy()
                 expect(fs.isFileSync(newPath)).toBeTruthy()
                 expect(addDialog.parent()).not.toExist()
                 expect(atom.workspace.getActivePaneItem().getPath()).toBe newPath
@@ -1391,7 +1390,6 @@
               newPath = path.join(dirPath, "new/dir")
               addDialog.miniEditor.insertText("new/dir")
               addDialog.trigger 'core:confirm'
-              expect(fs.existsSync(newPath)).toBeTruthy()
               expect(fs.isDirectorySync(newPath)).toBeTruthy()
               expect(addDialog.parent()).not.toExist()
               expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath
@@ -1406,7 +1404,6 @@
               newPath = path.join(dirPath, "new/dir")
               addDialog.miniEditor.insertText("new/dir/")
               addDialog.trigger 'core:confirm'
-              expect(fs.existsSync(newPath)).toBeTruthy()
               expect(fs.isDirectorySync(newPath)).toBeTruthy()
               expect(addDialog.parent()).not.toExist()
               expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath
@@ -1426,7 +1423,6 @@
               newPath = path.join(dirPath, "new2/")
               addDialog.miniEditor.insertText("new2/")
               addDialog.trigger 'core:confirm'
-              expect(fs.existsSync(newPath)).toBeTruthy()
               expect(fs.isDirectorySync(newPath)).toBeTruthy()
               expect(addDialog.parent()).not.toExist()
               expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath


rename class change key

rename class change key

--- shared/IdleController.coffee
+++ shared/IdleController.coffee
@@ -257,7 +257,7 @@
         null

       $scope.isHtmlStat = (stat) ->
-        stat in ['calculated kills', 'calculated kills by class', 'class changes']
+        stat in ['calculated kills', 'calculated kills by class', 'calculated class changes']

       $scope.statsObj = (player, key) ->
         val = player.statistics[key] or 0


Environments: EnvironmentSceneView: Revert createConnection change which causes high load on drawing and remove addFakeConnection call since we are not using it anymore

--- client/Environments/views/scene/environmentsceneview.coffee
+++ client/Environments/views/scene/environmentsceneview.coffee
@@ -47,9 +47,7 @@

   connect:(source, target, internal = no)->

-    createConnection = =>
-      KDDiaScene::connect.call this, source, target
-      @resetScene()
+    createConnection = => KDDiaScene::connect.call this, source, target

     return createConnection()  if internal

@@ -70,13 +68,13 @@
         return new KDNotificationView
           title : "A domain name can only be bound to one VM."

-    @addFakeConnection {
-      source, target,
-      options : {
-        lineColor  : "#cdcdcd"
-        lineDashes : [5]
-      }
-    }
+    # @addFakeConnection {
+    #   source, target,
+    #   options : {
+    #     lineColor  : "#cdcdcd"
+    #     lineDashes : [5]
+    #   }
+    # }

     if domain and machine
       jDomain = domain.dia.getData().domain # JDomain


Client: TeamSettings: Members: Using fat arrow to fix reference problem for destroy method

--- client/admin/lib/views/members/blockedmemberitemview.coffee
+++ client/admin/lib/views/members/blockedmemberitemview.coffee
@@ -45,7 +45,7 @@

     currentGroup = kd.singletons.groupsController.getCurrentGroup()

-    invitationWithNoEmail @getData(), currentGroup,  (err, result) ->
+    invitationWithNoEmail @getData(), currentGroup,  (err, result) =>

       if err
         customErr = new Error 'Failed to unblock user. Please try again.'


Creates ability to specify the type of sub expression in js exp builder

--- math/build_expression_from_javascript_object_spec.coffee
+++ math/build_expression_from_javascript_object_spec.coffee
@@ -46,18 +46,19 @@
       expect(exponentiation).toBeInstanceOf @components.exponentiation

       base = exponentiation.base()
-      expect(base).toBeInstanceOf @components.number
-      expect(base.value()).toEqual 10
+      expect(base).toBeInstanceOf @components.expression
+      expect(base.first().value()).toEqual 10

       power = exponentiation.power()
-      expect(power).toBeInstanceOf @components.number
-      expect(power.value()).toEqual 11
+      expect(power).toBeInstanceOf @components.expression
+      expect(power.first().value()).toEqual 11

-    it 'supports incomplete exponentiations (as blank member)', ->
+    it 'supports incomplete exponentiations (as an empty expression)', ->
       expression = @builder '^': [10, null]
       exponentiation = expression.nth(0)
       expect(exponentiation).toBeInstanceOf @components.exponentiation
-      expect(exponentiation.power()).toBeInstanceOf @components.blank
+      expect(exponentiation.power()).toBeInstanceOf @components.expression
+

   it "handles parenthetical sub-expressions via arrays", ->
     expression = @builder []


adds Blank expression component

adds Blank expression component

blank to be used instead of null, false, etc

--- math/build_expression_from_javascript_object_spec.coffee
+++ math/build_expression_from_javascript_object_spec.coffee
@@ -11,31 +11,50 @@

   it "handles numbers", ->
     expression = @builder(10)
-    expect(expression.first().value()).toEqual '10'
+    expect(expression.first().value()).toEqual 10

   it "handles addition", ->
     expression = @builder(10, '+', 11)
     expect(expression.nth(0)).toBeInstanceOf @components.number
     expect(expression.nth(1)).toBeInstanceOf @components.addition
-    expect(expression.last().value()).toEqual '11'
+    expect(expression.last().value()).toEqual 11

   it "handles division", ->
     expression = @builder(10, '/', 11)
     expect(expression.nth(0)).toBeInstanceOf @components.number
     expect(expression.nth(1)).toBeInstanceOf @components.division
-    expect(expression.last().value()).toEqual '11'
+    expect(expression.last().value()).toEqual 11
+
+  it "handles multiplication", ->
+    expression = @builder(10, '*', 11)
+    expect(expression.nth(0)).toBeInstanceOf @components.number
+    expect(expression.nth(1)).toBeInstanceOf @components.multiplication
+    expect(expression.last().value()).toEqual 11

-  it 'handles exponentiation', ->
-    expression = @builder '^': [10, 11]
-    exponentiation = expression.nth(0)
-    expect(exponentiation).toBeInstanceOf @components.exponentiation
+  describe 'exponentiation', ->
+    it 'handles a standard case', ->
+      expression = @builder '^': [10, 11]
+      exponentiation = expression.nth(0)
+      expect(exponentiation).toBeInstanceOf @components.exponentiation

-    base = exponentiation.base()
-    expect(base).toBeInstanceOf @components.number
-    expect(base.value()).toEqual '10'
+      base = exponentiation.base()
+      expect(base).toBeInstanceOf @components.number
+      expect(base.value()).toEqual 10

-    power = exponentiation.power()
-    expect(power).toBeInstanceOf @components.number
-    expect(power.value()).toEqual '11'
+      power = exponentiation.power()
+      expect(power).toBeInstanceOf @components.number
+      expect(power.value()).toEqual 11

+    it 'supports incomplete exponentiations (as blank member)', ->
+      expression = @builder '^': [10, null]
+      exponentiation = expression.nth(0)
+      expect(exponentiation).toBeInstanceOf @components.exponentiation
+      expect(exponentiation.power()).toBeInstanceOf @components.blank
+
+  it "handles parenthetical sub-expressions via arrays", ->
+      expression = @builder []
+      sub_exp = expression.first()
+      expect(expression).toBeInstanceOf @components.expression
+      expect(sub_exp).toBeInstanceOf @components.expression
+


FIX: This fix should kill this nightmare

--- src/Moon.coffee
+++ src/Moon.coffee
@@ -63,7 +63,7 @@
                 property = Moon.fn.camelize(pre + propCap)
                 if document.documentElement.style[property]?
                     Moon.fn._prefixes[prop] = property
-                    return propertie
+                    return property

         # capitalizes the first letter
         camelize: (str) ->


added to backbone

added to backbone

--- app/assets/javascripts/backbone/views/utils/info_help.coffee
+++ app/assets/javascripts/backbone/views/utils/info_help.coffee
@@ -10,7 +10,6 @@
     that = this
     $(@el).each ->
       $(this).popover(html: true, trigger: 'manual', placement: 'top').click((e) ->
-        console.log $(this)
         $(this).popover('show')
         isVisible = true
         e.preventDefault()


refresh user info on init project creation

--- app/modules/projects/create/create-project.controller.coffee
+++ app/modules/projects/create/create-project.controller.coffee
@@ -22,13 +22,16 @@
         "tgAppMetaService",
         "$translate",
         "tgProjectService",
-        "$location"
+        "$location",
+        "$tgAuth"
     ]

-    constructor: (@appMetaService, @translate, @projectService, @location) ->
+    constructor: (@appMetaService, @translate, @projectService, @location, @authService) ->
         taiga.defineImmutableProperty @, "project", () => return @projectService.project

         @appMetaService.setfn @._setMeta.bind(this)
+
+        @authService.refresh()

         @.displayScrumDesc = false
         @.displayKanbanDesc = false


removed unneccesary functions

removed unneccesary functions

--- core/app/backbone/views/user_channel_suggestion_view.coffee
+++ core/app/backbone/views/user_channel_suggestion_view.coffee
@@ -4,10 +4,6 @@

   events:
     'click a.btn' : 'addModel'
-
-  addModelSuccess: (model)->
-
-  addModelError: (model)->

   addModel: ->
     facts = @model.facts()


Update Atom config

Update Atom config

--- opt/config.cson
+++ opt/config.cson
@@ -1,13 +1,15 @@
-'exception-reporting':
-  'userId': 'f637adea-1050-8ece-429a-1a2be9ddef08'
-'welcome':
-  'showOnStartup': false
-'metrics':
-  'userId': 'a3d0e1b721f417804bf9c8a098629757bafe5628'
-'core':
-  'themes': [
-    'spacegray-dark-ui'
-    'spacegray-dark-syntax'
-  ]
-'editor':
-  'tabLength': 4
+'global':
+  'exception-reporting':
+    'userId': 'f637adea-1050-8ece-429a-1a2be9ddef08'
+  'welcome':
+    'showOnStartup': false
+  'metrics':
+    'userId': 'a3d0e1b721f417804bf9c8a098629757bafe5628'
+  'core':
+    'themes': [
+      'spacegray-dark-ui'
+      'spacegray-dark-syntax'
+    ]
+  'editor':
+    'tabLength': 4
+    'invisibles': {}


forgot to finish fixing the bigger dropzone

--- scripts/helpers/enable-dnd.coffee
+++ scripts/helpers/enable-dnd.coffee
@@ -78,6 +78,11 @@
             $drag = ui.draggable
             $drop = $(evt.target)

+            # drop is/can-be a child of the current element.
+            # If there is a child with `*[data-media-type]` then use it instead
+            if not $drop.is('*[data-media-type]')
+              $drop = $drop.children('*[data-media-type]')
+
             # Find the model representing the id that was dragged
             drag = $drag.data 'editor-model'
             drop = $drop.data 'editor-model'


fixes wrong module publishing

fixes wrong module publishing

--- tests/test_application.coffee
+++ tests/test_application.coffee
@@ -6,7 +6,7 @@

 class @CustomerApp extends Space.Application

-  @publish -> 'CustomerApp'
+  @publish this, 'CustomerApp'

   RequiredModules: ['Space.eventSourcing']


Add LanguageManager

Add LanguageManager

--- app/main.coffee
+++ app/main.coffee
@@ -4,6 +4,11 @@

 Api = require 'zooniverse/lib/api'
 api = new Api project: 'cyclone_center'
+
+LanguageManager = require 'zooniverse/lib/language-manager'
+languageManager = new LanguageManager
+  translations:
+    en: label: 'English', strings: enUs

 Navigation = require './controllers/navigation'
 navigation = new Navigation


got failing acceptence test

got failing acceptence test

--- services/docstore/test/acceptance/coffee/ArchiveDocsTests.coffee
+++ services/docstore/test/acceptance/coffee/ArchiveDocsTests.coffee
@@ -150,3 +150,15 @@
 					for doc, i in non_deleted_docs
 						doc.lines.should.deep.equal @docs[i].lines
 					done()
+
+	describe "Unarchiving automatically", ->
+		it "should unarchive the docs ", (done) ->
+			DocstoreClient.archiveAllDoc @project_id, (error, res) =>
+				DocstoreClient.getDoc @project_id, @docs[0]._id, {}, (error, res, doc) =>
+					doc.lines.should.deep.equal @docs[0].lines
+					doc.version.should.equal @docs[0].version
+					doc.ranges.should.deep.equal @docs[0].ranges
+					done()
+
+
+


null check the user correctly

null check the user correctly

--- services/web/app/coffee/Features/User/UserSessionsManager.coffee
+++ services/web/app/coffee/Features/User/UserSessionsManager.coffee
@@ -114,7 +114,7 @@
 					callback(null)

 	touch: (user, callback=(err)->) ->
-		if !user
+		if !user?
 			logger.log {}, "no user to touch sessions for, returning"
 			return callback(null)
 		sessionSetKey = UserSessionsRedis.sessionSetKey(user)
@@ -125,7 +125,7 @@
 			callback(null)

 	_checkSessions: (user, callback=(err)->) ->
-		if !user
+		if !user?
 			logger.log {}, "no user, returning"
 			return callback(null)
 		logger.log {user_id: user._id}, "checking sessions for user"


Client: AccountSessionListController: implement scroll view

--- client/account/lib/views/accountsessionlistcontroller.coffee
+++ client/account/lib/views/accountsessionlistcontroller.coffee
@@ -8,27 +8,61 @@

   constructor:(options,data) ->

-    options.noItemFoundText = "You have no active session."
+    options = kd.utils.extend {}, options,
+      limit               : 8
+      noItemFoundText     : 'You have no active session.'
     super options,data

-    @fetchSessions()
+    @busy = no
+    @skip = 0

-  fetchSessions: ->
+  followLazyLoad: ->

+    @on 'LazyLoadThresholdReached', kd.utils.debounce 300, =>
+
+      return  @hideLazyLoader()  if @busy
+
+      @busy = yes
+      limit = @getOption 'limit'
+      @skip += limit
+      console.log @skip
+
+      @fetch { @skip, limit }, (err, sessions) =>
+        @hideLazyLoader()
+
+        if err or not sessions
+          return @busy = no
+
+        @instantiateListItems sessions
+        @busy = no
+
+
+  loadItems: ->
     @removeAllItems()
-    @showLazyLoader no
+    @showLazyLoader()

-    whoami().fetchMySessions (err, sessions) =>
+    @fetch {}, (err, sessions) =>
+
+      @hideLazyLoader()
       @instantiateListItems sessions
-      @hideLazyLoader()

-      @header?.destroy()

-      @header = new KDHeaderView
-        title : 'Active Sessions'
+  fetch: (options = {}, callback) ->

-      @getListView().addSubView @header, '', yes
+    options.limit or= @getOption 'limit'
+    options.sort  or= { 'sessionBegan' : -1 }
+    console.log options
+
+    whoami().fetchMySessions options, (err, sessions) =>
+
+      if err
+        @hideLazyLoader()
+        showError err, \
+          KodingError : "Failed to fetch data, try again later."
+        return
+
+      callback err, sessions

   loadView: ->
@@ -37,6 +71,11 @@

     @hideNoItemWidget()

-    @listView.on 'ItemDeleted', (item) =>
+
+    @getListView().on 'ItemDeleted', (item) =>
       @removeItem item
-      @noItemView.show()  if @listView.items.length is 0
+      @noItemView.show()  if @getListView().items.length is 0
+
+    @loadItems()
+    @followLazyLoad()
+


set left anchor panel to string 0

set left anchor panel to string 0

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -274,7 +274,7 @@
         if anchor is 'left'
           xOffset = -(i * @panelWidth)
           if i is 0
-            panel.style.left = 0
+            panel.style.left = '0'
           else
             panel.style.left = @panelWidth + 'px'
         else


syntax tweaks

syntax tweaks

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -252,7 +252,7 @@
         content.style[transformProp] = @_transform [x, 0]

         if @shading
-          @shaders[anchor].left[i - 1] = panel.getElementsByClassName('oridomi-shader-left')[0]
+          @shaders[anchor].left[i - 1]  = panel.getElementsByClassName('oridomi-shader-left')[0]
           @shaders[anchor].right[i - 1] = panel.getElementsByClassName('oridomi-shader-right')[0]

         @panels[anchor][i - 1] = panel
@@ -286,9 +286,7 @@
         delay = 0
       else
         delay = @settings.speed * 1000
-      setTimeout =>
-        options.callback()
-      , delay
+      setTimeout options.callback, delay

   _transform: (translation, rotation) ->


fixed step args for methods with less than 3 arguments

--- oridomi.coffee
+++ oridomi.coffee
@@ -502,7 +502,9 @@
     @unfreeze() if @isFrozen
     next = =>
       @_setCallback {angle, anchor, options, fn}
-      fn.call @, angle, anchor, options
+      args = [angle, anchor, options]
+      args.shift() if fn.length < 3
+      fn.apply @, args

     if anchor isnt @lastOp.anchor
       @_stageReset anchor, next
@@ -687,6 +689,7 @@
     for panel, i in @panels[@lastOp.anchor]
       panel.style[css.transform] = @_transform 0, @lastOp.anchor
       @_setShader i, @lastOp.anchor, 0 if @shading
+
     @


single check for `$`, removed `root`

single check for `$`, removed `root`

--- oridomi.coffee
+++ oridomi.coffee
@@ -11,13 +11,10 @@
 # =====
 'use strict'

-# Set a reference to the global object within this scope.
-root = @
-
 # Set a reference to jQuery (or another `$`-aliased DOM library).
 # If it doesn't exist, set to false so oriDomi knows we are working without jQuery.
 # oriDomi doesn't require it to work, but offers a useful plugin bridge.
-$ = root.$ or false
+$ = if window.$?.data then window.$ else false

 # `devMode` determines whether oriDomi is vocal in the console with warnings and benchmarks.
 # Turn it on externally by calling `OriDomi.devMode()`.
@@ -253,7 +250,7 @@
       return @

     # Record the current global styling of the target element.
-    elStyle = root.getComputedStyle @el
+    elStyle = window.getComputedStyle @el
     @_originalStyle = {}
     @_originalStyle[key] = elStyle[key] for key in modifiedStyleKeys

@@ -1129,8 +1126,8 @@

 # Export constructor on `window` and `module.exports` (if applicable).
-root.OriDomi   = OriDomi
-module.exports = OriDomi if module?.exports?
+window.OriDomi = OriDomi
+module.exports = OriDomi if module?.exports

@@ -1139,7 +1136,7 @@

 # Only create bridge if jQuery (or an imitation supporting `data()`) exists.
-if root.jQuery? or root.$?.data?
+if $
   # Attach an `oriDomi` method to `$`'s prototype.
   $::oriDomi = (options) ->
     # Return selection if oriDomi is unsupported by the browser.


Workspace: Connectivity issue notification is mini now.

--- client/CollaborativeWorkspace/collaborativeworkspace.coffee
+++ client/CollaborativeWorkspace/collaborativeworkspace.coffee
@@ -415,6 +415,8 @@
       title       : message
       duration    : 20000
       container   : this
+      cssClass    : "error"
+      type        : "mini"
       overlay     : {}

   hideNotification: ->


Molecule needs a correct Atom NameSpace

Molecule needs a correct Atom NameSpace

--- source/class/molecule.coffee
+++ source/class/molecule.coffee
@@ -18,8 +18,8 @@

   constructor: (@attributes) ->
     super
+    @default = children: [] unless @default
     @children = []
-    @default = children: [] unless @default
     @constructor.type = "Molecule"
     do @scaffold
     do @output
@@ -29,10 +29,12 @@
     children = @attributes.children or @default.children
     for atom, index in children
       for key of atom when key in @available
-        className = key.toClassName()
-        if Atoms.Atom[className]?
+        item = key.split(".")
+        type = item[0]
+        class_name = item[1]
+        if Atoms[type][class_name]?
           attributes = Atoms.Core.Helper.mix atom[key], @default.children?[index]?[key]
-          instance = @_atomInstance key, className, attributes
+          instance = @_atomInstance class_name, attributes
           @children.push instance
           @[key] = [] unless @[key]?
           @[key].push instance
@@ -41,17 +43,15 @@

   entity: (entities) ->
     do @_removeAtomsEntities
-
     for entity in entities when Atoms.Atom[@atomEntity]?
       attributes = entity: entity
       attributes = Atoms.Core.Helper.mix attributes, @default.children?[@atomEntity]
-      @_entities.push @_atomInstance null, @atomEntity, attributes
+      @_entities.push @_atomInstance @atomEntity, attributes

   # Private Methods
-  _atomInstance: (key, className, attributes) ->
+  _atomInstance: (class_name, attributes) ->
     attributes.parent = @
-    attributes.events = attributes.events or []
-    new Atoms.Atom[className] attributes
+    new Atoms.Atom[class_name] attributes

   _removeAtomsEntities: ->
     entity.el.remove() for entity in @_entities


sidebar: show stack title when no template

For Managed VMs section

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/sidebar/components/ownedresourceslist.coffee
+++ client/app/lib/sidebar/components/ownedresourceslist.coffee
@@ -229,11 +229,11 @@

     resource = @props.resources[sectionIndex]

-    { template, unreadCount } = resource
+    { template, stack, unreadCount } = resource

     <OwnedResourceHeader
       ref={(header) => @headers[sectionIndex] = header}
-      title={template.title}
+      title={template?.title or stack?.title}
       onTitleClick={@lazyBound 'onHeaderTitleClick', resource}
       onMenuIconClick={@lazyBound 'onHeaderMenuClick', sectionIndex, resource}
       unreadCount={unreadCount} />
@@ -254,7 +254,7 @@
   render: ->

     <div className='SidebarTeamSection'>
-      <SectionHeader children='STACKS' />
+      <SectionHeader />
       <List
         sectionClassName='SidebarSection SidebarStackSection'
         rowClassName='SidebarSection-body'


validate SS

validate SS

--- api/apps/articles/model.coffee
+++ api/apps/articles/model.coffee
@@ -40,7 +40,7 @@
       body: @string().allow('', null)
     @object().keys
       type: @string().valid('artworks')
-      ids: @array().items(@string())
+      ids: @array().items(@objectId())
       layout: @string().allow('overflow_fillwidth', 'column_width', null)
     @object().keys
       type: @string().valid('video')


Track time taken to read window state

Track time taken to read window state

Refs atom/timecop#6

--- src/atom.coffee
+++ src/atom.coffee
@@ -29,7 +29,10 @@
   #
   # Returns an Atom instance, fully initialized
   @loadOrCreate: (mode) ->
-    @deserialize(@loadState(mode)) ? new this({mode, @version})
+    startTime = Date.now()
+    atom = @deserialize(@loadState(mode)) ? new this({mode, @version})
+    atom.deserializeTimings.atom = Date.now() -  startTime
+    atom

   # Deserializes the Atom environment from a state object
   @deserialize: (state) ->
@@ -152,6 +155,7 @@
     {@mode} = @state
     DeserializerManager = require './deserializer-manager'
     @deserializers = new DeserializerManager()
+    @deserializeTimings = {}

   # Sets up the basic services that should be available in all modes
   # (both spec and application).
@@ -597,7 +601,6 @@
     delete @state.packageStates

   deserializeEditorWindow: ->
-    @deserializeTimings = {}
     @deserializePackageStates()
     @deserializeProject()
     @deserializeWorkspaceView()


Display window after wiring up windor.onerror

--- src/atom.coffee
+++ src/atom.coffee
@@ -196,36 +196,36 @@
   #
   # Call after this instance has been assigned to the `atom` global.
   initialize: ->
+    sourceMapCache = {}
+
+    window.onerror = =>
+      @lastUncaughtError = Array::slice.call(arguments)
+      [message, url, line, column, originalError] = @lastUncaughtError
+
+      convertedLine = convertLine(url, line, column, sourceMapCache)
+      {line, column} = convertedLine if convertedLine?
+      originalError.stack = convertStackTrace(originalError.stack, sourceMapCache) if originalError
+
+      eventObject = {message, url, line, column, originalError}
+
+      openDevTools = true
+      eventObject.preventDefault = -> openDevTools = false
+
+      @emitter.emit 'will-throw-error', eventObject
+
+      if openDevTools
+        @openDevTools()
+        @executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
+
+      @emit 'uncaught-error', arguments... if includeDeprecatedAPIs
+      @emitter.emit 'did-throw-error', {message, url, line, column, originalError}
+
+    @disposables?.dispose()
+    @disposables = new CompositeDisposable
+
     dimensions = @restoreWindowDimensions()
     maximize = dimensions?.maximized and process.platform isnt 'darwin'
     @displayWindow({maximize})
-
-    sourceMapCache = {}
-
-    window.onerror = =>
-      @lastUncaughtError = Array::slice.call(arguments)
-      [message, url, line, column, originalError] = @lastUncaughtError
-
-      convertedLine = convertLine(url, line, column, sourceMapCache)
-      {line, column} = convertedLine if convertedLine?
-      originalError.stack = convertStackTrace(originalError.stack, sourceMapCache) if originalError
-
-      eventObject = {message, url, line, column, originalError}
-
-      openDevTools = true
-      eventObject.preventDefault = -> openDevTools = false
-
-      @emitter.emit 'will-throw-error', eventObject
-
-      if openDevTools
-        @openDevTools()
-        @executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
-
-      @emit 'uncaught-error', arguments... if includeDeprecatedAPIs
-      @emitter.emit 'did-throw-error', {message, url, line, column, originalError}
-
-    @disposables?.dispose()
-    @disposables = new CompositeDisposable

     @setBodyPlatformClass()


app: Disable default context menu so that Oghma can use it.

--- Oghma/app.coffee
+++ Oghma/app.coffee
@@ -160,6 +160,12 @@

       @join_table( initial_table )
     )
+
+    # Allow right clicks to hit nodes.
+    window.oncontextmenu = ( event ) ->
+        event.preventDefault()
+        event.stopPropagation()
+        false

     # Console
     @console = Ext.create( 'Oghma.Ext.Console',


Restore a broken test that was commented out temporarily

--- services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.coffee
+++ services/track-changes/test/unit/coffee/UpdatesManager/UpdatesManagerTests.coffee
@@ -105,11 +105,10 @@
 						.should.equal true

 				# FIXME: Broken test, was hidden by an api mistake
-
-				# it "should defer the compression of raw ops until they are written in a new pack", ->
-				# 	console.log @UpdateCompressor.compressRawUpdates.called
-				# 	@UpdateCompressor.compressRawUpdates
-				# 		.called.should.not.equal true
+				it "should defer the compression of raw ops until they are written in a new pack", ->
+					console.log @UpdateCompressor.compressRawUpdates.called
+					@UpdateCompressor.compressRawUpdates
+						.called.should.not.equal true

 				it "should save the new compressed ops into a pack", ->
 					@PackManager.insertCompressedUpdates


Support UNICHAR

Support UNICHAR

--- src/formula_evaluator.coffee
+++ src/formula_evaluator.coffee
@@ -1023,6 +1023,13 @@

     @ROUNDDOWN(args)

+  UNICHAR: (args)->
+    @checkArgumentSize(args, 1)
+    code = @expectInteger(args[0])
+    if code < 1
+      @error(ERROR_VALUE)
+    String.fromCharCode(code)
+
   WEEKDAY: (args)->
     @checkArgumentSize(args, 1, 2)
     dt = @expectMoment(args[0])


global settings supports depth now

global settings supports depth now

--- components/contextmenu/contextmenutreeviewcontroller.coffee
+++ components/contextmenu/contextmenutreeviewcontroller.coffee
@@ -6,7 +6,7 @@

   uId = 0
   getUId = -> ++uId
-  convertToArray = (items, pId = null)->
+  convertToArray = @convertToArray = (items, pId = null)->
     results = []

     for title, options of items


fix for context menu overflow menu position

--- components/contextmenu/contextmenutreeviewcontroller.coffee
+++ components/contextmenu/contextmenutreeviewcontroller.coffee
@@ -82,7 +82,16 @@
   expand:(nodeView)->

     super
+    @setOverflowExpandedContextMenuPosition nodeView
     @expandedNodes.push nodeView if nodeView.expanded
+
+  setOverflowExpandedContextMenuPosition: (nodeView)->
+
+    if nodeView.data?.children?
+      expandView = @listControllers[nodeView.getData().id].getView()
+      fullViewHeight = expandView.getY() + expandView.getHeight()
+      if fullViewHeight > window.innerHeight
+        expandView.$().css "bottom", 0

   ###
   NODE SELECTION


Refactors the directive example with jasmine-given

--- spec/directives/shows_message_when_hovered_spec.coffee
+++ spec/directives/shows_message_when_hovered_spec.coffee
@@ -1,22 +1,22 @@
-describe "directive: showsMessageWhenHovered", ->
-
-  html = scope = elem = directiveMessage = originalMessage = null
+describe "directive: shows-message-when-hovered (jasmine-given, coffeescript)", ->

   Given -> module("app")

   Given inject ($rootScope, $compile) ->
-    directiveMessage = 'ralph was here'
-    html = "

"
-    scope = $rootScope.$new()
-    scope.message = originalMessage = 'things are looking grim'
-    elem = $compile(html)(scope)
+    @directiveMessage = 'ralph was here'
+    @html = "

"
+    @scope = $rootScope.$new()
+    @scope.message = @originalMessage = 'things are looking grim'
+    @elem = $compile(@html)(@scope)

-  describe "mouseenter shows the message from the markup", ->
-    When -> elem.triggerHandler('mouseenter')
-    Then -> scope.message == directiveMessage
+  describe "when a user mouses over the element", ->
+    When -> @elem.triggerHandler('mouseenter')
+    Then "the message on the scope is set to the message attribute of the element", ->
+      @scope.message == @directiveMessage

-  describe "mouseleave shows the original message from the scope", ->
-    When -> elem.triggerHandler('mouseleave')
-    Then -> scope.message == originalMessage
+  describe "when a users mouse leaves the element", ->
+    When -> @elem.triggerHandler('mouseleave')
+    Then "the message is reset to the original message", ->
+      @scope.message == @originalMessage


fix opening a notification by clicking on .btn-toolbar

--- lib/notifications-log-item.coffee
+++ lib/notifications-log-item.coffee
@@ -30,7 +30,7 @@
     @element.appendChild(notificationElement)
     @element.appendChild(@timestamp)
     @element.addEventListener 'click', (e) =>
-      if not e.target.closest('.btn-toolbar')?
+      if not e.target.closest('.btn-toolbar a, .btn-toolbar button')?
         @emitter.emit 'click'

     @disposables.add new Disposable => @element.remove()


fix(account-switcher): Dark content mode, closes #382

--- internal_packages/account-sidebar/lib/account-switcher.cjsx
+++ internal_packages/account-sidebar/lib/account-switcher.cjsx
@@ -44,7 +44,7 @@

         <RetinaImg className="toggle"
                    name="account-switcher-dropdown.png"
-                   mode={RetinaImg.Mode.ContentPreserve} />
+                   mode={RetinaImg.Mode.ContentDark} />

         {@state.account.emailAddress.trim().toLowerCase()}


Small code clean up.

Small code clean up.

--- app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee
+++ app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee
@@ -322,8 +322,8 @@
       )
     )
     App.Event.trigger 'notify', {
-      type: 'success',
-      msg: App.i18n.translateContent('Bulk-Action executed!'),
+      type: 'success'
+      msg: App.i18n.translateContent('Bulk-Action executed!')
     }

   zoom: (e) =>


remove commented out code

remove commented out code

--- configs/webpack/base.coffee
+++ configs/webpack/base.coffee
@@ -42,12 +42,10 @@
   config = _.pick(loaderConfig, 'test')
   config.use ||= []

-
   if type is 'less'
     config.use = config.use.concat DEV_LOADERS.concat(LOADERS.style, LOADERS.css, LOADERS.less)
   else
-    config.use = loaderConfig.use #DEV_LOADERS # config.use.concat DEV_LOADERS.concat(loaderConfig)
-  console.log config
+    config.use = loaderConfig.use
   config
 )

@@ -70,9 +68,7 @@
       /\/sinon\.js/
     ]
     rules: _.values(STATICS)
-  plugins: [
-#    new webpack.optimize.DedupePlugin()
-  ]
+  plugins: [ ]

 mergeWebpackConfigs = ->
@@ -139,7 +135,6 @@

   # rename to minified
   output.filename = '[name].min.js'
-  # output.path = "#{projectConfig.basePath}/dist"
   {styleFilename} = projectConfig
   styleFilename ?= '[name].min.css'

@@ -184,7 +179,7 @@
     ]
     devServer:
       contentBase: "#{projectConfig.basePath}/"
-#      outputPath: outputPath
+
       publicPath: publicPath
       historyApiFallback: true
       inline: true


fix typo in webpack config

fix typo in webpack config

--- configs/webpack/base.coffee
+++ configs/webpack/base.coffee
@@ -162,7 +162,7 @@
         parallel: true,
         sourceMap: true,
         uglifyOptions:
-          mange: false
+          mangle: false
       )

       # Use the production version of React (no warnings/runtime checks)


Rename Meter module

Rename Meter module

--- src/game_object_meter.coffee
+++ src/game_object_meter.coffee
@@ -177,7 +177,6 @@

       player = GameObject

-
       # creates a health meter
       player.meter 'health'


fix(generator): Fix isSearchable on set attributes - issue #46

--- src/coffee/product-type-generator.coffee
+++ src/coffee/product-type-generator.coffee
@@ -121,7 +121,6 @@
         #   name,description,set_set_gender
         #   women,Woman Product Type,x
         attributeDefinition['isRequired'] = false
-        attributeDefinition['isSearchable'] = false

         if row['type']
           type['elementType'] = {name: @_type(@_typeOrElementType(rawTypeName))}


code cleanup 3

code cleanup 3

--- src/coffee/responsive.breadcrumbs.coffee
+++ src/coffee/responsive.breadcrumbs.coffee
@@ -267,7 +267,7 @@
                         o.opts.onAfterDecompress(_this) if $.isFunction(o.opts.onAfterDecompress)

             # delegate the normalized event for hoverIn to the holder element
-            o.el.on "#{ o.hoverIn }.#{ pluginName }", ".#{ pluginName.toLowerCase() }-holder", (e) ->
+            o.el.on "#{ o.hoverIn }.#{ pluginName }", ".#{ pluginName.toLowerCase() }-holder", ->
                 o.opts.holderAnimation.onBeforeShow(_this) if $.isFunction(o.opts.holderAnimation.onBeforeShow)

                 ps = o.el
@@ -289,7 +289,7 @@
                             o.opts.holderAnimation.onAfterShow(_this) if $.isFunction(o.opts.holderAnimation.onAfterShow)

             # delegate the normalized event for hoverOut to the holder element
-            o.el.on "#{ o.hoverOut }.#{ pluginName }", ".#{ pluginName.toLowerCase() }-holder", (e) ->
+            o.el.on "#{ o.hoverOut }.#{ pluginName }", ".#{ pluginName.toLowerCase() }-holder", ->
                 o.dropdownTimer = window.setTimeout( ->
                     o.dropdownWrapper.trigger "#{ o.hoverOut }.#{ pluginName }"
                 , 500)
@@ -297,7 +297,7 @@
             o.dropdownWrapper.on "#{ o.hoverIn }.#{ pluginName }", ->
                 window.clearTimeout o.dropdownTimer

-            o.dropdownWrapper.on "#{ o.hoverOut }.#{ pluginName }", (e) ->
+            o.dropdownWrapper.on "#{ o.hoverOut }.#{ pluginName }", ->
                 current = $ this

                 o.opts.holderAnimation.onBeforeHide(_this) if $.isFunction(o.opts.holderAnimation.onBeforeHide)


fix bug with answer choice with space in it and conditional logic associated with that answer choice

--- app/assets/javascripts/hanuman/hanuman-conditional-logic.js.coffee
+++ app/assets/javascripts/hanuman/hanuman-conditional-logic.js.coffee
@@ -35,7 +35,7 @@
         else
           if $conditionElement.is(":checkbox")
             # limit binding of each checkbox if data-label-value and answer are the same-kdh
-            $conditionElement = $conditionContainer.find(".form-control[data-label-value=" + condition.answer.replace("/","\\/") + "]")
+            $conditionElement = $conditionContainer.find(".form-control[data-label-value='" + condition.answer.replace("/","\\/") + "']")
             self.bindConditions($conditionElement)
           else
             for element in $conditionElement
@@ -118,7 +118,7 @@

       if $conditionElement.is(":checkbox")# || $triggerElement.is(":radio"))
         # limit binding of each checkbox if data-label-value and answer are the same-kdh
-        $conditionElement = $conditionElement.closest('.form-container-entry-item').find(".form-control[data-label-value=" + condition.answer.replace("/","\\/") + "]")
+        $conditionElement = $conditionElement.closest('.form-container-entry-item').find(".form-control[data-label-value='" + condition.answer.replace("/","\\/") + "']")
       hideQuestions = self.evaluateCondition(condition.operator, condition.answer, self.getValue($conditionElement))
       conditionMet = !hideQuestions
       conditionMetTracker.push conditionMet


auth: code style

auth: code style

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -353,10 +353,10 @@
       exchange  : @presenceExchange
       member    : @resourceName
     }
-    @presence.on 'join', (serviceKey)=>
+    @presence.on 'join', (serviceKey) =>
       try @addService parseServiceKey serviceKey
       catch e then console.error e
-    @presence.on 'leave', (serviceKey)=>
+    @presence.on 'leave', (serviceKey) =>
       try @removeService parseServiceKey serviceKey
       catch e then console.error e
     @presence.listen()
@@ -365,10 +365,6 @@
     { serviceGenericName, serviceUniqueName, routingKey
       correlationName, username } = messageData

-    servicesOfType = @services[serviceGenericName]
-
-    [matchingService] = (service for service in servicesOfType \
-                         when service.serviceUniqueName is serviceUniqueName)
     params = {
       serviceGenericName
       serviceUniqueName
@@ -377,6 +373,11 @@
       username
     }

+    servicesOfType = @services[serviceGenericName]
+
+    [matchingService] = (service for service in servicesOfType \
+                                 when service.serviceUniqueName \
+                                   is serviceUniqueName)
     if matchingService?
       @sendAuthJoin params
     else


Notify PluginsManager for orders

Notify PluginsManager for orders

--- src/actions_manager.coffee
+++ src/actions_manager.coffee
@@ -27,6 +27,7 @@
         addOrder: (data, callback) ->
           clearTimeout @pageview_timeout
           @_reportAction 'ecommerce', 'addOrder', data, -> callback() if callback
+          @plugins_manager.notify('order', data)
         addItem: (data, callback) ->
           clearTimeout @pageview_timeout
           @_reportAction 'ecommerce', 'addItem', data, -> callback() if callback


Subscribe to pusher channel to receive button presses

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -55,6 +55,22 @@
       Team.get {id: $scope.game.team2}, (team) ->
         $scope.team2 = team

+    Pusher.logToConsole = true;
+
+    pusher = new Pusher('cba436e88472fa7154a2', {
+      cluster: 'eu',
+      encrypted: true
+    })
+
+    channel = pusher.subscribe('gameshow')
+    channel.bind 'button', (data) ->
+      if $scope.listening
+        $scope.unlisten()
+        team = if data.id == "1" then $scope.team1 else $scope.team2
+        $scope.$apply () ->
+          $scope.buzzedFirst = team.name
+          $speechRecognition.speak(team.name + '!')
+
     $scope.countdown = 0
     timeout_promise = undefined
     heartbeat = () ->


Improve plot titles

Improve plot titles

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -125,9 +125,9 @@

     $scope.plots = [
-      {id:"statusSummary", title: "Waterpoint status (by % Functional)"},
+      {id:"statusSummary", title: "Waterpoint status (ordered by % Functional)"},
       {id:"percFunLeaders", title: "Leaderboard: Percentage Functional"},
-      {id:"popReach", title: "Leaderboard: % Population Served"}]
+      {id:"popReach", title: "Leaderboard: Percentage of the Population Served"}]

     $scope.groups = ['region', 'lga', 'ward', 'funder', 'source_type',
                      'construction_year', 'quantity_group',


Don't load buffer contents from disk without an isModified key in state

When we deserialize a replicated buffer, we always want to honor the
current contents of state. If the state was produced by a call to
`getState` instead of `serialize`, it won't have an `isModified` key,
so we should not load from disk.

--- src/app/text-buffer.coffee
+++ src/app/text-buffer.coffee
@@ -36,31 +36,29 @@
       {@project} = params
       @state = optionsOrState
       @id = @state.get('id')
-      wasModified = @state.get('isModified')
       filePath = @state.get('relativePath')
       @text = @state.get('text')
+      reloadFromDisk = @state.get('isModified') is false
     else
       {@project, filePath, initialText} = optionsOrState
       @text = site.createDocument(initialText ? '', shareStrings: true)
+      reloadFromDisk = true
       @id = guid.create().toString()
       @state = site.createDocument
         id: @id
         deserializer: @constructor.name
         version: @constructor.version
-
-    @state.set('text', @text)
+        text: @text
+
     @text.on 'changed', @handleTextChange
     @text.on 'marker-created', (marker) => @trigger 'marker-created', marker
     @text.on 'markers-updated', => @trigger 'markers-updated'

     if filePath
       @setPath(@project.resolve(filePath))
-      @updateCachedDiskContents()
-
-      unless wasModified
-        @reload() if @isModified() and fsUtils.exists(@getPath())
-    else
-      @text ?= site.createDocument('', shareStrings: true)
+      if fsUtils.exists(@getPath())
+        @updateCachedDiskContents()
+        @reload() if reloadFromDisk and @isModified()

   ### Internal ###


docs(main): documented modelize code

docs(main): documented modelize code

--- src/model.coffee
+++ src/model.coffee
@@ -1,3 +1,10 @@
+'use strict'
+
+# The modelize main function
+# Returns a modelize object
+#
+# @param [Object] options The main configuration object for modelize
+#
 Modelize = (options) ->
   api.add options.api unless api[options.api]?
   mapi = api[options.api]
@@ -6,35 +13,90 @@

   add_apis_to options.has_many, options.api if options.has_many?

-  # Enhance data!
+  # Provide the constructor function for the model object.
+  # Enriches the optionally provided object with model functions.
+  #
+  # @param [Object] self prepopulated data object
+  #
   model = (self = {}) ->
+    # Return api object
+    #
     self.api = -> mapi
+    # Provide all required relationship fields
+    # externalkey (modelname_id), modelplural for API, modelname, modelfunction
+    #
     self.relationship_fields = (name, model) -> [name + '_id', name + 's', name, window[model]]

+    # Check if de/encryption functions should be included
+    #
+    if options.encrypted_container?
+      Encryptable self, options.encrypted_container
+
+    # Include sortable functions
     if options.sortable? && options.sortable == true
       Sortable self
+
+    # Include selectable functions
     if options.selectable? && options.selectable == true
       Selectable self

     # Set relations
-    # Lonely
+    # API wise has_one and belongs_to are the same, so we merge them
+    #
     if options.belongs_to?
       unless options.has_one?
         options.has_one = []
       options.has_one = Ham.merge options.belongs_to, options.has_one

+    # Single relations
+    # Generated methods:
+    #
+    # @method #relationname_get(params, callbackOrObservable)
+    #   Get relation data from given model
+    # @method #relationname()
+    #   The observable, which is also a lazy get function
+    #   if there's no predefined content in the given object
+    #
     if options.has_one?
       for name, data of options.has_one
         Ham.merge data,
           model: name.capitalize()

+        relation_params = self.relationship_fields name, data.model
+        relation_params.push options
+
         fn = window[data.model]
+
+        ###if typeof self[name + '_id'] != 'function'
+          unless options.editable?
+            options.editable = []
+
+          #options.editable.push name + '_id'
+        ###
+
         if self[name]?
           Observable self, name, new fn(self[name])
         else
           LazyObservable self, name, lazy_single_get_fn, self.relationship_fields(name, data.model)

-    # Harem relations
+        self[name + '_get'] = single_get_fn.apply self, relation_params
+
+    # Multiple relations
+    # Generated methods:
+    #
+    # @method #relationnames()
+    #   ObservableArray containing all relation data.
+    #   Is also a lazy get function if there's no predefined
+    #   content in the given object
+    # @method #relationname_get(params, callbackOrObservable)
+    #   Get relation data
+    # @method #relationname_add(params, callback)
+    #   Add new model instance to relationship
+    # @method #relationname_update(id, params, callback)
+    #   Update model instance
+    # @method #relationname_destroy(id, callback)
+    #   Delete model instance
+    #
     if options.has_many?
       for name, data of options.has_many
         Ham.merge data,
@@ -44,6 +106,7 @@
         relation_params.push options

         # Get
+        #
         if self[name + 's']?
           fn = window[data.model]

@@ -57,56 +120,111 @@
         self[name + '_get'] = get_fn.apply self, relation_params

         # Create
+        #
         self[name + '_add'] = create_fn.apply self, relation_params

         # Update
+        #
         self[name + '_update'] = update_fn.apply self, relation_params

         # Destroy
+        #
         self[name + '_destroy'] = destroy_fn.apply self, relation_params

     # Set observable fields
+    #
     if options.observable?
       for index, name of options.observable
         Observable self, name

     # Set editable fields
+    #
     if options.editable?
       for index, name of options.editable
-        Editable self, name, (value, prop, delay = 250) =>
-          window.timeoutEditor = {} unless window.timeoutEditor?
-          clearTimeout(window.timeoutEditor[prop])
-          window.timeoutEditor[prop] = setTimeout(=>
-            edit_value = {}
-            edit_value[prop] = value
-            self.update edit_value
-          , delay)
+        Editable self, name, DelayedSave.apply(self, [options, self])
+
+    # Encrypted editable fields
+    #
+    if options.encrypted_editable?
+      for index, name of options.encrypted_editable
+        Editable self, name, EncryptedDelayedSave.apply(self, [options, self])

     # Set computed fields
+    #
     if options.computed?
       for name, fn of options.computed
         Computed self, name, fn
-
+    if options.purecomputed?
+      for name, fn of options.purecomputed
+        PureComputed self, name, fn
+    if options.computed_array?
+      for name, fn of options.computed_array
+        ComputedArray self, name, fn
+
+    # Add user defined functions
+    #
     if options.functions?
       for name, fn of options.functions
         self[name] = fn

-    self.update = (data, callback) =>
-      self.api().update(self.id, data).done callback
-
+    # Update self
+    #
+    self.update = (params, callback) =>
+      unless self.id?
+        console.error 'Trying to update nonexisting model object'
+        return false
+
+      self.api().update(self.id, params).done callback
+
+    # Destroy self
+    #
     self.destroy = (callback) =>
+      unless self.id?
+        console.error 'Trying to delete nonexisting model object'
+        return false
+
       self.api().destroy(self.id).done callback

+    # Save nonexisting instance of self against API and set self.id
+    #
+    self.create = (callback) =>
+      mapi.create(self.export()).done (data) =>
+        self.id = data.id
+
+    # Export all model data as an array
+    #
+    self.export = =>
+      data = {}
+      if options.editable?
+        for index, name of options.editable
+          data[name] = self[name]()
+
+      if options.encrypted_editable?
+        for index, name of options.encrypted_editable
+          data[name] = self[name]()
+
+      if options.belongs_to?
+        for name, has_data of options.belongs_to
+          data[name + '_id'] = self[name + '_id']
+
+      return data
+
     return self

-  model.get = (params, callbackOrObservable) ->
+  ###
+  # These are the collector functions and are not bound to a specific object but to the model
+  ###
+
+  # Get objects from API and save into specified observable or callback
+  #
+  model.get = (params = {}, callbackOrObservable) ->
     callback = (data) ->
       for m in data
         callbackOrObservable.push model(m)

     if typeof callbackOrObservable['push'] == 'undefined'
       if typeof callbackOrObservable != 'function'
-        console.log 'Collection.find 2nd parameter needs to be either a function or a pushable object (Array, ObservableArray).\nGiven:'
+        console.log 'model.get 2nd parameter needs to be either a function or a pushable object (Array, ObservableArray).\nGiven:'
         console.log callbackOrObservable

       callback = callbackOrObservable
@@ -116,29 +234,55 @@
     else
       mapi.read(params).done callback

-  model.getOne = (observable, id) ->
-    model.get observable, { id: id }
-
+  # Shorthand for model.get, just provide a single ID
+  #
+  model.getOne = (id, observable) ->
+    model.get { id: id }, observable
+
+  # Create new object with given params
+  #
   model.create = (params = {}, callbackOrObservable) ->
     callback = (data) ->
       callbackOrObservable.push model(data)

     if typeof callbackOrObservable['push'] == 'undefined'
       if typeof callbackOrObservable != 'function'
-        console.log 'Collection.find 2nd parameter needs to be either a function or a pushable object (Array, ObservableArray).\nGiven:'
+        console.log 'model.create 2nd parameter needs to be either a function or a pushable object (Array, ObservableArray).\nGiven:'
         console.log callbackOrObservable

-      callback = callbackOrObservable
+      callback = (data) =>
+        # Copy object hack
+        m = model(JSON.parse(JSON.stringify(data)))
+
+        if m.after_create?
+          m.after_create()
+
+        callbackOrObservable(data)

     if params.length > 0 || typeof params == 'object'
+      if model.encrypted_container?
+        params = model.encrypt_container(params)
+
       mapi.create(params).done callback
     else
       mapi.create().done callback

-  return model
-
-ko.bindingHandlers.sortable.afterMove = (element) ->
-  if element.item.move_ui?
-    element.item.move_ui element
-
-#module.exports = Modelize+  if options.encrypted_container?
+    model.encrypted_container = options.encrypted_container
+
+  if options.encrypted_editable?
+    model.encrypted_editable = options.encrypted_editable
+
+  # Debug function
+  model.encrypt_container = (data, container) ->
+    data[model.encrypted_container] = {}
+    for index, value of data
+      if index in model.encrypted_editable
+        data[model.encrypted_container][index] = value
+
+    encdata = encryptData(sessionStorage.getItem('appKey'), JSON.stringify(data[model.encrypted_container]))
+    data[model.encrypted_container] = encdata
+
+    return data
+
+  return model


Be robust against multiple Token.remove()s

--- src/model.coffee
+++ src/model.coffee
@@ -430,7 +430,7 @@
     # linked list.
     remove: ->
       if @prev? then @prev.append @next
-      else @next.prev = null
+      else if @next? then @next.prev = null

       @prev = @next = @parent = null


Workspace: Separated constructor logic into methods for TerminalPane to override these methods at child classes.

--- client/landing/app/CommonViews/workspace/panes/terminalpane.coffee
+++ client/landing/app/CommonViews/workspace/panes/terminalpane.coffee
@@ -6,17 +6,20 @@

     super options, data

+    @createWebTermView()
+    @webterm.on "WebTermConnected", (@remote) => @onWebTermConnected()
+
+    @on "PaneResized", @bound "forceResize"
+
+  createWebTermView: ->
     @webterm           = new WebTermView
       delegate         : @
       cssClass         : "webterm"
       advancedSettings : no

-    @webterm.on "WebTermConnected", (@remote)=>
-      {command} = @getProperties()
-      @runCommand command if command
-      @webterm.terminal.setScrollbackLimit 50
-
-    @on "PaneResized", @bound "forceResize"
+  onWebTermConnected: ->
+    {command} = @getProperties()
+    @runCommand command if command

   runCommand: (command) ->
     return unless command


finder settings button is moved to another branch

--- client/landing/app/MainApp/sidebar/findersettingsbuttonview.coffee
+++ client/landing/app/MainApp/sidebar/findersettingsbuttonview.coffee
@@ -1,49 +0,0 @@
-class FinderSettingsButtonView extends KDButtonView
-
-  constructor:(options={}, data)->
-    options.title or= "General Settings"
-    options.icon  or= yes
-    options.iconOnly or= yes
-
-    super options, data
-
-    @appStorage = KD.getSingleton('appStorageController').storage 'Finder', '1.0'
-    @appStorage.fetchStorage()
-
-  click:->
-    @switch?.destroy()
-    @switch = new HideDotFilesSwitch {}, {appStorage:@appStorage}
-    @switch.on "dotFileStateChanged", (state) => @emit "dotFileStateChanged", state
-    @switch.toggle.setValue @appStorage.getValue "hideDotFiles"
-    offset = @$().offset()
-    contextMenu = new JContextMenu
-      menuWidth   : 200
-      x           : offset.left + 40
-      y           : offset.top  - 19
-      arrow       :
-        placement : "left"
-        margin    : 10
-      lazyLoad    : yes
-    ,
-      customView1 : @switch
-
-
-class HideDotFilesSwitch extends JView
-
-  constructor:(options={}, data)->
-    super options, data
-
-    @appStorage = data.appStorage
-
-    @toggle = new KDOnOffSwitch
-      size     : "tiny"
-      callback : @bound "hideFiles"
-
-  hideFiles:(state)->
-    @appStorage.fetchStorage (storage)=>
-      return  unless storage
-      @appStorage.setValue "hideDotFiles", state
-      @emit "dotFileStateChanged", state
-
-  pistachio:->
-    """Hide Dot Files {{> @toggle}}"""


TAP5-2420: Confirm mixin has issues related to keyboard navigation

--- tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/confirm-click.coffee
+++ tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/confirm-click.coffee
@@ -1,17 +1,3 @@
-# Copyright 2013 The Apache Software Foundation
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http:#www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
 # ## t5/core/confirm-click
 #
 # Support for the Tapestry Confirm mixin, and for running confirmation dialogs programatically.
@@ -35,7 +21,7 @@
       confirmed = false

       content = """
-

+

@@ -65,6 +51,10 @@
           options.ok()

       $dialog.appendTo $ "body"
+
+      # Focus on the first button (the "OK") button.
+      $dialog.on "shown.bs.modal", ->
+        $dialog.find(".modal-footer .btn").first().focus()

     # Support for the Confirm mixin
     $("body").on "click", "[data-confirm-message]:not(.disabled)", ->


Better, safer endpoints objects

Better, safer endpoints objects

--- endpoint.coffee
+++ endpoint.coffee
@@ -8,4 +8,4 @@
       to: @endpoint
       body: body
     ,
-      callback).get()+      callback).get?()


First pass at encoding changes & comments in ranges

--- services/real-time/app/coffee/WebsocketController.coffee
+++ services/real-time/app/coffee/WebsocketController.coffee
@@ -101,6 +101,30 @@
 							logger.err {err, project_id, doc_id, fromVersion, line, client_id: client.id}, "error encoding line uri component"
 							return callback(err)
 						escapedLines.push line
+
+					if ranges.comments
+						escapedComments = []
+						for comment in ranges.comments
+							try
+								comment.op.c = unescape(encodeURIComponent(comment.op.c))
+							catch err
+								logger.err {err, project_id, doc_id, fromVersion, comment, client_id: client.id}, "error encoding comment uri component"
+								return callback(err)
+							escapedComments.push comment
+						ranges.comments = escapedComments
+
+					if ranges.changes
+						escapedChanges = []
+						for change in ranges.changes
+							try
+								change.op.i = unescape(encodeURIComponent(change.op.i)) if change.op.i
+								change.op.d = unescape(encodeURIComponent(change.op.d)) if change.op.d
+							catch err
+								logger.err {err, project_id, doc_id, fromVersion, change, client_id: client.id}, "error encoding change uri component"
+								return callback(err)
+							escapedChanges.push change
+						ranges.changes = escapedChanges
+
 					AuthorizationManager.addAccessToDoc client, doc_id
 					client.join(doc_id)
 					callback null, escapedLines, version, ops, ranges


compiling code nodes on demand

compiling code nodes on demand

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -23,13 +23,17 @@
 NEGATE  = -> @negated = not @negated; this

 class Type
-  constructor: (@basic, @args=[], @returnType) ->
+  constructor: (@basic, @args, @returnType) ->
     @synonym = this
-    if @basic is "func" and not @returnType?
-      @returnType = new Type
+    @codeNodes = []
   findRoot: ->
     return this if this is @synonym
     return @synonym = @synonym.findRoot()
+  linkTo: (otherType) ->
+    throw "assertion failed. everything's broken" unless @synonym is this and otherType.synonym is otherType
+    @synonym = otherType
+    # combine our lists of code nodes
+    otherType.codeNodes = otherType.codeNodes?.concat @codeNodes
 voidType = new Type "void"
 intType = new Type "int"
 stringType = new Type "string"
@@ -42,13 +46,13 @@
   return if leftType is rightType
   # if one of them is totally unknown, it's safe to link them.
   unless leftType.basic?
-    return leftType.synonym = rightType
+    return leftType.linkTo rightType
   unless rightType.basic?
-    return rightType.synonym = leftType
+    return rightType.linkTo leftType
   unless leftType.basic is rightType.basic
     throw TypeError "incompatible types #{leftType.basic} and #{rightType.basic}"
   unless leftType.basic is "func"
-    return leftType.synonym = rightType
+    return leftType.linkTo rightType
   unless leftType.args.length is rightType.args.length
     throw TypeError "number of arguments must match"
   for leftArg, i in leftType.args
@@ -56,7 +60,7 @@
     linkTypes leftArg, rightArg
   linkTypes leftType.returnType, rightType.returnType
   # seems legit
-  leftType.synonym = rightType
+  leftType.linkTo rightType

 class Variable
   constructor: (@label, @type, @namespace) ->
@@ -66,29 +70,29 @@
 class Namespace
   constructor: (@parent)->
     @names = {}
-    if @parent?
-      @root = @parent.root
-    else
+    unless @parent?
       @root = this
       @extraAsm = []
       labelCount = 0
       @nextLabel = -> "_#{labelCount++}"
+    else
+      @root = @parent.root
+      @extraAsm = @root.extraAsm
+      @nextLabel = @root.nextLabel
   find: (name) ->
     @names[name] or @parent?.find name
   createVariable: (name, type) ->
     variable = new Variable "_"+name, type, this
     @names[name] = variable
-    @root.extraAsm.push variable
+    @extraAsm.push variable
     variable
   createLiteralString: (value) ->
-    label = @root.nextLabel()
+    label = @nextLabel()
     asm = ":#{label} dat #{value.length}, #{JSON.stringify value}"
-    @root.extraAsm.push {asm}
+    @extraAsm.push {asm}
     label
   createFunc: (codeNode) ->
-    func = new Func @root.nextLabel(), codeNode
-    @root.extraAsm.push func
-    func
+    new Func @nextLabel(), codeNode
   createSubNamespace: ->
     new Namespace this

@@ -385,18 +389,21 @@
     funcName = @variable.unwrapAll().value
     funcVariable = o.namespace.find funcName
     throw SyntaxError "undefined variable \"#{funcName}\"" unless funcVariable?
-    unless funcVariable.type.basic is "func"
-      throw SyntaxError "can't call variable \"#{funcName}\" of type \"#{funcVariable.type.string}\""
-    unless funcVariable.type.args.length is @argNodes.length
-      throw SyntaxError "function \"#{funcName}\" takes #{funcVariable.type.args.length} argument, not #{@argNodes.length}"
+    funcType = funcVariable.type.findRoot()
+    unless funcType.basic is "func"
+      throw SyntaxError "can't call variable \"#{funcName}\" of type \"#{funcType.string}\""
+    unless funcType.args.length is @argNodes.length
+      throw SyntaxError "function \"#{funcName}\" takes #{funcType.args.length} argument, not #{@argNodes.length}"
     args = []
     for argNode in @argNodes
       arg = argNode.unwrapAll().compileExpression o
       forbidVoid arg
       args.push arg
-    callType = new Type "func", (arg.type for arg in args)
-    linkTypes funcVariable.type, callType
-
+    callType = new Type "func", (arg.type for arg in args), new Type
+    linkTypes funcType, callType
+    # good news, guys: you're parameter types are now known!
+    for codeNode in callType.findRoot().codeNodes
+      codeNode.compileFunc o
     codes = ("set push, #{arg.access}" for arg in args)
     codes.push "jsr #{funcVariable.label}"
     return codes.join("\n")
@@ -592,15 +599,17 @@

   compileExpression: (o) ->
     @func = o.namespace.createFunc this
-    {type: new Type("func", (new Type for _ in @params)), access: @func.label}
+    @func.type = new Type "func", (new Type for _ in @params), new Type
+    @func.type.codeNodes.push this
+    {type: @func.type, access: @func.label}

   compileFunc: (o) ->
     o = {namespace: o.namespace.createSubNamespace()}
     code = @body.compileStatement o
     # TODO figure returning stuff
-    @func.type.returnType = voidType
+    linkTypes @func.type.findRoot().returnType, voidType
     returnCode = "set pc, pop"
-    ":#{@func.label}\n#{code}\n#{returnCode}"
+    o.namespace.extraAsm.push {asm: ":#{@func.label}\n#{code}\n#{returnCode}"}

   children: ['params', 'body']


Use Dynamic for if statements

Use Dynamic for if statements

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -87,6 +87,18 @@
       subModel = get(model, ast.arguments[0])
       if subModel
         collection.update([subModel])
+      else
+        collection.update([])
+    update()
+    model.bind? "change:#{ast.arguments[0]}", update
+    new Dynamic(ast, collection, model, controller)
+
+  @if: (ast, model, controller) ->
+    collection = new Serenade.Collection([])
+    update = ->
+      value = get(model, ast.arguments[0])
+      if value
+        collection.update([model])
       else
         collection.update([])
     update()
@@ -222,40 +234,6 @@

   get: -> format(@model, @ast.value, @ast.bound)

-class If
-  constructor: (@ast, @model, @controller) ->
-    @anchor = Serenade.document.createTextNode('')
-    @model.bind? "change:#{@ast.arguments[0]}", @build
-
-  build: =>
-    if get(@model, @ast.arguments[0])
-      @nodes ||= (Nodes.compile(child, @model, @controller) for child in @ast.children)
-      node.insertAfter(@nodes[i-1]?.lastElement() or @anchor) for node, i in @nodes
-    else
-      @removeNodes()
-
-  append: (inside) ->
-    inside.appendChild(@anchor)
-    @build()
-
-  insertAfter: (after) ->
-    after.parentNode.insertBefore(@anchor, after.nextSibling)
-    @build()
-
-  remove: ->
-    @removeNodes()
-    @anchor.parentNode.removeChild(@anchor)
-
-  removeNodes: ->
-    node.remove() for node in @nodes if @nodes
-    @nodes = undefined
-
-  lastElement: ->
-    if @nodes?.length
-      @nodes[@nodes.length - 1].lastElement()
-    else
-      @anchor
-
 Nodes =
   compile: (ast, model, controller) ->
     switch ast.type
@@ -265,7 +243,7 @@
         switch ast.command
           when "view" then new Node.view(ast, model, controller)
           when "collection" then new Dynamic.collection(ast, model, controller)
-          when "if" then new If(ast, model, controller)
+          when "if" then new Dynamic.if(ast, model, controller)
           when "in" then new Dynamic.in(ast, model, controller)
           else new Node.helper(ast, model, controller)
       else throw SyntaxError "unknown type '#{ast.type}'"