Replace to jasmine matcher

Replace to jasmine matcher

From expect.js matcher

--- spec/Refactor-spec.coffee
+++ spec/Refactor-spec.coffee
@@ -1,4 +1,3 @@
-expect = require 'expect.js'
 Refactor = require '../lib/Refactor'
 { Range } = require 'atom'

@@ -16,45 +15,30 @@

       it 'should find no reference of whitespace', ->
         refs = refactor.find new Range [0, 1], [0, 2]
-        expect(refs).to.have.length 0
+        expect(refs).toHaveLength 0

       it 'should find no reference of operator', ->
         refs = refactor.find new Range [0, 2], [0, 3]
-        expect(refs).to.have.length 0
+        expect(refs).toHaveLength 0
         refs = refactor.find new Range [1, 6], [1, 7]
-        expect(refs).to.have.length 0
+        expect(refs).toHaveLength 0

       it 'should find a reference of variable', ->
         refs = refactor.find new Range [0, 0], [0, 1]
-        expect(refs).to.have.length 1
-        expect(refs[0].range.start.row).to.be 1
-        expect(refs[0].range.start.column).to.be 4
-        expect(refs[0].range.end.row).to.be 1
-        expect(refs[0].range.end.column).to.be 5
+        expect(refs).toHaveLength 1
+        expect(refs[0].range).toEqual new Range [1, 4], [1, 5]

       it 'should find references of variable', ->
         refs = refactor.find new Range [1, 0], [1, 1]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 0
-        expect(refs[0].range.start.column).to.be 4
-        expect(refs[0].range.end.row).to.be 0
-        expect(refs[0].range.end.column).to.be 5
-        expect(refs[1].range.start.row).to.be 1
-        expect(refs[1].range.start.column).to.be 8
-        expect(refs[1].range.end.row).to.be 1
-        expect(refs[1].range.end.column).to.be 9
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [0, 4], [0, 5]
+        expect(refs[1].range).toEqual new Range [1, 8], [1, 9]

       it 'should find references of value', ->
         refs = refactor.find new Range [1, 8], [1, 9]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 0
-        expect(refs[0].range.start.column).to.be 4
-        expect(refs[0].range.end.row).to.be 0
-        expect(refs[0].range.end.column).to.be 5
-        expect(refs[1].range.start.row).to.be 1
-        expect(refs[1].range.start.column).to.be 0
-        expect(refs[1].range.end.row).to.be 1
-        expect(refs[1].range.end.column).to.be 1
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [0, 4], [0, 5]
+        expect(refs[1].range).toEqual new Range [1, 0], [1, 1]

     do ->
       refactor = new Refactor """
@@ -68,23 +52,14 @@

       it 'should find function scoped refs', ->
         refs = refactor.find new Range [2, 8], [2, 9]
-        expect(refs).to.have.length 1
-        expect(refs[0].range.start.row).to.be 3
-        expect(refs[0].range.start.column).to.be 2
-        expect(refs[0].range.end.row).to.be 3
-        expect(refs[0].range.end.column).to.be 3
+        expect(refs).toHaveLength 1
+        expect(refs[0].range).toEqual new Range [3, 2], [3, 3]

       it 'should find function external scoped refs', ->
         refs = refactor.find new Range [3, 6], [3, 7]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 1
-        expect(refs[0].range.start.column).to.be 0
-        expect(refs[0].range.end.row).to.be 1
-        expect(refs[0].range.end.column).to.be 1
-        expect(refs[1].range.start.row).to.be 5
-        expect(refs[1].range.start.column).to.be 12
-        expect(refs[1].range.end.row).to.be 5
-        expect(refs[1].range.end.column).to.be 13
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [1, 0], [1, 1]
+        expect(refs[1].range).toEqual new Range [5, 12], [5, 13]

     do ->
       refactor = new Refactor """
@@ -97,27 +72,15 @@

       it 'should find ref from outer of object', ->
         refs = refactor.find new Range [0, 0], [0, 1]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 3
-        expect(refs[0].range.start.column).to.be 7
-        expect(refs[0].range.end.row).to.be 3
-        expect(refs[0].range.end.column).to.be 8
-        expect(refs[1].range.start.row).to.be 4
-        expect(refs[1].range.start.column).to.be 9
-        expect(refs[1].range.end.row).to.be 4
-        expect(refs[1].range.end.column).to.be 10
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [3, 7], [3, 8]
+        expect(refs[1].range).toEqual new Range [4, 9], [4, 10]

       it 'should find ref from inner of object', ->
         refs = refactor.find new Range [3, 11], [3, 12]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 1
-        expect(refs[0].range.start.column).to.be 0
-        expect(refs[0].range.end.row).to.be 1
-        expect(refs[0].range.end.column).to.be 1
-        expect(refs[1].range.start.row).to.be 4
-        expect(refs[1].range.start.column).to.be 13
-        expect(refs[1].range.end.row).to.be 4
-        expect(refs[1].range.end.column).to.be 14
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [1, 0], [1, 1]
+        expect(refs[1].range).toEqual new Range [4, 13], [4, 14]

     do ->
       refactor = new Refactor """
@@ -131,27 +94,15 @@

       it 'should find ref from outer of array', ->
         refs = refactor.find new Range [0, 0], [0, 1]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 3
-        expect(refs[0].range.start.column).to.be 2
-        expect(refs[0].range.end.row).to.be 3
-        expect(refs[0].range.end.column).to.be 3
-        expect(refs[1].range.start.row).to.be 4
-        expect(refs[1].range.start.column).to.be 2
-        expect(refs[1].range.end.row).to.be 4
-        expect(refs[1].range.end.column).to.be 3
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [3, 2], [3, 3]
+        expect(refs[1].range).toEqual new Range [4, 2], [4, 3]

       it 'should find ref from inner of array', ->
         refs = refactor.find new Range [3, 6], [3, 7]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 1
-        expect(refs[0].range.start.column).to.be 0
-        expect(refs[0].range.end.row).to.be 1
-        expect(refs[0].range.end.column).to.be 1
-        expect(refs[1].range.start.row).to.be 4
-        expect(refs[1].range.start.column).to.be 6
-        expect(refs[1].range.end.row).to.be 4
-        expect(refs[1].range.end.column).to.be 7
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [1, 0], [1, 1]
+        expect(refs[1].range).toEqual new Range [4, 6], [4, 7]

     do ->
       refactor = new Refactor """
@@ -162,24 +113,12 @@

       it 'should find ref of child class', ->
         refs = refactor.find new Range [0, 6], [0, 7]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 1
-        expect(refs[0].range.start.column).to.be 16
-        expect(refs[0].range.end.row).to.be 1
-        expect(refs[0].range.end.column).to.be 17
-        expect(refs[1].range.start.row).to.be 2
-        expect(refs[1].range.start.column).to.be 16
-        expect(refs[1].range.end.row).to.be 2
-        expect(refs[1].range.end.column).to.be 17
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [1, 16], [1, 17]
+        expect(refs[1].range).toEqual new Range [2, 16], [2, 17]

       it 'should find ref of extends class', ->
         refs = refactor.find new Range [1, 16], [1, 17]
-        expect(refs).to.have.length 2
-        expect(refs[0].range.start.row).to.be 0
-        expect(refs[0].range.start.column).to.be 6
-        expect(refs[0].range.end.row).to.be 0
-        expect(refs[0].range.end.column).to.be 7
-        expect(refs[1].range.start.row).to.be 2
-        expect(refs[1].range.start.column).to.be 16
-        expect(refs[1].range.end.row).to.be 2
-        expect(refs[1].range.end.column).to.be 17
+        expect(refs).toHaveLength 2
+        expect(refs[0].range).toEqual new Range [0, 6], [0, 7]
+        expect(refs[1].range).toEqual new Range [2, 16], [2, 17]


Kill commented specs that were used as a reminder

--- spec/app/pane-spec.coffee
+++ spec/app/pane-spec.coffee
@@ -503,23 +503,3 @@
     it "can serialize and deserialize the pane and all its serializable items", ->
       newPane = deserialize(pane.serialize())
       expect(newPane.getItems()).toEqual [editSession1, editSession2]
-
-#   This relates to confirming the closing of a tab
-#
-#   describe "when buffer is modified", ->
-#     it "triggers an alert and does not close the session", ->
-#       spyOn(editor, 'remove').andCallThrough()
-#       spyOn(atom, 'confirm')
-#       editor.insertText("I AM CHANGED!")
-#       editor.trigger "core:close"
-#       expect(editor.remove).not.toHaveBeenCalled()
-#       expect(atom.confirm).toHaveBeenCalled()
-#
-#     it "doesn't trigger an alert if the buffer is opened in multiple sessions", ->
-#       spyOn(editor, 'remove').andCallThrough()
-#       spyOn(atom, 'confirm')
-#       editor.insertText("I AM CHANGED!")
-#       editor.splitLeft()
-#       editor.trigger "core:close"
-#       expect(editor.remove).toHaveBeenCalled()
-#       expect(atom.confirm).not.toHaveBeenCalled()


Remove redundant spec count

Remove redundant spec count

--- spec/atom-reporter.coffee
+++ spec/atom-reporter.coffee
@@ -170,7 +170,7 @@
       if coreSpecs is 0 and bundledPackageSpecs is 0
         packageFolderName = path.basename(path.dirname(specs[0].specDirectory))
         packageName = _.undasherize(_.uncamelcase(packageFolderName))
-        @userHeader.text("#{packageName} Specs (#{userPackageSpecs})")
+        @userHeader.text("#{packageName} Specs")
       else
         @userHeader.text("User Package Specs (#{userPackageSpecs})")
     else


Remove some code duplication inside of grammar-spec

Create an @reloadGrammar function which can be called to remove the
cached grammar module and require it again.

--- spec/grammars-spec.coffee
+++ spec/grammars-spec.coffee
@@ -20,18 +20,18 @@
   describe 'Operating system specific runners', ->
     beforeEach ->
       @_originalPlatform = OperatingSystem.platform
+      @reloadGrammar = ->
+        delete require.cache[require.resolve('../lib/grammars.coffee')]
+        grammarMap = require '../lib/grammars.coffee'

     afterEach ->
       OperatingSystem.platform = @_originalPlatform
-      delete require.cache[require.resolve('../lib/grammars.coffee')]
-      # TODO: Validate that reassigns the global grammarMap variable
-      grammarMap = require '../lib/grammars.coffee'
+      @reloadGrammar()

     describe 'C', ->
       it 'returns the appropriate File Based runner on Mac OS X', ->
         OperatingSystem.platform = -> 'darwin'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['C']
         fileBasedRunner = grammar['File Based']
@@ -42,8 +42,7 @@

       it 'is not defined on other operating systems', ->
         OperatingSystem.platform = -> 'win32'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['C']
         expect(grammar).toBe(undefined)
@@ -51,8 +50,7 @@
     describe 'C++', ->
       it 'returns the appropriate File Based runner on Mac OS X', ->
         OperatingSystem.platform = -> 'darwin'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['C++']
         fileBasedRunner = grammar['File Based']
@@ -63,8 +61,7 @@

       it 'is not defined on other operating systems', ->
         OperatingSystem.platform = -> 'win32'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['C++']
         expect(grammar).toBe(undefined)
@@ -72,8 +69,7 @@
     describe 'Objective-C', ->
       it 'returns the appropriate File Based runner on Mac OS X', ->
         OperatingSystem.platform = -> 'darwin'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['Objective-C']
         fileBasedRunner = grammar['File Based']
@@ -84,8 +80,7 @@

       it 'is not defined on other operating systems', ->
         OperatingSystem.platform = -> 'win32'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['Objective-C']
         expect(grammar).toBe(undefined)
@@ -93,8 +88,7 @@
     describe 'Objective-C++', ->
       it 'returns the appropriate File Based runner on Mac OS X', ->
         OperatingSystem.platform = -> 'darwin'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['Objective-C++']
         fileBasedRunner = grammar['File Based']
@@ -105,8 +99,7 @@

       it 'is not defined on other operating systems', ->
         OperatingSystem.platform = -> 'win32'
-        delete require.cache[require.resolve('../lib/grammars.coffee')]
-        grammarMap = require '../lib/grammars.coffee'
+        @reloadGrammar()

         grammar = grammarMap['Objective-C++']
         expect(grammar).toBe(undefined)


tweak

tweak

--- lib/manifest.coffee
+++ lib/manifest.coffee
@@ -1,5 +1,4 @@
 async   = require("async")
-spawner = require("spawner").init()
 uuid    = require("node-uuid")

 class Manifest
