Table rules (taken from l-latex)

Table rules (taken from l-latex)

--- grammars/latex.cson
+++ grammars/latex.cson
@@ -157,15 +157,19 @@
       { include: '#envDocument' }
       { include: '#envFigure' }
       { include: '#metaEnvMath' }
-      # { include: '#metaEnvTable' }
       { include: '#envList' }
       { include: '#envGnuplot' }
       { include: '#metaEnvMinted' }
-      # { include: '#metaEnvListing' }
       { include: '#envLuaCode' }
       { include: '#envVerbatim' }
       { include: '#envTikz' }
+      { include: '#envTable' }
       { include: '#envGeneric' }
+
+      # Potential additions:
+      # Better table rules; currently taken from language-latex
+      # { include: '#metaEnvListing' }
+      # More languages with minted
     ]
   }
   metaPrimitives: {
@@ -1167,6 +1171,71 @@
         name: 'punctuation.definition.arguments.end.latex'
     patterns: [{ include: 'text.tex.latex.tikz' }] # The l-latex package has complicated stuff here, so this might break
   }
+  envTable: {
+    comment: 'The following is taken verbatim (sans key quotes) from language-latex'
+    begin: '''(?x)
+                (?:\\s*)
+                ((\\\\)begin)
+                (\\{)
+                (array|tabular[xy*]?)
+                (\\})
+                (\\s*)?
+             '''
+    captures:
+      1:
+        name: 'support.function.begin.latex'
+      2:
+        name: 'punctuation.definition.function.latex'
+      3:
+        name: 'punctuation.definition.arguments.begin.latex'
+      4:
+        name: 'variable.parameter.function.latex'
+      5:
+        name: 'punctuation.definition.arguments.end.latex'
+    contentName: 'meta.data.environment.tabular.latex'
+    end: '''(?x)
+              (?:\\s*)
+              ((\\\\)end)
+              (\\{)
+              (\\4)
+              (\\})
+              (?:\\s*)?
+           '''
+    name: 'meta.function.environment.tabular.latex'
+    patterns: [
+      {
+        match: '\\\\\\\\'
+        name: 'punctuation.definition.table.row2.latex'
+      }
+      {
+        begin: '(?:^|(?<=\\\\\\\\))(?!\\\\\\\\|\\s*\\\\end\\{(?:tabular|array))'
+        end: '(?=\\\\\\\\|\\s*\\\\end\\{(?:tabular|array))'
+        name: 'meta.row2.environment.tabular.latex'
+        patterns: [
+          {
+            match: '&'
+            name: 'punctuation.definition.table.cell.latex'
+          }
+          {
+            begin: '(?:^|(?<=&))((?!&|\\\\\\\\|$))'
+            end: '(?=&|\\\\\\\\|\\s*\\\\end\\{(?:tabular|array))'
+            name: 'meta.cell.environment.tabular.latex'
+            patterns: [
+              {
+                include: '$base'
+              }
+            ]
+          }
+          {
+            include: '$base'
+          }
+        ]
+      }
+      {
+        include: '$base'
+      }
+    ]
+  }
   metaEnvMinted: {
     comment: 'This groups together all the minted languages supported by this grammar.'
     patterns: [
@@ -1181,7 +1250,6 @@
       { include: '#mintGeneric' }
     ]
   }
