app: flux: teams: actions minor changes

app: flux: teams: actions minor changes

--- client/app/lib/flux/teams/actions.coffee
+++ client/app/lib/flux/teams/actions.coffee
@@ -10,6 +10,7 @@
 immutable   = require 'immutable'
 Tracker     = require 'app/util/tracker'
 isKoding    = require 'app/util/isKoding'
+isEmailValid = require 'app/util/isEmailValid'

 loadTeam = ->
@@ -38,16 +39,40 @@
       resolve { message }

-validateEmail = (email) ->
-  emailPattern = /// ^ #begin of line
-   ([\w.-]+)         #one or more letters, numbers, _ . or -
-   @                 #followed by an @ sign
-   ([\w.-]+)         #then one or more letters, numbers, _ . or -
-   \.                #followed by a period
-   ([a-zA-Z.]{2,6})  #followed by 2 to 6 letters or periods
-   $ ///i            #end of line and ignore case
-
-  return emailPattern.test email
+updateInviteInput = (index, inputType, value) ->
+
+  { reactor } = kd.singletons
+
+
+  reactor.dispatch actions.SET_TEAM_INVITE_INPUT_VALUE, { index, inputType, value }
+
+
+fetchMembers = (options = {}) ->
+
+  { groupsController, reactor } = kd.singletons
+
+  team = groupsController.getCurrentGroup()
+  reactor.dispatch actions.FETCH_TEAM_MEMBERS_BEGIN, { team }
+
+  team.fetchMembersWithEmail {}, options, (err, users) ->
+    reactor.dispatch actions.FETCH_TEAM_MEMBERS_SUCCESS, { users }
+
+
+fetchMembersRole = ->
+
+  { groupsController, reactor } = kd.singletons
+
+  team = groupsController.getCurrentGroup()
+  reactor.dispatch actions.FETCH_TEAM_MEMBERS_ROLES_BEGIN, { team }
+
+  myId = whoami().getId()
+  teamMemberIds = reactor.evaluate getters.TeamMembersIdStore
+
+  ids = teamMemberIds.toArray()
+  ids.push myId
+
+  team.fetchUserRoles ids, (err, roles) ->
+    reactor.dispatch actions.FETCH_TEAM_MEMBERS_ROLES_SUCCESS, roles

 inviteMembers = (inviteInputs) ->
@@ -58,20 +83,21 @@
   admins  = []

   new Promise (resolve, reject) ->
+
     whoami().fetchEmail (err, ownEmail) =>

       inviteInputs.forEach (inviteInput) ->
+
         email = inviteInput.get('email').trim()
-
         return  unless email

-        validatedEmail = validateEmail email
+        validEmail = isEmailValid email

         if email.toLowerCase() is ownEmail

           return reject { message: 'You can not invite yourself!'}

-        if email and not validatedEmail
+        if email and not validEmail

           return reject { message: 'That doesn\'t seem like a valid email address.'}

@@ -87,54 +113,6 @@
         resolve { invites, admins }
       else
         resolve { invites }
-
-
-getNewInvitations = (invites, pendingInvitations) ->
-  new Promise (resolve, reject) ->
-    pendingEmails=[]
-
-    pendingInvitations.map (invite) ->
-      pendingEmails.push invite.get('email')
-
-    newInvitations = (invite for invite, i in invites when invite.email not in pendingEmails)
-    resolve { newInvitations }
-
-
-
-updateInviteInput = (index, inputType, value) ->
-
-  { reactor } = kd.singletons
-
-
-  reactor.dispatch actions.SET_TEAM_INVITE_INPUT_VALUE, { index, inputType, value }
-
-
-fetchMembers = (options = {}) ->
-
-  { groupsController, reactor } = kd.singletons
-
-  team = groupsController.getCurrentGroup()
-  reactor.dispatch actions.FETCH_TEAM_MEMBERS_BEGIN, { team }
-
-  team.fetchMembersWithEmail {}, options, (err, users) ->
-    reactor.dispatch actions.FETCH_TEAM_MEMBERS_SUCCESS, { users }
-
-
-fetchMembersRole = ->
-
-  { groupsController, reactor } = kd.singletons
-
-  team = groupsController.getCurrentGroup()
-  reactor.dispatch actions.FETCH_TEAM_MEMBERS_ROLES_BEGIN, { team }
-
-  myId = whoami().getId()
-  teamMemberIds = reactor.evaluate getters.TeamMembersIdStore
-
-  ids = teamMemberIds.toArray()
-  ids.push myId
-
-  team.fetchUserRoles ids, (err, roles) ->
-    reactor.dispatch actions.FETCH_TEAM_MEMBERS_ROLES_SUCCESS, roles

 loadPendingInvites = (invites) ->
@@ -161,7 +139,7 @@
 sendInvitations = (invites, pendingInvites) ->

   new Promise (resolve, reject) ->
-    # return resolve { title: 'There is no body new to send invitation' }  if invites.length is 0
+
     remote.api.JInvitation.create { invitations: invites }, (err) =>
       if err
         return reject { title }
@@ -195,6 +173,18 @@
         title  = "Invitation is resent to <strong>#{pendingInvitations.get(0).get('email')}</strong>"
         title  = 'All invitations are resent.'  if pendingInvitations.size > 1
         resolve { title }
+
+
+getNewInvitations = (invites, pendingInvitations) ->
+
+  new Promise (resolve, reject) ->
+    pendingEmails=[]
+
+    pendingInvitations.map (invite) ->
+      pendingEmails.push invite.get('email')
+
+    newInvitations = (invite for invite, i in invites when invite.email not in pendingEmails)
+    resolve { newInvitations }

 setSearchInputValue = (value) ->


realtime: fix not all messages are fetched via fetchHistory bug

--- client/app/lib/realtimecontroller.coffee
+++ client/app/lib/realtimecontroller.coffee
@@ -317,14 +317,16 @@

         return  unless response?.length and Array.isArray(response)

-        [messages, start] = response
+        [messages, start, end] = response

         return  unless messages?.length and Array.isArray(messages)

         @handleMessage message, channel  for message in messages

         # since the maximum message limit is 100, we are making a recursive call here
-        @fetchHistory channel, start  if messages.length is limit
+        if messages.length is limit
+          historyOptions.start = end
+          @fetchHistory {channel, timestamp: end}
       err: (err) ->
         # instead of getting into a stale state, just reload the page
         window.location.reload()  if err


[#38] Remove paginação da parte do rodapé da tabela

--- app/assets/javascripts/angular_app/directives/my_data_table.js.coffee
+++ app/assets/javascripts/angular_app/directives/my_data_table.js.coffee
@@ -3,7 +3,7 @@
     default_options =
       aoColumns: columns
       bSort: true
-      sDom: 'fptip'
+      sDom: 'fpti'
       sPaginationType: 'full_numbers'
       oLanguage:
         sSearch: ''


remove uneeded name prop

remove uneeded name prop

--- client/lanes/screens/SystemSettings.cjsx
+++ client/lanes/screens/SystemSettings.cjsx
@@ -55,7 +55,8 @@
                 </BS.NavItem>
             </BS.Nav>
             <BS.Row>
-                <LC.ImageAsset sm=4 model={@config} name='logo'
+
+                <LC.ImageAsset sm=4 asset={@config.logo}
                     label='Logo' size='thumb' />
             </BS.Row>
             {for id, Ext of Lanes.Extensions.instances when Ext.getSettingsElement


Compare values with a 1-digit precision

Compare values with a 1-digit precision

This should avoid false results due to differences smaller then 0.1 point

--- client/src/sprint/services/sprint.coffee
+++ client/src/sprint/services/sprint.coffee
@@ -93,7 +93,7 @@
     return unless _.isArray sprint?.bdcData
     [first, ..., last] = sprint.bdcData
     if _.isNumber last.done
-      return if last.done >= last.standard then 'ok' else 'ko'
+      return if last.done.toFixed(1) >= last.standard.toFixed(1) then 'ok' else 'ko'
     else
       return 'unknown'
   # return true if done > standard, false if done < standard, undefined otherwise


Add issues and merge requests to gitlab script

Gitlab doesn't send any useful meta data for these
kinds of events, so they are limited to text only.
This is better than just ignoring them though!

--- src/scripts/gitlab.coffee
+++ src/scripts/gitlab.coffee
@@ -75,13 +75,25 @@
             robot.send user, "We will be missing #{bold(hook.name)} (#{bold(hook.email)}) on Gitlab"
       when "web"
         message = ""
-        branch = hook.ref.split("/")[2..].join("/")
-        # if the ref before the commit is 00000, this is a new branch
-        if /^0+$/.test(hook.before)
-            message = "#{bold(hook.user_name)} pushed a new branch (#{bold(branch)}) to #{bold(hook.repository.name)}: #{underline(hook.repository.homepage)}"
+        # is it code being pushed?
+        if hook.ref
+          branch = hook.ref.split("/")[2..].join("/")
+          # if the ref before the commit is 00000, this is a new branch
+          if /^0+$/.test(hook.before)
+            message = "#{bold(hook.user_name)} pushed a new branch (#{bold(branch)}) to #{bold(hook.repository.name)} (#{underline(hook.repository.homepage)})"
+          else
+            message = "#{bold(hook.user_name)} pushed #{bold(hook.total_commits_count)} commits to #{bold(branch)} in #{bold(hook.repository.name)} (#{underline(hook.repository.homepage + '/compare/' + hook.before.substr(0,9) + '...' + hook.after.substr(0,9))})"
+          robot.send user, message
+        # not code? must be a something good!
         else
-            message = "#{bold(hook.user_name)} pushed #{bold(hook.total_commits_count)} commits to #{bold(branch)} in #{bold(hook.repository.name)}: #{underline(hook.repository.homepage + '/compare/' + hook.before.substr(0,9) + '...' + hook.after.substr(0,9))}"
-        robot.send user, message
+          switch hook.object_kind
+            when "issue"
+              robot.send user, "Issue #{bold(hook.object_attributes.id)}: #{hook.object_attributes.title} (#{h
+ook.object_attributes.state})"
+            when "merge_request"
+              robot.send user, "Merge Request #{bold(hook.object_attributes.id)}: #{hook.object_attributes.title} (#{hook.object_attributes.state}) between #{bold(hook.object_attributes.source_branch)} and #{bold(hook.object_attributes.target_branch)}"
+          if hook.object_attributes.description
+            robot.send user, ">> #{hook.object_attributes.description}"

   robot.router.post "/gitlab/system", (req, res) ->
     handler "system", req, res


Made my environment variable naming more consistant

--- src/scripts/shodan.coffee
+++ src/scripts/shodan.coffee
@@ -5,7 +5,7 @@
 #   None
 #
 # Configuration:
-#   None
+#   SHODAN_API_KEY
 #
 # Commands:
 #   hubot shodan <string> - Search Shodan for any information about string
@@ -13,7 +13,7 @@
 # Author:
 #   Scott J Roberts - @sroberts

-SHODAN_KEY = process.env.SHODAN_KEY
+SHODAN_KEY = process.env.SHODAN_API_KEY

 shodan_url = "http://www.shodanhq.com"


Fixed multiple fetching of koding group bug.

--- workers/sitemapgenerator/lib/sitemapgenerator/sitemapgeneratorworker.coffee
+++ workers/sitemapgenerator/lib/sitemapgenerator/sitemapgeneratorworker.coffee
@@ -38,13 +38,20 @@

     sitemapNames = []

-
     groupSelector = {
       privacy:'public'
     }

     JGroup.count groupSelector, (err, groupCount)=>
       numberOfGroupPages = Math.ceil(groupCount / GROUPPERPAGE)
+
+      publicGroups = []
+
+      # We behave 'koding' as a public group, even though
+      # its privacy is marked as "private" in the model.
+      # We add 'koding' group to publicGroups selector for the first cycle.
+      publicGroups.push 'koding'
+
       for groupPageNumber in [1..numberOfGroupPages]
         groupSkip = (groupPageNumber - 1) * GROUPPERPAGE
         groupOptions = {
@@ -52,11 +59,6 @@
           skip  : groupSkip
         }
         JGroup.some groupSelector, groupOptions, (err, jGroups)=>
-          publicGroups = []
-
-          # We behave 'koding' as a public group, even though
-          # its privacy is marked as "private" in the model.
-          publicGroups.push 'koding'

           for group in jGroups
             publicGroups.push group.slug
@@ -66,24 +68,27 @@
             'slugs.group': { $in: publicGroups }
           }