@@ -14,7 +13,7 @@

   build: (options, cb) =>
     @save (err) =>
-      @builder.build(@manifest_url(), options, cb)
+      @builder.build @manifest_url(), options, cb

   save: (cb) ->
     manifest = new Buffer(JSON.stringify(@manifest), "binary")


Change disable_submit to oauth_redirect

Change disable_submit to oauth_redirect

--- src/js/collection-editor.coffee
+++ src/js/collection-editor.coffee
@@ -333,7 +333,7 @@
     authorization_expires_in = parseInt(get_cookie('access_token_expires_at')) - Date.now()
     console.log "Disabling submit in #{authorization_expires_in}ms"
     setTimeout ->
-      disable_submit()
+      oauth_redirect()
     , authorization_expires_in

 # set the author name using Google profile information
@@ -453,12 +453,11 @@
     window.location.href + new_hash
   history.pushState(null,$(selected).text(),new_url)

-# disable the submit button with an informative dialogue if the cookie expires while editing
-disable_submit = ->
+# disable submit, save form, and redirect to oauth when cookie expires
+oauth_redirect = ->
   $('#submit_button').prop('disabled',true)
-  $('#submit_button').before $('<div>').attr('class','alert alert-warning').attr('id','oauth_expiration_warning').append('Your Google Fusion Tables authorization has expired. ')
-  $('#oauth_expiration_warning').append $('<a>').attr('href',google_oauth_url()).append('Click here to save your work and re-authorize.').attr('onclick','save_collection_form()')
-  $('#oauth_expiration_warning').append(' You will be able to submit your work upon return.')
+  save_collection_form()
+  window.location.assign(google_oauth_url())

 build_collection_editor_from_capabilities = (capabilities_url) ->
   $.ajax capabilities_url,


leftover ussd string for action type

leftover ussd string for action type

--- static/coffee/flows/controllers.coffee
+++ static/coffee/flows/controllers.coffee
@@ -619,7 +619,7 @@
     # if its the base language, don't show the from text
     if Flow.language and Flow.flow.base_language != Flow.language.iso_code

-      if action.type in ["send", "reply", "say", "ussd"]
+      if action.type in ["send", "reply", "say"]

         fromText = action.msg[Flow.flow.base_language]


fix manual merge issue

fix manual merge issue

--- static/coffee/flows/controllers.coffee
+++ static/coffee/flows/controllers.coffee
@@ -1068,13 +1068,12 @@

       # set the operands
       else if rule.test.type != "between" and rule.test.type != "ward"
-
-      if rule.test.test
-        if rule._config.localized
-          rule.test._base = rule.test.test[Flow.flow.base_language]
-        else
-          rule.test =
-            _base: rule.test.test
+        if rule.test.test
+          if rule._config.localized
+            rule.test._base = rule.test.test[Flow.flow.base_language]
+          else
+            rule.test =
+              _base: rule.test.test

     # and finally the category name
     rule.category._base = rule.category[Flow.flow.base_language]


visual clockwise don't auto expand scanRange

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -108,7 +108,7 @@
         unless @patternForOccurence
           {pattern, bufferRange} = @getPatternAndBufferRangeForOccurrence()
           @patternForOccurence = pattern
-          if scanRanges?.length
+          if scanRanges?.length and not @isMode('visual', 'blockwise')
             lastRangeIndex = scanRanges.length - 1
             scanRanges[lastRangeIndex] = scanRanges[lastRangeIndex].union(bufferRange)


reduce dependency to event

reduce dependency to event

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -83,18 +83,8 @@
     else
       selection.destroy()

+  # now only for occurence
   observeSelectTarget: ->
-    unless @instanceof('Select')
-      if @needStay()
-        @onWillSelectTarget =>
-          unless @isMode('visual')
-            @updateSelectionProperties()
-            console.log 'update prop on will-select-target'
-      # else
-      #   @onDidSelectTarget =>
-      #     console.log 'update prop on did-select-target'
-      #     @updateSelectionProperties()
-
     if @isWithOccurrence()
       scanRanges = null
       @onWillSelectTarget =>
@@ -164,6 +154,10 @@
     @observeSelectTarget()
     @saveStartOfSelections() if @isMode('visual')

+    if not @instanceof('Select') and @needStay()
+      unless @isMode('visual')
+        @updateSelectionProperties()
+        console.log 'update prop on will-select-target'
     @emitWillSelectTarget()
     @target.select()
     @saveStartOfSelections() unless @_restoreStartOfSelections?


:art: refer displayNameTable, for being declarative

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -335,11 +335,15 @@
     'IncrementNumber'
     'DecrementNumber'
   ]
+  displayNameTable: {
+    'EncodeUriComponent': 'Encode URI Component'
+    'DecodeUriComponent': 'Decode URI Component'
+  }

   getItems: ->
-    @transformers.map (klass) ->
+    @transformers.map (klass) =>
       className = if _.isString(klass) then klass else klass.name
-      displayName = _.humanizeEventName(_.dasherize(className)).replace(/\bUri\b/, 'URI')
+      displayName = @displayNameTable[className] ? _.humanizeEventName(_.dasherize(className))
       {name: klass, displayName}

   initialize: ->


suppress highlight for linewise-delete, its just noisy

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -432,8 +432,10 @@
   setToFirstCharacterOnLinewise: true

   execute: ->
-    if @target.wise is 'blockwise'
-      @restorePositions = false
+    @onDidSelectTarget =>
+      @flashTarget = false if (@occurrenceSelected and @occurrenceWise is "linewise")
+
+    @restorePositions = false  if @target.wise is 'blockwise'
     super

   mutateSelection: (selection) ->


#137 accuracy improved e.g. `3Oabc` at frist line.

Now separately manage inserted text by Operator and inserted text by
user’s input.
Why this require is when type `3Oabc`, and
getNewTextRangeFromCheckpoint(editor, ‘undo’) include range user’s
input. Since user’s insertion start at point BEFORE operator’s
insertion point.
To avoid this complexity, I introduced textByOperator instance variable
which store inserted text BEFORE user’s input(insertion) start.

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -763,31 +763,23 @@
     range = getNewTextRangeFromCheckpoint(@editor, @getCheckpoint(purpose))
     fn(range) if range?

-  repeatInsertCountTimes: (count) ->
-    @withAddedBufferRangeFromCheckpoint 'undo', (range) =>
-      text = @editor.getTextInBufferRange(range)
-      _.times count, =>
-        for selection in @editor.getSelections()
-          selection.insertText(text, autoIndent: true)
-
   observeWillDeactivateMode: ->
-    # Need to preserve here since count is reset on operationStack::process finished()
-    count = @getCount() - 1
     disposable = @vimState.modeManager.preemptWillDeactivateMode ({mode}) =>
       return unless mode is 'insert'
       disposable.dispose()

       @vimState.mark.set('^', @editor.getCursorBufferPosition())
       text = ''
-      @withAddedBufferRangeFromCheckpoint 'insert', (range) =>
-        # Marker can track following extra insert incase count specified
-        @setMarkForChange(range)
+      if (range = getNewTextRangeFromCheckpoint(@editor, @getCheckpoint('insert')))?
+        @setMarkForChange(range) # Marker can track following extra insertion incase count specified
         text = @editor.getTextInBufferRange(range)
-        @saveInsertedText(text)
-        @vimState.register.set('.', {text})
-
-      if @supportInsertionCount and count > 0
-        @repeatInsertCountTimes(count)
+      @saveInsertedText(text)
+      @vimState.register.set('.', {text})
+
+      _.times @getInsertionCount(), =>
+        text = @textByOperator + @getInsertedText()
+        for selection in @editor.getSelections()
+          selection.insertText(text, autoIndent: true)

       # grouping changes for undo checkpoint need to come last
       @editor.groupChangesSinceCheckpoint(@getCheckpoint('undo'))
@@ -814,6 +806,10 @@
   # called when repeated
   repeatInsert: (selection, text) ->
     selection.insertText(text, autoIndent: true)
+
+  getInsertionCount: ->
+    @insertionCount ?= if @supportInsertionCount then (@getCount() - 1) else 0
+    @insertionCount

   execute: ->
     if @isRepeated()
@@ -827,6 +823,9 @@
           @repeatInsert(s, text)
           moveCursorLeft(s.cursor)
     else
+      if @getInsertionCount() > 0
+        range = getNewTextRangeFromCheckpoint(@editor, @getCheckpoint('undo'))
+        @textByOperator = if range? then @editor.getTextInBufferRange(range) else ''
       @setCheckpoint('insert')
       @vimState.activate('insert', @submode)

@@ -888,7 +887,6 @@
   @extend()
   target: 'MoveToNextFoldStart'

-# FIXME need support count
 class InsertAboveWithNewline extends ActivateInsertMode
   @extend()
   execute: ->


cleanup and annotate done method

cleanup and annotate done method

--- src/layout/mixins/events.coffee
+++ src/layout/mixins/events.coffee
@@ -44,7 +44,6 @@
   error: (world, message) ->
     console.error message
     beep(my.duration.tone, 2)
-

   done: (world, args) ->
     success = args > 0


model_postgres: fix query result handling

--- src/local/model_postgres.coffee
+++ src/local/model_postgres.coffee
@@ -672,7 +672,9 @@
                      GROUP BY node
                      ORDER BY count DESC
                      LIMIT $3"""
-        , [nodePattern, timespan, count], cb
+        , [nodePattern, timespan, count]
+        , (err, res) ->
+            cb err, res?.rows

     getTopPublishedNodes: (count, timespan="7 days", nodePattern="/user/%@%/posts", cb) ->
         @db.query """SELECT node,
