Update Ranges Tracker

Update Ranges Tracker

--- services/document-updater/app/coffee/RangesTracker.coffee
+++ services/document-updater/app/coffee/RangesTracker.coffee
@@ -105,6 +105,7 @@
 				throw new Error("unknown op type")

 		addComment: (op, metadata) ->
+			# TODO: Don't allow overlapping comments?
 			@comments.push comment = {
 				id: op.t or @newId()
 				op: # Copy because we'll modify in place
@@ -167,12 +168,14 @@
 			op_length = op.i.length
 			op_end = op.p + op_length

+
 			already_merged = false
 			previous_change = null
 			moved_changes = []
 			remove_changes = []
 			new_changes = []
-			for change in @changes
+
+			for change, i in @changes
 				change_start = change.op.p

 				if change.op.d?
@@ -200,6 +203,16 @@
 					# Only merge inserts if they are from the same user
 					is_same_user = metadata.user_id == change.metadata.user_id

+					# If this is an insert op at the end of an existing insert with a delete following, and it cancels out the following
+					# delete then we shouldn't append it to this insert, but instead only cancel the following delete.
+					# E.g.
+					#                   foo|<--- about to insert 'b' here
+					#  inserted 'foo'  --^ ^-- deleted 'bar'
+					# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
+					next_change = @changes[i+1]
+					is_op_adjacent_to_next_delete = next_change? and next_change.op.d? and op.p == change_end and next_change.op.p == op.p
+					will_op_cancel_next_delete = is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
+
 					# If there is a delete at the start of the insert, and we're inserting
 					# at the start, we SHOULDN'T merge since the delete acts as a partition.
 					# The previous op will be the delete, but it's already been shifted by this insert
@@ -222,7 +235,8 @@
 					if @track_changes and
 							is_change_overlapping and
 							!is_insert_blocked_by_delete and
-							!already_merged and
+							!already_merged and
+							!will_op_cancel_next_delete and
 							is_same_user
 						offset = op_start - change_start
 						change.op.i = change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset)
@@ -396,9 +410,13 @@
 				@emit "changes:moved", moved_changes

 		_addOp: (op, metadata) ->
+			# Don't take a reference to the existing op since we'll modify this in place with future changes
+			clone_op = {}
+			for k,v of op
+				clone_op[k] = v
 			change = {
 				id: @newId()
-				op: op
+				op: clone_op
 				metadata: metadata
 			}
 			@changes.push change


fix project results order, sort by time

fix project results order, sort by time

--- services/track-changes/app/coffee/MongoPackManager.coffee
+++ services/track-changes/app/coffee/MongoPackManager.coffee
@@ -154,6 +154,9 @@
 				return false if cutoff? && ts < cutoff
 				return true

+			timeOrder = (a, b) ->
+				b.meta.end_ts - a.meta.end_ts
+
 			updates = MongoPackManager._filterAndLimit(updates, unpackedSet, filterFn, limit)
 			#console.log 'initial updates are', updates

@@ -210,15 +213,15 @@

 			tail.toArray (err, result2) ->
 				if err?
-					return callback err, updates
+					return callback err, updates.sort timeOrder
 				else
 					applyAndUpdate result2
 					overlap.toArray (err, result3) ->
 						if err?
-							return callback err, updates
+							return callback err, updates.sort timeOrder
 						else
 							applyAndUpdate result3
-							callback err, updates
+							callback err, updates.sort timeOrder

 	_unpackResults: (updates) ->
 		#	iterate over the updates, if there's a pack, expand it into ops and


Fix race condition in frontend address js

Creating the state selector for the current country requires an ajax
request. When the response was returned, the value of the country
selector was used at the time the response was started, even though it
may have changed in the interim.

Updated the js to always use the current value of the country field when
filling in state information.

--- frontend/app/assets/javascripts/spree/frontend/checkout/address.js.coffee
+++ frontend/app/assets/javascripts/spree/frontend/checkout/address.js.coffee
@@ -14,11 +14,15 @@
               Spree.Checkout[countryId] =
                 states: data.states
                 states_required: data.states_required
-              Spree.fillStates(Spree.Checkout[countryId], region)
+              Spree.fillStates(region)
           else
-            Spree.fillStates(Spree.Checkout[countryId], region)
+            Spree.fillStates(region)

-      Spree.fillStates = (data, region) ->
+      Spree.fillStates = (region) ->
+        countryId = getCountryId(region)
+        data = Spree.Checkout[countryId]
+        return unless data?
+
         statesRequired = data.states_required
         states = data.states


Don’t use backbone context in mediator events.

--- src/chaplin/mediator.coffee
+++ src/chaplin/mediator.coffee
@@ -28,9 +28,9 @@

   # Mixin event methods from Backbone.Events,
   # create Publish/Subscribe aliases
-  mediator.subscribe   = Backbone.on
-  mediator.unsubscribe = Backbone.off
-  mediator.publish     = Backbone.trigger
+  mediator.subscribe   = Backbone.Events.on
+  mediator.unsubscribe = Backbone.Events.off
+  mediator.publish     = Backbone.Events.trigger

   # Initialize an empty callback list so we might seal the mediator later
   mediator._callbacks = null


Apply promise attrs from instance onto message

--- src/coffee/messenger.coffee
+++ src/coffee/messenger.coffee
@@ -498,6 +498,11 @@

         msg._actionInstance = m_opts.action opts, args...

+        promiseAttrs = ['done', 'progress', 'fail', 'state', 'then']
+        for attr in promiseAttrs
+          delete msg[attr] if msg[attr]?
+          msg[attr] = msg._actionInstance?[attr]
+
         return msg

 $.fn.messenger = (func, args...) ->


Logging errors

Logging errors