+          # Empty the group, it'll be filled in next cycle.
+          publicGroups = []
+
           JName.count selector, (err, count)=>
             numberOfNamePages = Math.ceil(count / NAMEPERPAGE)

             queue = [1..numberOfNamePages].map (pageNumber)=>=>
-                skip = (pageNumber - 1) * NAMEPERPAGE
-                option = {
-                  limit : NAMEPERPAGE,
-                  skip  : skip
-                }
-                JName.some selector, option, (err, names)=>
-                  urls = (name.name for name in names)
+              skip = (pageNumber - 1) * NAMEPERPAGE
+              option = {
+                limit : NAMEPERPAGE,
+                skip  : skip
+              }
+              JName.some selector, option, (err, names)=>
+                urls = (name.name for name in names)

-                  sitemapName =  @generateSitemapName skip
-                  content = @generateSitemapString urls
-                  @saveSitemap sitemapName, content
+                sitemapName =  @generateSitemapName skip
+                content = @generateSitemapString urls
+                @saveSitemap sitemapName, content

-                  sitemapNames.push sitemapName + ".xml"
-                  queue.next()
+                sitemapNames.push sitemapName + ".xml"
+                queue.next()
             queue.push => @generateMainSitemap sitemapNames

             daisy queue


perf(threads): Add an index specifically for the "starred" view

--- src/flux/models/thread.coffee
+++ src/flux/models/thread.coffee
@@ -100,7 +100,8 @@

   @additionalSQLiteConfig:
     setup: ->
-      ['CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(last_message_received_timestamp DESC, id)']
+      ['CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(last_message_received_timestamp DESC, id)',
+       'CREATE INDEX IF NOT EXISTS ThreadStarIndex ON Thread(account_id, starred)']

   fromJSON: (json) ->
     super(json)


activity: render different states of modals if pre existing channel

--- client/activity/lib/components/createprivatechannelmodal/index.coffee
+++ client/activity/lib/components/createprivatechannelmodal/index.coffee
@@ -72,13 +72,31 @@

   getModalProps: ->
-    isOpen             : yes
-    title              : 'Create a Private Conversation'
-    className          : 'CreateChannel-Modal'
-    buttonConfirmTitle : 'CREATE'
-    onConfirm          : @bound 'createChannel'
-    onClose            : @bound 'onClose'
-    onAbort            : @bound 'onClose'
+    props =
+      isOpen             : yes
+      title              : 'Create a Private Conversation'
+      className          : 'CreateChannel-Modal'
+      buttonConfirmTitle : 'CREATE'
+      onConfirm          : @bound 'createChannel'
+      onClose            : @bound 'onClose'
+      onAbort            : @bound 'onClose'
+
+    if @state.preExistingChannel
+      props = _.assign {}, props,
+        buttonExtraTitle: 'CONTINUE EXISTING CONVERSATION'
+        onButtonExtraClick: (event) =>
+          kd.utils.stopDOMEvent event
+          @_isRouting = yes
+          kd.singletons.router.handleRoute "/Messages/#{@state.preExistingChannel.get 'id'}"
+        buttonConfirmTitle: 'CREATE NEW THREAD'
+        buttonConfirmClassName: 'Button--cancel'
+    else
+      props = _.assign {}, props,
+        buttonExtraTitle: null
+        buttonConfirmTitle: 'CREATE'
+        buttonConfirmClassName: 'Button--primary'
+
+    return props

   setName: (event) ->


Workspace: Added setContent method to EditorPane.

--- client/app/CommonViews/workspace/panes/collaborativeeditorpane.coffee
+++ client/app/CommonViews/workspace/panes/collaborativeeditorpane.coffee
@@ -23,6 +23,9 @@
         if @firepad.isHistoryEmpty()
           @firepad.setText "" # fix for a firepad bug

+  setContent: (content) ->
+    @firepad.setText content
+
   createSessionKey: ->
     nick = KD.nick()
     u    = KD.utils


improve pluginKey assignment

improve pluginKey assignment

--- views/single-tabarea.cjsx
+++ views/single-tabarea.cjsx
@@ -68,9 +68,10 @@
     @setState {key} if key isnt @state.key
   handleSelectMenuItem: (e, key) ->
     e.preventDefault()
-    @setState {key} if key isnt @state.key
-    pluginKey = key
-    @setState {pluginKey}
+    if key isnt @state.key
+      @setState {key}
+      pluginKey = key
+      @setState {pluginKey}
   handleSelectMainView: ->
     event = new CustomEvent 'view.main.visible',
       bubbles: true


Use waitsFor/runs when exec'ing git commands

/cc @gjtorikian

--- spec/git-spec.coffee
+++ spec/git-spec.coffee
@@ -206,7 +206,13 @@
       # in this test, we need to fake a commit and try to switch to a new branch
       it 'does not check a branch out if the dirty tree interferes', ->
         fs.writeFileSync(path.join(repo.getWorkingDirectory(), 'README.md'), 'great words', 'utf8')
-        exec "cd #{repoDirectory} && git add . && git commit -m 'update README'", ->
+        gitCommandHandler = jasmine.createSpy('gitCommandHandler')
+        exec "cd #{repoDirectory} && git add . && git commit -m 'update README'", gitCommandHandler
+
+        waitsFor ->
+          gitCommandHandler.callCount is 1
+
+        runs ->
           expect(repo.checkoutReference('refs/heads/getHeadOriginal')).toBe true
           fs.writeFileSync(path.join(repo.getWorkingDirectory(), 'README.md'), 'more words', 'utf8')
           expect(repo.checkoutReference('refs/heads/master')).toBe false


Add copyRepository helper

Add copyRepository helper

--- spec/git-spec.coffee
+++ spec/git-spec.coffee
@@ -5,6 +5,12 @@
 Task = require '../src/task'

 describe "Git", ->
+copyRepository = ->
+  workingDirPath = temp.mkdirSync('atom-working-dir')
+  fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
+  fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
+  workingDirPath
+
   repo = null

   beforeEach ->
@@ -44,9 +50,7 @@
     [repo, filePath, newPath] = []

     beforeEach ->
-      workingDirPath = temp.mkdirSync('atom-working-dir')
-      fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
-      fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
+      workingDirPath = copyRepository()
       repo = new Git(workingDirPath)
       filePath = path.join(workingDirPath, 'a.txt')
       newPath = path.join(workingDirPath, 'new-path.txt')
@@ -70,9 +74,7 @@
     [filePath, newPath] = []

     beforeEach ->
-      workingDirPath = temp.mkdirSync('atom-working-dir')
-      fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
-      fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
+      workingDirPath = copyRepository()
       repo = new Git(workingDirPath)
       filePath = path.join(workingDirPath, 'a.txt')
       newPath = path.join(workingDirPath, 'new-path.txt')
@@ -86,12 +88,10 @@
         expect(repo.isPathNew(filePath)).toBeFalsy()

   describe ".checkoutHead(path)", ->
-    [filePath, workingDirPath] = []
-
-    beforeEach ->
-      workingDirPath = temp.mkdirSync('atom-working-dir')
-      fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
-      fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
+    [filePath] = []
+
+    beforeEach ->
+      workingDirPath = copyRepository()
       repo = new Git(workingDirPath)
       filePath = path.join(workingDirPath, 'a.txt')


[metrics] properly dispose of resize handler

--- app/assets/javascripts/states/monitoring/views/monitoring_overview.js.coffee
+++ app/assets/javascripts/states/monitoring/views/monitoring_overview.js.coffee
@@ -5,5 +5,9 @@
     template: "monitoring/monitoring_overview"

     after_render: ->
-      $(window).resize _.debounceR 500, =>
+      $(window).on "resize.monitoring.overview", _.debounceR 500, =>
         @redraw()
+
+    dispose: ->
+      super
+      $(window).off "resize.monitoring.overview"


Added quick block cache.

Added quick block cache.

--- scripts/cpu.coffee
+++ scripts/cpu.coffee
@@ -23,8 +23,10 @@
 			0, 0, 0, 0, 0, 0, 0, 0
 		]

+		@last_block_pc = -1
+		@last_block_func = null
+
 	execute_one: ->
-		@decompiling = false
 		@inscount++
 		@delayed = @delay
 		cpc = if @delayed != null then @delay else @pc
@@ -37,7 +39,10 @@
 		@pc += 4 if @delayed == null

 	decompile_block: (pc) ->
-		@decompiling = true
+		if @last_block_pc == pc
+			return [true, pc, @last_block_func]
+
+		@last_block_pc = pc
 		total = ''
 		emit = (snippet) -> code += snippet + '\n'
 		branched = false
@@ -62,6 +67,7 @@
 				total += code

 		func = eval('(function(state) { state.delayed = false;\n' + total + ' })')
+		@last_block_func = func
 		[true, pc, func]

 	branch: (pc, recompiled=false) ->


Wrong commit.

Wrong commit.


initializing vendors properly and fixing assignment

--- src/toaster/config.coffee
+++ src/toaster/config.coffee
@@ -45,5 +45,6 @@

 	build:( name, params )=>
 		params.name = name
+		params.vendors = params.vendors ? []
 		params.release = pn "#{@basepath}/#{params.release}"
-		@builds[name] params+		@builds[name] = params


Use @tabLength instead of an arbitrary value

--- src/tokenized-line.coffee
+++ src/tokenized-line.coffee
@@ -97,7 +97,7 @@
       leftTextLength += nextToken.value.length
       leftTokens.push nextToken

-    tab = leftTokens[0].buildPhantomToken(@indentLevel * 2)
+    tab = leftTokens[0].buildPhantomToken(@indentLevel * @tabLength)

     leftFragment = new TokenizedLine(
       tokens: leftTokens
@@ -105,7 +105,8 @@
       ruleStack: @ruleStack
       invisibles: @invisibles
       lineEnding: null,
-      indentLevel: @indentLevel
+      indentLevel: @indentLevel,
+      tabLength: @tabLength
     )
     rightFragment = new TokenizedLine(
       tokens: [tab].concat(rightTokens)
@@ -113,7 +114,8 @@
       ruleStack: @ruleStack
       invisibles: @invisibles
       lineEnding: @lineEnding,
-      indentLevel: @indentLevel
+      indentLevel: @indentLevel,
+      tabLength: @tabLength
     )
     [leftFragment, rightFragment]


use old disk quota for setting new one

use old disk quota for setting new one

--- salvus/compute.coffee
+++ salvus/compute.coffee
@@ -1295,6 +1295,8 @@
                 else
                     quotas = {}
                     result = result[0]
+                    if result.disk and not result.disk_quota
+                        result.disk_quota = int(result.disk*1.5)
                     for k, v of DEFAULT_SETTINGS
                         if not result?[k]
                             quotas[k] = v


GameObject.Effect

GameObject.Effect

--- test/tweening.coffee
+++ test/tweening.coffee
@@ -3,7 +3,7 @@
 test "should allow for simple linear tweening", ->
   o = GameObject
     x: 0
-
+
   targetValue = 10
   o.tween 10,
     x: targetValue


Remove template variable

Remove template variable

--- harvest/template/project_name/static/scripts/coffeescript/main.coffee
+++ harvest/template/project_name/static/scripts/coffeescript/main.coffee
@@ -1,5 +1,5 @@
 # The modules defined here are included in Cilantro. All project-level modules
-# must be access via the namespaced path '{{ project_name }}/main'. This is to
+# must be access via the namespaced path 'project/main'. This is to
 # ensure modules of the same name do not conflict Cilantro's modules.

 define [


Using canvasScale instead of pixel multiplier


add observer to watch flex scale change event in pane-element.coffee

--- src/pane-element.coffee
+++ src/pane-element.coffee
@@ -64,6 +64,8 @@
     @subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this))
     @subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this))
     @subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this))
+    @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
+
     @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
     this


Add defaultOptions to support missing input options:

For example, when options = { type: 'number' }
It misses lang. Convert function will add back lang value,
according to defaultOptions.

--- src/numbo.coffee
+++ src/numbo.coffee
@@ -47,6 +47,10 @@
 	zhTW: zhTW
 	zhCN: zhCN

+defaultOptions =
+	lang: 'enUS'
+	type: 'number'
+
 ###
 Convert numerical input to a specific language.
 It will pass the whole option object to another modules for further processing.
@@ -55,7 +59,9 @@
 @return {string} - Final output of numbo.
 ###
 convert = (input, options) ->
