fix total complexity calculation

fix total complexity calculation

--- src/chuck.coffee
+++ src/chuck.coffee
@@ -325,7 +325,7 @@
   metrics.lines = cls.position.last_line+1 # due to a bug in the ascent parser position information
   metrics.linesPerMethod = metrics.lines / metrics.methodCount
   metrics.statements = sum(m.statements for n, m of metrics.methods)
-  metrics.complexity = sum(m.complexity for n, m of metrics.methods)
+  metrics.complexity = sum(m.complexity for n, m of metrics.methods) - metrics.methodCount + 1
   metrics.complexityPerMethod = metrics.complexity / metrics.methodCount
   metrics.complexityPerLine = metrics.complexity / metrics.lines
   metrics.halstead = calculateHalstead combineHalsteads hals


добавлен тест проверки стандартных плагинов

--- test/test-builder.coffee
+++ test/test-builder.coffee
@@ -123,6 +123,13 @@
       assert.include(builder.types, 'test')
       assert.include(builder.types, 'test2')
       assert.include(builder.types, 'test3')
+  'load standard plugins':
+    topic: -> new Builder
+      configFiles: [ join(WORK_DIR,'config-3.nproj') ]
+      plugins: [ '../../plugins' ]
+    'check plugins load': (builder) ->
+      assert.include(builder.types, 'copy')
+      assert.include(builder.types, 'remove')
   # 'detect environment':
   # 'state':
   # 'async lock':


Renamed command handler parameter from 'event' to 'command'

--- tests/server_module.integration.coffee
+++ tests/server_module.integration.coffee
@@ -155,9 +155,9 @@
     repository: 'Space.cqrs.Repository'
     registrations: 'CustomerRegistrations'

-  @handle CustomerApp.RegisterCustomer, on: (event) ->
-
-    registration = new CustomerRegistration event.registrationId, event
+  @handle CustomerApp.RegisterCustomer, on: (command) ->
+
+    registration = new CustomerRegistration command.registrationId, command
     @repository.save registration, registration.getVersion()

   @handle CustomerApp.CustomerCreated, on: (event) ->


add basic eval_outputs testing

add basic eval_outputs testing

--- src/smc-hub/test/postgres-user-queries-syncstring-eval.coffee
+++ src/smc-hub/test/postgres-user-queries-syncstring-eval.coffee
@@ -166,3 +166,158 @@
                 expect(err).toEqual("string_id (='null') must be a string of length 40")
                 done()

+    it 'verifies that user_id must be nonnegative', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {eval_inputs:{string_id:string_id, time:t1, user_id:-1, input:input}}
+            cb    : (err) ->
+                expect(err).toEqual('postgresql error: new row for relation "eval_inputs" violates check constraint "eval_inputs_user_id_check"')
+                done()
+
+# NOTE: this is very similar to eval_inputs above.
+describe 'basic use of eval_outputs table --', ->
+    before(setup)
+    after(teardown)
+
+    accounts = projects = string_id = undefined
+    path = 'a.txt'
+    it 'creates 3 accounts', (done) ->
+        create_accounts 3, (err, x) -> accounts=x; done(err)
+    it 'creates 2 projects', (done) ->
+        create_projects 2, accounts[0], (err, x) -> projects=x; done(err)
+
+    t0 = new Date()
+    output = {stdout:"hello world", done:true}
+
+    it 'creates a valid syncstring', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {syncstrings:{project_id:projects[0], path:'a.sagews', users:accounts}}
+            cb    : done
+
+    it 'gets the string_id', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {syncstrings:{project_id:projects[0], path:'a.sagews', string_id:null}}
+            cb    : (err, result) ->
+                string_id = result?.syncstrings.string_id
+                done(err)
+
+    it 'verifies anonymous set queries are not allowed', (done) ->
+        db.user_query
+            query : {eval_outputs:{string_id:string_id, time:t0, number:0, output:output}}
+            cb    : (err) ->
+                expect(err).toEqual("no anonymous set queries")
+                done()
+
+    it 'verifies anonymous get queries are not allowed', (done) ->
+        db.user_query
+            query : {eval_outputs:[{string_id:string_id, time:null, number:null, output:null}]}
+            cb    : (err) ->
+                expect(err).toEqual("anonymous get queries not allowed for table 'eval_outputs'")
+                done()
+
+    it 'verifies set query by user not on syncstring is not allowed', (done) ->
+        db.user_query
+            account_id : accounts[2]
+            query : {eval_outputs:{string_id:string_id, time:t0, number:0, output:output}}
+            cb    : (err) ->
+                expect(err).toEqual("user must be an admin")
+                done()
+
+    it 'verifies get query by user not on project not allowed', (done) ->
+        db.user_query
+            account_id : accounts[2]
+            query : {eval_outputs:[{string_id:string_id, time:null, number:null, output:null}]}
+            cb    : (err) ->
+                expect(err).toEqual("user must be an admin")
+                done()
+
+    it 'make that user an admin', (done) ->
+        db.make_user_admin(account_id:accounts[2], cb:done)
+
+    test_write_and_read = (account_id, project_id, cb) ->
+        async.series([
+            (cb) ->
+                # deletes records
+                db._query
+                    query : "DELETE FROM eval_outputs"
+                    cb    : cb
+            (cb) ->
+                db.user_query
+                    account_id : account_id
+                    project_id : project_id
+                    query : {eval_outputs:{string_id:string_id, time:t0, number:0, output:output}}
+                    cb    : cb
+            (cb) ->
+                db.user_query
+                    account_id : account_id
+                    project_id : project_id
+                    query : {eval_outputs:[{string_id:string_id, time:null, number:null, output:null}]}
+                    cb    : (err, x) ->
+                        expect(x).toEqual({ eval_outputs: [ { output: output, string_id: string_id, time: t0, number: 0 } ] })
+                        cb(err)
+        ], cb)
+
+    it 'verifies set/get by admin user', (done) ->
+        test_write_and_read(accounts[2], undefined, done)
+
+    it 'verifies set/get FAILS by user who is listed on syncstring, but is actually not on project', (done) ->
+        test_write_and_read accounts[1], undefined, (err) ->
+            expect(err).toEqual('user must be an admin')
+            done()
+
+    it 'adds other user to project', (done) ->
+        db.add_user_to_project(account_id:accounts[1], project_id:projects[0], cb:done)
+
+    it 'verifies set/get succeeds by other user who is listed on syncstring and is now on project', (done) ->
+        test_write_and_read(accounts[1], undefined, done)
+
+    it 'verifies set/get by other project fails', (done) ->
+        test_write_and_read undefined, projects[1], (err) ->
+            expect(err).toEqual('project not allowed to write to syncstring in different project')
+            done()
+
+    # one that succeeds should be done last, since this is used below.
+    it 'verifies set/get by user of syncstring', (done) ->
+        test_write_and_read(accounts[0], undefined, done)
+
+    t1 = misc.hours_ago(5)
+    it 'writes an old eval_outputs', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {eval_outputs:{string_id:string_id, time:t1, number:0, output:output}}
+            cb    : done
+
+    it 'queries for eval_outputs newer than some time', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {eval_outputs:[{string_id:string_id, time:{'>=':misc.hours_ago(4)}, number:null, output:null}]}
+            cb    : (err, x) ->
+                expect(x).toEqual({ eval_outputs: [ { output: output, string_id: string_id, time: t0, number: 0 } ] })
+                done(err)
+
+    it 'queries for eval_outputs older than some time', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {eval_outputs:[{string_id:string_id, time:{'<=':misc.hours_ago(4)}, number:null, output:null}]}
+            cb    : (err, x) ->
+                expect(x).toEqual({ eval_outputs: [ { output: output, string_id: string_id, time: t1, number: 0 } ] })
+                done(err)
+
+    it 'checks that string_id must be given', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {eval_outputs:[{string_id:null, time:null, number:null, output:null}]}
+            cb    : (err, x) ->
+                expect(err).toEqual("string_id (='null') must be a string of length 40")
+                done()
+
+    it 'verifies that number must be nonnegative', (done) ->
+        db.user_query
+            account_id : accounts[0]
+            query : {eval_outputs:{string_id:string_id, time:t1, number:-1, output:output}}
+            cb    : (err) ->
+                expect(err).toEqual('postgresql error: new row for relation "eval_outputs" violates check constraint "eval_outputs_number_check"')
+                done()
+


nodes: quit exporting Node

nodes: quit exporting Node

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -15,7 +15,7 @@
 # the environment from higher in the tree (such as if a returned value is
 # being requested by the surrounding function), information about the current
 # scope, and indentation level.
-class exports.Node
+class Node
   # Common logic for determining whether to wrap this node in a closure before
   # compiling it, or to compile directly. We need to wrap if this node is a
   # *statement*, and it's not a *pureStatement*, and we're not at


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -3,10 +3,10 @@
     @accessor 'data'

     ready: ->
-        #
+        @oldPieData = ''

     onData: (data) ->
-        @oldPieData = if @oldPieData then @oldPieData else ''
+        @oldPieData = @oldPieData ? ''

         #$(@node).fadeOut().fadeIn()
         @container = $(@node).parent()


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -1,7 +1,9 @@
 class Dashing.FullpieAgent extends Dashing.Widget

     @accessor 'data'
-    @oldPieData = ''
+
+    constructor: ({@oldPieData}) ->
+        @oldPieData = ''

     onData: (data) ->
         #$(@node).fadeOut().fadeIn()


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -305,7 +305,7 @@
                     return arc(b)
             )
             .remove()
-
+###
         labels = labelGroup.selectAll("text")
             .data([data].sort((p1,p2) -> return p1.startAngle - p2.startAngle))
         labels.enter()
@@ -426,6 +426,7 @@
                 return "M" + (d.r-2) + "," + d.b + "L" + (d.l+2) + "," + d.b + " " + d.cx + "," + d.cy

         )
+###
   renderX5: (data) ->
         #console.log("update pie", data);


adding test

adding test

--- test/vibrant.spec.coffee
+++ test/vibrant.spec.coffee
@@ -64,6 +64,14 @@
     LightVibrant: "#d07ec8"
     LightMuted:   null

+urlSwatch =
+  Vibrant: "#9b603b"
+  Muted: "#725147"
+  DarkVibrant: "#763a14"
+  DarkMuted: "#373745"
+  LightVibrant: "#bd8764"
+
+
 expect = require('chai').expect
 path = require('path')
 Vibrant = require('../')
@@ -86,3 +94,13 @@

     for i in [1..4]
       makeTest i
+  describe "usinng url", ->
+    url = 'http://i.imgur.com/xIPgZNM.jpg'
+    it url, (done) ->
+      v = new Vibrant url
+      v.getSwatches (err, actual) ->
+        if (err?) then throw err
+        for name, value of urlSwatch
+          expect(actual).to.have.property name
+          expect(actual[name]?.getHex()).to.equal value?.toLowerCase(), "wrong #{name} color"
+        done()


More fixes for #166.

More fixes for #166.

--- src/brunch.coffee
+++ src/brunch.coffee
@@ -160,8 +160,8 @@
         return helpers.logError error if error?
         helpers.log "Generated #{testFilePath}"
         callback()
-    path.exists testDirPath, (exists) ->
-      return write() if exists?
+    path.exists testDirPath, (error) ->
+      return write() unless error?
       mkdirp testDirPath, 0755, (error) ->
         return helpers.logError if error?
         write()


test(macModal): Cleaned up unused service

--- test/unit/modal.spec.coffee
+++ test/unit/modal.spec.coffee
@@ -2,7 +2,6 @@
   $animate       = null
   $compile       = null
   $rootScope     = null
-  $timeout = null
   modal          = null
   keys           = null

@@ -14,14 +13,12 @@
     _$animate_
     _$compile_
     _$rootScope_
-    _$timeout_
     _keys_
     _modal_
   ) ->
     $animate   = _$animate_
     $compile   = _$compile_
     $rootScope = _$rootScope_
-    $timeout = _$timeout_
     keys       = _keys_
     modal      = _modal_


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -41,6 +41,7 @@
     )

     I.scroll.x = I.scroll.x.clamp(I.cameraBounds.left, I.cameraBounds.right - I.screen.width)
+    I.

   followTypes =
     centered: (object) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -132,6 +132,3 @@
   self.include(Camera.Fade)

   return self
-
-
-Object.extend(Camera, oldCamera)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -411,7 +411,7 @@

     yellow = Color(255, 255, 0)

-    gray = color.grayscale()
+    gray = yellow.grayscale()

     # to see what they look like
     for color, index in ['yellow', 'gray']


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -539,7 +539,7 @@
     ###*
     A copy of the calling color with its hue shifted by `degrees`. This differs from the hue setter in that it adds to the existing hue value and will wrap around 0 and 360.

-