--- src/colour-extractor.coffee
+++ src/colour-extractor.coffee
@@ -10,6 +10,7 @@
   img = gm(sourceFilename)
   tmpFilename = temp.path({suffix: '.miff'})
   img.size((err, wh) ->
+    console.log err if err
     ratio = wh.width/MAX_W
     w2 = wh.width/2
     h2 = wh.height/2
@@ -17,6 +18,7 @@
        .crop(w2, h2, w2/2, w2/2)                  # Center should be the most interesting
        .scale(Math.ceil(wh.height/ratio), MAX_W)  # Scales the image, histogram generation can take some time
        .write('histogram:' + tmpFilename, (err) ->
+          console.log err if err
           histogram = ''
           miffRS = fs.createReadStream(tmpFilename, {encoding: 'utf8'})


Add course registration, simplfy logic

Add course registration, simplfy logic

--- src/concept-coach/base.cjsx
+++ src/concept-coach/base.cjsx
@@ -9,6 +9,7 @@

 {ExerciseStep} = require '../exercise'

+CourseRegistration = require '../course/registration'
 User = require '../user/model'

 {channel} = require './model'
@@ -17,12 +18,14 @@
   displayName: 'ConceptCoach'

   propTypes:
-    close: React.PropTypes.func
+    close:          React.PropTypes.func
+    moduleUUID:     React.PropTypes.string.isRequired
+    collectionUUID: React.PropTypes.string.isRequired

   getInitialState: ->
     isLoggedIn: User.isLoggedIn()
     displayLogin: false
-    isLoaded: false
+    isLoaded: User.isLoaded

   onAttemptLogin: ->
     @setState(displayLogin: true)
@@ -44,19 +47,26 @@
     User.channel.off('change', @update)

   update: ->
-    @setState(isLoggedIn: User.isLoggedIn(), isLoaded: User.loaded)
+    course = User.getCourse(@props.collectionUUID)
+    @setState(
+      isLoggedIn: User.isLoggedIn(),
+      isLoaded: User.isLoaded,
+      isRegistered: course?.isRegistered()
+    )
+
+  childComponent: ->
+    {isLoaded, isRegistered, isLoggedIn, displayLogin} = @state
+    if not isLoaded
+       Loading ...
+    else if not isLoggedIn
+      <UserLogin onComplete={@onLoginComplete} />
+    else if not isRegistered
+      <CourseRegistration {...@props} onComplete={@update} />
+    else
+      <Task {...@props} key='task'/>

   render: ->
-    {isLoaded, isLoggedIn, displayLogin} = @state
-
-    if isLoggedIn
-      coach = <Task {...@props} key='task'/>
-    else if displayLogin
-      coach = <UserLogin onComplete={@onLoginComplete} />
-    else if isLoaded
-      coach = <UserLoginButton onAttemptLogin={@onAttemptLogin} key='user-login-button'/>
-    else
-      coach = 'Loading...'
+    {isLoaded, isLoggedIn} = @state

     className = classnames 'concept-coach-view',
       loading: not (isLoggedIn or isLoaded)
@@ -64,7 +74,7 @@

       <UserStatus key='user-status' close={@props.close}/>

-        {coach}
+        {@childComponent()}


Moderation: Status field is used for marking topics as deleted/synonym

--- workers/social/lib/social/models/tag.coffee
+++ workers/social/lib/social/models/tag.coffee
@@ -7,7 +7,7 @@

   {Relationship} = jraphical

-  {ObjectId, ObjectRef, Inflector, daisy, secure, race, signature} = require 'bongo'
+  {ObjectId, ObjectRef, Inflector, daisy, secure, race, signature, dash} = require 'bongo'

   Validators  = require './group/validators'
   {permit}    = require './group/permissionset'
@@ -74,7 +74,7 @@
         fetchRandomFollowers:
           (signature Object, Function)
         createSynonym:
-          (signature Object, Function)
+          (signature String, Function)
       static        :
         one: [
           (signature Object, Function)
@@ -139,10 +139,8 @@
       meta          : require 'bongo/bundles/meta'
       synonyms      : [String]
       group         : String
-      type          :
-        type        : String
-        enum        : ['invalid type', ['deleted', 'synonym','complete']]
-        default     : 'complete'
+      status        : String
+
       # owner         : ObjectId
     relationships   :->
       JAccount = require './account'
@@ -154,7 +152,7 @@
       follower      :
         targetType  : JAccount
         as          : 'follower'
-      synonymOf   :
+      synonym       :
         targetType  : JTag
         as          : "synonymOf"
       content       :
@@ -192,7 +190,6 @@
           else
             @update $set: formData, (err) =>
               return callback err if err
-              @constructor.emit 'TagIsUpdated', tagId : @getId()
               callback null
       else
         callback new KodingError 'Access denied'
@@ -202,7 +199,7 @@

     selector or= {}
     selector['data.flags.isLowQuality'] = $ne: yes
-    selector['type'] = $ne: "deleted"
+    selector['status'] = $ne: "deleted"

     @fetchContents selector, options, (err, contents)->
       if err then callback err
@@ -253,7 +250,7 @@
               callbackForEach null, tag for tag in existingTags
       ]

-  create = (data, creator, callback) ->
+  create = (data, creator, callback) =>
     tag = new JTag data
     tag.createSlug (err, slug)->
       if err then callback err
@@ -273,24 +270,25 @@
   @create = permit 'create tags',
     success: (client, data, callback)->
       {delegate} = client.connection
-      data.group = client.context
+      data.group = client.context.group
       create data, delegate, callback

   createSynonym : permit ['create tags', 'read tags'],
     success: (client, title, callback)->
       addSynonym = (tag) =>
-        @addSynonymOf tag, (err) =>
+        @addSynonym tag, (err) =>
           return callback err if err
-          @update $set: type :'synonym', (err) =>
+          @update $set: status :'synonym', (err) =>
             return callback err if err
             @constructor.emit 'TagIsSynonym', {tagId:@getId()}
             callback null

-      if (@type is 'deleted' or @type is 'synonym')
-        return callback new KodingError "Topic is already set as #{@type}!"
+      if (@status is 'deleted' or @status is 'synonym')
+        return callback new KodingError "Topic is already set as #{@status}!"

       {delegate} = client.connection
       {group} = client.context
+
       JTag.one {title}, (err, tag) =>
         return callback err if err
         unless tag
@@ -332,8 +330,10 @@
         else unless role is 'super-admin'
           callback new KodingError 'Access denied'
         else
+          if (@status is 'deleted' or @status is 'synonym')
+            return callback new KodingError "Topic is already set as #{@status}!"
           tagId = @getId()
-          @update {$set: type: "deleted"}, (err)=>
+          @update {$set: status: "deleted"}, (err)=>
             if err
               callback err
             else


sort outcome gradebook by student name

sort outcome gradebook by student name

fixes CNVS-10634

test plan:
  * as a teacher, navigate to a course with the outcome
    gradebook enabled;
  * verify that rows are student by student sortable name.

Change-Id: Ie79872c6a0a7f04118fcbc95da2f17eb8ba3d703
Reviewed-on: https://gerrit.instructure.com/29003
Reviewed-by: Zach Pendleton <514dbb4d54d4f29e454990df7d075678f43f92aa@instructure.com>
Product-Review: Zach Pendleton <514dbb4d54d4f29e454990df7d075678f43f92aa@instructure.com>
QA-Review: Zach Pendleton <514dbb4d54d4f29e454990df7d075678f43f92aa@instructure.com>
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>

--- app/coffeescripts/gradebook2/OutcomeGradebookGrid.coffee
+++ app/coffeescripts/gradebook2/OutcomeGradebookGrid.coffee
@@ -167,7 +167,8 @@
       #
       # Returns an array of rows.
       toRows: (rollups, options = {}) ->
-        _.reject(_.map(rollups, Grid.Util._toRowFn(options.section)), (v) -> v == null)
+        rows = _.reject(_.map(rollups, Grid.Util._toRowFn(options.section)), (v) -> v == null)
+        rows.sort((a, b) -> Grid.Events._sortStudents(a, b, true))

       # Internal: Generate a toRow function that filters by the given section.
       #


Fix autocomplete specs

Fix autocomplete specs

--- spec/javascripts/lib/mobile/autocomplete_mobile_spec.js.coffee
+++ spec/javascripts/lib/mobile/autocomplete_mobile_spec.js.coffee
@@ -260,7 +260,7 @@
           expect(listItem.textContent).toBe(SEARCH_RESULTS[0].title)
           expect($listItem).toHaveClass("autocomplete__result")
           expect($listItem).toHaveClass("autocomplete__result__type")
-          expect($listItem).toHaveClass("autocomplete__result__type--#{SEARCH_RESULTS[0].type}")
+          expect($listItem).toHaveClass("icon--#{SEARCH_RESULTS[0].type}--white--before")

         it 'should create an anchor item without href if not specified', ->
           delete @myAutoComplete.responseMap.uri
@@ -274,7 +274,7 @@
           expect(listItem.textContent).toBe(SEARCH_RESULTS[0].title)
           expect($listItem).toHaveClass("autocomplete__result")
           expect($listItem).toHaveClass("autocomplete__result__type")
-          expect($listItem).toHaveClass("autocomplete__result__type--#{SEARCH_RESULTS[0].type}")
+          expect($listItem).toHaveClass("icon--#{SEARCH_RESULTS[0].type}--white--before")

         it 'should create an anchor item without result type classes if not specified', ->
           delete @myAutoComplete.responseMap.type


Fix following button on profile

Fix following button on profile

--- assets/javascripts/views/profile_follow_button.js.coffee
+++ assets/javascripts/views/profile_follow_button.js.coffee
@@ -6,7 +6,7 @@
     @buttons.submit = ($ '[type=submit]', @$el)

     new HTTP 'GET', "#{TentStatus.config.tent_api_root}/followings", {
-      entity: TentStatus.config.domain_entity
+      entity: TentStatus.config.domain_entity.toStringWithoutSchemePort()
     }, (followings, xhr) =>
       return unless xhr.status == 200
       if followings.length


no need to check if the domains contains .shared

--- client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
@@ -128,7 +128,7 @@
       warn err  if err
       domainList = []
       for domain in userDomains
-        if not domain.regYears > 0 and domain.domain.indexOf("shared") is -1
+        if not domain.regYears > 0
           domainList.push {title:".#{domain.domain}", value:domain.domain}
       @forms["Domain Address"].inputs.domains.setSelectOptions domainList


Add missing css property for order_stash plugin

--- src/plugins/order_stash.coffee
+++ src/plugins/order_stash.coffee
@@ -209,6 +209,7 @@
     display: inline-block;
     margin: 13px 0 6px 0;
     line-height: 17px;
+    color: #333;
     font-family: Verdana, Arial, sans-serif;
     font-size: 13px;
   }


snap keys to pixels, fix #9

snap keys to pixels, fix #9

--- src/scripts/graph/Keys.coffee
+++ src/scripts/graph/Keys.coffee
@@ -154,6 +154,7 @@

       keys.attr 'transform', (d) ->
         dx = self.timeline.x(d.time * 1000)
+        dx = parseInt(dx, 10)
         dy = 10
         return "translate(" + dx + "," + dy + ")"


add sd to stub

add sd to stub

--- apps/home/test/routes.coffee
+++ apps/home/test/routes.coffee
@@ -12,7 +12,8 @@
   beforeEach ->
     sinon.stub Backbone, 'sync'
     @req = { body: {}, query: {}, get: @getStub = sinon.stub(), cookies: {} }
-    @res = { render: sinon.stub(), redirect: sinon.stub(), cookie: @cookieStub = sinon.stub() }
+    @res = { locals: { sd: {} }, render: sinon.stub(), redirect: sinon.stub(), \
+      cookie: @cookieStub = sinon.stub() }

   afterEach ->
     Backbone.sync.restore()


Adding support for the most recent version of Font Awesome.

--- src/plugins/justify.coffee
+++ src/plugins/justify.coffee
@@ -18,7 +18,7 @@
           editable: @options.editable
           label: alignment
           command: "justify#{alignment}"
-          icon: "icon-align-#{alignment.toLowerCase()}"
+          icon: "fa-align-#{alignment.toLowerCase()}"
           cssClass: @options.buttonCssClass
         buttonset.append buttonElement
       buttonize "Left"


rewrite feature check to be more robust

rewrite feature check to be more robust

--- src/smc-webapp/feature.coffee
+++ src/smc-webapp/feature.coffee
@@ -100,18 +100,22 @@
     cookies_and_local_storage = ->
         if not navigator?
             return
+        page = require('./smc-react')?.redux?.getActions('page')
+        if not page?
+            # It's fine to wait until page has loaded and then some before showing a warning
+            # to the user.  This is also necessary to ensure the page actions/store have been defined.
+            setTimeout(2000, cookies_and_local_storage)
+            return

         # Check for cookies (see http://stackoverflow.com/questions/6125330/javascript-navigator-cookieenabled-browser-compatibility)
         if not navigator.cookieEnabled
-            require('./smc-react').redux.getActions('page').show_cookie_warning()
+            page.show_cookie_warning()

         # Check for local storage
         if not require('smc-util/misc').has_local_storage()
-            require('./smc-react').redux.getActions('page').show_local_storage_warning()
+            page.show_local_storage_warning()

-    # It's fine to wait until page has loaded and then some before showing a warning
-    # to the user.  This is also necessary to ensure the page actions/store have been defined.
-    setTimeout(cookies_and_local_storage, 1000)
+    cookies_and_local_storage()

 else
     # Backend.


fix url build

fix url build

--- controller.coffee
+++ controller.coffee
@@ -46,7 +46,7 @@
             return cb err if err

             urlObject = urlHelpers.parse url
-            url.query = key: rule.key
+            urlObject.query = key: rule.key
             url = urlObject.format()

             emailOptions = {doc, url, rule}


metric: added onboard step 1/2

metric: added onboard step 1/2

--- src/models/metric.coffee
+++ src/models/metric.coffee
@@ -198,6 +198,28 @@
     numerator:
       select: 'count(userId)'
       from: 'declineTrade'
+      where: (day) ->
+        "time >= #{dayToMS day}ms AND time < #{dayToMS day + 1}ms"
+  }
+  {
+    name: 'onboard step 1'
+    apps: ['kitten-cards']
+    format: '0'
+    isGroupSizeDependent: true
+    numerator:
+      select: 'count(userId)'
+      from: 'onboardStepOne'
+      where: (day) ->
+        "time >= #{dayToMS day}ms AND time < #{dayToMS day + 1}ms"
+  }
+  {
+    name: 'onboard step 2'
+    apps: ['kitten-cards']
+    format: '0'
+    isGroupSizeDependent: true
+    numerator:
+      select: 'count(userId)'
+      from: 'onboardStepTwo'
       where: (day) ->
         "time >= #{dayToMS day}ms AND time < #{dayToMS day + 1}ms"
   }


FIX: Amend typo where 'doubfire' was used instead of 'angular'

--- src/app/units/states/edit-unit-view/edit-unit-view.coffee
+++ src/app/units/states/edit-unit-view/edit-unit-view.coffee
@@ -1,4 +1,4 @@
-doubtfire.module('doubtfire.units.states.edit-unit-view', [])
+angular.module('doubtfire.units.states.edit-unit-view', [])

 #
 # State for convenors to edit a particular unit


Fixing Typo

Fixing Typo

--- src/quo.environment.coffee
+++ src/quo.environment.coffee
@@ -31,7 +31,7 @@
         environment = {}
         environment.browser = _detectBrowser(user_agent)
         environment.os = _detectOS(user_agent)
-        environment.isMobile = (if (environment.os) then true else false)
+        environment.isMobile = !!environment.os
         environment.screen = _detectScreen()
         environment

@@ -40,7 +40,7 @@
         if is_webkit then is_webkit[0] else user_agent

     _detectOS = (user_agent) ->
-        detected_os = undefined
+        detected_os = null
         for os of SUPPORTED_OS
             supported = user_agent.match(SUPPORTED_OS[os])
             if supported


Add CoffeeScript setSource test

Add CoffeeScript setSource test

--- spec/ComponentLoader.coffee
+++ spec/ComponentLoader.coffee
@@ -278,7 +278,7 @@
           @timeout 10000
           unless noflo.isBrowser()
             workingSource = workingSource.replace "'noflo'", "'../src/lib/NoFlo'"
-          l.setSource 'foo', 'RepeatData', workingSource, 'js', (err) ->
+          l.setSource 'foo', 'RepeatData', workingSource, 'javascript', (err) ->
             return done err if err
             done()
         it 'should be a loadable component', (done) ->
@@ -296,6 +296,39 @@
               chai.expect(ip.data).to.equal 'ES5'
               done()
             ins.send 'ES5'
+      describe 'with CoffeeScript', ->
+        workingSource = """
+        noflo = require 'noflo'
+        exports.getComponent = ->
+          c = new noflo.Component
+          c.inPorts.add 'in'
+          c.outPorts.add 'out'
+          c.process (input, output) ->
+            output.sendDone input.get 'in'
+        """
+
+        it 'should be able to set the source', (done) ->
+          @timeout 10000
+          unless noflo.isBrowser()
+            workingSource = workingSource.replace "'noflo'", "'../src/lib/NoFlo'"
+          l.setSource 'foo', 'RepeatDataCoffee', workingSource, 'coffeescript', (err) ->
+            return done err if err
+            done()
+        it 'should be a loadable component', (done) ->
+          l.load 'foo/RepeatDataCoffee', (err, inst) ->
+            return done err if err
+            chai.expect(inst).to.be.an 'object'
+            chai.expect(inst.inPorts).to.contain.keys ['in']
+            chai.expect(inst.outPorts).to.contain.keys ['out']
+            ins = new noflo.internalSocket.InternalSocket
+            out = new noflo.internalSocket.InternalSocket
+            inst.inPorts.in.attach ins
+            inst.outPorts.out.attach out
+            out.on 'ip', (ip) ->
+              chai.expect(ip.type).to.equal 'data'
+              chai.expect(ip.data).to.equal 'CoffeeScript'
+              done()
+            ins.send 'CoffeeScript'

     describe 'with non-working code', ->
       before ->


Give gutter line highlighting its own `describe`

--- spec/app/editor-spec.coffee
+++ spec/app/editor-spec.coffee
@@ -1594,6 +1594,11 @@
         miniEditor.attachToDom()
         expect(miniEditor.getCursorBufferPosition().row).toBe 0
         expect(miniEditor.find('.line.cursor-line').length).toBe 0
+
+
+  describe "gutter line highlighting", ->
+    beforeEach ->
+      editor.attachToDom(heightInLines: 5.5)

     describe "when there is no wrapping", ->
       it "highlights the line where the initial cursor position is", ->


Add spec confirming that multi-keystroke bindings can match against bindings in multiple binding sets

For example, if you have a very unspecific binding "ctrl-x ctrl-c", you can also have very specific "ctrl-x …" bindings, and bindings from both sets can be matched. A partial match in a more specific does not rule out later matches in a less specific set.

--- spec/app/keymap-spec.coffee
+++ spec/app/keymap-spec.coffee
@@ -164,7 +164,24 @@
             expect(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0]))).toBeTruthy()

       describe "when the event's target node descends from multiple nodes that match selectors with a partial binding match", ->
