reworked request method (fixes #88)

reworked request method (fixes #88)

--- both/methods/shift/shiftRequest.coffee
+++ both/methods/shift/shiftRequest.coffee
@@ -41,15 +41,15 @@
 							Shifts.update _id: shiftId, 'teams._id': teamId,
 								$pull: 'teams.$.declined': _id: userId
 								$addToSet: 'teams.$.participants': user
-
+
 							if team.participants.length == team.max
 								Meteor.call 'closeTeam', shiftId, teamId

 							Meteor.call 'sendTeamUpdate', shiftId, teamId, 'participant'
 						else throw new Meteor.Error 500, TAPi18n.__('modal.shift.maximumReached')
-					else if team.pending.length == team.min - 1
-						#TODO: set condition to team.pending.length >= team.min - 1
-						acceptedUsers = [ userId ]
+					else if team.pending.length >= team.min - 1
+						acceptedUsers = [ ]
+						declinedUsers = [ ]
 						hasTeamleader = false
 						chosenId = null
 						chosenIsTeamleader = false
@@ -78,22 +78,31 @@
 								chosenIsTeamleader = false

 						if hasTeamleader
-							#TODO: if team.pending > team.max, make sure to accept only {{team.max}} users
+							acceptedUsers.push userId
+
 							Shifts.update _id: shiftId, 'teams._id': teamId,
 								$pull: 'teams.$.declined': _id: userId
 								$addToSet: 'teams.$.participants': user

 							for pendingUser in team.pending
-								acceptedUsers.push pendingUser._id
+								if acceptedUsers.length < team.max
+									acceptedUsers.push pendingUser._id
+
+									Shifts.update _id: shiftId, 'teams._id': teamId,
+										$pull:
+											'teams.$.pending': _id: pendingUser._id
+											'teams.$.declined': _id: pendingUser._id
+										$addToSet: 'teams.$.participants': pendingUser
+								else
+									declinedUsers.push pendingUser._id
+
+									Shifts.update _id: shiftId, 'teams._id': teamId,
+										$pull:
+											'teams.$.pending': _id: pendingUser._id
+										$addToSet: 'teams.$.declined': pendingUser

 								if pendingUser.checked
 									pendingUser.checked = false
-
-								Shifts.update _id: shiftId, 'teams._id': teamId,
-									$pull:
-										'teams.$.pending': _id: pendingUser._id
-										'teams.$.declined': _id: pendingUser._id
-									$addToSet: 'teams.$.participants': pendingUser

 							for otherTeam in shift.teams when otherTeam._id != teamId
 								for pendingUser in otherTeam.pending when pendingUser._id in acceptedUsers
@@ -111,6 +120,8 @@

 							for acceptedUser in acceptedUsers
 								Meteor.call 'sendConfirmation', shiftId, teamId, acceptedUser
+							for declinedUser in declinedUsers
+								Meteor.call 'sendDeclined', shiftId, teamId, declinedUser

 							if acceptedUsers.length == team.max
 								Meteor.call 'closeTeam', shiftId, teamId


store zero in counter in fib-iterative example

--- example/fib-iterative.coffee
+++ example/fib-iterative.coffee
@@ -17,6 +17,9 @@
 one = f.value i32, 1
 f.store one, nlesstwofib
 f.store one, nlessonefib
+
+zero = f.value i32, 0
+f.store zero, n

 body = f.while (w) ->
   w.uLessThan w.load(n), target


Update example config

Update example config

--- example_config/config.coffee
+++ example_config/config.coffee
@@ -46,9 +46,10 @@
   # Installed apps.
   apps: {
     www: {
-      name: "Home"
+      name: "Groups"
       about: "Recent activity from all your groups"
       url: base_url
+      image: "#{base_url}/static/img/groups.png"
     }
     firestarter: {
       name: "Firestarter"


2 things for ephraim - more settings and mask style

--- public/dreamcatcher/controllers/entries/books_controller.coffee
+++ public/dreamcatcher/controllers/entries/books_controller.coffee
@@ -74,6 +74,7 @@

   showMore: (el) ->
     bookEl = el.closest '.book'
+    $('.settings-basic', bookEl).toggle()
     $('.more-settings', bookEl).toggle()

   '.book .flat click': (el) ->
@@ -155,6 +156,7 @@
       el.data 'image', image.id
       $('.cover', el).css 'background-image', "url(/images/uploads/#{image.id}-252x252.#{image.format})"
       $('.dropbox-field-shine', el).css 'background', "transparent url(/images/uploads/#{image.id}-252x252.#{image.format}) no-repeat center center"
+      $('.flat',el).addClass 'mask' if not $('.flat',el).hasClass 'mask'

   uploadCancel: ->
     log 'cancelled'


Add util to guarantee the file path

Add util to guarantee the file path

--- modules/util.coffee
+++ modules/util.coffee
@@ -1,3 +1,5 @@
+fs = require('fs')
+
 exports.formatTime = (time) ->
   hour = Math.floor(time / 3600)
   time -= hour * 3600
@@ -8,3 +10,13 @@
   time = '0' + time if time < 10
   return "#{hour}:#{minute}:#{time}"

+guranteeDirPath = (dirname) ->
+  dir = path.dirname dirname
+  unless fs.existsSync dir
+    guranteeDirPath dir
+    fs.mkdirSync dirname
+
+exports.guaranteeFilePath = guranteeFilePath = (filename) ->
+  dir = path.dirname filename
+  unless fs.existsSync dir
+    guaranteeDirPath dir


Account Settings: Credentials: Use fat arrow because it needs to access instance level.

--- client/account/lib/views/accountcredentiallistcontroller.coffee
+++ client/account/lib/views/accountcredentiallistcontroller.coffee
@@ -74,7 +74,7 @@

     options.limit or= @getOption 'limit'

-    JCredential.some query, options, (err, credentials) ->
+    JCredential.some query, options, (err, credentials) =>

       if err
         @hideLazyLoader()


[browser] Add some events on Application

--- src/browser/Application.coffee
+++ src/browser/Application.coffee
@@ -34,23 +34,49 @@
             options ?= @options
             new AppWindow(options)

+        @command.on "app:open", (e, options) ->
+            new AppWindow(options)
+            return
+
+        return
+
     ###
     Window Managements
     ###

     addWindow       : (window) ->
         @windows.push window
+        @emit "did-window-added", window
         return

     removeWindow    : (window) ->
         @windows.splice index, 1 for index, w of @windows when w is window
+        @emit "did-window-remove"
         return

     setLastFocusedWindow    : (window) ->
         AppWindow.lastFocusedWindow = window
+        @emit "did-focused-window-changed", window
         return

     getLastFocusedWindow    : ->
         AppWindow.lastFocusedWindow
         return

+
+    ###
+    Event handler register
+    ###
+
+    on              : (event, listener) ->
+        super
+        new Disposable => @off event, listener
+
+    onDidWindowAdded    : (fn) ->
+        @on "did-window-added", fn
+
+    onDidWindowRemove   : (fn) ->
+        @on "did-window-remove", fn
+
+    onDidFocusedWindowChanged   : (fn) ->
+        @on "did-focused-window-changed", fn


Use a real-path in the spec's fake stack trace

Node stack traces seem to always contain real-paths, even if the
file was required via a symlink path

--- spec/notifications-spec.coffee
+++ spec/notifications-spec.coffee
@@ -356,7 +356,7 @@

           stack = """
             ReferenceError: path is not defined
-              at Object.module.exports.LinkedPackage.wow (#{path.join(packageDir, 'linked-package.coffee')}:29:15)
+              at Object.module.exports.LinkedPackage.wow (#{path.join(fs.realpathSync(packageDir), 'linked-package.coffee')}:29:15)
               at atom-workspace.subscriptions.add.atom.commands.add.linked-package:wow (#{path.join(packageDir, 'linked-package.coffee')}:18:102)
               at CommandRegistry.module.exports.CommandRegistry.handleCommandEvent (/Applications/Atom.app/Contents/Resources/app/src/command-registry.js:238:29)
               at /Applications/Atom.app/Contents/Resources/app/src/command-registry.js:3:61


Translated all remaining unit tests

Translated all remaining unit tests

--- test/xml2js.test.coffee
+++ test/xml2js.test.coffee
@@ -145,16 +145,17 @@
     equ r.sample.arraytest[0].item[1].subitem[1], 'Bar.')

   'test ignore attributes': skeleton(ignoreAttrs: true, (r) ->
-    assert.equal r['chartest'], 'Character data here!'
-    assert.equal r['cdatatest'], 'CDATA here!'
-    assert.deepEqual r['nochartest'], {}
-    assert.equal r['listtest']['item'][0]['#'], 'This is character data!'
-    assert.equal r['listtest']['item'][0]['subitem'][0], 'Foo(1)'
-    assert.equal r['listtest']['item'][0]['subitem'][1], 'Foo(2)'
-    assert.equal r['listtest']['item'][0]['subitem'][2], 'Foo(3)'
-    assert.equal r['listtest']['item'][0]['subitem'][3], 'Foo(4)'
-    assert.equal r['listtest']['item'][1], 'Qux.'
-    assert.equal r['listtest']['item'][2], 'Quux.')
+    console.log 'Result object: ' + util.inspect r, false, 10
+    equ r.sample.chartest, 'Character data here!'
+    equ r.sample.cdatatest, 'CDATA here!'
+    assert.deepEqual r.sample.nochartest[0], {}
+    equ r.sample.listtest[0].item[0]._, '\n            This  is\n            \n            character\n            \n            data!\n            \n        '
+    equ r.sample.listtest[0].item[0].subitem[0], 'Foo(1)'
+    equ r.sample.listtest[0].item[0].subitem[1], 'Foo(2)'
+    equ r.sample.listtest[0].item[0].subitem[2], 'Foo(3)'
+    equ r.sample.listtest[0].item[0].subitem[3], 'Foo(4)'
+    equ r.sample.listtest[0].item[1], 'Qux.'
+    equ r.sample.listtest[0].item[2], 'Quux.')

   'test simple callback mode': (test) ->
     x2js = new xml2js.Parser()
@@ -163,7 +164,7 @@
       x2js.parseString data, (err, r) ->
         assert.equal err, null
         # just a single test to check whether we parsed anything
-        assert.equal r['chartest']['#'], 'Character data here!'
+        assert.equal r.sample.chartest[0]._, 'Character data here!'
         test.finish()

   'test double parse': (test) ->
@@ -173,25 +174,26 @@
       x2js.parseString data, (err, r) ->
         assert.equal err, null
         # make sure we parsed anything
-        assert.equal r['chartest']['#'], 'Character data here!'
+        assert.equal r.sample.chartest[0]._, 'Character data here!'
         x2js.parseString data, (err, r) ->
           assert.equal err, null
-          assert.equal r['chartest']['#'], 'Character data here!'
+          assert.equal r.sample.chartest[0]._, 'Character data here!'
           test.finish()

   'test validator': skeleton(validator: validator, (r) ->
-    assert.equal typeof r['validatortest']['stringtest'], 'string'
-    assert.equal typeof r['validatortest']['numbertest'], 'number'
-    assert.ok r['validatortest']['emptyarray']['item'] instanceof Array
-    assert.equal r['validatortest']['emptyarray']['item'].length, 0
-    assert.ok r['validatortest']['oneitemarray']['item'] instanceof Array
-    assert.equal r['validatortest']['oneitemarray']['item'].length, 1
-    assert.equal r['validatortest']['oneitemarray']['item'], 'Bar.'
-    assert.ok r['arraytest']['item'] instanceof Array
-    assert.equal r['arraytest']['item'].length, 2
-    assert.equal r['arraytest']['item'][0]['subitem'], 'Baz.'
-    assert.equal r['arraytest']['item'][1]['subitem'][0], 'Foo.'
-    assert.equal r['arraytest']['item'][1]['subitem'][1], 'Bar.')
+    console.log 'Result object: ' + util.inspect r, false, 10
+    equ typeof r.sample.validatortest[0].stringtest[0], 'string'
+    equ typeof r.sample.validatortest[0].numbertest[0], 'number'
+    assert.ok r.sample.validatortest[0].emptyarray[0].item instanceof Array
+    equ r.sample.validatortest[0].emptyarray[0].item.length, 0
+    assert.ok r.sample.validatortest[0].oneitemarray[0].item instanceof Array
+    equ r.sample.validatortest[0].oneitemarray[0].item.length, 1
+    equ r.sample.validatortest[0].oneitemarray[0].item[0], 'Bar.'
+    assert.ok r.sample.arraytest[0].item instanceof Array
+    equ r.sample.arraytest[0].item.length, 2
+    equ r.sample.arraytest[0].item[0].subitem[0], 'Baz.'
+    equ r.sample.arraytest[0].item[1].subitem[0], 'Foo.'
+    equ r.sample.arraytest[0].item[1].subitem[1], 'Bar.')

   'test validation error': (test) ->
     x2js = new xml2js.Parser({validator: validator})


Make left dive into diff view

Make left dive into diff view

--- keymaps/git.cson
+++ keymaps/git.cson
@@ -7,7 +7,6 @@
   'ctrl-enter': 'github:commit'

 '.github-StagingView':
-  'left': 'github:focus-diff-view'
   'tab': 'core:focus-next'
   'shift-tab': 'core:focus-previous'


Move BHL link into classify page template

--- overrides.coffee
+++ overrides.coffee
@@ -35,11 +35,7 @@
 current_tool = null
 rect_index = 0

-bhl_link = document.createElement 'a'
-bhl_link.classList.add 'readymade-clickable'
-bhl_link.setAttribute 'href', ''
-bhl_link.setAttribute 'target', 'bhl'
-bhl_link.innerHTML = ' Original page'
+bhl_link = document.querySelector('a[target=bhl]')

 drawing_controls = document.querySelector '.drawing-controls'
 drawing_controls.appendChild bhl_link


Comment some of the overrides

Comment some of the overrides

--- overrides.coffee
+++ overrides.coffee
@@ -113,13 +113,14 @@
         page.decisionTree.tasks.species.clearFilters()
         page.decisionTree.tasks.species.confirmButton.disabled = true

+# disable the species confirm until a value is chosen.
 field_page.el.on field_page.decisionTree.CHANGE, ({originalEvent: {detail}}) ->
   {key, value} = detail

   if key is 'species'
     field_page.decisionTree.tasks.species.confirmButton.disabled = false if value

-
+# pick a random photo if more than one is present on a field subject
 field_page.on field_page.CREATE_CLASSIFICATION, () ->
   subject = field_page.classification.subject
   return unless Array.isArray subject.location.standard
@@ -129,7 +130,8 @@
   field_page.classification.set 'image_src', subject.location.standard[randomIndex]

   subject.location.standard = subject.location.standard[randomIndex]
-
+
+# switch between batch 1 and batch 2 for transcription. Always start on batch 1.
 groups = require './workflows/groups'
 # start with 10 subjects from batch 1
 herbarium_page.Subject.queueMax = 10
@@ -141,6 +143,7 @@
   else
     herbarium_page.Subject.group = groups.batch1

+# Alert before you do your first full transcription record
 herbarium_page.on herbarium_page.LOAD_SUBJECT, (e, subject) ->
   is_transcriber = User.current?.preferences?[currentProject.id]?.transcriber || herbarium_page.transcriber
   transcribeAlert() if subject.group_id == groups.batch2 unless is_transcriber
@@ -164,10 +167,11 @@
   setTimeout ->
     prompt.el[0].querySelector('button').focus()

-
+# add a box to show herbarium barcode number etc.
 subject_metadata = new SubjectMetadata
 herbarium_page.el.find('.decision-tree').prepend subject_metadata.el

+# add a viewer to show the orchid type examples for field photos
 orchid_type = new OrchidType
 field_page.el.find('.decision-tree').before orchid_type.el


get rid of stray debugging statement :(

get rid of stray debugging statement :(

--- src/asset.coffee
+++ src/asset.coffee
@@ -12,7 +12,6 @@
     iterator values _assets

   find: ({path, extension}) ->
-    console.log _assets
     if (rx = _assets[path])?
       if !extension
         first values rx


Missing event bus on Sharer callback

Missing event bus on Sharer callback

--- src/client/sharer/index.coffee
+++ src/client/sharer/index.coffee
@@ -1,6 +1,7 @@
 _        = require '../../utils/lodash'
 track    = require '../track/index'
 findUrl  = require '../../utils/find-url'
+EventBus = require '../../utils/eventbus'

 FacebookShare = require './facebook'
 EmailShare = require './email'


Add team name to subscription model

Add team name to subscription model

--- services/web/app/coffee/models/Subscription.coffee
+++ services/web/app/coffee/models/Subscription.coffee
@@ -12,6 +12,7 @@
 	invited_emails:  [ String ]
 	teamInvites  :   [ TeamInviteSchema ]
 	recurlySubscription_id : String
+	teamName 	 : {type: String}
 	planCode 	 : {type: String}
 	groupPlan	 : {type: Boolean, default: false}
 	membersLimit: {type:Number, default:0}


Put note in pending

Put note in pending

Forever~

--- resources/assets/coffee/react/beatmap-discussions/main.coffee
+++ resources/assets/coffee/react/beatmap-discussions/main.coffee
@@ -194,8 +194,6 @@

         if d.deleted_at?
           filters.push 'deleted'
-        else if d.message_type == 'mapper_note'
-          filters.push 'mapperNotes'
         else if d.message_type == 'praise'
           filters.push 'praises'
         else
@@ -206,6 +204,9 @@

         if d.user_id == @state.currentUser.id
           filters.push 'mine'
+
+        if d.message_type == 'mapper_note'
+          filters.push 'mapperNotes'

         for filter in filters
           byFilter[filter][mode][d.id] = d


Fix sidebar logs

Fix sidebar logs

--- admin/static/coffee/app.coffee
+++ admin/static/coffee/app.coffee
@@ -129,23 +129,24 @@
         if not machines.get(machine_uuid)?
             continue # Machine not ready or down, we skip

-        if not machines.get(machine_uuid).get('log_entries')?
-            machines.get(machine_uuid).set 'log_entries', new LogEntries
-
         for json in log_entries
-            entry = new LogEntry json
-            entry.set('machine_uuid',machine_uuid)
-
-            machines.get(machine_uuid).get('log_entries').add entry
-
-
-            if recent_log_entries.length > 3
-                recent_log_entries.shift()
-            recent_log_entries.add entry
-
-
-            if parseFloat(json.timestamp) > recent_log_entries.min_timestamp
-                recent_log_entries.min_timestamp = Math.ceil parseFloat json.timestamp # /ajax/log juste compare integers
+
+            new_timestamp = parseFloat(json.timestamp)
+            for entry, i in recent_log_entries.models
+                if new_timestamp > parseFloat(entry.get('timestamp'))
+                    entry = new LogEntry json
+                    entry.set('machine_uuid',machine_uuid)
+
+                    recent_log_entries.splice i, 0, entry
+                    if recent_log_entries.length > 3
+                        recent_log_entries.shift()
+
+                    if parseFloat(json.timestamp) > recent_log_entries.min_timestamp
+                        recent_log_entries.min_timestamp = Math.ceil parseFloat json.timestamp # /ajax/log juste compare integers
+            if recent_log_entries.length < 4
+                entry = new LogEntry json
+                entry.set('machine_uuid',machine_uuid)
+                recent_log_entries.push entry

 set_stats = (stat_data) ->
     for machine_id, data of stat_data


Make the interpolation function works even when we are missing more than one step

--- admin/static/coffee/vis.coffee
+++ admin/static/coffee/vis.coffee
@@ -243,7 +243,7 @@
             for i in [0..(@ndp-1)]
                 @values.push(0)
             @next_value = null
-            @last_date = Date.now()
+            @last_date = null

         step: (num_points) ->
             # First, grab newest data
@@ -253,26 +253,34 @@
             return @values.slice(-num_points)

         push_data: ->
+            if @last_date is null # First time we call @push_data
+                @last_date = Date.now()
+                @values.push @get_data_fn()
+                return true
+
+            # Check if we need to restart interpolation
+            current_value = @get_data_fn()
+            if @next_value isnt current_value
+                @start_value = @values[@values.length - 1]
+                @next_value = current_value
+                @interpolation_step = 1
+
+
             elapsed_time = Date.now()-@last_date
-            current_value = @values[@values.length - 1]
-
-            next_value = @get_data_fn()
-
-            if elapsed_time > 1/@nip
-                missing_pixels = elapsed_time*@nip
-                for i in [1..missing_pixels]
-                    @values.push current_value+(next_value-current_value)/(elapsed_time*@nip)*i
-                @interpolation_step = 1
-            else
-                if @interpolation_step > @mip
-                    @values.push next_value
-                    @interpolation_step++
+            missing_steps = Math.max 1, Math.round(elapsed_time/1000*@nip) # If the tab has focus, we have missing_steps = 1 else we have missing_steps > 1
+            console.log missing_steps
+            for i in [1..missing_steps]
+                if @values[@values.length - 1] is @next_value
+                    value_to_push = @next_value
                 else
-                    @values.push current_value+(next_value-current_value)/(elapsed_time*@nip)*@interpolation_step
-                @interpolation_step++
+                    value_to_push = @start_value + ((@next_value - @start_value) / @nip * @interpolation_step)
+                    @interpolation_step += 1
+                    if @interpolation_step > @nip
+                        value_to_push = @next_value
+                @values.push(value_to_push)
+
             @last_date = Date.now()

             # Trim the cache
             if @values.length > @ndp
                 @values = @values.slice(-@ndp)
-


Convert collection name to camel case in query API

--- lib/nbi.coffee
+++ lib/nbi.coffee
@@ -403,6 +403,15 @@
       )
     else if QUERY_REGEX.test(urlParts.pathname)
       collectionName = QUERY_REGEX.exec(urlParts.pathname)[1]
+
+      # Convert to camel case
+      i = collectionName.indexOf('_')
+      while i >= 0
+        ++ i
+        up = if i < collectionName.length then collectionName[i].toUpperCase() else ''
+        collectionName = collectionName.slice(0, i - 1) + up + collectionName.slice(i + 1)
+        i = collectionName.indexOf('_', i)
+
       if request.method not in ['GET', 'HEAD']
         response.writeHead 405, {'Allow' : 'GET, HEAD'}
         response.end('405 Method Not Allowed')


Start express listener only if --disable-httpd not passed, and keep a rest of express setup

--- src/robot.coffee
+++ src/robot.coffee
@@ -48,7 +48,7 @@
     @logger    = new Log process.env.HUBOT_LOG_LEVEL or 'info'

     @parseVersion()
-    @setupExpress() if httpd
+    @setupExpress(httpd)
     @pingIntervalId = null
     @loadAdapter adapterPath, adapter

@@ -221,7 +221,7 @@
   # Setup the Express server's defaults.
   #
   # Returns nothing.
-  setupExpress: ->
+  setupExpress: (httpd)->
     user    = process.env.EXPRESS_USER
     pass    = process.env.EXPRESS_PASSWORD
     stat    = process.env.EXPRESS_STATIC
@@ -234,7 +234,7 @@
     app.use express.bodyParser()
     app.use express.static stat if stat

-    @server = app.listen process.env.PORT || 8080
+    @server = app.listen process.env.PORT || 8080 if httpd
     @router = app

     herokuUrl = process.env.HEROKU_URL


Environments-newui: hide domain panel on hiding environments

--- client/app/Applications/StartTab.kdapplication/AppView.coffee
+++ client/app/Applications/StartTab.kdapplication/AppView.coffee
@@ -35,6 +35,8 @@
         title         : "Hide environments"
         callback      : (cb)=>
           @serverContainer.setHeight 2
+          @serverContainer.scene.unsetClass 'out'
+          @serverContainer.scene.setClass 'in'
           cb()
       ]


SidebarOwnMachineList: show group stack title if it's a custom group

--- client/app/lib/activity/sidebar/sidebarownmachineslist.coffee
+++ client/app/lib/activity/sidebar/sidebarownmachineslist.coffee
@@ -1,10 +1,14 @@
+globals            = require 'globals'
 SidebarMachineList = require './sidebarmachinelist'

 module.exports = class SidebarOwnMachinesList extends SidebarMachineList

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

-    options.title       = 'Your VMs'
+    if globals.currentGroup.slug isnt 'koding'
+      title = "#{globals.currentGroup.title} VM Stack"
+
+    options.title       = title ? 'Your VMs'
     options.hasPlusIcon = yes
     options.cssClass    = 'my-machines'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -37,6 +37,9 @@
       parseInt(bits[3])
       if bits[4]? then parseFloat(bits[4]) else 1.0
     ]
+
+  normalizeKey = (key) ->
+    key.toLowerCase().replace(/ /g, "")

   window.Color = (color) ->
     color ||= "rgba(0, 0, 0, 0)"
@@ -1671,7 +1674,5 @@
   ]

   names.each (element) ->
-    lookup[element[1].toLowerCase().replace(/ /g, "")] = parseHex(element[0])
-
-  console.log lookup
+    lookup[normalize(element[1])] = parseHex(element[0])
 )()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -44,7 +44,7 @@
       if Object.prototype.toString.call(arguments[0]) == '[object Array]'
         parsedColor = [parseInt(arguments[0][0]), parseInt(arguments[0][1]), parseInt(arguments[0][2]), parseFloat(arguments[1])]
       else if Object.prototype.toString.call(arguments[0]) != '[object Array]'
-        parsedColor = [parseHex(arguments[0]) || parseRGB(arguments[0], arguments[1]]
+        parsedColor = [lookup[normalizeKey(arguments[0])] || parseHex(arguments[0]) || parseRGB(arguments[0], arguments[1]]
     else if arguments.length > 2
       alpha = if arguments[3]? then arguments[3] else 1
       parsedColor = [parseInt(arguments[0]), parseInt(arguments[1]), parseInt(arguments[2]), parseFloat(alpha)]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -764,6 +764,18 @@

     ###*
     returns the Hex representation of the color. Exclude the leading `#` by passing false.
+
+

+    color = Color('hsl(60, 1, 0.5)')
+
+    # passing nothing will leave the `#` intact
+    color.toHex()
+    # => '#ffff00'
+
+    # passing false will remove the `#`
+    color.toHex(false)
+    # => 'ffff00'
+

     @name toHex
     @methodOf Color#


fixed commended tests

fixed commended tests

--- services/clsi/test/unit/coffee/CompileManagerTests.coffee
+++ services/clsi/test/unit/coffee/CompileManagerTests.coffee
@@ -259,13 +259,19 @@
 				@CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout})
 				@CompileManager.syncFromCode @project_id, @user_id, @file_name, @line, @column, @callback

-			# it "should execute the synctex binary", ->
-			# 	bin_path = Path.resolve(__dirname + "/../../../bin/synctex")
-			# 	synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf"
-			# 	file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}"
-			# 	@child_process.execFile
-			# 		.calledWith(bin_path, ["code", synctex_path, file_path, @line, @column], timeout: 10000)
-			# 		.should.equal true
+			it "should execute the synctex binary", ->
+				bin_path = Path.resolve(__dirname + "/../../../bin/synctex")
+				synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf"
+				file_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/#{@file_name}"
+				@CommandRunner.run
+					.calledWith(
+						"#{@project_id}-#{@user_id}",
+						['/opt/synctex', 'code', synctex_path, file_path, @line, @column],
+						"#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}",
+						@Settings.clsi.docker.image,
+						10000,
+						{}
+						).should.equal true

 			it "should call the callback with the parsed output", ->
 				@callback
@@ -285,12 +291,17 @@
 				@CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:@stdout})
 				@CompileManager.syncFromPdf @project_id, @user_id, @page, @h, @v, @callback

-			# it "should execute the synctex binary", ->
-			# 	bin_path = Path.resolve(__dirname + "/../../../bin/synctex")
-			# 	synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf"
-			# 	@CommandRunner.run
-			# 		.calledWith(bin_path, ["pdf", synctex_path, @page, @h, @v], timeout: 10000)
-			# 		.should.equal true
+			it "should execute the synctex binary", ->
+				bin_path = Path.resolve(__dirname + "/../../../bin/synctex")
+				synctex_path = "#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}/output.pdf"
+				@CommandRunner.run
+					.calledWith(
+						"#{@project_id}-#{@user_id}",
+						['/opt/synctex', "pdf", synctex_path, @page, @h, @v],
+						"#{@Settings.path.compilesDir}/#{@project_id}-#{@user_id}",
+						@Settings.clsi.docker.image,
+						10000,
+						{}).should.equal true

 			it "should call the callback with the parsed output", ->
 				@callback


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -41,7 +41,7 @@

     parsedColor = null

-    parsedColor = parseRGB(color) || parseHex(color)
+    parsedColor = parseHex(color) || parseRGB(color)

     alpha = parsedColor[3]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -11,9 +11,9 @@

     bits = rgbParser.exec(colorString)
     return [
-      toHex(bits[1])
-      toHex(bits[2])
-      toHex(bits[3])
+      bits[1]
+      bits[2]
+      bits[3]
     ]

   window.Color = (color) ->


Inject Timer later in the load/inject chain.

--- community/browser/app/scripts/init/commandInterpreters.coffee
+++ community/browser/app/scripts/init/commandInterpreters.coffee
@@ -22,8 +22,7 @@
 .config([
   'FrameProvider'
   'Settings'
-  'Timer'
-  (FrameProvider, Settings, Timer) ->
+  (FrameProvider, Settings) ->

     cmdchar = Settings.cmdchar

@@ -326,7 +325,7 @@
         pattern = new RegExp("^[^#{cmdchar}]")
         input.match(pattern)
       templateUrl: 'views/frame-cypher.html'
-      exec: ['Cypher', 'CypherGraphModel', 'CypherParser', (Cypher, CypherGraphModel, CypherParser) ->
+      exec: ['Cypher', 'CypherGraphModel', 'CypherParser', 'Timer', (Cypher, CypherGraphModel, CypherParser, Timer) ->
         # Return the function that handles the input
         (input, q) ->
           current_transaction = Cypher.transaction()


Missed this

Missed this

--- packages/shrub-rpc/index.coffee
+++ packages/shrub-rpc/index.coffee
@@ -82,7 +82,7 @@
       (req, res, next) ->

         # Log an error without transmitting it.
-        logError = (error) -> logger.error errors.stack error
+        logError = (error) -> logger.error error

         # Hub for RPC calls. Dispatch routes.
         req.socket.on 'shrub-rpc', ({path, data}, fn) ->


:bug: fix error when cellStyle is undefined

--- src/renderer/components/doc-table-component.coffee
+++ src/renderer/components/doc-table-component.coffee
@@ -22,7 +22,7 @@
                 {
                   for cell, j in row
                     dstyle = if j == 0 then styles.tb_key else styles.tb_desc
-
+
                       {cell}

                 }
@@ -48,7 +48,7 @@
     paddingRight: 20

   tb_desc:
-    color: '#333'
+    color: '#666'

 DocTableComponent.contextTypes =
   ctx: React.PropTypes.any


Don't accept a column without a name. Also, default the column type to 'string'

--- app/assets/javascripts/erd/erd.js.coffee
+++ app/assets/javascripts/erd/erd.js.coffee
@@ -360,8 +360,8 @@
         model = $('#new_model_name').val()
         columns = ''
         $('#create_model_table > tbody > tr').each (i, row) ->
-          [name, type] = $(row).find('input')
-          columns += "#{$(name).val()}:#{$(type).val()} "
+          [name, type] = ($(v).val() for v in $(row).find('input'))
+          columns += "#{name}#{if type then ":#{type}" else ''} " if name
         window.erd.upsert_change 'create_model', model, columns, '', ''
         $(this).find('table > tbody > tr').each (i, row) ->
           row.remove() if i >= 1


Remove settings

Remove settings

--- src/components/course-administration/settings.cjsx
+++ src/components/course-administration/settings.cjsx
@@ -21,7 +21,7 @@
   render: ->
     course = CourseStore.get(@props.courseId)

-      <CourseInformation course={course}/>
+
Course: {course.name}

Roster

       <BS.TabbedArea defaultActiveKey=0>
           {@renderPeriodTabs(course)}


relative time js function complete

relative time js function complete

--- app/assets/javascripts/motions.js.coffee
+++ app/assets/javascripts/motions.js.coffee
@@ -41,14 +41,14 @@
   if $(".relative-time").length > 0
     date_offset = new Date()
     offset = date_offset.getTimezoneOffset()/-60
-    $(".relative-time").each((index, element) ->
+    $(".relative-time").each((index, element)->
       date = $(element).html()
       local_datetime = new Date()
-      local_datetime.setYear(date.substring(6,10))
-      local_datetime.setMonth((parseInt(date.substring(3,5)) - 1).toString())
-      local_datetime.setDate(date.substring(0,2))
+      local_datetime.setYear(date.substring(0,4))
+      local_datetime.setMonth((parseInt(date.substring(5,7)) - 1).toString())
+      local_datetime.setDate(date.substring(8,10))
       local_datetime.setHours((parseInt(date.substring(11,13)) + offset).toString())
-      $(element).html(local_datetime)
+      $(element).html(String(local_datetime))
     )
 #** presence validations: use this function any where just assign the class .presence-required
 #   to the text field in question and the .check-presence to the submit button **


Remove jQuery usage from lightcurve script.

--- assets/js/lightcurve.coffee
+++ assets/js/lightcurve.coffee
@@ -1,121 +1,119 @@
 d3 = require('d3')

-$ = jQuery
-$ ->
-    class LightCurve
-        constructor: (@selector, @epoch, @period) ->
-            @setGeometry()
-            @svg = @getSvg(@selector)
-            @isPeriodic = @epoch and @period
+class LightCurve
+    constructor: (@selector, @epoch, @period) ->
+        @setGeometry()
+        @svg = @getSvg(@selector)
+        @isPeriodic = @epoch and @period

-        setGeometry: ->
-            @margin =
-                top: 20
-                right: 10
-                bottom: 40
-                left: 50
-            @width = $(@selector).width()
-            @height = 0.3 * @width
-            @width = @width - @margin.left - @margin.right
-            @height = @height - @margin.top - @margin.bottom
-            @xScale = d3.scale.linear().range([0, @width]).nice()
-            @yScale = d3.scale.linear().range([0, @height]).nice()
-            @xAxis = d3.svg.axis().scale(@xScale).orient('bottom').ticks(10).tickSize(-@height, 0, 0)
-            @yAxis = d3.svg.axis().scale(@yScale).orient('left').ticks(10).tickSize(-@width, 0, 0)
+    setGeometry: ->
+        @margin =
+            top: 20
+            right: 10
+            bottom: 40
+            left: 50
+        @width = d3.select(@selector).node().offsetWidth - 40
+        @height = 0.3 * @width
+        @width = @width - @margin.left - @margin.right
+        @height = @height - @margin.top - @margin.bottom
+        @xScale = d3.scale.linear().range([0, @width]).nice()
+        @yScale = d3.scale.linear().range([0, @height]).nice()
+        @xAxis = d3.svg.axis().scale(@xScale).orient('bottom').ticks(10).tickSize(-@height, 0, 0)
+        @yAxis = d3.svg.axis().scale(@yScale).orient('left').ticks(10).tickSize(-@width, 0, 0)

-        getSvg: (selector) ->
-            @svg = d3.select(selector).append('svg')
-                .attr
-                    width: @width + @margin.left + @margin.right
-                    height: @height + @margin.top + @margin.bottom
-                .append('g')
-                .attr
-                    transform: "translate(#{@margin.left},#{@margin.top})"
-            @svg.append('g')
-                .attr
-                    class: 'x axis'
-                    transform: "translate(0,#{@height})"
-            @xTitle = @svg.append('text')
-                .attr
-                    class: 'axisTitle'
-                    transform: "translate(#{@width / 2},#{@height + @margin.bottom})"
-            @yTitle = @svg.append('text')
-                .attr
-                    class: 'axisTitle'
-                    transform: "rotate(-90) translate(-#{@height / 2}, -#{@margin.left / 1.3})"
-            @svg.append('g')
-                .attr
-                    class: 'y axis'
-            @svg
+    getSvg: (selector) ->
+        @svg = d3.select(selector).append('svg')
+            .attr
+                width: @width + @margin.left + @margin.right
+                height: @height + @margin.top + @margin.bottom
+            .append('g')
+            .attr
+                transform: "translate(#{@margin.left},#{@margin.top})"
+        @svg.append('g')
+            .attr
+                class: 'x axis'
+                transform: "translate(0,#{@height})"
+        @xTitle = @svg.append('text')
+            .attr
+                class: 'axisTitle'
+                transform: "translate(#{@width / 2},#{@height + @margin.bottom})"
+        @yTitle = @svg.append('text')
+            .attr
+                class: 'axisTitle'
+                transform: "rotate(-90) translate(-#{@height / 2}, -#{@margin.left / 1.3})"
+        @svg.append('g')
+            .attr
+                class: 'y axis'
+        @svg

-        getPhase: (jd) =>
-            if @isPeriodic
-                periods = (jd - @epoch) / @period
-                phase = periods % 1
-            else
-                0.0
+    getPhase: (jd) =>
+        if @isPeriodic
+            periods = (jd - @epoch) / @period
+            phase = periods % 1
+        else
+            0.0

-        setData: (@data) ->
+    setData: (@data) ->

-        drawChart: ->
-            @xScale.domain d3.extent @data, (d) -> d.jd
-            @yScale.domain d3.extent @data, (d) -> d.magnitude
-            @svg.select('.x.axis')
-                .call(@xAxis)
-            @svg.select('.y.axis')
-                .call(@yAxis)
-            @svg.selectAll('circle')
-                .data(@data)
-                .enter()
-                .append('circle')
-                .attr
-                    cx: (d) => @xScale d.jd
-                    cy: (d) => @yScale d.magnitude
-                    r: '2'
-                .on 'mouseover', ->
-                    d3.select(@).transition().duration(150).attr('r', '10')
-                .on 'mouseout', ->
-                    d3.select(@).transition().duration(150).attr('r', '2')
-                .append('title')
-                .text((d) -> "JD #{d.jd}:  #{d.magnitude} mag.")
-            @xTitle.text 'Julian Date'
-            @yTitle.text 'Magnitude'
+    drawChart: ->
+        @xScale.domain d3.extent @data, (d) -> d.jd
+        @yScale.domain d3.extent @data, (d) -> d.magnitude
+        @svg.select('.x.axis')
+            .call(@xAxis)
+        @svg.select('.y.axis')
+            .call(@yAxis)
+        @svg.selectAll('circle')
+            .data(@data)
+            .enter()
+            .append('circle')
+            .attr
+                cx: (d) => @xScale d.jd
+                cy: (d) => @yScale d.magnitude
+                r: '2'
+            .on 'mouseover', ->
+                d3.select(@).transition().duration(150).attr('r', '10')
+            .on 'mouseout', ->
+                d3.select(@).transition().duration(150).attr('r', '2')
+            .append('title')
+            .text((d) -> "JD #{d.jd}:  #{d.magnitude} mag.")
+        @xTitle.text 'Julian Date'
+        @yTitle.text 'Magnitude'

-        updateChart: (isPhaseChart) ->
-            xDomain = if isPhaseChart then [0, 1] else d3.extent @data, (d) -> d.jd
-            @xScale.domain xDomain
-            @svg.select('.x.axis')
-                .call(@xAxis)
-            @svg.selectAll('circle')
-                .transition()
-                .duration(1000)
-                .attr
-                    cx: (d) => @xScale if isPhaseChart then d.phase else d.jd
-            @xTitle.text if isPhaseChart then 'Phase' else 'Julian Date'
+    updateChart: (isPhaseChart) ->
+        xDomain = if isPhaseChart then [0, 1] else d3.extent @data, (d) -> d.jd
+        @xScale.domain xDomain
+        @svg.select('.x.axis')
+            .call(@xAxis)
+        @svg.selectAll('circle')
+            .transition()
+            .duration(1000)
+            .attr
+                cx: (d) => @xScale if isPhaseChart then d.phase else d.jd
+        @xTitle.text if isPhaseChart then 'Phase' else 'Julian Date'

-    selector = '.lightcurve'
-    epoch = +$('.catalog-data .epoch').text()
-    period = +$('.catalog-data .period').text()
-    lc = new LightCurve(selector, epoch, period)
-    csvUrl = $(selector).data 'csvSource'
-    if csvUrl
-        isPhaseChart = false
-        d3.csv(
-            csvUrl,
-            ((d) ->
-                jd: +d.jd
-                magnitude: +d.magnitude
-                phase: lc.getPhase +d.jd
-            ),
-            ((error, data) ->
-                lc.setData data
-                lc.drawChart()
-            )
+selector = '.lightcurve'
+epoch = +d3.select('.catalog-data .epoch').text()
+period = +d3.select('.catalog-data .period').text()
+lc = new LightCurve(selector, epoch, period)
+csvUrl = d3.select(selector).node().dataset.csvSource
+if csvUrl
+    isPhaseChart = false
+    d3.csv(
+        csvUrl,
+        ((d) ->
+            jd: +d.jd
+            magnitude: +d.magnitude
+            phase: lc.getPhase +d.jd
+        ),
+        ((error, data) ->
+            lc.setData data
+            lc.drawChart()
         )
-        d3.select('#toggle-chart')
-            .on 'click', =>
-                if lc.isPeriodic
-                    isPhaseChart = not isPhaseChart
-                    lc.updateChart isPhaseChart
-                d3.event.preventDefault()
+    )
+    d3.select('#toggle-chart')
+        .on 'click', =>
+            if lc.isPeriodic
+                isPhaseChart = not isPhaseChart
+                lc.updateChart isPhaseChart
+            d3.event.preventDefault()


Now comment has loading spinner

Now comment has loading spinner

--- app/coffee/modules/common/history.coffee
+++ app/coffee/modules/common/history.coffee
@@ -60,7 +60,7 @@
         return @rs.history.undeleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId)

-HistoryDirective = ($log) ->
+HistoryDirective = ($log, $loading) ->
     templateChangeDiff = _.template("""

@@ -440,14 +440,19 @@
         $el.on "click", ".add-comment a.button-green", (event) ->
             event.preventDefault()

+            target = angular.element(event.currentTarget)
+
             $el.find(".comment-list").addClass("activeanimation")
             onSuccess = ->
-                $ctrl.loadHistory(type, objectId)
+                $ctrl.loadHistory(type, objectId).finally ->
+                    $loading.finish(target)

             onError = ->
+                $loading.finish(target)
                 $confirm.notify("error")

             model = $scope.$eval($attrs.ngModel)
+            $loading.start(target)
             $ctrl.repo.save(model).then(onSuccess, onError)

         $el.on "click", ".show-more", (event) ->
@@ -512,4 +517,4 @@
     }

-module.directive("tgHistory", ["$log", HistoryDirective])
+module.directive("tgHistory", ["$log", "$tgLoading", HistoryDirective])


Use new parseFileSync method of plist npm

--- src/app/text-mate-theme.coffee
+++ src/app/text-mate-theme.coffee
@@ -18,10 +18,9 @@
     super

   buildRulesets: ->
-    plist.parseString fs.read(@path), (error, [{settings}]) =>
-      throw new Error("Error loading theme at '#{@path}': #{error}") if error
-      @buildGlobalSettingsRulesets(settings[0])
-      @buildScopeSelectorRulesets(settings[1..])
+    {settings} = plist.parseFileSync(@path)
+    @buildGlobalSettingsRulesets(settings[0])
+    @buildScopeSelectorRulesets(settings[1..])

   getStylesheet: ->
     lines = []


case insensitive email and password length validator

--- app/controllers/device-controller.coffee
+++ app/controllers/device-controller.coffee
@@ -13,10 +13,12 @@

   create: (request, response) =>
     {email,password} = request.body
+    return response.status(422).send new Error('Password required') if _.isEmpty(password)
     return response.status(422).send new Error('Invalid email') unless validator.isEmail(email)

     deviceModel = new DeviceAuthenticator @authenticatorUuid, @authenticatorName, meshblu: @meshblu, meshbludb: @meshbludb
     query = {}
+    email = email.toLowerCase()
     query[@authenticatorUuid + '.id'] = email
     device =
       type: 'octoblu:user'


remove album art from song queue

remove album art from song queue

--- app/frontend/scripts/templates.js.coffee
+++ app/frontend/scripts/templates.js.coffee
@@ -17,8 +17,6 @@
     {{name}}
     by {{{artist_tag}}}
     from {{{album_tag}}}
-
-    {{{art_tag}}}

     {{>star}}
     {{>queuing}}


Extract function to ensure scoll position doesn't inadvertantly change

--- app/js/jail_iframe/util/highlight.coffee
+++ app/js/jail_iframe/util/highlight.coffee
@@ -1,3 +1,12 @@
+ensureScrollSaved = (func) ->
+  try
+    # Store scroll settings to reset to afterwards
+    scrollTop = window.document.body.scrollTop
+    scrollLeft = window.document.body.scrollLeft
+    func()
+  finally
+    # Scroll back to previous location
+    window.scroll scrollLeft, scrollTop
 # Chrome, Firefox, Safari
 searchWithWindowFind = (searchString) ->
   # Trim


Switch Crystal beautifier to using Executables

--- src/beautifiers/crystal.coffee
+++ src/beautifiers/crystal.coffee
@@ -8,28 +8,28 @@
 module.exports = class Crystal extends Beautifier
   name: "Crystal"
   link: "http://crystal-lang.org"
-  isPreInstalled: false
+  executables: [
+    {
+      name: "Crystal"
+      cmd: "crystal"
+      homepage: "http://crystal-lang.org"
+      installation: "https://crystal-lang.org/docs/installation/"
+      version: {
+        parse: (text) -> text.match(/Crystal (\d+\.\d+\.\d+)/)[1]
+      }
+    }
+  ]

   options: {
     Crystal: false
   }

   beautify: (text, language, options) ->
-    # Seems that Crystal dosen't have Windows support yet.
-    if @isWindows
-      @Promise.reject(@commandNotFoundError(
-        'crystal'
-        {
-        link: "http://crystal-lang.org"
-        program: "crystal"
-        })
+    @exe("crystal").run([
+      'tool',
+      'format',
+      tempFile = @tempFile("temp", text)
+      ], {ignoreReturnCode: true})
+      .then(=>
+        @readFile(tempFile)
       )
-    else
-      @run("crystal", [
-        'tool',
-        'format',
-        tempFile = @tempFile("temp", text)
-        ], {ignoreReturnCode: true})
-        .then(=>
-          @readFile(tempFile)
-        )


:lipstick: Fix typo in dialog.

:lipstick: Fix typo in dialog.

--- src/browser/atom-window.coffee
+++ src/browser/atom-window.coffee
@@ -70,7 +70,7 @@
       chosen = dialog.showMessageBox @browserWindow,
         type: 'warning'
         buttons: ['Close', 'Keep Waiting']
-        message: 'Editor is not responsing'
+        message: 'Editor is not responding'
         detail: 'The editor is not responding. Would you like to force close it or just keep waiting?'
       @browserWindow.destroy() if chosen is 0


Decode blog excerpts too. (#3294)

Decode blog excerpts too. (#3294)

--- app/pages/home-not-logged-in/social.cjsx
+++ app/pages/home-not-logged-in/social.cjsx
@@ -67,7 +67,7 @@

{he.decode(post.title)}

-                  {post.excerpt}
+                  {he.decode(post.excerpt)}

                   {moment(new Date(post.created_at)).fromNow()}


trying some api things

trying some api things

--- app/routes/dashboard/repositories.coffee
+++ app/routes/dashboard/repositories.coffee
@@ -7,22 +7,24 @@
     filter: { replace: true }
   model: ->
     apiEndpoint = config.apiEndpoint
-    $.ajax(apiEndpoint + '/v3/repos?repository.active=true', {
+    console.log(@auth)
+    # $.ajax(apiEndpoint + '/v3/repos?repository.active=true', {
+    $.ajax(apiEndpoint + '/v3/#{params.owner}?include=user.repositories,organization.repositories,build.commit', {
       headers: {
         Authorization: 'token ' + @auth.token()
       }
     }).then (response) ->
-      response.repositories.sortBy('last_build.finished_at').filter( (repo) ->
-        repo.last_build
-      ).sort( (a, b) ->
-        if !a.last_build.finished_at || a.last_build.finished_at > b.last_build.finished_at
-          return -1
-        else if !b.last_build.finished_at || b.last_build.finished_at > a.last_build.finished_at
-          return 1
-        else
-          return 0
-      ).map( (repo) ->
-        Ember.Object.create(repo)
-      )
+      # response.repositories.sortBy('last_build.finished_at').filter( (repo) ->
+      #   repo.last_build
+      # ).sort( (a, b) ->
+      #   if !a.last_build.finished_at || a.last_build.finished_at > b.last_build.finished_at
+      #     return -1
+      #   else if !b.last_build.finished_at || b.last_build.finished_at > a.last_build.finished_at
+      #     return 1
+      #   else
+      #     return 0
+      # ).map( (repo) ->
+      #   Ember.Object.create(repo)
+      # )

 `export default Route`


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -14,7 +14,7 @@
   # Add events and methods here
   self.bind "update", ->
     for property, data of I.activeTweens
-      I[property] = E
+      I[property] = Easing[]

   tween: (duration, properties) ->
     for property, target of properties


- [grunt] css source maps excluded from browserSync task since they forced page to reload

--- tasks/grunt-browser-sync.coffee
+++ tasks/grunt-browser-sync.coffee
@@ -21,4 +21,5 @@
       src: [
         '<%= path.build.root %>/{,**/}*'
         '<%= path.build.styles %>/{,**/}*'
+        '!<%= path.build.styles %>/{,**/}*.css.map'
       ]


Ukrainian list of contributors update

Ukrainian list of contributors update

--- app/views/contribute/DiplomatView.coffee
+++ app/views/contribute/DiplomatView.coffee
@@ -133,7 +133,7 @@
     he: ['OverProgram', 'monetita']             # עברית, Hebrew
     lt: []             # lietuvių kalba, Lithuanian
     sr: []             # српски, Serbian
-    uk: ['fess89', 'ImmortalJoker', 'gorodsb', 'endrilian', 'OlenaGapak']             # українська мова, Ukrainian
+    uk: ['fess89', 'ImmortalJoker', 'gorodsb', 'endrilian', 'OlenaGapak', 'probil', 'Rarst']             # українська мова, Ukrainian
     hi: []             # मानक हिन्दी, Hindi
     ur: []             # اُردُو, Urdu
     ms: []             # Bahasa Melayu, Bahasa Malaysia


Don't explode when there are no events on a BeatmapSet

--- resources/assets/coffee/react/beatmap-discussions/main.coffee
+++ resources/assets/coffee/react/beatmap-discussions/main.coffee
@@ -136,7 +136,7 @@
       params.last_updated = moment(@state.beatmapsetDiscussion.updated_at).unix()

     if @state.beatmapsetDiscussion.beatmapset_events?
-      params.last_updated = _.max [params?.last_updated, moment(_.last(@state.beatmapsetDiscussion.beatmapset_events).created_at).unix()]
+      params.last_updated = _.max [params?.last_updated, moment(_.last(@state.beatmapsetDiscussion.beatmapset_events)?.created_at).unix()]

     @checkNewAjax = $.get laroute.route('beatmapsets.discussion', beatmapset: @state.beatmapset.id), params
     .done (data, _textStatus, xhr) =>


rewrite to move stuff into react state

rewrite to move stuff into react state

--- resources/assets/coffee/react/beatmap-discussions/post.coffee
+++ resources/assets/coffee/react/beatmap-discussions/post.coffee
@@ -49,6 +49,7 @@

   componentWillUnmount: =>
     @throttledUpdatePost.cancel()
+    clearTimeout @state.permalinkTimer if @state.permalinkTimer?

     for own _id, xhr of @xhr
       xhr?.abort()
@@ -243,7 +244,11 @@
               href: BeatmapDiscussionHelper.hash discussionId: @props.discussion.id
               onClick: @permalink
               className: "#{bn}__action #{bn}__action--button"
-              osu.trans('common.buttons.permalink')
+
+              if @state.permalinkTimer?
+                osu.trans('common.buttons.permalink_copied')
+              else
+                osu.trans('common.buttons.permalink')

           if @props.canBeEdited
             button
@@ -287,19 +292,20 @@
                 'data-confirm': osu.trans('common.confirmation')
                 osu.trans('beatmaps.discussions.allow_kudosu')

+  clearPermalinkClicked: =>
+    @setState permalinkTimer: null
+

   permalink: (e) =>
     e.preventDefault()
-    targetEl = e.currentTarget

     # copy url to clipboard
-    clipboard.writeText targetEl.href
+    clipboard.writeText e.currentTarget.href

     # show feedback
-    currentLabel = targetEl.innerHTML
-    targetEl.innerHTML = osu.trans('common.buttons.permalink_copied')
-    Timeout.set 2000, ->
-      targetEl.innerHTML = currentLabel
+    permalinkTmer = Timeout.set 2000, @clearPermalinkClicked
+
+    @setState permalinkTimer: permalinkTmer

   setMessage: (e) =>


Use window instead of this

Use window instead of this

--- src/frontend/js/message.coffee
+++ src/frontend/js/message.coffee
@@ -1,13 +1,13 @@
-@getID = ->
+window.getID = ->
   $(".active .id").val()

-@getMessage = ->
+window.getMessage = ->
   $(".active .message").val()

-@setID = (value) ->
+window.setID = (value) ->
   $(".active .id").val value

-@setMessage = (value) ->
+window.setMessage = (value) ->
   $(".active .message").val value

 @select = (tab) ->


Fixed typo

Fixed typo

--- src/world.coffee
+++ src/world.coffee
@@ -87,7 +87,7 @@
     @seed = seed
     @generator = new webglmc.WorldGenerator this
     @chunkSize = CHUNK_SIZE
-    @vboBuildTheshold = +webglmc.getRuntimeParameter 'vboBuildTheshold', 40
+    @vboBuildThreshold = +webglmc.getRuntimeParameter 'vboBuildThreshold', 40
     @chunks = {}
     @cachedVBOs = {}
     @dirtyVBOs = {}
@@ -205,7 +205,7 @@
     if !chunk
       return null
     vbo = @cachedVBOs[key]
-    if (!vbo || @dirtyVBOs[key]) && this.generatingChunks < @vboBuildTheshold
+    if (!vbo || @dirtyVBOs[key]) && this.generatingChunks < @vboBuildThreshold
       vbo.destroy() if vbo
       vbo = this.updateVBO x, y, z
       delete @dirtyVBOs[key]


Password recover with email hint

Password recover with email hint

--- app/controllers/recover.coffee
+++ app/controllers/recover.coffee
@@ -29,8 +29,8 @@
       data =
         username: username
       Ember.$.post recoverUrl, data
-        .then ->
-          Notify.success "Pasword reset is successfully requested."
+        .then (data, status, xhr)->
+          Notify.success data.message, closeAfter: 7000
         .fail (xhr, message, status) ->
           Notify.error xhr.responseJSON.message


Remove broken code

Remove broken code

--- app/core/mixins/Movable.coffee
+++ app/core/mixins/Movable.coffee
@@ -39,31 +39,6 @@

   _movable_update: ->
-    checkPosition = false
-
-    # TODO not all entities need the following block.. actually the fewest entities need this
-    # so move it into a subclass to increase performance
-    # TODO: this breaks stuff, checkposition is true when the yeti won't move for 2 seconds, what if he's not supposed to move?
-    # TODO: move task management to own mixin
-
-    # get current task
-    task = @tasks[0]
-
-    if task
-      # remove task if finished
-      @tasks.shift() if task.apply @
-
-    else if @onFollow
-      @counter +=1
-      if @counter % 10 == 0
-        @counter = 1
-        if Date.now() - @positionCheckTimer > 2000
-          @checkPosition = true
-          @positionCheckTimer == Date.now()
-          # TODO the fewest entities need this block
-          @oldPosition = _.clone(@position) # if checkPosition
-      @moveToPosition(@positionToMoveTo, @maxDistance)
-

   # TODO maybe add velocity to define the speed of movement


Delete obsolete modeling source title.

Delete obsolete modeling source title.

--- app/javascripts/filters.coffee
+++ app/javascripts/filters.coffee
@@ -138,26 +138,7 @@

     filters.filter 'multiVolume', ->
       (sequences) ->
-        [seq for seq in sequences when seq.volumes.count > 1]
-
-
-    filters.filter 'modelingSourceTitle', ->
-      # Formats the given modelable display title as follows:
-      # * If the modelable parent is a scan, then the title is the
-      #   capitalized scan type and orientation, e.g. 'T1 axial'.
-      # * Otherwise, the modelable is a registration configuration
-      #   with REST class 'Registration.Configuration', manifested
-      #   by the _cls variable value 'Configuration', and the title
-      #   is 'Registration' followed by the registration key, e.g.
-      #  'Registration pY3x'.
-      (modelable) ->
-        if modelable._cls is 'ScanSet'
-          "#{ _s.capitalize(modelable.scanType) } Scan"
-        else if modelable._cls is 'Configuration'
-          "Registration #{ modelable.key }"
-        else
-          throw new ValueError("Modelable class is unrecognized:" +
-                               " #{ modelable._cls }")
+        (seq for seq in sequences when seq.volumes.length > 1)

     filters.filter 'imageContainerTitle', ['Image', (Image) ->


Update bio text, requested in #757

Update bio text, requested in #757

--- app/pages/about/team-page.cjsx
+++ app/pages/about/team-page.cjsx
@@ -122,7 +122,7 @@
         as well as developing advanced tools for the volunteers.'''
       lucyFortson:
         title: 'Zooniverse Director'
-        bio: '''Associate Professor of Physics and Astronomy at the University of Minnesota. One of the Zooniverse co-founders, she helps with project wrangling (aka management) and strategic planning while trying to squeeze a bit of galaxy evolution science in.'''
+        bio: '''Associate Professor of Physics and Astronomy at the University of Minnesota. One of the University of Minnesota Zooniverse co-founders, she helps with project wrangling (aka management) and strategic planning while trying to squeeze a bit of galaxy evolution science in.'''
       martenVeldthuis:
         title: 'Developer'
         bio: '''Marten joined the development team at the Zooniverse in 2015. He has a degree in


added comments and cleaned up settings controller

--- public/dreamcatcher/controllers/settings_controller.coffee
+++ public/dreamcatcher/controllers/settings_controller.coffee
@@ -3,7 +3,7 @@
   init: ->
     @setupDefaults()
     @setupAjaxBinding()
-
+
   setupDefaults: ->
     # GATHER THE IN PAGE DEFAULTS
     defaultLandingPage = $('#default-landingPage').data 'id'
@@ -14,14 +14,18 @@
     $('#default-landingPage-list').val(defaultLandingPage)
     $('#default-menuStyle-list').val(defaultMenuStyle)

-
     @displayDefaultLandingPage defaultLandingPage

-
+
   showPanel: ->
     $('#settingsPanel').show()
+
+
+  updateSettingsModel: (params) ->
+    Dreamcatcher.Models.Settings.update params

-
+
+  # DEFAULT SETTINGS DISPLAY METHODS
   displayDefaultLandingPage: (defaultLandingPage) ->
     landingIcon = $('#default-landingPage-icon')
     landingIcon.removeClass(className) for className in ['stream','home','today']
@@ -36,9 +40,29 @@
     pageBody = $('#body')
     pageBody.removeClass(className) for className in ['fontSmall','fontMedium','fontLarge']
     pageBody.addClass(defaultFontSize)
+

-  updateSettingsModel: (params) ->
-    Dreamcatcher.Models.Settings.update params
+  # ON CLICK AND CHANGE METHODS
+
+  '#default-landingPage-list change': (element) ->
+    defaultLandingPage = element.val()
+    @displayDefaultLandingPage defaultLandingPage
+    @updateSettingsModel {'user[default_landing_page]': defaultLandingPage}
+
+  '#default-menuStyle-list change': (element) ->
+    defaultMenuStyle = element.val()
+    @displayDefaultMenuStyle defaultMenuStyle
+    @updateSettingsModel {'user[default_menu_style]': defaultMenuStyle}
+
+  '#default-fontSize .fontSize click': (element) ->
+    defaultFontSize = element.attr("id")
+    @displayDefaultFontSize defaultFontSize
+    @updateSettingsModel {'user[default_font_size]': defaultFontSize}
+
+  '.cancel click': ->
+    $('.changePasswordForm').hide()
+    $('#user_password,#user_password_confirmation').val ''
+

   setupAjaxBinding: ->
     # TODO: Needs refactoring.
@@ -60,26 +84,4 @@
         $('#user_old_password, #user_password, #user_password_confirmation').val ''

     $('form#change_password').bind 'ajax:error', (xhr, status, error)->
-      log xhr.errors
-
-
-  # ON CLICK AND CHANGE METHODS
-
-  '#default-landingPage-list change': (element) ->
-    defaultLandingPage = element.val()
-    @displayDefaultLandingPage defaultLandingPage
-    @updateSettingsModel {'user[default_landing_page]': defaultLandingPage}
-
-  '#default-menuStyle-list change': (element) ->
-    defaultMenuStyle = element.val()
-    @displayDefaultMenuStyle defaultMenuStyle
-    @updateSettingsModel {'user[default_menu_style]': defaultMenuStyle}
-
-  '#default-fontSize .fontSize click': (element) ->
-    defaultFontSize = element.attr("id")
-    @displayDefaultFontSize defaultFontSize
-    @updateSettingsModel {'user[default_font_size]': defaultFontSize}
-
-  '.cancel click': ->
-    $('.changePasswordForm').hide()
-    $('#user_password,#user_password_confirmation').val ''
+      log xhr.errors


prelim tutorial

prelim tutorial

--- app/tutorials/spacewarp.coffee
+++ app/tutorials/spacewarp.coffee
@@ -2,9 +2,31 @@

 module.exports = new zootorial.Tutorial
   id: "spacewarp_dashboard_tutorial"
-  steps: [
-    new Step
-      header: 'Welcome'
-      details: "Let's learn about the SpaceWarps Dashboard."
-      nextButton: 'Start Tutorial'
-  ]
+  firstStep: 'welcome'
+  steps:
+    welcome: new Step
+              header: 'Welcome'
+              details: "Let's learn about the SpaceWarps Dashboard."
+              nextButton: 'Start Tutorial'
+              next: 'viewer'
+    viewer: new Step
+              header: "Space Warps Viewer"
+              details: "The Space Warps viewer is used to examine raw astronomical images. Right of the image are various tools to expose different features in the image."
+              next: 'alpha'
+    alpha: new Step
+              header: "Parameters (alpha)"
+              details: "Adjust this slider to raise or lower the overall brightness of the image."
+              attachment: 'right center input[name="alpha"] -0.2 0.30'
+              className: 'arrow-right'
+              next: 'Q'
+    Q: new Step
+              header: "Parameters (Q)"
+              details: "Adjust this slider to raise or lower only the saturated objects such as bright stars."
+              attachment: 'right center input[name="Q"] -0.2 0.30'
+              className: 'arrow-right'
+              next: 'rgb'
+    rgb: new Step
+              header: "Parameters (RGB)"
+              details: "Adjust these sliders to adjust the red, green, and blue channels."
+              attachment: 'right center input.green -0.2 0.30'
+              className: 'arrow-right'


fixing pseudotype conversion for R_JSON in the js driver

--- drivers/javascript/util.coffee
+++ drivers/javascript/util.coffee
@@ -40,10 +40,46 @@
         arr[i] = value
     return arr.buffer

+convertPseudotype = (obj, opts) ->
+    # An R_OBJECT may be a regular object or a "pseudo-type" so we need a
+    # second layer of type switching here on the obfuscated field "$reql_type$"
+    switch obj['$reql_type$']
+        when 'TIME'
+            switch opts.timeFormat
+                # Default is native
+                when 'native', undefined
+                    if not obj['epoch_time']?
+                        throw new err.RqlDriverError "pseudo-type TIME #{obj} object missing expected field 'epoch_time'."
+
+                    # We ignore the timezone field of the pseudo-type TIME object. JS dates do not support timezones.
+                    # By converting to a native date object we are intentionally throwing out timezone information.
+
+                    # field "epoch_time" is in seconds but the Date constructor expects milliseconds
+                    (new Date(obj['epoch_time']*1000))
+                when 'raw'
+                    # Just return the raw (`{'$reql_type$'...}`) object
+                    obj
+                else
+                    throw new err.RqlDriverError "Unknown timeFormat run option #{opts.timeFormat}."
+        else
+            # Regular object or unknown pseudo type
+            obj
+
+recursivelyConvertPseudotype = (obj, opts) ->
+    if obj instanceof Array
+        for value, i in obj
+            obj[i] = recursivelyConvertPseudotype(value, opts)
+    else if obj instanceof Object
+        for key, value of obj
+            obj[key] = recursivelyConvertPseudotype(value, opts)
+        obj = convertPseudotype(obj, opts)
+    obj
+
 deconstructDatum = (datum, opts) ->
     pb.DatumTypeSwitch(datum, {
         "R_JSON": =>
-            JSON.parse(datum.r_str)
+            obj = JSON.parse(datum.r_str)
+            recursivelyConvertPseudotype(obj, opts)
        ,"R_NULL": =>
             null
        ,"R_BOOL": =>
@@ -59,29 +95,7 @@
             for pair in datum.r_object
                 obj[pair.key] = deconstructDatum(pair.val, opts)

-            # An R_OBJECT may be a regular object or a "pseudo-type" so we need a
-            # second layer of type switching here on the obfuscated field "$reql_type$"
-            switch obj['$reql_type$']
-                when 'TIME'
-                    switch opts.timeFormat
-                        # Default is native
-                        when 'native', undefined
-                            if not obj['epoch_time']?
-                                throw new err.RqlDriverError "pseudo-type TIME #{obj} object missing expected field 'epoch_time'."
-
-                            # We ignore the timezone field of the pseudo-type TIME object. JS dates do not support timezones.
-                            # By converting to a native date object we are intentionally throwing out timezone information.
-
-                            # field "epoch_time" is in seconds but the Date constructor expects milliseconds
-                            (new Date(obj['epoch_time']*1000))
-                        when 'raw'
-                            # Just return the raw (`{'$reql_type$'...}`) object
-                            obj
-                        else
-                            throw new err.RqlDriverError "Unknown timeFormat run option #{opts.timeFormat}."
-                else
-                    # Regular object or unknown pseudo type
-                    obj
+            convertPseudotype(obj, opts)
         },
             => throw new err.RqlDriverError "Unknown Datum type"
         )


Add doc for eventric.boundedContext

Add doc for eventric.boundedContext

--- src/index.coffee
+++ src/index.coffee
@@ -46,6 +46,18 @@
     @_params[key]

+  ###*
+  *
+  * @description
+  *
+  * Use as: example = eventric.boundedContext(params)
+  *
+  * Get a new [[BoundedContext]] instance.
+  *
+  * @param {Object}
+  * - `name` Name for the BoundedContext
+  * - `store` An Instance of a eventric StoreAdapter (optional if configured globally)
+  ###
   boundedContext: (name) ->
     if !name
       throw new Error 'BoundedContexts must have a name'


fix typo

fix typo

--- src/index.coffee
+++ src/index.coffee
@@ -1,6 +1,6 @@
 window.Ubret = new Object unless typeof window.Ubret isnt 'undefined'

-Ubret.BaseUrl = if location.port < 1024 and
+Ubret.BaseUrl = if location.port < 1024
   "http://ubret.s3.amazonaws.com/ubret_library/lib/"
 else
   "http://localhost:3001/"


Changed quit to ctrl-C instead of Q

Changed quit to ctrl-C instead of Q

--- src/index.coffee
+++ src/index.coffee
@@ -34,7 +34,8 @@

 		@timeManager.targets.push @entities...

-		@events.on 'key.q', => @quit()
+		@events.on 'key.c', (ch, key) =>
+			@quit() if key.ctrl

 	quit: ->
 		deinitialize @


Better scoring

Better scoring

--- src/index.coffee
+++ src/index.coffee
@@ -21,8 +21,8 @@
       followingHunters: 0
       followingUpvotes: 0
       followingComments: 0
-      totalUpvotes: 0
-      totalComments: 0
+      totalUpvotes: post.votes_count
+      totalComments: post.comments_count
     client.evaluate agentID, inputs, (err, outputs) ->
       if err
         console.error err
@@ -34,7 +34,7 @@
     if err
       callback err
     else
-      scored = _.sortBy scored, "score"
+      scored = _.sortByOrder scored, ["score"], [false]
       callback null, scored

 defaultAgent =


Check for response

Check for response

--- src/index.coffee
+++ src/index.coffee
@@ -21,10 +21,10 @@
 TwitterTags.fetch = (url) ->
   deferred = Q.defer()
   request url, (error, response, data) ->
-    if not error and response.statusCode is 200
+    if not error and response?.statusCode is 200
       deferred.resolve TwitterTags.parseHtml(data)
     else
-      deferred.reject error: error
+      deferred.reject({error})

   deferred.promise


remove overly verbose comment

remove overly verbose comment

--- src/index.coffee
+++ src/index.coffee
@@ -10,17 +10,7 @@
     if global.Error.captureStackTrace
       global.Error.captureStackTrace(@, arguments.callee)

-  util.inherits(error, inherit)
-  # No need to set the prototype because:
-  #
-  # 1) We've got util.inherits to handle prototype already
-  # 2) assigning prototype value directly will cause the instanceof operator
-  #    returing wrong values.
-  #
-  # See this gist for more example:
-  # https://gist.github.com/dotnil/8381942
-  #
-  # error:: = inherit::
+  util.inherits error, inherit
   error

 Liquid.Error = customError "Error"


honestly this is getting ridiculous

honestly this is getting ridiculous

--- src/index.coffee
+++ src/index.coffee
@@ -59,8 +59,7 @@
         io.of('/teacher').emit 'render', onlineData()

     socket.on 'edit', (data) ->
-        #socket.emit 'online', onlineData()
-        io.of("/teacher").emit 'render', onlineData()
+        io.of('/teacher').emit 'render', data

     socket.on 'disconnect', (data) ->
         online = onlineData()


Ticker: decrease limit to 10 from 15

Ticker: decrease limit to 10 from 15

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

     options      =
       # do not fetch more than 15 at once
-      limit      : Math.min options.limit ? 15, 15
+      limit      : 10 # Math.min options.limit ? 15, 15
       sort       : timestamp  : -1

     Relationship.some selector, options, (err, relationships) ->


Re-render the metadata contents when the page fully loads

--- src/scripts/modules/media/footer/metadata/metadata.coffee
+++ src/scripts/modules/media/footer/metadata/metadata.coffee
@@ -8,4 +8,4 @@

     initialize: () ->
       super()
-      @listenTo(@model, 'changePage', @render)
+      @listenTo(@model, 'changePage:content', @render)


Optimized iterations

Optimized iterations

--- src/lib/iteration_utils.coffee
+++ src/lib/iteration_utils.coffee
@@ -6,11 +6,8 @@
   Dependencies: Backbone.js and Underscore.js.
 ###

-_ = require 'underscore'
-Queue = require './queue'
-
 # @nodoc
-nextTick = process?.nextTick or _.defer
+nextTick = process?.nextTick or (require 'underscore').defer

 module.exports = class IterationUtils
   @MAX_ITERATION_COUNT: 300


need to return false after groupby to prevent form submission

--- subtree/bokehjs/static/coffee/pandas/pandas.coffee
+++ subtree/bokehjs/static/coffee/pandas/pandas.coffee
@@ -124,7 +124,9 @@
         group : @fromcsv(@$el.find(".pandasgroup").val())
         offset : 0
       )
-     @model.save()
+      @model.save()
+      return false
+

   counts : () ->
     @mget('tabledata').data._counts


feat(funnel): group notifications by type

--- scripts/gitter-activity-notifier.coffee
+++ scripts/gitter-activity-notifier.coffee
@@ -10,7 +10,13 @@
     gitter = new Gitter(process.env.HUBOT_GITTER2_TOKEN)
     store = {}
     timeoutHandle = null
-    timeoutDuration = 3000
+    timeoutDuration = 4000
+
+    primg = '![](https://goo.gl/hjqlaA)  '
+    commentimg = '![](https://goo.gl/8IdEJl)  '
+    infoimg = '![](https://goo.gl/9bHFwO)  '
+    pushimg = '![](https://goo.gl/UHCtHx)  '
+

     for roomName in roomNames
         #console.log(roomName)
@@ -37,7 +43,12 @@

             console.log('room id', room.name, room.id)

-            store[room.id] = []
+            store[room.id] = {
+                comment: {}
+                issue: {}
+                pr: {}
+                commit: {}
+            }

             events.on('snapshot', (snapshot) ->
                 console.log(snapshot.length + ' messages in the snapshot');
@@ -50,6 +61,15 @@
             )
         )

+    # help!
+    helper = (items, id, prop, value, parts) ->
+
+        if items[id] and items[id][prop].indexOf(value) == -1
+            items[id][prop] += ',' + value
+        # add new notification
+        else
+            items[id] = parts
+
     parseMessage = (roomid, text) ->
         message = ''

@@ -58,7 +78,7 @@
         comment = /\[Github\] (\w[\w-]+) commented in (.+?\/.+?) on issue: (.*?) http.*?\/(\d+)#.*/
         # name:1; reponame:2; issuename:3; issueid:4

-        issue = /\[Github\] (\w[\w-]+) (closed|opened|reopened|assigned|unassigned|labeled) an issue in (.+?\/.+?): (.*?) http.*?\/(\d+)/
+        issue = /\[Github\] (\w[\w-]+) (closed|opened|reopened|assigned|unassigned|labeled|unlabeled) an issue in (.+?\/.+?): (.*?) http.*?\/(\d+)/
         # name:1; action:2; reponame:3; issuename:4; issueid:5

         pr = /\[Github\] (\w[\w-]+) (opened|closed|reopened|synchronize) a Pull Request to (.+?\/.+?): (.*?) http.*?\/(\d+)/
@@ -74,53 +94,118 @@
         pushimg = '![](https://cdn0.iconfinder.com/data/icons/octicons/1024/repo-push-16.png)  '
         ###

-        primg = '![](https://goo.gl/hjqlaA)  '
-        commentimg = '![](https://goo.gl/8IdEJl)  '
-        infoimg = '![](https://goo.gl/9bHFwO)  '
-        pushimg = '![](https://goo.gl/UHCtHx)  '
-
-
         if comment.test text
             m = text.match comment
             m.shift(1)
             console.log(m)
-            #message = vsprintf('@%1$s commented in [%2$s](https://github.com/%2$s/) on issue %2$s#%4$s', m)
-            message = commentimg + vsprintf('%1$s commented on issue %2$s#%4$s', m)
+
+            helper(store[roomid].comment, m[3], 0, m[0], m)
+
+            ###
+            # if notification exists, add new name
+            if comment[id] and comment[id][0].indexOf(m[0]) == -1
+                comment[id][0] += ',' + m[0]
+            # add new notification
+            else
+                issue[id] = m
+            ###
+
         else if issue.test text
             m = text.match issue
             m.shift(1)
             console.log(m)
-            #message = vsprintf('@%1$s %2$s an issue in [%3$s](https://github.com/%3$s/): %3$s#%5$s', m)
-            message = infoimg + vsprintf('%1$s %2$s an issue: %3$s#%5$s', m)
+
+            helper(store[roomid].issue, m[0] + m[4], 1, m[1], m)
+
+            ###
+            # if notification exists, add new action
+            if issue[id] and issue[id][1].indexOf(m[1]) == -1
+                issue[id][1] += ',' + m[1]
+            # add new notification
+            else
+                issue[id] = m
+            ###
+
         else if pr.test text
             m = text.match pr
             m.shift(1)
             console.log(m)
+
+            helper(store[roomid].issue, m[0] + m[4], 1, m[1], m)
+
             #message = vsprintf('@%1$s %2$s a Pull Request to [%3$s](https://github.com/%3$s/): %3$s#%5$s', m)
-            message = primg + vsprintf('%1$s %2$s a Pull Request: %3$s#%5$s; [Reviewable %5$s](https://reviewable.io/reviews/%3$s/%5$s)', m)
+            #message = primg + vsprintf('%1$s %2$s a Pull Request: %3$s#%5$s; [Reviewable %5$s](https://reviewable.io/reviews/%3$s/%5$s)', m)
         else if commit.test text
             m = text.match commit
             m.shift(1)
             console.log(m)
-            message = pushimg + vsprintf('%1$s pushed %2$s commit(s) to [%3$s](https://github.com/%3$s/): [\[compare\]](%4$s)', m)
+
+            items[m[0] + m[1] + m[2]] = parts
+
+            #message = pushimg + vsprintf('%1$s pushed %2$s commit(s) to [%3$s](https://github.com/%3$s/): [\[compare\]](%4$s)', m)

         console.log(' -->', message)

         # store message and wait in case new messages come soon
-        if message != ''
-           store[roomid].push message
-           timeoutHandle = setTimeout repostMessage, timeoutDuration, roomid
-           console.log(' ---> timeout handle', timeoutHandle)
+        #if message != ''
+           #store[roomid].push message
+        timeoutHandle = setTimeout repostMessage, timeoutDuration, roomid
+        console.log(' ---> timeout handle', timeoutHandle)
+
+    # add 'and' between workd is needed
+    andify = (string) ->
+        ar = string.split(',')
+        joint = ', '
+
+        if ar.length == 1
+            return string
+        else if ar.length == 2
+            joint = ' '
+
+        ar.push('and ' + ar.pop())
+
+        return ar.join(joint)

     # when timeout resolves, repost all captured messages
     repostMessage = (roomid) ->
+        messages = []

         console.log(' ---> time out; reposting', roomid)

+        # construct issue messages
+        for commentid, parts of store[roomid].comment
+            console.log(commentid, parts)
+            parts[0] = andify parts[0]
+            messages.push(commentimg + vsprintf('%1$s commented on issue %2$s#%4$s', parts))
+
+        # construct comment messages
+        for issueid, parts of store[roomid].issue
+            console.log(issueid, parts)
+            parts[1] = andify parts[1]
+            messages.push(infoimg + vsprintf('%1$s %2$s an issue: %3$s#%5$s', parts))
+
+        for prid, parts of store[roomid].pr
+            console.log(prid, parts)
+            parts[1] = andify parts[1]
+            messages.push(primg + vsprintf('%1$s %2$s a Pull Request: %3$s#%5$s; [Reviewable %5$s](https://reviewable.io/reviews/%3$s/%5$s)', parts))
+
+        for commitid, parts of store[roomid].commit
+            console.log(commitid, parts)
+            messages.push(pushimg + vsprintf('%1$s pushed %2$s commit(s) to [%3$s](https://github.com/%3$s/): [\[compare\]](%4$s)', parts))
+
+        # join different types of messages
         robot.send
             room: roomid
-            store[roomid].join '\n'
-
-        store[roomid] = []
+            messages.join('\n')
+
+        console.log store[roomid]
+
+        # clear store
+        store[roomid] = {
+            comment: {}
+            issue: {}
+            pr: {}
+            commit: {}
+        }

     return


Immediately set images when on a touch device

--- apps/about2/client/view.coffee
+++ apps/about2/client/view.coffee
@@ -143,14 +143,18 @@
       $(this).removeClass 'is-active' if direction is 'up'
     , offset: '90%'

-
   setImages: ->
-    @$('img').waypoint ->
-      $img = $(this)
+    setImage = (img) ->
+      $img = $(img)
       src = $img.data 'src'
       $parent = $img.parent()
       $parent.imagesLoaded -> $.waypoints('refresh')
       width = $parent.width()
       src = if isRetina() then src else resize(src, width: width)
       $img.attr 'src', src
-    , triggerOnce: true, offset: '150%'
+    if isTouchDevice()
+      @$('img').each -> setImage this
+    else
+      @$('img').waypoint ->
+        setImage this
+      , triggerOnce: true, offset: '150%'


fixes test

fixes test

--- apps/artist_2/test/meta.coffee
+++ apps/artist_2/test/meta.coffee
@@ -17,7 +17,7 @@

   describe 'basic artist with name and short blurb', ->
     beforeEach ->
-      @artist = _.pick artistJSON, '_id', 'id', 'name', 'gender', 'blurb', 'stastuses', 'counts'
+      @artist = _.pick artistJSON, '_id', 'id', 'name', 'gender', 'blurb', 'stastuses', 'counts', 'meta'
       @html = jade.render fs.readFileSync(@file).toString(),
         artist: @artist
         sd: @sd
@@ -32,8 +32,8 @@
       @html.should.containEql "<meta property=\"twitter:card\" content=\"summary"
       @html.should.containEql "<link rel=\"canonical\" href=\"http://localhost:5000/artist/jeff-koons-1"
       @html.should.containEql "<meta property=\"og:url\" content=\"http://localhost:5000/artist/jeff-koons-1"
-      @html.should.containEql "<meta property=\"og:description\" content=\"Welcome to the official Jeff Koons page on Artsy. Jeff Koons plays with ideas of taste, pleasure, celebrity, and commerce. “I believe in advertisement and media completely” ..."
-      @html.should.containEql "<meta property=\"og:title\" content=\"Jeff Koons - 185 Artworks, Bio"
+      @html.should.containEql "<meta property=\"og:description\" content=\"Welcome to the official Jeff Koons page on Artsy. Jeff Koons plays with ideas of taste, pleasure, celebrity, and commerce. “I believe in advertisement and media completely”"
+      @html.should.containEql "<meta property=\"og:title\" content=\"Jeff Koons - 100+ Artworks, Bio &amp; Shows on Artsy"
       @html.should.containEql "<link rel=\"next\" href=\"http://localhost:5000/artist/jeff-koons-1/shows\""
       @html.should.containEql "<link rel=\"prev\" href=\"http://localhost:5000/artist/jeff-koons-1/works\""


Make the markdown a bit more customizable with h1, h2, and div

--- src/desktop/models/page.coffee
+++ src/desktop/models/page.coffee
@@ -11,5 +11,13 @@
   urlRoot: "#{sd.API_URL}/api/v1/page"

   sanitizedHtml: (attr) ->
-    clean = insane(@get(attr))
+    clean = insane(@get(attr), {
+      allowedAttributes: {
+        a: ["href", "name", "target"],
+        img: ["src"],
+        h1: ['style'],
+        h2: ['style'],
+        div: ['style']
+      }
+    })
     Markdown.mdToHtml.apply { get: -> clean }, [null, sanitize: false]


Optimized Creature handling.

Optimized Creature handling.

--- packages/artificialengines/spectrum/animatedmesh.coffee
+++ packages/artificialengines/spectrum/animatedmesh.coffee
@@ -2,110 +2,36 @@
 AS = Artificial.Spectrum

 class AS.AnimatedMesh extends AS.RenderObject
-  @_dataCache = {}
-
-  @getCachedDataField: (dataUrl) ->
+  @_dataEntryCache = {}
+
+  @getCachedDataEntryField: (dataUrl) ->
     # Return the singleton field if we already have it.
-    return @_dataCache[dataUrl].source if @_dataCache[dataUrl]
+    return @_dataEntryCache[dataUrl] if @_dataEntryCache[dataUrl]

     # Create a new field.
-    @_dataCache[dataUrl] =
-      source: new ReactiveField null
-      boneCorrected: []
+    @_dataEntryCache[dataUrl] = new ReactiveField null

     # Start loading its content.
     $.getJSON dataUrl, (data) =>
-      @_dataCache[dataUrl].source data
+      animations = {}
+
+      for name, animationData of data.animation
+        animations[name] = new AS.CreatureAnimation data, name
+
+      @_dataEntryCache[dataUrl] {data, animations}

     # Return the new field.
-    @_dataCache[dataUrl].source
-
-  @getCachedDataWithBoneCorrections: (dataUrl, boneCorrections) ->
-    # Return the singleton field if we already have it.
-    if dataCache = @_dataCache[dataUrl]
-      boneCorrectedEntry = _.find dataCache.boneCorrected, (boneCorrectedEntry) => _.isEqual boneCorrectedEntry.boneCorrections, boneCorrections
-
-      if boneCorrectedEntry
-        return boneCorrectedEntry.data()
-
-    else
-      # Ensure we have the source data field.
-      @getCachedDataField dataUrl
-
-    boneCorrectedData = Tracker.nonreactive =>
-      new ComputedField =>
-        return unless dataField = @getCachedDataField dataUrl
-
-        @_applyBoneCorrections dataField(), boneCorrections
-      ,
-        true
-
-    @_dataCache[dataUrl].boneCorrected.push
-      data: boneCorrectedData
-      boneCorrections: boneCorrections
-
-    boneCorrectedData()
-
-  @_applyBoneCorrections: (data, boneCorrections) ->
-    data = _.clone data
-    data.animation = _.clone data.animation
-
-    # See which bones will need offset.
-    bonesThatNeedOffset = {}
-
-    addBonesThatNeedOffset = (bone) =>
-      return if bonesThatNeedOffset[bone.name]
-      bonesThatNeedOffset[bone.name] = true
-
-      for childId in bone.children
-        childBone = _.find data.skeleton, (bone) => bone.id is childId
-        addBonesThatNeedOffset childBone
-
-    addBonesThatNeedOffset data.skeleton[boneName] for boneName of boneCorrections
-
-    for animationName, animation of data.animation
-      data.animation[animationName] = _.clone animation
-      bones = _.clone animation.bones
-      data.animation[animationName].bones = bones
-
-      for frameNumber, frame of bones
-        bones[frameNumber] = _.clone frame
-
-        # Only clone bones that will be offset.
-        for boneName of bonesThatNeedOffset
-          bones[frameNumber][boneName] = _.clone frame[boneName]
-
-    for boneName, correction of boneCorrections
-      unless data.skeleton[boneName]
-        console.warn "Bone #{boneName} to be corrected does not exist in the skeleton."
-        continue
-
-      for animationName, animation of data.animation
-        for frameNumber, frame of animation.bones
-          # Offset bone and all children. We do this in world space to avoid computation of proper correction in
-          # bone space which would require calculating all hierarchy matrices per frame. For non-extreme animations
-          # (that don't go too far away from the rest pose) this is good enough.
-          offsetBone = (bone) =>
-            frame[bone.name].start_pt = [frame[bone.name].start_pt[0] + correction.x, frame[bone.name].start_pt[1] - correction.y]
-            frame[bone.name].end_pt = [frame[bone.name].end_pt[0] + correction.x, frame[bone.name].end_pt[1] - correction.y]
-
-            for childId in bone.children
-              childBone = _.find data.skeleton, (bone) => bone.id is childId
-              offsetBone childBone
-
-          offsetBone data.skeleton[boneName]
-
-    data
+    @_dataEntryCache[dataUrl]

   constructor: (@options) ->
     super arguments...

     # Load data if provided with its URLs.
     if @options.dataUrl
-      @data = @constructor.getCachedDataField @options.dataUrl
+      @dataEntry = @constructor.getCachedDataEntryField @options.dataUrl

     else
-      @data = new ReactiveField @options.data
+      @dataEntry = new ReactiveField @options.data

     @boneCorrections = new ReactiveField @options.boneCorrections

@@ -145,41 +71,41 @@

       @materials.updated()

-    # Prepare animation data.
-    @correctedData = new ComputedField =>
-      return unless data = @data()
-
+    # Create creature objects.
+    @creature = new ComputedField =>
+      return unless dataEntry = @dataEntry()
+
+      new AS.Creature dataEntry.data
+    ,
+      true
+
+    @creatureManager = new ComputedField =>
       boneCorrections = @boneCorrections()
       return if @options.waitForBoneCorrections and not boneCorrections

-      if boneCorrections
-        if @options.dataUrl
-          data = @constructor.getCachedDataWithBoneCorrections @options.dataUrl, boneCorrections
-
-        else
-          data = @constructor._applyBoneCorrections data, boneCorrections
-
-      data
-
-    # Create creature objects.
-    @creature = new ComputedField =>
-      return unless data = @correctedData()
-
-      new AS.Creature data
-    ,
-      true
-
-    @creatureManager = new ComputedField =>
       return unless creature = @creature()
-      data = @correctedData()
+      dataEntry = @dataEntry()

       creatureManager = new AS.CreatureManager creature
       @_autoBlendSet = false

       creatureManager.SetTimeScale @options.dataFPS

-      for name, animationData of data.animation
-        creatureManager.AddAnimation new AS.CreatureAnimation data, name
+      for name, animation of dataEntry.animations
+        creatureManager.AddAnimation animation
+
+      if boneCorrections and _.keys(boneCorrections).length
+        creatureManager.bones_override_callback = (bonesMap) =>
+          for boneName, correction of boneCorrections
+            unless bone = bonesMap[boneName]
+              unless @_missingBoneWarned?[boneName]
+                console.warn "Bone #{boneName} to be corrected does not exist in the skeleton."
+                @_missingBoneWarned ?= {}
+                @_missingBoneWarned[boneName] = true
+
+              continue
+
+            @_applyBoneCorrection bone, correction

       creatureManager
     ,
@@ -249,8 +175,17 @@
     @creatureManager.stop()
     @creatureRenderer.stop()

+  _applyBoneCorrection: (bone, correction) ->
+    bone.world_start_pt[0] += correction.x
+    bone.world_start_pt[1] -= correction.y
+    bone.world_end_pt[0] += correction.x
+    bone.world_end_pt[1] -= correction.y
+
+    for child in bone.children
+      @_applyBoneCorrection child, correction
+
   animationNames: ->
-    _.keys @data()?.animation
+    _.keys @dataEntry()?.animation

   update: (appTime, updateData = true) ->
     @_accumulatedTime += appTime.elapsedAppTime


A couple more tokenizer specs

A couple more tokenizer specs

--- test/html_to_tokens.spec.coffee
+++ test/html_to_tokens.spec.coffee
@@ -18,3 +18,10 @@

     it 'should return 11', ->
       (expect @res.length).to.equal 11
+
+  it 'should identify contiguous whitespace as a single token', ->
+    (expect @cut 'a   b').to.eql ['a', '   ', 'b']
+
+  it 'should identify self closing tags as tokens', ->
+    (expect @cut '
hello
goodbye
')
+    .eql ['
', 'hello', '
', 'goodbye', '
']


Update expected spy in buffer cached paths spec

--- src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee
+++ src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee
@@ -275,23 +275,23 @@
         expect(finderView.loadPathsTask.start).not.toHaveBeenCalled()

     it "doesn't cache buffer paths", ->
-      spyOn(rootView, "getOpenBufferPaths").andCallThrough()
+      spyOn(rootView.project, "getEditSessions").andCallThrough()
       rootView.trigger 'fuzzy-finder:toggle-buffer-finder'

       waitsFor ->
         finderView.list.children('li').length > 0

       runs ->
-        expect(rootView.getOpenBufferPaths).toHaveBeenCalled()
-        rootView.getOpenBufferPaths.reset()
-        rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
-        rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
-
-      waitsFor ->
-        finderView.list.children('li').length > 0
-
-      runs ->
-        expect(rootView.getOpenBufferPaths).toHaveBeenCalled()
+        expect(rootView.project.getEditSessions).toHaveBeenCalled()
+        rootView.project.getEditSessions.reset()
+        rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
+        rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
+
+      waitsFor ->
+        finderView.list.children('li').length > 0
+
+      runs ->
+        expect(rootView.project.getEditSessions).toHaveBeenCalled()

     it "busts the cache when the window gains focus", ->
       spyOn(LoadPathsTask.prototype, "start").andCallThrough()


tack on tmp _path fix

tack on tmp _path fix

--- lib/index.coffee
+++ lib/index.coffee
@@ -140,7 +140,9 @@
     ###

     set_locals = (types) ->
-      W.map types, (t) => @roots.config.locals.contentful[t.name] = t.content
+      W.map types, (t) =>
+        @roots.config.locals.contentful[t.name] = t.content
+        @roots.config.locals._path = undefined

     ###*
      * Compiles single entry views for content types


Change action context for this.

Change action context for this.

--- lib/index.coffee
+++ lib/index.coffee
@@ -7,7 +7,7 @@
 setupEvent = (eventName, rootSelector, selector, action) ->

 bone.view = (selector, options) ->
-    view = {}
+    view = {}
     events = options.events
     for eventSelector, functionName of events
         continue if functionName is 'events'
@@ -21,7 +21,7 @@
             $ -> $('body').on eventName, fullSelector, (event) ->
                 console.log "Interface: [#{fullSelector}:#{eventName}]", event.currentTarget
                 root = $(fullSelector).parents(selector)[0]
-                action root, event
+                action.call view, root, event

     for name, action of options
         continue if name is 'events'


Remove rotatable

Remove rotatable

--- framer/LayerRotatable.coffee
+++ framer/LayerRotatable.coffee
@@ -1,44 +0,0 @@
-{BaseClass} = require "./BaseClass"
-{Events} = require "./Events"
-{Gestures} = require "./Gestures"
-
-Events.RotateStart = Gestures.RotateStart
-Events.RotateEnd = Gestures.RotateEnd
-Events.Rotate = Gestures.Rotate
-
-class exports.LayerRotatable extends BaseClass
-
-	@define "enabled", @simpleProperty("enabled", true)
-	@define "threshold", @simpleProperty("threshold", 10)
-
-	constructor: (@layer) ->
-		super
-		@layer.gestures.on(Gestures.RotateStart, @_rotateStart)
-		@layer.gestures.on(Gestures.Rotate, @_rotate)
-		@layer.gestures.on(Gestures.RotateEnd, @_rotateEnd)
-
-	_rotateStart: (event) =>
-		@_rotationStart = null
-		@_rotationOffset = null
-		@emit(Events.RotateStart, event)
-
-	_rotate: (event) =>
-
-		return unless event.pointers.length is 2
-
-		pointA =
-			x: event.pointers[0].pageX
-			y: event.pointers[0].pageY
-		pointB =
-			x: event.pointers[1].pageX
-			y: event.pointers[1].pageY
-
-		return unless Utils.pointTotal(Utils.pointAbs(Utils.pointSubtract(pointA, pointB))) > @threshold
-
-		@_rotationStart ?= @layer.rotation
-		@_rotationOffset ?= event.rotation
-		@layer.rotation = event.rotation - @_rotationOffset + @_rotationStart
-		@emit(Events.Rotate, event)
-
-	_rotateEnd: (event) =>
-		@emit(Events.RotateEnd, event)


fix (#1)

fix (#1)

--- lib/parse.coffee
+++ lib/parse.coffee
@@ -60,8 +60,8 @@
 		file.lines.push {
 			type: 'normal'
 			normal: true
-			ln1: ln_del++ unless noeol
-			ln2: ln_add++ unless noeol
+			ln1: ln_del++ unless line is noeol
+			ln2: ln_add++ unless line is noeol
 			content: line
 		}


Use more descriptive prompts

Use more descriptive prompts

--- src/login.coffee
+++ src/login.coffee
@@ -45,7 +45,7 @@
   getUser: (state) =>
     return Q(state) if state.user

-    @prompt({prompt: 'Username>', edit: true})
+    @prompt({prompt: 'GitHub Username or Email>', edit: true})
       .spread (user) ->
         state.user = user
         Q(state)
@@ -77,7 +77,7 @@
       if statusCode is 200
         deferred.resolve(state)
       else if statusCode is 401 and headers['x-github-otp']
-        @prompt({prompt: 'Authentication Code>', edit: true})
+        @prompt({prompt: 'Two-Factor Authentication Code>', edit: true})
           .spread (authCode) ->
             state.authCode = authCode
             deferred.resolve(state)


feature(lorax): Added creating markdown format changelog and shortcut generate function

--- src/lorax.coffee
+++ src/lorax.coffee
@@ -14,15 +14,53 @@

 util   = require "util"
 fs     = require "fs"
-path   = require "path"
-pkg    = require path.join(__dirname, "package.json")
 config = require "./lib/config"
 git    = require "./lib/git"

-GIT_GREP = "" # Should be generated through option passed in
+Config     = new config()
+url        = Config.get "url"
+issueTmpl  = Config.get "issue"
+commitTmpl = Config.get "commit"

-Config = new config()
+###
+@function
+@name linkToIssue
+@description
+Create a markdown link to issue page with issue number as text
+@param {String} issue Issue number
+@returns {String} markdown text
+###
+linkToIssue  = (issue) ->
+  if url and issueTmpl
+    issueLink = "[#%s](#{url}#{issueTmpl})"
+    util.format issueLink, issue, issue
+  else
+    "##{issue}"

+###
+@function
+@name linkToCommit
+@description
+Create a markdown link to commit page with commit hash as text
+@param {String} hash Commit hash
+@returns {String} markdown text
+###
+linkToCommit = (hash) ->
+  if url and commitTmpl
+    commitLink = "[%s](#{url}#{commitTmpl})"
+    util.format commitLink, hash.substr(0, 8), hash
+  else
+    hash.substr(0, 8)
+
+###
+@function
+@name parseCommit
+@description
+Given a string of commits in a special format, parse string and creates an array of
+commit objects with information
+@param {String} commit Commit string
+@returns {Array} Array of commit objects
+###
 parseCommit = (commit) ->
   return unless commit? and commit

@@ -52,4 +90,92 @@

   return commitObj

-lorax = module.exports = {}+###
+@function
+@name write
+@description
+Using preprocessed array of commits, generate a changelog in markdown format with version
+and today's date as the header
+@param {Array} commits Preprocessed array of commits
+@param {String} version Current version
+@returns {String} Markdown format changelog
+###
+write = (commits, version) ->
+  output        = ""
+  sections      = {}
+  display       = Config.get "display"
+  sections[key] = {} for key, name of display
+
+  for commit in commits
+    section = sections[commit.type]
+    section[commit.component] ?= []
+    section[commit.component].push commit
+
+  today   = new Date()
+  output += "# #{version} (#{today.getFullYear()}/#{today.getMonth() + 1}/#{today.getDate()})\n"
+
+  for sectionType, list of sections
+    components = Object.getOwnPropertyNames(list).sort()
+
+    continue unless components.length
+
+    output += "## #{display[sectionType]}\n"
+
+    for componentName in components
+      prefix   = "-"
+
+      if list[componentName].length > 1
+        output += util.format "- **%s:**\n", componentName
+        prefix  = "  -"
+      else
+        prefix = util.format "- **%s:**", componentName
+
+      for item in list[componentName]
+        output += util.format "%s %s\n  (%s", prefix, item.message, linkToCommit(item.hash)
+        if item.issues.length
+          output += ",\n   #{item.issues.map(linkToIssue).join(", ")}"
+        output += ")\n"
+
+    output += "\n"
+
+  output += "\n"
+
+  return output
+
+###
+@function
+@name generate
+@description
+A shortcut function to get the latest tag, parse all the commits and generate the changelog
+@param {String} toTag The latest tag
+@param {String} file Filename to write to
+@param {String} fromTag A specific tag to start from
+###
+generate = (toTag, file, fromTag) ->
+  parseAndWrite = (commits) ->
+    parsedCommits = (parseCommit commit for commit in commits when commit)
+
+    console.log "Parsed #{parsedCommits.length} commit(s)"
+
+    result = write parsedCommits, toTag
+    fs.writeFileSync file, result, encoding: "utf-8"
+
+    console.log "Generated changelog to #{file} (#{toTag})"
+
+  parseFromLastTag = (lastTag) ->
+    tag = fromTag or lastTag
+    if tag
+      console.log "Reading commits since #{tag}"
+    else
+      console.log "Reading all commits"
+
+    grep = Config.get("type").join "|"
+    git.getLog(grep, tag).then parseAndWrite
+
+  git.getLastTag().then parseFromLastTag, -> parseFromLastTag()
+
+lorax = module.exports = {
+  parseCommit
+  write
+  generate
+}


remove callback from synchronous constructor

--- oridomi.coffee
+++ oridomi.coffee
@@ -519,8 +519,6 @@
     @$el = $ @el if $
     # Push this instance into the instances collection.
     instances.push @
-    # If a callback was passed in the constructor options, run it.
-    @_callback @settings
     # End the constructor benchmark if `devMode` is active.
     console.timeEnd 'oridomiConstruction' if devMode


call stageReset() in freeze()

call stageReset() in freeze()

--- oridomi.coffee
+++ oridomi.coffee
@@ -836,7 +836,7 @@
       callback?()
     else
       # Make sure to reset folding first.
-      @reset =>
+      @_stageReset @lastOp.anchor, =>
         @isFrozen = true
         # Swap the visibility of the elements.
         @stageHolder.style[css.transform] = 'translate3d(-9999px, 0, 0)'


Added signature check back.

Added signature check back.

--- lib/saml2.coffee
+++ lib/saml2.coffee
@@ -117,7 +117,6 @@
   sig.keyInfoProvider = getKey: -> format_pem(certificate, 'CERTIFICATE')
   sig.loadSignature signature[0].toString()
   return cb null if sig.checkSignature(xml)
-  return cb null
   cb new Error("SAML Assertion signature check failed!")

 # Takes in an xml @dom containing a SAML Status and returns true if at least one status is Success.