-	return langs[options.lang](input, options)
+	# Clone defaultOptions, then override it using options
+	oneTimeOptions = Object.assign({}, defaultOptions, options)
+	return langs[oneTimeOptions.lang](input, oneTimeOptions)

 class Numbo


Commented out this for now cause they break stuff locally.

--- server/init.coffee
+++ server/init.coffee
@@ -10,7 +10,8 @@
 ###

 # TODO: May not need hopefully at some point
-process.env.PORT = 80
-process.env.ROOT_URL = "http://qa.colabs.biz/"
-process.env.MONGO_URL = "mongodb://qa.colabs.biz:82/meteor"
-process.env.BIND_IP = "qa.colabs.biz"+#This can't go on local environments....it breaks the build
+#process.env.PORT = 80
+#process.env.ROOT_URL = "http://qa.colabs.biz/"
+#process.env.MONGO_URL = "mongodb://qa.colabs.biz:82/meteor"
+#process.env.BIND_IP = "qa.colabs.biz"


Fix tooltips in PDF buttons

Fix tooltips in PDF buttons

--- client/messagePDF.coffee
+++ client/messagePDF.coffee
@@ -115,16 +115,22 @@

 Template.messagePDF.events
   'click .prevPage': (e, t) ->
+    e.currentTarget.blur()
     if t.page.get() > 1
       t.page.set t.page.get() - 1
       t.renderPage?()
   'click .nextPage': (e, t) ->
+    e.currentTarget.blur()
     if t.page.get() < t.pages.get()
       t.page.set t.page.get() + 1
       t.renderPage?()
   'click .fitWidth': (e, t) ->
+    e.currentTarget.blur()
+    tooltipHide t  ## because button disappears from DOM
     t.fit.set 'width'
     t.renderPage?()
   'click .fitPage': (e, t) ->
+    e.currentTarget.blur()
+    tooltipHide t  ## because button disappears from DOM
     t.fit.set 'page'
     t.renderPage?()


Remove deprecated fs.exists

Remove deprecated fs.exists

--- src/webpack.config.coffee
+++ src/webpack.config.coffee
@@ -182,11 +182,13 @@
         symto = path.resolve(__dirname, "#{MATHJAX_LIB}")
         console.log("mathjax symlink: pointing to #{symto}")
         mksymlink = (dir, cb) ->
-            fs.exists dir,  (exists, cb) ->
-                if not exists
+            fs.access dir, (err) ->
+                if err
                     fs.symlink(symto, dir, cb)
-        compiler.plugin "done", (compilation, cb) ->
-            async.concat([MATHJAX_ROOT, misc_node.MATHJAX_NOVERS], mksymlink, -> cb())
+        done = (compilation) ->
+            async.concat([MATHJAX_ROOT, misc_node.MATHJAX_NOVERS], mksymlink)
+        plugin = name: 'MathjaxVersionedSymlink'
+        compiler.hooks.done.tap(plugin, done)

 mathjaxVersionedSymlink = new MathjaxVersionedSymlink()


[master] unroll

[master] unroll

--- scraper/guba.coffee
+++ scraper/guba.coffee
@@ -75,12 +75,15 @@
     symbol: symbol
     startPage: startPage
   loo = (page) ->
-    parseSinglePage symbol, page, redis
+    unroll = 4
+    Q.all _.map([page..page + unroll - 1], (page) ->
+      parseSinglePage symbol, page, redis
+    )
     .then (res) ->
-      if res == 0
+      if _.min(res) == 0
         return
       else
-        loo page + 1
+        loo page + unroll
   loo startPage

 parseAll = (redis) ->


Fixed Continue button showing on timed pauses


Add a method that detects 'impossible/impossiburu'

--- scripts/custom.coffee
+++ scripts/custom.coffee
@@ -16,4 +16,8 @@
   # Super simple example: If the robot detects the word "thank you",
   # it just responds it with "you're welcome".
   robot.hear /(.*)thank you(.*)/i, (res) ->
-    res.send "you're welcome."+    res.send "you're welcome."
+
+  robot.hear /(.*)(impossible|impossiburu)(.*)/i, (res) ->
+    imageMe res,'impossiburu', (url) ->
+      msg.send url


should be better meow

should be better meow

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -64,7 +64,7 @@
   Trello = require("node-trello");
   t = new Trello(process.env.TRELLO_API_KEY, process.env.TRELLO_TOKEN);
   args =
-    name: msg.match[1]
+    name: msg.match[2]
     idList: process.env.TRELLO_LIST_ID

   t.post "/1/cards", args, (err, data) ->


Player: implemented shell registry mechanism

--- coffee/src/player.coffee
+++ coffee/src/player.coffee
@@ -19,3 +19,39 @@
     @view = new acorn.player.PlayerView
       model: {acornModel: @acornModel, shellModel: @shellModel}
       eventhub: @
+
+    @registerShellModule (shellModule) =>
+      # static shell registry; created on first call
+      @shellRegistry ?= {}
+
+      # validate that shellModule contains required String properties
+      required_properties = [ 'id', 'title', 'description' ]
+      _.each required_properties, (property) ->
+        unless shellModule[property]?
+          MissingParameterError "shell registration", property
+
+        unless _.isString shellModule[property]
+          TypeError property, 'str'
+
+      # validate that shellModule contains a Model object
+      unless shellModule.Model?
+        MissingParameterError "shell registration", "Model"
+
+      # populate shellModule with default properties from Shell
+      _.each acorn.shells.Shell, (value, key) ->
+        unless shellModule[key]?
+          shellModule[key] = _.clone value
+
+      # validate shellModule's Model and View properties
+      class_properties = [ 'Model', 'ContentView', 'SummaryView', 'EditView' ]
+      _.each view_properties, (property) ->
+        unless _.isFunction shellModule[property]
+          TypeError property, 'class'
+
+      # set the `shell` property of each class within shellModule to point
+      # back to the shellModule namespace
+      _.each class_properties, (property) ->
+        shellModule[property].shell = shellModule
+
+      # register shell
+      @shellRegistry[shellModule.id] = shellModule