-
   mintGeneric: {
     comment: 'A catch all for any other languages.'
     name: 'meta.environment.embedded.$11.latex'


Color macro functions that take multiple args accurately. Fixes #71

--- grammars/stata.cson
+++ grammars/stata.cson
@@ -950,12 +950,18 @@
         ]
       }
       {
-        'begin': '\\b(tempvar|tempname|tempfile)\\s+(?=[a-zA-Z_`\\$])'
+        'begin': '\\b(tempvar|tempname|tempfile)\\s*(?=\\s)'
+        'comment': 'The \\s*(?=\\s) means that if you have `tempvar\\n`, the next line won\'t be colored as a macro. This is because the newline character is whitespace, so you have to let the \\n be caught by the end pattern.'
         'beginCaptures':
           '1':
             'name': 'keyword.macro.stata'
-        'end': '(?=\\"|\\s|\\n|/|,)'
-        'patterns': [
+        'end': '\\n'
+        'patterns': [
+          {
+            'begin': '///'
+            'end': '\\n'
+            'name': 'comment.block.stata'
+          }
           {
             'include': '#macro-reserved_names'
           }
@@ -972,12 +978,22 @@
         ]
       }
       {
-        'begin': '\\b(ma(cro|cr|c)?)\\s+(drop|l(ist|is|i)?)'
+        'begin': '\\b(ma(cro|cr|c)?)\\s+(drop|l(ist|is|i)?)\\s*(?=\\s)'
+        'comment': 'The \\s*(?=\\s) means that if you have `tempvar\\n`, the next line won\'t be colored as a macro. This is because the newline character is whitespace, so you have to let the \\n be caught by the end pattern.'
         'beginCaptures':
           '0':
             'name': 'keyword.macro.stata'
-        'end': '(?=\\n)'
-        'patterns': [
+        'end': '\\n'
+        'patterns': [
+          {
+            'begin': '///'
+            'end': '\\n'
+            'name': 'comment.block.stata'
+          }
+          {
+            'match': '\\*'
+            'name': 'keyword.operator.arithmetic.stata'
+          }
           {
             'include': '#constants'
           }


ebb/flow updates

ebb/flow updates

--- grow-systems.coffee
+++ grow-systems.coffee
@@ -110,7 +110,7 @@
 				@planFlow()
 			else
 				debug.log 'Going to Ebb in ', schedule.flow - fromFlow, 'min'
-				@time.delay schedule.flow - fromFlow, => planEbb()
+				@time.delay schedule.flow - fromFlow, => @planEbb()

 		planFlow: ->
 			fromEbb = @time.from( @ebbStart )
@@ -120,7 +120,7 @@
 				@planEbb()
 			else
 				debug.log 'Going to Flow in ', schedule.ebb - fromEbb, 'min'
-				@time.delay schedule.ebb - fromEbb, => planFlow()
+				@time.delay schedule.ebb - fromEbb, => @planFlow()


account: payment modal validation fixes

account: payment modal validation fixes

--- client/app/Applications/Account.kdapplication/account/accSettingsPaymentMethods.coffee
+++ client/app/Applications/Account.kdapplication/account/accSettingsPaymentMethods.coffee
@@ -81,8 +81,13 @@
       for key, val of ERRORS
         if e.field.indexOf(key) > -1
           val.input.giveValidationFeedback yes
-          field = val.field.subViews[0].labelTitle
-          val.input.showValidationError "#{field} #{e.message}"
+          inputName = if val.input.inputLabel
+            val.input.inputLabel.getTitle()
+          else
+            inputEl = val.input.$()[0]
+            inputEl.getAttribute("placeholder") or inputEl.getAttribute("name") or ""
+
+          val.input.showValidationError "#{inputName} #{e.message}"

 class AccountPaymentMethodsListController extends KDListViewController
   constructor:(options,data)->
@@ -134,64 +139,77 @@
 createAccountPaymentMethodModal = (data, callback) ->
   modal = new KDModalViewWithForms
     title                       : "Billing Information"
-    content                     : ''
-    helpContent                 :
-      """
-      """
+    width                       : 500
+    cssClass                    : "payments-modal"
     overlay                     : yes
-    width                       : 500
-    height                      : "auto"
-    cssClass                    : "payments-modal"
     tabs                        :
       navigable                 : yes
       goToNextFormOnSubmit      : no
       forms                     :
         "Billing Info"          :
+          callback              : (formData)=>
+            form      = modal.modalTabs.forms["Billing Info"]
+            button    = form.buttons.Save
+            onError   = (err)->
+              warn err
+              showAccountPaymentErrors form.fields, form.inputs, err
+              button.hideLoader()
+            onSuccess = modal.destroy.bind modal
+            callback formData, onError, onSuccess
           buttons               :
             Save                :
               title             : "Save"
               style             : "modal-clean-green"
               type              : "submit"
               loader            :
-                color           : "#444444"
+                color           : "#ffffff"
                 diameter        : 12
-              callback          : =>
-                tabs = modal.modalTabs
-                form = tabs.forms["Billing Info"]
-                button = form.buttons["Save"]
-                onError = ->
-                    button.hideLoader()
-                onSuccess = ->
-                    modal.destroy()
-                callback form.getFormData(), onError, onSuccess
           fields                :
             cardFirstName       :
               label             : "Name"
-              itemClass         : KDInputView
               name              : "cardFirstName"
               placeholder       : "First Name"
+              validate          :
+                # event           : "blur"
+                rules           :
+                  required      : yes
+                messages        :
+                  required      : "First name is required!"
               nextElementFlat   :
                 cardLastName    :
-                  itemClass     : KDInputView
                   name          : "cardLastName"
                   placeholder   : "Last Name"
+                  validate      :
+                    # event       : "blur"
+                    rules       :
+                      required  : yes
+                    messages    :
+                      required  : "Last name is required!"
             cardNumber          :
               label             : "Card Number"
-              itemClass         : KDInputView
               name              : "cardNumber"
               placeholder       : 'Card Number'
-              # validate          :
-              #   event           : "blur"
-              #   rules           : "creditCard"
+              validate          :
+                event           : "blur"
+                rules           :
+                  creditCard    : yes
               nextElementFlat   :
                 cardCV          :
-                  tooltip       :
-                    placement   : 'right'
-                    direction   : 'center'
-                    title       : 'The location of this verification number depends on the issuer of your credit card'
-                  itemClass     : KDInputView
+                  # tooltip       :
+                  #   placement   : 'right'
+                  #   direction   : 'center'
+                  #   title       : 'The location of this verification number depends on the issuer of your credit card'
                   name          : "cardCV"
                   placeholder   : "CV Number"
+                  validate      :
+                    rules       :
+                      required  : yes
+                      minLength : 3
+                      regExp    : /[0-9]/
+                    messages    :
+                      required  : "Card security code is required! (CVV)"
+                      minLength : "Card security code needs to be at least 3 digits!"
+                      regExp    : "Card security code should be a number!"
             cardMonth           :
               label             : "Expire Date"
               itemClass         : KDSelectBox
@@ -207,36 +225,60 @@
                   defaultValue  : (new Date().getFullYear())
             address1            :
               label             : "Address"
-              itemClass         : KDInputView
               name              : "address1"
               placeholder       : "Street Name & Number"
+              validate          :
+                rules           :
+                  required      : yes
+                messages        :
+                  required      : "First address field is required!"
             address2            :
               label             : " "
-              itemClass         : KDInputView
               name              : "address2"
               placeholder       : "Apartment/Suite Number"
             city                :
               label             : "City & State"
-              itemClass         : KDInputView
               name              : "city"
               placeholder       : "City Name"
+              validate          :
+                rules           :
+                  required      : yes
+                messages        :
+                  required      : "City is required!"
               nextElementFlat   :
                 state           :
-                  itemClass     : KDInputView
                   name          : "state"
                   placeholder   : "State"
+                  validate      :
+                    rules       :
+                      required  : yes
+                    messages    :
+                      required  : "State is required!"
             zip                 :
               label             : "ZIP & Country"
-              itemClass         : KDInputView
               name              : "zipCode"
               placeholder       : "ZIP Code"
+              validate          :
+                rules           :
+                  required      : yes
+                messages        :
+                  required      : "Zip code is required!"
               nextElementFlat   :
                 country         :
-                  itemClass     : KDInputView
                   name          : "country"
                   placeholder   : "Country"
+                  defaultValue  : "United States of America"
+                  validate      :
+                    rules       :
+                      required  : yes
+                    messages    :
+                      required  : "First address field is required!"

   form = modal.modalTabs.forms["Billing Info"]
+
+  form.on "FormValidationFailed", =>
+    form.buttons.Save.hideLoader()
+
   for k, v of data
     if form.inputs[k]
       form.inputs[k].setValue v
@@ -258,11 +300,10 @@
       modal = createAccountPaymentMethodModal data, (newData, onError, onSuccess) ->
         KD.remote.api.JRecurlyPlan.setUserAccount newData, (err, result)->
           if err
-            onError()
+            onError err
           else
-
             controller.emit 'reload'
-            onSuccess()
+            onSuccess result

       form = modal.modalTabs.forms["Billing Info"]


remove inop spec

remove inop spec

--- spec/skr/screens/fresh-books-import/FreshBooksImportSpec.coffee
+++ spec/skr/screens/fresh-books-import/FreshBooksImportSpec.coffee
@@ -1,5 +1 @@
 describe "Skr.Screens.FreshBooksImport", ->
-
-    it "can be rendered", ->
-        screen = LT.renderComponent(Skr.Screens.FreshBooksImport)
-        expect(screen.getDOMNode().textContent).toMatch("Hello")


fix css bug

fix css bug

--- src/themes/color-picker.coffee
+++ src/themes/color-picker.coffee
@@ -13,5 +13,9 @@
     ScribeDOM.setText(item, '')
     return item

+  selectItem: (item) ->
+    super(item)
+    @label.innerHTML = ''
+

 module.exports = ScribeColorPicker


Newline for readability.

Newline for readability.

--- src/quests/example/example.coffee
+++ src/quests/example/example.coffee
@@ -28,6 +28,8 @@
     console.log "People older than 20:".underline
     for i, person of @getPeopleOlderThan 20
       console.log "* #{person}".underline
+    console.log()
+
     @transaction =>
       @sql "create temp table foo (x int);"
       @sql "insert into table foo VALUES ('on no');"


made tests more friendly

made tests more friendly

--- test/api.coffee
+++ test/api.coffee
@@ -29,6 +29,7 @@

 describe 'model', ->
   it 'should save data', ->
+    should.exist User, 'expected User model to be defined'
     User.create Factory.userData, (err, user) ->
       should.not.exist err
       should.exist user
@@ -57,7 +58,7 @@
       done()

   it 'should respond to GET /users/:id', (done) ->
-    verify 'get', "users/#{@user._id}", 200, null, (result) ->
+    verify 'get', "users/#{@user?._id}", 200, null, (result) ->
       should.exist result?.user, 'expected {user: {}}'
       result.user.should.include Factory.userData
       done()
@@ -78,13 +79,13 @@
     data = {
       role: 'Bilge Cleaner'
     }
-    verify 'put', "users/#{@user._id}", 200, data, (result) ->
+    verify 'put', "users/#{@user?._id}", 200, data, (result) ->
       should.exist result?.user
       result.user.should.include data
       done()

   it 'should respond to DELETE /users/:id', (done) ->
-    verify 'del', "users/#{@user._id}", 200, null, (result) ->
+    verify 'del', "users/#{@user?._id}", 200, null, (result) ->

       if Factory.connected
         User.count (err, count) ->


Track collection id and page number on each page

--- src/scripts/models/content.coffee
+++ src/scripts/models/content.coffee
@@ -64,12 +64,16 @@
     # Create a flat collection to store all the pages
     setupToc: () ->
       toc = new Backbone.Collection()
+      id = @get('id')
       depth = 0
+      page = 1
       parent = -1

       traverse = (o = {}, sub) ->
         for item in o.contents
+          item.collectionId = id
           item.depth = depth
+          item.page = page++
           if depth is 1 then item.parent = parent
           else if depth is 0 then parent++
           if item.contents


Ticker: add permission for activity ticker

--- workers/social/lib/social/models/activityticker.coffee
+++ workers/social/lib/social/models/activityticker.coffee
@@ -71,20 +71,19 @@
     JGroup            = require './group'
     from              = options.from or +(new Date())

-    relOptions        =    # do not fetch more than 15 at once
-      limit           : 10 # Math.min options.limit ? 15, 15
-      sort            : timestamp : -1
+    JGroup.canReadGroupActivity client, (err, hasPermission)->
+      return callback new Error "Not allowed to open this group"  if err or not hasPermission

-    JGroup.one slug : groupSlug, (err, group)->
-      return callback err if err
-      return callback new Error "Group not found" if not group
-
-      selector    =
+      relSelector =
         timestamp : {"$lt" : new Date(from)}
         data      :
           group   : groupSlug

-      Relationship.some selector, relOptions, (err, relationships) ->
+      relOptions  =    # do not fetch more than 15 at once
+        limit     : 10 # Math.min options.limit ? 15, 15
+        sort      : timestamp : -1
+
+      Relationship.some relSelector, relOptions, (err, relationships) ->
         buckets = []
         return  callback err, buckets  if err
         queue = relationships.map (relationship) ->


Activity: generate time clause

Activity: generate time clause

--- workers/social/lib/social/models/graph/activity.coffee
+++ workers/social/lib/social/models/graph/activity.coffee
@@ -180,7 +180,8 @@
           groupName  : currentGroup.slug or "koding"
           userId     : delegate.getId()

-        query = QueryRegistry.activity.followingnew exemptClause
+        timeQuery = @generateTimeQuery options
+        query = QueryRegistry.activity.followingnew exemptClause, timeQuery
         @fetch query, queryOptions, (err, results) =>
           return callback err  if err
           callback err, results


Call onFinished if the callback returns false

--- drivers/javascript/cursor.coffee
+++ drivers/javascript/cursor.coffee
@@ -189,6 +189,9 @@
                 else
                     stopFlag = cb(null, data) is false
                     @_next nextCb
+            else
+                if onFinished?
+                    onFinished()
         @_next nextCb
     )


new fields for middlename and suffix feature

--- src/salesforce-sobjects.coffee
+++ src/salesforce-sobjects.coffee
@@ -1264,7 +1264,9 @@
     "jigsawcontactid",
     "canallowportalselfreg",
     "othergeocodeaccuracy",
-    "mailinggeocodeaccuracy"
+    "mailinggeocodeaccuracy",
+    "middlename",
+    "suffix"
   ],
   "contactfeed": [
     "id",
@@ -2757,7 +2759,9 @@
     "donotcall",
     "hasoptedoutoffax",
     "lasttransferdate",
-    "geocodeaccuracy"
+    "geocodeaccuracy",
+    "middlename",
+    "suffix"
   ],
   "leadshare": [
     "id",
@@ -3076,7 +3080,9 @@
     "phone",
     "username",
     "lastvieweddate",
-    "lastreferenceddate"
+    "lastreferenceddate",
+    "middlename",
+    "suffix"
   ],
   "opportunitycompetitor": [
     "id",
@@ -3627,7 +3633,8 @@
     "permissionsportalsuperuser",
     "permissionsdelegatedportaluseradmin",
     "permissionslightningexperienceuser",
-    "permissionsselectfilesfromsalesforce"
+    "permissionsselectfilesfromsalesforce",
+    "permissionsmanagesandboxes"
   ],
   "permissionsetassignment": [
     "id",
@@ -3805,7 +3812,8 @@
     "maximumpermissionsportalsuperuser",
     "maximumpermissionsdelegatedportaluseradmin",
     "maximumpermissionslightningexperienceuser",
-    "maximumpermissionsselectfilesfromsalesforce"
+    "maximumpermissionsselectfilesfromsalesforce",
+    "maximumpermissionsmanagesandboxes"
   ],
   "permissionsetlicenseassign": [
     "id",
@@ -4212,7 +4220,8 @@
     "permissionsportalsuperuser",
     "permissionsdelegatedportaluseradmin",
     "permissionslightningexperienceuser",
-    "permissionsselectfilesfromsalesforce"
+    "permissionsselectfilesfromsalesforce",
+    "permissionsmanagesandboxes"
   ],
   "publisher": [
     "id",
@@ -4266,7 +4275,9 @@
     "phone",
     "lastvieweddate",
     "lastreferenceddate",
-    "language"
+    "language",
+    "middlename",
+    "suffix"
   ],
   "recordtype": [
     "id",
@@ -5024,7 +5035,9 @@
     "geocodeaccuracy",
     "userpreferencespathassistantcollapsed",
     "userpreferencescachediagnostics",
-    "userpreferenceslightningexperiencepreferred"
+    "userpreferenceslightningexperiencepreferred",
+    "middlename",
+    "suffix"
   ],
   "userfeed": [
     "id",
@@ -5375,7 +5388,8 @@
     "contentdescription",
     "contenttype",
     "contentsize",
-    "insertedbyid"
+    "insertedbyid",
+    "isrichtext"
   ],
   "order": [
     "id",
@@ -5421,7 +5435,9 @@
     "isdeleted",
     "systemmodstamp",
     "lastvieweddate",
-    "lastreferenceddate"
+    "lastreferenceddate",
+    "billinggeocodeaccuracy",
+    "shippinggeocodeaccuracy"
   ],
   "orderitem": [
     "id",
@@ -5455,132 +5471,6 @@
     "newvalue"
   ],
   "orderitemfeed": [
-    "id",
-    "parentid",
-    "type",
-    "createdbyid",
-    "createddate",
-    "isdeleted",
-    "lastmodifieddate",
-    "systemmodstamp",
-    "commentcount",
-    "likecount",
-    "title",
-    "body",
-    "linkurl",
-    "relatedrecordid",
-    "contentdata",
-    "contentfilename",
-    "contentdescription",
-    "contenttype",
-    "contentsize",
-    "insertedbyid"
-  ],
-  "ordertag": [
-    "id",
-    "tagdefinitionid",
-    "itemid",
-    "createddate",
-    "systemmodstamp",
-    "isdeleted",
-    "name",
-    "type"
-  ],
-  "ordershare": [
-    "id",
-    "orderid",
-    "userorgroupid",
-    "orderaccesslevel",
-    "rowcause",
-    "lastmodifieddate",
-    "lastmodifiedbyid",
-    "isdeleted"
-  ],
-  "reporttag": [
-    "id",
-    "tagdefinitionid",
-    "itemid",
-    "createddate",
-    "systemmodstamp",
-    "isdeleted",
-    "name",
-    "type"
-  ],
-  "selfserviceuser": [
-    "id",
-    "lastname",
-    "firstname",
-    "name",
-    "username",
-    "email",
-    "isactive",
-    "timezonesidkey",
-    "localesidkey",
-    "contactid",
-    "languagelocalekey",
-    "superuser",
-    "lastlogindate",
-    "createddate",
-    "createdbyid",
-    "lastmodifieddate",
-    "lastmodifiedbyid",
-    "systemmodstamp"
-  ],
-  "solutiontag": [
-    "id",
-    "tagdefinitionid",
-    "itemid",
-    "createddate",
-    "systemmodstamp",
-    "isdeleted",
-    "name",
-    "type"
-  ],
-  "tagdefinition": [
-    "id",
-    "name",
-    "lastmodifiedbyid",
-    "systemmodstamp",
-    "isdeleted",
-    "type"
-  ],
-  "taskrelation": [
-    "id",
-    "relationid",
-    "taskid",
-    "iswhat",
-    "accountid",
-    "createddate",
-    "createdbyid",
-    "lastmodifieddate",
-    "lastmodifiedbyid",
-    "systemmodstamp",
-    "isdeleted"
-  ],
-  "tasktag": [
-    "id",
-    "tagdefinitionid",
-    "itemid",
-    "createddate",
-    "systemmodstamp",
-    "isdeleted",
-    "name",
-    "type"
-  ],
-  "taskwhorelation": [
-    "id",
-    "relationid",
-    "taskid",
-    "accountid",
-    "createddate",
-    "createdbyid",
-    "lastmodifieddate",
-    "lastmodifiedbyid",
-    "systemmodstamp",
-    "isdeleted",
-    "type"
-  ],
-  "document_management__feed": [
     "id",
     "parentid",
     "type",
@@ -5603,6 +5493,133 @@
     "insertedbyid",
     "isrichtext"
   ],
+  "ordertag": [
+    "id",
+    "tagdefinitionid",
+    "itemid",
+    "createddate",
+    "systemmodstamp",
+    "isdeleted",
+    "name",
+    "type"
+  ],
+  "ordershare": [
+    "id",
+    "orderid",
+    "userorgroupid",
+    "orderaccesslevel",
+    "rowcause",
+    "lastmodifieddate",
+    "lastmodifiedbyid",
+    "isdeleted"
+  ],
+  "reporttag": [
+    "id",
+    "tagdefinitionid",
+    "itemid",
+    "createddate",
+    "systemmodstamp",
+    "isdeleted",
+    "name",
+    "type"
+  ],
+  "selfserviceuser": [
+    "id",
+    "lastname",
+    "firstname",
+    "name",
+    "username",
+    "email",
+    "isactive",
+    "timezonesidkey",
+    "localesidkey",
+    "contactid",
+    "languagelocalekey",
+    "superuser",
+    "lastlogindate",
+    "createddate",
+    "createdbyid",
+    "lastmodifieddate",
+    "lastmodifiedbyid",
+    "systemmodstamp"
+  ],
+  "solutiontag": [
+    "id",
+    "tagdefinitionid",
+    "itemid",
+    "createddate",
+    "systemmodstamp",
+    "isdeleted",
+    "name",
+    "type"
+  ],
+  "tagdefinition": [
+    "id",
+    "name",
+    "lastmodifiedbyid",
+    "systemmodstamp",
+    "isdeleted",
+    "type"
+  ],
+  "taskrelation": [
+    "id",
+    "relationid",
+    "taskid",
+    "iswhat",
+    "accountid",
+    "createddate",
+    "createdbyid",
+    "lastmodifieddate",
+    "lastmodifiedbyid",
+    "systemmodstamp",
+    "isdeleted"
+  ],
+  "tasktag": [
+    "id",
+    "tagdefinitionid",
+    "itemid",
+    "createddate",
+    "systemmodstamp",
+    "isdeleted",
+    "name",
+    "type"
+  ],
+  "taskwhorelation": [
+    "id",
+    "relationid",
+    "taskid",
+    "accountid",
+    "createddate",
+    "createdbyid",
+    "lastmodifieddate",
+    "lastmodifiedbyid",
+    "systemmodstamp",
+    "isdeleted",
+    "type"
+  ],
+  "document_management__feed": [
+    "id",
+    "parentid",
+    "type",
+    "createdbyid",
+    "createddate",
+    "isdeleted",
+    "lastmodifieddate",
+    "systemmodstamp",
+    "commentcount",
+    "likecount",
+    "title",
+    "body",
+    "linkurl",
+    "relatedrecordid",
+    "contentdata",
+    "contentfilename",
+    "contentdescription",
+    "contenttype",
+    "contentsize",
+    "insertedbyid",
+    "isrichtext"
+  ],
   "document_management__history": [
     "id",
     "isdeleted",


fix misleading heading

fix misleading heading

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -1232,12 +1232,14 @@
         switch @props.file_action
             when 'rename'
                 action_title = 'Rename'
+                first_heading = 'Change the name'
             when 'duplicate'
                 action_title = 'Duplicate'
+                first_heading = 'File to duplicate'

             <Row>
                 <Col sm={5} style={color:'#666'}>
-
Change the name

+
{first_heading}

                     {@render_selected_files_list()}
                 </Col>
                 <Col sm={5} style={color:'#666'}>


Update danbooru.coffee

Update danbooru.coffee

--- modules/danbooru.coffee
+++ modules/danbooru.coffee
@@ -20,7 +20,7 @@
       # Blacklisted tags (discord community guidelines)
       # coffeelint: disable=max_line_length
       for tag in args.split(' ')
-        if tag in ['loli', 'shota', 'lolicon'] then return msg.reply '', embed: image: url: 'https://cdn.discordapp.com/attachments/244581077610397699/315655455143886850/Screenshot_from_2017-05-20_21-01-03.png'
+        if tag in ['loli', 'shota', 'lolicon', 'toddlercon'] then return msg.reply '', embed: image: url: 'https://cdn.discordapp.com/attachments/244581077610397699/315655455143886850/Screenshot_from_2017-05-20_21-01-03.png'
         if tag in ['gore', 'guro'] then return msg.reply 'nope', embed: image: url: 'http://25.media.tumblr.com/tumblr_lqhsh2zVkZ1qjlcvoo1_500.jpg'
       # coffeelint: enable=max_line_length
       # Get a random post


collate cell renderers

collate cell renderers

--- h2o-web/src/main/steam/scripts/flow.coffee
+++ h2o-web/src/main/steam/scripts/flow.coffee
@@ -62,6 +62,16 @@
   _selectedCellIndex = -1
   _clipboardCell = null
   _lastDeletedCell = null
+  _renderers =
+    h1: -> Flow.HtmlTag _, 'h1'
+    h2: -> Flow.HtmlTag _, 'h2'
+    h3: -> Flow.HtmlTag _, 'h3'
+    h4: -> Flow.HtmlTag _, 'h4'
+    h5: -> Flow.HtmlTag _, 'h5'
+    h6: -> Flow.HtmlTag _, 'h6'
+    md: -> Flow.Markdown _
+    cs: -> Flow.Coffeescript _
+    raw: -> Flow.Raw _

   countLines = (text) ->
     newlineCount = 1
@@ -86,17 +96,7 @@

   createCell = (type='cs', input='') ->
     _type = node$ type
-    _renderer = lift$ _type, (type) ->
-      switch type
-        when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
-          Flow.HtmlTag _, type
-        when 'md'
-          Flow.Markdown _
-        when 'cs'
-          Flow.Coffeescript _
-        else
-          Flow.Raw _
-
+    _renderer = lift$ _type, (type) -> _renderers[type]()
     _isSelected = node$ no
     _isActive = node$ no
     _hasError = node$ no


add an issues tab showing upstream smc issues

--- src/smc-webapp/editor_git.cjsx
+++ src/smc-webapp/editor_git.cjsx
@@ -14,6 +14,7 @@
     {"name": "Configuration", "icon": "settings", "description": "Configure global git settings as well as repo settings", "init_actions": ['get_git_user_name', 'get_git_user_email']},
     {"name": "Commit", "icon": "git-commit", "description": "Commit files", "init_actions": ['get_changed_tracked_files', 'get_changed_untracked_files', 'update_diff']},
     {"name": "Log", "icon": "history", "description": "Log of commits", "init_actions": ['update_log']},
+    {"name": "Issues", "icon": "issue-opened", "description": "Github issues for upstream", "init_actions": ['get_github_issues']},
 ]

 TABS_BY_NAME = {}
@@ -46,6 +47,11 @@
                     console.warn("git editor ERROR exec'ing #{opts.cmd} #{opts.args.join(' ')}")
                     opts.cb(err, output)
                 opts.cb(err, output)
+
+    get_github_issues : =>
+        url = 'https://api.github.com/repos/sagemathinc/smc/issues'
+        callback = (response) => @setState(github_issues: response)
+        $.get url, callback

     set_git_user_name : =>
         store = @redux.getStore(@name)
@@ -350,6 +356,7 @@
             show_create_branch_modal    : rtypes.bool
             new_branch_name             : rtypes.string
             interval                    : rtypes.func
+            github_issues               : rtypes.array

     propTypes :
         actions : rtypes.object
@@ -523,6 +530,15 @@
                 </Col>
             </Row>
         </div>
+
+    render_issues : ->
+        <div>
+            <Row>
+                <Col sm=12>
+                    {JSON.stringify(@props.github_issues)}
+                </Col>
+            </Row>
+        </div>

     render_tab_header : (name, icon, description)->


fix bug with create modal not being hidden

--- src/smc-webapp/editor_git.cjsx
+++ src/smc-webapp/editor_git.cjsx
@@ -342,7 +342,7 @@
             git_changed_untracked_files : rtypes.array
             checked_files               : rtypes.object
             file_to_diff                : rtypes.string
-            show_create_branch_modal    : rtypes.boolean
+            show_create_branch_modal    : rtypes.bool
             new_branch_name             : rtypes.string

     propTypes :
@@ -549,6 +549,7 @@
             @props.actions[action]()

     render : ->
+        console.log(@props.show_create_branch_modal)

Git Repository at {@props.git_repo_root}

@@ -562,7 +563,7 @@
                     {@render_branches()}
                 </DropdownButton>

-                    <Modal.Dialog show={@props.show_create_branch_modal} onHide={=>@props.actions.setState(show_create_branch_modal:false)}>
+                    <Modal show={@props.show_create_branch_modal} onHide={=>@props.actions.setState(show_create_branch_modal:false)}>
                         <Modal.Header>
                             <Modal.Title>Create a banch</Modal.Title>
                         </Modal.Header>
@@ -579,11 +580,11 @@
                       </Modal.Body>

                       <Modal.Footer>
-                        <Button>Close</Button>
+                        <Button onClick={=>@props.actions.setState(show_create_branch_modal:false)}>Close</Button>
                         <Button bsStyle="primary" onClick={=>@props.actions.create_branch_and_reset_to_upstream_master();@props.actions.setState(show_create_branch_modal:false)}>Create the branch</Button>
                       </Modal.Footer>

-                    </Modal.Dialog>
+                    </Modal>

             <Tabs animation={false} activeKey={@props.tab} onSelect={(key)=>@props.actions.set_tab(key)}>


Update src/plugins/markdown-page.coffee

Update src/plugins/markdown-page.coffee

minor regex optimization

--- src/plugins/markdown-page.coffee
+++ src/plugins/markdown-page.coffee
@@ -69,7 +69,7 @@

   if content[0...3] is '---'
     # "Front Matter"
-    result = content.match /^-{3,}\s([\s\S]*?)-{3,}(\s[\s\S]*|\s?[^\s\S]*)$/
+    result = content.match /^-{3,}\s([\s\S]*?)-{3,}(\s[\s\S]*|\s?)$/
     if result?.length is 3
       metadata = result[1]
       markdown = result[2]


fixed bug in simaple API implementation

fixed bug in simaple API implementation

--- examples/example-api.coffee
+++ examples/example-api.coffee
@@ -56,7 +56,7 @@
   getTask: (taskId) ->
     for t in @tasks
       if t.id is taskId
-        return task
+        return t
     return null
   addTask: (taskId, task) ->
     unless task.done then task.done = no


check sha1 hash value only if present

check sha1 hash value only if present

--- app/coffee/RedisManager.coffee
+++ app/coffee/RedisManager.coffee
@@ -67,7 +67,9 @@
 		multi.exec (error, [docLines, version, storedHash, doc_project_id, ranges])->
 			timer.done()
 			return callback(error) if error?
-			if docLines?
+
+			# check sha1 hash value if present
+			if docLines? and storedHash?
 				computedHash = RedisManager._computeHash(docLines)
 				if computedHash isnt storedHash
 					logger.error project_id: project_id, doc_id: doc_id, doc_project_id: doc_project_id, computedHash: computedHash, storedHash: storedHash, "hash mismatch on retrieved document"


Add in missed error catching

Add in missed error catching

--- app/coffee/RedisManager.coffee
+++ app/coffee/RedisManager.coffee
@@ -81,6 +81,7 @@
 		multi.lrange keys.pendingUpdates(doc_id:doc_id), 0 , -1
 		multi.del keys.pendingUpdates(doc_id:doc_id)
 		multi.exec (error, replys) ->
+			return callback(error) if error?
 			jsonUpdates = replys[0]
 			updates = []
 			for jsonUpdate in jsonUpdates


Revert "Behaviors support for Yandex.Maps"

--- vendor/assets/javascripts/gmaps4rails/yandex/objects/map.coffee
+++ vendor/assets/javascripts/gmaps4rails/yandex/objects/map.coffee
@@ -19,10 +19,9 @@
     @options  = @mergeObjects map_options, defaultOptions

     yandexOptions =
-      center:    @createLatLng(@options.center_latitude, @options.center_longitude)
-      zoom:      @options.zoom
-      behaviors: @options.behaviors
-      type:      @options.type
+      center: @createLatLng(@options.center_latitude, @options.center_longitude)
+      zoom:   @options.zoom
+      type:   @options.type

     mergedYandexOptions = @mergeObjects map_options.raw, yandexOptions


fix onMenuCreated $container argument

fix onMenuCreated $container argument

--- src/scripts/editor/Editor.coffee
+++ src/scripts/editor/Editor.coffee
@@ -35,7 +35,7 @@
       @selectionManager = new SelectionManager(@tweenTime)
       @timeline = new Timeline(@tweenTime, @selectionManager)
       @menu = new EditorMenu(@tweenTime, @$timeline)
-      if options.onMenuCreated? then options.onMenuCreated(@$timeline.find('.editor__menu'))
+      if options.onMenuCreated? then options.onMenuCreated(@$timeline.find('.timeline__menu'))

       @propertiesEditor = new PropertiesEditor(@timeline, @timer, @selectionManager)
       @propertiesEditor.keyAdded.add(@onKeyAdded)


hubot should have emote functionality asap, so switching to that command

--- src/scripts/hubot-factoid.coffee
+++ src/scripts/hubot-factoid.coffee
@@ -22,14 +22,6 @@
 util = require 'util'

 module.exports = (robot) ->
-  if robot.adapter.bot?
-    robot.adapter.action = (envelope, strings...) ->
-      target = @_getTargetFromEnvelope envelope
-      unless target
-        return console.log "ERROR: Not sure who to send to. envelope=", envelope
-      for str in strings
-        @bot.action target, str
-
   class Factoid
     constructor: (name, data) ->
       @name = name
@@ -90,8 +82,8 @@
       if tidbit.verb == "<reply>"
         msg.send output
       else if tidbit.verb == "<action>"
-        if msg.action?
-          msg.action output
+        if msg.emote?
+          msg.emote output
         else
           msg.send "/me " + output
       else if tidbit.verb == "is" && factoid.name.toLowerCase() == robot.name.toLowerCase()


Revert obscured passcode feature

Revert obscured passcode feature

--- app/assets/javascripts/components/overview/details.cjsx
+++ app/assets/javascripts/components/overview/details.cjsx
@@ -48,11 +48,11 @@
         <fieldset>
           <TextInput
             onChange={@updateDetails}
-            value={display_code}
+            value={@props.course.passcode}
             value_key='passcode'
             editable={@props.editable}
             type='text'
-            label={I18n.t('courses.passcode')}
+            label='Passcode'
             placeholder='Not set'
             required=true
           />


Properly bind to papers tasks' assignees on edit screen.

--- app/assets/javascripts/controllers/paper_edit.js.coffee
+++ app/assets/javascripts/controllers/paper_edit.js.coffee
@@ -14,7 +14,7 @@
   assignedTasks: (->
     assignedTasks = @get('allTasks').filterBy 'assignee', @get('controllers.application.currentUser')
     _.difference assignedTasks, @get('authorTasks')
-  ).property('allTasks.@each')
+  ).property('allTasks.@each.assignee')

   reviewerTasks: Ember.computed.filterBy('allTasks', 'role', 'reviewer')


add p after pre in wysiwyg after focus

add p after pre in wysiwyg after focus

--- app/modules/components/wysiwyg/wysiwyg.directive.coffee
+++ app/modules/components/wysiwyg/wysiwyg.directive.coffee
@@ -76,6 +76,8 @@
         refreshCodeBlocks(elm)

     refreshCodeBlocks = (mediumInstance) ->
+        return if !mediumInstance
+
         # clean empty <p> content editable adds it when range.extractContents has been execute it
         for mainChildren in mediumInstance.elements[0].children
             if mainChildren && mainChildren.tagName.toLowerCase() == 'p' && !mainChildren.innerHTML.trim().length
@@ -493,6 +495,7 @@
                 $scope.$applyAsync () ->
                     if !$scope.editMode
                         setEditMode(true)
+                        refreshCodeBlocks(mediumInstance)

             mediumInstance.subscribe 'editableDrop', (event) ->
                 $scope.onUploadFile({files: event.dataTransfer.files, cb: uploadEnd})


login: handle error condition first in `JUser#convert` callback [#77218792]

--- client/Login/AppView.coffee
+++ client/Login/AppView.coffee
@@ -340,25 +340,26 @@
     # that'll leak a guest account.
     KD.getSingleton('groupsController').groupChannel?.close()

-    KD.remote.api.JUser.convert formData, (err, {account, newToken})=>
-
-      account ?= KD.whoami()
+    KD.remote.api.JUser.convert formData, (err, user) =>
+
       @registerForm.button.hideLoader()

       if err
-
         {message} = err
         warn "An error occured while registering:", err
         @registerForm.notificationsDisabled = no
         @registerForm.emit "SubmitFailed", message
       else

+        {account, newToken} = user
+        account  ?= KD.whoami()
+
         KD.mixpanel.alias account.profile.nickname
         KD.mixpanel "Signup, success"

         Cookies.set 'newRegister', yes
         KD.getSingleton("mainController").swapAccount {
-          account, replacementToken:newToken
+          account, replacementToken: newToken
         }

         titleText = unless err then 'You\'re good to go, Enjoy!' \


Remove "checkPaneInTabView" and move its logic to inline.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -1304,7 +1304,8 @@

       if p.hash is context.paneHash

-        unless @checkPaneInTabView originTabView, context.paneHash
+        check = originTabView.panes.filter (p) -> p.hash is context.paneHash
+        if not originTabView.panes.length or not check.length
           originTabView = p.parent.parent # Reach the `IDEApplicationTabView`

         originTabView.activePane = null
@@ -1334,22 +1335,6 @@

     @setActiveTabView tabView
     callback()
-
-
-  ###*
-   * `pane` there in `tabView` really?
-   *
-   * @param {IDEApplicationTabView} tabView
-   * @param {KDTabPaneView} hash
-   * @return {boolean}
-  ###
-  checkPaneInTabView: (tabView, hash) ->
-
-    return  unless tabView.panes.length
-
-    check = tabView.panes.filter (p) -> p.hash is hash
-
-    return check.length > 0

   getPaneByChange: (change) ->


Emulate intuition. For text selection on multiple columns at least.

--- client/lib/annotator.coffee
+++ client/lib/annotator.coffee
@@ -95,66 +95,163 @@
     #@_showSegments()
     #@_showTextSegments()

-  _distance: (position, area) =>
-    distanceXLeft = position.left - area.left
-    distanceXRight = position.left - (area.left + area.width)
-
-    distanceYTop = position.top - area.top
-    distanceYBottom = position.top - (area.top + area.height)
-
-    distanceX = if Math.abs(distanceXLeft) < Math.abs(distanceXRight) then distanceXLeft else distanceXRight
+  _distanceX: (position, area) =>
+    return Number.POSITIVE_INFINITY unless area
+
+    distanceXLeft = Math.abs(position.left - area.left)
+    distanceXRight = Math.abs(position.left - (area.left + area.width))
+
     if position.left > area.left and position.left < area.left + area.width
       distanceX = 0
-
-    distanceY = if Math.abs(distanceYTop) < Math.abs(distanceYBottom) then distanceYTop else distanceYBottom
+    else
+      distanceX = Math.min(distanceXLeft, distanceXRight)
+
+    distanceX
+
+  _distanceY: (position, area) =>
+    return Number.POSITIVE_INFINITY unless area
+
+    distanceYTop = Math.abs(position.top - area.top)
+    distanceYBottom = Math.abs(position.top - (area.top + area.height))
+
     if position.top > area.top and position.top < area.top + area.height
       distanceY = 0
-
-    distanceX * distanceX + distanceY * distanceY
-
-  _findClosestSegment: (position) =>
+    else
+      distanceY = Math.min(distanceYTop, distanceYBottom)
+
+    distanceY
+
+  _distance: (position, area) =>
+    return Number.POSITIVE_INFINITY unless area
+
+    distanceX = @_distanceX position, area
+    distanceY = @_distanceY position, area
+
+    Math.sqrt(distanceX * distanceX + distanceY * distanceY)
+
+  _eventToPosition: (e) =>
+    $canvas = @$displayPage.find('canvas')
+
+    offset = $canvas.offset()
+
+    left: e.pageX - offset.left
+    top: e.pageY - offset.top
+
+  # Finds a text layer segment which is it to the left and up of the given position
+  # and has highest index. Highest index means it is latest in the text flow of the
+  # page. So we are searching for for latest text layer segment in text flow on the
+  # page before the given position. Left and up is what is intuitively right for
+  # text which flows left to right, top to bottom.
+  _findLastLeftUpTextSegment: (position) =>
+    segmentIndex = -1
+    for segment, index in @textSegments
+      segmentIndex = index if segment.boundingBox.left <= position.left and segment.boundingBox.top <= position.top and index > segmentIndex
+
+    segmentIndex
+
+  # Simple search for closest text layer segment by euclidean distance
+  _findClosestTextSegment: (position) =>
     closestSegmentIndex = -1
-    closestDistance = Number.MAX_VALUE
-
-    for segment, i in @textSegments
+    closestDistance = Number.POSITIVE_INFINITY
+
+    for segment, index in @textSegments
       distance = @_distance position, segment.boundingBox
       if distance < closestDistance
-        closestSegmentIndex = i
+        closestSegmentIndex = index
         closestDistance = distance

-    [closestSegmentIndex, Math.sqrt(closestDistance)]
-
-  _findClosestSegmentFromEvent: (e) =>
-    $canvas = @$displayPage.find('canvas')
-
-    offset = $canvas.offset()
-    left = e.pageX - offset.left
-    top = e.pageY - offset.top
-    @_findClosestSegment
-      left: left
-      top: top
-
-  padTextSegment: (e) =>
-    [closestSegmentIndex, closestDistance] = @_findClosestSegmentFromEvent e
-    $closestSegmentDom = @textSegments[closestSegmentIndex].$domElement
-    angle = @textSegments[closestSegmentIndex].angle
-    scale = @textSegments[closestSegmentIndex].scale
-
-    padding = closestDistance + 10
-
-    # 2D vector rotation: http://www.siggraph.org/education/materials/HyperGraph/modeling/mod_tran/2drota.htm
-    # x' = x cos(f) - y sin(f), y' = x sin(f) + y cos(f)
-    # We scale x because we use scaling transformation along x-axis as well
-    left = padding * (scale * Math.cos(angle) - Math.sin(angle))
-    top = padding * (scale * Math.sin(angle) + Math.cos(angle))
+    closestSegmentIndex
+
+  # Pads a text layer segment (identified by index) so that its padding comes
+  # under the position of the mouse. This makes text selection in browsers
+  # behave like mouse is still over the text layer segment DOM element, even
+  # when mouse is moved from it, for example, when dragging selection over empty
+  # space in pages where there are no text layer segments.
+  _padTextSegment: (position, index) =>
+    # If there are no text layer segments on the page, index will be -1.
+    # Then we just set padding and margin to 0, to be sure.
+    if index isnt -1
+      segment = @textSegments[index]
+      distance = @_distance position, segment.boundingBox
+      $dom = segment.$domElement
+
+      # Text layer segments can be rotated and scalled along x-axis
+      angle = segment.angle
+      scale = segment.scale
+
+      # Padding is scaled later on, so we apply scaling inversely here so that it is
+      # exact after scalling later on. Without that when scaling is < 1, when user moves
+      # far away from the text segment, padding falls behind and does not reach mouse
+      # position anymore.
+      # Additionally, we add few pixels so that user can move mouse fast and still stay in.
+      padding = distance / scale + 20
+
+      # Padding (and text) rotation transformation is done through CSS and
+      # we have to match it for margin, so we compute here margin under rotation.
+      # 2D vector rotation: http://www.siggraph.org/education/materials/HyperGraph/modeling/mod_tran/2drota.htm
+      # x' = x cos(f) - y sin(f), y' = x sin(f) + y cos(f)
+      # Additionally, we use CSS scaling transformation along x-axis on padding
+      # (and text), so we have to scale margin as well.
+      left = padding * (scale * Math.cos(angle) - Math.sin(angle))
+      top = padding * (scale * Math.sin(angle) + Math.cos(angle))

     @$displayPage.find('.text-layer-segment').css
       padding: 0
       margin: 0
-    $closestSegmentDom.css
-      marginLeft: -left
-      marginTop: -top
-      padding: padding
+
+    if index isnt -1
+      # To make life easy, we apply padding all around the text segment DOM element,
+      # but then we have to counteract text content position change by set negative
+      # margin. With this, text content stays in place, but DOM element gets a
+      # necessary padding.
+      $dom.css
+        marginLeft: -left
+        marginTop: -top
+        padding: padding
+
+  padTextSegments: (e) =>
+    position = @_eventToPosition e
+
+    # Find latest text layer segment in text flow on the page before the given position
+    segmentIndex = @_findLastLeftUpTextSegment position
+
+    # segmentIndex might be -1, but @_distanceY returns
+    # infinity in this case, so things work out
+    if @_distanceY(position, @textSegments[segmentIndex]?.boundingBox) is 0
+      # A clear case, we are directly over a segment y-wise. This means that
+      # we are really directly over a segment, or that segment is to the right
+      # of mouse position (because we searched for all segments to the left and
+      # up of the position). In any case, this is the segment we want to pad.
+      @_padTextSegment position, segmentIndex
+      return
+
+    # So we are close to the segment we want to pad, but we might currently have
+    # a segment which is in the middle of the text line above our position, so we
+    # search for the last text segment in that line, before it goes to the next
+    # (our, where our position is) line.
+    # On the other hand, segmentIndex might be -1 because we are on the left border
+    # of the page and there are no text segments to the left and up. So we as well
+    # do a search from the beginning of the page to the last text segment on the
+    # text line just above our position.
+    while @textSegments[segmentIndex + 1]
+      if @textSegments[segmentIndex + 1].boundingBox.top + @textSegments[segmentIndex + 1].boundingBox.height > position.top
+        break
+      else
+        segmentIndex++
+
+    # segmentIndex can still be -1 if there are no text segments before
+    # the mouse position, so let's simply find closest segment and pad that.
+    # Not necessary for Chrome. There you can start selecting without being
+    # over any text segment and it will correctly start when you move over
+    # one. But in Firefox you have to start selecting over a text segment
+    # (or padded text segment) to work correctly later on.
+    segmentIndex = @_findClosestTextSegment position if segmentIndex is -1
+
+    # segmentIndex can still be -1 if there are no text segments on
+    # the page at all, but we leave to @_padTextSegment to handle that
+    @_padTextSegment position, segmentIndex
+
+    return # To prevent CoffeScript returning something

   render: =>
     assert @highlightsEnabled
@@ -167,14 +264,16 @@

     $textLayerDummy.hide()

-    divs = for segment in @textSegments
-      segment.$domElement = $('<div/>').addClass('text-layer-segment').css(segment.style).text(segment.text)
+    divs = for segment, index in @textSegments
+      segment.$domElement = $('<div/>').addClass('text-layer-segment').css(segment.style).text(segment.text).data
+        pageNumber: @pageNumber
+        index: index

     # There is no use rendering so many divs to make browser useless
     # TODO: Report this to the server? Or should we simply discover such PDFs already on the server when processing them?
     @$displayPage.find('.text-layer').append divs if divs.length <= MAX_TEXT_LAYER_SEGMENTS_TO_RENDER

-    @$displayPage.on 'mousemove.annotator', @padTextSegment
+    @$displayPage.on 'mousemove.annotator', @padTextSegments

     @rendering = false

@@ -197,11 +296,22 @@
 class @Annotator
   constructor: ->
     @_pages = []
+    @mouseDown = false

     $(window).on 'scroll.annotator', @checkRender
     $(window).on 'resize.annotator', @checkRender

+    $(document).on 'mousedown.annotator', =>
+      @mouseDown = true
+      return # To prevent CoffeScript returning something
+    $(document).on 'mouseup.annotator', =>
+      @mouseDown = false
+      return # To prevent CoffeScript returning something
+
   destroy: =>
+    $(document).off 'mousedown.annotator'
+    $(document).off 'mouseup.annotator'
+
     $(window).off 'scroll.annotator'
     $(window).off 'resize.annotator'

@@ -251,3 +361,5 @@

     page.render() for page in pagesToRender
     page.remove() for page in pagesToRemove
+
+    return # To prevent CoffeScript returning something


Standardize comment about preventing arbitrary returns

--- client/lib/annotator.coffee
+++ client/lib/annotator.coffee
@@ -269,7 +269,7 @@
     # the page at all, then we do not have aynthing to do
     @_padTextSegment position, segmentIndex if segmentIndex isnt -1

-    return # To prevent CoffeScript returning something
+    return # Make sure CoffeeScript does not return anything

   render: =>
     assert @highlightsEnabled
@@ -321,10 +321,10 @@

     $(document).on 'mousedown.annotator', =>
       @mouseDown = true
-      return # To prevent CoffeScript returning something
+      return # Make sure CoffeeScript does not return anything
     $(document).on 'mouseup.annotator', =>
       @mouseDown = false
-      return # To prevent CoffeScript returning something
+      return # Make sure CoffeeScript does not return anything

   destroy: =>
     $(document).off 'mousedown.annotator'
@@ -380,4 +380,4 @@
     page.render() for page in pagesToRender
     page.remove() for page in pagesToRemove

-    return # To prevent CoffeScript returning something
+    return # Make sure CoffeeScript does not return anything


test: rm gulpfile

test: rm gulpfile

--- client/test/gulpfile.coffee
+++ client/test/gulpfile.coffee
@@ -1,8 +0,0 @@
-gulp   = require 'gulp'
-coffee = require 'gulp-coffee'
-
-
-gulp.task 'default', ->
-  gulp.src './src/**/*.coffee'
-  .pipe coffee()
-  .pipe gulp.dest './output/'


User: remove stupid lines

User: remove stupid lines

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -839,16 +839,6 @@
         queue.next()

       ->
-        # if username starts with ktu mark it as exempt this is a temp fix for
-        # load test, we dont want test user's posts to be seen by others
-        if username.indexOf("ktu-") is 0
-          account.markUserAsExemptUnsafe client, yes, (err, res)->
-            return callback err if err?
-            queue.next()
-        else
-          queue.next()
-      ->
-
         callback error, {account, recoveryToken, newToken}
         queue.next()
     ]


backend: registration cutoff date updated

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -501,7 +501,7 @@
         opts =
           groupName: groupName
           account: account
-          cutoffDate: new Date 2016, 2, 1 # 1 March 2016
+          cutoffDate: new Date 2016, 2, 11 # 11 March 2016
           env: KONFIG.environment

         return next() if isSoloAccessible opts


Static: separate loggedout page preps WIP

--- workers/social/lib/social/render/styleblock.coffee
+++ workers/social/lib/social/render/styleblock.coffee
@@ -1,5 +1,8 @@
 module.exports = (options={})->
+
   options.logo or= "/a/images/favicon.ico"
+  {loggedOut}    = options
+
   """
   <meta charset="utf-8"/>
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
@@ -9,6 +12,6 @@
   <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1" />
   <link rel="shortcut icon" href="#{options.logo}" />
   <link rel="fluid-icon" href="/a/images/logos/fluid512.png" title="Koding" />
-  <link rel="stylesheet" href="/a/css/kd.css?#{KONFIG.version}" />
-  <link rel="stylesheet" href="/a/css/koding.css?#{KONFIG.version}" />
+  <link rel="stylesheet" href="/a/#{if loggedOut then 'out/' else ''}css/kd.css?#{KONFIG.version}" />
+  <link rel="stylesheet" href="/a/#{if loggedOut then 'out/' else ''}css/koding.css?#{KONFIG.version}" />
   """


mixpanel: track webterm bug fixes

mixpanel: track webterm bug fixes

--- client/app/Applications/WebTerm.kdapplication/AppController.coffee
+++ client/app/Applications/WebTerm.kdapplication/AppController.coffee
@@ -8,7 +8,6 @@
     route        :
       slug       : "/:name?/Develop/Terminal"
       handler    : ({params:{name}, query})->
-        KD.mixpanel "Click open Webterm", name
         KD.utils.wait 800, ->
           router = KD.getSingleton 'router'
           warn "webterm handling itself", name, query, arguments
@@ -23,6 +22,7 @@
     behavior     : "application"
     preCondition :
       condition  : (options, cb)->
+        KD.mixpanel "Click open Webterm", name

         {vmName} = options
         vmController = KD.getSingleton 'vmController'
@@ -32,9 +32,8 @@
           vmController.info vmName, KD.utils.getTimedOutCallback (err, vm, info)->
             cb  info?.state is 'RUNNING', {vmName, info}
           , ->
-            cb yes
+            cb no
             KD.logToExternal "failed to fetch vminfo, couldn't open terminal"
-            KD.mixpanel "Can't open Webterm", {vmName}
           , 2500
       failure     : (options, cb)->
         {vmName} = options


WebTerm: Minor changes for code quality and performance.

--- client/app/Applications/WebTerm.kdapplication/AppController.coffee
+++ client/app/Applications/WebTerm.kdapplication/AppController.coffee
@@ -42,8 +42,12 @@
         , cb

   constructor:(options = {}, data)->
-    vmName          = options.params?.vmName or (KD.getSingleton 'vmController').defaultVmName
-    options.view    = new WebTermAppView {vmName, joinUser: options.params?.joinUser, session: options.params?.session}
+    params          = options.params or {}
+    vmName          = params.vmName  or KD.getSingleton("vmController").defaultVmName
+    options.view    = new WebTermAppView
+      vmName        : vmName
+      joinUser      : params.joinUser
+      session       : params.session
     options.appInfo =
       title         : "Terminal on #{vmName}"
       cssClass      : "webterm"


Sitewide: @getSingleton -> KD.singleton

Sitewide: @getSingleton -> KD.singleton

--- client/ide/filetree/controllers/findercontextmenucontroller.coffee
+++ client/ide/filetree/controllers/findercontextmenucontroller.coffee
@@ -389,7 +389,7 @@
     fileExtension    = FSItem.getFileExtension path

     # FIXME: Add this ability later ~ GG
-    # appsController   = @getSingleton "kodingAppsController"
+    # appsController   = KD.singleton "kodingAppsController"
     # {extensionToApp} = appsController
     # possibleApps     = (extensionToApp[fileExtension] or extensionToApp.txt) or []
     # for appName in possibleApps
@@ -410,7 +410,7 @@
   #   {path}           = fileView.getData()
   #   plainPath        = FSHelper.plainPath path
   #   fileExtension    = FSItem.getFileExtension path
-  #   appsController   = @getSingleton "kodingAppsController"
+  #   appsController   = KD.singleton "kodingAppsController"
   #   {extensionToApp} = appsController
   #   possibleApps     = (extensionToApp[fileExtension] or extensionToApp.txt) or []
   #   for appName in possibleApps


[Cartilage.Views.SplitView] Fixed an issue where setting the second view would append its element to the first view's element.

--- app/assets/javascripts/cartilage/views/split_view.js.coffee
+++ app/assets/javascripts/cartilage/views/split_view.js.coffee
@@ -141,7 +141,7 @@
   setSecondView: (view) ->
     @secondView.removeFromSuperview() if @secondView
     @secondView = view
-    @addSubview view, @firstElement
+    @addSubview view, @secondElement

   onMouseDown: (event) =>
     return unless @isResizable


sort usernames

sort usernames

--- resources/assets/coffee/react/beatmap-discussions/header.coffee
+++ resources/assets/coffee/react/beatmap-discussions/header.coffee
@@ -106,7 +106,7 @@
             className: "#{bn}__filter-group #{bn}__filter-group--stats"
             el BeatmapDiscussions.UserFilter,
               selectedUser: if @props.selectedUserId? then @props.users[@props.selectedUserId] else null
-              users: _.pickBy(@props.users, (value) -> value.id?)
+              users: _.sortBy(_.pickBy(@props.users, (value) -> value.id?), 'username')

             div
               className: "#{bn}__stats"


fitBounds function for Yandex Map

fitBounds function for Yandex Map

--- vendor/assets/javascripts/gmaps4rails/yandex/objects/map.coffee
+++ vendor/assets/javascripts/gmaps4rails/yandex/objects/map.coffee
@@ -39,7 +39,7 @@
   extendBound: (bound)->

   fitBounds: ->
-    @serviceObject.setBounds(@boundsObject)
+    @serviceObject.setBounds(@boundsObject) if @boundsObject?

   adaptToBounds: ->
     @fitBounds()


Review functionality

Review functionality

--- app/assets/javascripts/crowdblog/views/posts/post_view.js.coffee
+++ app/assets/javascripts/crowdblog/views/posts/post_view.js.coffee
@@ -5,6 +5,7 @@
     events:
       'click .delete' : 'deletePost'
       'click .publish' : 'togglePublish'
+      'click .review' : 'toggleReview'

     template:
       JST["crowdblog/templates/posts/post"]
@@ -33,8 +34,24 @@

       this.paintButtons()

+    toggleReview: (e) ->
+      e.preventDefault()
+      if this.model.get('ready_for_review')
+        this.model.save
+          ready_for_review: false
+        this.model.set 'ready_for_review', false
+      else
+        this.model.save
+          ready_for_review: true
+        this.model.set 'ready_for_review', true
+
+      this.paintButtons()
+
     paintButtons: ->
       this.$el.find('.publish').removeClass('btn-success btn-danger')
       this.$el.find('.publish').addClass('btn-success') if this.model.get('published?')
       this.$el.find('.publish').addClass('btn-danger') unless this.model.get('published?')

+      this.$el.find('.review').removeClass('btn-warning')
+      this.$el.find('.review').addClass('btn-warning') if this.model.get 'ready_for_review'
+


make Backbone.syncWithMultipart return a deferred object

…to keep consistent with Backbone.sync's API

Change-Id: I212cd9ce0fbf1864efd22e7750d201a2e73b1cdb
Reviewed-on: https://gerrit.instructure.com/12651
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Ryan Shaw <ea3cd978650417470535f3a4725b6b5042a6ab59@instructure.com>

--- app/coffeescripts/backbone-ext/Backbone.syncWithMultipart.coffee
+++ app/coffeescripts/backbone-ext/Backbone.syncWithMultipart.coffee
@@ -1,13 +1,15 @@
 define [
   'use!vendor/backbone'
   'underscore'
-], (Backbone, _) ->
+  'jquery'
+], (Backbone, _, $) ->

   Backbone.syncWithoutMultipart = Backbone.sync
   Backbone.syncWithMultipart = (method, model, options) ->
     # Create a hidden iframe
