was using in on an object rather than of, fixed loggic

--- coffeescript/feedlyconsole.coffee
+++ coffeescript/feedlyconsole.coffee
@@ -337,8 +337,9 @@
       return callback(cache)  if cache
       unless chrome.extension?
         # not embedded, demo mode
-        demo = resource in demo_data
-        _self[resource] = if demo then demo_data[resource] else {}
+        demo = demo_data[resource]
+        demo = {} unless demo?
+        _self[resource] = demo
         callback _self[resource]
       else
         if args
@@ -400,13 +401,13 @@

     # });
     insertCSSLink = (name) ->
-
+      console.debug "inserting css #{name}"
       # insert css into head
-      $("head").append $("<link/>",
+      $("head").append $ "<link/>",
         rel: "stylesheet"
         type: "text/css"
         href: chrome.extension.getURL(name)
-      )
+
     doInsertShellUI = ->
       observer.disconnect()
       file = "feedlyconsole.html"
@@ -615,8 +616,8 @@
         $consoletab.slideDown()
       _console.log "[Josh.FeedlyConsole] initializeUI."
       $consoletab = $("#consoletab")
-      console.error "failed to find %s",
-        $consoletab.selector  if $consoletab.length is 0
+      if $consoletab.length is 0
+        console.error "failed to find %s", $consoletab.selector
       $consoletab.hover (->
         $consoletab.addClass "consoletab-hover"
         $consoletab.removeClass "consoletab"


Add underscore dependency to header

Add underscore dependency to header

--- src/scripts/truckstopsf.coffee
+++ src/scripts/truckstopsf.coffee
@@ -3,7 +3,7 @@
 #   See http://truckstopsf.com
 #
 # Dependencies:
-#   None
+#   "underscore": "*"
 #
 # Configuration:
 #   None
@@ -16,11 +16,11 @@
 # Author:
 #   chris

-utils = require 'underscore'
+_ = require 'underscore'

 data_by_matcher = (input, matcher) ->
   get_data = (match.match(matcher).slice(1) for match in input)
-  utils.flatten(get_data)
+  _.flatten(get_data)

 module.exports = (robot) ->
   robot.respond /truckstopsf\s?(!|details|deets)?/i, (res) ->


fix(destroy-draft): Update destroy draft task to reflect new tasks API

--- src/flux/tasks/destroy-draft.coffee
+++ src/flux/tasks/destroy-draft.coffee
@@ -3,6 +3,7 @@
 Message = require '../models/message'
 DatabaseStore = require '../stores/database-store'
 Actions = require '../actions'
+NylasAPI = require '../nylas-api'

 SyncbackDraftTask = require './syncback-draft'
 SendDraftTask = require './send-draft'
@@ -33,29 +34,31 @@
     # We don't need to do anything if we weren't able to find the draft
     # when we performed locally, or if the draft has never been synced to
     # the server (id is still self-assigned)
-    return Promise.resolve() unless @draft
-    return Promise.resolve() unless @draft.isSaved()
+    return Promise.resolve(Task.Status.Finished) unless @draft
+    return Promise.resolve(Task.Status.Finished) unless @draft.isSaved()

-    atom.inbox.makeRequest
+    NylasAPI.makeRequest
       path: "/n/#{@draft.namespaceId}/drafts/#{@draft.id}"
       method: "DELETE"
       body:
         version: @draft.version
       returnsModel: false
+    .then =>
+      return Promise.resolve(Task.Status.Finished)
+    .catch APIError, (err) =>
+      inboxMsg = err.body?.message ? ""

-  recoverFromRemoteAPIError: (err) ->
-    inboxMsg = err.body?.message ? ""
+      # Draft has already been deleted, this is not really an error
+      if err.statusCode is 404
+        return Promise.resolve(Task.Status.Finished)

-    # Draft has already been deleted, this is not really an error
-    if err.statusCode is 404
-      return Promise.resolve(Task.Status.Finished)
+      # Draft has been sent, and can't be deleted. Not much we can do but finish
+      if inboxMsg.indexOf("is not a draft") >= 0
+        return Promise.resolve(Task.Status.Finished)

-    if err.statusCode in NylasAPI.PermanentErrorCodes
-      Actions.postNotification({message: "Unable to delete this draft. Restoring...", type: "error"})
-      return DatabaseStore.persistModel(@draft)
+      if err.statusCode in NylasAPI.PermanentErrorCodes
+        Actions.postNotification({message: "Unable to delete this draft. Restoring...", type: "error"})
+        DatabaseStore.persistModel(@draft).then =>
+          return Promise.resolve(Task.Status.Finished)

-    # Draft has been sent, and can't be deleted. Not much we can do but finish
-    if inboxMsg.indexOf("is not a draft") >= 0
-      return Promise.resolve(Task.Status.Finished)
-
-    Promise.resolve(Task.Status.Retry)
+      Promise.resolve(Task.Status.Retry)


Added setGoalState

Added setGoalState

--- app/lib/world/thang.coffee
+++ app/lib/world/thang.coffee
@@ -38,6 +38,9 @@
   publishNote: (channel, event) ->
     event.thang = @
     @world.publishNote channel, event
+
+  setGoalState: (goalID, status) ->
+    @world.setGoalState goalID, status

   addComponents: (components...) ->
     # We don't need to keep the components around after attaching them, but we will keep their initial config for recreating Thangs


DevTools: implemented enforcelogin for devtools

--- client/DevTools/AppController.coffee
+++ client/DevTools/AppController.coffee
@@ -1,13 +1,18 @@
 class DevToolsController extends AppController

-  name    = "DevTools"
-  version = "0.1"
-  route   = "/:name?/#{name}"
-
-  KD.registerAppClass this, {
-    name, version, behavior: "application", route,
-    menu          :
-      items       : [
+  KD.registerAppClass this,
+    name            : 'DevTools'
+    version         : '0.1'
+    route           : '/:name?/DevTools'
+    behavior        : 'application'
+    preCondition    :
+      condition     : (options, cb)-> cb KD.isLoggedIn()
+      failure       : (options, cb)->
+        KD.singletons.appManager.open 'DevTools', conditionPassed : yes
+        KD.showEnforceLoginModal()
+    menu            :
+      hiddenOnStart : yes
+      items         : [
         { title     : "Create a new App",    eventName : "create" }
         { type      : "separator" }
         { title     : "Save",                eventName : "save" }
@@ -23,8 +28,7 @@
         { type      : "separator" }
         { title     : "Exit",                eventName : "exit" }
       ]
-      hiddenOnStart : yes
-  }
+

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


Update Google Maps query

Update Google Maps query

--- client/about/lib/aboutappview.coffee
+++ client/about/lib/aboutappview.coffee
@@ -57,7 +57,7 @@

Where we are?

       <address>358 Brannan Street, San Francisco, CA, 94107</address>

-
+

@@ -86,5 +86,3 @@

     {{> @footer}}
     """
-
-


Remove useless originalConversationID

Remove useless originalConversationID

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -54,10 +54,7 @@
         MessageUtils.makeReplyMessage @props

     isNew: ->
-        not @state.originalConversationID
-
-    unsetNew: ->
-        @state.originalConversationID = @state.conversationID
+        not @state.conversationID

     shouldComponentUpdate: (nextProps, nextState) ->
         !!nextProps.accounts
@@ -385,10 +382,6 @@
                     if _.isUndefined @state[key]
                         @state[key] = message[key]

-            # use another field to prevent the empty conversationID of draft
-            # to override the original conversationID
-            @unsetNew() if @isNew()
-
             # TODO : move this into render
             unless @state.isDraft
                 # Display confirmation message


Client: Automatically clean up chat messages.

--- client/app/js/views/chat_view.coffee
+++ client/app/js/views/chat_view.coffee
@@ -9,6 +9,7 @@
     'scroll_to_bottom': 'scrollToBottom'

   MAX_USERNAME_HISTORY = 10
+  MAX_MESSAGES_LENGTH = 500

   # Takes a `collection`, which is a UserList instance.
   initialize: (options) =>
@@ -162,8 +163,15 @@
     wasAtBottom = @isAtBottom()
     klass = []
     klass.push('bg-blue')  if options.highlight
-    @print("
#{message}
")
+    @print("
#{message}
")
+    @cleanChat()
     if wasAtBottom then @scrollToBottom()
+
+  cleanChat: =>
+    $messages = @$('.chat_message')
+    numToRemove = ($messages.length - MAX_MESSAGES_LENGTH)
+    if numToRemove > 0
+      $messages.slice(0, numToRemove).remove()

   announce: (klass, message) =>
     wasAtBottom = @isAtBottom()


remove empty line

remove empty line

--- client/code/view/dataset/list.coffee
+++ client/code/view/dataset/list.coffee
@@ -33,7 +33,6 @@
         </tr>
       </thead>
       <tbody>
-
       </tbody>
     </table>"""
     @addDatasets()


a number of general improvements to markdown helpers

--- src/helpers/helpers-markdown.coffee
+++ src/helpers/helpers-markdown.coffee
@@ -48,11 +48,11 @@
   ###
   Handlebars.registerHelper 'authors', (authors) ->
     if Utils.isUndefined(authors)
-      authors = Utils.readFile("./AUTHORS")
+      authors = Utils.readSync("./AUTHORS")
     else
-      authors = Utils.readFile(authors)
+      authors = Utils.readSync(authors)
     matches = authors.replace(/(.*?)\s*\((.*)\)/g, '[$1]' + '($2)') or []
-    new Handlebars.SafeString(matches)
+    Utils.safeString(matches)

   ###
@@ -63,43 +63,39 @@
   ###
   Handlebars.registerHelper "changelog", (changelog) ->
     if Utils.isUndefined(changelog)
-      changelog = yaml.load Utils.readFile('./CHANGELOG', 'utf8')
+      changelog = yaml.load Utils.readSync('./CHANGELOG')
     else
-      changelog = yaml.load Utils.readFile(changelog, 'utf8')
+      changelog = yaml.load Utils.readSync(changelog)
     source = "{{#each .}}* {{date}}    {{{@key}}}    {{#each changes}}{{{.}}}{{/each}}\n{{/each}}"
     template = Handlebars.compile(source)
-    new Handlebars.SafeString(template(changelog))
+    Utils.safeString(template(changelog))

   ###
   Section: reads in data from a markdown file, and uses the first heading
   as a section heading, and then copies the rest of the content inline.
-  Usage: {{{ section [file] }}
+  Usage: {{ section [file] }}
   ###
   Handlebars.registerHelper 'section', (file) ->
-    file = Utils.readFile(file)
+    file = Utils.readSync(file)
     content = file.replace(/(^[^ ]*\s)(.+)([^#]+(?=.*)$)/gim, '$2\n' + '$3') or []
-    new Handlebars.SafeString(content)
-
+    Utils.safeString(content)

   ###
   Glob: reads in data from a markdown file, and uses the first heading
   as a section heading, and then copies the rest of the content inline.
-  Usage: {{{ section [file] }}
+  Usage: {{{ glob [file] }}
   ###
   Handlebars.registerHelper 'glob', (file) ->
-    file = glob.find(file)
-    content = Utils.readFile(file)
+    file    = glob.find(file)
+    content = Utils.readSync(file)
     content = content.replace(/(^[^ ]*\s)(.+)([^#]+(?=.*)$)/gim, '$2\n' + '$3') or []
-    new Handlebars.SafeString(content)
-
+    Utils.safeString(content)

   ###
   Markdown: Markdown helper used to write markdown inside and
   rendered the markdown inline with the HTML
-
   Usage: {{#markdown}} # This is a title. {{/markdown}}
-
   Renders to: <h1>This is a title </h1>
   ###
   Handlebars.registerHelper "markdown", (options) ->
@@ -115,6 +111,6 @@
     ###
     Handlebars.registerHelper "md", (path) ->
       content = markdown.read(path)
-      new Handlebars.SafeString(content)
+      Utils.safeString(content)

   @


rename save to edit

rename save to edit

--- client/lanes/screens/Commands.coffee
+++ client/lanes/screens/Commands.coffee
@@ -17,7 +17,8 @@
         @screen.setModelUrl?(model)
         @options.modelDidRebind?(model)

-    canSaveModel: ->
+    canEditModel: ->
+
         @screen.hasWriteAccess?()

     getSyncOptions: ->


stack-editor: fix lint

stack-editor: fix lint

--- client/stack-editor/lib/index.coffee
+++ client/stack-editor/lib/index.coffee
@@ -62,14 +62,14 @@
           @removeEditor stackTemplate._id
           kd.singletons.router.handleRoute '/IDE'

-        isClonedTemplate stackTemplate, (originalTemplate) =>
+        isClonedTemplate stackTemplate, (originalTemplate) ->
           if originalTemplate
             editor.addClonedFrom originalTemplate
             originalTemplate.on 'update', (update) ->
               if stackTemplate.template.sum isnt originalTemplate.template.sum
                 return editor.addCloneUpdateView originalTemplate
             if stackTemplate.template.sum isnt originalTemplate.template.sum
-                editor.addCloneUpdateView originalTemplate
+              editor.addCloneUpdateView originalTemplate

     else
       @showView()


Pass browser window vs viewport size ratio to android code

--- src/js/OTHelpers.coffee
+++ src/js/OTHelpers.coffee
@@ -66,13 +66,18 @@
 TBUpdateObjects = ()->
   console.log("JS: Objects being updated in TBUpdateObjects")
   objects = document.getElementsByClassName('OT_root')
+
+  # Ratio between browser window size and viewport size
+  widthRatio = window.outerWidth / window.innerWidth
+  heightRatio = window.outerHeight / window.innerHeight
+
   for e in objects
     console.log("JS: Object updated")
     streamId = e.dataset.streamid
     console.log("JS sessionId: " + streamId )
     id = e.id
     position = getPosition(id)
-    Cordova.exec(TBSuccess, TBError, OTPlugin, "updateView", [streamId, position.top, position.left, position.width, position.height, TBGetZIndex(e)] )
+    Cordova.exec(TBSuccess, TBError, OTPlugin, "updateView", [streamId, position.top, position.left, position.width, position.height, TBGetZIndex(e), widthRatio, heightRatio] )
   return
 TBGenerateDomHelper = ->
   domId = "PubSub" + Date.now()


add note about plan publish store

add note about plan publish store

--- src/helpers/plan.coffee
+++ src/helpers/plan.coffee
@@ -9,6 +9,7 @@
     isPublishing = (plan.is_publish_requested? and plan.is_publish_requested) or plan.publish_last_requested_at?

     if plan.published_at and plan.publish_job?.status?
+      # TODO could add publish job from plan to PlanPublishStore if we really want to.
       isPublishing = plan.publish_job.status isnt 'succeeded'
     else if plan.published_at? and plan.publish_last_requested_at?
       # is the last requested publishing after the last successful publish?


Regenerate help page data when key bindings are updated

--- background_scripts/commands.coffee
+++ background_scripts/commands.coffee
@@ -9,7 +9,6 @@

     Settings.postUpdateHooks["keyMappings"] = @loadKeyMappings.bind this
     @loadKeyMappings Settings.get "keyMappings"
-    @prepareHelpPageData()

   loadKeyMappings: (customKeyMappings) ->
     @keyToCommandRegistry = {}
@@ -43,6 +42,7 @@

     chrome.storage.local.set mapKeyRegistry: @mapKeyRegistry
     @installKeyStateMapping()
+    @prepareHelpPageData()

     # Push the key mapping for passNextKey into Settings so that it's available in the front end for insert
     # mode.  We exclude single-key mappings (that is, printable keys) because when users press printable keys


there may not be any arguments

there may not be any arguments

--- src/incoming-message-handler.coffee
+++ src/incoming-message-handler.coffee
@@ -9,11 +9,11 @@

   { arguments: args, links, callbacks, method, authentication: auth } = req

-  [{ withArgs, responseCallback, kite, authentication: auth }] = args
+  if args.length?
+    [{ withArgs, responseCallback, kite, authentication: auth }] = args

   handleAuth(auth, @key).then (token) ->

-    console.log token
     if !withArgs? and !responseCallback?
       # it's a normal dnode protocol message.
       proto.handle req


Now loading *all* 'dive'-type textures

Now loading *all* 'dive'-type textures

--- src/js/marbleous.coffee
+++ src/js/marbleous.coffee
@@ -135,7 +135,7 @@
     'middle':
       'curve':           4
       'straight':        2
-      'dive':            1
+      'dive':            4

   constructor: (@map, canvasID, @onload) ->


setZoomLevelLimits -> setVisualZoomLevelLimits

--- src/main-process/atom-window.coffee
+++ src/main-process/atom-window.coffee
@@ -310,4 +310,4 @@
   copy: -> @browserWindow.copy()

   disableZoom: ->
-    @browserWindow.webContents.setZoomLevelLimits(1, 1)
+    @browserWindow.webContents.setVisualZoomLevelLimits(1, 1)


:email: negative lookahead or whatever the hell

only redact the domain

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -82,7 +82,7 @@
   text = text.replace(/(https?:\/\/[^\s]+)/, "[redacted]")

   # Emails 'R' Secret
-  text = text.replace(/[^@\s]+@[^@\s]+\.[^@\s]+/, "[redacted]")
+  text = text.replace(/(?![^@\s]+@)[^@\s]+\.[^@\s]+/, "[redacted]")

   # Twitter length, then truncate with `...`
   limit = 140


Why does he need less letters? No one knows!

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -106,8 +106,8 @@
     # http://pixxx.wtf.cat/image/0Q3B34330v0c/12188116_10205398142892807_4244989619067012464_o.jpg
     name = 'solient'

-  # has multiple connections because computers Я hard
-  if /justinw/i.test(name)
+  # decides to shed letters randomly!
+  if /justin/i.test(name)
     name = 'justinw2'

   if /blah64/i.test(name)


not watermark unless at bottom and good state

--- src/ui/models/viewstate.coffee
+++ src/ui/models/viewstate.coffee
@@ -35,7 +35,7 @@
     updateActivity: (time) ->
         @lastActivity = time
         updated 'lastActivity'
-        return unless document.hasFocus()
+        return unless document.hasFocus() and @atbottom and @state == STATES.STATE_NORMAL
         c = conv[@selectedConv]
         return unless c
         ur = conv.unread c


Use those @ signs.

Use those @ signs.

--- app/assets/javascripts/activity-feed.js.coffee
+++ app/assets/javascripts/activity-feed.js.coffee
@@ -1,16 +1,16 @@
 SubstoryModel = Backbone.Model.extend
   decoratedJSON: ->
-    json = this.toJSON()
+    json = @toJSON()
     json["type"] = {}
-    json["type"][this.get("substory_type")] = true
+    json["type"][@get("substory_type")] = true
     return json

 StoryModel = Backbone.Model.extend
   initialize: ->
-    substories = this.get("substories")
+    substories = @get("substories")

     substoriesCollection = new SubstoryCollectionClass
-    this.set "substories", substoriesCollection
+    @set "substories", substoriesCollection

     that = this
     _.each substories, (x) -> x.user = that.get("user")
@@ -28,13 +28,13 @@
   return -moment(story.get("updated_at")).unix()

 StoryView = Backbone.View.extend
-  initialize: -> this.expanded = false
+  initialize: -> @expanded = false
   template: HandlebarsTemplates["stories/story"]

   render: ->
-    json = this.model.toJSON()
+    json = @model.toJSON()
     json["type"] = {}
-    json["type"][this.model.get("story_type")] = true
+    json["type"][@model.get("story_type")] = true

     json["morethantwo"] = json["substories"].length > 2
     json["additional"] = json["substories"].length - 2
@@ -45,46 +45,46 @@
       q["type"][s.get("substory_type")] = true
       return q

-    unless this.expanded
+    unless @expanded
       json["substories"] = json["substories"].slice(0, 2)

-    json["expanded"] = this.expanded
+    json["expanded"] = @expanded

-    this.$el.html this.template(json)
+    @$el.html @template(json)

     that = this
-    this.$el.find("a.show-more").click ->
+    @$el.find("a.show-more").click ->
       that.toggleExpand()

   toggleExpand: ->
-    this.expanded = !this.expanded
-    this.render()
+    @expanded = !@expanded
+    @render()

 StoryCollectionViewClass = Backbone.View.extend
   initialize: ->
     _(this).bindAll 'add', 'remove'
-    this.views = {}
-    this.collection.bind 'add', this.add
-    this.collection.bind 'remove', this.remove
-    this.loadedAll = false
-    this.fetchInProgress = false
+    @views = {}
+    @collection.bind 'add', @add
+    @collection.bind 'remove', @remove
+    @loadedAll = false
+    @fetchInProgress = false
   add: (story) ->
-    this.views[story.cid] = new StoryView
+    @views[story.cid] = new StoryView
       model: story
   remove: (story) ->
-    delete this.views[story.cid]
+    delete @views[story.cid]
   render: ->
-    this.$el.empty()
+    @$el.empty()
     that = this
-    this.collection.each (model) ->
+    @collection.each (model) ->
       view = that.views[model.cid]
       view.render()
       that.$el.append view.$el
-    $(".activity-feed").append this.$el
+    $(".activity-feed").append @$el
   fetchMore: (baseURL) ->
-    unless this.loadedAll or this.fetchInProgress
-      page = 1 + Math.floor(this.collection.length / 20)
-      this.fetchInProgress = true
+    unless @loadedAll or @fetchInProgress
+      page = 1 + Math.floor(@collection.length / 20)
+      @fetchInProgress = true
       that = this
       $.ajax baseURL + "page=" + page,
         dataType: "json"


Tweak menu.coffee (13da825701a6f81b27743837ab542db4f96a55bf)

--- app/assets/javascripts/application/menu.coffee
+++ app/assets/javascripts/application/menu.coffee
@@ -10,16 +10,17 @@
     areAllItemsOnTheSameLine = ->
       firstItem.offset().top is lastItem.offset().top

-    ajustFontSize = ->
-      classNames = ['smaller-14', 'smaller-13', 'smaller-12', 'smaller-11']
-      classNamesString = classNames.join(' ')
+    unless areAllItemsOnTheSameLine()
+      ajustFontSize = ->
+        classNames = ['smaller-14', 'smaller-13', 'smaller-12', 'smaller-11']
+        classNamesString = classNames.join(' ')

-      for className in classNames
-        if areAllItemsOnTheSameLine()
-          break
+        for className in classNames
+          if areAllItemsOnTheSameLine()
+            break

-        menu.removeClass(classNamesString)
-        menu.addClass(className)
+          menu.removeClass(classNamesString)
+          menu.addClass(className)

-    ajustFontSize()
-    (window).load ajustFontSize
+      ajustFontSize()
+      $(window).load ajustFontSize


fix export ewall naming problem

fix export ewall naming problem

--- app/assets/javascripts/export_import.js.coffee
+++ app/assets/javascripts/export_import.js.coffee
@@ -1,7 +1,7 @@

 download_export_file = (url, file) ->
   $('#exporting_wall').html("
Export finished
")
-  window.location.href = url + '.ewall?export_file=' + encodeURIComponent(file)
+  window.location.href = url + '.ewall?file=' + encodeURIComponent(file)

 export_progress = (url, file) ->
   $.ajax
@@ -35,7 +35,7 @@
 $(document).ready ->
   if ($('#exporting_wall').length)
     exporting = $('#exporting_wall')
-    file = exporting.data('export-file')
+    file = exporting.data('file')
     url = exporting.data('url')
     export_progress(url, file)
   if ($('#importing_wall').length)


Load flow manager data on mount.

Load flow manager data on mount.

--- app/assets/javascripts/flow_managers.js.coffee
+++ app/assets/javascripts/flow_managers.js.coffee
@@ -50,6 +50,9 @@
             (li {}, PaperProfile {profile: paperProfile}))))

 FlowManager = React.createClass
+  componentDidMount: ->
+    $.getJSON 'flow_manager', (data,status) =>
+      @setProps flows: data.flows
   render: ->
     {div} = React.DOM
     (div {className: 'columns'},
@@ -60,6 +63,5 @@
 Tahi.flowManager =
   init: ->
     if document.getElementById('flow-manager')
-      $.getJSON 'flow_manager', (data,status) ->
-        flowManager = FlowManager flows: data.flows
-        React.renderComponent flowManager, document.getElementById('flow-manager')
+      flowManager = FlowManager flows: []
+      React.renderComponent flowManager, document.getElementById('flow-manager')


Slice move backwards (again).

Slice move backwards (again).

--- app/assets/javascripts/roofpig/Layer.js.coffee
+++ app/assets/javascripts/roofpig/Layer.js.coffee
@@ -18,7 +18,7 @@
   @D: new Layer('D', v3( 0, 0,-1), ['DFR','DFL','DBL','DBR'],['DF','DL','DB','DR'], ['D'], F:'L', L:'B', B:'R', R:'F', U:'U', D:'D')

   @M: new Layer('M', @L.normal, ['UF', 'UB', 'DB', 'DF'], ['U', 'B', 'D', 'F'], [], @L.sticker_cycle)
-  @E: new Layer('E', @D.normal, ['FL', 'FR', 'BR', 'BL'], ['F', 'R', 'B', 'L'], [], @D.sticker_cycle)
+  @E: new Layer('E', @D.normal, ['BL', 'BR', 'FR', 'FL'], ['L', 'B', 'R', 'F'], [], @D.sticker_cycle)
   @S: new Layer('S', @F.normal, ['DL', 'DR', 'UR', 'UL'], ['L', 'D', 'R', 'U'], [], @F.sticker_cycle)

   shift: (side_name, turns) ->


fix issue with opacity for vector layers

--- app/assets/javascripts/workspace/layers.coffee
+++ app/assets/javascripts/workspace/layers.coffee
@@ -96,7 +96,7 @@
         layout: { 'visibility': 'visible' }
         paint: {
           "#{type}-color": color,
-          "#{type}-opacity": @getOpacity(config.name)
+          "#{type}-opacity": @ws.ui.getOpacity(config.name)
         }
       }, beforeLayer, true)


Added details summary redirect

Added details summary redirect

--- app/assets/pg/level/level.controller.js.coffee
+++ app/assets/pg/level/level.controller.js.coffee
@@ -22,4 +22,7 @@
           # Round changed and we are now to round 2
           else if seasons.current().progression.round is 2
             $state.go 'main.season.pg.level.round.gender.summary'
+          # Round changed and we are now to round 3
+          else if seasons.current().progression.round is 3
+            $state.go 'main.season.pg.level.round.details.summary'
           else do @startRound


Add extremely critical culture to the cow.

Auditors: alpert

--- scripts/moo.coffee
+++ scripts/moo.coffee
@@ -12,3 +12,9 @@
       if !msg.envelope.room
         msg.send "This incident will be reported"

+  robot.hear /\bdesmondo{1,}\b/i, (msg) ->
+    if msg.envelope.room == "1s_and_0s"
+      robot.messageHipchat "desmondOOOOOOOooooooo"
+    else
+      if !msg.envelope.room
+        msg.send "This incident will be reported"


Add doc of qhm and haik commands

Add doc of qhm and haik commands

--- scripts/qhm.coffee
+++ scripts/qhm.coffee
@@ -11,6 +11,8 @@
 #   hubot qhm:set <url> - set QHM site url
 #   hubot qhm:edit <pagename|url> - get edit-page URL
 #   hubot qhm:release - Release latest QHM
+#   hubot qhm:help <query> - get URL of search result at QHM manual
+#   hubot haik:<help|lessons|plugins|colors|themes> - get URL of QHM-haik manual.

 Path = require "path"
 URL = require "url"


reduced filepicker code

reduced filepicker code

--- app/assets/javascripts/admin/views/task/task_create_subviews/task_create_from_nml_view.coffee
+++ app/assets/javascripts/admin/views/task/task_create_subviews/task_create_from_nml_view.coffee
@@ -52,14 +52,8 @@
     # grab file list from event
     files = evt.target.files

-    # do nothing if nothing selected
-    if files
+    # build list
+    filePath = _.pluck(files, "name").join(", ")

-      filePath = files[0].name
-
-      # concat files comma separated
-      for index in [1..files.length-1]
-        filePath += ", " + files[index].name
-
-      # update ui
-      @ui.fileInfo.val(filePath)
+    # update ui
+    @ui.fileInfo.val(filePath)


dont need layout/header templates for client

--- packages/tower-generator/server/generators/tower/app/templates/grunt.coffee
+++ packages/tower-generator/server/generators/tower/app/templates/grunt.coffee
@@ -22,6 +22,8 @@
     for file in files
       continue unless file.match(/app\/views\/.+\.coffee$/)
       continue unless file.match(/\.coffee$/)
+      # @todo tmp, dont need these for the client
+      continue if file.match('layouts/application') || file.match('shared')
       result.push [file.replace(/\.coffee$/, ""), fs.readFileSync(file)]

     template      = "Tower.View.cache =\n"


fixes

fixes

--- src/xmpp/backend_pubsub.coffee
+++ src/xmpp/backend_pubsub.coffee
@@ -68,7 +68,7 @@
                 if pending < 1 and not resultSent
                     resultSent = true
                     cb new errors.NotFound("No pubsub channels service discovered")
-            for item in items
+            items.forEach (item) =>
                 @infoCache.get item.jid, (err, result) ->
                     console.log infoCacheErr: err, infoCache: result
                     for identity in result?.identities or []


only include one copy of the Mousetrap library

--- src/lib.includes.coffee
+++ src/lib.includes.coffee
@@ -16,7 +16,6 @@
   "./libs/inflector.js"
   "./libs/canvas-loader.js"
   "./libs/mousetrap.js"
-  "./libs/mousetrap.js"
   "./libs/mousetrap-global-bind.js"
   "./libs/marked.js"
   "./libs/jspath.js"


added helen to build

added helen to build

--- scripts/name-expander.coffee
+++ scripts/name-expander.coffee
@@ -22,7 +22,7 @@

 dert_mermbers = ["CDERCKERSERN", "MAHCHERLERVER", "JERS HERERTH", "JKERL", "JERMAH WERNSER", "CHIN YERN", "KERLERN", "ERNDRERW GERSERN", "JERSTEN CERMPBELL"]

-build_members = ["mvuong", "mstewart", "jromigh", "pmckeown", "pmckeown2", "pmckeown_home2", "hzhang"]
+build_members = ["mvuong", "mstewart", "jromigh", "pmckeown", "pmckeown2", "pmckeown_home2", "hzhang", "helen"]

 merlin_remote_members = [
   cliff,


add another wtf images

add another wtf images

--- scripts/what-the-fuck.coffee
+++ scripts/what-the-fuck.coffee
@@ -1,6 +1,11 @@
 # What the fuck?
 #

+images = [
+  "http://mlkshk.com/r/PWFK.gif",
+  "http://i49.tinypic.com/2vi191d.gif"
+]
+
 module.exports = (robot) ->
   robot.hear /what the fuck(.*)/i, (msg) ->
-    msg.send "http://i49.tinypic.com/2vi191d.gif"
+    msg.send msg.random images


Spree.disableSaveOnClick: do not truly disable the submit buttons, so that their name gets sent as a param.

Required for the “Apply Store Credit” opt-in button.

--- frontend/app/assets/javascripts/spree/frontend/checkout.js.coffee
+++ frontend/app/assets/javascripts/spree/frontend/checkout.js.coffee
@@ -4,8 +4,15 @@
 //= require spree/frontend/checkout/payment

 Spree.disableSaveOnClick = ->
-  ($ 'form.edit_order').submit ->
-    ($ this).find(':submit, :image').attr('disabled', true).removeClass('primary').addClass 'disabled'
+  ($ 'form.edit_order').on('submit', (e) ->
+    if (($ this).data('submitted') == true)
+      # Previously submitted, don't submit again
+      e.preventDefault()
+    else
+      # Mark it so that the next submit gets ignored
+      ($ this).data('submitted', true)
+      ($ this).find(':submit, :image').removeClass('primary').addClass 'disabled'
+  )

 Spree.ready ($) ->
   Spree.Checkout = {}


Change cdns to version 1.0.0

Change cdns to version 1.0.0

--- snippets/html.cson
+++ snippets/html.cson
@@ -405,21 +405,21 @@
         """
   'Materialize css cdn':
     'prefix': 'm-cdn:css'
-    'body': '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css">'
+    'body': '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">'
   'Google fonts cdn':
     'prefix': 'm-cdn:google-icons'
     'body': '<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">'
   'Materialize js cdn':
     'prefix': 'm-cdn:js'
-    'body': '<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/js/materialize.min.js"></script>'
+    'body': '<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>'
   'Materialize css and js cdn':
     'prefix': 'm-cdn'
     'body': """
         <!-- Compiled and minified CSS -->
-        <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css'>
+        <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css'>

         <!-- Compiled and minified JavaScript -->
-        <script src='https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/js/materialize.min.js'></script>
+        <script src='https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js'></script>
         """
   'Materialize chip for contacts with optional close':
     'prefix': 'm-chip:contact'


Allow keyboard scrolling thru carousel on /artists

--- apps/artists/client.coffee
+++ apps/artists/client.coffee
@@ -17,6 +17,7 @@
   initialize: (options) ->
     @resize = _.debounce @updateValues, 100
     $(window).on 'resize', @resize
+    $(document).on 'keyup', (e) => @keyUp(e)

     # Wait for the first image to load before enabling anything
     @$el.imagesLoaded?().progress _.once(@updateValues)
@@ -61,6 +62,8 @@

   next: (e) ->
     e?.preventDefault()
+    if @active is @$images.length - @increment
+      @active -= @$images.length

     @active += @increment
     @moveToActive()
@@ -69,11 +72,22 @@

   prev: (e) ->
     e?.preventDefault()
+    if @active is 0
+      @active = @$images.length

     @active -= @increment
     @moveToActive()

     analytics.track.click 'Previous page in /artists carousel'
+
+  keyUp: (e) ->
+    switch e.keyCode
+      # left arrow and the h key
+      when 37, 72
+        @prev()
+      # right arrow and the l key
+      when 39, 76
+        @next()

 module.exports.ArtistsView = class ArtistsView extends Backbone.View
   initialize: (options) ->


(feature) Keyboard shortcuts for undo and redo. Resolves #3.

--- app/scripts/pages/correction/correction.coffee
+++ app/scripts/pages/correction/correction.coffee
@@ -30,6 +30,15 @@
     .on 'scribble:undoUnavailable', => @canUndo no
     .on 'scribble:redoAvailable', => @canRedo yes
     .on 'scribble:redoUnavailable', => @canRedo no
+
+    $(document).on 'keydown.correction', (event) =>
+      if event.keyCode == 90 and event.ctrlKey #Ctrl+Z
+        if event.shiftKey #Ctrl+Shift+Z
+          @redo()
+        else
+          @undo()
+      else if event.keyCode == 89 and event.ctrlKey #Ctrl+Y
+        @redo()

     @tool = ko.observable()
     @tool.subscribe (v) =>


fix hiring agents

fix hiring agents

--- source/desk.coffee
+++ source/desk.coffee
@@ -30,7 +30,7 @@
       email =  agent.skills.email
       phoneSkill =  agent.skills.phone
       salary =  agent.salary
-      agentTx = agent.name + "\ne:" + email  + "\np:" +  phoneSkill + "\nsalary:" +  salary
+      agentTx = agent.name + "\ne: " + email  + "\np: " +  phoneSkill + "\nsalary: " +  salary.toFixed(2)

       button = {text: agentTx, click: ->
                                   ui.close()
@@ -45,7 +45,7 @@
     ui.open()

   hireAgent: (agentD) ->
-    @agent = new Agent(this.x, this.y, agentD)
+    @agent = new Agent(this.x, this.y+1, agentD)

   infoScreen: () ->
     if(!@agent)


Fix # Cross-domain redirect should be permanent and protocol shouldn't be changed

--- server/config/express.coffee
+++ server/config/express.coffee
@@ -33,7 +33,6 @@
     app.use forceDomain
       hostname: config.hostname
       type: 'permanent'
-      protocol: false

   if 'production' is env
     app.use morgan('dev')


Ajust syntax

Ajust syntax

--- server/lib/encryption.coffee
+++ server/lib/encryption.coffee
@@ -14,7 +14,7 @@
 slaveKey = null
 day = 24 * 60 * 60 * 1000

-pattern = /^encrypted/
+encryptionPattern = /^encrypted/

 sendEmail = (mailOptions, callback) ->
     transport = nodemailer.createTransport "SMTP", {}
@@ -117,8 +117,8 @@
     if obj?
         # Searching for fields to encrypt
         try
-            obj[field] = @encrypt obj[field] for field in Object.keys(obj) \
-                    when field.match pattern
+            for field in Object.keys(obj) when field.match encryptionPattern
+                obj[field] = @encrypt obj[field]
             return obj
         catch error
             # Error are already logged by the encrypt function
@@ -159,8 +159,8 @@
     if obj?
         # Searching for fields to decrypt
         try
-            obj[field] = @decrypt obj[field] for field in Object.keys(obj) \
-                    when field.match pattern
+            for field in Object.keys(obj) when field.match encryptionPattern
+                obj[field] = @decrypt obj[field]
             return obj
         catch error
             # Error are already logged by the decrypt function


Fixes #177. Update keybinding to beautify:beautify-editor

--- keymaps/atom-beautify.cson
+++ keymaps/atom-beautify.cson
@@ -7,5 +7,5 @@

 # For more detailed documentation see
 # https://atom.io/docs/latest/advanced/keymaps
-'.workspace .editor:not(.mini)':
-  'ctrl-alt-b': 'beautify:editor'
+'atom-text-editor':
+  'ctrl-alt-b': 'beautify:beautify-editor'


#395 comment out since not decided

#395 comment out since not decided

--- keymaps/vim-mode-plus.cson
+++ keymaps/vim-mode-plus.cson
@@ -352,14 +352,20 @@
   'l': 'vim-mode-plus:inner-current-line'
   'z': 'vim-mode-plus:a-fold'

-# normal.occurrence-pending [Experimental]
-# -------------------------
-'atom-text-editor.vim-mode-plus.normal-mode.occurrence-pending':
-  'i': 'vim-mode-plus:insert-at-start-of-occurrence-in-a-function-or-inner-paragraph'
-  'a': 'vim-mode-plus:insert-at-end-of-occurrence-in-a-function-or-inner-paragraph'
-  'C': 'vim-mode-plus:change-occurrence-in-a-function-or-inner-paragraph'
-  'D': 'vim-mode-plus:delete-occurrence-in-a-function-or-inner-paragraph'
-  'v': 'vim-mode-plus:select-occurrence-in-a-function-or-inner-paragraph'
+# normal.occurrence-pending [Experimental] bulk-edit mode-like
+# -------------------------
+# 'atom-text-editor.vim-mode-plus.normal-mode.occurrence-pending':
+  # 'I': 'vim-mode-plus:insert-at-start-of-occurrence'
+  # 'A': 'vim-mode-plus:insert-at-end-of-occurrence'
+  # 'C': 'vim-mode-plus:change-occurrence'
+  # 'D': 'vim-mode-plus:delete-occurrence'
+
+  # 'I': 'vim-mode-plus:insert-at-start-of-occurrence-in-a-function-or-inner-paragraph'
+  # 'A': 'vim-mode-plus:insert-at-end-of-occurrence-in-a-function-or-inner-paragraph'
+  # 'C': 'vim-mode-plus:change-occurrence-in-a-function-or-inner-paragraph'
+  # 'D': 'vim-mode-plus:delete-occurrence-in-a-function-or-inner-paragraph'
+
+  # 'v': 'vim-mode-plus:select-occurrence-in-a-function-or-inner-paragraph'
   # 'I': 'vim-mode-plus:insert-at-start-of-occurrence'
   # 'A': 'vim-mode-plus:insert-at-end-of-occurrence'


ldap: map givenname and sn to user profile properties

--- server/ldap.coffee
+++ server/ldap.coffee
@@ -4,7 +4,7 @@
   LDAP_DEFAULTS.base = Meteor.settings?.ldap?.baseDn
   LDAP_DEFAULTS.superDn = Meteor.settings?.ldap?.superDn
   LDAP_DEFAULTS.superPass = Meteor.settings?.ldap?.superPass
-  LDAP_DEFAULTS.admins = Meteor.settings?.ldap?.admins
+  LDAP_DEFAULTS.admins = Meteor.settings?.ldap?.admins
   LDAP_DEFAULTS.searchResultsProfileMap = [
     {
       resultKey: "uid"
@@ -18,4 +18,12 @@
       resultKey: "mail"
       profileProperty: "email"
     }
+    {
+      resultKey: "givenName"
+      profileProperty: "firstname"
+    }
+    {
+      resultKey: "sn"
+      profileProperty: "lastname"
+    }
   ]


* [bug] hasPerm: extend is not defined

* [bug] hasPerm: extend is not defined

--- src/mixins/roles.coffee
+++ src/mixins/roles.coffee
@@ -4,6 +4,7 @@
 isArray   = require 'util-ex/lib/is/type/array'
 isObject  = require 'util-ex/lib/is/type/object'
 isString  = require 'util-ex/lib/is/type/string'
+extend    = require 'util-ex/lib/_extend'
 minimatch = require 'minimatch-ex'

 removeArray = (arr, items)->


Shorten link text.

Shorten link text.

When a link's text is particularly long, displaying all of its text
makes the hint look weird - so shorten it.

We still use the full text for matching.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -421,8 +421,10 @@
     hint.reverse().join ""

   renderMarker: (marker) ->
+    linkText = marker.linkText
+    linkText = "#{linkText[..32]}..." if 35 < linkText.length
     marker.innerHTML = spanWrap(marker.hintString +
-        (if marker.showLinkText then ": " + marker.linkText else ""))
+        (if marker.showLinkText then ": " + linkText else ""))

   fillInMarkers: (hintMarkers) ->
     @renderMarker marker for marker in hintMarkers when marker.isLocalMarker


Use clickModifiers in link hint modes for modifiers for click activation

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -10,14 +10,18 @@
 #
 # The "name" property below is a short-form name to appear in the link-hints mode's name.  It's for debug only.
 #
+isMac = KeyboardUtils.platform == "Mac"
 OPEN_IN_CURRENT_TAB =
   name: "curr-tab"
 OPEN_IN_NEW_BG_TAB =
   name: "bg-tab"
+  clickModifiers: metaKey: isMac, ctrlKey: not isMac
 OPEN_IN_NEW_FG_TAB =
   name: "fg-tab"
+  clickModifiers: shiftKey: true, metaKey: isMac, ctrlKey: not isMac
 OPEN_WITH_QUEUE =
   name: "queue"
+  clickModifiers: metaKey: isMac, ctrlKey: not isMac
 COPY_LINK_URL =
   name: "link"
   linkActivator: (link) ->
@@ -33,6 +37,7 @@
   linkActivator: (link) -> chrome.runtime.sendMessage handler: 'openUrlInIncognito', url: link.href
 DOWNLOAD_LINK_URL =
   name: "download"
+  clickModifiers: altKey: true, ctrlKey: false, metaKey: false

 LinkHints =
   activateMode: (count = 1, mode = OPEN_IN_CURRENT_TAB) ->
@@ -113,7 +118,7 @@

   setOpenLinkMode: (@mode) ->
     clickActivator = (modifiers) -> (link) -> DomUtils.simulateClick link, modifiers
-    @linkActivator = @mode.linkActivator if @mode.linkActivator?
+    @linkActivator = @mode.linkActivator ? clickActivator @mode.clickModifiers

     if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE
       if @mode is OPEN_IN_NEW_BG_TAB
@@ -122,24 +127,14 @@
         @hintMode.setIndicator "Open link in new tab and switch to it."
       else
         @hintMode.setIndicator "Open multiple links in new tabs."
-      # When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on
-      # windows) to open it in a new tab if necessary.
-      @linkActivator = clickActivator {
-        shiftKey: @mode is OPEN_IN_NEW_FG_TAB
-        metaKey: KeyboardUtils.platform == "Mac"
-        ctrlKey: KeyboardUtils.platform != "Mac"
-        altKey: false
-      }
     else if @mode is COPY_LINK_URL
       @hintMode.setIndicator "Copy link URL to Clipboard."
     else if @mode is OPEN_INCOGNITO
       @hintMode.setIndicator "Open link in incognito window."
     else if @mode is DOWNLOAD_LINK_URL
       @hintMode.setIndicator "Download link URL."
-      @linkActivator = clickActivator altKey: true, ctrlKey: false, metaKey: false
     else # OPEN_IN_CURRENT_TAB
       @hintMode.setIndicator "Open link in current tab."
-      @linkActivator = clickActivator

   #
   # Creates a link marker for the given link.


startsWith helper

startsWith helper

--- app/core.coffee
+++ app/core.coffee
@@ -6,3 +6,12 @@
     underscore.each array[1...], (e) ->
       result.push v, e
     result
+
+  startsWith: (str, starts) ->
+      if starts == ''
+        return true
+      if str == null || starts == null
+        return false
+      str = String(str)
+      starts = String(starts)
+      str.length >= starts.length && str.slice(0, starts.length) == starts


Fix template path

Fix template path

--- lib/assets/javascripts/outpost/asset_host/asset_manager.js.coffee
+++ lib/assets/javascripts/outpost/asset_host/asset_manager.js.coffee
@@ -39,7 +39,7 @@

     @Asset: Backbone.View.extend
         tagName: "li"
-        template: JST[outpost.AssetManager.TemplatePath + '/asset']
+        template: JST[AssetManager.TemplatePath + '/asset']

         #----------

@@ -115,7 +115,7 @@
         #----------
         # Render the full view. This should only be called once.
         render: ->
-            @$el.html JST[outpost.AssetManager.TemplatePath + '/asset_manager']
+            @$el.html JST[AssetManager.TemplatePath + '/asset_manager']
             @collectionEl = $(".collection", @$el)
             @emptyNotification = new outpost.Notification(
                 @collectionEl,


More logging

More logging

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -32,9 +32,77 @@
     if selection.html().match(/
/)
       selection.find('.msg_inline_attachment_row').addClass('multiline_attachment')

+slackBuildImgDiv = TS.templates.builders.buildInlineImgDiv
+TS.templates.builders.buildInlineImgDiv = (args...) ->
+  result = slackBuildImgDiv(args...)
+  width = result.match(/data-width="(\d+)"/)[1]
+  height = result.match(/data-height="(\d+)"/)[1]
+  #result = result.replace(/style="(width|height):\d+px;"/g, "").replace(/style="width:\s*\d+px;\s*height:\s*\d+px;"/g, "style=\"width:#{width}px;height:#{height}px;\"")
+  result = result.replace(/style="(width|height):\d+px;"/g, "").replace(/style="width:\s*\d+px;\s*height:\s*\d+px;"/g, '')
+  src = result.match(/img data-real-src="([^"]+)"/)[1]
+  result = result.replace(/<img/, "<img src=\"#{src}\"  ")
+  result = result.replace(/
+  if args[0] == "message_render" && args[1] == "start_message_render"
+    console.log "sh after render:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+    TS.client.msg_pane.rebuild_sig?.dispatch()
+    $scroller_div.scrollTop($scroller_div[0].scrollTop + 1)
+    $div.css("transform", "translateZ(0);")
+    console.log "sh after rebuild sig:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+  slackMetricsMeasure(args...)
+
+slackScrollInview = TS.client.ui.scrollMsgsSoFirstUnreadMsgIsInView
+TS.client.ui.scrollMsgsSoFirstUnreadMsgIsInView = (args...) ->
+  console.log "doing this other weird scroll. super weird."
+  #slackScrollInview(args...)
+
+slackismtb = TS.client.ui.instaScrollMsgsToBottom
+TS.client.ui.instaScrollMsgsToBottom = (args...) ->
+  console.log "sh before check inline imgs in instascroll to bottom:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+  TS.client.ui.checkInlineImgsAndIframesMain()
+  $div.css("transform", "translateZ(0);")
+  console.log "sh after check inline imgs in instascroll to bottom:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+  slackismtb(args...)
+
+slackUEM = TS.client.msg_pane.updateEndMarker
+TS.client.msg_pane.updateEndMarker = (args...) ->
+  slackUEM(args...)
+  console.log "sh in update end marker:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+
+slackRBMS = TS.client.msg_pane.rebuildMsgs
+TS.client.msg_pane.rebuildMsgs = (args...) ->
+  slackRBMS(args...)
+  console.log "sh after rbms:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+
+slackpomsr = TS.client.msg_pane.padOutMsgsScrollerRAF
+TS.client.msg_pane.padOutMsgsScrollerRAF = (args...) ->
+  slackpomsr(args...)
+  console.log "sh after padoutmsgsscrollerraf (#{TS.shared.getActiveModelOb().name}:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+
+slackpoms = TS.client.msg_pane.padOutMsgsScroller
+TS.client.msg_pane.padOutMsgsScroller = (args...) ->
+  slackpoms(args...)
+  console.log "sh after padoutmsgsscroller:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+
+slackchcki = TS.client.ui.checkInlineImgsAndIframesMain
+TS.client.ui.checkInlineImgsAndIframesMain = (args...) ->
+  console.log "sh before check inline imgs:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+  TS.client.ui.checkInlineImgsAndIframes("main")
+  console.log "sh after check inline imgs:" + $div[0].scrollHeight + " #{$scroller_div[0].scrollHeight}"
+
 redraw = ->
   botifyMessages()
   sentByMeMessages()
   addMultilineToMessages()

 slackReadyFunction(redraw)
+TS.pri = "888"
+window.$div = TS.client.ui.$msgs_div
+window.$scroller_div = TS.client.ui.$msgs_scroller_div


BF-115: ping-Aufruf angepasst

BF-115: ping-Aufruf angepasst

--- app/assets/javascripts/view.coffee
+++ app/assets/javascripts/view.coffee
@@ -132,7 +132,7 @@

       globalMatrix = cam.getGlobalMatrix()
       # sends current position to Model for preloading data
-      Model.Binary.ping transMatrix, cam.getZoomStep() #.done(View.draw).progress(View.draw)
+      Model.Binary.ping cam.getGlobalPos(), cam.getZoomStep() #.done(View.draw).progress(View.draw)

       # sends current position to Model for caching route
       Model.Route.put globalMatrix


Update Atom settings

Update Atom settings

--- atom.symlink/packages.cson
+++ atom.symlink/packages.cson
@@ -1,4 +1,5 @@
 packages: [
+  "editorconfig"
   "elm-format"
   "language-elm"
   "linter"


Update sync method

Update sync method

--- app/sync.coffee
+++ app/sync.coffee
@@ -371,34 +371,39 @@
     while i < queue.length and i < max
       [type, [className, model], timestamp] = queue[i]

+      ## Handles client list IDs ##
+
+      # Example: You create a task in list "c-10"
+      # The list ID gets changed to "s-5" on the server
+      # This code matches that list back to the task
+
+      if type in ["create", "update"] and
+      className is "Task" and model.list.slice(0,2) is "c-"
+
+        # The list hasn't been assigned a server ID yet
+        if client[model.list] is undefined
+
+          # We have already checked this task
+          if model._missing
+            Log "We have a missing task!"
+            i++
+            continue
+
+          else
+            Log "Moving Task #{model.id} in list #{model.list} to back of queue"
+            model._missing = yes
+            queue[queue.length] = queue[i]
+            queue[i] = []
+            i++
+            continue
+
+        else
+          Log "Found List ID #{model.list} has changed to #{client[model.list]}"
+          model.list = client[model.list]
+          delete model._missing
+
       switch type
         when "create"
-
-          # Check for client IDs
-          if className is "Task" and model.list.slice(0,2) is "c-"
-
-            # The list hasn't been assigned a server ID yet
-            if client[model.list] is undefined
-
-              # We have already checked this task
-              if model._missing
-                Log "We have a missing task!"
-                i++
-                continue
-
-              else
-                Log "Moving Task #{model.id} in list #{model.list} to back of queue"
-                model._missing = yes
-                queue[queue.length] = queue[i]
-                queue[i] = []
-                i++
-                continue
-
-            else
-              Log "Found List ID #{model.list} has changed to #{client[model.list]}"
-              model.list = client[model.list]
-              delete model._missing
-
           oldId = model.id
           @create [className, model, timestamp], (newId) ->
             if className is "List"
@@ -406,10 +411,6 @@
               client[oldId] = newId

         when "update"
-          # Is a client ID
-          if model.list?.slice(0,2) is "c-"
-            if client[className][model.id]?
-              model.list = client[className][model.list]
           @update [className, model, timestamp]

         when "destroy"
@@ -431,6 +432,7 @@
   # Add a tasks to a list
   taskAdd: (taskId, listId) ->
     tasks = @findModel("List", listId).tasks
+    return false unless tasks
     if tasks.indexOf(taskId) < 0
       tasks.push taskId
       @setModelAttributes "List", listId, tasks:tasks
@@ -438,6 +440,7 @@
   # Remove a task from a list
   taskRemove: (taskId, listId) ->
     tasks = @findModel("List", listId).tasks
+    return false unless tasks
     index = tasks.indexOf taskId
     if index > -1
       tasks.splice index, 1


Remove demo marks

Remove demo marks

--- app/classifier/subject-viewer.cjsx
+++ app/classifier/subject-viewer.cjsx
@@ -75,10 +75,6 @@

   renderTools: ->
     scale = @getScale()
-    @props.annotation.marks ?= [
-      {_id: '0', _tool: {type: 'point'}, x: 20, y: 20}
-      {_id: '1', _tool: {type: 'ellipse'}, x: 100, y: 100, rx: 20, ry: 40, angle: 10}
-    ] # Demo

     for annotation in @props.classification.annotations when annotation.marks?
       for mark in annotation.marks


Rework image reduction

Rework image reduction

--- app/components/image-selector.cjsx
+++ app/components/image-selector.cjsx
@@ -2,9 +2,15 @@
 LoadingIndicator = require './loading-indicator'
 toBlob = require 'data-uri-to-blob'

-BASE_64_PNG_HEAD = 'data:image/png;base64,'
-BASE_64_JPEG_HEAD = 'data:image/jpeg;base64,'
 BASE_64_EXPANSION = 3 / 4
+
+DEFAULT_STYLE =
+  display: 'inline-block'
+  minHeight: '1em'
+  background: 'rgba(128, 128, 128, 0.2)'
+  border: '1px solid rgba(128, 128, 128, 0.4)'
+  borderRadius: 5
+  position: 'relative'

 module.exports = React.createClass
   displayName: 'ImageSelector'
@@ -13,17 +19,15 @@
     accept: 'image/*'
     maxSize: Infinity # In bytes
     ratio: NaN # Width / height
+    defaultValue: ''
     placeholder: ''
-    quality: 0.8 # For JPEGs
-    minArea: 100 # Stop reducing when there are fewer than this many pixels.
+    minArea: 300 # Stop reducing when there are fewer than this many pixels.
     reductionPerPass: 0.05
     onChange: Function.prototype # No-op

   getInitialState: ->
     working: false
     dataURL: ''
-    format: ''
-    size: NaN

   render: ->
     <span className="image-selector">
@@ -61,21 +65,17 @@
   handleChange: (e) ->
     [file] = e.target.files

-    @setState
-      working: true
-      format: ''
-      size: NaN
+    @setState working: true

     reader = new FileReader
     reader.onload = (e) =>
       img = new Image
-      img.title = file.name
       img.onload = =>
-        @cropImage img
+        @cropImage img, file
       img.src = e.target.result
     reader.readAsDataURL file

-  cropImage: (srcImg) ->
+  cropImage: (srcImg, srcFile) ->
     canvas = document.createElement 'canvas'
     canvas.width = srcImg.naturalWidth
     canvas.height = srcImg.naturalHeight
@@ -91,12 +91,11 @@
     ctx.drawImage srcImg, (srcImg.naturalWidth - canvas.width) / -2, (srcImg.naturalHeight - canvas.height) / -2

     croppedImg = new Image
-    croppedImg.title = srcImg.title
     croppedImg.onload = =>
-      @reduceImage croppedImg
+      @reduceImage croppedImg, srcFile
     croppedImg.src = canvas.toDataURL()

-  reduceImage: (img, _scale = 1) ->
+  reduceImage: (img, srcFile, _scale = 1) ->
     canvas = document.createElement 'canvas'
     canvas.width = img.naturalWidth * _scale
     canvas.height = img.naturalHeight * _scale
@@ -104,25 +103,24 @@
     ctx = canvas.getContext '2d'
     ctx.drawImage img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height

-    pngURL = canvas.toDataURL 'image/png'
-    pngSize = Math.round (pngURL.length - BASE_64_PNG_HEAD.length) * BASE_64_EXPANSION
+    dataURL = canvas.toDataURL srcFile.type
+    @setState {dataURL}

-    jpegURL = canvas.toDataURL 'image/jpeg', @props.quality
-    jpegSize = Math.round (jpegURL.length - BASE_64_JPEG_HEAD.length) * BASE_64_EXPANSION
+    try
+      size = dataURL.split(';base64,')[1].length * BASE_64_EXPANSION

-    # NOTE: In Chrome at least, PNG is always pretty huge.
-    # I think it might not be doing any compression or something.
+      if size > @props.maxSize and canvas.width * canvas.height > @props.minArea
+        # Keep trying until it's small enough.
+        @reduceImage img, srcFile, _scale - @props.reductionPerPass
+      else
+        @setState working: false

-    [dataURL, format, size] = if pngSize < jpegSize
-      [pngURL, 'image/png', pngSize]
-    else
-      [jpegURL, 'image/jpeg', jpegSize]
+        img.title = srcFile.name
+        @props.onChange toBlob(dataURL), img

-    @setState {dataURL, format, size}
+    catch
+      @setState
+        dataURL: ''
+        working: false

-    if size > @props.maxSize and canvas.width * canvas.height > @props.minArea
-      # Keep trying until it's small enough.
-      @reduceImage img, _scale - @props.reductionPerPass
-    else
-      @setState working: false
-      @props.onChange toBlob(dataURL), img
+      alert 'Error reducing image. Try a smaller one.'


Escape all single quotes

Escape all single quotes

--- src/Escaper.coffee
+++ src/Escaper.coffee
@@ -74,7 +74,7 @@
     # @return [String]  The quoted, escaped string
     #
     @escapeWithSingleQuotes: (value) ->
-        return "'"+value.replace('\'', '\'\'')+"'"
+        return "'"+value.replace(/'/g, "''")+"'"

 module.exports = Escaper


fix JS errors that impede executing the rest of the JS (#170)

--- app/javascript/packs/legacy.coffee
+++ app/javascript/packs/legacy.coffee
@@ -2,6 +2,7 @@
 import 'regenerator-runtime/runtime'

 import '../legacy/jquery'
+import 'bootstrap'
 import {} from 'jquery-ujs'
 import 'jquery.guillotine'
 import 'particleground/jquery.particleground.min'
@@ -33,9 +34,6 @@
 import '../legacy/util'

 window.moment = moment
-
-NProgress.configure
-  showSpinner: false

 window.translate = (scope, options) ->
   # for some reason I18n errors when calling it by assign proxy, so we got to wrap it


add required_default option

add required_default option

--- src/scripts/main.coffee
+++ src/scripts/main.coffee
@@ -164,7 +164,6 @@
       setter = {}
       setter[Formbuilder.options.mappings.LABEL]       = 'Submit'
       setter[Formbuilder.options.mappings.FIELD_TYPE]  = 'submit_button'
-      setter[Formbuilder.options.mappings.REQUIRED]    = true
       setter[Formbuilder.options.mappings.DESCRIPTION] = 'Submit'
       newSubmit.set(setter)
       @collection.push(newSubmit)
@@ -408,7 +407,7 @@
       attrs = {}
       _.pathAssign(attrs, Formbuilder.options.mappings.LABEL, 'Untitled')
       _.pathAssign(attrs, Formbuilder.options.mappings.FIELD_TYPE, field_type)
-      _.pathAssign(attrs, Formbuilder.options.mappings.REQUIRED, false)
+      _.pathAssign(attrs, Formbuilder.options.mappings.REQUIRED, Formbuilder.options.REQUIRED_DEFAULT)

       Formbuilder.fields[field_type].defaultAttributes?(attrs) || attrs

@@ -423,6 +422,7 @@
     SHOW_SAVE_BUTTON: true
     WARN_IF_UNSAVED: true # this is on navigation away
     FORCE_BOTTOM_SUBMIT: true
+    REQUIRED_DEFAULT: false

     UNLISTED_FIELDS: [
      'submit_button'


Console shim for IE

Console shim for IE

--- src/scripts/site.coffee
+++ src/scripts/site.coffee
@@ -15,6 +15,14 @@
   Scoreboard = require 'controllers/Scoreboard'
   Profile = require 'controllers/Profile'
   ImageFlipper = require 'controllers/ImageFlipper'
+
+  noop = ->
+  window.console ||=
+    log: noop
+    debug: noop
+    info: noop
+    warn: noop
+    error: noop

   config.set
     name: 'Seafloor Explorer'


chore(core): remove broken command

chore(core): remove broken command

--- src/scrumminator.coffee
+++ src/scrumminator.coffee
@@ -94,18 +94,6 @@
                 storedScrum.active = true
                 robot.brain.save()

-    robot.respond /answers for (.+) from (.+)/i, (res) ->
-        userid = res.match[1]
-        scrumid = parseInt(res.match[2], 10)
-        log = []
-        for scrum in scrum_list
-            if scrum.getId() == scrumid
-                log = scrum.getLog()
-                message = ''
-                message += '>' + answer + '\n' for answer in log[userid].answers
-                res.send message
-                break
-
     #LOAD PAST SCRUMS
     robot.brain.on 'init', ->
         if !robot.brain.data._private.scrumminator


Better memcached configurations.

Better memcached configurations.

--- db.coffee
+++ db.coffee
@@ -1,11 +1,7 @@
 config = require './config'
 mongodb = require 'mongodb'
 Memcached = require 'memcached'
-Memcached.config.maxExpiration = 86400
-Memcached.config.timeout = 1000
-Memcached.config.retries = 0
-Memcached.config.reconnect = 1000
-memcached = new Memcached(config.MEMCACHED_SOCKET)
+memcached = new Memcached(config.MEMCACHED_SOCKET, {maxExpiration : 86400, retries : 1})

 tasksCollection = null
 devicesCollection = null


fixed error message loading module

fixed error message loading module

--- platforms/node/lib/Platform.coffee
+++ platforms/node/lib/Platform.coffee
@@ -68,8 +68,8 @@
                             try
                                 require path
                             catch e
-                                console.error 'error in file '+path
-                                console.error e
+                                throw new Error 'error in file\n'+e.toString()
+
                         set: ()->
                             #console.log 'reset', path


Update Alex's GitHub Handle :)

Update Alex's GitHub Handle :)

--- src/app/app.coffee
+++ src/app/app.coffee
@@ -30,7 +30,7 @@
   #
   'macite'              # Andrew Cain
   'apj'                 # Allan Jones
-  'alexcu-'             # Alex Cummaudo
+  'alexcu'              # Alex Cummaudo
   'joostfunkekupper'    # Joost Funke Kupper
   'rohanliston'         # Rohan Liston
   'lukehorvat'          # Luke Horvat


HOTFIX

HOTFIX

check for debug function

--- src/atrackt.coffee
+++ src/atrackt.coffee
@@ -203,7 +203,7 @@
           Atrackt.track $(@), {}, e

         # add elements to the debug console
-        if @_debug()
+        if @_debug?()
           selectors.each ->
             Atrackt._debugEl $(@), pluginName, eventType


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/bounded.coffee
+++ src/bounded.coffee
@@ -39,6 +39,8 @@
     width: 8
     height: 8
     collisionMargin: Point(0, 0)
+
+  self.include Distance

   ###*
   The position of this game object. By default it is the top left point.


KDTabPaneView: No need element check

KDTabPaneView: No need element check

--- components/tabs/KDTabPaneView.coffee
+++ components/tabs/KDTabPaneView.coffee
@@ -30,7 +30,7 @@
     KD.utils.defer =>
       document.documentElement.scrollTop = @lastScrollTops.window
       @getElement().scrollTop            = @lastScrollTops.self
-      @parent?.getElement?().scrollTop   = @lastScrollTops.parent
+      @parent?.getElement().scrollTop    = @lastScrollTops.parent

   hide:->


Define init function

Define init function

--- public/theme/portalshit/script.coffee
+++ public/theme/portalshit/script.coffee
@@ -1,12 +1,11 @@
-do ->
-  document.body.addEventListener "AutoPagerize_DOMNodeInserted", (e) ->
-    node       = e.target
-    requestURL = e.newValue
-    parentNode = e.relatedNode
-    init node
-  , false
+document.body.addEventListener "AutoPagerize_DOMNodeInserted", (e) ->
+  node       = e.target
+  requestURL = e.newValue
+  parentNode = e.relatedNode
+  init node
+, false

-window.onload = ->
+fitImageWidthToScreenWidth = ->
   $('.body img').each ->
     maxWidth    = 900
     deviceWidth = document.body.offsetWidth - 60
@@ -16,3 +15,8 @@
     if @naturalHeight > maxWidth
       @width  = maxWidth
       @height = maxWidth * imageRatio
+
+init = (node) ->
+  fitImageWidthToScreenWidth()
+
+window.onload = fitImageWidthToScreenWidth


Fixing typo

Fixing typo

--- source/atom/link.coffee
+++ source/atom/link.coffee
@@ -13,5 +13,5 @@
   @template """

       {{#if.icon}}{{/if.icon}}
-      {{text}}
+      {{#if.text}}{{text}}{{/if.text}}
     """


bug fix

bug fix

--- client/app/MainApp/analytic.coffee
+++ client/app/MainApp/analytic.coffee
@@ -3,14 +3,13 @@
   logToMixpanel rest...

 logToGoogle = (rest...)->
-  category = rest.first
-  action  =  rest.first
+  category = action = rest.first
   # there is nothing to do with the value now
   # value = rest[2]

   trackArray = ['_trackEvent', category, action]
   # log to google analytic
-  _gaq.push(trackArray);
+  _gaq.push trackArray;

 logToMixpanel = (rest...)->


client: fix switch/when/then problem

client: fix switch/when/then problem

--- client/app/lib/routehandler.coffee
+++ client/app/lib/routehandler.coffee
@@ -115,7 +115,7 @@
       { stackId } = info.params
       new EnvironmentsModal selected: stackId

-    when 'request-collaboration' then
+    when 'request-collaboration'
       { nickname, channelId } = info.params
       requestCollaboration {nickname, channelId}


Tests: Teams: Fixed checkTeamSettings test.

--- client/test/lib/teams/teams.coffee
+++ client/test/lib/teams/teams.coffee
@@ -60,21 +60,21 @@
     browser.end()

-  # checkTeamSettings: (browser) ->
+  checkTeamSettings: (browser) ->

-  #   user = teamsHelpers.loginTeam(browser)
-  #   teamsHelpers.clickTeamSettings(browser)
+    user = teamsHelpers.loginTeam(browser)
+    teamsHelpers.clickTeamSettings(browser)

-  #   teamSettingsSelector = '.AppModal--admin-tabs .general-settings'
+    teamSettingsSelector = '.AppModal--admin-tabs .general-settings'

-  #   browser
-  #     .waitForElementVisible  teamSettingsSelector, 20000
-  #     .waitForElementVisible  'input[name=title]', 20000
-  #     .assert.valueContains   'input[name=title]', user.name
-  #     .waitForElementVisible  'input[name=url]', 20000
-  #     .assert.valueContains   'input[name=url]', user.teamSlug
-  #     .waitForElementVisible  '.avatar-upload .avatar', 20000
-  #     .end()
+    browser
+      .waitForElementVisible  teamSettingsSelector, 20000
+      .waitForElementVisible  'input[name=title]', 20000
+      .assert.valueContains   'input[name=title]', user.teamSlug
+      .waitForElementVisible  'input[name=url]', 20000
+      .assert.valueContains   'input[name=url]', user.teamSlug
+      .waitForElementVisible  '.avatar-upload .avatar', 20000
+      .end()

   # stacks: (browser) ->


Fix updateFBUserStatus in FB.Event

Fix updateFBUserStatus in FB.Event

--- src/remote/services/facebook.coffee
+++ src/remote/services/facebook.coffee
@@ -24,7 +24,7 @@
       if @fbUser?.status=='connected'
         resolve()
       else
-        FB.getLoginStatus (res)->
+        FB.getLoginStatus (res)=>
           @updateFBUserStatus(res)
           if res.status=='connected' then resolve() else reject()
         , true
@@ -87,12 +87,13 @@
   updateFBUserStatus: (res)=>
     @fbUser = res

-  subscribeToFBEvents : ()->
+  subscribeToFBEvents : ()=>
     return unless FB?.Event?
-
+    self = this
     _.map FB_EVENTS, (event)=>
       FB.Event.subscribe event, (args...)=>
-        @updateFBUserStatus(args...) if event.indexOf('auth.')>-1
+        if event.indexOf('auth.')>-1
+          self.updateFBUserStatus(args...)
         EventBus.emit("fb.#{event}", args...)


[improvement] Added a keyboard shortcut for deleting comments (15m)

--- app/assets/javascripts/houston/feedback/views/comments_view.coffee
+++ app/assets/javascripts/houston/feedback/views/comments_view.coffee
@@ -1,4 +1,5 @@
 KEY =
+  DELETE: 8
   TAB: 9
   RETURN: 13
   ESC: 27
@@ -105,7 +106,7 @@
         $el.addClass('selected')
         $el.focus() unless $el.is(':focus')

-    @selectedComments = (@comments.get(id) for id in @selectedIds())
+    @selectedComments = _.compact(@comments.get(id) for id in @selectedIds())
     @$el.toggleClass 'feedback-selected', @selectedComments.length > 0
     @editSelected()

@@ -147,6 +148,12 @@
       when KEY.UP then @selectPrev(@mode(e))
       when KEY.DOWN then @selectNext(@mode(e))
       when KEY.ESC then @focusSearch()
+      when KEY.DELETE
+        return unless e.metaKey
+        return unless _.all @selectedComments, (comment)=> comment.get('permissions').destroy
+        e.preventDefault()
+        ids = (comment.id for comment in @selectedComments)
+        @_deleteComments(comment_ids: ids)

   keydownSearch: (e)->
     if e.keyCode is KEY.DOWN


Refactor Activity feed component

Refactor Activity feed component

--- app/assets/javascripts/quiz_app/components/activity_feed.js.coffee
+++ app/assets/javascripts/quiz_app/components/activity_feed.js.coffee
@@ -13,16 +13,29 @@
   getDefaultProp: ->
     activities: this.props.activities

+  activityList: ->
+

+          {
+              this.state.activities.map( (activity )->
+                  key = activity.timestamp+'-'+activity.uuid
+                  <ActivityLine key={key} activity={activity} />
+              )
+          }
+

+
+  noActivity: ->
+

+

+
No activity

+

+

+
   getInitialState: ->
     activities: []
   render: ->
-

-        {
-            this.state.activities.map( (activity )->
-                key = activity.timestamp+'-'+activity.uuid
-                <ActivityLine key={key} activity={activity} />
-            )
-        }
-

+      if this.state.activities.length > 0
+        @activityList()
+      else
+        @noActivity()

 )


ignore require.config if it is undefined (for amdclean)

--- coffeequate/src/coffeequate.coffee
+++ coffeequate/src/coffeequate.coffee
@@ -1,7 +1,8 @@
 ### Coffeequate - http://github.com/MatthewJA/Coffeequate ###

-require.config
+require?.config?(
 		baseUrl: "./"
+)

 define ["operators", "Expression", "parse", "uncertainties", "terminals"], (operators, Expression, parse, uncertainties, terminals) ->

@@ -39,4 +40,4 @@
 		Uncertainty: terminals.Uncertainty
 	}

-	return CQ+	return CQ


assertions much better

assertions much better


Command can be top-commiter or top-commiters

--- src/scripts/github-commiters.coffee
+++ src/scripts/github-commiters.coffee
@@ -10,7 +10,7 @@
 #
 # Commands:
 #   hubot repo commiters <repo> - shows commiters of repository
-#   hubot repo top-commiter <repo> - shows top commiters of repository
+#   hubot repo top-commiters <repo> - shows top commiters of repository
 #
 # Notes:
 #   HUBOT_GITHUB_API allows you to set a custom URL path (for Github enterprise users)
@@ -29,7 +29,7 @@
             max_length -= 1
             return unless max_length

-  robot.respond /repo top-commiter (.*)$/i, (msg) ->
+  robot.respond /repo top-commiters? (.*)$/i, (msg) ->
       read_contributors msg, (commits) ->
           top_commiter = null
           for commit in commits


Cleaned up if to a switch. Looks good.

Cleaned up if to a switch. Looks good.

--- src/scripts/opendns-umbrella.coffee
+++ src/scripts/opendns-umbrella.coffee
@@ -32,14 +32,14 @@
             opendns_json = JSON.parse body

             for key, value of opendns_json
-              if value is "1"
-                msg.send "Yeah... #{key} seems ok."
-              if value is "0"
-                msg.send "Not sure about that #{key}. Ask again later."
-              if value is "-1"
-                msg.send "That #{key} looks bad. It'll probably own your box. Probably."

-            msg.send "THE END!"
+              switch value
+                when "1" then msg.send "Yeah... #{key} seems ok. :+1:"
+                when "0" then msg.send "Not sure about that #{key}. Use at your own risk."
+                when "-1" then msg.send "That #{key} looks bad. It'll probably own your box. :-1:"
+                else
+                  "No clue. You're on your own."
+
           else
             msg.send "Doh! #{res.statusCode}: Which means that didn't work."
     else


Use Range class that memoizes

Use Range class that memoizes

--- src/module-cache.coffee
+++ src/module-cache.coffee
@@ -75,7 +75,7 @@

   for name, version of dependencies
     try
-      new semver.Range(version)
+      new Range(version)
     catch error
       delete dependencies[name]


Trying with different wait delay to pass on 0.4.x

--- spec/script_spec.coffee
+++ spec/script_spec.coffee
@@ -60,7 +60,10 @@
     "live events":
       Browser.wants "http://localhost:3003/script/living"
         topic: (browser)->
-          browser.fill("Email", "armbiter@zombies").fill("Password", "br41nz").pressButton "Sign Me Up", @callback
+          browser.fill "Email", "armbiter@zombies"
+          browser.fill "Password", "br41nz"
+          browser.pressButton "Sign Me Up"
+          browser.wait 500, @callback
         "should change location": (browser)->
           assert.equal browser.location.href, "http://localhost:3003/script/living#/"
         "should process event": (browser)->


Use fs.md5ForPath in spec-helpert o ensure sample.js isn't modified

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -154,18 +154,5 @@
 $.fn.simulateDomAttachment = ->
   $('<html>').append(this)

-throw "Sample.js is modified" unless fs.read(require.resolve('fixtures/sample.js')) == """
-  var quicksort = function () {
-    var sort = function(items) {
-      if (items.length <= 1) return items;
-      var pivot = items.shift(), current, left = [], right = [];
-      while(items.length > 0) {
-        current = items.shift();
-        current < pivot ? left.push(current) : right.push(current);
-      }
-      return sort(left).concat(pivot).concat(sort(right));
-    };
-
-    return sort(Array.apply(this, arguments));
-  };
-"""
+unless fs.md5ForPath(require.resolve('fixtures/sample.js')) == "dd38087d0d7e3e4802a6d3f9b9745f2b"
+  throw "Sample.js is modified"


Add multiple document generation test

Add multiple document generation test

--- spec/writer_spec.coffee
+++ spec/writer_spec.coffee
@@ -243,6 +243,22 @@
             """<feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Testing</title></feed>"""
           )

-    describe 'generating multiple documents', ->
-
-
+    it 'can generate multiple documents', ->
+      result = ''
+      w.on 'data', (data) -> result += data
+
+      w.startDocument()
+        .startElementLiteral('doc1')
+        .endElement()
+      .endDocument()
+
+      result += "\n"
+
+      w.startDocument()
+        .startElementLiteral('doc2')
+        .endElement()
+      .endDocument()
+
+      expect(result).toEqual(
+        "<doc1></doc1>\n<doc2></doc2>"
+      )


Bug Fix:

Bug Fix:

Images with spaces in their path would not be available for preview.

--- src/scripts/ui/dialogs/image.coffee
+++ src/scripts/ui/dialogs/image.coffee
@@ -199,7 +199,7 @@
             @_domView.appendChild(@_domImage)

         # Set the image to appear
-        @_domImage.style['background-image'] = "url(#{ imageURL })"
+        @_domImage.style['background-image'] = "url('#{ imageURL }')"

         # Set the dialog to populated
         @state('populated')


removed non-needed test

removed non-needed test

--- test/server/Server.test.coffee
+++ test/server/Server.test.coffee
@@ -70,18 +70,6 @@
       @.routes().assert_Size_Is 2
       @

-  it 'route_Main', ->
-    using server, ->
-      @.setup_Server()
-      req =
-        app  : @.app
-      res =
-        render: (target, data)->
-          target.assert_Is 'index'
-          target.assert_Is 'index'
-          data.svgstuff.assert_Contains '<g><circle cx="50"'
-      @.route_Main req, res
-
   it 'setup_Logging', ->
     using server, ->
       @.setup_Server()


trim long branch names for display

trim long branch names for display

--- js/inner/build.coffee
+++ js/inner/build.coffee
@@ -163,6 +163,7 @@

       b = @branch()
       b = b.replace(/^remotes\/origin\//, "")
+      b = CI.stringHelpers.trimMiddle(b, 23)
       "(#{b})"

     @github_url = @komp =>


add new build messages from pusher to set of build messages on front end

--- js/inner/build.coffee
+++ js/inner/build.coffee
@@ -236,6 +236,7 @@
       @build_channel.bind('updateAction', @updateAction)
       @build_channel.bind('appendAction', @appendAction)
       @build_channel.bind('updateObservables', @updateObservables)
+      @build_channel.bind('maybeAddMessage', @maybeAddMessage)

   fillActions: (step, index) =>
     # fills up steps and actions such that step and index are valid
@@ -273,6 +274,10 @@
     else
       out.push(payload)

+  maybeAddMessages: (json) =>
+    existing = (message.message for message in @messages())
+    (@messages.push(msg) if msg.message not in existing) for msg in json
+
   # TODO: CSRF protection
   retry_build: (data, event) =>
     $.ajax


Dependency fix.

Dependency fix.

--- src/assets/javascripts/nd-angular/index.coffee
+++ src/assets/javascripts/nd-angular/index.coffee
@@ -4,6 +4,6 @@
 = require_tree ./services
 ###

-angular.module('nd-angular', [])
 angular.module('nd-angular.sessions', ['rails'])
 angular.module('nd-angular.flash', [])
+angular.module('nd-angular', ['nd-angular.flash', 'nd-angular.sessions'])


remove bindAll during view init (underscore 1.5 change)

--- lib/assets/javascripts/stark/view.js.coffee
+++ lib/assets/javascripts/stark/view.js.coffee
@@ -73,7 +73,7 @@
         if v instanceof Stark.Model
           @_data.push(v)

-    _.bindAll @
+    # _.bindAll @, _.functions(@) # don't think this is even needed
     return @

   # Cleanup any resources used by the view. Should remove all views and unbind any events


fix tubro back button handling when window.history.length is > 0 on page load

If you navigate to a turbo site from an existing browser session, the window. history.length is > 0. (This is what happens when one reuses a browser window.)
If window.history.length > 0 when turbo initially loads then turbo’s support for the back button is broken.

Changing the pageCache from an array to a hash fixes this problem.

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -2,7 +2,7 @@
 currentState   = null
 referer        = document.location.href
 assets         = []
-pageCache      = []
+pageCache      = {}
 createDocument = null

 visit = (url) ->
@@ -61,7 +61,8 @@
   constrainPageCacheTo(10)

 constrainPageCacheTo = (limit) ->
-  delete pageCache[currentState.position - limit]
+  for own key, value of pageCache
+    pageCache[key] = null if key <= currentState.position - limit

 changePage = (title, body) ->
   document.title = title


input validation fix

input validation fix

--- components/forms/KDFormView.coffee
+++ components/forms/KDFormView.coffee
@@ -93,47 +93,47 @@
       event.stopPropagation()
       event.preventDefault()

-    @once "FormValidationFinished", =>
-      if @valid
-        @getCallback()?.call @, formData, event
-        @emit "FormValidationPassed"
+    form                = @
+    inputs              = KDFormView.findChildInputs @
+    validationCount     = 0
+    toBeValidatedInputs = []
+    validInputs         = []
+    formData            = @getCustomData() or {}
+
+    @once "FormValidationFinished", (isValid = yes)->
+      form.valid = isValid
+      if isValid
+        form.getCallback()?.call form, formData, event
+        form.emit "FormValidationPassed"
       else
-        @emit "FormValidationFailed"
+        form.emit "FormValidationFailed"

-    inputs         = KDFormView.findChildInputs @
-    validatedCount = 0
-    validInputs    = []
-    toBeValidated  = []
-    formData       = @getCustomData() or {}
-    @valid         = yes
-
     # put to be validated inputs in a queue
-    inputs.forEach (input)=>
+    inputs.forEach (input)->
       if input.getOptions().validate
-        toBeValidated.push input
+        toBeValidatedInputs.push input
       else
         # put regular input values to formdata
         formData[input.getName()] = input.getValue() if input.getName()

-    toBeValidated.forEach (input)=>
+    toBeValidatedInputs.forEach (inputToBeValidated)->
       # wait for the validation result of each input
-      do =>
-        input.once "ValidationResult", (result)=>
-          validatedCount++
-          validInputs.push input if result
+      do ->
+        inputToBeValidated.once "ValidationResult", (result)->
+          validationCount++
+          validInputs.push inputToBeValidated if result
           # check if all inputs were validated
-          if toBeValidated.length is validatedCount
+          if toBeValidatedInputs.length is validationCount
             # check if all inputs were valid
-            if toBeValidated.length is validInputs.length
+            if validInputs.length is toBeValidatedInputs.length
               # put valid inputs to formdata
-              # formData = $.extend formData, @getCustomData()
-              for inputView in toBeValidated
-                formData[inputView.getName()] = inputView.getValue()
+              formData[input.getName()] = input.getValue() for input in validInputs
             else
               valid = no
             # tell validation was finished
-            @emit "FormValidationFinished", valid
-      input.validate null, event
+            form.emit "FormValidationFinished", valid
+      inputToBeValidated.validate null, event

     # if no validation is required mimic as all were validated
-    @emit "FormValidationFinished" if toBeValidated.length is 0
+    form.emit "FormValidationFinished" if toBeValidatedInputs.length is 0
+


Fix server side setData.

Fix server side setData.

--- lib.coffee
+++ lib.coffee
@@ -50,11 +50,18 @@
     check key, Match.Where checkKey
     check value, Match.Any

-    # @connection is available only on the server side, but this is OK,
-    # because on the client side "_connectionId" field does not exist
-    # anyway (it is not published), so {_connectionId: null} in fact does
-    # exactly the right thing, finds documents without "_connectionId".
-    connectionId = @connection?.id or null
+    if Meteor.isClient
+      # @connection is available only on the server side, but this is OK,
+      # because on the client side "_connectionId" field does not exist
+      # anyway (it is not published), so {_connectionId: null} in fact does
+      # exactly the right thing: finds documents without "_connectionId".
+      connectionId = null
+    else
+      # On the server side @connection does not exist when method is called
+      # from the server side (for example, from the publish function) so we
+      # have to get "connectionId" ourselves. We can do that because server
+      # side is trusted.
+      connectionId = @connection?.id or collection.findOne(subscriptionId)?._connectionId

     if _.isString key
       update = {}


Improve node location for failure reporting

--- src/textorum/relaxng/objects.coffee
+++ src/textorum/relaxng/objects.coffee
@@ -137,7 +137,7 @@
      *   GoodElement status.
      * @return {Node} GoodElement or NotAllowed type
     ###
-    require: =>
+    require: (node) =>
       return this

     ###*
@@ -220,14 +220,14 @@
   ###*
    * An empty Pattern node, matching only comments and empty text node blocks
   ###
-  class EmptyNode extends Empty
+  class EmptyNode extends Pattern
     constructor: (@message, @pattern, @childNode) ->
     toString: =>
       "empty"
     nullable: =>
       true
-    require: =>
-      return new Empty("#{this}", this)
+    require: (node) =>
+      return this
     _check: (node, descend) =>
       switch h.getNodeType(node)
         when Node.TEXT_NODE
@@ -293,30 +293,32 @@
       @pattern1.contains(nodeName) or @pattern2.contains(nodeName)
     nullable: =>
       @pattern1.nullable() or @pattern2.nullable()
-    require: =>
-      p1 = @pattern1.require()
+    require: (node) =>
+      p1 = @pattern1.require(node)
       if p1 instanceof GoodElement
         return p1
-      p2 = @pattern2.require()
+      p2 = @pattern2.require(node)
       if p2 instanceof GoodElement
         return p2
       if p1 instanceof NotAllowed
-        if p2 instanceof Empty
+        if p2 instanceof Empty or p2 instanceof EmptyNode
           return new Empty("#{p2}", p2)
-        return new Choice(p1, new MissingContent("Missing: #{p2}", p2))
+        if p2 instanceof MissingContent and p1 instanceof MissingContent
+          return new MissingContent("#{p1} | #{p2}", new Choice(p1, p2), node)
+        return new NotAllowed("#{p1} | #{p2}", new Choice(p1, p2), node)
       if p2 instanceof NotAllowed
-        if p1 instanceof Empty
+        if p1 instanceof Empty or p2 instanceof EmptyNode
           return new Empty("#{p1}", p1)
-        return new Choice(new MissingContent("Missing: #{p1}"), p2)
-      if p2 instanceof Empty
+        return new NotAllowed("#{p1} | #{p2}", new Choice(p1, p2), node)
+      if p2 instanceof Empty or p2 instanceof EmptyNode
         return new Empty("#{p2}", p2)
       return p2

     _check: (node, descend) =>
-      if @pattern1 instanceof Empty and @pattern2 instanceof Attribute
-        return @pattern1
-      if @pattern2 instanceof Empty and @pattern1 instanceof Attribute
-        return @pattern2
+      if @pattern1 instanceof EmptyNode and @pattern2 instanceof Attribute
+        return @pattern1.check(node)
+      if @pattern2 instanceof EmptyNode and @pattern1 instanceof Attribute
+        return @pattern2.check(node)
       if @pattern1 instanceof GoodElement
         return @pattern1
       if @pattern2 instanceof GoodElement
@@ -327,21 +329,25 @@
       #   return @pattern1.check(node, descend)
       p1 = @pattern1.check(node, descend)
       p2 = @pattern2.check(node, descend)
-      if @pattern2 + "" is "c-idp9392+"
-        console.log "tick"
-      if p1 instanceof NotAllowed and @pattern1.require() instanceof Empty
-        return new Choice(@pattern1, p2)
-      if p2 instanceof NotAllowed and @pattern2.require() instanceof Empty
-        return new Choice(p1, @pattern2)
-      if p2.require() instanceof Empty and p1.require() instanceof Empty
+      if p1 instanceof GoodElement
+        return p1
+      if p2 instanceof GoodElement
+        return p2
+      if p1 instanceof NotAllowed and @pattern1.require(node) instanceof Empty
+        return p2
+      if p2 instanceof NotAllowed and @pattern2.require(node) instanceof Empty
+        return p1
+      if p2.require(node) instanceof Empty and p1.require(node) instanceof Empty
         if p1 instanceof Empty
           return p2
         if p2 instanceof Empty
           return p1
         return new Choice(p1, p2)
       if p1 instanceof NotAllowed and p2 instanceof NotAllowed
-        failed = new Choice(p1, p2)
-        return new NotAllowed("choice failed: #{failed}", failed, node)
+        return p2
+        # Simplification makes the right side more likely to be interesting
+        # failed = new Choice(p1, p2)
+        # return new NotAllowed("choice failed: #{failed}", failed, node)
       return new Choice(p1, p2)
     attrCheck: (node) =>
       if @pattern1 instanceof NotAllowed or @pattern1 instanceof Empty
@@ -369,11 +375,11 @@
       "(#{@pattern1} & #{@pattern2})"
     nullable: =>
       @pattern1.nullable() and @pattern2.nullable()
-    require: =>
-      p1 = @pattern1.require()
+    require: (node) =>
+      p1 = @pattern1.require(node)
       if p1 instanceof Empty
-        return @pattern2.require()
-      p2 = @pattern2.require()
+        return @pattern2.require(node)
+      p2 = @pattern2.require(node)
       if p2 instanceof Empty
         return p1
       if p1 instanceof GoodElement and p2 instanceof GoodElement
@@ -414,22 +420,23 @@
       "#{@pattern1}, #{@pattern2}"
     nullable: =>
       @pattern1.nullable() and @pattern2.nullable()
-    require: =>
-      p1 = @pattern1.require()
-      p2 = @pattern2.require()
+    require: (node) =>
+      p1 = @pattern1.require(node)
+      p2 = @pattern2.require(node)
       if p1 instanceof GoodElement
-        if p2 instanceof Empty
+        if p2 instanceof Empty or p2 instanceof EmptyNode
           return p1
       if p2 instanceof GoodElement
-        if p1 instanceof Empty
+        if p1 instanceof Empty or p2 instanceof EmptyNode
           return p2
-      if p1 instanceof Empty and p2 instanceof Empty
-        return p1
-      if p1 instanceof Empty
-        return new MissingContent("missing group element 2", this)
-      if p2 instanceof Empty
-        return new MissingContent("missing group element 1", this)
-      return new MissingContent("missing both group elements", this)
+      if p1 instanceof Empty or p1 instanceof EmptyNode
+        if p2 instanceof Empty or p2 instanceof EmptyNode
+          return p1
+      if p1 instanceof Empty or p1 instanceof EmptyNode
+        return new MissingContent("#{p2} not found", p2, node)
+      if p2 instanceof Empty or p2 instanceof EmptyNode
+        return new MissingContent("#{p1} not found", p1, node)
+      return new MissingContent("missing #{p1}, #{p2}", node)

     _check: (node, descend) =>
       if @pattern2 instanceof GoodElement
@@ -440,9 +447,9 @@
         return @pattern1
       if @pattern2 instanceof NotAllowed
         return @pattern2
-      if @pattern1 instanceof Empty or @pattern1 instanceof GoodElement
+      if @pattern1 instanceof Empty or @pattern1 instanceof EmptyNode or @pattern1 instanceof GoodElement
         return @pattern2.check(node, descend)
-      if @pattern2 instanceof Empty
+      if @pattern2 instanceof Empty or @pattern2 instanceof EmptyNode
         return @pattern1.check(node, descend)
       _nodelog node, "p1 group, checking pattern1 #{@pattern1} against", node, [this, @pattern1, node]
       p1 = @pattern1.check(node, descend)
@@ -458,10 +465,10 @@
       return new Group(p1, p2)
     attrCheck: (node) =>
       p1 = @pattern1.attrCheck(node)
-      if p1 instanceof NotAllowed or p1 instanceof Empty
+      if p1 instanceof NotAllowed or p1 instanceof Empty or p1 instanceof EmptyNode
         return @pattern2.attrCheck(node)
       p2 = @pattern2.attrCheck(node)
-      if p2 instanceof NotAllowed or p2 instanceof Empty
+      if p2 instanceof NotAllowed or p2 instanceof Empty or p1 instanceof EmptyNode
         return p1
       return (new Interleave(p1, p2)).attrCheck(node)

@@ -476,7 +483,7 @@
       "#{@pattern}+"
     nullable: =>
       @pattern.nullable()
-    require: =>
+    require: (node) =>
       return new MissingContent("missing #{this}", this)
     _check: (node, descend) =>
       p1 = @pattern.check(node, descend)
@@ -574,7 +581,9 @@
     toString: =>
       "attribute #{@nameClass} { #{@pattern} }"
     _check: (node, descend) =>
-      return new Empty()
+      return @require(node)
+    require: (node) =>
+      return new EmptyNode("#{this}", this)
     attrCheck: (node) =>
       error = []
       for attr in h.getNodeAttributes(node)
@@ -630,6 +639,7 @@
         unless descend is true
           descend = descend - 1
         nextPattern = @pattern
+        lastElementNode = undefined
         if node.childNodes?.length
           for child in node.childNodes
             switch h.getNodeType(child)
@@ -638,6 +648,8 @@
                   continue
               when Node.COMMENT_NODE
                 continue
+              when Node.ELEMENT_NODE
+                lastElementNode = child
             curPattern = nextPattern
             _nodelog node, "==> checking child", child, "against", nextPattern
             nextPattern = nextPattern.check(child, descend)
@@ -645,10 +657,12 @@
             if nextPattern instanceof NotAllowed
               return nextPattern
         # TODO: Detect missing elements
-        if nextPattern.require() instanceof Empty
+        if nextPattern.require(node) instanceof Empty
           return new GoodElement(@name, nextPattern)
         else
-          return nextPattern.require()
+          if lastElementNode?
+            return nextPattern.require(lastElementNode)
+          return nextPattern.require(node)
       else
         emptyPattern = new Empty("not descending into #{h.getLocalName(node)}", @pattern, node)
         return new GoodElement(emptyPattern, node)


tests(credentials): add test for loading credentials from env

--- src/spec/helpers/project-credentials-config.spec.coffee
+++ src/spec/helpers/project-credentials-config.spec.coffee
@@ -1,6 +1,6 @@
 _ = require 'underscore'
 Promise = require 'bluebird'
-{ProjectCredentialsConfig} = require '../../lib/main'
+{ProjectCredentialsConfig} = require '../../../lib/main'

 describe 'ProjectCredentialsConfig', ->

@@ -41,6 +41,23 @@
     .catch (error) -> done(error)
     .done()

+  it "should load credentials from environment variables", (done) ->
+    process.env.CTP_PROJECT_KEY = 'project-key'
+    process.env.CTP_CLIENT_ID = 'client-id'
+    process.env.CTP_CLIENT_SECRET = 'client-secret'
+
+    ProjectCredentialsConfig.create()
+    .then (config) ->
+      actual = config.forProjectKey('project-key')
+      expected = {
+        project_key: 'project-key'
+        client_id: 'client-id'
+        client_secret: 'client-secret'
+      }
+      expect(actual).toEqual(expected)
+      done()
+    .catch (err) -> done(err)
+
   it "should throw an error if credentials are not defined", (done) ->
     ProjectCredentialsConfig.create
       lookupFiles: ['data/config/config3']


Tweak campaign connecton arrows so they point more accurately to the next level

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -365,12 +365,13 @@
     return experienceScore

   createLine: (o1, o2) ->
-    p1 = x: o1.x, y: 0.66 * o1.y + 0.5
-    p2 = x: o2.x, y: 0.66 * o2.y + 0.5
+    ratio = parseFloat($(".map").css("width")) / parseFloat($(".map").css("height"))
+    p1 = x: o1.x, y: o1.y / ratio - 0.5
+    p2 = x: o2.x, y: o2.y / ratio
     length = Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y))
     angle = Math.atan2(p1.y - p2.y, p2.x - p1.x) * 180 / Math.PI
     transform = "rotate(#{angle}deg)"
-    line = $('
').appendTo('.map').addClass('next-level-line').css(transform: transform, width: length + '%', left: o1.x + '%', bottom: (o1.y + 0.5) + '%')
+    line = $('
').appendTo('.map').addClass('next-level-line').css(transform: transform, width: length + '%', left: o1.x + '%', bottom: (o1.y - 0.5) + '%')
     line.append($('
')).append($('
'))

   applyCampaignStyles: ->


Fix error on repeated anonymous homepage play actions

Subsequent plays would throw an error at /play?classCode=

CampaignView would try to load the `classCode=` campaign.

Not sure why it was interpreted like this, or where in the code we are
setting this, but this fixes the resulting buggy behavior.

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -116,6 +116,8 @@

   constructor: (options, @terrain) ->
     super options
+    if /^classCode/.test @terrain
+      @terrain = ''  # Stop /play?classCode= from making us try to play a classCode campaign
     @terrain = 'picoctf' if window.serverConfig.picoCTF
     @editorMode = options?.editorMode
     @requiresSubscription = not me.isPremium()


Next Level arrow shouldn't point to Kithgard Mastery

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -302,7 +302,8 @@
       unless foundNext
         for nextLevelOriginal in level.nextLevels
           nextLevel = _.find levels, original: nextLevelOriginal
-          if nextLevel and not nextLevel.locked and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug isnt 'lost-viking' and not nextLevel.replayable and (
+          dontPointTo = ['lost-viking','kithgard-mastery']
+          if nextLevel and not nextLevel.locked and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and not nextLevel.replayable and (
             me.isPremium() or
             not nextLevel.requiresSubscription or
             nextLevel.slug is 'apocalypse' or


Bind "- collection" to change events. Fixes #77

Makes tests pass

Refactor the fix

--- src/compile.coffee
+++ src/compile.coffee
@@ -146,8 +146,10 @@

   collection: (ast, model, controller) ->
     compileItem = (item) -> compileAll(ast.children, item, controller)
+    update = (dynamic, collection) ->
+      dynamic.replace(compileItem(item) for item in collection)

-    dynamic = new DynamicNode(ast)
+    dynamic = @bound(ast, model, controller, update)
     collection = model[ast.arguments[0]]
     if typeof(collection.bind) is "function"
       dynamic.bindEvent(collection, 'set', => dynamic.replace(compileItem(item) for item in collection))
@@ -155,7 +157,6 @@
       dynamic.bindEvent(collection, 'add', (item) => dynamic.appendNodeSet(compileItem(item)))
       dynamic.bindEvent(collection, 'insert', (index, item) => dynamic.insertNodeSet(index, compileItem(item)))
       dynamic.bindEvent(collection, 'delete', (index) => dynamic.deleteNodeSet(index))
-    dynamic.replace(compileItem(item) for item in collection)
     dynamic

   in: (ast, model, controller) ->


Remove a console.log

Remove a console.log

--- webapp/app/scripts/views/Map.coffee
+++ webapp/app/scripts/views/Map.coffee
@@ -96,8 +96,6 @@
       lastPolygons = []
       lastPolygonsDistrictId = districtId

-      console.log('setDistrictId', districtId)
-
       if lastPolygonsDistrictId?
         @_districtIdToPolygonsAsync(districtId).done (polygons) =>
           return if districtId != lastPolygonsDistrictId


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -20,6 +20,35 @@
 @param {Object} I Instance variables
 ###
 TimedEvents = (I={}) ->
+    ###*
+  Distance between two objects. Proxies to Point.distance.
+  In order for this to work, `otherObj` must have a
+  position method.
+
+

+  player = GameObject
+    x: 50
+    y: 50
+    width: 10
+    height: 10
+
+  player.include Bounded
+
+  enemy = GameObject
+    x: 110
+    y: 120
+    width: 7
+    height: 20
+
+  player.distance(enemy)
+  # => 92.19544457292888
+

+
+  @name distance
+  @methodOf Bounded#
+  @see Point.distance
+  @returns {Number} Distance between the two objects
+  ###
   every: (n, fn) ->
     if I.age.mod(n) is 0
       fn()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -13,8 +13,8 @@

   self.bind "update", (elapsedTime) ->
     for event in I.everyEvents
-      {fn, period, lastFired} = event
-      while lastFired < I.age + elapsedTime
+      {fn, period} = event
+      while event.lastFired < I.age + elapsedTime
         self.sendOrApply(fn)
         event.lastFired += period


:lipstick: Initialize variables inside constructor

--- src/grammar.coffee
+++ src/grammar.coffee
@@ -16,18 +16,12 @@
 class Grammar
   Emitter.includeInto(this)

-  name: null
-  rawPatterns: null
-  rawRepository: null
-  fileTypes: null
-  scopeName: null
-  repository: null
-  initialRule: null
-  firstLineRegex: null
-  includedGrammarScopes: null
-  maxTokensPerLine: 100
+  constructor: (@registry, {@name, @fileTypes, @scopeName, injections, injectionSelector, patterns, repository, @foldingStopMarker, firstLineMatch}) ->
+    repository = null
+    initialRule = null
+    firstLineRegex = null
+    maxTokensPerLine = 100

-  constructor: (@registry, {@name, @fileTypes, @scopeName, injections, injectionSelector, patterns, repository, @foldingStopMarker, firstLineMatch}) ->
     @rawPatterns = patterns
     @rawRepository = repository
     @injections = new Injections(this, injections)


Make Vector2 tests use a range

Make Vector2 tests use a range

Accounts for minor float calculation differences

--- test/wintermute/vector2.coffee
+++ test/wintermute/vector2.coffee
@@ -70,19 +70,19 @@
   it "has a magnitude", ->
     vector.set 20, 30

-    expect(vector.magnitude()).to.be.equal(36.05551275463989)
+    expect(vector.magnitude()).to.be.within(36.0, 36.1)

   it "has an angle", ->
     vector.set 20, 20
-    expect(vector.angle()).to.be.equal(0.7853981633974483)
+    expect(vector.angle()).to.be.within(0.78, 0.79)

   it "has an angle in degrees", ->
     vector.set 20, 20
-    expect(vector.angle(degrees: true)).to.be.equal(44.99999998972517)
+    expect(vector.angle(degrees: true)).to.be.within(44.9, 50.0)

   it "get an angle to another vector in radians", ->
     other = new Vector2(99, 300)
-    expect(vector.angleTo(other)).to.equal(-1.889543887215541)
+    expect(vector.angleTo(other)).to.be.within(-1.89, -1.88)

   it "get an angle to another vector in degrees", ->
     other = new Vector2(99, 300)
@@ -134,12 +134,12 @@
     vector.set 20, 30
     vector.rotate 1

-    expect(vector.x).to.be.equal(19.48087668059005)
-    expect(vector.y).to.be.equal(30.339667825400497)
+    expect(vector.x).to.be.within(19.4, 19.5)
+    expect(vector.y).to.be.within(30.3, 30.4)

   it "can be rotated by degrees", ->
     vector.set 20, 20
     vector.rotate 45, degrees: true

-    expect(vector.x).to.be.equal(19.999999996413404)
-    expect(vector.y).to.be.equal(20.000000003586596)
+    expect(vector.x).to.be.within(19.9, 20.0)
+    expect(vector.y).to.be.within(20.0, 20.1)