@@ -683,7 +685,9 @@
                      GROUP BY node
                      ORDER BY count DESC
                      LIMIT $3"""
-        , [nodePattern, timespan, count], cb
+        , [nodePattern, timespan, count]
+        , (err, res) ->
+            cb err, res?.rows

 parseEl = (xml) ->
     try


FIX: Ensure polling stops when dashboard controller dies

--- src/app/helpdesk/states/dashboard/dashboard.coffee
+++ src/app/helpdesk/states/dashboard/dashboard.coffee
@@ -75,15 +75,15 @@
       pollForStats()
     # Call poll at least once to start now
     pollFunction()
-    _pollInterval = $interval pollFunction, interval
+    pollInterval = $interval pollFunction, interval

   #
   # Stops helpdesk statistics from polling
   #
   stopPolling = ->
-    return unless HelpdeskStats.isPolling()
-    $interval.cancel(HelpdeskStats._pollInterval)
-    HelpdeskStats._pollInterval = null
+    return unless isPolling()
+    $interval.cancel(pollInterval)
+    pollInterval = null

   # When we load, start polling
   startPolling(1000)


QUALITY: Remove switch tutorial from student tutorial list

--- src/app/projects/states/tutorials/tutorials.coffee
+++ src/app/projects/states/tutorials/tutorials.coffee
@@ -18,22 +18,4 @@

 .controller("ProjectsTutorialsStateCtrl", ($scope, $modal, User, Project, alertService, projectService, analyticsService) ->
   $scope.sortOrder = 'abbreviation'
-  $scope.setTutorial = (id) ->
-    Project.update(
-      { id: $scope.project.project_id, tutorial_id: id }
-      (project) ->
-        oldTutorialAbbr = $scope.unit.tutorialFromId($scope.project.tutorial_id)?.abbreviation
-        $scope.project.tutorial_id = project.tutorial_id
-        $scope.project.tutorial = $scope.unit.tutorialFromId( $scope.project.tutorial_id )
-        if id == -1
-          eventName = "Withdrew from all tutorials"
-          successMsg = "Withdrew from #{oldTutorialAbbr}"
-        else
-          eventName = "Changed tutorial"
-          successMsg = "Enrolled in #{$scope.project.tutorial.abbreviation}"
-        analyticsService.event("Student Project View - Tutorials Tab", eventName)
-        alertService.add("success", successMsg, 3000)
-        projectService.updateGroups($scope.project) #can be removed from groups by changing labs
-      (response) -> alertService.add("danger", response.data.error, 6000)
-    )
 )


Remove dead code

Remove dead code

--- test/acceptance/website/channel.coffee
+++ test/acceptance/website/channel.coffee
@@ -3,7 +3,5 @@
   it 'should have a facebook channel file', (done) ->
     @browser.visit('/channel.html').then(=>
       expect(@browser.window.FB).to.be.ok
-
-      #expect(@browser.html()).to.contain 'id=\"fb-root\"'
       return null
     ).then(done, done)


Firefox: Fix waitForEnterForFilteredHints.

This is the filtered-hints feature whereby links aren't activiated until
the user hits `Enter`.

There was a race condition caused by forcing this setting to true for
new users *before* the correct storage area was determined in
`Settings.init()`.

Mention @mrmr1993.

--- lib/settings.coffee
+++ lib/settings.coffee
@@ -35,16 +35,18 @@
         storageArea = "local"
         @storage = chrome.storage[storageArea]

-    chrome.storage.local.get null, (localItems) =>
-      localItems = {} if chrome.runtime.lastError
-      @storage.get null, (syncedItems) =>
-        unless chrome.runtime.lastError
-          @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
-
-        chrome.storage.onChanged.addListener (changes, area) =>
-          @propagateChangesFromChromeStorage changes if area == storageArea
-
-        @runOnLoadedCallbacks()
+      # Delay this initialisation until after the correct storage area is known.  The significance of this is
+      # that it delays the on-loaded callbacks.
+      chrome.storage.local.get null, (localItems) =>
+        localItems = {} if chrome.runtime.lastError
+        @storage.get null, (syncedItems) =>
+          unless chrome.runtime.lastError
+            @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
+
+          chrome.storage.onChanged.addListener (changes, area) =>
+            @propagateChangesFromChromeStorage changes if area == storageArea
+
+          @runOnLoadedCallbacks()

   # Called after @cache has been initialized.  On extension pages, this will be called twice, but that does
   # not matter because it's idempotent.
@@ -193,21 +195,21 @@

 # Perform migration from old settings versions, if this is the background page.
 if Utils.isBackgroundPage()
-
-  unless Settings.get "settingsVersion"
-    # This is a new install.  For some settings, we retain a legacy default behaviour for existing users but
-    # use a non-default behaviour for new users.
-
-    # For waitForEnterForFilteredHints, "true" gives a better UX; see #1950.  However, forcing the change on
-    # existing users would be unnecessarily disruptive.  So, only new users default to "true".
-    Settings.set "waitForEnterForFilteredHints", true
-
-  # We use settingsVersion to coordinate any necessary schema changes.
-  Settings.set("settingsVersion", Utils.getCurrentVersion())
-
-  # Remove legacy key which was used to control storage migration.  This was after 1.57 (2016-10-01), and can
-  # be removed after 1.58 has been out for sufficiently long.
-  Settings.nuke "copyNonDefaultsToChromeStorage-20150717"
+  Settings.onLoaded ->
+    unless Settings.get "settingsVersion"
+      # This is a new install.  For some settings, we retain a legacy default behaviour for existing users but
+      # use a non-default behaviour for new users.
+
+      # For waitForEnterForFilteredHints, "true" gives a better UX; see #1950.  However, forcing the change on
+      # existing users would be unnecessarily disruptive.  So, only new users default to "true".
+      Settings.set "waitForEnterForFilteredHints", true
+
+    # We use settingsVersion to coordinate any necessary schema changes.
+    Settings.set("settingsVersion", Utils.getCurrentVersion())
+
+    # Remove legacy key which was used to control storage migration.  This was after 1.57 (2016-10-01), and can
+    # be removed after 1.58 has been out for sufficiently long.
+    Settings.nuke "copyNonDefaultsToChromeStorage-20150717"

 root = exports ? window
 root.Settings = Settings


Refactor Reporter specs

Refactor Reporter specs

--- spec/reporter_spec.coffee
+++ spec/reporter_spec.coffee
@@ -2,39 +2,10 @@
   @timeout(0) # Disable the spec's timeout

   before (done) ->
-    window.__requirejs__.clearRequireState()
-    require ['promise', 'settings'], (Promise, Settings) =>
-      @promise = Promise
-      @settings = Settings
-
-      # mock BrowserHelper
-      requirejs.undef 'helpers/browser_helper'
-
-      class BrowserHelperMock
-        @checkImages: ->
-          new Promise().resolve(true)
-
-      @browser_helper = BrowserHelperMock
-
-      define 'helpers/browser_helper', [], ->
-        return BrowserHelperMock
-
-      # require modules
-      require ['reporter'], (Reporter) =>
-        @reporter = Reporter
-        done()
-
-  after ->
-    requirejs.undef 'helpers/browser_helper'
-    window.__requirejs__.clearRequireState()
-
-  beforeEach ->
-    @img_len_before = document.getElementsByTagName('img').length
-    @scr_len_before = document.getElementsByTagName('script').length
-
     @url = 'foo.bar'
-    @payload = {
-      url: 'http://www.yogurt.foo/products/show/15400722'
+
+    @single_beacon_data_array = [{
+      url: 'some_url'
       shop_code: 'SA-XXXX-Y'
       actions: [
         {
@@ -50,20 +21,120 @@
             shop_id: '2032'
           }
         },
+        {
+          category: 'yogurt'
+          type: 'productClick'
+          data: {
+            product_id: '15400722'
+            shop_product_id: '752'
+            shop_id: '2032'
+          }
+        }
       ]
-    }
-    @serialized_data = 'foo.bar?url=http%3A%2F%2Fwww.yogurt.foo%2Fproducts%2Fshow%2F15400722&shop_code=SA-XXXX-Y&actions=%5B%7B%22category%22%3A%22site%22%2C%22type%22%3A%22sendPageView%22%7D%2C%7B%22category%22%3A%22yogurt%22%2C%22type%22%3A%22productClick%22%2C%22data%22%3A%7B%22product_id%22%3A%2215400722%22%2C%22shop_product_id%22%3A%22752%22%2C%22shop_id%22%3A%222032%22%7D%7D%5D'
+    }]
+
+    @multiple_beacon_data_array = [
+      {
+        url: 'some_url'
+        shop_code: 'SA-XXXX-Y'
+        actions: [{
+          category: 'site'
+          type: 'sendPageView'
+        }]
+      },
+      {
+        url: 'some_url'
+        shop_code: 'SA-XXXX-Y'
+        actions: [{
+          category: 'yogurt'
+          type: 'productClick'
+          data: {
+            product_id: '15400722'
+            shop_product_id: '752'
+            shop_id: '2032'
+          }
+        }]
+      },
+      {
+        url: 'some_url'
+        shop_code: 'SA-XXXX-Y'
+        actions: [{
+          category: 'yogurt'
+          type: 'productClick'
+          data: {
+            product_id: '15400722'
+            shop_product_id: '752'
+            shop_id: '2032'
+          }
+        }]
+      }
+    ]
+
+    @mockup_dom_methods = =>
+      @createElement_stub = sinon.stub()
+      document.createElement = @createElement_stub.returns({})
+      Node::insertBefore = (who, where)-> who.onload()
+
+    @start = =>
+      @subject = new @reporter()
+      @handleJob_spy = sinon.spy(@subject, '_handleJob')
+      @report_spy = sinon.spy(@subject, 'report')
+      @clock.tick 10
+
+    @serialized_data = '?url=some_url&shop_code=SA-XXXX-Y&actions=%5B%7B%22category%22%3A%22site%22%2C%22type%22%3A%22sendPageView%22%7D%2C%7B%22category%22%3A%22yogurt%22%2C%22type%22%3A%22productClick%22%2C%22data%22%3A%7B%22product_id%22%3A%2215400722%22%2C%22shop_product_id%22%3A%22752%22%2C%22shop_id%22%3A%222032%22%7D%7D%2C%7B%22category%22%3A%22yogurt%22%2C%22type%22%3A%22productClick%22%2C%22data%22%3A%7B%22product_id%22%3A%2215400722%22%2C%22shop_product_id%22%3A%22752%22%2C%22shop_id%22%3A%222032%22%7D%7D%5D'
+    window.__requirejs__.clearRequireState()
+    require ['promise', 'settings'], (Promise, Settings) =>
+      @promise = Promise
+      @settings = Settings
+
+      # mock BrowserHelper
+      requirejs.undef 'helpers/browser_helper'
+
+      class BrowserHelperMock
+        @checkImages: ->
+          promise = new Promise()
+
+          ## TODO EXPLAIN THE REGRESSION TEST
+          setTimeout ->
+            # console.log 'RESOLVING CHECK IMAGE WITH TRUE'
+            promise.resolve(true)
+          , 0
+
+          promise
+
+      @browser_helper = BrowserHelperMock
+
+      define 'helpers/browser_helper', [], ->
+        return BrowserHelperMock
+
+      # require modules
+      require ['reporter'], (Reporter) =>
+        @reporter = Reporter
+        done()
+
+  after ->
+    requirejs.undef 'helpers/browser_helper'
+    window.__requirejs__.clearRequireState()
+
+  beforeEach ->
+    @clock = sinon.useFakeTimers()
+    @_createElement = document.createElement
+    @_insertBefore  = Node::insertBefore
+
+  afterEach ->
+    @clock.restore()
+    @createElement_stub?.reset()
+
+    @report_spy?.restore()
+    @handleJob_spy?.restore()
+
+    document.createElement = @_createElement
+    Node::insertBefore = @_insertBefore

   describe 'instance', ->
-    beforeEach (done) ->
+    beforeEach ->
       @subject = new @reporter()
-      done()
-
-    it 'has own property base', ->
-      expect(@subject).to.have.ownProperty('base')
-
-    it 'has own property queue', ->
-      expect(@subject).to.have.ownProperty('queue')
+      @clock.tick 10

     it 'has own property transport', ->
       expect(@subject).to.have.ownProperty('transport')
@@ -77,159 +148,155 @@
     it 'responds to then', ->
       expect(@subject).to.respondTo('then')

-  describe '#report', ->
-    beforeEach ->
-      @subject = new @reporter()
-      @pr = new @promise()
-      @spy = sinon.spy(@subject, 'report')
-      @subject.report(@url, @payload)
-
-    afterEach ->
-      @spy.restore()
-
-    it 'accepts as 1st argument the url to report to', ->
-      expect(@spy.args[0][0]).to.equal @url
-
-    it 'accepts as 2nd argument the payload to report', ->
-      expect(@spy.args[0][1]).to.deep.equal @payload
-
-    describe 'payload argument', ->
-      it 'has property String url', ->
-        payload = @spy.args[0][1]
-        expect(payload)
-          .to.have.property('url')
-          .that.is.a('string')
-
-      it 'has property String shop_code', ->
-        payload = @spy.args[0][1]
-        expect(payload)
-          .to.have.property('shop_code')
-          .that.is.a('string')
-
-      it 'has property Array actions', ->
-        payload = @spy.args[0][1]
-        expect(payload)
-          .to.have.property('actions')
-          .that.is.an('array')
-
-    it 'returns a promise', ->
-      expect(@subject.report(@url, @payload))
-        .to.be.an.instanceof @promise
-
-    context 'when transport uses image', ->
+  describe 'API', ->
+    describe '#then', ->
       beforeEach ->
-        @subject.transport = 'img'
-        @res   = @subject._createTransport(@pr, @url)
-        ## Get element's src
-        images = document.getElementsByTagName('img')
-        @src   = images[images.length - 1].src
-
-      it 'inserts an image element into the DOM', ->
-        expect(document.getElementsByTagName('img').length)
-          .to.be.above @img_len_before
-
-      describe 'image element', ->
-        it 'has the proper url', ->
-          expect(@src).to.contain(@url)
-
-        it 'has the buster parameter', ->
-          expect(@src).to.contain('buster=')
-
-    context 'when transport uses script', ->
-      beforeEach ->
-        @subject.transport = 'script'
-        @res = @subject._createTransport(@pr, @url)
-        ## Get element's src
-        @src = document.getElementsByTagName('script')[0].src
-
-      it 'inserts a script element into the DOM', ->
-        expect(document.getElementsByTagName('script').length)
-          .to.be.above @scr_len_before
-
-      describe 'script element', ->
-        it 'has the proper url', ->
-          expect(@src).to.contain(@url)
-
-        it 'has the buster parameter', ->
-          expect(@src).to.contain('buster=')
-
-  describe '#then', ->
-    beforeEach ->
-      @pr = new @promise()
-      @stub = sinon.stub(@browser_helper, 'checkImages').returns(@pr)
-
-    afterEach ->
-      @stub.restore()
-
-    it 'returns a promise', ->
-      @subject = new @reporter()
-      res = @subject.then(@success, @failure)
-      expect(res).to.be.an.instanceof @promise
-
-    context 'when fulfilled', ->
-      it 'calls the success callback', (done)->
+        @checkImage_promise = new @promise()
+        @checkImage_stub = sinon.stub(@browser_helper, 'checkImages').returns(@checkImage_promise)
+
+      afterEach ->
+        @checkImage_stub.restore()
+
+      it 'returns a promise', ->
+        @subject = new @reporter()
+        res = @subject.then()
+        expect(res).to.be.an.instanceof @promise
+
+      it 'calls success argument when transport_ready succeeds', (done)->
+        @subject = new @reporter()
         @success = ->
           expect(true).to.equal(true)
           done()
         @failure = ->
           expect(true).to.equal(false)
           done()
-
+        res = @subject.then(@success, @failure)
+        @checkImage_promise.resolve()
+
+      it 'calls error argument when transport_ready fails', (done)->
         @subject = new @reporter()
-        res = @subject.then(@success, @failure)
-        @pr.resolve()
-
-    context 'when rejected', ->
-      it 'calls the failure callback', (done)->
         @success = ->
           expect(true).to.equal(false)
           done()
         @failure = ->
           expect(true).to.equal(true)
           done()
-
-        @subject = new @reporter()
         res = @subject.then(@success, @failure)
-        @pr.reject()
-
-  describe '#_determineTransport', ->
+        @checkImage_promise.reject()
+
+    describe '#report', ->
+      beforeEach ->
+        @mockup_dom_methods()
+
+      it 'returns a promise', (done)->
+        @start()
+        result = @subject.report(@url, @single_beacon_data_array)
+        result.then =>
+          expect(result).to.be.an.instanceof @promise
+          done()
+
+      it 'accepts as 1st argument the url to report to', (done)->
+        @start()
+        @subject.report(@url, @single_beacon_data_array).then =>
+          expect(@report_spy.args[0][0]).to.equal @url
+          done()
+
+      it 'accepts as 2nd argument to be the data_array', (done)->
+        @start()
+        @subject.report(@url, @single_beacon_data_array).then =>
+          expect(@report_spy.args[0][1]).to.deep.equal @single_beacon_data_array
+          done()
+
+      context 'when we have multiple data items to report', ->
+        it 'creates multiple transport elements', (done)->
+          @start()
+
+          callback = =>
+            expect(@createElement_stub.callCount).to.equal 3
+            done()
+
+          @subject.report(@url, @multiple_beacon_data_array).then callback, callback
+
+        it 'waits for every transport element to finish before fulfilling the promise returned', (done)->
+          @start()
+          callback = =>
+            states = []
+            for call in @handleJob_spy.getCalls()
+              states.push call.args[2].state
+
+            expect(states).to.not.contain 'pending'
+            done()
+
+          @subject.report(@url, @multiple_beacon_data_array).then callback, callback
+
+      context 'when we have one data to report', ->
+        it 'creates one transport element', (done)->
+          @start()
+
+          callback = =>
+            expect(@createElement_stub.callCount).to.equal 1
+            done()
+
+          @subject.report(@url, @single_beacon_data_array).then callback, callback
+
+        it 'waits for every transport element to finish before fulfilling the promise returned', (done)->
+          @start()
+          callback = =>
+            states = []
+            for call in @handleJob_spy.getCalls()
+              states.push call.args[2].state
+
+            expect(states).to.not.contain 'pending'
+            done()
+
+          @subject.report(@url, @single_beacon_data_array).then callback, callback
+
+  describe 'Reporting', ->
+    beforeEach ->
+      @mockup_dom_methods()
+
+    it 'serializes data', (done)->
+      @start()
+      callback = =>
+        transport = @createElement_stub.returnValues[0]
+        expect(transport.src).to.contain("#{@url}#{@serialized_data}")
+        done()
+      @subject.report(@url, @single_beacon_data_array).then callback, callback
+      done()
+
+    it 'appends a cache busting param in the transport url', (done)->
+      @start()
+      callback = =>
+        transport = @createElement_stub.returnValues[0]
+        expect(transport.src).to.contain('buster=')
+        done()
+      @subject.report(@url, @single_beacon_data_array).then callback, callback
+
+    context 'when images are disabled', ->
+      beforeEach ->
+        @checkImage_stub = sinon.stub(@browser_helper, 'checkImages').returns( new @promise().resolve(false) )
+
+      afterEach ->
+        @checkImage_stub.restore()
+
+      it 'creates script element transports', (done)->
+        @start()
+        callback = =>
+          expect(@createElement_stub.args[0][0]).to.equal('script')
+          done()
+        @subject.report(@url, @single_beacon_data_array).then callback, callback
+
+
     context 'when images are enabled', ->
-      it 'sets transport to image', (done) ->
-        stub = sinon
-          .stub(@browser_helper, 'checkImages')
-          .returns(new @promise().resolve(true))
-
-        @subject = new @reporter()
-        @subject.then =>
-          expect(@subject.transport).to.equal('img')
-          stub.restore()
-          done()
-
-    context 'when images are not enabled', ->
-      it 'sets transport to script', (done) ->
-        stub = sinon
-          .stub(@browser_helper, 'checkImages')
-          .returns((new @promise()).resolve(false))
-
-        @subject = new @reporter()
-        @subject.then =>
-          expect(@subject.transport).to.equal('script')
-          stub.restore()
-          done()
-
-  describe '#_handleJob', ->
-    beforeEach ->
-      @pr = new @promise()
-      @subject = new @reporter()
-      @stub = sinon.stub(@subject, '_createTransport')
-
-      @subject._handleJob(@url, @payload, @pr)
-
-    afterEach ->
-      @stub.restore()
-
-    it 'calls #_createTransport', ->
-      expect(@stub).to.be.calledOnce
-
-    it 'calls #_createTransport with proper args', ->
-      expect(@stub).to.be.calledWith(@pr, @serialized_data)
+      beforeEach ->
+        @checkImage_stub = sinon.stub(@browser_helper, 'checkImages').returns( new @promise().resolve(true) )
+
+      afterEach ->
+        @checkImage_stub.restore()
+
+      it 'creates image element transports', (done)->
+        @start()
+        callback = =>
+          expect(@createElement_stub.args[0][0]).to.equal('img')
+          done()
+        @subject.report(@url, @single_beacon_data_array).then callback, callback


Staging : added missing MemberFollowToggleButton class

--- client/app/CommonViews/followbutton.coffee
+++ client/app/CommonViews/followbutton.coffee
@@ -53,3 +53,25 @@
   mouseLeave:->
     if @getTitle() is "Unfollow"
       @setTitle "Following"
+
+class MemberFollowToggleButton extends FollowButton
+
+  constructor:(options = {}, data)->
+
+    options = $.extend
+
+      errorMessages  :
+        KodingError  : 'Something went wrong while follow'
+        AccessDenied : 'You are not allowed to follow members'
+      stateOptions   :
+        unfollow     :
+          cssClass   : 'following-btn'
+      dataType       : 'JAccount'
+
+    , options
+
+    super options, data
+
+  decorateState:(name)->
+    KD.track "Members", name, @getData().profile.nickname
+    super


Use lazyLoader at start in messageList too.

--- client/app/CommonViews/messagesList.coffee
+++ client/app/CommonViews/messagesList.coffee
@@ -10,9 +10,9 @@
 class MessagesListController extends KDListViewController

   constructor:(options, data)->
-    options.subItemClass or= InboxMessagesListItem
-    options.listView     or= new MessagesListView
-
+    options.subItemClass        or= InboxMessagesListItem
+    options.listView            or= new MessagesListView
+    options.startWithLazyLoader   = yes
     super options, data

     @getListView().registerListener
@@ -37,6 +37,7 @@
         unreadCount++ unless message.flags_?.read

       @emit "MessageCountDidChange", unreadCount
+      @propagateEvent KDEventType : 'LazyLoadComplete'
       callback? err,messages

   fetchNotificationTeasers:(callback)->
@@ -58,6 +59,7 @@
         unglanced = items.filter (item)-> item.getFlagValue('glanced') isnt yes
         @emit 'NotificationCountDidChange', unglanced.length
         callback? items
+      @propagateEvent KDEventType : 'LazyLoadComplete'

 class NotificationListItem extends KDListItemView


Update zh-HANS.coffee (#5779)

Update zh-HANS.coffee (#5779)

* Update zh-HANS.coffee

Translate Ozaria encouragement modal

* Update zh-HANS.coffee

Co-authored-by: Lance <478ee996df1fd51b45becb63ea7a1f7f9c4ea57e@163.com>


update (#5687)

update (#5687)

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -1,8 +1,8 @@
 module.exports = nativeDescription: "简体中文", englishDescription: "Chinese (Simplified)", translation:

   new_home:
-    title: "CodeCombat - 美国大学理事会认证硅谷原版青少年编程平台"
-    meta_description: "CodeCombat（中国官方网址：koudashijie.com）于2013年成立于美国硅谷，是美国大学理事会权威认证的青少年编程学习平台，学习者可以在CodeCombat学习后参加AP计算机考试，拿到美国和世界一流名校如斯坦福大学、哈佛大学、麻省理工学院等学分减免政策，是全球公认的权威计算机编程能力学习平台之一。"
+    title: "CodeCombat - 硅谷原创AP官方权威认证青少年编程平台"
+    meta_description: "CodeCombat（中国官方网址：koudashijie.com）于2013年成立于美国硅谷，是美国大学理事会权威认证的青少年编程学习平台和工信部赛迪人工智能大赛全球独家技术合作平台，学习者可以在CodeCombat学习后参加AP计算机考试，拿到世界一流名校如斯坦福大学、哈佛大学、麻省理工学院等学分减免政策，是全球公认的权威青少年计算机编程学习平台之一。"
     built_for_teachers_title: "一个以教师为中心的课程体系"
     built_for_teachers_blurb: "教孩子们写代码往往会觉得无从下手。 CodeCombat 旨在帮助所有的教育工作者传授学生学习和掌握当下最流行的两种计算机语言，JavaScript 和 Python。整个体系包含六门计算机科学课程，以及通过基于项目的游戏开发和网页开发单元巩固学习，教会孩子们一步步从基本语法到高级递归前进！"
     built_for_teachers_subtitle1: "计算机科学"
@@ -199,7 +199,7 @@
   modal:
     close: "关闭"
     okay: "是"
-#    cancel: "Cancel"
+    cancel: "取消"

   not_found:
     page_not_found: "找不到网页"
@@ -213,12 +213,12 @@
     subscribe_as_diplomat: "提交“外交官”申请"

   play:
-#    title: "Play CodeCombat Levels - Learn Python, JavaScript, and HTML"
-#    meta_description: "Learn programming with a coding game for beginners. Learn Python or JavaScript as you solve mazes, make your own games, and level up. Challenge your friends in multiplayer arena levels!"
-#    level_title: "__level__ - Learn to Code in Python, JavaScript, HTML"
-#    video_title: "__video__ | Video Level"
-#    game_development_title: "__level__ | Game Development"
-#    web_development_title: "__level__ | Web Development"
+    title: "挑战CodeCombat关卡 - 学习Python，Javascript和C++"
+    meta_description: "通过场景化教学学习输入真实的代码。 在解决难题的同时掌握Python，JavaScript和C++，并学习制作自己的代码游戏和网站，在多人竞技场挑战你的同学们"
+    level_title: "__level__ - 学习Python，Javascript和C++编程"
+    video_title: "__video__ | 观看教学视频"
+    game_development_title: "__level__ | 游戏开发"
+    web_development_title: "__level__ | 网页开发"
 #    anon_signup_title_1: "CodeCombat has a"
 #    anon_signup_title_2: "Classroom Version!"
 #    anon_signup_enter_code: "Enter Class Code:"
@@ -433,8 +433,8 @@
     books: "书籍"

   common:
-#    default_title: "CodeCombat - Coding games to learn Python and JavaScript"
-#    default_meta_description: "Learn typed code through a programming game. Learn Python, JavaScript, and HTML as you solve puzzles and learn to make your own coding games and websites."
+    default_title: "CodeCombat - 在场景化编程中学习Python，Javascript和C++"
+    default_meta_description: "通过场景化教学学习输入真实的代码。 在解决难题的同时掌握Python，JavaScript和C++，并学习制作自己的代码游戏和网站。"
     back: "返回" # When used as an action verb, like "Navigate backward"
     coming_soon: "敬请期待！"
     continue: "继续" # When used as an action verb, like "Continue forward"
@@ -466,9 +466,9 @@
     submit_changes: "提交更新"
     save_changes: "保存更新"
     required_field: "必填" # {change}
-#    submit: "Submit"
-#    replay: "Replay"
-#    complete: "Complete"
+    submit: "提交"
+    replay: "重新开始"
+    complete: "完成"

   general:
     and: "与"
@@ -1064,8 +1064,8 @@
     editor_config_behaviors_description: "自动匹配括号、大括号和引号。"

   about:
-#    title: "About CodeCombat - Engaging Students, Empowering Teachers, Inspiring Creation"
-#    meta_description: "Our mission is to level computer science through game-based learning and make coding accessible to every learner. We believe programming is magic and want learners to be empowered to to create things from pure imagination."
+    title: "关于CodeCombat - 激发学生，赋能老师，释放创造力"
+    meta_description: "我们的愿景是通过基于场景化的学习来提高计算机科学水平，让世界上的每个人都能学习编程。 我们相信编程是充满魔力的，学习者能被赋予纯粹想象的创造力。"
     learn_more: "了解更多"
     main_title: "学习编程需要动手写大量代码"
     main_description: "CodeCombat让你带着微笑学习"


Uncomment header from zh-HANT.coffee

Uncomment header from zh-HANT.coffee

--- app/locale/zh-HANT.coffee
+++ app/locale/zh-HANT.coffee
@@ -543,7 +543,7 @@
     tip_programming_not_about_computers: "電腦科學之於電腦不外乎於望遠鏡之於天文學。 - Edsger Dijkstra"
     tip_mulan: "相信你可以做到，然後你就會做到。 - Mulan"

-#  play_game_dev_level:
+  play_game_dev_level:
     created_by: "由{{name}}創建"
     how_to_play_title: "遊戲方法："
     how_to_play_1: "用滑鼠控制英雄！"


mixpanel: send campaign params as event, not property

--- client/landing/app/MainApp/mixpanel.coffee
+++ client/landing/app/MainApp/mixpanel.coffee
@@ -121,9 +121,6 @@
       {createdAt} = meta
       {firstName, lastName, nickname} = profile

-      params     = KD.utils.parseQuery(window.location.search.slice(1))
-      {campaign} = params
-
       # register user to mixpanel
       mixpanel.identify nickname
       mixpanel.people.set
@@ -134,5 +131,10 @@
         "$created"      : createdAt
         "Status"        : type
         "Randomizer"    : KD.utils.getRandomNumber 4
-        "Campaign"      : campaign
       mixpanel.name_tag "#{nickname}.kd.io"
+
+      params     = KD.utils.parseQuery(window.location.search.slice(1))
+      {campaign} = params
+
+      if campaign
+        mixpanel.track "User came from campaign", campaign


minor bug fix

minor bug fix

--- client/landing/app/MainApp/mixpanel.coffee
+++ client/landing/app/MainApp/mixpanel.coffee
@@ -3,6 +3,7 @@
   createEvent:(rest...) =>
     eventName = rest.first
     eventData = rest[1]
+    $user   = KD.nick()

     if eventName is "Login" or (eventName is "Groups" and eventData is "ChangeGroup")
       # identify user on mixpanel
@@ -13,33 +14,28 @@

     else if eventName is "User Opened Ace"
       {title, privacy, visibility} = eventData
-      $user   = KD.nick()
-
+
       options = {title, privacy, visibility, $user}
       @setOnce 'First Time Ace Opened', Date.now()
       @track eventName, options

     else if eventName is "User Opened Terminal"
       {title, privacy, visibility} = eventData
-      $user   = KD.nick()
-
+
       options = {title, privacy, visibility, $user}
       @setOnce 'First Time Terminal Opened', Date.now()
       @track eventName, options

     else if eventName is "Apps" and eventData is "Install"
-      $user      = KD.nick()
+
       appTitle   = rest[2]
-
       options    = {$user, appTitle}
       @track "Application Installed", options

     else if eventName is "User Clicked Buy VM"
-      $user = KD.nick()
       @track eventName, $user

     else if eventName is "Read Tutorial Book"
-      $user = KD.nick()
       @track eventName, $user

     else if eventName is "Activity"
@@ -54,7 +50,6 @@
         else activity is eventData

       group     = KD.getSingleton('groupsController').getCurrentGroup()
-      $user     = KD.nick()
       $activity = activity

       {title, privacy, visibility} = group
@@ -75,14 +70,13 @@
     else
       if eventData is "GroupJoinRequest"
         group = rest[3]
-        $user = KD.nick()
         {title, privacy, visibility} = group
         options = {title, privacy, visibility, $user}
         @track "Group Join Request", options

       else if eventData is "InvitationSentToFriend"
         options       =
-          $user       : KD.nick()
+          $user       : $user
           $recipient  : rest[2]

         @track "Invitation Send", options


Use dataset to set data attributes

Use dataset to set data attributes

--- lib/tab-view.coffee
+++ lib/tab-view.coffee
@@ -45,8 +45,8 @@

   updateDataAttributes: ->
     if itemPath = @item.getPath?()
-      @title.attr('data-name', path.basename(itemPath))
-      @title.attr('data-path', itemPath)
+      @title.element.dataset.name = path.basename(itemPath)
+      @title.element.dataset.path = itemPath

   updateTitle: ({updateSiblings, useLongTitle}={}) ->
     return if @updatingTitle


Deprecate selection:changed

Deprecate selection:changed

--- src/workspace-view.coffee
+++ src/workspace-view.coffee
@@ -178,6 +178,8 @@
           deprecate('Use Pane::onDidDestroy instead')
         when 'cursor:moved'
           deprecate('Use Editor::onDidChangeCursorPosition instead')
+        when 'selection:changed'
+          deprecate('Use Editor::onDidChangeSelectionRange instead')
       originalOn.apply(this, arguments)

   ###


tweaked file watcher, fixed issue with not detecting .js files

--- src/sew.coffee
+++ src/sew.coffee
@@ -26,10 +26,11 @@
 class Worker

   configFile: 'config.json'
+  watchedFiles: []
   defaults:
     public: 'public' # Static Files
     libs: [] # JS to concat and append before app scripts
-    scripts: [ 'app' ]
+    scripts: [ 'app' ] # Folders containing scripts and templates to stitch
     scriptsOutput: 'public/js/app.js'
     styles: [ 'css/style.less' ]
     stylesOutput: 'public/css/app.css'
@@ -53,12 +54,10 @@

   _watch: ->
     @_build()
-    @walk './', (file) =>
-      fs.watchFile file, (curr, prev) =>
-        if curr and (curr.nlink is 0 or +curr.mtime isnt +prev.mtime)
-          switch fpath.extname file
-            when '.coffee', '.eco', 'js' then @compileScripts()
-            when '.less', '.css' then @compileStyles()
+    @walk lib, @watchFile for lib in @options.libs
+    @walk scripts, @watchFile for scripts in @options.scripts
+    @walk styles, @watchFile for styles in @options.styles
+

   _serve: ->
     @_watch()
@@ -114,17 +113,29 @@
     process.exit(1)

   walk: (path, callback) ->
-    for f in fs.readdirSync(path)
-      f = fpath.join(path, f)
-      stats = fs.statSync(f)
-      if stats.isDirectory()
-        @walk f, callback
-      else
-        callback.call(@, f) if @isWatchable(f)
+    stats = fs.statSync path
+    console.log(path)
+    if stats.isFile()
+      callback.call(@, path) if @isWatchable(path)
+    else
+      for f in fs.readdirSync(path)
+        f = fpath.join(path, f)
+        stats = fs.statSync(f)
+        if stats.isDirectory()
+          @walk f, callback
+        else
+          callback.call(@, f) if @isWatchable(f)

   isWatchable: (file) ->
     switch fpath.extname file
       when '.js', '.coffee', '.css', '.less', '.eco' then return true
     false
+
+  watchFile: (file) ->
+    fs.watchFile file, (curr, prev) =>
+      if curr and (curr.nlink is 0 or +curr.mtime isnt +prev.mtime)
+        switch fpath.extname file
+          when '.coffee', '.eco', '.js' then @compileScripts()
+          when '.less', '.css' then @compileStyles()

 new Worker()


Document resin/token

Document resin/token

--- lib/resin/token/token.coffee
+++ lib/resin/token/token.coffee
@@ -1,16 +1,65 @@
 data = require('../data/data')

+# @nodoc
 TOKEN_KEY = 'token'

+# Save token
+#
+# The token is saved to $(dataPrefix)/token, which usually equals to $HOME/.resin/token
+#
+# @param {String} newToken the token
+# @param {Function} callback callback(error)
+#
+# @note The token is saved as plain text.
+#
+# @todo We should make the token more secure
+#
+# @example Save Token
+#		resin.token.saveToken myToken, (error) ->
+#			throw error if error?
+#
 exports.saveToken = (newToken, callback) ->
 	data.set(TOKEN_KEY, newToken, encoding: 'utf8', callback)

+# Check if we have any token saved
+#
+# @param {Function} callback callback(hasToken)
+#
+# @example Has Token
+#		resin.token.hasToken (hasToken) ->
+#			if hasToken
+#				console.log('It\'s there!')
+#			else
+#				console.log('It\'s not there!')
+#
 exports.hasToken = (callback) ->
 	data.has(TOKEN_KEY, callback)

+# Get saved token value
+#
+# @param {Function} callback callback(error, token)
+#
+# @note If the key doesn't exist, undefined and no error is returned
+#
+# @example Get token
+#		resin.token.getToken (error, token) ->
+#			throw error if error?
+#			if token?
+#				console.log("My token is: #{token}")
+#
 exports.getToken = (callback) ->
 	data.get(TOKEN_KEY, encoding: 'utf8', callback)

+# Remove token from the filesystem
+#
+# @param {Function} callback callback(error)
+#
+# @note If the token doesn't exist, no action is performed
+#
+# @example Clear Token
+#		resin.token.clearToken (error) ->
+#			throw error if error?
+#
 exports.clearToken = (callback) ->
 	data.has TOKEN_KEY, (hasToken) ->
 		if hasToken


corrected possible bug

corrected possible bug

--- node_modules_koding/kite-amqp/lib/kite-amqp/kite.coffee
+++ node_modules_koding/kite-amqp/lib/kite-amqp/kite.coffee
@@ -39,8 +39,13 @@
         console.log 'You can not start this Kite'
         return

-      res.on "data", (chunk) =>
-        data = JSON.parse chunk.toString()
+      data = ""
+
+      res.on "data", (chunk) ->
+        data += chunk
+
+      res.on "end", ->
+        data = JSON.parse data

         @config = __createAmqpConfig data
         console.log 'Kite has started'


Remove legacy code

Remove legacy code

--- extension/scripts/command-store.coffee
+++ extension/scripts/command-store.coffee
@@ -70,9 +70,4 @@
   trigger: (eventName, eventData) ->
     window.Events.sendTrigger eventName, eventData

-  requestFileSystem: (
-    window.requestFileSystem ||
-    window.webkitRequestFileSystem
-  ).bind(window)
-
 window.CommandStore = new CommandStore


:bug: Minor change in name of spec

:bug: Minor change in name of spec

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -771,7 +771,7 @@
             expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
             expect(editor.getSelectedText()).toBe 'something'

-        it "unescapes \\ in list to match snippet behavior", ->
+        it "escapes \\ in list to match snippet behavior", ->
           triggerAutocompletion(editor, true, 'm')

           runs ->


Fix a browser global bug.

Fix a browser global bug.

--- src/9.coda.coffee
+++ src/9.coda.coffee
@@ -1,5 +1,5 @@
 if $mate.environmentType == "browser"
-    global.$mate = $mate
+    window.$mate = $mate
 else if $mate.environmentType == "node"
     module.exports = $mate
 else


Fix failing test on Windows

Fix failing test on Windows

The exit code tests include an assert on the output we get from a
spawned electron process. This doesn't currently work on Windows, so we
skip it for now

--- spec/api-app-spec.coffee
+++ spec/api-app-spec.coffee
@@ -44,7 +44,10 @@
       output = ''
       appProcess.stdout.on 'data', (data) -> output += data
       appProcess.on 'close', (code) ->
-        assert.notEqual output.indexOf('Exit event with code: 123'), -1
+        # We skip the following assert on Windows, since we can't currently get
+        # stdout from a spawned electron process on Windows
+        if process.platform isnt 'win32'
+            assert.notEqual output.indexOf('Exit event with code: 123'), -1
         assert.equal code, 123
         done()


add mouseWheel support

add mouseWheel support

--- framer/Components/FlowComponent.coffee
+++ framer/Components/FlowComponent.coffee
@@ -86,6 +86,11 @@

 	@define "scroll",
 		get: -> return @current?._flowScroll
+
+	@define "mouseWheelEnabled",
+		default: false
+		get: -> return @_mouseWheelEnabled
+		set: (value) ->	@_mouseWheelEnabled = value

 	##############################################################
 	# Header and footer
@@ -265,6 +270,7 @@
 				inset.bottom = @footer?.height or 0 if scroll.maxY is @height
 				scroll.contentInset = inset
 				flowLayer._flowScroll = scroll
+				scroll.mouseWheelEnabled = @_mouseWheelEnabled

 		# Set the background color for he created scroll component
 		if layer instanceof ScrollComponent


improved success message of request access / invitation

--- client/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/app/Applications/Groups.kdapplication/AppController.coffee
@@ -267,11 +267,11 @@
     if isApproval
       title   = 'Request Access'
       content = 'Membership to this group requires administrative approval.'
-      success = "Thanks! You'll receive an email when group's admin accepts you."
+      success = "Thanks! You'll be notified when group's admin accepts you."
     else
       title   = 'Request an Invite'
       content = 'Membership to this group requires an invitation.'
-      success = "Invitation has been sent to the group's admin."
+      success = "Your request has been sent to the group's admin."

     modal = new KDModalView
       title          : title


sort by published_at

sort by published_at

--- apps/articles/routes.coffee
+++ apps/articles/routes.coffee
@@ -10,6 +10,7 @@
     data:
       published: true
       limit: 50
+      sort: '-published_at'
     error: res.backboneError
     success: (articles) ->
       res.locals.sd.ARTICLES = articles.toJSON()


component-lab: add loading state to SubscriptionHeader

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

--- client/component-lab/SubscriptionHeader/SubscriptionHeader.coffee
+++ client/component-lab/SubscriptionHeader/SubscriptionHeader.coffee
@@ -12,46 +12,76 @@

 module.exports = class SubscriptionHeader extends Component

-  render: ->
-    { title, teamSize, isTrial, endsAt
-      nextBillingAmount, freeCredit } = @props
+  renderTitle: ->
+
+    { loading, title } = @props
+
+    title = if loading then 'Loading' else title
+
+    <Col xs={8}>
+
+        {title}
+
+    </Col>
+
+
+  renderFreeCredit: ->
+
+    { freeCredit } = @props
+
+    <Col xs={4} className={textStyles.right}>
+      Free Credit:
+      ${freeCredit}
+    </Col>
+
+
+  renderSubtitle: ->
+
+    { loading, isTrial, endsAt } = @props

     isDanger = no
-    if isTrial
-      daysLeft = Math.max 0, dateDiffInDays(new Date(Number endsAt), new Date)
-      subtitle = "You have #{daysLeft} days left in your trial."
-      isDanger = yes  if daysLeft < 4
-    else
-      billingDate = moment(Number endsAt).format 'MMM Do, YYYY'
-      subtitle = "Your next billing date is #{billingDate}."
-      title = "#{title} (#{teamSize} Developers)"
+    subtitle = switch
+      when loading
+        "Loading subscription info..."
+
+      when isTrial
+        daysLeft = Math.max 0, dateDiffInDays(new Date(Number endsAt), new Date)
+        isDanger = daysLeft < 4
+        "You have #{daysLeft} days left in your trial."
+
+      else
+        billingDate = moment(Number @props.endsAt).format 'MMM Do, YYYY'
+        "Your next billing date is #{billingDate}."
+
+    <Col xs={8}>
+
+        {subtitle}
+
+    </Col>
+
+
+  renderNextBillingAmount: ->
+
+    return  if @props.isTrial
+
+    { nextBillingAmount } = @props
+
+    <Col xs={4} className={textStyles.right}>
+
+        Next Bill Amount: ${nextBillingAmount or 0}
+
+    </Col>
+
+
+  render: ->
+
+    { loading } = @props

     <Row>
-
-      <Col xs={8}>
-
-          {title}
-
-      </Col>
-
-      <Col xs={4} className={textStyles.right}>
-        Free Credit:
-        ${@props.freeCredit}
-      </Col>
-
-      <Col xs={8}>
-
-          {subtitle}
-
-      </Col>
-
-      {unless @props.isTrial
-        <Col xs={4} className={textStyles.right}>
-
-            Next Bill Amount: ${nextBillingAmount}
-
-        </Col>
-      }
+      {@renderTitle()}
+      {@renderFreeCredit()  unless loading}
+      {@renderSubtitle()}
+      {@renderNextBillingAmount()  unless loading}
     </Row>


home: minor

home: minor

--- client/landing/app/Applications/Home.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Home.kdapplication/AppView.coffee
@@ -42,12 +42,12 @@
     vmController = @getSingleton("vmController")
     {JAccount, JTag, JGroup, CActivity} = KD.remote.api

-    members.ready => JAccount.count "",       (err, count)=> members.update count or 0
-    vms.ready => vmController.fetchTotalVMCount (err, count)=> vms.update count or 0
-    loc.ready => vmController.fetchTotalLoC     (err, count)=> loc.update count or 0
-    groups.ready => JGroup.count "",          (err, count)=> groups.update count or 0
-    topics.ready => JTag.count "",            (err, count)=> topics.update count or 0
-    activities.ready => CActivity.count "",   (err, count)=> activities.update count or 0
+    members.ready => JAccount.count "",         (err, count)=> members.update count    or 0
+    vms.ready => vmController.fetchTotalVMCount (err, count)=> vms.update count        or 0
+    loc.ready => vmController.fetchTotalLoC     (err, count)=> loc.update count        or 0
+    groups.ready => JGroup.count "",            (err, count)=> groups.update count     or 0
+    topics.ready => JTag.count "",              (err, count)=> topics.update count     or 0
+    activities.ready => CActivity.count "",     (err, count)=> activities.update count or 0

     @getSingleton("activityController").on "ActivitiesArrived", (newActivities=[])->
       activities.increment newActivities.length


Update clock.coffee

Update clock.coffee

Updated setTime function and minute update interval

--- clock.coffee
+++ clock.coffee
@@ -105,8 +105,8 @@
     @setTime('all')
     # start ticking, update all contexts, colors every second
     window.setInterval ( => @setTime('all') ), 1000
-    # start ticking, update minute context 50 times a second
-    window.setInterval ( => @setTime('minute') ), 20
+    # start ticking, update minute context
+    window.setInterval ( => @setTime('minute') ), 100
     # save configuration every 10 seconds
     window.setInterval ( => @saveConfig() ), 10000

@@ -277,35 +277,24 @@
     @rotateClock(context)

   setTime: (context='all') ->
-    # initialize dates
+    # initialize date
     d = (new Date())
-    d2 = new Date(d.getFullYear(), 0, 1)
     # set time for each context
-    if context is 'second' or 'all'
-      @second = ((new Date()).getMilliseconds() / 1000) * 60
     if context is 'minute'
       @minute = d.getSeconds() + (parseInt(d.getTime() / 10) % parseInt(d.getTime() / 1000)) / 100
-    if context is 'hour' or 'all'
+    else
+      d2 = new Date(d.getFullYear(), 0, 1)
+      @second = ((new Date()).getMilliseconds() / 1000) * 60
       @hour = d.getMinutes() + (@minute / 60)
-    if context is 'day' or 'all'
       @day = (d.getHours() + (@hour / 60)) / 24 * 60
-    if context is 'week' or 'all'
       @week = ((d.getDay() - d.getHours() / 24) / 7) * 60
-    if context is 'month' or 'all'
       @month = (d.getDate() / (32 - new Date(d.getYear(), d.getMonth(), 32).getDate())) * 60 - (1 - @day/60)
-    if context is 'year' or 'all'
       @year = (Math.ceil((d - d2) / 86400000) + @day / 60) / 365 * 60
-    if context is 'decade' or 'all'
       @decade = (d.getYear() % 10  + ((Math.ceil((d - d2) / 86400000)) / 365) + @hour / 60 / 365) / 10  * 60
-    if context is 'life' or 'all'
       @life = ((@config.age*365*24 + (Math.ceil((d - d2) / 86400000)) + @hour / 60)  / (@config.expectancy*365*24)) * 60
-      # update age display
       $('.age').val(@config.age=1) if @life == 60
-    if context is 'century' or 'all'
       @century = (((new Date()).getFullYear() % 100) / 100) * 60
-    if context is 'millenium' or 'all'
       @millenium = (((new Date()).getFullYear() % 1000) / 1000) * 60
-    if context is 'earth' or 'all'
       @earth = ((4570000000 + (new Date()).getTime() / 30000000000) / 10000000000) * 60
     # draw clock
     @drawClock(context)


Fixed typo

Fixed typo

--- spec/grammar-spec.coffee
+++ spec/grammar-spec.coffee
@@ -61,7 +61,7 @@
       expect(tokens[4]).toEqual value: ")", scopes: ["source.perl", "string.regexp.compile.nested_parens.perl", "punctuation.definition.string.perl"]
       expect(tokens[5]).toEqual value: ";", scopes: ["source.perl"]

-    it "does not tread ( in a class as a group", ->
+    it "does not treat ( in a class as a group", ->
       {tokens} = grammar.tokenizeLine("m/ \\A [(]? [?] .* - /smx")
       expect(tokens[1]).toEqual value: "/", scopes: ["source.perl", "string.regexp.find-m.simple-delimiter.perl", "punctuation.definition.string.perl"]
       expect(tokens[5]).toEqual value: "[", scopes: ["source.perl", "string.regexp.find-m.simple-delimiter.perl"]


Refactor ViolationTooltip

Refactor ViolationTooltip

--- lib/violation-tooltip.coffee
+++ lib/violation-tooltip.coffee
@@ -15,37 +15,46 @@
               '</div>'
   })

+  init: (type, element, options) ->
+    super(type, element, options)
+    @violation = options.violation
+
   getDefaults: ->
     ViolationTooltip.DEFAULTS

   setContent: ->
-    $content = @tip().find('.tooltip-inner')
-    violation = @options.violation
+    @setMessageContent()
+    @setTagsContent()
+    @setAttachmentContent()
+    @tip().removeClass('fade in top bottom left right')

-    $content.find('.message').html(violation.getMessageHTML() || '')
-    $content.find('.tags').html(violation.getTagsHTML() || '')
+  setMessageContent: ->
+    @content().find('.message').html(@violation.getMessageHTML() || '')

-    $attachment = $content.find('.attachment')
-    attachment = violation.getAttachmentHTML()
-    if attachment?
-      $attachment.html(attachment)
+  setTagsContent: ->
+    @content().find('.tags').html(@violation.getTagsHTML() || '')
+
+  setAttachmentContent: ->
+    $attachment = @content().find('.attachment')
+    HTML = @violation.getAttachmentHTML()
+    if HTML?
+      $attachment.html(HTML)
     else
       $attachment.hide()

-    @tip().removeClass('fade in top bottom left right')
-
   hasContent: ->
-    @options.violation?
+    @violation?

   applyAdditionalStyle: ->
     super()

-    $content = @tip().find('.tooltip-inner')
-
-    $code = $content.find('code, pre')
+    $code = @content().find('code, pre')

     if $code.length > 0
-      frontColor = Color($content.css('color'))
+      frontColor = Color(@content().css('color'))
       $code.css('color', frontColor.clone().rgbaString())
       $code.css('background-color', frontColor.clone().clearer(0.96).rgbaString())
       $code.css('border-color', frontColor.clone().clearer(0.86).rgbaString())
+
+  content: ->
+    @contentElement ?= @tip().find('.tooltip-inner')


refactor(app): fix lint errors

refactor(app): fix lint errors

--- src/components/App.coffee
+++ src/components/App.coffee
@@ -18,6 +18,7 @@
 SearchBar     = require "./SearchBar"
 Menu          = require "./Menu"
 pkg           = require "json!../../package.json"
+DONATE_URL    = "https://www.betterplace.org/de/projects/36213"

 { initialize, touch       }  = require "redux-form"
 { div, span, button, nav, li, i, a, br, p }  = React.DOM
@@ -313,11 +314,9 @@
                       div null,
                         p null,
                           "Du kannst uns mit Spenden ab sofort über unser "
-                          a href: "https://www.betterplace.org/de/projects/36213",
-                            "Crowdfunding Projekt"
+                          a href: DONATE_URL, "Crowdfunding Projekt"
                           " auf "
-                          a href: "https://www.betterplace.org/de/projects/36213",
-                            "www.betterplace.org"
+                          a href: DONATE_URL, "www.betterplace.org"
                           " unterstützen."
                         br null
                         br null


minor method reordering

minor method reordering

--- library/nucleus/stubs.coffee
+++ library/nucleus/stubs.coffee
@@ -98,29 +98,23 @@
     # Please be sure invoke the `next` arg to proceed, if relevant.
     postpushing: (response, content, next) -> next()

-    # Get the contents of the resources at the established path. It
-    # is a good idea for this HTTP method to be idempotent. As the
-    # rule, this method does not have to alter any contents or data
-    # of the resource. Use for unobtrusive retrieval of resources.
-    GET: @prototype.unsupported
-
     # This method should generally be used to obtain HTTP methods that
     # are allowed on this resources. This is not the only possible way
     # of implementing this method, because it usually can have a lot of
     # different interpretations other than the one in the HTTP spec.
     OPTIONS: @prototype.unsupported

+    # Delete the contents of the resources at the establushed path. It
+    # generally should destroy the contents of the resource for good.
+    # Be sure to provide enough protection for your API for destructive
+    # HTTP methods like this one. Apply it to indicate destruction.
+    DELETE: @prototype.unsupported
+
     # Alter the contents of the resources at the established path. It
     # usually means partial replacing contents with the new contents.
     # This HTTP method nicely maps to UPDATE method of the storages.
     # Use this method to partially replace the contents of resources.
     PATCH: @prototype.unsupported
-
-    # Delete the contents of the resources at the establushed path. It
-    # generally should destroy the contents of the resource for good.
-    # Be sure to provide enough protection for your API for destructive
-    # HTTP methods like this one. Apply it to indicate destruction.
-    DELETE: @prototype.unsupported

     # Append the contents to the resources at the established path. It
     # usually means adding new content in addition to the old one. This
@@ -133,3 +127,9 @@
     # This HTTP method nicely maps to UPDATE method of the storages.
     # Use this method to repetidely replace the contents of resources.
     PUT: @prototype.unsupported
+
+    # Get the contents of the resources at the established path. It
+    # is a good idea for this HTTP method to be idempotent. As the
+    # rule, this method does not have to alter any contents or data
+    # of the resource. Use for unobtrusive retrieval of resources.
+    GET: @prototype.unsupported


refactor safety ensurance

refactor safety ensurance

--- library/nucleus/watch.coffee
+++ library/nucleus/watch.coffee
@@ -154,12 +154,12 @@
     # modules that are not safe to reload are the ones that do export
     # zombie services. They won't be reloaded and a warning is emited.
     ensureSafety: (resolved) ->
-        cached = require.cache[resolved]
-        services = @collectServices cached
-        isZombie = (s) -> s.derives Zombie
-        zombies = _.any services, isZombie
-        assert _.isString cwd = process.cwd()
-        relative = paths.relative cwd, resolved
+        assert cached = require.cache[resolved]
+        services = @collectServices(cached) or []
+        isZombie = (srv) -> try srv.derives Zombie
+        zombies = _.any(services, isZombie) or false
+        assert _.isString current = try process.cwd()
+        relative = paths.relative current, resolved
         message = "Zombies at #{relative.underline}"
         logger.warn message.grey if zombies is yes
         return yes if nconf.get "watch:force"


reimplement

reimplement

git not cherry picking 156a0f3fe1ffe99656acc0d7f25e74bbe44cff0c

--- src/modules/multi-cursor.coffee
+++ src/modules/multi-cursor.coffee
@@ -29,6 +29,7 @@

   moveCursor: (userId, index) ->
     cursor = @cursors[userId]
+    return unless cursor?
     cursor.index = index
     dom(cursor.elem).removeClass('hidden')
     clearTimeout(cursor.timer)


Add play priority to Rebuild

Add play priority to Rebuild

--- cards.coffee
+++ cards.coffee
@@ -2844,6 +2844,8 @@
         state.log("...#{state.current.ai} gains #{newCard}.")
       else
         state.log("...#{state.current.ai} gains nothing.")
+
+  ai_playValue: (state, my) -> 1000
 }

 # Also new in Dark Ages.


what shall we do with the ole' abbot?

what shall we do with the ole' abbot?

--- resource/translators/translator.coffee
+++ resource/translators/translator.coffee
@@ -289,7 +289,10 @@
   return "\\href{#{href}}{#{LaTeX.text2latex(href)}}" if f.name == 'url' && Translator.fancyURLs
   return href

-Reference::particleSpacing = new XRegExp("[-\\s]$")
+Reference::particleSpacing =
+  atEnd: new XRegExp("[-\\s]$")
+  atStart: new XRegExp("^[-\\s]")
+
 Reference::enc_creators = (f, raw) ->
   # family
   # given
@@ -328,14 +331,17 @@
             Translator.debug('particle parser:', creator, '=>', name)

             if name['non-dropping-particle']
-              name['non-dropping-particle'] += ' ' unless XRegExp.test(name['non-dropping-particle'], Reference::particleSpacing)
+              if ! XRegExp.test(name['non-dropping-particle'], Reference::particleSpacing.atEnd) && ! XRegExp.test(name.family, Reference::particleSpacing.atStart)
+                name['non-dropping-particle'] += ' '
               name.family = @enc_latex({value: new String((name['non-dropping-particle'] + name.family).trim())})
             else
               name.family = @enc_latex({value: name.family}).replace(/ and /g, ' {and} ')

             if name['dropping-particle']
-              name['dropping-particle'] += ' ' unless XRegExp.test(name['dropping-particle'], Reference::particleSpacing)
-              name.family = @enc_latex({value: name['dropping-particle']}).replace(/ and /g, ' {and} ') + name.family
+              particle = @enc_latex({value: name['dropping-particle']}).replace(/ and /g, ' {and} ')
+              if ! XRegExp.test(name['dropping-particle'], Reference::particleSpacing.atEnd) && ! XRegExp.test(name.family, Reference::particleSpacing.atStart)
+                particle += ' '
+              name.family = particle + name.family

         if name.given
           name.given = @enc_latex({value: name.given}).replace(/ and /g, ' {and} ')


Change ChoiceInput to be a controlled input

--- lib/inputs.coffee
+++ lib/inputs.coffee
@@ -176,10 +176,6 @@
   propTypes:
     choices: React.PropTypes.object.isRequired
     btnClass: React.PropTypes.string
-    default: React.PropTypes.string
-
-  getInitialState: ->
-    value: null

   onChoiceSelect: (value) ->
     @setState {value}
@@ -191,7 +187,7 @@
       button
         key: choice
         onClick: => @onChoiceSelect choice
-        className: btnClass + " " + cx active: @state.value is choice
+        className: btnClass + " " + cx active: @props.value is choice
       , title

 module.exports = {


Update git-plus.cson

Update git-plus.cson

--- menus/git-plus.cson
+++ menus/git-plus.cson
@@ -36,6 +36,10 @@
         {
           'label': 'Diff All'
           'command': 'git-plus:diff-all'
+        }
+        {
+          'label': 'Add Modified'
+          'command': 'git-plus:add-modified'
         }
         {
           'label': 'Add + Commit'


Dimmer is generating 15% cpu load?!

Dimmer is generating 15% cpu load?!

--- web2/js/views/poi-generator.cjsx
+++ web2/js/views/poi-generator.cjsx
@@ -56,9 +56,6 @@

POI Generator

-

-
Downloading {@props.filename}…

-

Download Failed

Error something something.


removing debug message

removing debug message

--- src/coffee/controllers/player-options-ctrl.coffee
+++ src/coffee/controllers/player-options-ctrl.coffee
@@ -79,7 +79,6 @@
         isChanging = yes
         API.priority.add {stat: prop, points: propDiff[prop]}
         .then (res) ->
-          console.log res.data.isSuccess
           if res.data.isSuccess
             isChanging = no
             return


master

master

--- server/app/bongo/models/invitation.coffee
+++ server/app/bongo/models/invitation.coffee
@@ -63,7 +63,7 @@
   @__sendBetaInvites = bongo.secure do->
     betaTestersEmails = fs.readFileSync 'invitee-emails.txt', 'utf-8'
     # betaTestersEmails = 'chris123412341234@jraphical.com'
-    betaTestersHTML   = fs.readFileSync 'email/beta-testers-invite.html', 'utf-8'
+    betaTestersHTML   = fs.readFileSync 'email/beta-testers-invite.txt', 'utf-8'
     protocol = 'https://'
     (client,callback)->
       i = 0


no data in end allowed anymore, but that clears the header construction a little bit)

--- src/xml.coffee
+++ src/xml.coffee
@@ -27,7 +27,7 @@

     tag.on 'data', pipe.data = (data) =>
         if @pending[0] is tag
-            @emit 'data', data
+            @write data
         else
             @buffer.push data

@@ -35,8 +35,7 @@
         tag.on event, pipe[event] = (args...) =>
             @emit event, args...

-    tag.on 'end', on_end = (data) =>
-        @buffer.push data unless data is undefined
+    tag.on 'end', on_end = =>
         tag.removeListener 'end', on_end
         if @pending[0] is tag
             if tag.pending.length
@@ -52,7 +51,7 @@
                     tag.removeListener event, pipe[event]
                 if @buffer.length
                     for data in @buffer
-                        @emit 'data', data
+                        @write data
                     @buffer = []
                 if @closed and @pending.length is 0
                     @end()
@@ -68,6 +67,10 @@
                     for event in EVENTS
                         tag.removeListener event, pipe[event]
                     if @closed is 'pending'
+                        if @buffer.length
+                            for data in @buffer
+                                @write data
+                            @buffer = []
                         @end()
                     return
             throw new Error("this shouldn't happen D:")
@@ -93,8 +96,8 @@
         @pending = [] # no open child tag
         @closed = false
         @writable = true
+        @isempty = yes
         @content = ""
-        @headers = "<#{@name}#{new_attrs @attrs}"
         @children children, @opts

     tag: (args...) =>
@@ -112,7 +115,6 @@
             for own k, v of key
                 @attrs[k] = v
                 @emit 'attr', this, k, v
-        @headers = "<#{@name}#{new_attrs @attrs}" if @headers
         this

     removeAttr: (key) =>
@@ -123,7 +125,6 @@
             for own k, v of key
                 delete @attr[key]
                 @emit 'attr:remove', this, key
-        @headers = "<#{@name}#{new_attrs @attrs}" if @headers
         this

     children: (children) =>
@@ -143,28 +144,29 @@

     write: (content, {escape} = {}) =>
         content = safe(content) if escape
-        if @headers
-            @emit 'data', "#{indent this}#{@headers}>"
-            delete @headers
-        @emit 'data', "#{indent this}#{content}" if content # FIXME is this really nessary?
-        @content += content
+        if @isempty
+            @emit 'data', "#{indent this}<#{@name}#{new_attrs @attrs}>"
+            @isempty = no
+        @emit 'data', "#{indent this}#{content}" if content
         true

     up: () -> @builder # this node has no parent

     end: () =>
         if not @closed or @closed is 'pending'
-            if @headers
-                data = "#{indent this}#{@headers}/>"
-                @closed = 'self'
+            if @pending.length
+                @emit 'close', this if @closed isnt 'pending'
+                @closed = 'pending'
             else
-                data = "#{indent this}</#{@name}>"
-                if @pending.length
-                    @closed = 'pending'
+                if @isempty
+                    data = "#{indent this}<#{@name}#{new_attrs @attrs}/>"
+                    @closed = 'self'
                 else
+                    data = "#{indent this}</#{@name}>"
                     @closed = yes
-            @emit 'close', this
-            @emit 'end', data unless @closed is 'pending'
+                @emit 'data', data
+
+            @emit 'end' unless @closed is 'pending'
         else if @closed is 'removed'
             @emit 'end'
         else
@@ -198,9 +200,6 @@
         @Tag = @opts.Tag or Tag

     _new_tag: (parent, args...) =>
-        if parent.headers
-            parent.emit 'data', "#{indent parent}#{parent.headers}>"
-            delete parent.headers
         new_tag.apply parent, [this].concat args

     _new_sync_tag: (parent, name, attrs, children, opts) =>
@@ -227,13 +226,14 @@
         @level++
         tag

-    end: (data) =>
+    write: (data) =>
+        @emit 'data', data
+
+    end: () =>
         if @pending.length
             @closed = 'pending'
-            @pending[0].once 'end', =>
-                @end(data)
-        else
-            @emit 'data', "#{indent this}#{data}" if data
+            @pending[0].once 'end', @end
+        else
             @emit 'end' if not @closed or @closed is 'pending'
             @closed = yes
         this


add Tag::root to get to the parent without a parent

--- src/xml.coffee
+++ src/xml.coffee
@@ -196,6 +196,9 @@
         parent = @parent # might get removed
         @end arguments... if opts.end
         parent
+
+    root: () =>
+        @parent?.root() ? this

     show: () =>
         @hidden = no
@@ -316,6 +319,7 @@
         @tag = new_tag
         @$tag = sync_tag

+    root: Tag::root
     show: Tag::show
     hide: Tag::hide
     remove: Tag::remove


update logging ref: #1

update logging ref: #1

--- src/rekognition.coffee
+++ src/rekognition.coffee
@@ -34,15 +34,15 @@
     callRekog msg, 'father-and-son.jpg'

   robot.respond /What do you see?/i, (msg) ->
-    robot.logger.debug "Trying to figure out what's in this image..."
+    robot.logger.debug "hubot-rekognition: trying to figure out what's in this image..."
     if /uploads.hipchat.com/.test(msg.message.text)
       imageUrl = msg.message.text.split('https://s3.amazonaws.com/uploads.hipchat.com/')[1]
       if !imageUrl
         msg.send 'Sorry - I can\'t seem to parse that image url. :('
-      robot.logger.debug "uploading this to s3: https://s3.amazonaws.com/uploads.hipchat.com/#{imageUrl}"
+      robot.logger.debug "hubot-rekognition: uploading this to s3: https://s3.amazonaws.com/uploads.hipchat.com/#{imageUrl}"
       s3FetchAndUpload msg, "https://s3.amazonaws.com/uploads.hipchat.com/#{imageUrl}", imageUrl.split('.')[1]
     else
-      robot.logger.warning 'doesn\'t look like an image was provided'
+      robot.logger.warning 'hubot-rekognition: doesn\'t look like an image was provided'
       msg.send 'Sorry - it doesn\'t look like you gave me an image to look at.'

   sendRobotResponse = (msg, title, image, link) ->
@@ -57,10 +57,10 @@
       encoding: null
     request.get url, requestHeaders, (err, res, body) ->
       if err || res.statusCode != 200
-        robot.logger.warning "could not successfully fetch the image; res.statusCode: #{res.statusCode}, err: #{err}"
+        robot.logger.warning "hubot-rekognition: could not successfully fetch the image; res.statusCode: #{res.statusCode}, err: #{err}"
         msg.send "Something bad happened... I was not able to successfully fetch the image..."
       else
-        robot.logger.debug "Uploading file: #{body.length} bytes, content-type[#{res.headers['content-type']}]"
+        robot.logger.debug "hubot-rekognition: uploading file: #{body.length} bytes, content-type[#{res.headers['content-type']}]"
         uploadToS3(msg, ext, body, body.length, res.headers['content-type'])

   callRekog = (msg, filename) ->
@@ -80,6 +80,7 @@
     })
     rekog.detectLabels rekogParams, (err, data) ->
       if err
+        robot.logger.error "hubot-rekognition: #{err}\nrekogParams are: #{require('util').inspect(rekogParams)}"
         msg.send "Something bad happened... Although I was able to receive the image, I'm blind to it..."
       else
         labelArray = []
@@ -108,10 +109,10 @@
     req = client.put(filename, headers)
     req.on 'response', (res) ->
       if (200 == res.statusCode)
-        robot.logger.debug "file successfully uploaded here: #{client.https(filename)}"
+        robot.logger.debug "hubot-rekognition: file successfully uploaded here: #{client.https(filename)}"
         callRekog msg, filename
       else
-        robot.logger.debug res
-        robot.logger.error "Upload Error Code: #{res.statusCode}"
+        robot.logger.debug "hubot-rekognition: #{res}"
+        robot.logger.error "hubot-rekognition: upload error - res.statusCode: #{res.statusCode}"
         sendRobotResponse msg, title, '[Upload Error]', link
     req.end(content)


Name controls properly

Name controls properly

--- src/render/make.coffee
+++ src/render/make.coffee
@@ -11,7 +11,7 @@
 rows = (label, items) -> group(label, items, vector.axis.down)
 columns = (label, items) -> group(label, items, vector.axis.across)
 buttons = (kind, items, my_kind, action) ->
-  body = columns "#{name}s", items
+  body = columns "#{kind}s", items
   body.stroke = my.color.line
   body.padding = my_kind.padding
   body.fill = my_kind.background


fixed json load timer

fixed json load timer

--- src/core/data/load.coffee
+++ src/core/data/load.coffee
@@ -4,7 +4,8 @@
 # Load Data using JSON
 module.exports = (vars, key, next) ->

-  print.time "loading " + key if vars.dev.value
+  consoleMessage = vars.dev.value
+  print.time "loading " + key if consoleMessage

   url = vars[key].url

@@ -65,5 +66,5 @@
     else
       vars.internal_error = "Could not load data from: \"" + url + "\""

-    print.time "loading " + key if vars.dev.value
+    print.timeEnd "loading " + key if consoleMessage
     next()


Move the issue call under the authenticat middleware since we auth via API key now

--- apps/badges/routes.coffee
+++ apps/badges/routes.coffee
@@ -11,18 +11,17 @@
   app.namespace '/badges', authenticate, ->
     #INDEX
     app.get '/', (req, res) ->
-      if req.session.org_id
-        orgId = req.session.org_id
+      if req.org
+        orgId = req.org.id
       badges = Badge.find(issuer_id: orgId).limit(20).run (err, badges)->
         res.render "#{__dirname}/views/index",
-          badge: new Badge
           badges: badges
           orgId: orgId

     #NEW
     app.get '/new', (req, res) ->
-      if req.session.org_id
-        orgId = req.session.org_id
+      if req.org
+        orgId = req.org.id
       res.render "#{__dirname}/views/new",
         badge: new Badge
         orgId: orgId
@@ -69,16 +68,16 @@
             req.flash 'info', 'Badge Destroyed!'
             res.redirect '/badges'

-  app.post '/badges/issue/:id', (req, res, next) ->
-    username = req.query.username
-    Badge.findById req.params.id, (err, badge) ->
-      next(err) if err
+    app.post '/issue/:id', (req, res, next) ->
+      username = req.query.username
+      Badge.findById req.params.id, (err, badge) ->
+        next(err) if err

-      User.findOrCreate username, badge.issuer_id, (err, user) ->
-        user.earn badge, (err, response) ->
-          next(err) if err
-          res.send JSON.stringify(response),
-            'content-type': 'application/json'
+        User.findOrCreate username, badge.issuer_id, (err, user) ->
+          user.earn badge, (err, response) ->
+            next(err) if err
+            res.send JSON.stringify(response),
+              'content-type': 'application/json'

 formatBadgeResponse = (req, res, badge) ->
   cb = req.query.callback


When updating lines, build contiguous lines from a single html string

This cuts down on parse time and improves the frame rate when scrolling.

--- src/app/editor.coffee
+++ src/app/editor.coffee
@@ -881,15 +881,23 @@
     renderedLines = @renderedLines[0]
     nextIntact = intactRanges.shift()
     currentLine = renderedLines.firstChild
-    screenRow = renderFrom
-    for row in [renderFrom..renderTo]
+
+    row = renderFrom
+    while row <= renderTo
       if row == nextIntact?.end + 1
         nextIntact = intactRanges.shift()
       if !nextIntact or row < nextIntact.start
-        lineElement = @buildLineElementForScreenRow(row)
-        renderedLines.insertBefore(lineElement, currentLine)
+        if nextIntact
+          dirtyRangeEnd = nextIntact.start - 1
+        else
+          dirtyRangeEnd = renderTo
+
+        for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd)
+          renderedLines.insertBefore(lineElement, currentLine)
+          row++
       else
         currentLine = currentLine.nextSibling
+        row++

   updatePaddingOfRenderedLines: ->
     paddingTop = @firstRenderedScreenRow * @lineHeight
@@ -910,10 +918,13 @@
     @pendingChanges.push(change)
     @requestDisplayUpdate()

-  buildLineElementForScreenRow: (screenRow) ->
+  buildLineElementsForScreenRows: (startRow, endRow) ->
     div = document.createElement('div')
-    div.innerHTML = @buildLineHtml(@activeEditSession.lineForScreenRow(screenRow))
-    div.firstChild
+    div.innerHTML = @buildLinesHtml(@activeEditSession.linesForScreenRows(startRow, endRow))
+    new Array(div.children...)
+
+  buildLinesHtml: (screenLines) ->
+    screenLines.map((line) => @buildLineHtml(line)).join('\n\n')

   buildLineHtml: (screenLine) ->
     scopeStack = []


added the socket path as static path

added the socket path as static path

--- src/app/server.coffee
+++ src/app/server.coffee
@@ -8,6 +8,7 @@
     DEFAULT_PATH: "/"
     TEMPLATE_PATH: "/load/:view"
     DIRECTIVE_PATH: "/directive/:template"
+    SOCKET_PATH: "/socket.io/"
   options:
     templatePath: "#{__dirname}/../src/templates"
     isDev: "#{__dirname}/../dev"
@@ -25,6 +26,7 @@

     @app.use "/frontend", express.static(@options.contentPath)
     @app.use "/bower_components", express.static(@options.libraryPath)
+    @app.use @routes.SOCKET_PATH, express.static(@routes.SOCKET_PATH)
     @app.set "views", @options.templatePath
     @app.set "view engine", "jade"
     @app.engine "jade", jade.__express


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/active_bounds.coffee
+++ src/active_bounds.coffee
@@ -3,7 +3,7 @@
 are outside of the specified bounds. The default bounds are
 the dimensions of your game. Useful for bullet type objects.

-    player = GameObject
+    bullet = GameObject
       x: 10
       y: 50
       width: 20


:lipstick:

:lipstick:

--- src/packages/snippets/src/snippet-expansion.coffee
+++ src/packages/snippets/src/snippet-expansion.coffee
@@ -37,7 +37,6 @@
         _.remove(@tabStopAnchorRanges, anchorRange)
       anchorRange
     @setTabStopIndex(0)
-

   indentSubsequentLines: (startRow, snippet) ->
     initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]


Remove reference to Bootstrap

Remove reference to Bootstrap

--- app/assets/javascripts/hyper_admin/angularjs/directives/form/form_input.js.coffee
+++ app/assets/javascripts/hyper_admin/angularjs/directives/form/form_input.js.coffee
@@ -1,7 +1,7 @@
 angular.module("hyperadmin")
   .directive "formInput", ->
     template: """