+

     magenta = Color(255, 0, 255)

     magenta.hue()
@@ -552,8 +552,14 @@
     yellow.hue()
     # => 60

-    yellow.toString()
-    # => 'rgba(255, 255, 0, 1)'
+    # to see what they look like
+    for color, index in ['magenta', 'yellow']
+      canvas.drawRect
+        color: eval(color)
+        x: 20 + (60 * index)
+        y: 20 + (60 * index)
+        width: 60
+        height: 60

     @name shiftHue


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -180,10 +180,7 @@
       return this

     saturate: (amount) ->
-      hsl = @toHsl()
-      hsl[1] += amount
-
-      return Color2(hslToRgb(hsl))
+      @copy().saturate$(amount)

     saturate$: (amount) ->
       hsl = @toHsl()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -27,7 +27,9 @@

       when 6, 8
         if hexString.length == 8
-          alpha = (parseInt(hexString.substr(6, 2), 16) / 255.0) || 1.0
+          alpha = (parseInt(hexString.substr(6, 2), 16) / 255.0)
+        else
+          alpha = 1.0

         rgb = (parseInt(hexString.substr(2 * i, 2), 16) for i in [0..2])
         rgb.push(alpha)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -139,8 +139,10 @@
       "##{hexFromNumber(@r)}#{hexFromNumber(@g)}#{hexFromNumber(@b)}"

     toHsl: ->
-      min = Math.min(@r, @g, @b)
-      max = Math.max(@r, @g, @b)
+      [r, g, b] = (channel / 255.0 for channel in [@r, @g, @b])
+
+      min = Math.min(r, g, b)
+      max = Math.max(r, g, b)

       hue = saturation = lightness = (max + min) / 2.0

@@ -151,9 +153,9 @@
         saturation = (if lightness > 0.5 then delta / (2 - max - min) else delta / (max + min))

         switch max
-          when @r then hue = (@g - @b) / delta + (if @g < @b then 6 else 0)
-          when @g then hue = (@b - @r) / delta + 2
-          when @b then hue = (@r - @g) / delta + 4
+          when r then hue = (g - b) / delta + (if g < b then 6 else 0)
+          when g then hue = (b - r) / delta + 2
+          when b then hue = (r - g) / delta + 4

         hue *= 60


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -133,10 +133,7 @@
       return this

     desaturate: (amount) ->
-      hsl = @toHsl()
-      hsl[1] -= amount
-
-      return Color2(hslToRgb(hsl))
+      @copy().desaturate(amount)

     desaturate$: (amount) ->
       hsl = @toHsl()


Fix usage of vertical space

Fix usage of vertical space

--- src/common.coffee
+++ src/common.coffee
@@ -119,7 +119,7 @@

       # the ratio of border and middle line height to
       # height of the barcode when the number is printed
-      border_line_height_number: 0.9
+      border_line_height_number: 0.95

       # the ratio of order and middle line height to
       # height of the barcode without number
@@ -128,6 +128,7 @@
       # the ratio of normal lines to the border and
       # middle lines
       line_height: 0.9
+
       font_size: 0.15
       font_y: 1.03
       text_offset: 2


Fix order in which we extend config so passed in options overwrite
defaults.

Perhaps should make this more like the jQuery extend, which always
combines from src to dest?

--- src/config.coffee
+++ src/config.coffee
@@ -18,4 +18,4 @@
     @error = (problem) -> undefined

   extend: (options) ->
-    extend(extend({}, this), options)
+    extend(extend({}, options), this)


:memo: Clean up Config::observe

:memo: Clean up Config::observe

--- src/config.coffee
+++ src/config.coffee
@@ -222,11 +222,12 @@
   #
   # keyPath - The {String} name of the key to observe
   # options - An optional {Object} containing the `callNow` key.
-  # callback - The {Function} that fires when the. It is given a single argument, `value`,
-  #            which is the new value of `keyPath`.
+  # callback - The {Function} that fires when the value of the key is
+  #            changed. It is given a single argument, `value`, which is
+  #            the new value of `keyPath`.
   #
   # Returns an {Object} with the following keys:
-  #  :off - A {Function} that unobserves the `keyPath` with called.
+  #  :off - A {Function} that unobserves the `keyPath` when called.
   observe: (keyPath, options={}, callback) ->
     if _.isFunction(options)
       callback = options


Update ReferencesHandlerTests.

Update ReferencesHandlerTests.

--- services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee
+++ services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee
@@ -39,13 +39,11 @@
 				get: sinon.stub()
 				post: sinon.stub()
 			}
-			'../../models/Project': {
-				Project: @Project = {
-					findOne: sinon.stub().callsArgWith(1, null, @fakeProject)
-				}
-			}
-			'../../models/User': {
-				User: @User = {}
+			'../Project/ProjectGetter': @ProjectGetter = {
+				getProject: sinon.stub().callsArgWith(2, null, @fakeProject)
+			}
+			'../User/UserGetter': @UserGetter = {
+				getUser: sinon.stub()
 			}
 			'../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = {
 				flushDocToMongo: sinon.stub().callsArgWith(2, null)
@@ -73,10 +71,10 @@
 					@handler._findBibDocIds.callCount.should.equal 0
 					done()

-			it 'should call Project.findOne', (done) ->
-				@call (err, data) =>
-					@Project.findOne.callCount.should.equal 1
-					@Project.findOne.calledWith(_id: @projectId).should.equal true
+			it 'should call ProjectGetter.getProject', (done) ->
+				@call (err, data) =>
+					@ProjectGetter.getProject.callCount.should.equal 1
+					@ProjectGetter.getProject.calledWith(@projectId).should.equal true
 					done()

 			it 'should not call _findBibDocIds', (done) ->
@@ -112,10 +110,10 @@
 					expect(data).to.equal @fakeResponseData
 					done()

-		describe 'when Project.findOne produces an error', ->
-
-			beforeEach ->
-				@Project.findOne.callsArgWith(1, new Error('woops'))
+		describe 'when ProjectGetter.getProject produces an error', ->
+
+			beforeEach ->
+				@ProjectGetter.getProject.callsArgWith(2, new Error('woops'))

 			it 'should produce an error', (done) ->
 				@call (err, data) =>
@@ -132,7 +130,7 @@
 		describe 'when _isFullIndex produces an error', ->

 			beforeEach ->
-				@Project.findOne.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, new Error('woops'))

 			it 'should produce an error', (done) ->
@@ -150,7 +148,7 @@
 		describe 'when flushDocToMongo produces an error', ->

 			beforeEach ->
-				@Project.findOne.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, false)
 				@DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops'))

@@ -170,7 +168,7 @@
 		describe 'when request produces an error', ->

 			beforeEach ->
-				@Project.findOne.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, null, false)
 				@DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null)
 				@request.post.callsArgWith(1, new Error('woops'))
@@ -185,7 +183,7 @@
 		describe 'when request responds with error status', ->

 			beforeEach ->
-				@Project.findOne.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, null, false)
 				@request.post.callsArgWith(1, null, {statusCode: 500}, null)

@@ -237,10 +235,10 @@
 				expect(data).to.equal @fakeResponseData
 				done()

-		describe 'when Project.findOne produces an error', ->
-
-			beforeEach ->
-				@Project.findOne.callsArgWith(1, new Error('woops'))
+		describe 'when ProjectGetter.getProject produces an error', ->
+
+			beforeEach ->
+				@ProjectGetter.getProject.callsArgWith(2, new Error('woops'))

 			it 'should produce an error', (done) ->
 				@call (err, data) =>
@@ -257,7 +255,7 @@
 		describe 'when _isFullIndex produces an error', ->

 			beforeEach ->
-				@Project.findOne.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, new Error('woops'))

 			it 'should produce an error', (done) ->
@@ -275,7 +273,7 @@
 		describe 'when flushDocToMongo produces an error', ->

 			beforeEach ->
-				@Project.findOne.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, false)
 				@DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops'))

@@ -319,8 +317,8 @@
 			@owner =
 				features:
 					references: false
-			@User.findOne = sinon.stub()
-			@User.findOne.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner)
+			@UserGetter.getUser = sinon.stub()
+			@UserGetter.getUser.withArgs(@owner_ref, {features: true}).yields(null, @owner)
 			@call = (callback) =>
 				@handler._isFullIndex @fakeProject, callback


Setup globals in coverage

Setup globals in coverage

--- src/test/coverage.coffee
+++ src/test/coverage.coffee
@@ -2,31 +2,35 @@
 config = require('../config')
 path = require('path')

-module.exports = (base, spec, name) ->
-  if not name?
-    name = spec ? 'coverage'
-    spec ?= path.join(name, '*.{js,coffee}')
+global.assert = require('chai').assert