Honestly update recommendations info via WS [#14]

--- coffee/views/post.coffee
+++ coffee/views/post.coffee
@@ -74,4 +74,22 @@
       @$(".post-comments-count").text(data.num)

     updRecommendations: (data) ->
+      @model.set "recommendations", data.recommendations
+      if @singlePost
+        # It's ok to rerender full post since .post-footer2 must be
+        # completely rerendered.
+        @render()
+        return
+
+      # Actually we could rerender on any small websocket event but
+      # we do some micro-optimizations.
+      # TODO: Or we may fuck it up.
       @$(".post-recommendations-count").text(data.num)
+      attrs = @model.getAttributes()
+      i = @$(".post-recommendations-info")\
+          .attr("title", attrs.recommendationsTitle).children("i")
+      if utils.isLogged()
+        if attrs.recommended
+          i.addClass("icon-marked")
+        else
+          i.removeClass("icon-marked")


add jojo neta

add jojo neta

--- scripts/neta.coffee
+++ scripts/neta.coffee
@@ -1,5 +1,5 @@
 # Description:
-#   Pugme is the most important thing in your life
+#   Jojo Neta
 #
 # Dependencies:
 #   None
@@ -8,8 +8,8 @@
 #   None
 #
 # Commands:
-#   hubot pug me - Receive a pug
-#   hubot pug bomb N - get N pugs
+#   hubot daga - get daga kotowaru
+#   hubot tururu- get tururu

 module.exports = (robot) ->
   robot.respond /daga/i, (msg) ->
@@ -18,3 +18,36 @@
 module.exports = (robot) ->
   robot.respond /tururu/i, (msg) ->
     msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120712/15/1022545/47/329x237x2c75f6b1e78fdf342e6180c2.jpg"
+
+module.exports = (robot) ->
+  robot.respond /papau/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120719/15/1022545/3/302x310x418a0e9bee324ec2c22ab267.jpg"
+
+module.exports = (robot) ->
+  robot.respond /cool/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120811/15/1022545/1/269x368x0a0a6404a3651c4099b44e6f.jpg"
+
+module.exports = (robot) ->
+  robot.respond /memeta/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120710/15/1022545/54/458x441xb84f57c1c451ce1448a99c93.jpg"
+
+module.exports = (robot) ->
+  robot.respond /hinjaku/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120710/15/1022545/49/257x326x849652ae0d45097c66e944a3.jpg"
+
+module.exports = (robot) ->
+  robot.respond /yameru/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120710/15/1022545/45/220x284x98cffe07850629bb5ff4c8f4.jpg"
+
+module.exports = (robot) ->
+  robot.respond /nakumade/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120710/15/1022545/51/404x514x26ca2e71037ebf04e8e8f3e4.jpg"
+
+module.exports = (robot) ->
+  robot.respond /sasuga/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20120710/15/1022545/44/200x272xa51966b07fad7b0e3c3f745a.jpg"
+
+module.exports = (robot) ->
+  robot.respond /yameta/i, (msg) ->
+    msg.send "http://imgcc.naver.jp/kaze/mission/USER/20130131/15/1022545/8/272x454x786ec3abff91dd3f5b42789f.jpg"
+


bits: Add get_string()

bits: Add get_string()

--- bits.coffee
+++ bits.coffee
@@ -356,6 +356,15 @@
     startIndex = @marks.pop()
     return @buf[startIndex..@byte_index-1]

+  # Returns a null-terminated string
+  get_string: (encoding='utf8') ->
+    nullPos = @buf.indexOf 0x00, @byte_index
+    if nullPos is -1
+      throw new Error "bits.get_string: the string is not null-terminated"
+    str = @buf[@byte_index...nullPos].toString encoding
+    @byte_index = nullPos + 1
+    return str
+
   @searchBytesInArray: (haystack, needle, from_pos=0) ->
     if buffertools?  # buffertools is available
       if haystack not instanceof Buffer


fix for #850

fix for #850

--- src/pivot.coffee
+++ src/pivot.coffee
@@ -763,11 +763,11 @@
                                         return true if real_filter.length == 0
                                         return Math.sign(sorter(v.toLowerCase(), real_filter)) in accepted
                                     accept =
-                                        if      filter.startsWith(">=") then accept_gen(">=", [1,0])
-                                        else if filter.startsWith("<=") then accept_gen("<=", [-1,0])
-                                        else if filter.startsWith(">")  then accept_gen(">",  [1])
-                                        else if filter.startsWith("<")  then accept_gen("<",  [-1])
-                                        else if filter.startsWith("~")  then (v) ->
+                                        if      filter.indexOf(">=") == 0 then accept_gen(">=", [1,0])
+                                        else if filter.indexOf("<=") == 0 then accept_gen("<=", [-1,0])
+                                        else if filter.indexOf(">") == 0  then accept_gen(">",  [1])
+                                        else if filter.indexOf("<") == 0  then accept_gen("<",  [-1])
+                                        else if filter.indexOf("~") == 0  then (v) ->
                                                 return true if filter.substring(1).trim().length == 0
                                                 v.toLowerCase().match(filter.substring(1))
                                         else (v) -> v.toLowerCase().indexOf(filter) != -1


Rename the traversal orderings.

Rename the traversal orderings.

--- src/range.coffee
+++ src/range.coffee
@@ -86,19 +86,19 @@
       return true

     # Find the start TextNode.
-    # The guarantees above provide that a pre-order traversal visits every
+    # The guarantees above provide that a document order traversal visits every
     # Node in the Range before visiting the last leaf of the end container.
     node = startContainer
-    next = (node) -> node isnt last and preFirst(node) or null
+    next = (node) -> node isnt last and documentForward(node) or null
     last = lastLeaf(endContainer)
     node = next(node) while not isTextNodeInRange(node)
     start = node

     # Find the end TextNode.
-    # Similarly, a reverse pre-order traversal visits every Node in the Range
-    # before visiting the first leaf of the start container.
+    # Similarly, a reverse document order traversal visits every Node in the
+    # Range before visiting the first leaf of the start container.
     node = endContainer
-    next = (node) -> node isnt last and preLast(node) or null
+    next = (node) -> node isnt last and documentReverse(node) or null
     last = firstLeaf(startContainer)
     node = next(node) while not isTextNodeInRange(node)
     end = node
@@ -161,14 +161,14 @@

     if not contains(bounds, @start)
       node = bounds
-      next = (node) -> node isnt last and preFirst(node) or null
+      next = (node) -> node isnt last and documentForward(node) or null
       last = lastLeaf(bounds)
       node = next(node) while not isTextNode(node)
       @start = node

     if not contains(bounds, @end)
       node = bounds
-      next = (node) -> node isnt last and preLast(node) or null
+      next = (node) -> node isnt last and documentReverse(node) or null
       last = firstLeaf(bounds)
       node = next(node) while not isTextNode(node)
       @end = node
@@ -333,15 +333,15 @@
 # The predicate function is invoked once for each Node in the tree.
 # An optional third argument specifies a traversal order and should be a
 # function that takes a Node and returns its successor. The default order is
-# DOM position order (pre-order).
-everyNode = (node, fn, next = preFirst) ->
+# document order.
+everyNode = (node, fn, next = documentForward) ->
   return !someNode(node, ((n) -> !fn(n)), next)

 # Return the first Node in the tree that matches the predicate.
 # An optional third argument specifies a traversal and should be a function
 # that returns the successor of its argument. The default is document order.
-findNode = (node, fn, next = preFirst) ->
+findNode = (node, fn, next = documentForward) ->
   node = next(node) while node? and not result = fn(node)
   return (if result then node else undefined)

@@ -349,14 +349,14 @@
 # Return true if the given predicate is true for any Node of the tree.
 # An optional third argument specifies a traversal and should be a function
 # that returns the successor of its argument. The default is document order.
-someNode = (node, fn, next = preFirst) ->
+someNode = (node, fn, next = documentForward) ->
   return !!findNode(node, fn, next)

 # Return an Array of each Node in the tree for which the predicate is true.
 # An optional third argument specifies a traversal and should be a function
 # that returns the successor of its argument. The default is document order.
-filterNode = (node, fn, next = preFirst) ->
+filterNode = (node, fn, next = documentForward) ->
   collect = (acc, n) -> if fn(n) then acc.concat([n]) else acc
   return reduceNode(node, collect, [], next)

@@ -364,21 +364,21 @@
 # Return an Array of the result of applying a function to each Node in a tree.
 # An optional third argument specifies a traversal and should be a function
 # that returns the successor of its argument. The default is document order.
-mapNode = (node, fn, next = preFirst) ->
+mapNode = (node, fn, next = documentForward) ->
   return reduceNode(node, ((acc, n) -> acc.push(fn(n))), [], next)

-reduceNode = (node, fn, initialValue = undefined, next = preFirst) ->
+reduceNode = (node, fn, initialValue = undefined, next = documentForward) ->
   acc = initialValue
   last = lastLeaf(node)
   if arguments.length is 2
     if node is last then return node
     acc = node
-    node = preFirst(node)
+    node = documentForward(node)
   else
     acc = fn(acc, node)

-  while node = preFirst(node)
+  while node = documentForward(node)
     acc = fn(acc, node)
     if node is last then break

@@ -399,8 +399,9 @@
   return node.nodeType is TEXT_NODE

-# Return the next Node in a first-to-last-child pre-order traversal.
-preFirst = (node) ->
+# Return the next Node in a document order traversal.
+# This order is equivalent to a classic pre-order.
+documentForward = (node) ->
   if node.firstChild?
     return node.firstChild

@@ -411,8 +412,9 @@
   return node.nextSibling

-# Return the next Node in a last-to-first-child pre-order traversal.
-preLast = (node) ->
+# Return the next Node in a reverse document order traversal.
+# This order is equivalent to pre-order with the child order reversed.
+documentReverse = (node) ->
   if node.lastChild?
     return node.lastChild


Suggest better alternative in deprecation warnings

--- src/editor-view.coffee
+++ src/editor-view.coffee
@@ -214,22 +214,38 @@
     React.unmountComponentAtNode(@element) if @component.isMounted()

   splitLeft: ->
-    deprecate 'Use Pane::splitLeft instead. To duplicate this editor into the split use: editorView.getPane().splitLeft(editorView.getPane().copyActiveItem())'
+    deprecate """
+      Use Pane::splitLeft instead.
+      To duplicate this editor into the split use:
+      editorView.getPaneView().getModel().splitLeft(copyActiveItem: true)
+    """
     pane = @getPane()
     pane?.splitLeft(pane?.copyActiveItem()).activeView

   splitRight: ->
-    deprecate 'Use Pane::splitRight instead. To duplicate this editor into the split use: editorView.getPane().splitRight(editorView.getPane().copyActiveItem())'
+    deprecate """
+      Use Pane::splitRight instead.
+      To duplicate this editor into the split use:
+      editorView.getPaneView().getModel().splitRight(copyActiveItem: true)
+    """
     pane = @getPane()
     pane?.splitRight(pane?.copyActiveItem()).activeView

   splitUp: ->
-    deprecate 'Use Pane::splitUp instead. To duplicate this editor into the split use: editorView.getPane().splitUp(editorView.getPane().copyActiveItem())'
+    deprecate """
+      Use Pane::splitUp instead.
+      To duplicate this editor into the split use:
+      editorView.getPaneView().getModel().splitUp(copyActiveItem: true)
+    """
     pane = @getPane()
     pane?.splitUp(pane?.copyActiveItem()).activeView

   splitDown: ->
-    deprecate 'Use Pane::splitDown instead. To duplicate this editor into the split use: editorView.getPane().splitDown(editorView.getPane().copyActiveItem())'
+    deprecate """
+      Use Pane::splitDown instead.
+      To duplicate this editor into the split use:
+      editorView.getPaneView().getModel().splitDown(copyActiveItem: true)
+    """
     pane = @getPane()
     pane?.splitDown(pane?.copyActiveItem()).activeView


Add Spec T004-003

Add Spec T004-003

--- spec/src/coffee/label_category/models/t004_label_category_spec.coffee
+++ spec/src/coffee/label_category/models/t004_label_category_spec.coffee
@@ -1,17 +1,18 @@
 should = require "should"

 describe "T004: LabelCategory::Models::LabelCategory", ->
-  beforeEach ->
+  before ->
     @LabelCategory = requirejs "app/label_category"
     @Label         = requirejs "app/label"

-    # set root node
-    @root = new @LabelCategory.Models.LabelCategory
-    @fake_label =
-      set:  sinon.spy()
-      on:   sinon.spy()
+  context "001: parsing", ->
+    beforeEach ->
+      # set root node
+      @root = new @LabelCategory.Models.LabelCategory
+      @fake_label =
+        set:  sinon.spy()
+        on:   sinon.spy()

-  context "001: parsing", ->
     context "001: test1/test2/test3", ->
       beforeEach ->
         @root.parse_labels_recursive_func "test1/test2/test3", @fake_label
@@ -63,10 +64,30 @@
         @root.get("childrens")["b"].get("childrens")["a"].get("childrens")["a"].get("name").should.equal "a"

   context "002: properties", ->
+    beforeEach ->
+      @root = new @LabelCategory.Models.LabelCategory
+
     context "001: root", ->
       beforeEach ->
         @root.set "name", "this is root"

-      it "001 get name", ->
+      it "001: get name", ->
         @root.get("name").should.equal "this is root"

+  context "003: label watching", ->
+    beforeEach ->
+      @label = new @Label.Models.Label(
+        name: "this is label"
+        color: "777777"
+      )
+      @root = new @LabelCategory.Models.LabelCategory()
+
+    context "001: destroy", ->
+      it "001: destroy with label", ->
+        destroy_event_spy = sinon.spy()
+        @root.set_label @label
+        @label.destroy()
+        should.strictEqual @root.get("label"), undefined
+
+
+


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -45,12 +45,9 @@

   player.update()

-  equals player.I.velocity.y, 19
+  equals player.I.velocity.y, -3

-  player.update()
-
-  equals player.I.y, 18
-
+
   window.keydown.up = false

   equals player.I.y, 18


Rewrite event extension to be clearer and better factored.

Add and refactor with getComputedState,
setupCheckIfNeeded, teardownCheckIfNeeded, and
performCheck.

Replace abbreviations with English: ns, m, evt,
$t, dat (to attr), nsDat, nsLog (refactored out).

Dasherize data names. Refactor getting state to be
more DRY, which required renaming hlf-hover-intent
property to hlf-hover-intent-intentional.

Refactor and improve logging. Also use it to
obviate some of the inline comments.

Minor white-spacing and reformatting.

--- src/jquery.extension.hlf.event.coffee
+++ src/jquery.extension.hlf.event.coffee
@@ -7,7 +7,7 @@
 # jQuery Event Extension
 # ======================

-extension = ($, hlf) ->
+extension = ($, _, hlf) ->

   # Composed of three parts:
   #
@@ -27,9 +27,8 @@
       interval: 300
       toString: (context) ->
         switch context
-          when 'data' then 'hlfHoverIntent'
-          when 'log'  then 'hover-intent:'
-          else 'hlf.HoverIntent'
+          when 'attr' then 'hlf-hover-intent'
+          else 'hlf.hoverIntent'
     mouse:
       x:
         current: 0
@@ -38,80 +37,100 @@
         current: 0
         previous: 0

-  do (ns=hlf.hoverIntent, m=hlf.mouse) ->
+  do (hoverIntent = hlf.hoverIntent, mouse = hlf.mouse) ->

-    nsDat = ns.toString 'data'
-    nsLog = ns.toString 'log'
-    dat = (name) -> "#{nsDat}#{name}"
-    log = if ns.debug is on then hlf.log else $.noop
+    attr = (name='') -> "#{hoverIntent.toString 'attr'}#{name}"

-    check = (evt) ->
-      # `$t` for trigger.
-      $t = $ @
-      # - Get state.
-      intentional = $t.data(dat()) or yes # Default to show at first.
-      timer = $t.data(dat('Timer')) or { cleared: no, timeout:null }
-      sensitivity = $t.data(dat('Sensitivity')) or ns.sensitivity
-      interval = $t.data(dat('Interval')) or ns.interval
-      # - Guard.
-      if evt.type is 'mouseleave'
-        if timer.cleared is no
-          # - Clear state.
-          clearTimeout timer.timeout
-          $t.removeData(dat('Timer')).removeData(dat())
-        # - Trigger.
-        $t.trigger 'truemouseleave'
-        log nsLog, 'truemouseleave'
-        return
-      return if timer.cleared is no and timer.timeout?
-      # - Test.
-      timer.timeout = setTimeout ->
-        # log nsLog, 'timeout'
-        # - Measure and track.
-        intentional = Math.abs(m.x.previous - m.x.current) + Math.abs(m.y.previous - m.y.current) > sensitivity
-        intentional = intentional
-        m.x.previous = evt.pageX
-        m.y.previous = evt.pageY
-        if intentional is yes and evt.type is 'mouseover'
-          # - Trigger.
-          $t.trigger new $.Event 'truemouseenter', { pageX: m.x.current, pageY: m.y.current }
-          log nsLog, 'truemouseenter'
-        # - Save state.
-        timer.cleared = yes
-        $t.data dat(), intentional
-        $t.data dat('Timer'), timer
-      , interval
-      # - Save state.
-      timer.cleared = no
-      $t.data dat('Timer'), timer
+    log = $.noop
+    if hoverIntent.debug is on
+      arguments
+      log = -> hlf.log attr(), arguments...

-    track = (evt) ->
-      m.x.current = evt.pageX
-      m.y.current = evt.pageY
+    defaultState =
+      intentional: yes
+      timer: { cleared: no, timeout: null }
+      sensitivity: hoverIntent.sensitivity
+      interval: hoverIntent.interval
+
+    getComputedState = ($trigger) ->
+      state = {}
+      for key, value of defaultState
+        if $.isPlainObject value then value = _.clone value
+        state[key] = $trigger.data(attr(key)) or value
+      state
+
+    check = (event) ->
+      $trigger = $ @
+      state = getComputedState $trigger
+      log state
+      didTeardown = teardownCheckIfNeeded event, $trigger, state
+      if didTeardown is no then setupCheckIfNeeded event, $trigger, state
+
+    setupCheckIfNeeded = (event, $trigger, state) ->
+      return no if state.timer.cleared is no and state.timer.timeout?
+      log 'setup'
+      state.timer.timeout = setTimeout ->
+        log 'check and update'
+        performCheck event, $trigger, state
+      , state.interval
+      state.timer.cleared = no
+      $trigger.data attr('timer'), state.timer
+      return yes
+
+    teardownCheckIfNeeded = (event, $trigger, state) ->
+      if event.type is 'mouseleave'
+        log 'teardown'
+        if state.timer.cleared is no
+          clearTimeout state.timer.timeout
+          $trigger
+            .removeData attr('timer')
+            .removeData attr('intentional')
+        log 'truemouseleave'
+        $trigger.trigger 'truemouseleave'
+        return yes
+      return no
+
+    performCheck = (event, $trigger, state) ->
+      state.intentional =
+        (Math.abs(mouse.x.previous - mouse.x.current) +
+         Math.abs(mouse.y.previous - mouse.y.current)) >
+        state.sensitivity
+      mouse.x.previous = event.pageX
+      mouse.y.previous = event.pageY
+      if state.intentional is yes and event.type is 'mouseover'
+        log 'truemouseenter'
+        $trigger.trigger new $.Event(
+          'truemouseenter',
+          { pageX: mouse.x.current, pageY: mouse.y.current }
+        )
+      state.timer.cleared = yes
+      $trigger.data attr('intentional'), state.intentional
+      $trigger.data attr('timer'), state.timer
+
+
+    track = (event) ->
+      mouse.x.current = event.pageX
+      mouse.y.current = event.pageY

     $.event.special.truemouseenter =
       setup: (data, namespaces) ->
-        $(@).on
-          mouseover:  check
-          mousemove:  track
-
+        $(@).on   { mouseover: check, mousemove: track }
       teardown: (data, namespaces) ->
-        $(@).off
-          mouseover:  check
-          mousemove:  track
+        $(@).off  { mouseover: check, mousemove: track }

     $.event.special.truemouseleave =
       setup: (data, namespaces) ->
-        $(@).on
-          mouseleave: check
-
+        $(@).on   { mouseleave: check }
       teardown: (data, namespaces) ->
-        $(@).off
-          mouseleave: check
+        $(@).off  { mouseleave: check }

 # Export. Prefer AMD.
 if define? and define.amd?
-  define ['jquery', 'hlf/jquery.extension.hlf.core'], extension
+  define [
+    'jquery'
+    'underscore'
+    'hlf/jquery.extension.hlf.core'
+  ], extension
 else
-  extension jQuery, jQuery.hlf
+  extension jQuery, _, jQuery.hlf


button links to contact page

button links to contact page

--- app/assets/javascripts/home.coffee
+++ app/assets/javascripts/home.coffee
@@ -14,12 +14,14 @@
   $close_btn = null
   scrollClass = "is-scrolled"

+
   $($footerSubmit).click ->
     $submittedEmail = $('.input__text').val()
+    window.location.replace '/contact'
+    $('#mce-EMAIL').val($submittedEmail)
     console.log($submittedEmail)
-    window.location.replace 'http://www.doodles-academy.org/contact'
-    $('input#mce-EMAIL').val($submittedEmail)
-    return
+    return
+

   onScroll = ->
     top = $document.scrollTop()


Clean up routing problem during initialization.

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -94,6 +94,17 @@
     # Clear flash messages on route change.
     pwgFlash.clear 'all'

+  $scope.$on '$locationChangeStart', (e) ->
+    # Skip, while initializing. (Doing this during initialization screws
+    # things up, causing multiple redirects that play games with Angular.)
+    unless $scope.initializing
+      segment = window.segmentForURL $location.path()
+      useSegment = validateLocationChange segment
+      $scope.log.debug "segment=#{segment}, useSegment=#{useSegment}"
+      if useSegment isnt segment
+        e.preventDefault()
+        $scope.redirectToSegment useSegment
+
   # Page-handling.

   # Convenient way to show a page/segment
@@ -102,6 +113,7 @@
     url = $scope.pathForSegment segment
     if url?
       $scope.log.debug "Redirecting to #{url}"
+      $scope.log.debug (new Error("trace").stack)
       $location.path(url)
     else
       console.log "(BUG) No URL for segment #{segment}"
@@ -153,14 +165,6 @@
         useSegment = 'login'

     useSegment
-
-  $scope.$on '$locationChangeStart', (e) ->
-    segment = window.segmentForURL $location.path()
-    useSegment = validateLocationChange segment
-    $scope.log.debug "segment=#{segment}, useSegment=#{useSegment}"
-    if useSegment isnt segment
-      e.preventDefault()
-      $scope.redirectToSegment useSegment

   userPromise        = pwgCheckUser.checkUser()
   browserInfoPromise = pwgGetBrowserInfo.getBrowserInfo()


fixed a typo which was causing a crash

fixed a typo which was causing a crash

--- components/ConCat.coffee
+++ components/ConCat.coffee
@@ -10,7 +10,7 @@
       delimiter:
         datatype: 'string'
         description: 'String used to concatenate input strings'
-      addString:
+      addstring:
         datatype: 'string'
         description: 'first string'
       in:
@@ -22,10 +22,10 @@

     @inPorts.delimiter.on 'data', (data) =>
       @delimiter = data
-    @inPorts.addString.on 'data', (data) =>
+    @inPorts.addstring.on 'data', (data) =>
       @addString = data
     @inPorts.in.on 'data', (data) =>
-      @outPorts.out.send data.concat @delimiter, @addString
+      @outPorts.out.send data.concat @delimiter, @addstring
     @inPorts.in.on 'disconnect', =>
       @outPorts.out.disconnect()


fix(db-spec): Accompanying spec changes

fix(db-spec): Accompanying spec changes

--- spec/models/query-subscription-spec.coffee
+++ spec/models/query-subscription-spec.coffee
@@ -81,7 +81,6 @@
         ]
         mustUpdate: false
         mustTrigger: true