-        it "allows any of the bindings to be triggered upon a second keystroke"
+        fit "allows any of the bindings to be triggered upon a second keystroke, favoring the most specific selector", ->
+           keymap.bindKeys ".grandchild-node", 'ctrl-x ctrl-c': 'more-specific-quit'
+           grandchildNode = fragment.find('.grandchild-node')[0]
+           moreSpecificQuitHandler = jasmine.createSpy('moreSpecificQuitHandler')
+           fragment.on 'more-specific-quit', moreSpecificQuitHandler
+
+           expect(keymap.handleKeyEvent(keydownEvent('x', target: grandchildNode, ctrlKey: true))).toBeFalsy()
+           expect(keymap.handleKeyEvent(keydownEvent('1', target: grandchildNode))).toBeFalsy()
+           expect(quitHandler).not.toHaveBeenCalled()
+           expect(moreSpecificQuitHandler).not.toHaveBeenCalled()
+           expect(closeOtherWindowsHandler).toHaveBeenCalled()
+           closeOtherWindowsHandler.reset()
+
+           expect(keymap.handleKeyEvent(keydownEvent('x', target: grandchildNode, ctrlKey: true))).toBeFalsy()
+           expect(keymap.handleKeyEvent(keydownEvent('c', target: grandchildNode, ctrlKey: true))).toBeFalsy()
+           expect(quitHandler).not.toHaveBeenCalled()
+           expect(closeOtherWindowsHandler).not.toHaveBeenCalled()
+           expect(moreSpecificQuitHandler).toHaveBeenCalled()

       describe "when there is a complete binding with a less specific selector", ->
         it "favors the more specific partial match", ->


:white_check_mark: Add tests for inconsistent rows

--- spec/csv-editor-spec.coffee
+++ spec/csv-editor-spec.coffee
@@ -473,23 +473,43 @@

     describe 'when the user choose to open a table editor', ->
       describe 'when the csv has inconsistent row length', ->
-        beforeEach ->
-          openFixture('incomplete-first-row.csv')
-
-          runs ->
-            nextAnimationFrame()
-
-            csvEditor.onDidOpen ({editor}) ->
-              tableEditor = editor
-
-            tableEditorButton = csvEditorElement.form.openTableEditorButton
-            click(tableEditorButton)
-
-          waitsFor 'table editor created', -> tableEditor?
-
-        it 'has a table filled with the file content', ->
-          expect(tableEditor.getScreenColumnCount()).toEqual(5)
-          expect(tableEditor.getScreenRowCount()).toEqual(2)
+        describe 'due to an incomplete first row', ->
+          beforeEach ->
+            openFixture('incomplete-first-row.csv')
+
+            runs ->
+              nextAnimationFrame()
+
+              csvEditor.onDidOpen ({editor}) ->
+                tableEditor = editor
+
+              tableEditorButton = csvEditorElement.form.openTableEditorButton
+              click(tableEditorButton)
+
+            waitsFor 'table editor created', -> tableEditor?
+
+          it 'has a table filled with the file content', ->
+            expect(tableEditor.getScreenColumnCount()).toEqual(5)
+            expect(tableEditor.getScreenRowCount()).toEqual(2)
+
+        describe 'due to an incomplete row later in the file', ->
+          beforeEach ->
+            openFixture('inconsistent-column-count.csv')
+
+            runs ->
+              nextAnimationFrame()
+
+              csvEditor.onDidOpen ({editor}) ->
+                tableEditor = editor
+
+              tableEditorButton = csvEditorElement.form.openTableEditorButton
+              click(tableEditorButton)
+
+            waitsFor 'table editor created', -> tableEditor?
+
+          it 'has a table filled with the file content', ->
+            expect(tableEditor.getScreenColumnCount()).toEqual(6)
+            expect(tableEditor.getScreenRowCount()).toEqual(108)

       describe 'with the default options', ->
         beforeEach ->


New dev build

New dev build

--- core/index.coffee
+++ core/index.coffee
@@ -25,11 +25,11 @@
     @bot.Dispatcher.on 'MESSAGE_CREATE', @onMessage
     @bootDate = new Date()
     git.short @devVersion
-    @version = "0.5.1"
+    @version = "0.5.2"

   onReady: (e)=>
     @bot.User.setStatus 'online', {
-      name: "#{@prefix}help (new prefix!)"
+      name: "#{@prefix}help [dev-0.5.2]"
     }
     console.log 'Connected.'


Fix specs on windows

Fix specs on windows

--- spec/ui-watcher-spec.coffee
+++ spec/ui-watcher-spec.coffee
@@ -18,7 +18,7 @@
     it "reloads all the base styles", ->
       spyOn(atom.themes, 'reloadBaseStylesheets')

-      expect(uiWatcher.baseTheme.entities[1].getPath()).toContain '/static/'
+      expect(uiWatcher.baseTheme.entities[1].getPath()).toContain "#{path.sep}static#{path.sep}"

       uiWatcher.baseTheme.entities[0].emit('contents-changed')
       expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled()


make it all responsive

make it all responsive

--- src/scripts/jupiter.coffee
+++ src/scripts/jupiter.coffee
@@ -1,17 +1,24 @@
 el = document.getElementById 'jupiter'
-width = window.innerWidth
-height = window.innerHeight

-# make a square that fits centered onscreen
-if height > width
-  height = width
-else
-  width = height
+# Keep em on the higher scope so everything can use them
+width = null
+height = null
+do setCanvasSize = ->
+  width = window.innerWidth
+  height = window.innerHeight

-el.width = width
-el.height = height
-el.style.width = width
-el.style.height = height
+  # make a square that fits centered onscreen
+  if height > width
+    height = width
+  else
+    width = height
+
+  el.width = width
+  el.height = height
+  el.style.width = width
+  el.style.height = height
+
+window.addEventListener 'resize', setCanvasSize, false

 jupiter =  el.getContext '2d'
 jupiter.lineWidth = .7
@@ -87,10 +94,10 @@
   {a, b, c} = options
   triangleBorder {a, b, c, color: '#000'}

-triangleHeight  = height / 36
-midX = width / 2
+drawHalfJupiter = (side) ->
+  triangleHeight  = height / 36
+  midX = width / 2

-drawHalfJupiter = (side) ->
   # `a` is the stationary point the entire side is "pointing" to.
   # `b` and `c` maintain a constant `x` coord, but each move their `y` by a
   # fixed factor of `triangleHeight`.


Update master

Update master

--- src/scripts/manatee.coffee
+++ src/scripts/manatee.coffee
@@ -1,5 +1,5 @@
 # Description:
-#   Allows Hubot to pull down images from clamingmanatee.com
+#   Allows Hubot to pull down images from calmingmanatee.com
 #
 # Dependencies:
 #   "htmlparser": "1.7.6"


add planet in viewer

add planet in viewer

--- mechanics/002-restricted-3-bodies/orbit.coffee
+++ mechanics/002-restricted-3-bodies/orbit.coffee
@@ -23,9 +23,10 @@
         context.closePath()

     orbit.paint = (data) ->
-        [x1, y1, vx1, vy1, x2, y2, vx2, vy2] = data
+        [x1, y1, vx1, vy1, x2, y2, vx2, vy2, x3, y3, vx3, vy3] = data
         context.clearRect(0, 0, 512, 512)
-        circle(s(x1), s(y1), 4, "blue")
+        circle(s(x1), s(y1), 6, "blue")
         circle(s(x2), s(y2), 4, "yellow")
+        circle(s(x3), s(y3), 1, "gray")

     orbit


Oops, wrong use of if in coffee-script

Oops, wrong use of if in coffee-script

--- src/scripts/rajinikanth.coffee
+++ src/scripts/rajinikanth.coffee
@@ -28,5 +28,6 @@
           $ = cheerio.load(body)
           fact = $(".fact").find(".ftext").text()

-          fact = fact.replace /raji?ni(kanth?)?/ig, user if !!user
+          if !!user
+            fact = fact.replace /raji?ni(kanth?)?/ig, user
           msg.send fact


Added additional singularity commands: show active tasks, kill task and show taks updates

--- src/scripts/singularity.coffee
+++ src/scripts/singularity.coffee
@@ -13,6 +13,9 @@
 #   hubot singularity clear alias <alias>
 #   hubot singularity show requests on <alias>
 #   hubot singularity bounce <request> on <alias>
+#   hubot singularity show active tasks for <request> on <alias>
+#   hubot singularity kill task <taksId> on <alias>
+#   hubot singularity show task updates for <taskId> on <alias>
 #
 # Author:
 #   Arca Artem - <aartem@opentable.com>
@@ -38,9 +41,14 @@
     robot.brain.data.singularity_aliases = _singularityAliases
     msg.send("The singularity alias #{alias} for #{url} has been added")

-  showRequests = (msg, alias) ->
+  isAliasDefined = (msg, alias) ->
     if not _singularityAliases[alias]?
       msg.send "I don't know that singularity alias"
+      return false
+    return true
+
+  showRequests = (msg, alias) ->
+    if not isAliasDefined msg, alias
       return

     url = _singularityAliases[alias].url + '/api/requests'
@@ -68,10 +76,89 @@
     msg.send "hubot singularity clear alias <alias>"
     msg.send "hubot singularity show requests on <alias>"
     msg.send "hubot singularity bounce <request> on <alias>"
+    msg.send "hubot singularity show active tasks for <request> on <alias>"
+    msg.send "hubot singularity kill task <taksId> on <alias>"
+    msg.send "hubot singularity show task updates for <taskId> on <alias>"
+
+  showActiveTasks = (msg, request, alias) ->
+    if not isAliasDefined msg, alias
+      return
+
+    url = _singularityAliases[alias].url + '/api/history/request/' + request + '/tasks/active'
+
+    robot.http(url)
+      .header('Accept', 'application/json')
+      .get() (err, res, body) ->
+        if err
+          msg.send "There was an error while making the request:"
+          msg.send error
+          return
+
+        try
+          tasks = JSON.parse body
+          msgEnd = if tasks.length != 1 then 's' else ''
+          msg.send "I found #{tasks.length} active task#{msgEnd}:"
+          for task in tasks
+            taskUrl = _singularityAliases[alias].url + '/api/tasks/task/' + task.taskId.id
+            robot.http(taskUrl)
+              .get() (err, res, body) ->
+                if (err)
+                  msg.send "There was an error while making the request:"
+                  msg.send error
+                  return
+
+                taskDetails = JSON.parse body
+                host = taskDetails.offer.hostname
+
+                portsResource = taskDetails.mesosTask.resources.find (r) ->
+                  r.name == 'ports'
+
+                if (portsResource and portsResource.ranges.range)
+                  port = portsResource.ranges.range[0].begin;
+                  msg.send "Task started on '#{host}:#{port}' at #{new Date(task.taskId.startedAt)} with id: '#{task.taskId.id}'"
+        catch error
+          msg.send "Ran into an error parsing JSON :"
+          msg.send error
+
+  killTask = (msg, taskId, alias) ->
+    if not isAliasDefined msg, alias
+      return
+
+    url = _singularityAliases[alias].url + '/api/tasks/task/' + taskId
+
+    robot.http(url)
+      .del() (err, res, body) ->
+        if err or res.statusCode == 404
+          msg.send "There was an error while making the request:"
+          msg.send err or body
+          return
+        msg.send "Killing '#{taskId}' on '#{alias}'"
+
+  showTaskUpdates = (msg, taskId, alias) ->
+    if not isAliasDefined msg, alias
+      return
+
+    url = _singularityAliases[alias].url + '/api/history/task/' + taskId
+
+    robot.http(url)
+      .header('Accept', 'application/json')
+      .get() (err, res, body) ->
+        if err or res.statusCode == 404
+          msg.send "There was an error while making the request:"
+          msg.send error or body
+          return
+
+        try
+          updates = JSON.parse body
+          msg.send "Updates for task: '#{taskId}'"
+          for update in updates.taskUpdates
+            msg.send "Task state: '#{update.taskState}' started at #{new Date(update.timestamp)}; #{update.statusMessage || ''}"
+        catch error
+          msg.send "Ran into an error parsing JSON :"
+          msg.send error

   bounceRequest = (msg, alias, request) ->
-    if not _singularityAliases[alias]?
-      msg.send "I don't know that singularity alias"
+    if not isAliasDefined msg, alias
       return

     url = _singularityAliases[alias].url + '/api/requests/request/' + request + '/bounce'
@@ -103,5 +190,14 @@
   robot.respond /singularity bounce (.*) on (.*)/i, (msg) ->
     bounceRequest msg, msg.match[2], msg.match[1]

+  robot.respond /singularity show active tasks for (.*) on (.*)/i, (msg) ->
+    showActiveTasks msg, msg.match[1], msg.match[2]
+
+  robot.respond /singularity kill task (.*) on (.*)/i, (msg) ->
+    killTask msg, msg.match[1], msg.match[2]
+
+  robot.respond /singularity show task updates for (.*) on (.*)/i, (msg) ->
+    showTaskUpdates msg, msg.match[1], msg.match[2]
+
   robot.respond /singularity\?/i, (msg) ->
     showCommands(msg)


Updated hubot/slack switch logic

Updated hubot/slack switch logic