-  suite(name, () ->
+module.exports = (options) ->
+  options ?= {}
+  options.name ?= options.spec ? 'coverage'
+  options.spec ?= path.join(options.name, '*.{js,coffee}')
+  options.globals = lib.util.extend({}, config.test.globals, options.globals)
+  options.globalsDebug = lib.util.extend({}, config.test.globalsDebug, options.globalsDebug)
+
+  suite(options.name, () ->
     setup(() ->
-      @timeout(10000)
-
-      includes = lib.util.extend({}, config.test.globals)
+      @timeout(100000)
+      debugger
+      includes = lib.util.extend({}, options.globals)
       if typeof v8debug == 'object'
-        includes = lib.util.extend(includes, config.test.globalsDebug)
+        includes = lib.util.extend(includes, options.globalsDebug)

       for own name, filePath of includes
         if (filePath[0] == '.')
-          global[name] = require(path.join(__dirname, '../', filePath))
+          global[name] = require(path.join(options.base, filePath))
         else
           global[name] = require(filePath)

       return
     )

-    fileNames = lib.util.glob.sync(path.join(base, spec), {})
+    fileNames = lib.util.glob.sync(path.join(options.base, options.spec), {})
     for fileName in fileNames
-      suite(path.relative(base, fileName), () ->
+      suite(path.relative(options.base, fileName), () ->
         require(fileName)
       )


Improving all error messages.

Improving all error messages.

--- src/toaster/toast.coffee
+++ src/toaster/toast.coffee
@@ -37,12 +37,11 @@
       unless fs.existsSync filepath

         # rise and error and aborts
-        error "File not found: ".yellow + " #{filepath.red}\n" +
-          "Try running:".yellow + " toaster -i".green +
-          " or type".yellow + " #toaster -h'".green +
-          "for more info".yellow
-
-        return null
+        msg = "Config file not found: #{filepath.yellow}\n"
+        msg += "Try running:".white + " toaster -i".green
+        msg += " or type".white + " #toaster -h'".green
+        msg += "for more info".white
+        return error msg

       # otherwise if file exists, go ahead and read it's contents
       contents = fs.readFileSync filepath, "utf-8"
@@ -90,8 +89,8 @@

     # ...: dirs - mandatory
     if config.dirs is null or config.dirs.length is 0
-      msg = 'Check your `toaster.coffee` config file, you need to inform at'
-      msg += 'least one dir in your config file.'
+      msg = 'You need to inform at least one dir in your config file.'
+      msg += '\nCheck your `toaster.coffee` config file.'
       return error msg

     for dir, i in config.dirs
@@ -99,20 +98,21 @@
       if fs.existsSync dir
         config.dirs[i] = dir
       else
-        msg = 'Check your `toaster.coffee` config file, informed dir doens\'t '
-        msg += 'exist:\n\tleast one dir in your config file.'
+        msg = 'Informed dir doesn\'t exist:\n\t#{dir.yellow}'
+        msg += '\nCheck your `toaster.coffee` config file.'
         return error msg

     # ...: release_dir - mandatory
     if config.release_dir is null
-      msg = 'Check your `toaster.coffee` config file, `release_dir` must to'
-      msg += 'be informed.'
+      msg = 'You need to inform your `release_dir`'
+      msg += '\nCheck your `toaster.coffee` config file.'
       return error msg
     else
       config.release_dir = path.join @basepath, config.release_dir
       unless fs.existsSync (path.dirname config.release_dir)
-        error "Release dir doesn't exist:\n\t#{dir.yellow}"
-        return null
+        msg = "`release_dir` doesn't exist:\n\t#{config.release_dir.yellow}"
+        msg += '\nCheck your `toaster.coffee` config file.'
+        return error msg

     # ...:optimize - optional
     if config.optimize?


game state

game state


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -1,7 +1,7 @@
 ###*
 The Follow module provides a simple method to set an object's
 velocity so that it will approach another object. This method
-relies on the
+relies on the objects ha

 player = GameObject


Use baseUrl in resin-request in e2e test suite

--- tests/integration.coffee
+++ tests/integration.coffee
@@ -7,6 +7,7 @@
 token = require('resin-token')
 errors = require('resin-errors')
 resinRequest = require('resin-request')
+settings = require('resin-settings-client')
 request = Promise.promisifyAll(require('request'))
 cheerio = require('cheerio')
 tmp = require('tmp')
@@ -121,6 +122,7 @@
 						return resinRequest.send
 							method: 'DELETE'
 							url: "/ewa/user(#{userId})"
+							baseUrl: settings.get('apiUrl')
 						.then(resin.auth.logout)
 					.nodeify (error) ->
 						if error?.message is 'Request error: Unauthorized'


Changed app.coffee for unbranded prototype

--- Clusterarchetype.framer/app.coffee
+++ Clusterarchetype.framer/app.coffee
@@ -1,8 +1,11 @@
-#change cursor
+#Code elements for the unbranded protottype cluster. As can be seen at www.ustwo.com/auto and www.ustwo.com/blog
+#Built using the amazing FramerJS platform and Framer Studio
+
+#change global cursor
 document.body.style.cursor = "auto"
 bkgnd = new BackgroundLayer({backgroundColor:"white"})

-#container for buttons etc
+#CONTAINER FOR BUTTONS
 buttoncontainer = new Layer
  width: 900, height: 500, rotation:0, backgroundColor: "transparent",x:295,y:-295, scale :1, opacity : 0
 buttoncontainer.center()
@@ -12,7 +15,7 @@
  width: 550, height: 1440, rotation:270, backgroundColor: "#333132",x:295,y:-295, scale :0.6
 container.center()

-#print bkgnd.width
+#SCALE for devices
 if bkgnd.width<1000
  container.scale = 0.6 * (bkgnd.width/864)
  buttoncontainer.scale = (bkgnd.width/864)
@@ -20,13 +23,15 @@
 else
  container.scale = 0.6
  buttoncontainer.scale = 1
-
+
+#COLORS
 blue= "#0e95e6"
 green= "#2ad880"
 red= "#fc5454"
 orange= "#ff8948"

-# The font file is at 'fonts/customFont.otf'
+#FONTS USED FOR THE APP
+#Medium
 style = document.createElement('style');
 style.type = 'text/css';
 style.appendChild(document.createTextNode("@font-face {\n" +
@@ -37,7 +42,7 @@
 "}\n"));
 document.getElementsByTagName('head')[0].appendChild(style);

-#regular
+#Regular
 style = document.createElement('style');
 style.type = 'text/css';
 style.appendChild(document.createTextNode("@font-face {\n" +
@@ -48,6 +53,7 @@
 "}\n"));
 document.getElementsByTagName('head')[0].appendChild(style);

+#Light
 style = document.createElement('style');
 style.type = 'text/css';
 style.appendChild(document.createTextNode("@font-face {\n" +
@@ -59,29 +65,29 @@
 document.getElementsByTagName('head')[0].appendChild(style);

-#master layers
+#MASTER LAYERS
 fuel = new Layer
  width: 550, height: 1440, y: 80 ,x :0, backgroundColor: "#222222" , opacity : 0
 speed = new Layer
  width: 550, height: 1440, y: -1360 ,x :0, backgroundColor: "#333333"

+#Speed limits as grey background
 bkgnd = new Layer
 	x:0, y:28, width:550, height:1440, image:"images/bkgnd1440.png", opacity: 0.9
 speed.addSubLayer(bkgnd)

-
+#Range partitions to show relative position
 fubkgnd = new Layer
 	x:-445, y:445, width:1440, height:550, image:"images/bg_park.png",rotation:90
 fuel.addSubLayer(fubkgnd)

-#sublayers
+#RANGE elements
 fuelstatus = new Layer
  width: 10, height: 1440,backgroundColor: green, opacity: 0.8
 fuel.addSubLayer(fuelstatus)

 fuelstatuswhite = new Layer x:140, width: 130, height: 1440, backgroundColor: 'white', opacity:0.1
 fuelstatus.addSubLayer(fuelstatuswhite)
-

 locline1 = new Layer
    x:-370, y:713, width:1440, height:6, image:"images/dashedline.png", rotation:90, opacity:0
@@ -158,6 +164,7 @@
   padding:'0px'
 fuel.addSubLayer(miles)

+#SPEED elements
 #speed and statuses
 speedstatus = new Layer
  width: 2, height: 1440, backgroundColor: blue
@@ -216,7 +223,7 @@
 speed.addSubLayer(mph)

-#reverse
+#REVERSE
 reverserect= new Layer
  width:550, height: 1440, x : 0, y: 1443, opacity : 1,  backgroundColor: "#fff"
 reverseimage = new Layer
@@ -224,6 +231,7 @@
 #reverseimage.center()
 reverserect.addSubLayer(reverseimage)

+#PNDR - GEAR POSITIONING and elements
 #overlays
 PNDRbkgnd= new Layer
 	x:0, y:0, width:750, height:80,opacity:0 ,backgroundColor: "#ffffff"
@@ -231,7 +239,7 @@
 gearicon = new Layer
 	x:40, y:15, width:12, height:48, image:"images/icon_gear.png", rotation: 90,opacity: 0

-#PNDR actual
+
 P = new Layer x:420, y:-10, width:120, height:100, rotation:90,opacity: 0, backgroundColor:'transparent'
 P.html = 'P'
 P.style =
@@ -273,21 +281,15 @@
 	x:235, y:-200, width:96, height:102, image:"images/icon_message.png", rotation: 90,
 opacity:0

-
-
-
 #GLOBAL VARIABLES
-#speeds
+#Speeds of speed element's movement
 fastsp = 0.001
 medsp = 0.005
 slowsp = 0.01
-#color value
-red = 20
-

 #buttons for acce and decce and reverse
 buttonA = new Layer
- width:360 , height:40,  x: 522, y:430, opacity : 1, backgroundColor:"#e3e3e3", borderRadius: 0
+ width:360 , height:40,  x: 522, y:430, opacity : 1, backgroundColor:"#e3e3e3",
 buttonA.html = "Accelerate"
 buttonA.style =
   #textAlign:'center'
@@ -301,7 +303,7 @@

 buttonB = new Layer
- width:320 , height:40,  x:185, y:430, opacity : 1, backgroundColor:"#e3e3e3", borderRadius: 0
+ width:320 , height:40,  x:185, y:430, opacity : 1, backgroundColor:"#e3e3e3",
 buttonB.html = "Brake"
 buttonB.style =
   #textAlign:'center'
@@ -315,7 +317,7 @@

 buttonR = new Layer
- width:150 , height:40,  x: 18, y:430, opacity : 1, backgroundColor:"#b2b2b1", borderRadius: 0
+ width:150 , height:40,  x: 18, y:430, opacity : 1, backgroundColor:"#b2b2b1",
 buttonR.html = "Reverse"
 buttonR.style =
   #textAlign:'center'
@@ -329,7 +331,7 @@

 buttonAlert = new Layer
  width:80 , height:40,  x: 18, y:0, opacity : 1, backgroundColor:"transparent", borderRadius: 0
-buttonAlert.html = "SMS alert"
+#buttonAlert.html = "SMS alert"
 buttonAlert.style =
   #textAlign:'center'
   fontFamily:'myfont'
@@ -391,7 +393,7 @@
 container.addSubLayer(PNDRun)

-#resizing windows
+#RESIZE FUNCTION
 window.onresize = ->
 	#print window.Screen.width
 	container.center()
@@ -517,7 +519,7 @@
       time: 0.18
       curve: "ease"

-#speedy function
+#SPEEDY function - appears in the acceleration and brake functions
 speedyfunc = ->
  if speedy < 36
    rainicon.animate
@@ -556,8 +558,7 @@

 #ACTION EVENTS
-
-#Initialize fuel
+#Initialize Range
 setint = null
 if speedy < 1
      fuel.y = 0
@@ -626,7 +627,7 @@
      P.style.color='#444444'

-#accelerate with sms alert
+#ACCELERATE with SMS alerts
 altrack = 0
 raintrack = 0
 #Global null
@@ -696,7 +697,7 @@
     if speedy > 0
      speedup()

- #decelerate
+ #DECELERATE on TOUCH END
    buttonA.on Events.TouchEnd,(e) ->
    	 e.stopPropagation()
    	 #print "N"
@@ -730,7 +731,7 @@
           properties : {x :-445}
           time:0.3

- #brake
+ #BRAKE on touch start
    buttonB.on Events.TouchStart, (e) ->
    	 e.stopPropagation()
    	 #print "brake"
@@ -779,7 +780,8 @@
          gearson.animate
           properties : {x :-445}
           time:0.3
-
+
+   #BRAKE touch end
      buttonB.on Events.TouchEnd,(e) ->
        e.stopPropagation()
        #print "brake off"
@@ -814,7 +816,7 @@
           time:0.3

-#REVERSE
+#REVERSE actions
 buttonR.on Events.TouchStart, ->
   buttonR.style =
       padding:'11px'
@@ -827,7 +829,7 @@
   buttonR.shadowY = 5
   buttonR.shadowColor = "#7c7c7c"

-
+#Reverse button click actions
 r =0
 buttonR.on Events.Click, ->
   r = r+1


Make sure tooltips are initialized where needed

--- app/assets/javascripts/admin/admin_in_place_editor.js.coffee
+++ app/assets/javascripts/admin/admin_in_place_editor.js.coffee
@@ -24,6 +24,7 @@
       @clearModalForm($(@))
     @initModals()
     @initSearchTypeahead()
+    $("[rel='tooltip']").tooltip()

   clearModalForm: (modal) ->
     form = modal.find('form')
@@ -218,10 +219,7 @@

 class ListingChangesEditor extends AdminEditor
   init: () ->
-    @initEditors()
     @initForm()
-
-  initEditors: () ->
     $("[rel='tooltip']").tooltip()

   initForm: () ->
@@ -295,7 +293,6 @@
     @nonSuperInit()

   nonSuperInit: () ->
-    $("[rel='tooltip']").tooltip()
     $(".nav-tabs.new-reference-tabs a").click (e) ->
       e.preventDefault()
       window.adminEditor.clearModalForm $("#admin-new-taxon_concept_reference-form")
@@ -369,7 +366,7 @@
       options.push {}
       selection = $(@)
       if selection.hasClass('taxon_concept')
-        options[i]['select2'] =
+        options[i]['select2'] =
           ajax:
             url: "/api/v1/auto_complete_taxon_concepts.json"
             dataType: "json"
@@ -394,7 +391,7 @@
           formatSelection: (item) ->
             item.name
       else if selection.hasClass('term')
-        options[i]['select2'] =
+        options[i]['select2'] =
           data:{ results: terms, text: 'code' },
           placeholder: "Select Term"
           allowClear: true


Subscribe to onDidDestroy when available

--- src/pane.coffee
+++ src/pane.coffee
@@ -1,6 +1,6 @@
 {find, compact, extend, last} = require 'underscore-plus'
 {Model} = require 'theorist'
-{Emitter} = require 'event-kit'
+{CompositeDisposable, Emitter} = require 'event-kit'
 Serializable = require 'serializable'
 Grim = require 'grim'
 PaneAxis = require './pane-axis'
@@ -34,6 +34,7 @@
     super

     @emitter = new Emitter
+    @subscriptions = new CompositeDisposable
     @items = []

     @addItems(compact(params?.items ? []))
@@ -340,6 +341,8 @@
   addItem: (item, index=@getActiveItemIndex() + 1) ->
     return if item in @items

+    if typeof item.onDidDestroy is 'function'
+      @subscriptions.add item.onDidDestroy => @removeItem(item, true)
     if typeof item.on is 'function'
       @subscribe item, 'destroyed', => @removeItem(item, true)

@@ -579,6 +582,7 @@
     @container.activateNextPane() if @isActive()
     @emitter.emit 'did-destroy'
     @emitter.dispose()
+    @subscriptions.dispose()
     item.destroy?() for item in @items.slice()
     @container?.didDestroyPane(pane: this)


more bugfixes to the counter

more bugfixes to the counter

--- src/pbkdf2.iced
+++ src/pbkdf2.iced
@@ -48,7 +48,7 @@
     n = Math.ceil(dkLen / bs)
     words = []
     tph = null
-    ph = (block) => (iter) => progress_hook? { what : "pbkdf2", total : n * bs, i : block*bs + iter }
+    ph = (block) => (iter) => progress_hook? { what : "pbkdf2", total : n * @c, i : block*@c + iter }
     ph(0)(0)
     for i in [1..n]
       await @gen_T_i {i, progress_hook : ph(i-1) }, defer tmp


prevent toggle_meta on text select & link clicks

--- app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
@@ -583,8 +583,8 @@
     'click [data-type=internal]':   'public_internal'
     'click .show_toogle':           'show_toogle'
     'click [data-type=reply]':      'reply'
-    'click .text-bubble':           'more_toogle'
-    'click .close-details':         'more_toogle'
+    'click .text-bubble':           'toggle_meta'
+    'click .text-bubble a':         'stopPropagation'
 #    'click [data-type=reply-all]':  'replyall'

   constructor: ->
@@ -646,36 +646,61 @@
         $(e.target).text( App.i18n.translateContent('See more') )
         $(e.target).next('div').addClass('hide')

-  more_toogle: (e) ->
+  stopPropagation: (e) ->
+    e.stopPropagation()
+
+  toggle_meta: (e) ->
     e.preventDefault()
+
+    animSpeed = 0.25
     article = $(e.target).closest('.ticket-article-item')
     metaTopClip = article.find('.article-meta-clip.top')
     metaBottomClip = article.find('.article-meta-clip.bottom')
     metaTop = article.find('.article-content-meta.top')
     metaBottom = article.find('.article-content-meta.bottom')

+    if @elementContainsSelection( article.get(0) )
+      @stopPropagation(e)
+      return false
+
     if !metaTop.hasClass('hide')
       article.removeClass('state--folde-out')

       # scroll back up
-      TweenLite.to(article.scrollParent(), 0.5, { scrollTo: article.scrollParent().scrollTop() - metaTop.outerHeight() })
-
-      TweenLite.to(metaTop, 0.5, { y: 0, opacity: 0, onComplete: -> metaTop.addClass('hide') })
-      TweenLite.to(metaBottom, 0.5, { y: -metaBottom.outerHeight(), opacity: 0, onComplete: -> metaTop.addClass('hide') })
-      TweenLite.to(metaTopClip, 0.5, { height: 0 })
-      TweenLite.to(metaBottomClip, 0.5, { height: 0 })
+      TweenLite.to(article.scrollParent(), animSpeed, { scrollTo: article.scrollParent().scrollTop() - metaTop.outerHeight() })
+
+      TweenLite.to(metaTop, animSpeed, { y: 0, opacity: 0, onComplete: -> metaTop.addClass('hide') })
+      TweenLite.to(metaBottom, animSpeed, { y: -metaBottom.outerHeight(), opacity: 0, onComplete: -> metaTop.addClass('hide') })
+      TweenLite.to(metaTopClip, animSpeed, { height: 0 })
+      TweenLite.to(metaBottomClip, animSpeed, { height: 0 })
     else
       article.addClass('state--folde-out')
       metaBottom.removeClass('hide')
       metaTop.removeClass('hide')

       # balance out the top meta height by scrolling down
-      TweenLite.to(article.scrollParent(), 0.5, { scrollTo: article.scrollParent().scrollTop() + metaTop.outerHeight() })
-
-      TweenLite.fromTo(metaTop, 0.5, { y: metaTop.outerHeight(), opacity: 0 }, { y: 0, opacity: 1 })
-      TweenLite.fromTo(metaBottom, 0.5, { y: -metaBottom.outerHeight(), opacity: 0 }, { y: 0, opacity: 1 })
-      TweenLite.to(metaTopClip, 0.5, { height: metaTop.outerHeight() })
-      TweenLite.to(metaBottomClip, 0.5, { height: metaBottom.outerHeight() })
+      TweenLite.to(article.scrollParent(), animSpeed, { scrollTo: article.scrollParent().scrollTop() + metaTop.outerHeight() })
+
+      TweenLite.fromTo(metaTop, animSpeed, { y: metaTop.outerHeight(), opacity: 0 }, { y: 0, opacity: 1 })
+      TweenLite.fromTo(metaBottom, animSpeed, { y: -metaBottom.outerHeight(), opacity: 0 }, { y: 0, opacity: 1 })
+      TweenLite.to(metaTopClip, animSpeed, { height: metaTop.outerHeight() })
+      TweenLite.to(metaBottomClip, animSpeed, { height: metaBottom.outerHeight() })
+
+  isOrContains: (node, container) ->
+    while node
+      if node is container
+        return true
+      node = node.parentNode
+    false
+
+  elementContainsSelection: (el) ->
+    sel = window.getSelection()
+    if sel.rangeCount > 0 && sel.toString()
+      for i in [0..sel.rangeCount-1]
+        if !@isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)
+          return false
+      return true
+    false

   checkIfSignatureIsNeeded: (type) =>


Improved handling with form changes.

Improved handling with form changes.

--- app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
@@ -79,7 +79,7 @@
     return false if !@ticket
     formCurrent = @formParam( @el.find('.edit') )
     ticket      = App.Ticket.find(@ticket_id).attributes()
-    modelDiff   = @getDiff( ticket, formCurrent )
+    modelDiff   = App.Utils.formDiff( formCurrent, ticket  )
     return false if !modelDiff || _.isEmpty( modelDiff )
     return true

@@ -269,7 +269,7 @@

           defaults   = ticket.attributes()
           task_state = @taskGet('ticket')
-          modelDiff  = @getDiff( defaults, task_state )
+          modelDiff  = App.Utils.formDiff( task_state, defaults )
           #if @isRole('Customer')
           #  delete defaults['state_id']
           #  delete defaults['state']
@@ -483,18 +483,19 @@
       currentParams =
         ticket:  @formParam( @el.find('.edit') )
         article: @formParam( @el.find('.article-add') )
+      #console.log('lll', currentStore)
+      # remove not needed attributes
+      delete currentParams.article.form_id

       # get diff of model
       modelDiff =
-        ticket:  @getDiff( currentStore.ticket, currentParams.ticket )
-        article: @getDiff( currentStore.article, currentParams.article )
+        ticket:  App.Utils.formDiff( currentParams.ticket, currentStore.ticket )
+        article: App.Utils.formDiff( currentParams.article, currentStore.article )
       #console.log('modelDiff', modelDiff)

       # get diff of last save
       changedBetweenLastSave = _.isEqual(currentParams, @autosaveLast )
-      #console.log('changedBetweenLastSave', changedBetweenLastSave)
       if !changedBetweenLastSave
-        #console.log('autosave DIFF result', changedBetweenLastSave)
         console.log('model DIFF ', modelDiff)

         @autosaveLast = clone(currentParams)
@@ -503,21 +504,11 @@
         @taskUpdateAll( modelDiff )
     @interval( update, 3000, 'autosave' )

-  getDiff: (model, params) =>
-
-    # do type convertation to compare it against form
-    modelClone = clone(model)
-    for key, value of modelClone
-      if key is 'owner_id' && modelClone[key] is 1
-        modelClone[key] = ''
-      else if typeof value is 'number'
-        modelClone[key] = value.toString()
-    #console.log('LLL', modelClone)
-    result = difference( modelClone, params )
-
   markFormDiff: (diff = {}) =>
-    ticketForm  = @$('.edit')
-    articleForm = @$('.article-add')
+    ticketForm    = @$('.edit')
+    ticketSidebar = @$('.tabsSidebar-tab[data-tab="ticket"]')
+    articleForm   = @$('.article-add')
+    resetButton   = @$('.js-reset')

     params         = {}
     params.ticket  = @formParam( ticketForm )
@@ -526,23 +517,30 @@

     # clear all changes
     if _.isEmpty(diff.ticket) && _.isEmpty(diff.article)
+      ticketSidebar.removeClass('is-changed')
       ticketForm.removeClass('form-changed')
       ticketForm.find('.form-group').removeClass('is-changed')
-      @$('.js-reset').addClass('hide')
+      resetButton.addClass('hide')

     # set changes
     else
       ticketForm.addClass('form-changed')
+      if !_.isEmpty(diff.ticket)
+        ticketSidebar.addClass('is-changed')
+      else
+        ticketSidebar.removeClass('is-changed')
       for currentKey, currentValue of params.ticket
         element = @$('.edit [name="' + currentKey + '"]').parents('.form-group')
-        if diff.ticket[currentKey]
+        if !element.get(0)
+          element = @$('.edit [data-name="' + currentKey + '"]').parents('.form-group')
+        if currentKey of diff.ticket
           if !element.hasClass('is-changed')
             element.addClass('is-changed')
         else
           if element.hasClass('is-changed')
             element.removeClass('is-changed')

-      @$('.js-reset').removeClass('hide')
+      resetButton.removeClass('hide')

   submit: (e) =>
@@ -700,8 +698,12 @@
     App.TaskManager.update( @task_key, { 'state': @localTaskData })

   taskReset: =>
+
     # hide reset button
     @$('.js-reset').addClass('hide')
+
+    # remove change flag on tab
+    @$('.tabsSidebar-tab[data-tab="ticket"]').removeClass('is-changed')

     # reset task state
     @localTaskData =


Remove duplicated stuff

Remove duplicated stuff

--- app/assets/javascripts/views/game_instructions_screen.coffee
+++ app/assets/javascripts/views/game_instructions_screen.coffee
@@ -13,9 +13,6 @@
       <img src="assets/controller.png" />
     '

-    setTimeout (-> gameView.goto(new InitialCountdownScreen)), 5000
-
-    @$el.text('Game instructions go here')
     setTimeout (=> @goOn()), 5000

   goOn: ->


table chart is now drawn even when there is only one region

--- app/assets/javascripts/views/tables/TableRegionsChart.coffee
+++ app/assets/javascripts/views/tables/TableRegionsChart.coffee
@@ -30,17 +30,20 @@
       @xAxis = new RickshawUtil.LeftAlignedXAxis
         graph: @graph,
         element: @$(".x-axis")[0]
-        tickFormat: (x) => if x < @collection.length then "##{x+1}" else ""
+        tickFormat: (x) => if x < @collection.length && x % 1 == 0 then "##{x+1}" else ""

       @hoverDetail = new RickshawUtil.InteractiveHoverDetail
         graph: @graph
         xFormatter: ((x) =>
-          region = @collection.at(x)
-          "##{x+1} : #{region.get('regionName')}"
+          if x >= @collection.length
+            ""
+          else
+            region = @collection.at(x)
+            "##{x+1} : #{region.get('regionName')}"
         )
         yFormatter: ((y) => y)
         formatter: ((series, x, y, formattedX, formattedY, d) =>
-          region = @collection.at(x)
+          region = d.value.region
           headline = "<b>" + region.get('startKey') + "</b>"
           host = "Host: " + region.get('serverHostName')
           storefileSize = "Size (MB): " + region.get("storefileSizeMB").toFixed(0)
@@ -55,14 +58,23 @@

   createSeries: (name, regions) ->
     minSize = regions.max((region) -> region.get('storefileSizeMB')).get('storefileSizeMB') / 100
-    {
+    series = {
       data: regions.map((region, x) ->
         y = region.get('storefileSizeMB')
         return {
           x: x
           y: if y > minSize then y else minSize
-          region: region.get('regionName')
+          region: region
         }
       ),
       color: @palette.color()
-    }+    }
+
+    # Add another datapoint because the bar-renderer won't render anything when there is only one datapoint
+    if series.data.length == 1
+      series.data.push {
+        x: 1
+        y: 0
+        region: series.data[0].region
+      }
+    series


Simplify `changeScene` method

Simplify `changeScene` method

Also clean up comments and unecessary double quotes

--- src/app.coffee
+++ src/app.coffee
@@ -21,14 +21,14 @@

 Storage.prototype.getBoolean = (key) ->
   value = @getItem(key)
-  value == "true"
+  value == 'true'

 # Extend Backbone
 Backbone.View.prototype.close = ->
   @elem.remove()
   @undelegateEvents()

-  if typeof @onClose == "function"
+  if typeof @onClose == 'function'
     @onClose()

 class App extends Backbone.View
@@ -86,14 +86,14 @@

     # Handle being moved to the background in Cordova builds
     if ENV.cordova
-      document.addEventListener "pause", =>
-        if typeof @activeScene.pause is "function" then @activeScene.pause()
+      document.addEventListener 'pause', =>
+        if typeof @activeScene.pause is 'function' then @activeScene.pause()
         @stopMusic()
         @pausedMusic = @currentMusic
       , false

       # Handle resuming from background
-      document.addEventListener "resume", =>
+      document.addEventListener 'resume', =>
         @playMusic @pausedMusic
       , false

@@ -120,7 +120,6 @@

     @sona.play(id)

-  # Callback to play music
   playMusic: (id) ->
     return unless localStorage.getBoolean('playMusic')

@@ -135,7 +134,6 @@
     @sona.loop(id)
     @currentMusic = id

-  # Stop music!
   stopMusic: ->
     return if not @currentMusic

@@ -152,25 +150,14 @@

   # Handle hiding/showing the active scene
   changeScene: (scene, options) ->
+    throw new Error("Invalid scene '#{scene}") if @scenes[scene] == undefined
+
     @activeScene.hide()
-
-    switch scene
-      when 'title' then @activeScene = @scenes.title
-      when 'editor' then @activeScene = @scenes.editor
-      when 'about' then @activeScene = @scenes.about
-      when 'difficulty' then @activeScene = @scenes.difficultySelect
-      when 'level'
-        @activeScene = @scenes.levelSelect
-        @activeScene.difficulty = options.difficulty
-      when 'game'
-        # Set the game's diff & level props from the passed "options" arg
-        @activeScene = @scenes.game
-        @activeScene.difficulty = options.difficulty
-        @activeScene.level = options.level
-        @activeScene.tutorial = options.tutorial
-      else
-        console.log "Error! Scene not defined in switch statement"
-        @activeScene = @titleScene
+    @activeScene = @scenes[scene]
+
+    if options != undefined
+      for key, value of options
+        @activeScene[key] = value

     @activeScene.show()

@@ -207,7 +194,6 @@
         newHeight = width / ratio
         padding.height = height - newHeight
         height = newHeight
-      # TODO: Also update universal line-height value here
       $('html').css { 'font-size': "#{width * 0.1302}%" }

     else if orientation is 'portrait'
@@ -219,7 +205,6 @@
         newWidth = height / ratio
         padding.width = width - newWidth
         width = newWidth
-      # TODO: Also update universal line-height value here
       $('html').css { 'font-size': "#{height * 0.1302}%" }

     # Add the calculated padding to each scene <div>
@@ -228,12 +213,11 @@
       height: height
       padding: "#{padding.height / 2}px #{padding.width / 2}px"

-    # Call a "resize" method on other views
+    # Resize other views
     for name, scene of @scenes
       scene.resize(width, height, orientation) if typeof scene.resize == 'function'

   initializeDefaults: ->
-    # Obj that stores # of tries, best time, etc.
     if localStorage.getObject('stats') == null
       stats =
         beginner: {}
@@ -243,7 +227,6 @@
         random: {}
       localStorage.setObject 'stats', stats

-    # Obj that stores # of completed levels per difficulty
     if localStorage.getObject('complete') == null
       complete =
         beginner: 0
@@ -253,7 +236,6 @@
         random: 0
       localStorage.setObject 'complete', complete

-    # Obj that stores the most recently viewed level in a difficulty
     if localStorage.getObject('lastViewedLevel') == null
       lastViewedLevel =
         beginner: 0
@@ -263,20 +245,18 @@
         random: 0
       localStorage.setObject 'lastViewedLevel', lastViewedLevel

-    # Array that contains purchased IAP product IDs
     if localStorage.getObject('purchased') == null
       localStorage.setObject 'purchased', []

-    # Whether to play music/SFX
     if localStorage.getItem('playMusic') == null
-      localStorage.setItem 'playMusic', "true"
+      localStorage.setItem 'playMusic', 'true'

     if localStorage.getItem('playSfx') == null
-      localStorage.setItem 'playSfx', "true"
+      localStorage.setItem 'playSfx', 'true'

 # Cordova fires `deviceready` event when environment is ready
 if ENV.cordova
-  document.addEventListener "deviceready", ->
+  document.addEventListener 'deviceready', ->
     window.app = new App
   , false
 else


hwhat

hwhat

--- src/app.coffee
+++ src/app.coffee
@@ -1,6 +1,3 @@
-fs = require 'fs'
-
-
 Event = require 'event'
 Native = require 'native'
 KeyBinder = require 'key-binder'


Nicer use of promises

Nicer use of promises

--- src/app.coffee
+++ src/app.coffee
@@ -17,13 +17,15 @@
 ssidList = null
 dnsServer = null

+ignore = ->
+
 getIptablesRules = (callback) ->
 	async.retry {times: 100, interval: 100}, (cb) ->
 		Promise.try ->
 			os.networkInterfaces().tether[0].address
 		.nodeify(cb)
 	, (err, myIP) ->
-		throw err if err?
+		return callback(err) if err?
 		callback null, [
 				table: 'nat'
 				rule: 'PREROUTING -i tether -j TETHER'
@@ -43,16 +45,17 @@
 	console.log('Getting networks list')
 	wifi.getNetworksAsync().then (list) ->
 		ssidList = list
-	.then ->
 		wifi.openHotspotAsync(ssid, passphrase)
 	.then ->
 		console.log('Hotspot enabled')
 		dnsServer = spawn('named', ['-f'])
 		getIptablesRules (err, iptablesRules) ->
+			throw err if err?
 			iptables.appendManyAsync(iptablesRules)
 			.then ->
 				console.log('Captive portal enabled')
-				server = app.listen port, ->
+				server = app.listen port, (err) ->
+					throw err if err?
 					console.log('Server listening')

 console.log('Starting node connman app')
@@ -70,32 +73,36 @@
 		app.get '/ssids', (req, res) ->
 			res.send(ssidList)
 		app.post '/connect', (req, res) ->
-			if req.body.ssid and req.body.passphrase
-				console.log('Selected ' + req.body.ssid)
-				res.send('OK')
-				server.close()
-				iptables.delete { table: 'nat', rule: 'PREROUTING -i tether -j TETHER'}, ->
-					iptables.flush 'nat', 'TETHER', ->
-						dnsServer.kill()
-						console.log('Server closed and captive portal disabled')
-						wifi.joinWithAgentAsync(req.body.ssid, req.body.passphrase)
-						.then ->
-							console.log('Joined! Exiting.')
-							retryCallback()
-						.catch (err) ->
-							console.log(err)
-							return startServer(wifi)
+			if not (req.body.ssid? and req.body.passphrase?)
+				return res.sendStatus(400)
+			console.log('Selected ' + req.body.ssid)
+			res.send('OK')
+			server.close()
+			iptables.deleteAsync({ table: 'nat', rule: 'PREROUTING -i tether -j TETHER'})
+			.catch(ignore)
+			.then ->
+				iptables.flushAsync('nat', 'TETHER')
+			.catch(ignore)
+			.then ->
+				dnsServer.kill()
+				console.log('Server closed and captive portal disabled')
+				wifi.joinWithAgentAsync(req.body.ssid, req.body.passphrase)
+			.then ->
+				console.log('Joined! Exiting.')
+				retryCallback()
+			.catch (err) ->
+				console.log('Error joining network', err, err.stack)
+				return startServer(wifi)
+
 		app.use (req, res) ->
 			res.redirect('/')

 		# Create TETHER iptables chain (will silently fail if it already exists)
 		iptables.createChainAsync('nat', 'TETHER')
-		.catch ->
-			return
+		.catch(ignore)
 		.then ->
 			iptables.deleteAsync({ table: 'nat', rule: 'PREROUTING -i tether -j TETHER'})
-		.catch ->
-			return
+		.catch(ignore)
 		.then ->
 			iptables.flushAsync('nat', 'TETHER')
 		.then ->


scan for SSIDs before entering access point mode

Some wifi interfaces, for example the one in 5th Gen NUCs, don't support
SSID scanning while in AP mode.

Signed-off-by: Petros Angelatos <5ae4506022d2e10a17fe64868893c6af9c21e85f@gmail.com>

--- src/app.coffee
+++ src/app.coffee
@@ -16,10 +16,10 @@
 app.use(bodyParser.urlencoded(extended: true))
 app.use(express.static(__dirname + '/public'))

+ssids = []
+
 app.get '/ssids', (req, res) ->
-	wifiScan.scanAsync()
-	.then (ssids) ->
-		res.json(ssids)
+	res.json(ssids)

 app.post '/connect', (req, res) ->
 	if not (req.body.ssid? and req.body.passphrase?)
@@ -60,6 +60,10 @@
 app.use (req, res) ->
 	res.redirect('/')

-hotspot.start()
+wifiScan.scanAsync()
+.then (results) ->
+	ssids = results
+
+	hotspot.start()

 app.listen(80)


Pass options appropriately to new scenes

--- src/app.coffee
+++ src/app.coffee
@@ -161,15 +161,15 @@
       when 'title' then @activeScene = @scenes.title
       when 'about' then @activeScene = @scenes.about
       when 'difficulty' then @activeScene = @scenes.difficultySelect
-      when 'level'
-        @levelScene.difficulty = options.difficulty
+      when 'level'
         @activeScene = @scenes.levelSelect
+        @activeScene.difficulty = options.difficulty
       when 'game'
         # Set the game's diff & level props from the passed "options" arg
-        @gameScene.difficulty = options.difficulty
-        @gameScene.level = options.level
-        @gameScene.tutorial = options.tutorial
         @activeScene = @scenes.game
+        @activeScene.difficulty = options.difficulty
+        @activeScene.level = options.level
+        @activeScene.tutorial = options.tutorial
       else
         console.log "Error! Scene not defined in switch statement"
         @activeScene = @titleScene


added abilityto get fans and fan ofs for a user

--- src/bot.coffee
+++ src/bot.coffee
@@ -613,13 +613,30 @@
         callback {ids: res, success: true}

-  getFanOf: (callback) ->
     rq = api: 'user.get_fan_of'
-    @_send rq, callback
-
-
-  getFans: (callback) ->
+    callback = null
+    if arguments.length == 1
+      if typeof arguments[0] == 'function'
+        callback = arguments[0]
+      else if typeof arguments[0] == 'string'
+        rq.userid = arguments[0]
+    else if arguments.length == 2
+      rq.userid = arguments[0]
+      callback  = arguments[1]
+    @_send rq, callback
+
+
+  getFans: ->
     rq = api: 'user.get_fans'
+    callback = null
+    if arguments.length == 1
+      if typeof arguments[0] == 'function'
+        callback = arguments[0]
+      else if typeof arguments[0] == 'string'
+        rq.userid = arguments[0]
+    else if arguments.length == 2
+      rq.userid = arguments[0]
+      callback  = arguments[1]
     @_send rq, callback


Use better terminology.

Use better terminology.

--- src/cli.coffee
+++ src/cli.coffee
@@ -74,16 +74,16 @@
 fs.readFile input, 'utf8', (err, data) ->
   throw err if err

-  info = bless data
-  numFiles = info.data.length
-  {numSelectors} = info
+  result = bless data
+  numFiles = result.data.length
+  {numSelectors} = result

   dirname = path.dirname output
   extension = path.extname output
   filename = path.basename output, extension

   if numFiles > 1
-    for fileData, index in info.data
+    for fileData, index in result.data
       newFilename = "#{path.join(dirname, filename)}-blessed#{index + 1}#{extension}"

       fs.writeFile newFilename, fileData, (err) ->


Pretty print errors.

Pretty print errors.

--- src/cli.coffee
+++ src/cli.coffee
@@ -1,8 +1,9 @@
 #!/usr/bin/env coffee
-fs        = require 'fs'
-path      = require 'path'
-requisite = require '../lib'
-utils     = require '../lib/utils'
+fs         = require 'fs'
+path       = require 'path'
+requisite  = require '../lib'
+utils      = require '../lib/utils'
+postmortem = require 'postmortem'

 error = (message) ->
   console.log message
@@ -143,9 +144,13 @@

   unless opts.watch
     requisite.bundle opts, (err, bundle) ->
+      return postmortem.prettyPrint err if err?
+
       next bundle
   else
     requisite.watch opts, (err, bundle, filename) ->
+      return postmortem.prettyPrint err if err?
+
       if filename?
         console.log "#{utils.formatDate()} - recompiling, #{filename} changed"
       else


ENHANCE: Ensure previous alignment data is not lost on resubmit

--- src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee
+++ src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee
@@ -209,8 +209,11 @@
     )
     .value()

+  # Get initial alignment data...
+  initialAlignments = task.project().task_outcome_alignments.filter( (a) -> a.task_definition_id == task.definition.id )
+
   # ILO alignment defaults
-  $scope.alignmentsRationale = ""
+  $scope.alignmentsRationale = if initialAlignments.length > 0 then initialAlignments[0].description else ""
   staffAlignments = $scope.task.staffAlignments()
   $scope.ilos = _.map(task.unit().ilos, (ilo) ->
     staffAlignment = _.find(staffAlignments, {learning_outcome_id: ilo.id})
@@ -222,7 +225,9 @@
   )
   $scope.alignments = _.chain(task.unit().ilos)
     .map((ilo) ->
-      [ilo.id, {rating: 0}]
+      value = initialAlignments.filter((a) -> a.learning_outcome_id == ilo.id)?[0]?.rating
+      value ?= 0
+      [ilo.id, {rating: value }]
     )
     .fromPairs()
     .value()


Sliderbar Component: Minor codefix

Sliderbar Component: Minor codefix

--- client/Framework/components/sliderbar/KDSliderBarView.coffee
+++ client/Framework/components/sliderbar/KDSliderBarView.coffee
@@ -9,10 +9,10 @@
     options.snap       ?= yes
     data.handles

+    super options, data
+
     @handles            = []
     @labels             = []
-
-    super options, data

   createHandles:->
     for k, v of @getData().handles


do not click to the following / followers links if the counts are 0

--- client/app/Applications/Members.kdapplication/AppView.coffee
+++ client/app/Applications/Members.kdapplication/AppView.coffee
@@ -51,9 +51,9 @@
   click:(event)->
     KD.utils.showMoreClickHandler.call this, event
     targetATag = $(event.target).closest('a')
-    if targetATag.is(".followers") and targetATag.find('.data').text() isnt '0'
+    if targetATag.is(".followers") and parseInt(targetATag.text()) isnt 0
       KD.getSingleton('router').handleRoute "/#{@getData().profile.nickname}/Followers"
-    else if targetATag.is(".following") and targetATag.find('.data').text() isnt '0'
+    else if targetATag.is(".following") and parseInt(targetATag.text()) isnt 0
       KD.getSingleton('router').handleRoute "/#{@getData().profile.nickname}/Following"

   clickOnMyItem:(event)->


styleguide: unless over if not

styleguide: unless over if not

--- client/app/lib/providers/machinesettingssnapshotsview.coffee
+++ client/app/lib/providers/machinesettingssnapshotsview.coffee
@@ -120,7 +120,7 @@
         container = ideController.getView()
         break

-    if not container?
+    unless container?
       router = kd.getSingleton 'router'
       router.handleRoute "/IDE/#{machine.slug}"
       msg = "Error, unable to create snapshot. Please try again."


hackathon: added Intuit API and prize

hackathon: added Intuit API and prize

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -433,6 +433,12 @@

+
+
Intuit will award an extra $2,500 to the team that presents the best use of their API.

+

+

+

+

nginx is adding an additional $2,000 to the overall prize money for teams that use their web server as part of their final project  .

@@ -563,6 +569,7 @@

         Factual
         Iron.io
+        Intuit
         Google Places
         Google Geocoding
         Google Books


Repair the missing getBody method, not updated in testing

--- src/slack.coffee
+++ src/slack.coffee
@@ -117,7 +117,7 @@
     user = @client.dataStore.getUserById msg.user if msg.user
     user.room ?= msg.channel if channel and user

-    rawText = msg.text
+    rawText = @getBody msg
     text = @removeFormatting rawText

     if msg.hidden or (not rawText and not msg.attachments) or msg.subtype is 'bot_message' or not msg.user or not channel
@@ -162,6 +162,19 @@
         text = "#{@robot.name} #{text}"

       @receive new SlackTextMessage user, text, rawText, msg
+
+  getBody: (message) ->
+    body = ""
+    if message.text
+      body += message.text
+    if message.attachments
+      if message.text
+        body += '\n'
+      for attachment in message.attachments
+        if attachment != message.attachments[0]
+          body += '\n'
+        body += attachment.fallback
+    body

   removeFormatting: (text) ->
     # https://api.slack.com/docs/formatting


Fix unit test

Fix unit test

--- services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
+++ services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
@@ -177,7 +177,7 @@
 					.should.equal true

 			it "should return the project model view, privilege level and protocol version", ->
-				@callback.calledWith(null, @projectModelView, "owner", @EditorController.protocolVersion).should.equal true
+				@callback.calledWith(null, @projectModelView, "owner").should.equal true

 		describe "when not authorized", ->
 			beforeEach ->
@@ -186,7 +186,7 @@
 				@EditorController.buildJoinProjectView(@project_id, @user_id, @callback)

 			it "should return false in the callback", ->
-				@callback.calledWith(null, null, false, @EditorController.protocolVersion).should.equal true
+				@callback.calledWith(null, null, false).should.equal true

 	describe "leaveProject", ->


API change to dune

API change to dune

--- src/test.coffee
+++ src/test.coffee
@@ -36,7 +36,7 @@
 # Returns a function which runs the tests
 compileTest = (file) ->
   fullPath = path.join process.cwd(), file
-  exported = dune fullPath, null, coverage.require fullPath
+  exported = dune.file fullPath, null, coverage.require fullPath

   return runVows exported if app.vows


A few suggestions to update it to more common Danish. @martinjlowm what do you think?

--- vendor/assets/javascripts/mercury/locales/da.locale.js.coffee
+++ vendor/assets/javascripts/mercury/locales/da.locale.js.coffee
@@ -68,10 +68,10 @@

   # ### General
   # Error Messages
-  "Mercury.Regions.%s is unsupported in this client. Supported browsers are %s.": "Mercury.Regions.%s er ustøttet i denne klient. Støttede browsere er %s."
+  "Mercury.Regions.%s is unsupported in this client. Supported browsers are %s.": "Mercury.Regions.%s er ikke understøttet i denne browser. Understøttede browsere er %s."
   "Mercury.PageEditor can only be instantiated once.": "Mercury.PageEditor kan kun blive instantieret én gang."
-  "Opera isn't a fully supported browser, your results may not be optimal.": "Opera er ikke en fuldt ud støttet browser. Dine resultater kan derfor være uoptimale."
-  "Mercury.PageEditor failed to load: %s\n\nPlease try refreshing.": "Mercury.PageEditor fejlede med at indlæse: %s\n\n Prøv venligst at opdatere siden."
+  "Opera isn't a fully supported browser, your results may not be optimal.": "Opera er ikke en fuldt ud understøttet. Dine resultater kan derfor være uoptimale."
+  "Mercury.PageEditor failed to load: %s\n\nPlease try refreshing.": "Mercury.PageEditor kunne ikke starte: %s\n\n Prøv venligst at opdatere siden."
   "Region type is malformed, no data-type provided, or \"%s\" is unknown for the \"%s\" region.": "Regionstype er forkert formateret. Enten blev ingen datatype angivet, ellers er \"%s\" ukendt for \"%s\"regionen."
   "Mercury was unable to save to the url: %s": "Mercury var ikke i stand til at gemme til addressen: %s"
   # Confirmations
@@ -100,12 +100,12 @@
   "Error: Unable to upload the file": "Fejl: Kunne ikke uploade filen"
   "Malformed response from server": "Deform svar fra server"
   "Too large": "For stor"
-  "Unsupported format": "Formatet er ikke støttet"
+  "Unsupported format": "Formatet er ikke understøttet"
   # Statuses
   "Processing...": "Forarbejder..."
   "Uploading...": "Uploader..."
   "Aborted": "Afbrudt"
-  "Successfully uploaded...": "Uploadningen lykkedes..."
+  "Successfully uploaded...": "Upload færdig..."
   "Name: %s": "Navn: %s"
   "Size: %s": "Størrelse: %s"
   "Type: %s": "Type: %s"
@@ -129,7 +129,7 @@
   "Existing Links": "Eksisterende links"
   "Bookmark": "Bogmærke"
   "Options": "Indstillinger"
-  "Link Target": "Link mål"
+  "Link Target": "Link Target"
   "Self (the same window or tab)": "Self (det samme vindue eller fane)"
   "Blank (a new window or tab)": "Blank (et nyt vindue eller fane)"
   "Top (removes any frames)": "Top (fjerner eventuelle rammer)"
@@ -141,7 +141,7 @@
   # Insert Media Modal (media.html)
   "Images": "Billeder"
   "Videos": "Videoer"
-  "YouTube Share URL": "YouTube delnings-URL"
+  "YouTube Share URL": "YouTube del-URL"
   "Vimeo URL": "Vimeo-URL"
   "Alignment": "Justering"
   "None": "Ingen"


setup/teardown

setup/teardown

--- apps/home/test/client.coffee
+++ apps/home/test/client.coffee
@@ -107,7 +107,8 @@

     beforeEach (done) ->
       user = new CurrentUser(fabricate 'user', lab_features: ["Suggested Artworks"])
-      (@mod.__get__ 'CurrentUser').orNull = -> user
+      CurrentUser = @mod.__get__ 'CurrentUser'
+      sinon.stub(CurrentUser, 'orNull').returns user
       benv.render resolve(__dirname, '../templates/index.jade'), {
           heroUnits: new HeroUnits([
             fabricate 'site_hero_unit'
@@ -122,6 +123,9 @@
         }, =>
           @init()
           done()
+
+    afterEach ->
+      CurrentUser.orNull.restore()

     it 'renders suggested artworks', ->
       _.last(Backbone.sync.args)[2].success [fabricate 'artwork', title: 'Foobaz']


First pass at gruntfile

First pass at gruntfile

--- GruntFile.coffee
+++ GruntFile.coffee
@@ -1 +1,184 @@
-# TODO+os = require 'os'
+userConfig = require './build.config.coffee'
+
+module.exports = (grunt) ->
+
+  # Load tasks - - - - - - - - - - - - - - - - - - - - - - -
+
+  grunt.loadNpmTasks 'grunt-contrib-clean'
+  grunt.loadNpmTasks 'grunt-contrib-copy'
+  grunt.loadNpmTasks 'grunt-contrib-connect'
+  grunt.loadNpmTasks 'grunt-contrib-concat'
+  grunt.loadNpmTasks 'grunt-contrib-compress'
+  grunt.loadNpmTasks 'grunt-contrib-watch'
+  grunt.loadNpmTasks 'grunt-contrib-coffee'
+  grunt.loadNpmTasks 'grunt-conventional-changelog'
+  grunt.loadNpmTasks 'grunt-bump'
+  grunt.loadNpmTasks 'grunt-coffeelint'
+  grunt.loadNpmTasks 'grunt-recess'
+  grunt.loadNpmTasks 'grunt-karma'
+
+
+  # Helpers - - - - - - - - - - - - - - - - - - - - - - - -
+
+  filterForJS = (files) ->
+    files.filter (file) ->
+      file.match /\.js$/
+
+  filterForCSS = (files) ->
+    files.filter (file) ->
+      file.match /\.css$/
+
+  getLocalIp = ->
+    interfaces = os.networkInterfaces()
+    for name, value of interfaces
+      for details in value
+        if details.family is 'IPv4' and name is 'en1'
+          return details.address
+
+
+  # Config - - - - - - - - - - - - - - - - - - - - - - - - -
+
+  taskConfig =
+    connect:
+      dev:
+        options:
+          port: 5100
+          # hostname: getLocalIp()
+          base: 'build'
+
+    pkg: grunt.file.readJSON 'package.json'
+
+    meta:
+      banner:
+        '/**\n' +
+        ' * <%= pkg.name %> - v<%= pkg.version %> - ' +
+          '<%= grunt.template.today(\'yyyy-mm-dd\') %>\n' +
+        ' * <%= pkg.homepage %>\n' +
+        ' *\n' +
+        ' * Copyright (c) <%= grunt.template.today(\'yyyy\')' +
+          ' %> <%= pkg.author %>\n' +
+        ' */\n'
+
+    changelog:
+      options:
+        dest: 'CHANGELOG.md'
+        template: 'changelog.tpl'
+
+    bump:
+      options:
+        files: ['package.json', 'bower.json']
+        commit: false
+        commitMessage: 'chore(release): v%VERSION%'
+        commitFiles: ['package.json', 'client/bower.json']
+        createTag: false
+        tagName: 'v%VERSION%'
+        tagMessage: 'Version %VERSION%'
+        push: false
+        pushTo: 'origin'
+
+    clean: [
+      '<%= buildDir %>'
+      '<%= compileDir %>'
+    ]
+
+    coffee:
+      source:
+        # options:
+        #   bare: true
+
+        expand: true
+        cwd: '.'
+        src: ['<%= appFiles.coffee %>']
+        dest: '<%= buildDir %>'
+        ext: '.js'
+
+      karmaConfig:
+        options:
+          bare: true
+
+        src: '<%= buildDir %>/karma-unit.coffee'
+        dest: '<%= buildDir %>/karma-unit.js'
+
+    coffeelint:
+      src:
+        files:
+          src: [
+            '<%= appFiles.coffee %>'
+            '<%= appFiles.coffeeunit %>'
+            'GruntFile.coffee'
+          ]
+
+      test:
+        files:
+          src: ['<%= appFiles.coffeeunit %>']
+
+    karma:
+      options:
+        configFile: '<%= buildDir %>/karma-unit.js'
+
+      unit:
+        runnerPort: 9101
+        background: true
+
+      continuous:
+        singleRun: true
+
+      compile:
+        dir: '<%= compileDir %>'
+        src: ['<%= concat.compileJs.dest %>'
+          '<%= vendorFiles.css %>'
+          '<%= recess.compile.dest %>'
+        ]
+
+    karmaconfig:
+      unit:
+        dir: '<%= buildDir %>'
+        src: ['<%= vendorFiles.js %>'
+          '<%= html2js.app.dest %>'
+          '<%= html2js.common.dest %>'
+        ]
+
+    delta:
+      options:
+        livereload: true
+
+      gruntfile:
+        files: 'Gruntfile.coffee'
+        tasks: ['coffeelint']
+        options:
+          livereload: false
+
+      coffeesrc:
+        files: ['<%= appFiles.coffee %>']
+        # tasks: ['coffeelint:src', 'coffee:source', 'copy:buildAppjs']
+        tasks: ['build']
+
+      coffeeunit:
+        files: ['<%= appFiles.coffeeunit %>']
+        tasks: ['coffeelint:test', 'karma:unit:run']
+        options:
+          livereload: false
+
+  grunt.initConfig grunt.util._.extend taskConfig, userConfig
+  grunt.renameTask 'watch', 'delta'
+  grunt.registerTask ''
+
+
+  ###
+  In order to avoid having to specify manually the files needed for karma to
+  run, we use grunt to manage the list for us. The `karma/*` files are
+  compiled as grunt templates for use by Karma. Yay!
+  ###
+  grunt.registerMultiTask 'karmaconfig', 'Process karma config templates', ->
+    jsFiles = filterForJS @filesSrc
+    buildDir = grunt.config 'buildDir'
+    templatePath = 'karma/karma-unit.tpl.coffee'
+
+    grunt.file.copy templatePath, "#{buildDir}/karma-unit.coffee",
+      process: (contents, path) ->
+        grunt.template.process contents,
+          data:
+            scripts: jsFiles
+
+    grunt.task.run 'coffee:karmaConfig'


gruntfile improved

gruntfile improved

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -42,8 +42,8 @@
           compileDebug: false
           client: true
           namespace: 'app.templates'
-          processName: (file)-> file.replace(/views\/client\/([\w\/]+).jade/gi, '$1').replace('/', '_')
-        src: ['views/client/*.jade', 'views/client/**/*.jade']
+          processName: (file)-> file.replace(/views\/client\/([\w\/]+).jade/gi, '$1')
+        src: ['views/client/**/*.jade']
         dest: 'public/assets/views.js'

@@ -56,13 +56,6 @@

     uglify:
-      # options:
-        # mangle: false
-        # compress:
-        #   dead_code: true
-        # sourceMap: (file)-> file.replace(/\.js$/, '.map').replace(/\-min/, '')
-        # sourceMappingURL: (file)-> file.replace(/\.js$/, '.map').replace(/^public/, '').replace(/\-min/, '')
-
       app:
         src: 'public/assets/app.js'
         dest: 'public/assets/app-min.js'
@@ -136,43 +129,34 @@

       app_js:
         files: [
-          'app/client/*.js'
-          'app/client/*.coffee'
-
           'app/client/**/*.js'
           'app/client/**/*.coffee'

-          'app/shared/*.js'
-          'app/shared/*.coffee'
+          'app/shared/**/*.js'
+          'app/shared/**/*.coffee'

           '!app/client/dependencies.js'
-
-          '!app/client/vendor/*.js'
-          '!app/client/vendor/*.coffee'
-
-          '!app/client/vendor/**/*.js'
-          '!app/client/vendor/**/*.coffee'
         ]
         tasks: ['snocketsify:app']

       libs_js:
         files: [
           'app/client/dependencies.js'
-
-          'app/client/vendor/*.js'
-          'app/client/vendor/*.coffee'
-
-          'app/client/vendor/**/*.js'
-          'app/client/vendor/**/*.coffee'
         ]
         tasks: ['snocketsify:dependencies']

       css:
-        files: ['css/*.css', 'css/*.styl', 'css/**/*.css', 'css/**/*.styl']
+        files: [
+          'css/**/*.css'
+          'css/**/*.styl'
+        ]
         tasks: ['stylus']

       views:
-        files: ['views/client/*.jade', 'views/client/**/*.jade']
+        files: [
+          'views/client/**/*.jade'
+          'views/shared/**/*.jade'
+        ]
         tasks: ['jade2js']

       # images:
@@ -184,15 +168,6 @@

-    # mapper:
-    #   options:
-    #     fileRoot: 'public/assets/'
-    #     urlRoot: '/assets/'
-    #   defaults:
-    #     src: 'public/assets/hashmap.json'
-
-
-
   )

   grunt.loadNpmTasks('grunt-contrib-clean')
@@ -211,7 +186,7 @@
   grunt.loadNpmTasks('grunt-contrib-watch')

   # TODO:
-  # 1. Update Jade
+  # 1. Update Jade compiler

   grunt.registerTask('default', [
     'clean'


Remove further duplication from Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -580,8 +580,6 @@

               'generic_ui/scripts/copypaste.js'
               'generic_ui/scripts/get_logs.js'
-              'scripts/context.static.js'
-              'scripts/background.static.js'
               '!**/*spec*'

               'generic_ui/style/*.css'
@@ -613,11 +611,9 @@
               '**/freedom-module.json'
               '!generic_core/freedom-module.json'
               '**/*.static.js'
-              '!**/*spec*'

               'icons/*'
               'fonts/*'
-              '_locales/**'
             ]
             dest: 'build/dist/cca'
           }


Enable capture exceptions in grunt file.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -68,7 +68,7 @@
       coverage:
         command: "./node_modules/.bin/istanbul cover ./node_modules/.bin/jasmine-node --captureExceptions test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
       jasmine:
-        command: "./node_modules/.bin/jasmine-node --verbose --coffee src/spec"
+        command: "./node_modules/.bin/jasmine-node --verbose --captureExceptions --coffee src/spec"
       run:
         command: "node lib/run.js stock.xml"
       publish:


Edit Gruntfile

Edit Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -93,4 +93,11 @@
     ]
   )

+  grunt.registerTask(
+    "watch"
+    [
+      "esteWatch"
+    ]
+  )

+


update jshint setting to lint test scripts

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -21,12 +21,25 @@

     jshint:
       options:
-        evil: true
         camelcase: true
         trailing: true
         indent: 2
+        evil: true
+        browser: true
         node: true
+        globals:
+          'it': true
+          'describe': true
       main: ['src/*.js']
+      test_node: ['test/node/*.js']
+      test_browser:
+        options:
+          globals:
+            'chai': true
+            'isogram': true
+            'it': true
+            'describe': true
+        src: ['test/browser/*.js']

     uglify:
       options:


jade now jades all the things

jade now jades all the things

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -83,16 +83,15 @@
     jade:
       all:
         options:
+          basePath: './'
           extension: '.html'
           client: false
           pretty: true
         files:
-          'assets/partial/': [
+          './': [
+            'index.jade'
             'assets/partial/*.jade'
             'assets/partial/**/*.jade'
-          ]
-          './': [
-            'index.jade'
           ]
     karma:
       unit:


mad gruntfile tidying

mad gruntfile tidying

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,10 +1,20 @@
-# Official Grunt tasks to load
+# Officially-maintained Grunt tasks to load
 contribs = ['stylus', 'watch', 'jst', 'connect', 'copy', 'jshint', 'uglify']
-# Other Grunt tasks to load
+# Third-party Grunt tasks to load
 plugins = ['rsync', 'bake', 'sails-linker', 'jsdoc']

+# Install directory of all third party-assets
 vendor = 'vendor/'
+# Directory to build the site to
+site = 'site/'
+# File extensions
 js = '.js'
+css = '.css'
+# Bash wildcard patterns to match all
+# JS files
+js_src = 'js/**/*.js'
+# HTML templates
+templates = 'templates/**/*.html'

 # Library paths
 libs =
@@ -24,26 +34,34 @@
   tab: 'bootstrap/js/tab'
   server: 'felina-js/src/main'

+# Array to hold values of library map
 lib_list = []

+# List of full vendor directories to be included in the built site
 dir_list = [
-  'vendor/dropzone'
-  'vendor/webshim/js-webshim/minified'
-  'vendor/bootstrap/dist/fonts'
+  vendor + 'dropzone'
+  vendor + 'webshim/js-webshim/minified'
+  vendor + 'bootstrap/dist/fonts'
   'img'
 ]

+# List of vendor stylesheets to be included in the built site
 css_list = [
-  'vendor/bootstrap/dist/css/bootstrap.css'
-  'vendor/blueimp-gallery/css/blueimp-gallery.css'
-  'vendor/blueimp-bootstrap-image-gallery/css/bootstrap-image-gallery.css'
+  'bootstrap/dist/css/bootstrap'
+  'blueimp-gallery/css/blueimp-gallery'
+  'blueimp-bootstrap-image-gallery/css/bootstrap-image-gallery'
 ]

+# Add the root directory and file extension to CSS files
+css_list = css_list.map((path) -> vendor + path + css)
+
+# Add the root directory and file extension to JS files and put them in an array
 for k, v of libs
   full = vendor + v + js
   libs[k] = full
   lib_list.push(full)

+# Make a list of all vendor resources -- JS, CSS and directories
 all_list = lib_list.concat(dir_list).concat(css_list)

 # Module paths
@@ -104,12 +122,11 @@
 for k, v of dependencies
   dependencies[k] = shared.concat(v)

-js_src = 'js/**/*.js'

 # Mapping of source HTML pages to their output paths in the site directory
 bake_map = {}
 for k, v of dependencies
-  bake_map[k] = k.replace('site', 'html')
+  bake_map[k] = k.replace(site, 'html/')

 module.exports = (grunt) ->
   grunt.initConfig
@@ -162,9 +179,6 @@
       lib_test:
         src: js_src

-    qunit:
-      files: ['test/**/*.html']
-
     jst:
       options:
         processName: (name) ->
@@ -172,14 +186,14 @@

       compile:
         files:
-          'site/jst.js': ['templates/**/*.html']
+          site + jst_: [templates]

     stylus:
       compile:
         expand: true
         cwd: 'stylus'
         src: ['*.styl']
-        dest: 'site/css/'
+        dest: site + 'css/'
         ext: '.css'

     watch:
@@ -193,7 +207,7 @@
         tasks: ['stylus']
       # Render HTML/Underscore templates to a JST object when they change
       templates:
-        files: 'templates/**/*.html'
+        files: templates
         tasks: ['jst']
       # Run static imports/preprocessing on the main HTML pages when they change
       html:
@@ -204,19 +218,19 @@
       server:
         options:
           port: 9000
-          base: 'site'
+          base: site
           keepalive: true

     copy:
       default:
         cwd: '.'
         src: [js_src, 'data/**/*']
-        dest: 'site/'
+        dest: site

     'sails-linker':
       default:
         options:
-          appRoot: 'site/'
+          appRoot: site
           fileTmpl: '<script src="/%s"></script>'
           relative: true
         files: dependencies
@@ -234,15 +248,15 @@
     rsync:
       options:
         args: ['--relative', '--recursive']
-        exclude: ['.git*', 'node_modules']
-        recursive: true
       dist:
         options:
           src: all_list
-          dest: 'site'
-
+          dest: site
+
+  # Load all Grunt plugins
   grunt.loadNpmTasks "grunt-contrib-#{contrib}" for contrib in contribs
   grunt.loadNpmTasks "grunt-#{plugin}" for plugin in plugins

+  # Define custom composite tasks in terms of other tasks
   grunt.registerTask 'default', ['jshint', 'jst', 'stylus', 'bake', 'copy', 'sails-linker', 'rsync']
   grunt.registerTask 'release', ['jshint', 'uglify', 'concat']


Remove duplicate jquery

Remove duplicate jquery

ಠ_ಠ

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -25,7 +25,6 @@
           paths: ['./client/source', 'node_modules']
         alias: [
           './bower_components/backbone/backbone.js:backbone'
-          './bower_components/jquery/dist/jquery.js:jquery'
           './bower_components/chaplin/chaplin.js:chaplin'
           './bower_components/cocktail/Cocktail.js:cocktail'
           './bower_components/underscore/underscore.js:underscore'


Tweak Grunt task to run and report test results.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -107,6 +107,9 @@
     mocha:
       test:
         src: ['spec/**/*.html']
+        options:
+          run: true
+          reporter: 'Dot'

   grunt.loadNpmTasks 'grunt-contrib-clean'
   grunt.loadNpmTasks 'grunt-contrib-compress'


for module testing, timeTracker must be grobal.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -49,6 +49,8 @@
           ext: '.js'
         ]
       test:
+        options:
+          bare: true
         files: [
           expand: true
           cwd: 'test/'


default: build before linting!

default: build before linting!

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -166,6 +166,6 @@
     grunt.registerTask 'test', ['karma']
     grunt.registerTask 'build', ['concat', 'sass']
     grunt.registerTask 'minify', ['cssmin', 'uglify']
-    grunt.registerTask 'default', ['lint', 'update_web', 'test', 'minify']
+    grunt.registerTask 'default', ['concat', 'lint', 'update_web', 'test', 'minify']
     grunt.registerTask 'update_web', ['build', 'copy:web']
     grunt.registerTask 'quickbuild', ['update_web', 'minify']


necessary changes to gruntfile to make last commit work

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -180,7 +180,7 @@

     grunt.log.writeln "Replacing placeholder with hash value in file \"#{destination_file_path}\"."
     content = grunt.file.read destination_file_path
-    content_with_hash = content.replace /&\*HASH_PLACE_HOLDER\*&/, source_file_hash
+    content_with_hash = content.replace /__HASH_PLACEHOLDER_FOR_GRUNT__/, source_file_hash
     grunt.file.write destination_file_path, content_with_hash

   grunt.registerTask 'core', ['concat:core', 'uglify:core', 'corehasher']


Set system locale to fix build errors

Set system locale to fix build errors

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -22,9 +22,9 @@
         tasks: ['watch', 'exec:serve']
     exec:
       build:
-        cmd: 'bundle exec jekyll build'
+        cmd: 'export LC_CTYPE="C_BINARY" && bundle exec jekyll build'
       serve:
-        cmd: 'bundle exec jekyll serve --watch'
+        cmd: 'export LC_CTYPE="C_BINARY" && bundle exec jekyll serve --watch'
     less:
       blogTheme:
         options:
@@ -98,4 +98,3 @@
   grunt.registerTask('dist-css', ['copy', 'less', 'minify-css', 'clean'])
   grunt.registerTask('default', ['dist-css', 'exec:build'])
   grunt.registerTask('serve', ['concurrent'])
-


renaming task phonegap:debug to phonegap:debugios

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -520,7 +520,7 @@

   # phonegap tasks
   # ------------------------------------------------------------
-  grunt.registerTask 'phonegap:debug', [
+  grunt.registerTask 'phonegap:debugios', [
     'debug'
     'copy:debugphonegapwww'
     'copy:debugphonegapassets'


Compile DOM classes too

Compile DOM classes too

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -47,6 +47,14 @@
         src: ['**.coffee']
         dest: 'lib'
         ext: '.js'
+      dom:
+        options:
+          bare: true
+        expand: true
+        cwd: 'src/dom'
+        src: ['**.coffee']
+        dest: 'lib/dom'
+        ext: '.js'
       spec:
         options:
           bare: true
@@ -71,6 +79,6 @@
   @loadNpmTasks 'grunt-mocha-phantomjs'
   @loadNpmTasks 'grunt-contrib-watch'

-  @registerTask 'build', ['component', 'component_build', 'uglify']
-  @registerTask 'test', ['build',  'coffee', 'mocha_phantomjs']
+  @registerTask 'build', ['coffee', 'component', 'component_build', 'uglify']
+  @registerTask 'test', ['build',  'mocha_phantomjs']
   @registerTask 'default', ['build']


Added production environment to grunt file.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -140,6 +140,10 @@
         constants:
           environment: 'test'

+      production:
+        constants:
+          environment: 'production'
+
   grunt.loadTasks "tasks"

   grunt.loadNpmTasks('grunt-haml')


Show OTHER_ADDRESS contact fields

Show OTHER_ADDRESS contact fields

--- src/models.coffee
+++ src/models.coffee
@@ -383,7 +383,7 @@
                     .value()

             highlights = _.filter @get('connections'), (c) ->
-                (c.section_type in ['OTHER_INFO', 'TOPICAL']) and p13n.getLanguage() of c.name
+                (c.section_type in ['OTHER_INFO', 'TOPICAL', 'OTHER_ADDRESS']) and p13n.getLanguage() of c.name
             data.highlights = _.sortBy highlights, (c) -> c.order

             contact = _.filter @get('connections'), (c) ->


Fix typo.

Fix typo.

--- src/models.coffee
+++ src/models.coffee
@@ -150,7 +150,7 @@
             for service in @get 'services'
                 if not level or service.level < level
                     specifier_text = service.name[p13n.get_language()]
-                    lelel = service.level
+                    level = service.level
             return specifier_text

         toJSON: (options) ->


adding Luca.util.read method, which gets the value of a property whether it is a method or a value

--- src/util.coffee
+++ src/util.coffee
@@ -20,6 +20,8 @@
 Luca.util.argumentsLogger = (prompt)->
   ()-> console.log prompt, arguments

+Luca.util.read = (property, args...)->
+  if _.isFunction(property) then property.apply(@, args) else property

 # turns a word like form_view into FormView
 Luca.util.classify = (string="")->
@@ -116,10 +118,3 @@
   cssClass += " #{ baseClass }-#{ type }" if type?
   Luca.util.make("span",{class:cssClass},contents)

-Luca.util.inspectComponent = (component)->
-  {
-    name:         component.name
-    instanceOf:   component.displayName
-    subclassOf:   component._superClass()::displayName
-    inheritsFrom: Luca.parentClasses( component )
-  }


Allowed '*!null' to match null state.

Allowed '*!null' to match null state.

--- src/view.coffee
+++ src/view.coffee
@@ -175,7 +175,7 @@
                 if rule[0] is '*'
                     excludes = rule.split('!').slice(1)

-                    return not _.contains(excludes, state)
+                    return not _.contains(excludes, '' + state)

                 return false


Added regexp to split formatters. The regexp allows pipes in quoted arguments. Fixes #432

--- src/view.coffee
+++ src/view.coffee
@@ -34,7 +34,7 @@
   buildBinding: (binding, node, type, declaration) =>
     options = {}

-    pipes = (pipe.trim() for pipe in declaration.split '|')
+    pipes = (pipe.trim() for pipe in declaration.match /((?:'[^']*')*(?:(?:[^\|']+(?:'[^']*')*[^\|']*)+|[^\|]+))|^$/g)
     context = (ctx.trim() for ctx in pipes.shift().split '<')
     keypath = context.shift()


Fixed preloaded content lists with just one item would fail to recognise the item was there and create a duplicate in the DOM.

--- src/view.coffee
+++ src/view.coffee
@@ -406,8 +406,11 @@
 			# Set default to null.
 			view = null

+			# Normalize content views to array if there is only one.
+			views = if Array.isArray(@views.content) then @views.content else [@views.content]
+
 			# Iterate all our content views.
-			for v in @views.content
+			for v in views
 				# If the data-id attribute is the object's id, it belongs to the object; set view and break from loop.
 				if v.el.getAttribute('data-id') is object.id
 					view = v


Fixed other notification text

Fixed other notification text

--- core/app/backbone/views/share/preview_share_fact_view.coffee
+++ core/app/backbone/views/share/preview_share_fact_view.coffee
@@ -15,7 +15,7 @@
   _share: ->
     @model.share @options.provider_name,
       success: =>
-        FactlinkApp.NotificationCenter.success "Sharing to #{@_niceProviderName()} in a minute!"
+        FactlinkApp.NotificationCenter.success "Sharing to #{@_niceProviderName()}."
         @trigger 'success'
       error: =>
         FactlinkApp.NotificationCenter.error "Error when sharing to #{@_niceProviderName()}"


payment: observe inputs to enable/disable button

--- client/Main/payment/form.coffee
+++ client/Main/payment/form.coffee
@@ -28,8 +28,35 @@

     @state = KD.utils.extend @getInitialState(), state

+    @isInputValidMap = {}
+
     @initViews()
     @initEvents()
+
+
+  observeForm: ->
+
+    isVisible = (key) => @form.inputs[key]?.getOption('cssClass') isnt 'hidden'
+
+    Object.keys(@form.inputs).filter(isVisible).map (name) =>
+
+      input = @form.inputs[name]
+
+      @isInputValidMap[name] = no
+
+      input.on 'ValidationResult', (isValid) =>
+
+        @handleFormInputValidation name, isValid
+
+
+  handleFormInputValidation: (name, isValid) ->
+
+    @isInputValidMap[name] = isValid
+
+    formIsValid = yes
+    formIsValid = formIsValid and value  for own key, value of @isInputValidMap
+
+    @emit 'GotValidationResult', formIsValid

   initViews: ->
@@ -49,6 +76,8 @@
       partial  : pricePartial

     @form = @initForm()
+
+    @observeForm()

     @existingCreditCardMessage = new KDCustomHTMLView
       cssClass : 'existing-cc-msg'
@@ -162,6 +191,15 @@
       cardNumber.setClass type.toLowerCase()

     @paypalForm.on 'PaypalTokenLoaded', @bound 'initPaypalClient'
+
+    @on 'GotValidationResult', @bound 'handleValidationResult'
+
+
+  handleValidationResult: (isValid) ->
+
+    if isValid
+    then @submitButton.enable()
+    else @submitButton.disable()

   showValidationErrorsOnInputs: (error) ->


Use project passed in via options hash

Use project passed in via options hash

--- src/git.coffee
+++ src/git.coffee
@@ -61,7 +61,7 @@

     @statuses = {}
     @upstream = {ahead: 0, behind: 0}
-    {project, refreshOnWindowFocus} = options
+    {@project, refreshOnWindowFocus} = options

     refreshOnWindowFocus ?= true
     if refreshOnWindowFocus
@@ -70,8 +70,8 @@
         @refreshIndex()
         @refreshStatus()

-    if atom.project?
-      @subscribe atom.project.buffers.onEach (buffer) => @subscribeToBuffer(buffer)
+    if @project?
+      @subscribe @project.buffers.onEach (buffer) => @subscribeToBuffer(buffer)

   # Private: Subscribes to buffer events.
   subscribeToBuffer: (buffer) ->
@@ -145,7 +145,7 @@
   # Public: Returns true if at the root, false if in a subfolder of the
   # repository.
   isProjectAtRoot: ->
-    @projectAtRoot ?= atom.project.relativize(@getWorkingDirectory()) is ''
+    @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is ''

   # Public: Makes a path relative to the repository's working directory.
   relativize: (path) -> @getRepo().relativize(path)


adding object merging for producing api call options

--- src/giphy.coffee
+++ src/giphy.coffee
@@ -32,6 +32,14 @@
   timeout: Number(process.env.HUBOT_GIPHY_TIMEOUT) or null
   apiKey: process.env.HUBOT_GIPHY_API_KEY
 })
+
+extend = (object, properties) ->
+  for key, val of properties
+    object[key] = val if val
+  object
+
+merge = (options, overrides) ->
+  extend (extend {}, options), overrides

 class Giphy

@@ -112,8 +120,13 @@
   getSearchUri: (state) ->
     @log "getSearchUri:", state
     if state.args and state.args.length > 0
-      state.options.q = state.args
-      @api.search state.options, (err, res) =>
+      # state.options.q = state.args
+      options = merge {
+        q: state.args,
+        limit: process.env.HUBOT_GIPHY_DEFAULT_LIMIT
+        rating: process.env.HUBOT_GIPHY_DEFAULT_RATING
+      }, state.options
+      @api.search options, (err, res) =>
         if err
           @error state.msg, err
         else


no need to instrument this statement as it is only used for instrumentation

--- src/giphy.coffee
+++ src/giphy.coffee
@@ -168,5 +168,6 @@
     giphy.respond msg

   # this allows testing to instrument the giphy instance
+  ### istanbul ignore next ###
   if NODE_ENV == 'development'
     giphy


AppController#moveTab return if active tab has no parent

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -555,6 +555,8 @@
     @activeTabView.emit 'DrawingPaneRequested', paneHash

   moveTab: (direction) ->
+    return unless @activeTabView.parent?
+
     panel = @activeTabView.parent.parent
     return  unless panel instanceof KDSplitViewPanel


IDE: updateWorkspace explicitly returns promise

fixes cannot read property .then of undefinedQ

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1046,9 +1046,8 @@

   updateWorkspace: (options = {}) ->

-    KD.remote.api.JWorkspace.update @workspaceData._id, { $set : options }
-
-    log 'workspace data updated with real one'
+    return KD.remote.api.JWorkspace.update @workspaceData._id, { $set : options }
+

   startChatSession: (callback) ->


Collaboration: Fixed throwing realtime error if the vm is not turned on.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1010,10 +1010,10 @@
     switch change.type

       when 'NewPaneCreated'
-        @mySnapshot.set paneHash, change
+        @mySnapshot.set paneHash, change  if paneHash

       when 'PaneRemoved'
-        @mySnapshot.delete paneHash
+        @mySnapshot.delete paneHash  if paneHash

   watchParticipant: (nickname) -> @myWatchMap.set nickname, nickname


Pricing: Define new sub paths in /Pricing

--- client/pricing/lib/index.coffee
+++ client/pricing/lib/index.coffee
@@ -8,8 +8,12 @@
 module.exports = class PricingAppController extends KDViewController

   @options =
-    name  : 'Pricing'
-    route : '/:name?/Pricing'
+    name    : 'Pricing'
+    routes  :
+      '/:name?/Pricing'             : null
+      '/:name?/Pricing/Teams'       : null
+      '/:name?/Pricing/Individuals' : null
+

   constructor: (options = {}, data) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -25,7 +25,7 @@
         else
           easingFunction = data.easing

-        I[property] = easing((t - data.startTime) / data.duration)
+        I[property] = easingFunction((t - data.startTime) / data.duration)

   ###*
   Modify the object's properties over time.