-        mustRefetchAllIds: false
       },{
         name: 'Item in set saved - new sort value'
         change:
@@ -94,9 +93,8 @@
           new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3),
           new Thread(accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2),
         ]
-        mustUpdate: false
+        mustUpdate: true
         mustTrigger: true
-        mustRefetchAllIds: true
       },{
         name: 'Item saved - does not match query clauses, offset > 0'
         change:
@@ -105,7 +103,6 @@
           type: 'persist'
         nextModels: 'unchanged'
         mustUpdate: true
-        mustRefetchAllIds: true
       },{
         name: 'Item saved - matches query clauses'
         change:
@@ -113,7 +110,6 @@
           objects: [new Thread(accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: -2)]
           type: 'persist'
         mustUpdate: true
-        mustRefetchAllIds: true
       },{
         name: 'Item in set saved - no longer matches query clauses'
         change:
@@ -126,7 +122,6 @@
           new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1),
         ]
         mustUpdate: true
-        mustRefetchAllIds: false
       },{
         name: 'Item in set deleted'
         change:
@@ -139,7 +134,6 @@
           new Thread(accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1),
         ]
         mustUpdate: true
-        mustRefetchAllIds: false
       },{
         name: 'Item not in set deleted'
         change:
@@ -148,7 +142,6 @@
           type: 'unpersist'
         nextModels: 'unchanged'
         mustUpdate: false
-        mustRefetchAllIds: false
       }]

     },{
@@ -174,7 +167,6 @@
           objects: [new Thread(accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: true)]
           type: 'persist'
         mustUpdate: true
-        mustRefetchAllIds: true
       }]
     }]

@@ -193,31 +185,29 @@
             subscription._updateInFlight = false
             subscription.applyChangeRecord(test.change)

-            if test.mustRefetchAllIds
-              expect(subscription._set).toBe(null)
-            else if test.nextModels is 'unchanged'
-              expect(subscription._set.models()).toEqual(scenario.lastModels)
+            if test.mustUpdate
+              expect(subscription.update).toHaveBeenCalledWith({mustRefetchEntireRange: true})
             else
-              expect(subscription._set.models()).toEqual(test.nextModels)
-
-            if test.mustUpdate
-              expect(subscription.update).toHaveBeenCalled()
+              if test.nextModels is 'unchanged'
+                expect(subscription._set.models()).toEqual(scenario.lastModels)
+              else
+                expect(subscription._set.models()).toEqual(test.nextModels)

             if test.mustTriger
               expect(subscription._createResultAndTrigger).toHaveBeenCalled()

   describe "update", ->
     beforeEach ->
-      spyOn(QuerySubscription.prototype, '_fetchMissingRange').andCallFake ->
+      spyOn(QuerySubscription.prototype, '_fetchRange').andCallFake ->
         @_set ?= new MutableQueryResultSet()
         Promise.resolve()

     describe "when the query has an infinite range", ->
-      it "should call _fetchMissingRange for the entire range", ->
+      it "should call _fetchRange for the entire range", ->
         subscription = new QuerySubscription(DatabaseStore.findAll(Thread))
         subscription.update()
         advanceClock()
-        expect(subscription._fetchMissingRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: true, version: 1})
+        expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: true, version: 1})

       it "should fetch full full models only when the previous set is empty", ->
         subscription = new QuerySubscription(DatabaseStore.findAll(Thread))
@@ -225,23 +215,23 @@
         subscription._set.addModelsInRange([new Thread()], new QueryRange(start: 0, end: 1))
         subscription.update()
         advanceClock()
-        expect(subscription._fetchMissingRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: false, version: 1})
+        expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: false, version: 1})

     describe "when the query has a range", ->
       beforeEach ->
         @query = DatabaseStore.findAll(Thread).limit(10)

       describe "when we have no current range", ->
-        it "should call _fetchMissingRange for the entire range and fetch full models", ->
+        it "should call _fetchRange for the entire range and fetch full models", ->
           subscription = new QuerySubscription(@query)
           subscription._set = null
           subscription.update()
           advanceClock()
-          expect(subscription._fetchMissingRange).toHaveBeenCalledWith(@query.range(), {fetchEntireModels: true, version: 1})
+          expect(subscription._fetchRange).toHaveBeenCalledWith(@query.range(), {fetchEntireModels: true, version: 1})

       describe "when we have a previous range", ->

-        it "should call _fetchMissingRange with the missingRange", ->
+        it "should call _fetchRange with the missingRange", ->
           customRange = jasmine.createSpy('customRange1')
           spyOn(QueryRange, 'rangesBySubtracting').andReturn [customRange]
           subscription = new QuerySubscription(@query)
@@ -249,14 +239,14 @@
           subscription._set.addModelsInRange([new Thread()], new QueryRange(start: 0, end: 1))

           advanceClock()
-          subscription._fetchMissingRange.reset()
+          subscription._fetchRange.reset()
           subscription._updateInFlight = false
           subscription.update()
           advanceClock()
-          expect(subscription._fetchMissingRange.callCount).toBe(1)
-          expect(subscription._fetchMissingRange.calls[0].args).toEqual([customRange, {fetchEntireModels: true, version: 1}])
-
-        it "should call _fetchMissingRange for the entire query range when the missing range encompasses more than one range", ->
+          expect(subscription._fetchRange.callCount).toBe(1)
+          expect(subscription._fetchRange.calls[0].args).toEqual([customRange, {fetchEntireModels: true, version: 1}])
+
+        it "should call _fetchRange for the entire query range when the missing range encompasses more than one range", ->
           customRange1 = jasmine.createSpy('customRange1')
           customRange2 = jasmine.createSpy('customRange2')
           spyOn(QueryRange, 'rangesBySubtracting').andReturn [customRange1, customRange2]
@@ -267,9 +257,9 @@
           subscription._set.addModelsInRange([new Thread()], range)

           advanceClock()
-          subscription._fetchMissingRange.reset()
+          subscription._fetchRange.reset()
           subscription._updateInFlight = false
           subscription.update()
           advanceClock()
-          expect(subscription._fetchMissingRange.callCount).toBe(1)
-          expect(subscription._fetchMissingRange.calls[0].args).toEqual([@query.range(), {fetchEntireModels: true, version: 1}])
+          expect(subscription._fetchRange.callCount).toBe(1)
+          expect(subscription._fetchRange.calls[0].args).toEqual([@query.range(), {fetchEntireModels: true, version: 1}])


added load indicator [transfers]

added load indicator [transfers]

--- app/js/controllers/transfer.coffee
+++ app/js/controllers/transfer.coffee
@@ -35,7 +35,8 @@
         vote_recommended: "vote_as_delegates_recommended"

     $scope.my_accounts = []
-    Wallet.refresh_accounts().then ->
+    refresh_accounts_promise = Wallet.refresh_accounts()
+    refresh_accounts_promise.then ->
         $scope.accounts = Wallet.accounts
         $scope.my_accounts.splice(0, $scope.my_accounts.length)
         for k,a of Wallet.accounts
@@ -60,6 +61,8 @@
                 else
                     $scope.no_account = true