--- src/simple-greeting.coffee
+++ src/simple-greeting.coffee
@@ -1,30 +1,33 @@
 # Description
 #   A simple hubot script that gives your robot some friendly greetings
+#   Pass in `HUBOT_SIMPLE_GREETING_SLACK` (true or false) to have jazzy messages just for Slack!
 #
 # Configuration:
 #   HUBOT_SIMPLE_GREETING_SLACK - true or false
 #
 # Commands:
 #   hubot howdy|hallo|hey|hi|hello|yo|good day
-#   "Well hello there <person>! :grinning:",
+#   "Well hello there <person>! :grinning:"
 #
 # Notes:
-#   <optional notes required for the script>
+#   Update data/greetings.json to add your own custom greeting messages
+#   (If you update these, check the tests too)
 #
 # Author:
 #   Paul Theurer <paultheurer@gmail.com>

 module.exports = (robot) ->
   greetings = require('../data/greetings.json');
-  hellos = greetings.hubot.hellos
-  mornings = greetings.hubot.mornings
-  evenings = greetings.hubot.evenings

-  # hubot is running on slack, so jazz up those messages a little bit
-  if process.env.HUBOT_SIMPLE_GREETING_SLACK == true
+  # if hubot is running on slack, jazz up those messages a little bit
+  if process.env.HUBOT_SIMPLE_GREETING_SLACK
     hellos = greetings.slack.hellos
     mornings = greetings.slack.mornings
     evenings = greetings.slack.evenings
+  else
+    hellos = greetings.hubot.hellos
+    mornings = greetings.hubot.mornings
+    evenings = greetings.hubot.evenings

   robot.respond /(howdy|hallo|hey|hi|hello|yo|good day)/i, (res) ->
     message = res.random hellos


Fix EXDEV error on linux

Fix EXDEV error on linux

--- src/path-replacer.coffee
+++ src/path-replacer.coffee
@@ -56,7 +56,13 @@
       tempStat = fs.statSync(output.path)
       origStat = fs.statSync(filePath)
       fs.chmodSync(output.path, origStat.mode) if origStat.mode != tempStat.mode
-      fs.renameSync(output.path, filePath)
+      try
+        fs.renameSync output.path, filePath
+      catch e
+        if e.code is 'EXDEV'
+          readStream = fs.createReadStream output.path
+          writeStream = fs.createWriteStream filePath
+          readStream.pipe writeStream

       doneCallback(result)


shorten line

shorten line

--- spec/lanes/components/grid/RowEditorSpec.coffee
+++ spec/lanes/components/grid/RowEditorSpec.coffee
@@ -38,7 +38,7 @@
         })
         _.defer ->
             _.dom(grid).qs(LAST_ROW_SELECTOR).click()
-            editor = Lanes.Test.Utils.findRenderedComponentWithType(grid, Lanes.Components.Grid.RowEditor)
+            editor = Lanes.Test.Utils.findRenderedComponentWithType(grid, LC.Grid.RowEditor)
             expect(editor.props.rowIndex).toEqual(1)
             expect(editor.props.model).toEqual(jasmine.any(Model))
             expect(editor.props.model.id).toEqual(2)


ability to toggle websocket

ability to toggle websocket

--- client/Main/KD.extend.coffee
+++ client/Main/KD.extend.coffee
@@ -1,4 +1,7 @@
 # this class will register itself just before application starts loading, right after framework is ready
+
+if localStorage.disableWebSocket is "true"
+  window.WebSocket = null

 KD.extend


Fixing scale on High DPI screens. See #223

--- client/lib/background.coffee
+++ client/lib/background.coffee
@@ -77,8 +77,8 @@

 class @Background
   constructor: ->
-    @renderer = PIXI.autoDetectRenderer(window.innerWidth, window.innerHeight)
-    @ratio = window.devicePixelRatio or 1
+    @renderer = PIXI.autoDetectRenderer window.innerWidth, window.innerHeight
+    @computeRatio()

     @stage = null
     @triangles = {}
@@ -91,6 +91,17 @@
     @stage = null
     @triangles = {}
     @vectors = {}
+
+  computeRatio: =>
+    devicePixelRatio = window.devicePixelRatio or 1
+    context = @renderer.gl or @renderer.context
+    backingStoreRatio = context.webkitBackingStorePixelRatio or
+                        context.mozBackingStorePixelRatio or
+                        context.msBackingStorePixelRatio or
+                        context.oBackingStorePixelRatio or
+                        context.backingStorePixelRatio or 1
+
+    @ratio = devicePixelRatio / backingStoreRatio

   render: =>
     @resizeView()
@@ -107,7 +118,7 @@
   resizeView: =>
     return unless @renderer

-    @ratio = window.devicePixelRatio or 1
+    @computeRatio()

     width = window.innerWidth * @ratio
     height = window.innerHeight * @ratio
@@ -118,8 +129,8 @@
       width: width
       height: height
     .css
-      width: width
-      height: height
+      width: window.innerWidth
+      height: window.innerHeight

   generateTriangles: =>
     return unless @renderer


Add @ prefix to weibo ID

Add @ prefix to weibo ID

--- views/components/settings/parts/others.cjsx
+++ views/components/settings/parts/others.cjsx
@@ -11,7 +11,7 @@
         <Col xs={12}>

poi v{POI_VERSION} 基于 Electron v{process.versions.electron} 和 React.js 开发，是一个开源的跨平台舰队 Collection 浏览器。poi 的游戏行为与 Chrome 一致，原则上不提供任何影响收发包的功能。poi 仅提供基本浏览器功能，扩展功能请等待插件开发。

-
微博:  今天 poi 出新版本了吗

+
微博:  @ 今天 poi 出新版本了吗

开发讨论与意见交流群: 378320628

poi 掉落数据统计: http://db.kcwiki.moe

更多帮助与指南查看 poi wiki:  https://github.com/poooi/poi/wiki


fix some comments; comment out some not-used code

--- src/smc-util/syncstring.coffee
+++ src/smc-util/syncstring.coffee
@@ -282,7 +282,7 @@
     getting inserted into the changelog.

     If without is defined, it must be an array of Date objects; in that case
-    the current value of the strig is computed, but with all the patches
+    the current value of the string is computed, but with all the patches
     at the given times in "without" ignored.  This is used elsewhere as a building
     block to implement undo.
     ###
@@ -926,7 +926,6 @@
         )

     _patch_table_query: (cutoff) =>
-        # avoid potential roundoff errors and >= vs > for timestamps, which are iffy.
         query =
             string_id: @_string_id
             time     : if cutoff then {'>=':cutoff} else null
@@ -952,7 +951,7 @@
         ###
         TODO/CRITICAL: We are temporarily disabling same-user collision detection, since this seems to be leading to
         serious issues involving a feedback loop, which may be way worse than the 1 in a million issue
-        that this addresses.  This only address the *same* accout being used simultaneously on the same file
+        that this addresses.  This only address the *same* account being used simultaneously on the same file
         by multiple people which isn't something users should ever do (but they do in big demos).

         @_patch_list.on 'overwrite', (t) =>
@@ -963,6 +962,21 @@

         @_patches_table.on 'saved', (data) =>
             @_handle_offline(data)
+
+    ###
+    _check_for_timestamp_collision: (t) =>
+        obj = @_my_patches[t]
+        if not obj?
+            return
+        key = @_patches_table.key(obj)
+        if obj.patch != @_patches_table.get(key)?.get('patch')
+            #console.log("COLLISION! #{t}, #{obj.patch}, #{@_patches_table.get(key).get('patch')}")
+            # We fix the collision by finding the nearest time after time that
+            # is available, and reinserting our patch at that new time.
+            @_my_patches[t] = 'killed'
+            new_time = @_patch_list.next_available_time(new Date(t), @_user_id, @_users.length)
+            @_save_patch(new_time, JSON.parse(obj.patch))
+    ###

     _init_evaluator: (cb) =>
         if misc.filename_extension(@_path) == 'sagews'
@@ -1098,18 +1112,6 @@
         x = @_patches_table.set(obj, 'none', cb)
         @_patch_list.add([@_process_patch(x, undefined, undefined, patch)])

-    _check_for_timestamp_collision: (t) =>
-        obj = @_my_patches[t]
-        if not obj?
-            return
-        key = @_patches_table.key(obj)
-        if obj.patch != @_patches_table.get(key)?.get('patch')
-            #console.log("COLLISION! #{t}, #{obj.patch}, #{@_patches_table.get(key).get('patch')}")
-            # We fix the collision by finding the nearest time after time that
-            # is available, and reinserting our patch at that new time.
-            @_my_patches[t] = 'killed'
-            new_time = @_patch_list.next_available_time(new Date(t), @_user_id, @_users.length)
-            @_save_patch(new_time, JSON.parse(obj.patch))

     # Save current live string to backend.  It's safe to call this frequently,
     # since it will debounce itself.


make pdf viewer also work for public files

--- src/smc-webapp/editor_pdf.cjsx
+++ src/smc-webapp/editor_pdf.cjsx
@@ -103,19 +103,17 @@
     iframes[k].remove()
     delete iframes[k]

-require('./project_file').register_file_editor
-    ext       : 'pdf'
-
-    icon      : 'file-pdf-o'
-
-    component : PublicPDF
-
-    # In the init we create the hidden iframe.
-    init      : (path, redux, project_id) ->
-        create_iframe(project_id, path)
-        return undefined
-
-    remove    : (path, redux, project_id) ->
-        delete_iframe(project_id, path)
+for pub in [true, false]
+    require('./project_file').register_file_editor
+        ext       : 'pdf'
+        icon      : 'file-pdf-o'
+        is_public : pub
+        component : PublicPDF
+        # In the init we create the hidden iframe.
+        init      : (path, redux, project_id) ->
+            create_iframe(project_id, path)
+            return undefined
+        remove    : (path, redux, project_id) ->
+            delete_iframe(project_id, path)


Missing done callbacks

Missing done callbacks

--- src/spec/validator.spec.coffee
+++ src/spec/validator.spec.coffee
@@ -179,6 +179,7 @@
       @validator.parse csv
       .then (parsed) =>
         @validator.valProduct parsed.data
+        done()
       .fail (e) -> done(e)

   describe '#validateOffline', ->
@@ -192,4 +193,5 @@
       .then (parsed) =>
         @validator.validateOffline parsed.data
         expect(@validator.errors).toEqual []
+        done()
       .fail (e) -> done(e)


Add `length` to `Neo4jCursor`

Add `length` to `Neo4jCursor`

--- cursor.coffee
+++ cursor.coffee
@@ -5,6 +5,7 @@
 ###
 class Neo4jCursor
   constructor: (@_cursor) ->
+    @length = @_cursor.length
   @define 'cursor',
     get: -> @_cursor
     set: -> console.warn "This is not going to work, you trying to reset cursor, make new Cypher query instead"


Fix return

Fix return

--- src/widgets/queryinput.coffee
+++ src/widgets/queryinput.coffee
@@ -50,7 +50,6 @@
     @element = ($ elements).map (elem) =>
       if elem.doofinderQueryInput?
         throw Error.error("(#{elem.id}) was registered in another QueryInput", elem)
-        return null
       else
         elem.doofinderQueryInput = @


NEW: draw: fillRule: "nonzero" option ("evenodd" is default)

--- source/Art/Engine/Core/Drawing/ElementDrawLib.coffee
+++ source/Art/Engine/Core/Drawing/ElementDrawLib.coffee
@@ -77,6 +77,7 @@
       from
       radius
       radial
+      fillRule
     } = drawOptions

     if isOutline
@@ -101,6 +102,7 @@
     o.from          = colors && if from? then layoutToFrom from, drawArea else drawArea.topLeft
     o.to            = colors && if to?   then layoutToFrom to, drawArea else drawArea.bottomLeft
     o.radius        = radius
+    o.fillRule      = fillRule

     if colors?.constructor == GradientFillStyle
       colors.to = o.to


Minor bugfixes

Minor bugfixes

--- server/app/assets/javascripts/datavis/DataVisUI.coffee
+++ server/app/assets/javascripts/datavis/DataVisUI.coffee
@@ -17,11 +17,11 @@

   #deactivate on ESC
   onDocumentKeyUp = (event) ->
-    deactivate if event.code == 27
+    deactivate() if event.keyCode == 27

   #deactivate on click outside
   onDocumentClick = (event) ->
-    deactivate if event.target == cover
+    deactivate() if event.target == cover

   activate = (context) ->
     document.addEventListener( 'keyup', onDocumentKeyUp, false );
@@ -44,7 +44,7 @@
     document.removeEventListener( 'keyup', onDocumentKeyUp, false );
     document.removeEventListener( 'click', onDocumentClick, false );
     removeClass( container, 'nodeCodeEditor-active' );
-    root.dataVisEditor.unload()
+    root.dataVisEditor.unload() if root.dataVisEditor?
     root.dataVisEditor = null

   displayErrors = (errors) ->