-

+

   <ng-include src="type"></ng-include>


Provide data types and other port metadata

--- src/RemoteSubGraph.coffee
+++ src/RemoteSubGraph.coffee
@@ -101,11 +101,18 @@
     @prepareOutport port for port in ports.outPorts
     @setReady true

+  normalizePort: (definition) ->
+    type = definition.type or 'all'
+    type = 'all' if type is 'any'
+    return def =
+      datatype: type
+      required: definition.required or false
+      addressable: definition.addressable or false
+
   prepareInport: (definition) ->
     name = definition.id
     # Send data across to remote graph
-    # TODO: set metadata like datatype
-    @inPorts.add name, {}, (event, packet) =>
+    @inPorts.add name, @normalizePort(definition), (event, packet) =>
       @runtime.sendRuntime 'packet',
         port: name
         event: event
@@ -114,11 +121,9 @@

   prepareOutport: (definition) ->
     name = definition.id
-    # TODO: set metadata like datatype
-    port = @outPorts.add name, {}
+    port = @outPorts.add name, @normalizePort definition

   onPacketReceived: (packet) ->
-    # TODO: set metadata like datatype
     name = packet.port
     port = @outPorts[name]
     switch packet.event


payment: plan add products modal

payment: plan add products modal

--- client/app/Applications/Dashboard.kdapplication/views/planaddproductsmodal.coffee
+++ client/app/Applications/Dashboard.kdapplication/views/planaddproductsmodal.coffee
@@ -4,9 +4,16 @@

     options.title ?= "Add products"

+    options.overlay ?= yes
+
     super options, data

     data = @getData()
+
+    @loader = new KDLoaderView { size: 14 }
+
+    @addSubView @loader
+    @loader.show()

     @planExplanation = new KDCustomHTMLView
       cssClass: 'modalformline'
@@ -28,8 +35,33 @@

Add some products to this plan

         """

+    @products = new KDListViewController
+      itemClass: GroupAddProductListItem
+
+    @buttonField = new KDView
+      cssClass: "formline button-field clearfix"
+
+    @buttonField.addSubView new KDButtonView
+      title: 'Save'
+      cssClass: 'modal-clean-green'
+
+    @buttonField.addSubView new KDButtonView
+      title: 'cancel'
+      cssClass: 'modal-cancel'
+
     @addSubView @planExplanation
     @addSubView @planView
     @addSubView @productsExplanation
+    @addSubView @products.getView()
+    @addSubView @buttonField

-  setContents: (contents) -> debugger
+
+  setProducts: (products) ->
+    @loader.hide()
+
+    plan = @getData()
+
+    for product in products
+      qty = plan.quatities?[product.planCode] ? 0
+      item = @products.addItem product
+      item.setQuantity qty