+    $scope.showLoadingIndicator(refresh_accounts_promise)
+
     Blockchain.get_info().then (config) ->
         $scope.memo_size_max = config.memo_size_max


Remove console logs

Remove console logs

--- app/scripts/lib/LazerScript.coffee
+++ app/scripts/lib/LazerScript.coffee
@@ -255,7 +255,6 @@

     seaLevel = 420
     settings = _.defaults(settings, defaults)
-    console.log settings
     # TODO: Adjust water marker to movement position
     surfaceSize =
       w: enemy.w + 10
@@ -275,7 +274,6 @@
     deltaX = if settings.x? then Math.abs(settings.x - (enemy.x + Crafty.viewport.x)) else 0
     deltaY = if settings.y? then Math.abs(settings.y - (enemy.y + Crafty.viewport.y)) else 0
     delta = Math.max(deltaX, deltaY)
-    #console.log delta

     duration = (delta / settings.speed) * (1000 / Crafty.timer.FPS())

@@ -300,10 +298,7 @@
       delete @enemies[settings.enemy ? 'default']
       defer.resolve()

-    #TODO: Fix this
     enemy.hideMarker.choreography([
-      #type: 'viewport'
-      #x: settings.x
       type: 'follow'
       axis: 'x'
       target: enemy
@@ -323,7 +318,6 @@
       y: enemy.y + Crafty.viewport.y
       speed: enemy.speed

-    # TODO: Verify Choreography element
     settings = _.defaults(settings, defaults)

     deltaX = if settings.x? then Math.abs(settings.x - (enemy.x + Crafty.viewport.x)) else 0


Add outdent for ) and }

Add outdent for ) and }

--- settings/language-xquery.cson
+++ settings/language-xquery.cson
@@ -3,4 +3,4 @@
     'commentStart': '(: '
     'commentEnd': ' :)'
     'increaseIndentPattern': '[\\({]\\s*$|<[^>]+>\\s*$|(then|return)\\s*$|let.*:=\\s*$'
-    'decreaseIndentPattern': '^\\s*</[^>]+>'+    'decreaseIndentPattern': '^\\s*[)}]|^\\s*</[^>]+>'


not needed now

not needed now