-    iframeId = 'file_upload_iframe_' + (new Date()).getTime()
+    iframeId = _.uniqueId 'file_upload_iframe_'
     $iframe = $("<iframe id='#{iframeId}' name='#{iframeId}' ></iframe>").hide()
+    dfd = new $.Deferred()

     # Create a hidden form
     httpMethod = {create: 'POST', update: 'PUT', delete: 'DELETE', read: 'GET'}[method]
@@ -18,9 +20,9 @@
           $orig = $(attr)
           $orig.after($orig.clone(true))
           attr
-        else if not _.isEmpty(attr) and (_.isArray(attr) or typeof attr is 'object')
+        else if !_.isEmpty(attr) and (_.isArray(attr) or typeof attr is 'object')
           toForm(attr, key)
-        else if not "#{key}".match(/^_/) and attr? and typeof attr isnt 'object' and typeof attr isnt 'function'
+        else if !"#{key}".match(/^_/) and attr? and typeof attr isnt 'object' and typeof attr isnt 'function'
           $el = $ "<input/>",
             name: key
             value: attr
@@ -32,7 +34,9 @@
         <input type='hidden' name='authenticity_token' value='#{ENV.AUTHENTICITY_TOKEN}' />
       </form>
     """).hide()
-    $form.prepend(el for el in toForm(model) when el)
+
+    _.each toForm(model.attributes), (el) ->
+      $form.prepend(el) if el

     $(document.body).prepend($iframe, $form)

@@ -46,8 +50,10 @@

       if iframeBody.className is "error"
         options.error?(response)
+        dfd.reject(response)
       else
         options.success?(response)
+        dfd.resolve(response)

       $iframe.remove()
       $form.remove()
@@ -60,6 +66,7 @@
     $iframe[0].onload = callback

     $form[0].submit()
+    dfd

   Backbone.sync = (method, model, options) ->
     if options?.multipart


Add course edits stats properties to user schema

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -284,6 +284,9 @@
     pollMiscPatches: c.int()
     campaignTranslationPatches: c.int()
     campaignMiscPatches: c.int()
+    courseTranslationPatches: c.int()
+    courseMiscPatches: c.int()
+    courseEdits: c.int()
     concepts: {type: 'object', additionalProperties: c.int(), description: 'Number of levels completed using each programming concept.'}

   earned: c.RewardSchema 'earned by achievements'


Update user schema (to avoid cherry picking)

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -107,7 +107,46 @@
         name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "GitHub"', format: 'link-name'}
         link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'}
     photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'}
-
+    curated: c.object {title: 'Curated', required: ['mainTag','location','education','workHistory','phoneScreenFilter','schoolFilter','locationFilter','roleFilter','seniorityFilter']},
+      mainTag:
+        title: 'Main tag'
+        description: 'A main tag to describe this candidate'
+        type: 'string'
+      location:
+        title: 'Location'
+        description: "The CURRENT location of the candidate"
+        type: 'string'
+      education:
+        title: 'Education'
+        description: 'The main educational institution of the candidate'
+        type: 'string'
+      workHistory: c.array
+        title: 'Work history'
+        description: 'One or two places the candidate has worked'
+        type: 'array'
+      ,
+        title: 'Workplace'
+        type: 'string'
+      phoneScreenFilter:
+        title: 'Phone screened'
+        type: 'boolean'
+        description: 'Whether the candidate has been phone screened.'
+      schoolFilter:
+        title: 'School'
+        type: 'string'
+        enum: ['Top 20 Eng.', 'Other US', 'Other Intl.']
+      locationFilter:
+        title: 'Location'
+        type: 'string'
+        enum: ['Bay Area', 'New York', 'Other US', 'International']
+      roleFilter:
+        title: 'Role'
+        type: 'string'
+        enum: ['Web Developer', 'Software Developer', 'iOS Developer', 'Android Developer', 'Project Manager']
+      seniorityFilter:
+        title: 'Seniority'
+        type: 'string'
+        enum: ['College Student', 'Recent Grad', 'Junior', 'Senior', 'Management']
   jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'}
   jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''}
   employerAt: c.shortString {description: "If given employer permissions to view job candidates, for which employer?"}


print received strings to console

print received strings to console

--- app/scripts/robot/radio.coffee
+++ app/scripts/robot/radio.coffee
@@ -45,6 +45,9 @@
         if error
           console.log('failed to open: ' + error)
         _ndl3Radio.connectXBee(radioAddr, serialPort)
+        _ndl3Radio.on('string', (str) ->
+          console.log('got string', str)
+          )
         )

     return {
@@ -86,6 +89,7 @@
               _ndl3Radio.send(channel: object)
           else
             console.log "_ndl3Radio not defined"
+            return false

           console.log "radio channel 'chname': fake sent object"
           .replace(/object/, String(object))


Don't make a modeling result parent property.

--- src/session/modeling.data.coffee
+++ src/session/modeling.data.coffee
@@ -165,19 +165,16 @@
 ###
 ModelingResult =
   ###*
-   * Adds the modeling result parent modeling object
-   * reference property and extends the parameter result
-   * objects as described in extendParameterResult.
+   * Extends the parameter result objects as described
+   * in
+   * {{#crossLink "ParameterResult/extend"}}{{/crossLink}}.
    *
    * @method extend
    * @protected
    * @param modelingResult the modeling result object to extend
-   * @param modeling the parent modeling object
-  ###
-  extend: (modelingResult, modeling) ->
+  ###
+  extend: (modelingResult) ->
     return modelingResult unless modelingResult?
-    # Set the modeling result parent reference.
-    modelingResult.modeling = modeling

     # Extend each modeling parameter result object.
     for key, paramResult of modelingResult
@@ -211,7 +208,7 @@
     # Set the modeling parent session reference.
     modeling.session = session
     # Extend the modeling result object.
-    ModelingResult.extend(modeling.result, modeling)
+    ModelingResult.extend(modeling.result)
     # Add the virtual properties.
     Object.defineProperties modeling,
       ###*
@@ -222,7 +219,7 @@
       intensities:
         get: ->
           _.mapValues(@result, 'image.metadata.averageIntensity')
-
+
       ###*
        * @method overlays
        * @return the modeling results which have an overlay,


Add codeplay redirect when logging in or signing up

--- app/views/core/CocoView.coffee
+++ app/views/core/CocoView.coffee
@@ -236,6 +236,14 @@
       return visibleModal.hide() if visibleModal.$el.is(':visible') # close, then this will get called again
       return @modalClosed(visibleModal) # was closed, but modalClosed was not called somehow
     modalView.render()
+
+    # Redirect to the woo when trying to log in or signup
+    if features.codePlay
+      if modalView.id is 'create-account-modal'
+        return document.location.href = '//gs-dev.thewoo.com/register/?cocoId='+me.id
+      if modalView.id is 'auth-modal'
+        return document.location.href = '//gs-dev.thewoo.com/login/?cocoId='+me.id
+
     $('#modal-wrapper').removeClass('hide').empty().append modalView.el
     modalView.afterInsert()
     visibleModal = modalView


always set highlight index on searchString change

--- app/components/eui-popup.coffee
+++ app/components/eui-popup.coffee
@@ -38,7 +38,7 @@
     @$().find('input:first').focus()

   searchStringDidChange: (->
-    @set('highlightedIndex', 0) if @get('filteredOptions')
+    @set('highlightedIndex', 0)
   ).observes 'searchString'

   filteredOptions: (->


Workspace: Added initial content support for EditorPane.

--- client/landing/app/CommonViews/workspace/panes/editorpane.coffee
+++ client/landing/app/CommonViews/workspace/panes/editorpane.coffee
@@ -17,9 +17,12 @@
     , file

   createSingleEditor: ->
-    path = @files or "localfile:/Untitled.txt"
-    file = FSHelper.createFileFromPath path
-    @ace = @createEditorInstance file
+    path      = @files or "localfile:/Untitled.txt"
+    file      = FSHelper.createFileFromPath path
+    @ace      = @createEditorInstance file
+    {content} = @getOptions()
+    @ace.on "ace.ready", =>
+      @ace.editor.setValue content  if content

   createEditorTabs: ->
     @tabHandleContainer = new ApplicationTabHandleHolder


:fire: Remove useless null assignment

:fire: Remove useless null assignment

--- src/text-editor-component.coffee
+++ src/text-editor-component.coffee
@@ -122,8 +122,6 @@
     @verticalScrollbarComponent.destroy()
     @horizontalScrollbarComponent.destroy()

-    @verticalScrollbarComponent = null
-    @horizontalScrollbarComponent = null
     @onVerticalScroll = null
     @onHorizontalScroll = null


Add OverlayManager to the TextEditorComponent

--- src/text-editor-component.coffee
+++ src/text-editor-component.coffee
@@ -11,6 +11,7 @@
 LinesComponent = require './lines-component'
 ScrollbarComponent = require './scrollbar-component'
 ScrollbarCornerComponent = require './scrollbar-corner-component'
+OverlayManager = require './overlay-manager'

 module.exports =
 class TextEditorComponent
@@ -56,8 +57,14 @@
     @domNode = document.createElement('div')
     if @useShadowDOM
       @domNode.classList.add('editor-contents--private')
+
+      insertionPoint = document.createElement('content')
+      insertionPoint.setAttribute('select', 'atom-overlay')
+      @domNode.appendChild(insertionPoint)
+      @overlayManager = new OverlayManager(@presenter, @hostElement)
     else
       @domNode.classList.add('editor-contents')
+      @overlayManager = new OverlayManager(@presenter, @domNode)

     @scrollViewNode = document.createElement('div')
     @scrollViewNode.classList.add('scroll-view')
@@ -139,6 +146,8 @@
     @horizontalScrollbarComponent.updateSync(@newState)
     @verticalScrollbarComponent.updateSync(@newState)
     @scrollbarCornerComponent.updateSync(@newState)
+
+    @overlayManager?.render(@newState)

     if @editor.isAlive()
       @updateParentViewFocusedClassIfNeeded()


presenter: :fire: redundant decoration state updates

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -1134,8 +1134,6 @@
         @shouldUpdateLineNumbersState = true
       else if decoration.isType('gutter')
         @shouldUpdateCustomGutterDecorationState = true
-    if decoration.isType('highlight')
-      @updateHighlightState(decoration)
     if decoration.isType('overlay')
       @shouldUpdateOverlaysState = true

@@ -1145,14 +1143,14 @@
     @observeDecoration(decoration)

     if decoration.isType('line') or decoration.isType('gutter')
-      @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
+      @shouldUpdateDecorations = true
       @shouldUpdateTilesState = true if decoration.isType('line')
       if decoration.isType('line-number')
         @shouldUpdateLineNumbersState = true
       else if decoration.isType('gutter')
         @shouldUpdateCustomGutterDecorationState = true
     else if decoration.isType('highlight')
-      @updateHighlightState(decoration)
+      @shouldUpdateDecorations = true
     else if decoration.isType('overlay')
       @shouldUpdateOverlaysState = true

@@ -1357,7 +1355,6 @@
     @shouldUpdateHiddenInputState = true
     @pauseCursorBlinking()
     @updateCursorState(cursor)
-
     @emitDidUpdateState()

   startBlinkingCursors: ->


Don't stringify new classifications

Don't stringify new classifications

--- app/pages/project/classify.cjsx
+++ app/pages/project/classify.cjsx
@@ -135,7 +135,7 @@
       animatedScrollTo document.body, el.offsetTop - space, 333

   handleCompletion: ->
-    console?.info 'Completed classification', JSON.stringify @state.classification, null, 2
+    console?.info 'Completed classification', @state.classification
     @state.classification.save().then =>
       console?.log 'Saved classification', @state.classification.id


nginx: add sitemap location

nginx: add sitemap location

--- deployment/nginx.coffee
+++ deployment/nginx.coffee
@@ -292,6 +292,16 @@
         #{if environment is "sandbox" then basicAuth else ""}
       }

+      # TODO after custom location support PR is merged, remove this from here
+      location ~ /sitemap(.*).xml {
+        proxy_pass            http://socialapi/sitemap$1.xml;
+        proxy_set_header      X-Real-IP       $remote_addr;
+        proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_next_upstream   error timeout   invalid_header http_500;
+        proxy_connect_timeout 1;
+
+      }
+
       # special case for kontrol to support additional paths, like /kontrol/heartbeat
       location ~^/kontrol/(.*) {
         proxy_pass            http://kontrol/$1$is_args$args;


added test for branch install

added test for branch install

--- test/applications_tests.coffee
+++ test/applications_tests.coffee
@@ -91,6 +91,8 @@

 describe "Application installation", ->

+    fakeHaibuLastBody = {}
+
     before (done) ->
         server.listen(8888)
         clearDb ->
@@ -100,7 +102,8 @@
                     done()

     before ->
-        @haibu = fakeServer { drone: { port: 8001 } }, 200, (body) ->
+        @haibu = fakeServer { drone: { port: 8001 } }, 200, (body) =>
+            fakeHaibuLastBody = body
             should.exist body.start.user
             should.exist body.start.name
             should.exist body.start.repository
@@ -160,6 +163,37 @@
             @body.rows.length.should.equal 2
             @body.rows[0].name.should.equal "My App"

+    describe "Install a new application with specific branch", ->
+        it "When I send a request to install an application on branch mybranch", \
+                     (done) ->
+            data =
+                name: "My App 2",
+                description: "description",
+                git: "git@github.com:mycozycloud/my-app2.git"
+                branch:"mybranch"
+            client.post "api/applications/install", \
+                        data, (error, response, body) =>
+                @response = response
+                @body = body
+                done()
+
+        it "Then it sends me back my app with correct branch", ->
+            @response.statusCode.should.equal 201
+            should.exist @body.success
+            should.exist @body.app.branch
+            @body.app.branch.should.equal "mybranch"
+            fakeHaibuLastBody.start.repository.branch.should.equal "mybranch"
+            @body.app.port.should.equal 8001
+
+        it "When I send a request to retrieve all applications", (done) ->
+            client.get "api/applications", (error, response, body) =>
+                @body = body
+                done()
+
+        it "The branch is still there", ->
+            @body.rows.length.should.equal 3
+            @body.rows[1].branch.should.equal "mybranch"
+

 describe "Application update", ->

@@ -290,7 +324,7 @@
                 done()

         it "Then I do not see my application in the list", ->
-            @body.rows.length.should.equal 1
+            @body.rows.length.should.equal 2
             @body.rows[0].name.should.not.equal "My App"


added dot conversion warning

added dot conversion warning

--- device-generator.coffee
+++ device-generator.coffee
@@ -5,14 +5,14 @@
 class DeviceGenerator
   toMessageSchema: (filePath, callback=->) =>
       console.error 'YOU NEED TO CHANGE THE DEVICE EDITOR TO SHOW A SUBSET OF THE SCHEMA FOR REQUIRED, PAYLOAD ISSUES'
+      console.error 'DOT CONVERSION FOR GOOGLE'
       swaggerFile = require filePath
       swaggerTransformer = new Swagger2ToMessageSchema swaggerFile
       swaggerTransformer.init (error) =>
         return callback error if error?
         callback null, swaggerTransformer.transform()

-  toForm: (filePath, callback=->) =>
-    console.error 'YOU NEED TO CHANGE THE DEVICE EDITOR TO SHOW A SUBSET OF THE SCHEMA FOR REQUIRED, PAYLOAD ISSUES'
+  toForm: (filePath, callback=->) =>
     @toMessageSchema filePath, (error, messageSchema) =>
       return callback error if error?
       messageSchemaTransformer = new MessageSchemaToForm


fix(sentry-2518): Better debugging information for when token is not found

--- internal_packages/preferences/lib/tabs/preferences-accounts.cjsx
+++ internal_packages/preferences/lib/tabs/preferences-accounts.cjsx
@@ -107,9 +107,13 @@
     return [] unless @props.config

     tokens = @props.config.get('tokens') || []
-    token = _.find tokens, (token) ->
-      token.provider is 'nylas' and token.identifier is account.emailAddress
+    token = _.find tokens, (t) ->
+      t.provider is 'nylas' and t.identifier is account.emailAddress
     tokens = _.without(tokens, token)
+
+    if not token
+      console.warn("Could not find nylas token for email address #{account.emailAddress}")
+      return

     DatabaseStore.unpersistModel(account).then =>
       # TODO: Delete other mail data


Fix request for id in cancel question bug

--- app/assets/javascripts/angular/controllers/options_ctrl.js.coffee
+++ app/assets/javascripts/angular/controllers/options_ctrl.js.coffee
@@ -38,8 +38,7 @@
   )

   $scope.$on('CANCEL_QUESTION', ->
-    if $scope.question_id == id
-      $scope.options = $scope.queryOptions()
+    $scope.options = $scope.queryOptions()
   )

   $scope.$on('EDIT_QUESTION', (event, id) ->


position absolute only for big screen

position absolute only for big screen

--- app/assets/javascripts/backend/list_total.js.coffee
+++ app/assets/javascripts/backend/list_total.js.coffee
@@ -9,18 +9,9 @@
             bottom: '0px',
             'max-width': $('#computation-results').closest('table').width() + 'px'
           })
-      else
-        $('#computation-results').css({
-            position: 'absolute',
-            # bottom: '13px',
-            width: $('#computation-results').closest('table').width() + 'px'
-          })
-        # $('#computation-results').closest('table').css({
-        #     position: relative
-        #   })
-      $('table').css({
-        'margin-bottom': '28px',
-      })
+        $('table').css({
+          'margin-bottom': '28px',
+        })
       count = 0
       $("#computation-results > td").each ->
         totest = $(this).closest('table').find($('thead > tr > th'))[count]


TT-23 make backgrid look like the old table

--- app/assets/javascripts/controller/controller.coffee
+++ app/assets/javascripts/controller/controller.coffee
@@ -47,6 +47,7 @@

   testTableHierarchy: ->

+    # avoids ugly  tags in header
     MinimalHeaderCell = Backgrid.HeaderCell.extend(

       render: ->
@@ -58,38 +59,53 @@

     )

-
+    # allows for styled section-rows
     ClickableRow = Backgrid.Row.extend(

-      events: { "click" : "onClick" }
+      events:
+        "click" : "onClick"
+        "style" : "onStyle"

       onClick: ->
-        console.log("this", @)
-        window.test = @
-        @$el.addClass("project-row")
-        Backbone.trigger("rowclicked", @model)
-
-    )
-
-    Backbone.on("rowclicked", (model) ->
-      console.log("model", model)
-    )
+
+        console.log("this was clicked", @)
+        # Backbone.trigger("rowclicked", @model)
+
+
+      onStyle: ->
+
+        if @model.attributes.sectionRow
+          @$el.addClass("project-row")
+
+
+    )
+
+    ClickableCell = Backgrid.Row.extend(
+
+      events:
+
+        "click" : "onClick"
+
+
+      onClick: ->
+
+        console.log("clickableCell this was clicked", @)
+
+    )
+
+
+
+    # Backbone.on("rowclicked", (model) ->
+    #   console.log("model", model)
+    # )

     columns = [
       name: "issue"
-      label: "Issue"
-      editable: false
-      cell: "string"
-      sortable: false
-      headerCell: MinimalHeaderCell
+      label: "Issue"
     ,
       name: "sum"
       label: "∑"
-      editable: false
-      sortable: false
-      cell: "string"
-      headerCell: MinimalHeaderCell
     ]

     data = @prepareModel()
@@ -98,26 +114,43 @@
       columns.push(
         name: d
         label: Utils.zeroPad(d)
-        editable: false
-        sortable: false
-        cell: "string"
-        headerCell: MinimalHeaderCell
-      )
-    )
-
-
-    console.log("data", data)
+      )
+    )
+
+    for aColumn in columns
+      aColumn.editable = false
+      aColumn.cell = "string"
+      aColumn.sortable = false
+      aColumn.headerCell = MinimalHeaderCell
+

     dataCollection = new Backbone.Collection(data)

+
+    console.warn("footer doesnt work?")
+    CaptionFooter = Backgrid.Footer.extend(
+
+      render = ->
+
+        @$el.empty()
+        @$el.append($("
Hello World!
"))
+        @delegateEvents()
+        return @
+
+    )
+
+
     grid = new Backgrid.Grid(
       columns: columns
       collection: dataCollection
       row: ClickableRow
+      cell: ClickableCell
+      footer: CaptionFooter
       className: "table table-hover table-bordered table-striped"
     )

     $("div.report-table").append(grid.render().$el)
+    grid.$el.find("tr").trigger("style")

   instantiateView : ->
@@ -170,13 +203,13 @@
       sectionHeaderRow =
         "issue" : element
         "sum" : Utils.minutesToHours(Utils.sum(daySums))
+        "sectionRow" : true

       _.map(daySums, (sum, index) ->
-        sectionHeaderRow[index] = Utils.minutesToHours(sum) || ""
+        sectionHeaderRow[index + 1] = Utils.minutesToHours(sum) || ""
       )

       table.push(sectionHeaderRow)
-

       _.forOwn(_.groupBy(elementEntries, @groupByIterator),
@@ -184,12 +217,12 @@

           entriesDaysGroups = _.groupBy(entries, (a) -> moment(a.date).date())

-          entry = @groupByIteratorToString entries[0]
+          entry = @groupByIteratorToString(entries[0])

           currentRow =
             "issue" : entry
             "sum" : Utils.minutesToHours(Utils.sum(_.map(entries, "duration")))
-
+            "entriesDaysGroups" : entriesDaysGroups

           _.map(daysRange, (day) =>
               value = Utils.minutesToHours(Utils.sum(
@@ -200,7 +233,7 @@
           )

-          table.push currentRow
+          table.push(currentRow)
       )

     #tfoot


OX-397 setting nodes now affects space direction in all 3 views

--- app/assets/javascripts/oxalis/model/flycam2d.coffee
+++ app/assets/javascripts/oxalis/model/flycam2d.coffee
@@ -111,12 +111,7 @@

   setDirection : (direction) ->
     @direction = direction
-    @updateSpaceDirection()
-
-  updateSpaceDirection : ->
-    ind = Dimensions.getIndices @activePlane
-    if @direction[ind[0]] <= 0 then @spaceDirection[ind[0]] = -1 else @spaceDirection[ind[0]] = 1
-    if @direction[ind[1]] <= 0 then @spaceDirection[ind[1]] = -1 else @spaceDirection[ind[1]] = 1
+    @setSpaceDirection(direction)

   setSpaceDirection : (direction) ->
     for index in [0..2]


SharePopup: move share popup widget to rigth

--- client/Social/Activity/views/activityactions.coffee
+++ client/Social/Activity/views/activityactions.coffee
@@ -61,7 +61,7 @@
           cssClass    : "activity-share-popup"
           type        : "activity-share"
           delegate    : this
-          x           : @getX() + 145
+          x           : @getX() + 160
           y           : @getY() - 7
           menuMaxWidth: 400
           lazyLoad    : yes


LocationForm: Hide button loader on form fail

--- client/app/CommonViews/location/locationform.coffee
+++ client/app/CommonViews/location/locationform.coffee
@@ -7,6 +7,8 @@
     @countryLoader = new KDLoaderView
       size        : { width: 14 }
       showLoader  : yes
+
+    @on 'FormValidationFailed', => @buttons.Save.hideLoader()

   prepareOptions: (options, data) ->


activity: reimplement rendering of like summary view

--- client/Social/Activity/views/likesummaryview.coffee
+++ client/Social/Activity/views/likesummaryview.coffee
@@ -47,32 +47,26 @@
     super

-    names  = []
-    strong = (x) -> "<strong>#{x}</strong>"
-
-    @fetchPreviewAccounts (err, accounts) =>
-
-      return KD.showError err  if err
-      return  if accounts.length is 0
-
-      for i in [0..2]
-        account = accounts[i]
-        return  unless view = this["placeholder#{i}"]
-        view.addSubView new ProfileLinkView null, account
-

   pistachio: ->

-    {actorsCount} = @getData().interactions.like
+    {actorsCount, actorsPreview} = @getData().interactions.like

-    body = switch
-      when actorsCount is 0 then ""
-      when actorsCount is 1 then "{{> @placeholder0}}"
-      when actorsCount is 2 then "{{> @placeholder0}} and {{> @placeholder1}}"
-      when actorsCount is 3 then "{{> @placeholder0}}, {{> @placeholder1}} and {{> @placeholder2}}"
-      when actorsCount > 3
-        othersCount = actorsCount - 3
-        "{{> @placeholder0}}, {{> @placeholder1}}, {{> @placeholder2}} and \
-         {{> @showMoreLink}} other#{if othersCount > 1 then 's' else ''}"
+    body = ""

-    body += " liked this."  if actorsCount > 0
+    return body  unless actorsPreview.length
+
+    linkCount = Math.min actorsPreview.length, 3
+
+    for i in [0..linkCount - 1]
+      body += "{{> this.placeholder#{i}}}"
+
+      if (linkCount - i) is (if actorsCount - linkCount then 1 else 2)
+        body += " and "
+      else if i < (linkCount - 1)
+        body += ", "
+
+    if (diff = actorsCount - linkCount) > 0
+      body += "{{> this.showMoreLink}} other#{if diff > 1 then 's' else ''}"
+
+    body += " liked this."


activity: prevent duplicate likes

activity: prevent duplicate likes

--- client/Social/Activity/views/likesummaryview.coffee
+++ client/Social/Activity/views/likesummaryview.coffee
@@ -39,8 +39,9 @@
     {actorsPreview} = @getData().interactions.like

     if liked
-      actorsPreview.unshift KD.whoami()._id
-      @getData().interactions.like.actorsCount += 1
+      if KD.whoami()._id not in actorsPreview
+        actorsPreview.unshift KD.whoami()._id
+        @getData().interactions.like.actorsCount += 1
     else
       index = actorsPreview.indexOf KD.whoami()._id
       if index > -1


shortcuts: remove obsolete CTRL+L

shortcuts: remove obsolete CTRL+L

--- client/ide/views/shortcutsview/shortcutsview.coffee
+++ client/ide/views/shortcutsview/shortcutsview.coffee
@@ -78,7 +78,6 @@
       'Go to left'             : 'Ctrl-B'
       'Go to right'            : 'Ctrl-F'
       'Go to line'             : 'Meta-G'
-      'Go to line'             : 'Meta-L'
       'Go to line end'         : { Others: 'Alt-Right',      Apple: 'Meta-Right'}
       'Go to line end'         : { Others: 'End',            Apple: 'Ctrl-E'    }
       'Go to line start'       : { Others: 'Alt-Left',       Apple: 'Meta-Left' }


groups: wip selecting activity types

groups: wip selecting activity types

--- client/landing/app/MainApp/lazydomcontroller.coffee
+++ client/landing/app/MainApp/lazydomcontroller.coffee
@@ -255,7 +255,6 @@
             defaultValue : 'CStatusActivity' in types
             callback  : (state)=>
               profileUser["#{if state then 'add' else 'remove'}StaticPageType"] 'CStatusActivity', =>
-                log 'changed type',arguments

           profileBlogPostActivityItem.addSubView blogPostSwitch = new KDOnOffSwitch
             cssClass : 'profile-stream-switch'
@@ -264,7 +263,31 @@
             defaultValue : 'CBlogPostActivity' in types
             callback  : (state)=>
               profileUser["#{if state then 'add' else 'remove'}StaticPageType"] 'CBlogPostActivity', =>
-                log 'changed type',arguments
+
+          profileCodeSnipActivityItem.addSubView codeSnipSwitch = new KDOnOffSwitch
+            cssClass : 'profile-stream-switch'
+            size : 'tiny'
+            title     : 'Show'
+            defaultValue : 'CCodeSnipActivity' in types
+            callback  : (state)=>
+              profileUser["#{if state then 'add' else 'remove'}StaticPageType"] 'CCodeSnipActivity', =>
+
+          profileDiscussionActivityItem.addSubView discussionSwitch = new KDOnOffSwitch
+            cssClass : 'profile-stream-switch'
+            size : 'tiny'
+            title     : 'Show'
+            defaultValue : 'CDiscussionActivity' in types
+            callback  : (state)=>
+              profileUser["#{if state then 'add' else 'remove'}StaticPageType"] 'CDiscussionActivity', =>
+
+          profileTutorialActivityItem.addSubView tutorialSwitch = new KDOnOffSwitch
+            cssClass : 'profile-stream-switch'
+            size : 'tiny'
+            title     : 'Show'
+            defaultValue : 'CTutorialActivity' in types
+            callback  : (state)=>
+              profileUser["#{if state then 'add' else 'remove'}StaticPageType"] 'CTutorialActivity', =>
+

     @on 'ShowMoreButtonClicked', =>


Update atom keymap

Update atom keymap

--- atom/atom/symlinks/keymap.cson
+++ atom/atom/symlinks/keymap.cson
@@ -26,12 +26,18 @@
 # Atom Flight Manual:
 # https://atom.io/docs/latest/using-atom-basic-customization#cson

+'atom-text-editor':
+  'ctrl-l': 'git-diff:move-to-next-diff'
+  'cmd n': 'tree-view:add-file'
+  'cmd f': 'tree-view:add-folder'
+  'cmd r': 'tree-view:rename'
+  'cmd d': 'tree-view:duplicate'
+  'cmd x': 'tree-view:remove'
 'atom-text-editor.vim-mode':
   'ctrl-j': 'vim-mode:reset-normal-mode'
-'atom-text-editor':
-  'ctrl-l': 'git-diff:move-to-next-diff'
 '.platform-darwin':
-  'ctrl-space s': 'git-plus:status'
-  'ctrl-space a': 'git-plus:add'
-  'ctrl-space c': 'git-plus:commit'
-  'ctrl-space p': 'git-plus:push'
+  ', s': 'git-plus:status'
+  ', a': 'git-plus:add'
+  ', c': 'git-plus:commit'
+  ', p': 'git-plus:push'
+  ', c c': 'git-plus:checkout-current-file'


a little too eager there

a little too eager there

--- apps/artwork/client/view.coffee
+++ apps/artwork/client/view.coffee
@@ -66,6 +66,8 @@
     @listenTo @artwork, 'change:sale_message', @renderDetail
     @listenTo @artwork, 'change:ecommerce', @renderDetail
     @listenToOnce @artwork.related().sales, 'sync', @afterSalesFetch
+    @listenToOnce @artwork.related().fairs, 'sync', @handleFairs
+    @listenToOnce @artwork.related().shows, 'sync', @handleShows

     $.when.apply(null, relatedContentFetches).done =>
       relatedCollections = _.values _.pick(@artwork.related(), ['sales', 'features', 'fairs', 'shows'])
@@ -81,13 +83,11 @@
       model: @artwork
       collection: @artwork.related().images

-  afterSalesFetch: ->
+  afterSalesFetch: (sales) ->
     @renderActions()
-    @handleSales()
+    @handleSales sales
     @setupPartnerLocations()
     @setupEmbeddedInquiryForm()
-    @handleFairs()
-    @handleShows()

   handleFairs: (fairs) ->
     return unless fairs.length


Ensure the channel you create is visible

--- components/connect_v2/components/create/view.coffee
+++ components/connect_v2/components/create/view.coffee
@@ -35,7 +35,7 @@
       .then =>
         @render()
         channel.set "selected:#{@connectable.get 'base_class'}:#{@connectable.id}", true
-        @collection.add channel
+        @collection.unshift channel

       .catch =>
         @$el.text 'Error'


Don't rely on String.startsWith

Don't rely on String.startsWith

--- client/routing.coffee
+++ client/routing.coffee
@@ -27,11 +27,11 @@
     logger.debug('User not logged in, rendering login page')
     @render('login')
   else
-    if @url.startsWith('/account')
+    if _.startsWith(@url, '/account')
       curSection = "account"
-    else if @url.startsWith("/create")
+    else if _.startsWith(@url, "/create")
       curSection = "create"
-    else if @url.startsWith("/about")
+    else if _.startsWith(@url, "/about")
       curSection = "about"
     else
       curSection = "explore"


show new bookmarks on top

show new bookmarks on top

--- static/js/bookmarks.model.coffee
+++ static/js/bookmarks.model.coffee
@@ -16,7 +16,7 @@
         $.ajax ajaxSettings

     save: (data) ->
-        bookmarks = data.bookmarks
+        bookmarks = data.bookmarks.reverse()
         @setBookmarks(bookmarks)

     onEmptyInput: (search) ->


fix bid dropdown dismissal

fix bid dropdown dismissal

--- desktop/apps/artwork/components/auction/view.coffee
+++ desktop/apps/artwork/components/auction/view.coffee
@@ -1,4 +1,4 @@
-{ defer, extend, before } = require 'underscore'
+{ defer, extend, before, isEqual } = require 'underscore'
 Backbone = require 'backbone'
 { AUCTION, CURRENT_USER } = require('sharify').data
 Form = require '../../../../components/form/index.coffee'
@@ -10,6 +10,9 @@
 helpers = require './helpers.coffee'
 metaphysics = require '../../../../../lib/metaphysics.coffee'
 template = -> require('./templates/index.jade') arguments...
+
+LOT_STANDING_MAX_POLLS = 10
+LOT_STANDING_POLL_INTERVAL = 1000

 module.exports = class ArtworkAuctionView extends Backbone.View
   className: 'artwork-auction'
@@ -23,7 +26,7 @@
     'change .js-artwork-auction-max-bid': 'setMaxBid'

   initialize: ({ @data }) -> #
-    setInterval before(10, @updateBidLabel), 1000
+    @delayedUpdateBidLabel LOT_STANDING_MAX_POLLS

   openBuyersPremium: (e) ->
     e.preventDefault()
@@ -102,7 +105,7 @@
     @$('.js-artwork-auction-bid-button')
       .text "Bid #{$(e.target).find('option:selected').attr 'data-display'}"

-  updateBidLabel: =>
+  updateBidLabel: (remaining) ->
     metaphysics
       query: """
         query artwork($id: String!, $sale_id: String!) {
@@ -125,7 +128,15 @@
       variables: id: AUCTION.artwork_id, sale_id: AUCTION.id
       req: user: CURRENT_USER
     .then (data) =>
+      old_data = @data
       @data = extend data, user: CURRENT_USER?
-      @render()
+
+      @render() unless isEqual(old_data, data)
+
+      @delayedUpdateBidLabel(remaining - 1) if remaining > 0
+
     .catch console.error.bind console

+  delayedUpdateBidLabel: (remaining) ->
+    setTimeout (=> @updateBidLabel remaining), LOT_STANDING_POLL_INTERVAL
+


Remove commented emails

Remove commented emails

--- both/config/adminConfig.coffee
+++ both/config/adminConfig.coffee
@@ -1,7 +1,5 @@
 AdminConfig =
 	name: Config.name
-	# adminEmails: ['benjaminpeterjonesesquire@gmail.com','dave.southperth@gmail.com']
-	fromEmail: 'noreply@app.com'
 	collections :
 		Posts: {
 			color: 'red'


fix(message): fix truncation of messages

It would seem the document.documentElement.scrollHeight can return 0 when
the document.body.scrollHeight reliably returns the correct height.
Changed to fallback to various height checking mechanisms.

Fixes #425
Fixes #1102
Fixes #1153

--- internal_packages/message-list/lib/email-frame.cjsx
+++ internal_packages/message-list/lib/email-frame.cjsx
@@ -59,12 +59,15 @@
     @refs.iframe.documentWasReplaced()
     @_setFrameHeight()

+  _getFrameHeight: (doc) ->
+    return 0 unless doc
+    return doc.body?.scrollHeight ? doc.documentElement?.scrollHeight ? 0
+
   _setFrameHeight: =>
     return unless @_mounted

     domNode = React.findDOMNode(@)
-    wrapper = domNode.contentDocument.getElementsByTagName('html')[0]
-    height = wrapper.scrollHeight
+    height = @_getFrameHeight(domNode.contentDocument)

     # Why 5px? Some emails have elements with a height of 100%, and then put
     # tracking pixels beneath that. In these scenarios, the scrollHeight of the
@@ -74,7 +77,7 @@
       domNode.height = "#{height}px"
       @_lastComputedHeight = height

-    unless domNode?.contentDocument?.readyState is 'complete'
+    unless domNode.contentDocument?.readyState is 'complete'
       _.defer => @_setFrameHeight()

   _emailContent: =>


- [grunt] invoke `shell:build` instead of obsolete `duojs` during watch

--- tasks/grunt-contrib-watch.coffee
+++ tasks/grunt-contrib-watch.coffee
@@ -32,7 +32,7 @@
       tasks: ['nunjucks']
     scripts:
       files: ['<%= path.source.scripts %>/{,**}*.js']
-      tasks: ['duojs']
+      tasks: ['shell:build']
     scss:
       files: [
         '<%= path.source.styles %>/{,**/}*.scss'