make it a one-liner

make it a one-liner

--- source/server/infrastructure/aggregate-router.coffee
+++ source/server/infrastructure/aggregate-router.coffee
@@ -56,5 +56,4 @@
     aggregate.handle command
     @repository.save aggregate

-  _logMsg: (message) ->
-    "#{@configuration.appId}: #{this}: #{message}"
+  _logMsg: (message) -> "#{@configuration.appId}: #{this}: #{message}"


updated embed for new permissions

updated embed for new permissions

--- static/embed/oab_embed.coffee
+++ static/embed/oab_embed.coffee
@@ -608,11 +608,11 @@
           _L.set '#_oab_email', 'placeholder', ph
           _L.html '._oab_terms', tcs
         refs = ''
-        for p of this.f?.permissions?.permissions?.policy_full_text ? []
-          refs += ' [' + (parseInt(p)+1) + ']'
+        for p of this.f?.permissions?.best_permission?.provenance?.archiving_policy ? []
+          refs += ' [' + (parseInt(p)+1) + ']'
         _L.html '._oab_refs', refs
         paper = if this.f?.metadata?.doi then 'your paper' else 'your paper'
-        _L.html '._oab_your_paper', (if this.f?.permissions?.permissions?.version_allowed is 'publisher pdf' then 'the publisher pdf of ' else '') + paper
+        _L.html '._oab_your_paper', (if this.f?.permissions?.best_permission?.version is 'publishedVersion' then 'the publisher pdf of ' else '') + paper
         _L.html '._oab_journal', this.f?.metadata?.journal_short ? 'the journal'
         # set config by this.config.repo_name put name in ._oab_repo

@@ -625,18 +625,18 @@
           else
             this.file = true # no file required for oa deposit...
             _L.show '._oab_oa_deposit'
-        else if this.f?.permissions?.permissions?.archiving_allowed
+        else if this.f?.permissions?.best_permission?.can_archive
           # can be shared, depending on permissions info
-          _L.hide('#_oab_not_pdf') if this.f?.permissions?.permissions?.version_allowed is 'publisher pdf'
-          if typeof this.f?.permissions?.permissions?.licence_required is 'string' and this.f.permissions.permissions.licence_required.indexOf('other-') is 0
+          _L.hide('#_oab_not_pdf') if this.f?.permissions?.best_permission?.version is 'publishedVersion'
+          if typeof this.f?.permissions?.best_permission?.licence is 'string' and this.f.permissions.best_permission.licence.indexOf('other-') is 0
             _L.html '._oab_licence', 'under the publisher terms' + refs
           else
-            _L.html '._oab_licence', this.f?.permissions?.permissions?.licence_required ? 'CC-BY'
+            _L.html '._oab_licence', this.f?.permissions?.best_permission?.licence ? 'CC-BY'
           _L.show '._oab_archivable'
         else if this.config.dark_deposit_off
           # permission must be requested first
-          rm = 'mailto:' + (this.f.permissions?.ricks?.application?.can_archive_conditions?.permission_required_contact ? this.config.deposit_help ? this.cml()) + '?'
-          rm += 'cc=' + (this.config.deposit_help ? this.cml()) + '&' if this.f.permissions?.ricks?.application?.can_archive_conditions?.permission_required_contact
+          rm = 'mailto:' + (this.f.permissions?.best_permission?.permissions_contact ? this.config.deposit_help ? this.cml()) + '?'
+          rm += 'cc=' + (this.config.deposit_help ? this.cml()) + '&' if this.f.permissions?.best_permission?.permissions_contact
           rm += 'subject=Request%20to%20self%20archive%20' + (this.f.metadata?.doi ? '') + '&body=';
           rm += encodeURIComponent 'To whom it may concern,\n\n'
           rm += encodeURIComponent 'I am writing to request permission to deposit the full text of my paper "' + (this.f.metadata?.title ? this.f.metadata?.doi ? 'Untitled paper') + '" '
@@ -823,9 +823,9 @@
         data.metadata.journal = 'Proceedings of the 16th IFLA ILDS conference: Beyond the paywall - Resource sharing in a disruptive ecosystem'
         data.metadata.author = [{given: 'Mike', family: 'Paxton'}, {given: 'Gary', family: 'Maixner III'}, {given: 'Joseph', family: 'McArthur'}, {given: 'Tina', family: 'Baich'}]
         data.ill = {subscription: {findings:{}, uid: this.uid, lookups:[], error:[], url: 'https://scholarworks.iupui.edu/bitstream/handle/1805/20422/07-PAXTON.pdf?sequence=1&isAllowed=y'}}