--- resources/assets/coffee/_classes/turbolinks-disable.coffee
+++ resources/assets/coffee/_classes/turbolinks-disable.coffee
@@ -19,7 +19,6 @@
 internal = [
   'admin'
   'api/v2'
-  'beatmappacks'
   'beatmaps'
   'beatmapsets'
   'community'


Added full join

Added full join

--- src/squel.coffee
+++ src/squel.coffee
@@ -1093,6 +1093,10 @@
   left_outer_join: (table, alias = null, condition = null) ->
     @join table, alias, condition, 'LEFT OUTER'

+  # Add an FULL JOIN with the given table.
+  outer_join: (table, alias = null, condition = null) ->
+    @join table, alias, condition, 'FULL'
+

   buildStr: (queryBuilder) ->
     joins = ""


Add tooltip for download button

Add tooltip for download button

--- resources/assets/coffee/react/_components/beatmapset-panel.coffee
+++ resources/assets/coffee/react/_components/beatmapset-panel.coffee
@@ -164,6 +164,7 @@
               if currentUser?.id
                 a
                   href: laroute.route 'beatmapsets.download', beatmapset: beatmapset.id
+                  title: osu.trans('beatmapsets.show.details.download._')
                   className: 'beatmapset-panel__icon js-beatmapset-download-link'
                   'data-turbolinks': 'false'
                   i className: 'fas fa-download'


removing this for its own thing

removing this for its own thing

--- src/stati.coffee
+++ src/stati.coffee
@@ -1,30 +0,0 @@
-_ = require "underscore"
-
-# stati is a submodule for localize-api, it will generate counts/stats and more
-
-stati = (opts, model, app) ->
-
-  # we're going to have to do some searching for a model
-  # so we'll give that a quick name.
-  @model = model
-
-  # store our localize options here so we have access to them
-  @localize = if opts? then _.extend @localize, opts else null
-
-  @query = null
-
-  _app = app
-  self = @
-
-stati::find = (query, fn) ->
-
-  q = if query? then query else {}
-  [model].find q, (err, found) ->
-    return if err? then fn err, null
-
-    if found.length > 0 then fn null, found
-
-
-    console.log "mom"
-module.exports = stati
-


Fix level ordering arrows in campaign editor for type: 'course' campaigns

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -422,9 +422,17 @@
       false

     foundNext = false
-    for level in orderedLevels
+    for level, levelIndex in orderedLevels
       # Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level.
-      level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
+      if @campaign.get('type') is 'course'
+        level.nextLevels = []
+        for nextLevel, nextLevelIndex in orderedLevels when nextLevelIndex > levelIndex
+          continue if nextLevel.practice and level.nextLevels.length
+          break if level.practice and not nextLevel.practice
+          level.nextLevels.push nextLevel.original
+          break unless nextLevel.practice
+      else
+        level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
       foundNext = findNextLevel(level.nextLevels, true) unless foundNext # Check practice levels first
       foundNext = findNextLevel(level.nextLevels, false) unless foundNext


Replaced msg with res Added module name to logger

--- src/steam.coffee
+++ src/steam.coffee
@@ -78,30 +78,30 @@
 #
 # Steam API
 #
-GetSteamResult = (msg, endpoint, params = {}, handler, version = 1) ->
+GetSteamResult = (res, endpoint, params = {}, handler, version = 1) ->
     params.key = STEAM_API_KEY

-    msg.http("#{STEAM_API_URL}/#{endpoint}/v#{version}/")
+    res.http("#{STEAM_API_URL}/#{endpoint}/v#{version}/")
         .query(params)
-        .get() (err, res, body) ->
-            if err or res.statusCode isnt 200
-                err = "Bad request (invalid Steam web API key)" if res.statusCode is 400
-                msg.reply "An error occurred while attempting to process your request. Please try again later."
-                log = if msg.logger? then msg.logger else msg.robot.logger
-                return log.error err
+        .get() (err, httpRes, body) ->
+            if err or httpRes.statusCode isnt 200
+                err = "Bad request (invalid Steam web API key)" if httpRes.statusCode is 400
+                res.reply "An error occurred while attempting to process your request. Please try again later."
+                log = if res.logger? then res.logger else res.robot.logger
+                return log.error "hubot-steam-webapi: #{err}"
             handler JSON.parse(body)

 #
 # User API
 #
-GetSteamID = (msg, customURL, callback) ->
+GetSteamID = (res, customURL, callback) ->
     # Retrieve from cache
     return callback steamID for steamID, user of _GetSteamData() when user.url is customURL

     # Retrieve from API
-    GetSteamResult msg, "ISteamUser/ResolveVanityURL", vanityurl: customURL, (object) ->
+    GetSteamResult res, "ISteamUser/ResolveVanityURL", vanityurl: customURL, (object) ->
         if object.response.success is 42
-            msg.reply "The custom URL you have entered (\"#{customURL}\") does not exist."
+            res.reply "The custom URL you have entered (\"#{customURL}\") does not exist."
             return

         # Cache results
@@ -124,9 +124,9 @@
     _GetSteamData()[steamID].cID = communityID
     communityID

-GetPlayerSummaries = (msg, genericID, callback) ->
+GetPlayerSummaries = (res, genericID, callback) ->
     summary = (steamID) ->
-        GetSteamResult msg, "ISteamUser/GetPlayerSummaries", steamids: steamID, (object) ->
+        GetSteamResult res, "ISteamUser/GetPlayerSummaries", steamids: steamID, (object) ->
             callback object.response.players[0]
         , 2

@@ -135,7 +135,7 @@
         summary genericID
     # Custom URL -> Steam ID
     else
-        GetSteamID msg, genericID, (steamID) ->
+        GetSteamID res, genericID, (steamID) ->
             summary steamID

 _GetStatus = (status = 0, invisible = 0) ->
@@ -147,28 +147,28 @@
 #
 # Dota API
 #
-GetHeroes = (msg, callback) ->
-    GetSteamResult msg, "IEconDOTA2_570/GetHeroes", language: "en", (object) ->
+GetHeroes = (res, callback) ->
+    GetSteamResult res, "IEconDOTA2_570/GetHeroes", language: "en", (object) ->
         mappedHeroes = {}
         for hero in object.result.heroes
             mappedHeroes[hero.id] = hero.localized_name
         callback mappedHeroes

-GetMatchDetails = (msg, matchID, callback) ->
-    GetSteamResult msg, "IDOTA2Match_570/GetMatchDetails", match_id: matchID, (object) ->
+GetMatchDetails = (res, matchID, callback) ->
+    GetSteamResult res, "IDOTA2Match_570/GetMatchDetails", match_id: matchID, (object) ->
         callback object.result

-GetMatchHistory = (msg, genericID, callback) ->
+GetMatchHistory = (res, genericID, callback) ->
     history = (steamID, type = "Steam ID") ->
         params =
             account_id: steamID
             matches_requested: DOTA_MAX_RESULTS
-        GetSteamResult msg, "IDOTA2Match_570/GetMatchHistory", params, (object) ->
+        GetSteamResult res, "IDOTA2Match_570/GetMatchHistory", params, (object) ->
             if object.result.status is 15
-                msg.reply "The user has disabled the \"Expose Public Match Data\" option."
+                res.reply "The user has disabled the \"Expose Public Match Data\" option."
                 return
             else if object.result.num_results is 0
-                msg.reply "No game matches were found for the #{type}: #{genericID}."
+                res.reply "No game matches were found for the #{type}: #{genericID}."
             callback steamID, object.result

     # Steam ID
@@ -176,7 +176,7 @@
         history genericID
     # Custom URL -> Steam ID
     else
-        GetSteamID msg, genericID, (steamID) ->
+        GetSteamID res, genericID, (steamID) ->
             history steamID, "profile URL"

 _GetTowers = (dec) ->
@@ -195,7 +195,7 @@
 #
 Init = (robot) ->
     if not STEAM_API_KEY?
-        return robot.logger.error "Missing STEAM_API_KEY in environment. Please set and try again."
+        return robot.logger.error "hubot-steam-webapi: Missing STEAM_API_KEY in environment. Please set and try again."

     GetHeroes robot, (heroes) ->
         DOTA_HEROES = heroes
@@ -214,23 +214,23 @@
     Init robot

     # GET 32-bit Steam ID
-    robot.respond /steam id( me)? (.+)/i, (msg) ->
-        customURL = msg.match[2]
-        GetSteamID msg, customURL, (steamID) ->
-            msg.reply "#{_PossessionModifier(customURL)} Steam ID is: #{steamID}"
+    robot.respond /steam id( me)? (.+)/i, (res) ->
+        customURL = res.match[2]
+        GetSteamID res, customURL, (steamID) ->
+            res.reply "#{_PossessionModifier(customURL)} Steam ID is: #{steamID}"

     # GET Steam profile status
-    robot.respond /steam status (.+)/i, (msg) ->
-        genericID = msg.match[1]
-        GetPlayerSummaries msg, genericID, (player) ->
+    robot.respond /steam status (.+)/i, (res) ->
+        genericID = res.match[1]
+        GetPlayerSummaries res, genericID, (player) ->
             status = _GetStatus player.personastate, player.communityvisibilitystate
             lastOnline = moment.unix(player.lastlogoff).fromNow()
-            msg.reply "#{genericID} belongs to #{player.personaname} who is currently #{status} and was last online #{lastOnline}."
+            res.reply "#{genericID} belongs to #{player.personaname} who is currently #{status} and was last online #{lastOnline}."

     # GET Player Dota 2 match history (overview)
-    robot.respond /dota history (.+)/i, (msg) ->
-        genericID = msg.match[1]
-        GetMatchHistory msg, genericID, (steamID, history) ->
+    robot.respond /dota history (.+)/i, (res) ->
+        genericID = res.match[1]
+        GetMatchHistory res, genericID, (steamID, history) ->
             communityID = _GetCommunityID steamID

             for match in history.matches
@@ -238,13 +238,13 @@
                 target = _GetPlayer communityID, match.players
                 hero = DOTA_HEROES[target.hero_id] or "No hero"
                 # TODO: W / L TBA
-                msg.send "Match ID: #{match.match_id} | Lobby: #{DOTA_LOBBIES[match.lobby_type]} | Hero: #{hero} | #{date}"
+                res.send "Match ID: #{match.match_id} | Lobby: #{DOTA_LOBBIES[match.lobby_type]} | Hero: #{hero} | #{date}"

     # GET Player Dota 2 match details
-    robot.respond /dota match (\d+)\s*(.+)?/i, (msg) ->
-        matchID = msg.match[1]
-        genericID = msg.match[2]
-        GetMatchDetails msg, matchID, (match) ->
+    robot.respond /dota match (\d+)\s*(.+)?/i, (res) ->
+        matchID = res.match[1]
+        genericID = res.match[2]
+        GetMatchDetails res, matchID, (match) ->
             date = moment.unix(match.start_time).fromNow()
             duration = moment.duration(match.duration, "seconds").minutes()
             firstBlood = moment.duration(match.first_blood_time, "seconds").humanize()
@@ -252,26 +252,26 @@
             radiantTowers = _GetTowers(match.tower_status_radiant).join(", ") or "None"
             direTowers = _GetTowers(match.tower_status_dire).join(", ") or "None"

-            msg.send "Match ID #{match.match_id} is a #{DOTA_LOBBIES[match.lobby_type].toLowerCase()} game that took place #{date}. The #{victor} won the game in #{duration} minutes. First blood was drawn #{firstBlood} into the game."
+            res.send "Match ID #{match.match_id} is a #{DOTA_LOBBIES[match.lobby_type].toLowerCase()} game that took place #{date}. The #{victor} won the game in #{duration} minutes. First blood was drawn #{firstBlood} into the game."

             # Generate map image and graphs?
-            msg.send "Radiant towers remaining: #{radiantTowers} | Dire towers remaining: #{direTowers}"
-            msg.send "Dotabuff: http://www.dotabuff.com/matches/#{match.match_id}"
+            res.send "Radiant towers remaining: #{radiantTowers} | Dire towers remaining: #{direTowers}"
+            res.send "Dotabuff: http://www.dotabuff.com/matches/#{match.match_id}"

             additionalInfo = (steamID, type = "Steam ID") ->
                 communityID = _GetCommunityID steamID
                 target = _GetPlayer communityID, match.players
                 unless target
-                    msg.reply "The #{type} you have entered (\"#{genericID}\") was not found in Match ID #{match.match_id}."
+                    res.reply "The #{type} you have entered (\"#{genericID}\") was not found in Match ID #{match.match_id}."
                     return
                 faction = _GetFaction target.player_slot
                 hero = DOTA_HEROES[target.hero_id] or "No hero"
-                msg.reply "#{faction} - #{hero} (Lvl #{target.level}) | KDA: #{target.kills}/#{target.deaths}/#{target.assists} | LH: #{target.last_hits} | GPM: #{target.gold_per_min} | XPM: #{target.xp_per_min} | HD: #{target.hero_damage} | TD: #{target.tower_damage} | TGE: #{target.gold_per_min*duration}"
+                res.reply "#{faction} - #{hero} (Lvl #{target.level}) | KDA: #{target.kills}/#{target.deaths}/#{target.assists} | LH: #{target.last_hits} | GPM: #{target.gold_per_min} | XPM: #{target.xp_per_min} | HD: #{target.hero_damage} | TD: #{target.tower_damage} | TGE: #{target.gold_per_min*duration}"
                 # Future (TODO): Team fight contribution? Overall contribution algo.

             if genericID?
                 if _IsSteamID genericID
                     additionalInfo steamID
                 else
-                    GetSteamID msg, genericID, (steamID) ->
+                    GetSteamID res, genericID, (steamID) ->
                         additionalInfo steamID, "profile URL"


pretty node-like output in the REPL

pretty node-like output in the REPL

--- repl.coffee
+++ repl.coffee
@@ -7,6 +7,7 @@
 # Require the **coffee-script** module to get access to the compiler.
 CoffeeScript = require './coffee-script'
 readline     = require 'readline'
+{inspect}    = require 'util'
 Script       = process.binding('evals').Script

 # REPL Setup
@@ -32,7 +33,7 @@
   backlog = ''
   try
     val = CoffeeScript.eval code, bare: on, globals: on, filename: 'repl'
-    process.stdout.write val + '\n' if val isnt undefined
+    process.stdout.write inspect(val, no, 2, yes) + '\n' if val isnt undefined
   catch err
     error err
   repl.prompt()


Исправил логику определения должен показыватся лоадер или нет

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -157,6 +157,7 @@

     @container.removeClass 'show'
     @header.removeClass 'show' if @header.hasClass 'show'
+    @spinner.addClass 'hide' if not @spinner.hasClass 'hide'

     if isHistory and updateHistory
       title = if @images[cid].title then 'Image: ' + @images[cid].title else null
@@ -183,18 +184,18 @@

     fn = => after.call @, cid if cid is @current

-    @spinner.removeClass 'hide' if @spinner.hasClass 'hide'
-
     if not image.width or not image.height
+      @spinner.removeClass 'hide' if @spinner.hasClass 'hide'
+
       element        = new Image()
       element.src    = image.source
       element.onload = =>
-        @spinner.addClass 'hide'
-
         @images[cid].width  = element.width
         @images[cid].height = element.height

         delete element
+
+        @spinner.addClass 'hide' if not @spinner.hasClass 'hide'

         fn()
     else


[minor] remove redundant semicolons

[minor] remove redundant semicolons

--- tap_i18n_db-client.coffee
+++ tap_i18n_db-client.coffee
@@ -68,9 +68,9 @@
       callbacks = params.pop()

   # We want the onReady/onError methods to be called only once (not for every language change)
-  onReadyCalled = false;
-  onErrorCalled = false;
-  original_onReady = callbacks.onReady;
+  onReadyCalled = false
+  onErrorCalled = false
+  original_onReady = callbacks.onReady
   callbacks.onReady = ->
     if onErrorCalled
       return


Properly submit at least utf8-encoded programs

--- server/testSystems/informatics/InformaticsSubmitDownloader.coffee
+++ server/testSystems/informatics/InformaticsSubmitDownloader.coffee
@@ -1,4 +1,5 @@
 moment = require('moment')
+iconv = require('iconv-lite')
 import { JSDOM } from 'jsdom'
 Entities = require('html-entities').XmlEntities

@@ -57,8 +58,10 @@
             [contest, run] = @parseRunId(runid)
             href = "https://informatics.mccme.ru/py/problem/run/#{run}/source"
             #page = await @adminUser.download(href, {encoding: 'latin1'})
-            page = await @adminUser.download(href)
+            page = await @adminUser.download(href, {encoding: 'utf8'})
             source = JSON.parse(page)?.data?.source || ""
+            buf = Buffer.from(source, "utf8")
+            source = iconv.decode(buf, "latin1")
             #if source.length == 0
             #    throw "Source with length 0"
             return normalizeCode(entities.decode(source))


handle missing settings.apis.project_history

--- services/web/app/coffee/Features/History/HistoryController.coffee
+++ services/web/app/coffee/Features/History/HistoryController.coffee
@@ -21,7 +21,7 @@
 			next(error)

 	buildHistoryServiceUrl: () ->
-		if settings.apis.project_history.enabled
+		if settings.apis.project_history?.enabled
 			return settings.apis.project_history.url
 		else
 			return settings.apis.trackchanges.url


Remove unused toggleRichText method

Remove unused toggleRichText method

--- services/web/public/coffee/ide/editor/EditorManager.coffee
+++ services/web/public/coffee/ide/editor/EditorManager.coffee
@@ -188,6 +188,3 @@
 						@$scope.editor.trackChanges = want
 				else
 					@_syncTimeout = setTimeout tryToggle, 100
-
-		toggleRichText: () ->
-			@$scope.editor.richText = !@$scope.editor.richText


Using elapsed time in delayed events

Using elapsed time in delayed events

--- test/timed_events.coffee
+++ test/timed_events.coffee
@@ -10,8 +10,8 @@
     o.update(1)
     o.trigger "afterUpdate", 1

-test "#delay", 1 ->
+test "#delay", 1, ->

-
+
 # Clear out the module
 module()


lower the incoming throttling rate to 100ms

--- controllers/websocket.coffee
+++ controllers/websocket.coffee
@@ -3,7 +3,7 @@

 app.ws?.sockets.on 'connection', (client) ->
   client.on 'message', (data) ->
-    return if Date.now() - client.lastMessageAt < 200
+    return if Date.now() - client.lastMessageAt < 100
     client.lastMessageAt = Date.now()
     data.id = client.id
     client.json.broadcast.send data


fix js multiplexed consumer specs to use json messages

--- spec/javascripts/firehose/multiplexed_consumer_spec.coffee
+++ spec/javascripts/firehose/multiplexed_consumer_spec.coffee
@@ -23,19 +23,17 @@
       @instance.connect()

       for i in [1..5]
-        @instance.message({channel: "/foo", message: "msg-#{i}"})
+        @instance.message({channel: "/foo", message: JSON.stringify { message: "msg-#{i}" } })
       for i in [6..10]
-        @instance.message({channel: "/bar", message: "msg-#{i}"})
+        @instance.message({channel: "/bar", message: JSON.stringify { message: "msg-#{i}" } })

       @instance.unsubscribe("/foo")

       for i in [11..15]
-        @instance.message({channel: "/foo", message: "msg-#{i}"})
+        @instance.message({channel: "/foo", message: JSON.stringify { message: "msg-#{i}" } })
       for i in [16..20]
-        @instance.message({channel: "/bar", message: "msg-#{i}"})
+        @instance.message({channel: "/bar", message: JSON.stringify { message: "msg-#{i}" } })

-      expect(@receivedMessages).toEqual [
-        "msg-1", "msg-2", "msg-3", "msg-4", "msg-5",
-        "msg-6", "msg-7", "msg-8", "msg-9", "msg-10",
-        "msg-16", "msg-17", "msg-18", "msg-19", "msg-20",
-      ]+      expect(@receivedMessages).toEqual(
+        {message: "msg-#{i}"} for i in [1,2,3,4,5,6,7,8,9,10,16,17,18,19,20]
+      )


Fix lint error

Fix lint error

Signed-off-by: Tim Perry <5ee0edb9e2229c0838f1959779f1949031de0123@resin.io>


ide: fix problem with saving of new file

--- client/Ace/AppView.coffee
+++ client/Ace/AppView.coffee
@@ -176,7 +176,7 @@
         {tabView} = @getDelegate()
         return  if tabView.willClose

-        @getDelegate().openFile newFile, contents
+        @getDelegate().openSavedFile newFile, contents

     , { inputDefaultValue: file.name }


ace: remove findprevious and findnext commands which are already handled by findandreplace dialog :hatched_chick:

--- client/ace/lib/ace.coffee
+++ client/ace/lib/ace.coffee
@@ -110,6 +110,8 @@
         'gotoline'         # overriding this
         'sortlines'        # using same mapping for 'save all' action. XXX: re-map sortlines
         'showSettingsMenu' # f ace default settings menu
+        'findprevious',    # we have our own find and replace dialogs
+        'findnext'         # ace default for findnext is cmd+g (mapped to gotoline)
       ]

       @focus()


Check render on window resize.

Check render on window resize.

--- client/publication.coffee
+++ client/publication.coffee
@@ -48,6 +48,7 @@
             @checkRender()

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

   checkRender: =>
     for page in @_pages
@@ -66,6 +67,7 @@
     @_pages = null # To remove references to canvas elements

     $(window).off 'scroll.publication'
+    $(window).off 'resize.publication'

   renderPage: (page) =>
     return if page.rendering


wO -> wo

wO -> wo

--- background_scripts/commands.coffee
+++ background_scripts/commands.coffee
@@ -178,7 +178,7 @@
   "w$": "moveTabEnd"
   "wh": "moveTabLeft"
   "wl": "moveTabRight"
-  "wO": "onlyTab"
+  "wo": "onlyTab"
   "wH": "closeTabsLeft"
   "wL": "closeTabsRight"
   "X": "restoreTab"


addressing XSS vulnerability from #401

addressing XSS vulnerability from #401

--- pivot.coffee
+++ pivot.coffee
@@ -295,7 +295,7 @@
                 $("thead > tr > th", input).each (i) -> tblCols.push $(this).text()
                 $("tbody > tr", input).each (i) ->
                     record = {}
-                    $("td", this).each (j) -> record[tblCols[j]] = $(this).html()
+                    $("td", this).each (j) -> record[tblCols[j]] = $(this).text()
                     addRecord(record)
             else
                 throw new Error("unknown input format")
@@ -419,14 +419,14 @@
                 tr.appendChild th
             th = document.createElement("th")
             th.className = "pvtAxisLabel"
-            th.innerHTML = c
+            th.textContent = c
             tr.appendChild th
             for own i, colKey of colKeys
                 x = spanSize(colKeys, parseInt(i), parseInt(j))
                 if x != -1
                     th = document.createElement("th")
                     th.className = "pvtColLabel"
-                    th.innerHTML = colKey[j]
+                    th.textContent = colKey[j]
                     th.setAttribute("colspan", x)
                     if parseInt(j) == colAttrs.length-1 and rowAttrs.length != 0
                         th.setAttribute("rowspan", 2)
@@ -445,7 +445,7 @@
             for own i, r of rowAttrs
                 th = document.createElement("th")
                 th.className = "pvtAxisLabel"
-                th.innerHTML = r
+                th.textContent = r
                 tr.appendChild th
             th = document.createElement("th")
             if colAttrs.length ==0
@@ -462,7 +462,7 @@
                 if x != -1
                     th = document.createElement("th")
                     th.className = "pvtRowLabel"
-                    th.innerHTML = txt
+                    th.textContent = txt
                     th.setAttribute("rowspan", x)
                     if parseInt(j) == rowAttrs.length-1 and colAttrs.length !=0
                         th.setAttribute("colspan",2)
@@ -472,7 +472,7 @@
                 val = aggregator.value()
                 td = document.createElement("td")
                 td.className = "pvtVal row#{i} col#{j}"
-                td.innerHTML = aggregator.format(val)
+                td.textContent = aggregator.format(val)
                 td.setAttribute("data-value", val)
                 tr.appendChild td

@@ -480,7 +480,7 @@
             val = totalAggregator.value()
             td = document.createElement("td")
             td.className = "pvtTotal rowTotal"
-            td.innerHTML = totalAggregator.format(val)
+            td.textContent = totalAggregator.format(val)
             td.setAttribute("data-value", val)
             td.setAttribute("data-for", "row"+i)
             tr.appendChild td
@@ -498,7 +498,7 @@
             val = totalAggregator.value()
             td = document.createElement("td")
             td.className = "pvtTotal colTotal"
-            td.innerHTML = totalAggregator.format(val)
+            td.textContent = totalAggregator.format(val)
             td.setAttribute("data-value", val)
             td.setAttribute("data-for", "col"+j)
             tr.appendChild td
@@ -506,7 +506,7 @@
         val = totalAggregator.value()
         td = document.createElement("td")
         td.className = "pvtGrandTotal"
-        td.innerHTML = totalAggregator.format(val)
+        td.textContent = totalAggregator.format(val)
         td.setAttribute("data-value", val)
         tr.appendChild td
         result.appendChild tr
@@ -674,7 +674,7 @@
                                 .attr("type", "checkbox").addClass('pvtFilter')
                                 .attr("checked", !filterItemExcluded).data("filter", [c,k])
                                 .appendTo filterItem
-                             filterItem.append $("<span>").html k
+                             filterItem.append $("<span>").text k
                              filterItem.append $("<span>").text " ("+v+")"
                              checkContainer.append $("<p>").append(filterItem)


touches: pointer-events=none for ripple-related objects


Don't show leading phantom tokens

Don't show leading phantom tokens

...thanks @grizzilus :smile:

--- src/token.coffee
+++ src/token.coffee
@@ -229,7 +229,7 @@
       else match

   hasLeadingWhitespace: ->
-    @firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0
+    !@isPhantom and @firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0

   hasTrailingWhitespace: ->
     @firstTrailingWhitespaceIndex? and @firstTrailingWhitespaceIndex < @value.length


Littles fixes

Littles fixes

--- server/konnectors/linkedin.coffee
+++ server/konnectors/linkedin.coffee
@@ -328,9 +328,9 @@
             log.debug "LContact #{linkContact.n}already synced and uptodate"

     else
-        if entries.cozyContactsByFn[linkContact.fb]?
+        if entries.cozyContactsByFn[linkContact.fn]?
             log.info "Link #{cozyContact.fn} to linkedin account"
             updateContact cozyContact, linkContact
-            return
-        log.info "Create #{linkContact.fn} contact"
-        Contact.create linkContact, endSavePicture
+        else
+            log.info "Create #{linkContact.fn} contact"
+            Contact.create linkContact, endSavePicture


cola.i18n()重构并命名为cola.resource() 补充部分jsdoc文档

--- src/core/date.coffee
+++ src/core/date.coffee
@@ -10,8 +10,7 @@

 if XDate
 	$?(() ->
-		XDate.defaultLocale = cola.setting("locale") or defaultLocale
-		XDate.locales[defaultLocale] = localeStrings = {}
+		XDate.locales[''] = localeStrings = {}
 		localeStrings.monthNames = cola.resource("cola.date.monthNames").split(",") if cola.resource("cola.date.monthNames")
 		localeStrings.monthNamesShort = cola.resource("cola.date.monthNamesShort").split(",") if cola.resource("cola.date.monthNamesShort")
 		localeStrings.dayNames = cola.resource("cola.date.dayNames").split(",") if cola.resource("cola.date.dayNames")


Exo.Spine.Controller: Fixed a bug in descendantById and added a function to set the nodeId

--- src/core/node.coffee
+++ src/core/node.coffee
@@ -96,6 +96,9 @@
     @nodeId = ->
       id

+    @setNodeId = (nid)->
+      id = nid
+
     # Did we receive an array of children from the opts hash?
     if opts.children
       for node in opts.children
@@ -201,7 +204,7 @@
       __childMap[id]

     @descendantById = (id) ->
-      child = childById(id)
+      child = @childById(id)
       if child
         return child


Update# update map center

Update# update map center

--- coffeescripts/main.coffee
+++ coffeescripts/main.coffee
@@ -81,7 +81,7 @@
 ###

 loadMap = ()->
-  mapLatLng = new google.maps.LatLng(25.0422145, 121.6141917)
+  mapLatLng = new google.maps.LatLng(25.0410096, 121.6118796)
   mapOptions = {
     center: mapLatLng
     zoom: 19


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -10,6 +10,8 @@
 player.every 4, ->
   doSomething()
 </pre></code>
+
+

 @see GameObject


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -1,5 +1,5 @@
 ###*
-The TimedEvents module allows arbitrary code to be executed at set int
+The TimedEvents module allows arbitrary code to be executed at set intervals.

 player = Core


Define text to title bar

Define text to title bar

--- src/views.coffee
+++ src/views.coffee
@@ -32,19 +32,34 @@
                     # TODO: decide where to route if route has invalid unit id.

         render_units_with_filter: (params)->
-            querys = params.split('&')
-            paramsArray = querys[0].split '=', 2
-            unitCollection = new models.UnitList()
+            queries = params.split('&')
+            paramsArray = queries[0].split '=', 2
+
+            needForTitleBar = -> _.contains(queries, 'tb')
+
+            @unit_list = new models.UnitList()
             dataFilter = page_size: 1000
             dataFilter[paramsArray[0]] = paramsArray[1]
-            unitCollection.fetch(
+            @unit_list.fetch(
                 data: dataFilter
                 success: (collection)=>
+                    @fetchAdministrativeDivisions(paramsArray[1], @findUniqueAdministrativeDivisions) if needForTitleBar()
                     @draw_units collection, zoom: true, drawMarker: true
                 error: ->
                     # TODO: what happens if no models are found with query?
             )

+        fetchAdministrativeDivisions: (params, callback)->
+            divisions = new models.AdministrativeDivisionList()
+            divisions.fetch
+                data: ocd_id: params
+                success: callback
+
+        findUniqueAdministrativeDivisions: (collection) =>
+            byName = (division_model) -> division_model.toJSON().name
+            divisionNames = collection.chain().map(byName).compact().unique().value()
+            parseTitleString = -> divisionNames.join(', ').replace(/, ([^, ]*)$/, ' #{and} $1')
+            app.vent.trigger('administration-divisions-fetched', parseTitleString())

         clear_all_markers: ->
             @all_markers.clearLayers()
@@ -121,6 +136,9 @@
     class TitleBarView extends Backbone.View
         events:
             'click a': 'preventDefault'
+
+        initialize: ->
+            @listenTo(app.vent, 'administration-divisions-fetched', @render)

         render: (titleText)->
             @el.innerHTML = jade.template 'embedded-title-bar', 'titleText': titleText


do collisions on collections of children

--- layout.coffee
+++ layout.coffee
@@ -5,6 +5,7 @@

 drawTree = (svg, tree) ->
+    topNodes = []
     nodes = []
     nodeMap = {}

@@ -22,6 +23,8 @@
                 parentNode = nodeMap[parent.name]
                 node.parent = parentNode
                 parentNode.children.push(node)
+            else
+                topNodes.push(node)

     r = 40

@@ -62,10 +65,10 @@
                 node.x = xMin + node.width / 2
                 node.y = yMin + node.height / 2

-        q = d3.geom.quadtree(nodes)
-
-        for i in [0 .. nodes.length - 1]
-            q.visit(collide(nodes[i]))
+        for node in nodes
+            doCollisions(node.children)
+
+        doCollisions(topNodes)

         svg.selectAll('.cell')
             .attr('transform', (node) -> "translate(#{node.x},#{node.y})")
@@ -75,6 +78,13 @@
             .attr('y', (node) -> - node.height / 2)
             .attr('width', (node) -> node.width)
             .attr('height', (node) -> node.height)
+
+
+doCollisions = (children) ->
+    if children.length
+        q = d3.geom.quadtree(children)
+        for child in children
+            q.visit(collide(child))

 collide = (node) ->
@@ -85,18 +95,18 @@
     ny2 = node.y + r

     fn = (quad, x1, y1, x2, y2) ->
-        if quad.point and (quad.point != node)
-            dx = node.x - quad.point.x
-            dy = node.y - quad.point.y
+        other = quad.point
+        if other and (other != node)
+            dx = node.x - other.x
+            dy = node.y - other.y
             l = Math.sqrt(dx * dx + dy * dy)
-            r = node.radius + quad.point.radius
-            r = 30
+            r = d3.max([node.width + other.width, node.height + other.height]) / 2
             if l < r  # found a collision
                 l = (l - r) / l * .5
                 node.x -= (dx *= l)
                 node.y -= (dy *= l)
-                quad.point.x += dx
-                quad.point.y += dy
+                other.x += dx
+                other.y += dy
         return x1 > nx2 or x2 < nx1 or y1 > ny2 or y2 < ny1

     return fn


permit project to access and write to status field of project settings.

--- src/smc-util/db-schema.coffee
+++ src/smc-util/db-schema.coffee
@@ -785,11 +785,13 @@
                 project_id     : null
                 title          : null
                 description    : null
+                status         : null
         set :
             fields :
                 project_id     : 'project_id'
                 title          : true
                 description    : true
+                status         : true

 # Table that enables set queries to the course field of a project.  Only
 # project owners are allowed to use this table.  The point is that this makes


fix https://github.com/sagemathinc/smc/issues/1777 -- "r_accounts → onGravatarSelect → @props.email_address not always defined"

--- src/smc-webapp/r_account.cjsx
+++ src/smc-webapp/r_account.cjsx
@@ -639,7 +639,14 @@
             </Button>
         </Well>

+    render_gravatar_needs_email: ->
+        <div className="lighten">
+            Gravatar only available if you set an email address in the account settings panel above.
+        </div>
+
     render_set_gravatar: ->
+        if not @props.email_address
+            return @render_gravatar_needs_email()
         <Row>
             <Col md=6 key='checkbox'>
                 <Checkbox
@@ -672,7 +679,7 @@
             <LabeledRow label='Color'>
                 <ColorPicker color={@props.profile.get('color')} style={maxWidth:"150px"} onChange={@onColorChange}/>
             </LabeledRow>
-            <LabeledRow label='Color'>
+            <LabeledRow label='Picture'>
                 {if @state.show_instructions then @render_instruction_well() else @render_set_gravatar()}
              </LabeledRow>
         </Panel>


mock: don't mutate resource fields when applying shadow properties

--- mock.coffee
+++ mock.coffee
@@ -40,7 +40,7 @@
         return false unless obj[key] is val
       return true
     .map (obj) ->
-      if obj._shadow? then  _.extend(obj, obj._shadow) else obj
+      if obj._shadow? then  _.extend({}, obj, obj._shadow) else obj
     .map (raw_json) ->
       Klass = clever[_(resource).chain().capitalize().rtrim('s').value()]
       return new Klass raw_json