-        data.permissions = { permissions: {
-          archiving_allowed: if this.data.doi is '10.1234/oab-syp-aam' then true else false,
-          version_allowed: if this.data.doi is '10.1234/oab-syp-aam' then "postprint" else undefined
+        data.permissions = { best_permission: {
+          can_archive: if this.data.doi is '10.1234/oab-syp-aam' then true else false,
+          version: if this.data.doi is '10.1234/oab-syp-aam' then "postprint" else undefined
         }, file: {archivable: true, archivable_reason: "Demo acceptance", version: "postprint", licence: "cc-by", same_paper: true, name: "example.pdf", format: "pdf", checksum: "example-checksum"}}
         if this.plugin is 'instantill' then this.findings(data) else this.permissions(data)
       else


activityticker: removed the unused comment view decoration

--- workers/social/lib/social/models/activityticker.coffee
+++ workers/social/lib/social/models/activityticker.coffee
@@ -41,45 +41,6 @@
       callback null, {source, target, as, timestamp}

-  decorateCommentEvent = (relationship, callback) ->
-    # it will be decorated as
-    # source is JAccount          -- doer
-    # target is JAccount          -- owner of the status update
-    # object is JComment          -- the actual comment
-    # subject is JNewStatusUpdate -- post that is commented on
-    {source, target, as, timestamp} = relationship
-    modifiedEvent =
-      as        : as
-      subject   : source
-      object    : target
-      timestamp : timestamp
-
-    # rewrite this part without dash ~ C.S
-    queue = [
-      ->
-        source.fetchTags (err, tags) ->
-          return callback err if err
-          modifiedEvent.subject.tags = tags
-          queue.fin()
-      # disable for now
-      # ->
-      #   target.fetchTags (err, tags) ->
-      #     return callback err if err
-      #     modifiedEvent.object.tags = tags
-      #     queue.fin()
-      ->
-        JAccount.one "_id": source.originId, (err, targetAccount) ->
-          return callback err if err
-          modifiedEvent.target = targetAccount
-          queue.fin()
-      ->
-        JAccount.one "_id": target.originId, (err, sourceAccount) ->
-          return callback err if err
-          modifiedEvent.source = sourceAccount
-          queue.fin()
-    ]
-
-    dash queue, -> callback null, modifiedEvent

   decorateLikeEvent = (relationship, callback) ->
     {source, target, as, timestamp} = relationship


Bug fix

Bug fix

--- config/karma/generate.coffee
+++ config/karma/generate.coffee
@@ -16,7 +16,7 @@
 browserify = require 'gulp-browserify'

 FILES = require '../files'
-TEST_GROUPS = require '../test_groups')
+TEST_GROUPS = require '../test_groups'

 module.exports = (callback) ->
   queue = new Queue(1)


Escape the hyphens in the roles regexes

Escape the hyphens in the roles regexes

Inside square brackets, unescaped hyphens are position dependent. They
act like hyphens normally when at the beginning or end of a square
bracket expression. Otherwise it's used to signify a character range.

http://stackoverflow.com/questions/4068629/how-to-match-hyphens-with-regular-expression/4068725#4068725

--- src/scripts/roles.coffee
+++ src/scripts/roles.coffee
@@ -15,7 +15,7 @@
   getAmbiguousUserText = (users) ->
     "Be more specific, I know #{users.length} people named like that: #{(user.name for user in users).join(", ")}"

-  robot.respond /who is @?([\w .-]+)\?*$/i, (msg) ->
+  robot.respond /who is @?([\w .\-]+)\?*$/i, (msg) ->
     joiner = ', '
     name = msg.match[1].trim()

@@ -39,7 +39,7 @@
       else
         msg.send "#{name}? Never heard of 'em"

-  robot.respond /@?([\w .-_]+) is (["'\w: -_]+)[.!]*$/i, (msg) ->
+  robot.respond /@?([\w .\-_]+) is (["'\w: \-_]+)[.!]*$/i, (msg) ->
     name    = msg.match[1].trim()
     newRole = msg.match[2].trim()

@@ -63,7 +63,7 @@
         else
           msg.send "I don't know anything about #{name}."

-  robot.respond /@?([\w .-_]+) is not (["'\w: -_]+)[.!]*$/i, (msg) ->
+  robot.respond /@?([\w .\-_]+) is not (["'\w: \-_]+)[.!]*$/i, (msg) ->
     name    = msg.match[1].trim()
     newRole = msg.match[2].trim()


Extract line and character from runtime error stack (Chrome).

--- src/Witness/Scripts/witness/SpecificationFile.coffee
+++ src/Witness/Scripts/witness/SpecificationFile.coffee
@@ -4,6 +4,15 @@
 # reference "Event.coffee"

 Witness = this.Witness
+
+extractRuntimeErrorFromStack = (stack) ->
+	firstLine = stack.match(/^(.*)(:?\r|\n|$)/)[1]
+	location = stack.match(/sandbox\.htm:(\d+):(\d+)/)
+
+	if location?
+		"#{firstLine}. Line #{location[1]}, character #{location[2]}."
+	else
+		firstLine

 Witness.SpecificationFile = class SpecificationFile extends Witness.ScriptFile

@@ -49,7 +58,12 @@
 			failed = false
 			iframeWindow._witnessScriptError = (args...) ->
 				failed = true
-				fail.apply this, args
+				error = args[0]
+				if typeof error.stack == "string"
+					message = extractRuntimeErrorFromStack error.stack
+					fail.call this, new Error message
+				else
+					fail.apply this, args

 			dsl = new Witness.Dsl iframeWindow
 			dsl.activate()


Added commandline option parsing from the old bin.coffee file

--- src/server/newbin.coffee
+++ src/server/newbin.coffee
@@ -1,19 +1,71 @@
 Server              = require('./index')
 FS                  = require('fs')
+Util                = require('util')
+
+serverConfig = {}

 if FS.existsSync "server_config.json"
     serverConfig = JSON.parse FS.readFileSync "server_config.json"
-else
-    throw new Error "Missing required configuration file - server_config.json. Please run CloudBrowser from the project root directory."

 Opts = require('nomnom')
     .option 'deployment',
         flag    : true
-        default : false
         help    : "Start the server in deployment mode"
+    .option 'debug',
+        flag    : true
+        help    : "Show the configuration parameters."
+    .option 'noLogs',
+        full    : 'disable-logging'
+        flag    : true
+        help    : "Disable all logging to files."
+    .option 'debugServer',
+        full    : 'debug-server'
+        flag    : true
+        help    : "Enable the debug server."
+    .option 'compression',
+        help    : "Enable protocol compression."
+    .option 'compressJS',
+        full : 'compress-js'
+        help : "Pass socket.io and client engine through uglify and gzip."
+    .option 'knockout',
+        flag    : true
+        help    : "Enable server-side knockout.js bindings."
+    .option 'strict',
+        flag    : true
+        help    : "Enable strict mode - uncaught exceptions exit the program."
+    .option 'resourceProxy',
+        full    : 'resource-proxy'
+        help    : "Enable ResourceProxy."
+    .option 'monitorTraffic',
+        full    : 'monitor-traffic'
+        help    : "Monitor/log traffic to/from socket.io clients."
+    .option 'traceProtocol',
+        full    : 'trace-protocol'
+        help    : "Log protocol messages to browserid-rpc.log."
+    .option 'multiProcess',
+        full    : 'multi-process'
+        help    : "Run each browser in its own process (can't be used with shared global state)."
+    .option 'useRouter',
+        full    : 'router'
+        help    : "Use a front-end router process with each app server in its own process."
+    .option 'port',
+        help    : "Starting port to use."
+    .option 'traceMem',
+        full    : 'trace-mem'
+        flag    : true
+        help    : "Trace memory usage."
+    .option 'adminInterface',
+        full    : 'admin-interface'
+        help    : "Enable the admin interface."
+    .option 'simulateLatency',
+        full    : 'simulate-latency'
+        help    : "Simulate latency for clients in ms (if not given assign uniform randomly in 20-120 ms range."
     .parse()

-if Opts.deployment
+for own k,v of Opts
+    serverConfig[k] = v
+
+if serverConfig.deployment
     console.log "Server started in deployment mode"
 else
     paths = []


Scroll to current section when selected

Scroll to current section when selected

--- src/components/task-plan/homework/add-exercises.cjsx
+++ src/components/task-plan/homework/add-exercises.cjsx
@@ -10,6 +10,7 @@
 ExerciseControls = require './exercise-controls'
 ExerciseDetails  = require '../../exercises/details'
 ExerciseCards    = require '../../exercises/cards'
+ScrollTo         = require '../../scroll-to-mixin'

 AddExercises = React.createClass

@@ -22,7 +23,7 @@
   getInitialState: ->
     { currentView: 'cards' }

-  mixins: [LoadingExercises]
+  mixins: [ScrollTo, LoadingExercises]

   onShowDetailsViewClick: -> @setState(currentView: 'details')
   onShowCardViewClick:    -> @setState(currentView: 'cards')
@@ -81,7 +82,9 @@
     TaskPlanStore.hasExercise(@props.planId, exercise.id)

   setCurrentSection: (currentSection) ->
+    @scrollToSelector("[data-section='#{currentSection}']")
     @setState({currentSection})
+

   render: ->
     return @renderLoading() if @exercisesAreLoading()


Only expect prefix when there are 5 or more segments

--- src/extensions/outline-view/src/tag-generator.coffee
+++ src/extensions/outline-view/src/tag-generator.coffee
@@ -24,10 +24,14 @@

     label = sections[0]
     line = parseInt(sections[2]) - 1
-    if prefix = @parsePrefix(sections[4])
-      label = "#{prefix}::#{label}"
-    if signature = @parsePrefix(sections[5])
-      label = "#{label}#{signature}"
+    if sections.length > 5
+      if prefix = @parsePrefix(sections[4])
+        label = "#{prefix}::#{label}"
+      if suffix = @parsePrefix(sections[5])
+        label = "#{label}#{suffix}"
+    else
+      if suffix = @parsePrefix(sections[4])
+        label = "#{label}#{suffix}"

     tag =
       position: new Point(line, 0)


date will be updated too on index page after form submission

--- src/components/cs-fields/cs-date/cs-date.js.coffee
+++ src/components/cs-fields/cs-date/cs-date.js.coffee
@@ -53,6 +53,7 @@
     $scope.$watch 'formItem.attributes[field.attribute]', (newValue, oldValue) ->
       if (newValue != oldValue)
         $scope.$emit 'input-value-changed', $scope.field
+        format_date($scope)

   # ===== CONFIGURE ===========================================================
@@ -77,9 +78,6 @@
         if (newValue != oldValue)
           format_date($scope)

-      $scope.$watch 'formItem.attributes[field.attribute + \'dateFormat\']', (newValue, oldValue) ->
-        if (newValue != oldValue)
-          format_date($scope)
     ]

   }


Groups: subscribeToPlan method "nonce not found" error message fix

--- workers/social/lib/social/models/payment/groupplan.coffee
+++ workers/social/lib/social/models/payment/groupplan.coffee
@@ -110,6 +110,6 @@
           return callback err  if err
           JPaymentFulfillmentNonce.one nonce:nonceStr, (err, nonce) ->
             return callback err  if err
-            return callback "nonce not found"  unless nonce
+            return callback message: "nonce not found"  unless nonce
             nonce.addOwner client.connection.delegate, (err) ->
               callback err, subscription, nonce.nonce


TrollMode: Wrap api callback error with ApiError

--- workers/social/lib/social/models/socialapi/message.coffee
+++ workers/social/lib/social/models/socialapi/message.coffee
@@ -1,6 +1,7 @@
 Bongo          = require "bongo"
 {Relationship} = require "jraphical"
 request        = require 'request'
+ApiError       = require './error'

 {secure, daisy, dash, signature, Base} = Bongo
 {uniq} = require 'underscore'
@@ -87,12 +88,12 @@

   @delete = secure (client, data, callback)->
-    if not data.id
+    if not data?.id
       return callback { message: "Request is not valid for deleting a message" }

     # check ownership of the account
     SocialMessage.canDelete client, data, (err, res)->
-      return callback err if err
+      return callback err  if err
       return callback {message: "You can not delete this post"} unless res
       doRequest 'deleteMessage', client, data, callback

@@ -157,13 +158,14 @@
     return callback {message: "Id is not set"} unless data.id
     {delegate} = client.connection
     # get api id of the client
+    # TODO we are also calling this method inside do request
     delegate.createSocialApiId (err, socialApiId)=>
       return callback err  if err
       # fetch the message
       if delegate.checkFlag "super-admin"
         data.showExempt = true
       @byId client, data, (err, message)->
-        return callback err  if err
+        return callback new ApiError err  if err
         return callback { message: "Post is not found" }  unless message?.message
         {message: {accountId}} = message
         if accountId is socialApiId


format

format

--- examples/lib/a-star/AStar.coffee
+++ examples/lib/a-star/AStar.coffee
@@ -5,33 +5,33 @@
 global = window or this

 global.AStar =
-  NODE_STATE_DEFAULT: 0
-  NODE_STATE_START: 1
-  NODE_STATE_END: 2
-  NODE_STATE_DETECTED: 11
-  NODE_STATE_REACHABLE: 21
-  NODE_STATE_SHORTEST: 22
-  NODE_STATE_OBSTACLE: 99
+	NODE_STATE_DEFAULT: 0
+	NODE_STATE_START: 1
+	NODE_STATE_END: 2
+	NODE_STATE_DETECTED: 11
+	NODE_STATE_REACHABLE: 21
+	NODE_STATE_SHORTEST: 22
+	NODE_STATE_OBSTACLE: 99

 ###
 # get preset color for each node state
 ###

 AStar.getStateColor = (state) ->
-  switch state
-    when AStar.NODE_STATE_DEFAULT
-      return '#AAA'  # gray
-    when AStar.NODE_STATE_START
-      return '#F88'  # red
-    when AStar.NODE_STATE_END
-      return '#4B4'  # green
-    when AStar.NODE_STATE_REACHABLE
-      return '#FC9'  # orange
-    when AStar.NODE_STATE_DETECTED
-      return '#FF8'  # yellow
-    when AStar.NODE_STATE_SHORTEST
-      return '#8BF'  # blue
-    when AStar.NODE_STATE_OBSTACLE
-      return '#222'  # black
-    else
-      return '#FFF'  # white+	switch state
+		when AStar.NODE_STATE_DEFAULT
+			return '#AAA'  # gray
+		when AStar.NODE_STATE_START
+			return '#F88'  # red
+		when AStar.NODE_STATE_END
+			return '#4B4'  # green
+		when AStar.NODE_STATE_REACHABLE
+			return '#FC9'  # orange
+		when AStar.NODE_STATE_DETECTED
+			return '#FF8'  # yellow
+		when AStar.NODE_STATE_SHORTEST
+			return '#8BF'  # blue
+		when AStar.NODE_STATE_OBSTACLE
+			return '#222'  # black
+		else
+			return '#FFF' # white


select contribution input text when starting to contribute

--- src/node_modules/bucket-item/bucket-item-link.coffee
+++ src/node_modules/bucket-item/bucket-item-link.coffee
@@ -61,7 +61,7 @@
             # HACK for firefox micro-blurring on focus
             scope.model.focusHack = true
             debug("focusing input", input)
-            input.focus() if not input.is(':focus')
+            input.select() if not input.is(':focus')
             scope.model.focusHack = false

       saveContribution:


Add back edit menu.

Add back edit menu.

On OS X, applications rely on the menu to trigger native events for
various commands. Editor view doesn't need it before it listens to the
keyboard shortcuts itself. Since spec window and devtools view don't
listen to keyboard shortcuts, we should rely on the menu to make edit
commands work.

Fixed atom/atom-shell#18.

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -147,6 +147,18 @@
       ]

     menus.push
+      label: 'Edit'
+      submenu:[
+        { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }
+        { label: 'Redo', accelerator: 'Command+Shift+Z', selector: 'redo:' }
+        { type: 'separator' }
+        { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }
+        { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }
+        { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }
+        { label: 'Select All', accelerator: 'Command+A', selector: 'selectAll:' }
+      ]
+
+    menus.push
       label: 'View'
       submenu:[
         { label: 'Reload', accelerator: 'Command+R', click: => BrowserWindow.getFocusedWindow()?.restart() }


Add newline

Add newline

--- src/packages/fuzzy-finder/lib/load-paths-task.coffee
+++ src/packages/fuzzy-finder/lib/load-paths-task.coffee
@@ -34,4 +34,5 @@
     deferred

   abort: ->
-    @aborted = true+    @aborted = true
+


Extract logic into beforeEach blocks

Extract logic into beforeEach blocks

--- test/integration_tests.coffee
+++ test/integration_tests.coffee
@@ -22,15 +22,22 @@
       done()

   context 'with failing tests', ->
-    it 'returns status code 1', (done) ->
+
+    beforeEach (done) ->
       mycha_process = run_mycha test_dir: 'failing_test', args: ['run']
-      mycha_process.on 'close', (exit_code) ->
-        expect(exit_code).to.equal 1
+      mycha_process.on 'close', (@exit_code) =>
         done()

+    it 'returns status code 1', ->
+      expect(@exit_code).to.equal 1
+
   context 'with passing tests', ->
-    it 'returns status code 0', (done) ->
+
+    beforeEach (done) ->
       mycha_process = run_mycha test_dir: 'passing_test', args: ['run']
-      mycha_process.on 'close', (exit_code) ->
-        expect(exit_code).to.equal 0
+      mycha_process.on 'close', (@exit_code) =>
         done()
+
+    it 'returns status code 0', ->
+      expect(@exit_code).to.equal 0
+


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -110,7 +110,7 @@
   equals 0.5.turns, Math.TAU / 2

 test "#circularPoints", ->
-  p
+  points = []
   4.circularPoints (p) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -17,7 +17,7 @@
   equals 3.ceil(), 3, "(3).ceil() equals 3"

 test "#clamp", ->
-  equals
+  equals 5.

 test "#floor", ->
   equals 4.9.floor(), 4, "(4.9).floor() equals 4"


tests: Report errors better

tests: Report errors better

--- test/schema/test_graph.coffee
+++ test/schema/test_graph.coffee
@@ -250,7 +250,10 @@
             node: 'node2'
             port: 'IN'

-      chai.expect(tv4.validate event, schema).to.be.true
+      res = tv4.validateMultiple event, schema
+      chai.expect(res.missing).to.eql []
+      chai.expect(res.errors).to.eql []
+      chai.expect(res.valid).to.be.true

     it 'should invalidate event without required fields', ->
         event =


Fix test

Fix test

--- test/schema/test_graph.coffee
+++ test/schema/test_graph.coffee
@@ -293,8 +293,8 @@
         command: 'removeinitial'
         payload:
           graph: 'mygraph'
-          src:
-            data: 5
+          metadata:
+            route: 5
           tgt:
             node: 'node2'
             port: 'IN'


make variable names more descriptive

make variable names more descriptive

--- src/scripts/helpers/backbone/views/editable.coffee
+++ src/scripts/helpers/backbone/views/editable.coffee
@@ -51,33 +51,33 @@
           $editable = @$el.find(selector)

           if typeof options.value is 'function'
-            value = options.value.apply(@)
+            attributeName = options.value.apply(@)
           else
-            value = options.value
+            attributeName = options.value

           # do not rely on `currentPage.` when setting the value because
           # the currentPage may have changed in the meantime
-          if /^currentPage\./.test(value)
+          if /^currentPage\./.test(attributeName)
             myModel = @model.asPage()
-            value = value.replace(/^currentPage\./, '')
+            attributeName = attributeName.replace(/^currentPage\./, '')
             isCurrentPage = true
           else
             myModel = @model
             isCurrentPage = false

           getValue = () ->
-            myModel.get(value)
+            myModel.get(attributeName)

-          setValue = (v) =>
-            return if myModel.get(value) is v
+          setValue = (value) =>
+            return if getValue() is value

-            myModel.set(value, v)
+            myModel.set(attributeName, value)
             myModel.set('changed', true)
             @model.set('childChanged', true) if isCurrentPage

             # Changing a module's title also change's a book's ToC
-            if isCurrentPage and value is 'title'
+            if isCurrentPage and attributeName is 'title'
               @model.set('changed', true)

             options.onEdit.apply(@) if typeof options.onEdit is 'function'
@@ -88,7 +88,7 @@
           switch options.type
             when 'textinput'
               $input = $('<input type="text" />')
-              $input.attr('placeholder', "Enter a #{value} here").val(getValue())
+              $input.attr('placeholder', "Enter a #{attributeName} here").val(getValue())
               $editable.html($input)
               $input.on 'change', () =>
                 setValue($input.val())
@@ -123,7 +123,7 @@
                   alohaId = $editable.attr('id')
                   alohaEditable = Aloha.getEditableById(alohaId)

-                  if value is 'content'
+                  if attributeName is 'content'
                     # See aloha.coffee for where this is used
                     window.GLOBAL_UPLOADER_HACK = () =>
                       editableBody = alohaEditable.getContents()


fixed indentation

fixed indentation

--- src/scripts/modules/minimal/media/body/body.coffee
+++ src/scripts/modules/minimal/media/body/body.coffee
@@ -148,15 +148,15 @@
                 if href.search(/http:/) < 0
                   $el.attr('href', href+"?minimal=true")
           else
-              $temp.find('a:not([data-type=footnote-number])').each (i, el) =>
-                $el = $(el)
-                href = $el.attr('href')
-                if href.search(/http:/) < 0
-                  hashIndex = href.indexOf('#')
-                  if hashIndex > 0
-                    $el.attr('href', href[..hashIndex-1]+"?minimal=true"+href[hashIndex..])
-                  else if hashIndex < 0
-                    $el.attr('href', href+"?minimal=true")
+            $temp.find('a:not([data-type=footnote-number])').each (i, el) ->
+              $el = $(el)
+              href = $el.attr('href')
+              if href.search(/http:/) < 0
+                hashIndex = href.indexOf('#')
+                if hashIndex > 0
+                  $el.attr('href', href[..hashIndex-1]+"?minimal=true"+href[hashIndex..])
+                else if hashIndex < 0
+                  $el.attr('href', href+"?minimal=true")

           # Add nofollow to external user-generated links
           $temp.find('a[href^="http:"], a[href^="https:"], a[href^="//"]').attr('rel', 'nofollow')


Skip absolute positioned elements of SplitViewWithOlderSiblings for offset calculation

--- client/app/CommonViews/SplitViewWithOlderSiblings.coffee
+++ client/app/CommonViews/SplitViewWithOlderSiblings.coffee
@@ -9,6 +9,9 @@
     super
     offset        = 0
     for olderSibling in @_olderSiblings
-      offset += olderSibling.getHeight()
+      # absolute positioned elements doesn't provide offset for relative elements
+      siblingStyle = window.getComputedStyle olderSibling.getElement()
+      unless siblingStyle.position is "absolute"
+        offset += olderSibling.getHeight()
     newH = @parent.getHeight() - offset
     @setHeight newH


fixed opinion loader logic

fixed opinion loader logic

--- client/app/CommonViews/opinions/opinionviewheader.coffee
+++ client/app/CommonViews/opinions/opinionviewheader.coffee
@@ -18,7 +18,7 @@
       @onListCount = data.repliesCount
       @hide()

-    @hide() if data.repliesCount is 0
+    @hide() if data.repliesCount < @maxCommentToShow

     list = @getDelegate()

@@ -48,6 +48,8 @@
         @loader.destroy()
         @allItemsLink.destroy()

+
+
     @loader = new KDLoaderView
       cssClass      : "opinion-loader hidden"
       size          :
@@ -75,6 +77,14 @@
     @setClass "in"
     super

+  viewAppended:->
+    @setTemplate @pistachio()
+    @template.update()
+
+    remainingOpinions = @getData().repliesCount-@getDelegate().items.length
+    if (remainingOpinions)<@maxCommentToShow
+        @allItemsLink.updatePartial "View last #{remainingOpinions} #{@getOptions().itemTypeString}"
+
   pistachio:->
     """
       {{> @allItemsLink}}


added join notifications to group admins

--- client/landing/app/MainApp/notificationcontroller.coffee
+++ client/landing/app/MainApp/notificationcontroller.coffee
@@ -45,6 +45,7 @@
     # 7 - Your membership request to < group title > has been approved.
     # 8 - < actor fullname > has requested access to < group title >.
     # 9 - < actor fullname > has invited you to < group title >.
+    # 9 - < actor fullname > has joined < group title >.

     options = {}
     {origin, subject, actionType, actorType} = notification.contents
@@ -89,6 +90,8 @@
             "#{actorName} has requested access to #{subjectObj.title}."
           when "groupInvited"
             "#{actorName} has invited you to #{subjectObj.title}."
+          when "groupJoined"
+            "#{actorName} has joined #{subjectObj.title}."
           else
             if actorType is "follower"
               "#{actorName} started following you."


Routes: clean up

Routes: clean up

--- client/landing/site.hackathon/coffee/login/routes.coffee
+++ client/landing/site.hackathon/coffee/login/routes.coffee
@@ -5,83 +5,6 @@
     return KD.singletons.router.clear()  if KD.isLoggedIn()
     KD.singletons.router.openSection 'Login', null, null, cb

-  # handleResetRoute = ({params:{token}}) ->
-  #   {appManager} = KD.singletons
-  #   appManager.require 'Login', (app) ->
-  #     if KD.isLoggedIn()
-  #       KD.remote.api.JUser.fetchUser (err, user)->
-  #         if user and user.passwordStatus is "valid"
-  #           KD.getSingleton('router').handleRoute "/Activity"
-  #         else
-  #           KD.getSingleton('router').handleRoute "/Account/Profile?focus=password&token=#{token}"
-  #     else
-  #       appManager.open 'Login', ->
-  #         app.getView().setCustomDataToForm('reset', {recoveryToken:token})
-  #         app.getView().animateToForm('reset')
-
-  # handleVerifyRoute = ({params:{token}})->
-  #   router = KD.getSingleton 'router'
-  #   return router.handleRoute "/Login"  unless token
-  #   KD.singleton('appManager').open 'Login', (app) ->
-  #     KD.remote.api.JPasswordRecovery.validate token, (err, isValid)=>
-  #       return KD.notify_ err.message  if err
-  #       if isValid
-  #         KD.notify_ "Thanks for confirming your email address"
-  #         router.handleRoute "/Login"
-  #       else
-  #         KD.notify_ "Token is not valid"
-  #         router.handleRoute "/Login"
-
-  # handleRedeemRoute = ({params:{name, token}})->
-  #   token = decodeURIComponent token
-  #   KD.remote.api.JInvitation.byCode token, (err, invite)->
-  #     return callback err if err
-  #     if KD.isLoggedIn()
-  #       KD.remote.cacheable invite.group, (err, [group])->
-  #         group.redeemInvitation token, (err)->
-  #           if err
-  #             KD.notify_ err.message or err
-  #             return window.location.href = "/"
-
-  #           new KDNotificationView
-  #             title : 'Success!'
-  #             type  : 'tray'
-
-  #           KD.getSingleton('router').handleRoute "/#{group.slug}"
-  #           KD.getSingleton('mainController').accountChanged KD.whoami()
-  #     else
-  #       if err or !invite? or invite.status not in ['active','sent']
-  #         KD.singleton('appManager').open 'Login', (app) ->
-  #           new KDNotificationView
-  #             title: 'Invalid invitation code!'
-  #           app.getView().animateToForm 'login'
-  #       else
-  #         KD.singleton('appManager').open 'Login', (app) ->
-  #           app.getView().animateToForm 'login'
-  #           app.headBannerShowInvitation invite
-
-  # handleFinishRegistration = ({params:{token}}) ->
-  #   KD.singleton('appManager').open 'Login', (app) ->
-  #     app.prepareFinishRegistrationForm token  unless KD.isLoggedIn()
-
-  #  handleFailureOfRestriction =->
-  #    KD.utils.defer -> new KDNotificationView title: "Login restricted"
-
-  #  handleRestriction = (handler) ->
-  #    ({params: {token : token }})->
-  #      for url in [
-  #        'http:\/\/localhost'
-  #        'https:\/\/koding.com'
-  #      ] when 0 is window.location.href.indexOf url
-  #        return do handler()
-
-  #      return handleFailureOfRestriction()  unless token
-
-  #      KD.remote.api.JInvitation.byCodeForBeta token, (err, invite)->
-  #        if err or !invite?
-  #          return handleFailureOfRestriction()
-
-  #        do handler()
   redirect = (route) -> (options) -> KD.singletons.router.handleRoute "/WFGH#{route}"

   KD.registerRoutes 'Login',


Added url and url helpers to EvidenceCollection

--- core/app/backbone/collections/evidence_collection.coffee
+++ core/app/backbone/collections/evidence_collection.coffee
@@ -16,4 +16,8 @@
           console.info "Evidence type not supported: #{item.evidence_type}"
     results

-  url: -> "/facts/#{@fact.id}/#{@type}_evidence/combined"
+  url:     -> "#{@baseUrl()}/combined"
+  baseUrl: -> "/facts/#{@fact.id}/#{@type}_evidence"
+
+  commentsUrl: -> "#{@fact.url()}/comments/#{@type}"
+  factRelationsUrl: -> @baseUrl()


Used more logical names

Used more logical names

--- core/app/backbone/controllers/channels_controller.coffee
+++ core/app/backbone/controllers/channels_controller.coffee
@@ -15,7 +15,7 @@
   restoreTopicView: (slug_title, new_callback) ->
     @restoreChannelView "topic-#{slug_title}", new_callback

-  commonTopicViews: (topic) ->
+  showSidebarForTopic: (topic) ->
     FactlinkApp.leftBottomRegion.close()
     @showUserProfile currentUser
     FactlinkApp.Sidebar.showForTopicsAndActivateCorrectItem(topic, currentUser)
@@ -24,7 +24,7 @@
     FactlinkApp.mainRegion.show(@channel_views)

     @loadTopic slug_title, (topic) =>
-      @commonTopicViews(topic)
+      @showSidebarForTopic(topic)
       @restoreTopicView slug_title, => new TopicView model: topic
       @makePermalinkEvent(topic.url())

@@ -33,7 +33,7 @@
     @main = new TabbedMainRegionLayout();
     FactlinkApp.mainRegion.show(@main)

-    topic = @loadTopic slug_title, => @commonTopicViews topic
+    topic = @loadTopic slug_title, => @showSidebarForTopic topic

     fact = new Fact(id: fact_id)
     fact.fetch
@@ -61,7 +61,7 @@

     channel

-  commonChannelViews: (channel) ->
+  showSidebarForChannel: (channel) ->
     @showRelatedChannels channel
     @showUserProfile channel.user()
     FactlinkApp.Sidebar.showForChannelsOrTopicsAndActivateCorrectItem window.Channels, channel, channel.user()
@@ -83,7 +83,7 @@
     FactlinkApp.mainRegion.show(@channel_views)

     @loadChannel username, channel_id, (channel) =>
-      @commonChannelViews(channel)
+      @showSidebarForChannel(channel)
       @makePermalinkEvent(channel.url())

       @restoreChannelView channel_id, => new ChannelView(model: channel)
@@ -97,7 +97,7 @@
     FactlinkApp.mainRegion.show(@channel_views)

     @loadChannel username, channel_id, (channel) =>
-      @commonChannelViews(channel)
+      @showSidebarForChannel(channel)
       FactlinkApp.Sidebar.activate('stream')
       @makePermalinkEvent(channel.url() + '/activities')


Implemented issue# 376. Added more headers to match.

--- app/assets/javascripts/app/controllers/_ui_element/postmaster_match.coffee
+++ app/assets/javascripts/app/controllers/_ui_element/postmaster_match.coffee
@@ -34,6 +34,10 @@
         name: 'Expert Settings'
         options: [
           {
+            value:    'x-spam'
+            name:     'X-Spam'
+          },
+          {
             value:    'x-spam-flag'
             name:     'X-Spam-Flag'
           },
@@ -50,6 +54,22 @@
             name:     'X-Spam-Status'
           },
           {
+            value:    'x-dspam-result'
+            name:     'X-DSPAM-Result'
+          },
+          {
+            value:    'x-dspam-confidence'
+            name:     'X-DSPAM-Confidence'
+          },
+          {
+            value:    'x-dspam-probability'
+            name:     'X-DSPAM-Probability'
+          },
+          {
+            value:    'x-dspam-signature'
+            name:     'X-DSPAM-Signature'
+          },
+          {
             value:    'importance'
             name:     'Importance'
           },
@@ -98,6 +118,10 @@
           {
             value:    'message-id'
             name:     'Message-Id'
+          },
+          {
+            value:    'in-reply-to'
+            name:     'In-Reply-To'
           },
           {
             value:    'auto-submitted'


Updated shipping-rates-selection directive to use live service

--- app/assets/javascripts/sprangular/directives/shippingRatesSelection.coffee
+++ app/assets/javascripts/sprangular/directives/shippingRatesSelection.coffee
@@ -3,9 +3,33 @@
   templateUrl: 'shipping/rates.html'
   scope:
     order: '='
-  controller: ($scope) ->
-    $scope.rates = [
-      {shippingMethodId: 1, name: 'Standard', cost: 0},
-      {shippingMethodId: 2, name: '2 Day Air', cost: 10.45},
-      {shippingMethodId: 3, name: 'Next Day Air', cost: 29.20}
-    ]
+  controller: ($scope, Cart) ->
+    $scope.loading = false
+    $scope.rates = []
+
+    $scope.$watch 'order.shipping_method_id', (shippingMethodId) ->
+      rate = _.find($scope.rates, (rate) -> rate.shippingMethodId == shippingMethodId)
+
+      if rate
+        $scope.order.shipTotal = rate.cost
+      else
+        $scope.order.shipTotal = 0
+
+      $scope.order.updateTotals()
+
+    # use $scope.$watchGroup when its released
+    for expr in ['order.actualShippingAddress().country', 'order.actualShippingAddress().state', 'order.actualShippingAddress().zipcode']
+      $scope.$watch expr, ->
+        return if $scope.loading
+
+        $scope.loading = true
+        order = $scope.order
+        address = order.actualShippingAddress()
+
+        Cart.shippingRates({countryId: address.countryId, stateId: address.stateId, zipcode: address.zipcode})
+            .then (results) ->
+              $scope.rates = results
+
+              order.shipping_method_id = null unless _.find(results, (rate) -> rate.shippingMethodId == order.shipping_method_id)
+
+              $scope.loading = false


Automatic sluggification of options is lowerCased by default

--- dkobo/koboform/static/js/xlform/XLF.model.choices.coffee
+++ dkobo/koboform/static/js/xlform/XLF.model.choices.coffee
@@ -27,7 +27,7 @@
       if not name
         name = XLF.sluggify(label, {
           preventDuplicates: names
-          lowerCase: false
+          lowerCase: true
           lrstrip: true
           characterLimit: 14
           incrementorPadding: false


[Prefetcher] Remove console.logs from prefetcher

--- workers/social/lib/social/prefetcher.coffee
+++ workers/social/lib/social/prefetcher.coffee
@@ -72,7 +72,6 @@
     route = getRoute options
     cachedRoutes[route] = prefetchedFeeds
     cachedRouteTTL[route] = Date.now()
-    console.log "#{route} cache is updated"
     queue.fin()

   dash queue, ()->
@@ -85,8 +84,6 @@
   prefetchAll intervalOptions, client, (err, data)->
     if err
       return console.log "An error occured while fetching in interval", err
-    else
-      console.log "updating in interval"

 intervalFetcher = setInterval repeatFetchingItems, 25000

@@ -102,7 +99,6 @@
   route = getRoute options
   if cachedRoutes[route]
     if (Date.now() - (cachedRouteTTL[route] || 0)  < cachingTimeInMS)
-      console.log "#{route} cache is still valid"
       prefetchedFeeds = cachedRoutes[route]
       return callback null, prefetchedFeeds


reset popup on success

reset popup on success

--- app/assets/javascripts/droom/forms.js.coffee
+++ app/assets/javascripts/droom/forms.js.coffee
@@ -378,7 +378,9 @@
             on_cancel: popup.hide
             on_complete: (response) =>
               popup.hide()
+              popup = null
               $(affected).trigger "refresh"
+


Update jquery.dialogextend.maximize.coffee

--- src/modules/jquery.dialogextend.maximize.coffee
+++ src/modules/jquery.dialogextend.maximize.coffee
@@ -14,8 +14,8 @@
     "maximize" : null

   maximize:()->
-    newHeight = $(window).height()-11;
-    newWidth = $(window).width()-11;
+    newHeight = $(window).height();
+    newWidth = $(window).width();
     # start!
     # trigger custom event
     @_trigger "beforeMaximize"
@@ -97,4 +97,4 @@
       style += '.ui-dialog .ui-dialog-titlebar-maximize:hover,'
       style += '.ui-dialog .ui-dialog-titlebar-maximize:focus { padding: 0; }'
       style += '</style>'
-      $(style).appendTo("body")+      $(style).appendTo("body")


Fixed rule description.

Fixed rule description.

--- src/rules/no_private_function_fat_arrows.coffee
+++ src/rules/no_private_function_fat_arrows.coffee
@@ -6,9 +6,9 @@
         level: 'warn'
         message: 'Used the fat arrow for a private function'
         description: """
-            Warns when you use the fat arrow for an object method. This is
-            useful for the code style where caller is expected to assure calls
-            of object methods are bound with the correct `this` value.
+            Warns when you use the fat arrow for a private function
+            inside a class defintion scope. It is not necessary and
+            it does not do anything.
             """

     lintAST: (node, @astApi) ->


Announce who was mentioned in new pull request

--- src/scripts/github-pull-request-notifier.coffee
+++ src/scripts/github-pull-request-notifier.coffee
@@ -45,6 +45,10 @@

 announcePullRequest = (data, cb) ->
   if data.action == 'opened'
-    cb "New pull request \"#{data.pull_request.title}\" by #{data.pull_request.user.login}: #{data.pull_request.html_url}"
+    mentioned = data.pull_request.body.match(/(@\w+)/g)
+    if mentioned
+      mentioned_line = "\nMentioned: #{mentioned.join(", ")}"
+    else
+      mentioned_line = ''
+    cb "New pull request \"#{data.pull_request.title}\" by #{data.pull_request.user.login}: #{data.pull_request.html_url}#{mentioned_line}"

-


fix bug in html/markdown rendering in cocalc jupyter notebooks: r kernel with input "2\n3" would output "23"

--- src/smc-webapp/jupyter/cell-output-message.cjsx
+++ src/smc-webapp/jupyter/cell-output-message.cjsx
@@ -177,6 +177,26 @@
         trust      : rtypes.bool

     mixins: [ImmutablePureRenderMixin]
+
+    render_html: (value) ->
+

+
+

+
+    render_markdown: (value) ->
+

+            <Markdown
+                value          = {value}
+                project_id     = {@props.project_id}
+                file_path      = {@props.directory}
+                safeHTML       = {not @props.trust}
+            />
+

     render: ->
         type  = undefined
@@ -196,19 +216,9 @@
                             else
                                 return <TextPlain value={value}/>
                         when 'html', 'latex'  # put latex as HTML, since jupyter requires $'s anyways.
-                            return
+                            return @render_html(value)
                         when 'markdown'
-                            return <Markdown
-                                    value          = {value}
-                                    project_id     = {@props.project_id}
-                                    file_path      = {@props.directory}
-                                    safeHTML       = {not @props.trust}
-                                />
+                            return @render_markdown(value)
                 when 'image'
                     height = width = undefined
                     @props.message.get('metadata')?.forEach? (value, key) =>


plot context default view broken from moving stuff around

--- static/coffee/container.coffee
+++ static/coffee/container.coffee
@@ -531,7 +531,7 @@

 class PlotContext extends Continuum.HasParent
   type : 'PlotContext',
-  default_view : Bokeh.PlotContextView
+  default_view : PlotContextView
   url : () ->
     return super()
   defaults :


apply ussd styling to simulator if needed

--- static/coffee/simulator.coffee
+++ static/coffee/simulator.coffee
@@ -3,6 +3,7 @@
 level_classes = {"I": "iinfo", "W": "iwarn", "E": "ierror"}

 window.updateSimulator = (data) ->
+  ussd = if window.ussd then "ussd" else ""

   $(".simulator-body").html ""
   i = 0
@@ -14,7 +15,7 @@
     level = (if msg.level? then level_classes[msg.level] else "")
     direction = (if (msg.direction is "O") then "from" else "to")

-    $(".simulator-body").append "
" + msg.text + "
"
+    $(".simulator-body").append "
" + msg.text + "
"
     i++
   $(".simulator-body").scrollTop $(".simulator-body")[0].scrollHeight
   $("#simulator textarea").val ""


fix unresolved counts?

fix unresolved counts?

--- resources/assets/coffee/react/beatmap-discussions/main.coffee
+++ resources/assets/coffee/react/beatmap-discussions/main.coffee
@@ -182,7 +182,7 @@

     countsByBeatmap = {}
     countsByPlaymode = {}
-    totalHype = _.filter(@state.beatmapset.discussions, message_type: 'hype', deleted_at: null).length
+    totalHype = 0
     unresolvedIssues = 0
     byMode =
       timeline: []
@@ -202,27 +202,31 @@
       for own _filter, modes of byFilter
         modes[mode] = {}

-
-    for d in @filterDiscussions(@state.beatmapset.discussions)
+    for d in @state.beatmapset.discussions
       # skipped discussion
       # - not privileged (deleted discussion)
       # - deleted beatmap
       continue if _.isEmpty(d)

-      if !d.deleted_at? && d.can_be_resolved && !d.resolved
-        beatmap = @beatmaps()[d.beatmap_id]
-
-        if !d.beatmap_id? || (beatmap? && !beatmap.deleted_at?)
-          unresolvedIssues++
-
-        if beatmap?
-          countsByBeatmap[beatmap.id] ?= 0
-          countsByBeatmap[beatmap.id]++
-
-          if !beatmap.deleted_at?
-            countsByPlaymode[beatmap.mode] ?= 0
-            countsByPlaymode[beatmap.mode]++
-
+      if !d.deleted_at?
+        totalHype++ if d.message_type == 'hype'
+
+        if d.can_be_resolved && !d.resolved
+          beatmap = @beatmaps()[d.beatmap_id]
+
+          if !d.beatmap_id? || (beatmap? && !beatmap.deleted_at?)
+            unresolvedIssues++
+
+          if beatmap?
+            countsByBeatmap[beatmap.id] ?= 0
+            countsByBeatmap[beatmap.id]++
+
+            if !beatmap.deleted_at?
+              countsByPlaymode[beatmap.mode] ?= 0
+              countsByPlaymode[beatmap.mode]++
+
+      # skip if filtering users
+      continue if @state.selectedUserId? && d.user_id != @state.selectedUserId

       mode =
         if d.beatmap_id?
@@ -272,12 +276,6 @@

   discussions: =>
     @cache.discussions ?= _.keyBy @state.beatmapset.discussions, 'id'
-
-
-  filterDiscussions: (discussions) =>
-    return discussions unless @state.selectedUserId?
-
-    _.filter(discussions, user_id: @state.selectedUserId)

   groupedBeatmaps: (discussionSet) =>


Added a stop-when-not-started test

Added a stop-when-not-started test

--- test/RedisPort_test.coffee
+++ test/RedisPort_test.coffee
@@ -21,6 +21,19 @@
 client = null

 describe "Unit", ->
+
+	describe "stop when not started", ->
+		it "should stop anyway", (done) ->
+			client = new RedisPort
+				redisHost: "localhost"
+				# redisPort: 6380
+				redisPort: 6380
+				project:   "vems"
+				env:       "production"
+				host:      "localhost"
+
+			client.stop ->
+				done()

 	describe "not running", ->
 		it "should wait or retry", (done) ->


fix(bower): correct object path

fix(bower): correct object path

--- config.coffee
+++ config.coffee
@@ -29,7 +29,7 @@
         # Vendor code
         'vendor.js': /^(bower_components)\/(?!.+\.spec\.)/
       order:
-        after: bower.curve.javascripts.after or []
+        after: bower.curve.javascripts.order.after or []

         before: (glob.sync(
           # Templates first
@@ -37,7 +37,7 @@

         )).concat(
           # Essential libraries to load before the rest
-          bower.curve.javascripts.before or []
+          bower.curve.javascripts.order.before or []

         ).concat(glob.sync(
           # Library files first


Disable Hubot's HTTP server during the tests

--- test/codenames-test.coffee
+++ test/codenames-test.coffee
@@ -11,10 +11,9 @@
       .reply(200, [{title: 'black', attributes: ['colour']}])
       .get('/data/animals.json')
       .reply(200, [{title: 'bat', attributes: ['air', 'mammal']}])
-    @room = helper.createRoom(http: false)
+    @room = helper.createRoom(httpd: false)

   afterEach ->
-    @room.destroy()
     nock.cleanAll()

   context 'user asks for a suggestion', ->


Fix trade eligibility hardcoded year JS

Fix trade eligibility hardcoded year JS

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -418,11 +418,15 @@
       else
         $("#trade-org-fulfilled-info").addClass("visuallyhidden")

-  # Show trade expiry info if it isn't 2015
+  # Show trade expiry info if it isn't 2015 (earliest year)
   if $(".trade-expiry-input").size() > 0
     $(".trade-expiry-input").bind "change", () ->
-      # TODO: Hardcoded date
-      if $(this).val().toString() != "2015"
+      lowest_year = "9999"
+      $(".trade-expiry-input option").each ->
+        if $(this).attr("value") != ""
+          if parseInt(lowest_year) > parseInt($(this).attr("value"))
+            lowest_year = $(this).attr("value")
+      if $(this).val().toString() != lowest_year
         $("#trade-expiry-info").removeClass("visuallyhidden")
       else
         $("#trade-expiry-info").addClass("visuallyhidden")