Add regex support to notification center

--- Web/js/classes/notification-center.coffee
+++ Web/js/classes/notification-center.coffee
@@ -9,20 +9,26 @@
 webkitNotifications = window.webkitNotifications or {}
 class window.NotificationCenter
 	constructor: () ->
-		@denied = false;
-		@hasPermission = false;
+		@denied = false
+		@hasPermission = false
+		@regex = null
+		@enabled = false
 		if window.localStorage.getItem 'notifications-enabled'
+			@enabled = true
 			@getPermission()
 			# Just in case we're using an older ver of the speck, ask on the first click as well.
 			clickus = () =>
 				document.removeEventListener 'click', clickus
 				@getPermission()
 			document.addEventListener 'click', clickus
+			# Load the stored notification regex
+			if regex = window.localStorage.getItem 'notifications-regex'
+				try @regex = new RegExp regex, 'gim'

-	getPermission: (cback = null) ->
+
+	getPermission: () ->
 		if @hasPermission or Notification.permission == 'granted' or webkitNotifications.checkPermission?() == 0
 			@hasPermission = true
-			cback?()
 		else if @denied or Notification.permission == 'denied' or webkitNotifications.checkPermission?() == 2
 			@denied = true
 		else
@@ -30,14 +36,12 @@
 				Notification.requestPermission (permission) =>
 					if permission == 'granted'
 						@hasPermission = true
-						cback?()
 					else
 						@denied = true
 			else
 				webkitNotifications.requestPermission () =>
 					if webkitNotifications.checkPermission() == 0
 						@hasPermission = true
-						cback?()
 					else
 						@denied = true

@@ -63,4 +67,20 @@
 	enableNotifications: () ->
 		@getPermission();
 		window.localStorage.setItem 'notifications-enabled', true
+		@enabled = true

+	disableNotifications: () ->
+		window.localStorage.deleteItem 'notifications-enabled'
+		@enabled = false
+
+	setNotificationRegex: ( str ) ->
+		try
+			regex = new RegExp str, 'gim'
+		catch e
+			return e.message
+		@enableNotifications()
+		@regex = regex
+		window.localStorage.setItem 'notifications-regex', regex.source
+		return false
+
+	checkMessage: (message) -> @enabled and @regex? and @regex.test message


Changed how popups are hidden.

Changed how popups are hidden.

--- app/assets/javascripts/calendar.js.coffee
+++ app/assets/javascripts/calendar.js.coffee
@@ -93,7 +93,7 @@
       window.TooltipManager.set_tooltips()
       this.truncate_to_now()
       this.color_reservations("#{year}-#{month}-#{day}")
-      window.ReservationPopupManager.hide_popup()
+      $(".popup").hide()
     )
     return
   color_reservations: (date)->


check if collection is allowed for the platform requesting the change

--- _source/lib/routes/server/api.coffee
+++ _source/lib/routes/server/api.coffee
@@ -122,7 +122,13 @@
   href = host.href

   for name, details of Apollos.api.platforms
-    if details.href is href
+    hrefMatches = details.href is href
+    collectionAuthorized = details.collections is "all"
+
+    if not collectionAuthorized
+      collectionAuthorized = details.collections.indexOf(collection) isnt -1
+
+    if hrefMatches and collectionAuthorized
       return name

   return false


Make the SVG charts reponsive.

Make the SVG charts reponsive.

--- app/static/coffee/production-chart.coffee
+++ app/static/coffee/production-chart.coffee
@@ -59,6 +59,8 @@
     .append("svg:svg")
       .attr("width", w + p * 2)
       .attr("height", h + p * 2)
+      .attr("viewBox", "0 0 " + (w + p * 2) + " " + (h + p * 2))
+      .attr("preserveAspectRatio", "xMidYMid meet")
     .append("svg:g")

   rules = vis.selectAll("g.rule")


Dedup undo/redo code

Dedup undo/redo code

--- coffee/react-gui/components.coffee
+++ coffee/react-gui/components.coffee
@@ -111,19 +111,8 @@
     )

-LC.React.UndoRedo = React.createClass
-  displayName: 'UndoRedo'
-  render: ->
-    {div} = React.DOM
-    {lc} = @props
-    (div {className: 'lc-undo-redo'},
-      LC.React.UndoButton({lc}),
-      LC.React.RedoButton({lc})
-    )
-
-
-LC.React.UndoButton = React.createClass
-  displayName: 'UndoButton'
+createUndoRedoButtonComponent = (undoOrRedo) -> React.createClass
+  displayName: if undoOrRedo == 'undo' then 'UndoButton' else 'RedoButton'

   # We do this a lot, even though it reads as a React no-no.
   # The reason is that '@props.lc' is a monolithic state bucket for
@@ -132,7 +121,11 @@
   # However, this component really does read and write only to the 'undo'
   # part of the state bucket, and we have to get react to update somehow, and
   # we don't want the parent to have to worry about this, so it's in @state.
-  getState: -> {isEnabled: @props.lc.canUndo()}
+  getState: -> {
+    isEnabled: switch
+      when undoOrRedo == 'undo' then @props.lc.canUndo()
+      when undoOrRedo == 'redo' then @props.lc.canRedo()
+  }
   getInitialState: -> @getState()
   mixins: [LC.React.Mixins.UpdateOnDrawingChangeMixin]

@@ -140,38 +133,27 @@
     {div} = React.DOM
     {lc} = @props

-    className = React.addons.classSet
-      'lc-undo': true
+    className = "lc-#{undoOrRedo} " + React.addons.classSet
       'toolbar-button': true
       'thin-button': true
       'disabled': not @state.isEnabled
-    onClick = if lc.canUndo() then (=> lc.undo()) else ->
-
-    (div {
-      className, onClick,
-      dangerouslySetInnerHTML: {__html: "←"}})
-
-
-LC.React.RedoButton = React.createClass
-  displayName: 'RedoButton'
-  getState: -> {isEnabled: @props.lc.canRedo()}
-  getInitialState: -> @getState()
-  mixins: [LC.React.Mixins.UpdateOnDrawingChangeMixin]
-
-  render: ->
-    {div} = React.DOM
-    {lc} = @props
-
-    className = React.addons.classSet
-      'lc-redo': true
-      'toolbar-button': true
-      'thin-button': true
-      'disabled': not @state.isEnabled
-    onClick = if lc.canRedo() then (=> lc.redo()) else ->
-
-    (div {
-      className, onClick,
-      dangerouslySetInnerHTML: {__html: "→"}})
+    onClick = switch
+      when !@state.isEnabled then ->
+      when undoOrRedo == 'undo' then -> lc.undo()
+      when undoOrRedo == 'redo' then -> lc.redo()
+
+    (div {className, onClick, dangerouslySetInnerHTML: {
+      __html: if undoOrRedo == 'undo' then "←" else "→"}})
+
+
+UndoButton = createUndoRedoButtonComponent('undo')
+RedoButton = createUndoRedoButtonComponent('redo')
+LC.React.UndoRedo = React.createClass
+  displayName: 'UndoRedo'
+  render: ->
+    {div} = React.DOM
+    {lc} = @props
+    (div {className: 'lc-undo-redo'}, UndoButton({lc}), RedoButton({lc}))

 createZoomButtonComponent = (inOrOut) -> React.createClass
@@ -197,10 +179,10 @@
       'toolbar-button': true
       'thin-button': true
       'disabled': not @state.isEnabled
-    zoom = switch
+    onClick = switch
+      when !@state.isEnabled then ->
       when inOrOut == 'in' then -> lc.zoom(0.2)
       when inOrOut == 'out' then -> lc.zoom(-0.2)
-    onClick = if @state.isEnabled then zoom else ->

     (div {className, onClick}, switch
       when inOrOut == 'in' then '+'


Fix player 2 flying in front of scenery

Fix player 2 flying in front of scenery

--- app/scripts/scenery/Lunch.coffee
+++ app/scripts/scenery/Lunch.coffee
@@ -96,7 +96,7 @@
     @add(0, 0, Crafty.e('2D, Canvas, Solid, Color').color('#404040').attr({ w: 350, h: 15 }))
     @add(350, 0, Crafty.e('2D, Canvas, Solid, Color').color('#404040').attr({ w: 100, h: 70 }))
     @add(450, 0, Crafty.e('2D, Canvas, Solid, Color').color('#404040').attr({ w: 550, h: 25 }))
-    @add(380, 0, Crafty.e('2D, Canvas, Color').color('#202020').attr({ z: -1, w: @delta.x - 380, h: @level.visibleHeight }))
+    @add(380, 0, Crafty.e('2D, Canvas, Color').color('#202020').attr({ z: -100, w: @delta.x - 380, h: @level.visibleHeight }))

 generator.defineBlock class extends @Game.LevelScenery
@@ -119,7 +119,7 @@
     h = 25
     @add(450, @level.visibleHeight - h, Crafty.e('2D, Canvas, Solid, Color').color('#404040').attr({ w: 550, h: h }))

-    @add(0, 0, Crafty.e('2D, Canvas, Color').color('#202020').attr({ z: -1, w: 380, h: @level.visibleHeight }))
+    @add(0, 0, Crafty.e('2D, Canvas, Color').color('#202020').attr({ z: -100, w: 380, h: @level.visibleHeight }))

 generator.defineBlock class extends @Game.LevelScenery
   name: 'City.Tunnel'


null check

null check


fixed: throwing on unknown route with a capital first letter

--- client/app/MainApp/kodingrouter.coffee
+++ client/app/MainApp/kodingrouter.coffee
@@ -424,7 +424,8 @@
             open.call this, routeInfo, state

           else
-            KD.remote.cacheable routeInfo.params.name, (err, [model], name)=>
+            KD.remote.cacheable routeInfo.params.name, (err, models, name)=>
+              model = models.first  if models
               open.call this, routeInfo, model

     routes


Fix cookies encoder/decoder to properly handle + signs

Fixes #34.

--- app/assets/javascripts/cookie.coffee
+++ app/assets/javascripts/cookie.coffee
@@ -1,8 +1,15 @@
 # welp
-# Reference: https://github.com/js-cookie/js-cookie/issues/70
-oldCookies = Cookies.noConflict()
-window.Cookies = oldCookies.withConverter (value) ->
-  decodeURIComponent value.replace(/\+/g, ' ')
+# Reference: https://github.com/js-cookie/js-cookie/blob/3f2b5e6884407c54b391483f39ddcd4c70f9243c/SERVER_SIDE.md
+window.Cookies = Cookies.withConverter
+  write: (value) =>
+    encodeURIComponent value
+      .replace /%(23|24|26|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent
+
+  read: (value) =>
+    value
+      .replace /\+/g, " "
+      .replace /(%[0-9A-Z]{2})+/g, decodeURIComponent
+

 Cookies.defaults['path'] = PREFIX
 Cookies.defaults['expires'] = 365


Fix infinite scrolling initialization

Fix infinite scrolling initialization

--- app/assets/javascripts/shared.coffee
+++ app/assets/javascripts/shared.coffee
@@ -62,17 +62,17 @@
       $(@).find('.order').last().find('input').val(i + 1)

 window.init_infinite_scroll = (ids) ->
+  window.loading = false
+
   for id in ids
-    window.loading = false
+    do (id) ->
+      $("##{ id }").scroll (e) ->
+        url = $("##{ id }").find('.next_page_infinite_scroll').attr('href')

-    $("##{ id }").scroll (e) ->
-      url = $("##{ id }").find('.next_page_infinite_scroll').attr('href')
+        if window.loading == false && url && ($(@).scrollTop() + $(@).innerHeight() >= $(@)[0].scrollHeight)
+          window.loading = true

-      if window.loading == false && url && ($(@).scrollTop() + $(@).innerHeight() >= $(@)[0].scrollHeight)
-        window.loading = true
-
-        $.getScript(url)
-      return
+          $.getScript(url)

 init_functions = ->
   init_tags()


* working now

* working now

--- app/assets/javascripts/webrtc.coffee
+++ app/assets/javascripts/webrtc.coffee
@@ -16,10 +16,10 @@
     'mandatory': {
       'OfferToReceiveAudio': true,
       'OfferToReceiveVideo': true
-    },
-    'optional': [{
-      'VoiceActivityDetection': false
-    }]
+    }
+    #'optional': [{
+    #  'VoiceActivityDetection': false
+    #}]
   }

   constructor: (worker) ->
@@ -227,13 +227,13 @@
     console.log('config',config)
     if (config.sendOffer)
       console.log("Creating RTC offer to " + peer_id)
-      offerConstraints = {
-        "optional": [
-          { "OfferToReceiveAudio": true },
-          { "OfferToReceiveVideo": true },
-        ]
-      }
-      constraints = @mergeConstraints(offerConstraints, sdpConstraints)
+      #offerConstraints = {
+      #  "optional": [
+      #    { "OfferToReceiveAudio": true },
+      #    { "OfferToReceiveVideo": true },
+      #  ]
+      #}
+      #constraints = @mergeConstraints(offerConstraints, sdpConstraints)

       peer_connection.createOffer((local_description) =>
         console.log("Local offer description is: ", local_description)
@@ -253,7 +253,8 @@
         )
       , (error) ->
         console.log("Error sending offer: "+error)
-      ,constraints)
+      , sdpConstraints)
+      #,constraints)
     peer_connection

   mergeConstraints: (cons1, cons2) ->


* testing timeout

* testing timeout

--- app/assets/javascripts/webrtc.coffee
+++ app/assets/javascripts/webrtc.coffee
@@ -296,32 +296,34 @@
       desc = new RTCSessionDescription(remote_description)
       console.log("Description Object: ", desc)

-      peer.setRemoteDescription(desc, =>
-        console.log("setRemoteDescription succeeded")
-        if (remote_description.type == "offer")
-          console.log("Creating answer");
-          peer.createAnswer((local_description) =>
-            console.log("Answer description is: ", local_description);
-            peer.setLocalDescription(local_description
-            , =>
-              @send('relay',
-                {
-                  'type':'sessionDescription'
-                  'actors': [peer_id],
-                  'session_description': local_description
-                }
+      setTimeout(=>
+        peer.setRemoteDescription(desc, =>
+          console.log("setRemoteDescription succeeded")
+          if (remote_description.type == "offer")
+            console.log("Creating answer");
+            peer.createAnswer((local_description) =>
+              console.log("Answer description is: ", local_description);
+              peer.setLocalDescription(local_description
+              , =>
+                @send('relay',
+                  {
+                    'type':'sessionDescription'
+                    'actors': [peer_id],
+                    'session_description': local_description
+                  }
+                )
+                console.log("Answer setLocalDescription succeeded")
+              , ->
+                alert("Answer setLocalDescription failed!")
               )
-              console.log("Answer setLocalDescription succeeded")
-            , ->
-              alert("Answer setLocalDescription failed!")
-            )
-          , (error) ->
-            console.log("Error creating answer: ", error)
-            console.log(peer)
-          ,sdpConstraints)
-      ,(error) ->
-        console.log("setRemoteDescription error: ", error)
-      )
+            , (error) ->
+              console.log("Error creating answer: ", error)
+              console.log(peer)
+            ,sdpConstraints)
+        ,(error) ->
+          console.log("setRemoteDescription error: ", error)
+        )
+      ,1000)


improve changed props check

improve changed props check

--- app/classifier/tasks/text/index.cjsx
+++ app/classifier/tasks/text/index.cjsx
@@ -67,7 +67,7 @@
     @updateHeight()

   componentWillReceiveProps: (nextProps) ->
-    if nextProps.task.instruction isnt @props.task.instruction
+    if nextProps.task isnt @props.task
       @setState textareaHeight: @state.initOffsetHeight, =>
         @updateHeight()


fix coffeescript syntax error due to wrong comment syntax

--- app/assets/javascripts/rails_admin/ui.js.coffee
+++ app/assets/javascripts/rails_admin/ui.js.coffee
@@ -28,5 +28,5 @@
     width = $(this).data("animate-width-to")
     $(this).animate(width: width, length, 'easeOutQuad')

-  // bootstrap init
+  # bootstrap init
   $('[rel=tooltip]').tooltip(delay: { show: 500, hide: 100 });


Add statusCode option

Add statusCode option

--- app/assets/javascripts/react/ajax_helper.coffee
+++ app/assets/javascripts/react/ajax_helper.coffee
@@ -20,6 +20,7 @@
       data: formattedData
       success: options['success']
       error: options['error']
+      statusCode: options['statusCode']
       complete: ( jqXHR, textStatus) ->
         redirectToUrl = jqXHR.getResponseHeader('Location')
         if redirectToUrl # url is presented


fix sortable checkpoints

fix sortable checkpoints

--- app/assets/javascripts/teacher_tracks.js.coffee
+++ app/assets/javascripts/teacher_tracks.js.coffee
@@ -16,5 +16,15 @@
     Checkpoint.remove(data.id)

   # Checkpoints sort
-  if @Curri && @Curri.user.classrole_type == 'Teacher'
-    Checkpoint.sortable()+  if Curri && Curri.user.classrole_type == 'Teacher'
+    $('.checkpoints').sortable
+      items: "> div.row"
+      handle: '.expectation'
+      cursor: 'move'
+      axis: 'y'
+      placeholder: "checkpoint-drop-highlight expectation content"
+      start: (e, ui) ->
+        ui.placeholder.height(ui.item.height())
+        ui.placeholder.width(ui.item.width())
+      update: ->
+        $.post($(this).data('url'), $(this).sortable('serialize'))


Add a promised configure

Add a promised configure

does not return the configured object but a promise that will be fulfilled with the
configured object

--- app/core/configurationManager.coffee
+++ app/core/configurationManager.coffee
@@ -1,3 +1,4 @@
+# TODO: reimplement as singleton
 mediator = Chaplin.mediator

 soundPrefix = 'gameassets/sounds/'
@@ -12,11 +13,17 @@
   configure: (object, configuration) ->
     if @[configuration]?
       @[configuration].apply(object)
-
     else
       console.error "Configuration #{configuration} not found!"

     object
+
+
+  promisedConfigure: (object, configuration) ->
+    deferred = Q.defer()
+    @configure object, configuration
+    deferred.resolve(object)
+    deferred.promise

   level1: ->


remove console.log

remove console.log

--- app/components/project-page/project-page.coffee
+++ app/components/project-page/project-page.coffee
@@ -12,7 +12,6 @@
     Records.groups.findOrFetchById(groupId).then (group) ->
       $scope.group = group
       $scope.currentMembership = group.membershipFor(CurrentUser())
-      console.log('(project-page) currentMembership: ', $scope.currentMembership)

     Records.buckets.findOrFetchById(projectId).then (project) ->
       $scope.project = project


Improve fixCorruptContainerBounds editor command (#715)

--- app/views/editor/thang/ThangTypeEditView.coffee
+++ app/views/editor/thang/ThangTypeEditView.coffee
@@ -647,10 +647,31 @@
   #  no big ones.
   #  When dryRun is true, no mutation takes place. Instead all containers with bounds
   #  larger than the maxBounds are logged letting you find them.
-  fixCorruptContainerBounds: (boundsWidthToFix=400, dryRun=true) ->
+  fixCorruptContainerBounds: (boundsWidthToFix=400, dryRun=true, backupBounds=undefined) ->
+    console.log("""
+      Running fixCorruptContainerBounds.
+        First argument is size of bounds to fix.
+        Second argument is whether or not to fix the container.
+
+      To find the numbers of large bounds, run the script:
+        `currentView.fixCorruptContainerBounds()`
+      To run a test run without making changes to bounds of size 700 run:
+        `currentView.fixCorruptContainerBounds(700, true)`
+      And to make changes:
+        `currentView.fixCorruptContainerBounds(700, false)`
+      You can always revert the changes from the editor if the animation is broken.
+
+      Finally if there are no shape bounds present you can pass in your own with the format of [x, y, width, height]
+      i.e.
+        `currentView.fixCorruptContainerBounds(700, false, [0, 0, 50, 100])`
+      This will cut out a 50px wide and 100px tall container around the sprite.
+      Incorrect settings of custom bounds will cut the artwork.
+    """)
     # Fix all the messed up bounds
     data = @thangType.attributes
     fixCount = 0
+    failCount = 0
+    backupBoundsCount = 0
     for [key, container] in Object.entries(data.raw.containers)
       if dryRun
         if container.b?[2] >= boundsWidthToFix
@@ -660,10 +681,16 @@
           reference = container.c[0]
           if data.raw.shapes[reference]
             shape = data.raw.shapes[reference]
-            container.b = [shape.bounds[0] + shape.t[0] - 5, shape.bounds[1] + shape.t[1] - 5, shape.bounds[2] + 10, shape.bounds[3] + 10]
-            fixCount += 1
+            if shape.bounds is undefined and Array.isArray(backupBounds)
+              backupBoundsCount += 1
+              shape.bounds ?= backupBounds # Lets user pass in backup bounds with options: [x, y, width, height]
+            try
+              container.b = [shape.bounds[0] + shape.t[0] - 5, shape.bounds[1] + shape.t[1] - 5, shape.bounds[2] + 10, shape.bounds[3] + 10]
+              fixCount += 1
+            catch e
+              failCount += 1
             console.log('.')
-    console.log('Fixed:', fixCount)
+    console.log('Fixed:', fixCount, 'Failed:', failCount, 'Used backup bounds:', backupBoundsCount)


tweak(tooltip): Fix Sentry 2938

tweak(tooltip): Fix Sentry 2938

--- internal_packages/tooltip/lib/tooltip.cjsx
+++ internal_packages/tooltip/lib/tooltip.cjsx
@@ -36,6 +36,7 @@
     clearTimeout @_showTimeout
     clearTimeout @_showDelayTimeout
     @_mutationObserver?.disconnect()
+    @_enteredTooltip = false

   render: =>
     <div className="tooltip-wrap #{@state.pos}" style={@_positionStyles()}>
@@ -56,15 +57,16 @@
     if elWithTooltip and DOMUtils.nodeIsVisible(elWithTooltip)
       if elWithTooltip isnt @_lastTarget
         @_onTooltipEnter(elWithTooltip)
-    else
-      @_hideTooltip() if @state.display
+    else if @state.display
+      @_hideTooltip()

   onMouseOut: (e) =>
     if @_elementWithTooltip(e.fromElement) and not @_elementWithTooltip(e.toElement)
       @_onTooltipLeave()

   onMouseDown: (e) =>
-    if @state.display then @_hideTooltip()
+    if @state.display
+      @_hideTooltip()

   _elementWithTooltip: (target) =>
     while target
@@ -75,7 +77,7 @@
   _onTooltipEnter: (target) =>
     # https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
     @_mutationObserver?.disconnect()
-    @_mutationObserver = new MutationObserver @_onTooltipLeave
+    @_mutationObserver = new MutationObserver(@_onTooltipLeave)
     @_mutationObserver.observe(target.parentNode, attributes: true, subtree: true, childList: true)

     @_lastTarget = target


Use fs.writeFileSync instead of file.writeSync

file.writeSync doesn't appear to trigger file.onDidChange
(atom/node-pathwatcher#110)

--- spec/markdown-preview-spec.coffee
+++ spec/markdown-preview-spec.coffee
@@ -201,7 +201,7 @@
           preview.onDidChangeTitle(titleChangedCallback)
           fs.renameSync(preview.getPath(), path.join(path.dirname(preview.getPath()), 'file2.md'))

-        waitsFor ->
+        waitsFor "title to update", ->
           preview.getTitle() is "file2.md Preview"

         runs ->
@@ -210,9 +210,9 @@
         spyOn(preview, 'showLoading')

         runs ->
-          preview.file.writeSync "Hey!"
-
-        waitsFor ->
+          fs.writeFileSync(preview.getPath(), "Hey!")
+
+        waitsFor "contents to update", ->
           preview.text().indexOf("Hey!") >= 0

         runs ->
@@ -221,7 +221,7 @@
         preview.onDidChangeMarkdown(listener = jasmine.createSpy('didChangeMarkdownListener'))

         runs ->
-          preview.file.writeSync "Hey!"
+          fs.writeFileSync(preview.getPath(), "Hey!")

         waitsFor "::onDidChangeMarkdown handler to be called", ->
           listener.callCount > 0


Update tooltip plugin

Update tooltip plugin

--- vendor/assets/javascripts/mice/tooltip.coffee
+++ vendor/assets/javascripts/mice/tooltip.coffee
@@ -28,7 +28,8 @@
       @enabled  = true
       @type     = type
       @$element = $(element)
-      @options  = @options.viewport and $(@options.viewport.selector or @options.viewport)
+      @options   = @getOptions(options)
+      @$viewport  = @options.viewport and $(@options.viewport.selector or @options.viewport)

       for trigger in @options.trigger.split(' ')
         if trigger == 'click'
@@ -51,7 +52,7 @@
       $.fn.tooltip.defaults

     getOptions: (options) ->
-      options = $.extend({}, @getDefaults(), @$element.data(), options)
+      options = $.extend({}, @getDefault(), @$element.data(), options)
       options.delay = show: options.delay, hide: options.delay if options.delay and typeof options.delay == 'number'
       options

@@ -65,7 +66,7 @@
       options

     enter: (obj) ->
-      self = if obj instanceof this.constructor then obj else $(obj.currentTarget).data('bs.' + @type)
+      self = if obj instanceof @constructor then obj else $(obj.currentTarget).data('bs.' + @type)

       if !self
         self = new @constructor(obj.currentTarget, @getDelegateOptions())
@@ -80,10 +81,10 @@
       self.timeout = setTimeout (-> self.show() if self.hoverState == 'in') , self.options.delay.show

     leave: (obj) ->
-      self = if obj instanceof this.constructor then obj else $(obj.currentTarget).data('bs.' + @type)
+      self = if obj instanceof @constructor then obj else $(obj.currentTarget).data('bs.' + @type)

       if !self
-        self = new this.constructor(obj.currentTarget, @getDelegateOptions())
+        self = new @constructor(obj.currentTarget, @getDelegateOptions())
         $(obj.currentTarget).data('bs.' + @type, self)

       clearTimeout(self.timeout)
@@ -94,9 +95,233 @@

       self.timeout = setTimeout (-> self.hide() if self.hoverState == 'out') , self.options.delay.hide

-  $.fn.tooltip = (options) ->
-    @.each ->
-
+    show: ->
+      e = $.Event('show.bs.'+@type)
+      if @hasContent() and @enabled
+        @$element.trigger e
+
+        inDom =  $.contains(document.documentElement, @$element[0])
+        return if e.isDefaultPrevented() or !inDom
+
+        that = @
+
+        $tip = @tip()
+        @setContent()
+
+        $tip.addClass 'fade' if @options.animation
+
+        placement = if typeof @options.placement == 'function' then @options.placement.call(@, $tip[0], @$element[0]) else @options.placement
+
+        autoToken = /\s?auto?\s?/i
+        autoPlace = autoToken.test(placement)
+        placement = placement.replace(autoToken, '') or 'top' if autoPlace
+
+        $tip.detach().css({ top: 0, left: 0, display: 'block' }).addClass(placement).data('bs.' + @type, @)
+
+        if @options.container then $tip.appendTo(@options.container) else $tip.insertAfter(@$element)
+
+        pos          = @getPosition()
+        actualWidth  = $tip[0].offsetWidth
+        actualHeight = $tip[0].offsetHeight
+
+        if autoPlace
+          orgPlacement = placement
+          $parent      = @$element.parent()
+          parentDim    = @getPosition($parent)
+
+          placement = if placement == 'bottom' and pos.top   + pos.height       + actualHeight - parentDim.scroll > parentDim.height then 'top'    else
+                      if placement == 'top'    and pos.top   - parentDim.scroll - actualHeight < 0                                   then 'bottom' else
+                      if placement == 'right'  and pos.right + actualWidth      > parentDim.width                                    then 'left'   else
+                      if placement == 'left'   and pos.left  - actualWidth      < parentDim.left                                     then 'right'  else placement
+
+          $tip.removeClass(orgPlacement).addClass(placement)
+
+        calculatedOffset = @getCalculatedOffset placement, pos, actualWidth, actualHeight
+
+        @applyPlacement calculatedOffset, placement
+
+        complete = ->
+          that.$element.trigger('shown.bs.' + that.type)
+          that.hoverState = null
+
+        if $.support.transition and @$tip.hasClass('fade') then $tip.one('bsTransitionEnd', complete).emulateTransitionEnd(150) else complete()
+
+    applyPlacement: (offset, placement) ->
+      $tip   = @tip()
+      width  = $tip[0].offsetWidth
+      height = $tip[0].offsetHeight
+
+      # manually read margins because getBoundingClientRect includes difference
+      marginTop = parseInt($tip.css('margin-top'), 10)
+      marginLeft = parseInt($tip.css('margin-left'), 10)
+
+      # we must check for NaN for ie 8/9
+      marginTop  = 0 if isNaN(marginTop)
+      marginLeft = 0 if isNaN(marginLeft)
+
+      offset.top  = offset.top  + marginTop
+      offset.left = offset.left + marginLeft
+
+      # $.fn.offset doesn't round pixel values
+      # so we use setOffset directly with our own function B-0
+      $.offset.setOffset($tip[0], $.extend({ using: (props) -> $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }) }, offset), 0)
+
+      $tip.addClass 'in'
+
+      # check to see if placing tip in new offset caused the tip to resize itself
+      actualWidth  = $tip[0].offsetWidth
+      actualHeight = $tip[0].offsetHeight
+
+      offset.top = offset.top + height - actualHeight if placement == 'top' and actualHeight != height
+
+      delta = @getViewportAdjustedDelta placement, offset, actualWidth, actualHeight
+
+      if delta.left then offset.left += delta.left else offset.top += delta.top
+
+      arrowDelta          = if delta.left then delta.left * 2 - width + actualWidth else delta.top * 2 - height + actualHeight
+      arrowPosition       = if delta.left then 'left'        else 'top'
+      arrowOffsetPosition = if delta.left then 'offsetWidth' else 'offsetHeight'
+
+      $tip.offset(offset)
+      @replaceArrow arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition
+
+    replaceArrow: (delta, dimension, position) ->
+      @arrow().css(position, if delta then (50 * (1 - delta / dimension) + '%') else '')
+
+    setContent: ->
+      $tip  = @tip()
+      title = @getTitle()
+
+      $tip.find('.tooltip-inner')[if @options.html then 'html' else 'text'](title)
+      $tip.removeClass('fade in top bottom left right')
+
+    hide: ->
+      that = @
+      $tip = @tip()
+      e    = $.Event('hide.bs.' + @type)
+
+      complete = ->
+        $tip.detach() if that.hoverState != 'in'
+        that.$element.trigger('hidden.bs.' + that.type)
+
+      @$element.trigger(e)
+
+      return if e.isDefaultPrevented()
+
+      $tip.removeClass('in')
+
+      if $.support.transition and @$tip.hasClass('fade') then $tip.one('bsTransitionEnd', complete).emulateTransitionEnd(150) else complete()
+
+      @hoverState = null
+
+      @
+
+    fixTitle: ->
+      $e = @$element
+      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') if $e.attr('title') or typeof ($e.attr('data-original-title')) != 'string'
+
+    hasContent: ->
+      @getTitle()
+
+    getPosition: ($element) ->
+      $element  = $element or @$element
+      el        = $element[0]
+      isBody    = el.tagName == 'BODY'
+
+      return $.extend({}, (if (typeof el.getBoundingClientRect == 'function') then el.getBoundingClientRect() else null), {
+        scroll: if isBody then document.documentElement.scrollTop or document.body.scrollTop else $element.scrollTop()
+        width:  if isBody then $(window).width()  else $element.outerWidth()
+        height: if isBody then $(window).height() else $element.outerHeight()
+      }, if isBody then { top: 0, left: 0 } else $element.offset())
+
+    getCalculatedOffset: (placement, pos, actualWidth, actualHeight) ->
+      return if placement == 'bottom' then { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } else
+             if placement == 'top'    then { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } else
+             if placement == 'left'   then { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } else { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
+
+    getViewportAdjustedDelta: (placement, pos, actualWidth, actualHeight) ->
+      delta = { top: 0, left: 0 }
+      return delta if !@$viewport
+
+      viewportPadding = @options.viewport and @options.viewport.padding or 0
+      viewportDimensions = @getPosition @$viewport
+
+      if /right|left/.test placement
+        topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll
+        bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
+        if topEdgeOffset < viewportDimensions.top
+          delta.top = viewportDimensions.top - topEdgeOffset
+        else if bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height
+          delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
+      else
+        leftEdgeOffset  = pos.left - viewportPadding
+        rightEdgeOffset = pos.left + viewportPadding + actualWidth
+        if leftEdgeOffset < viewportDimensions.left
+          delta.left = viewportDimensions.left - leftEdgeOffset
+        else if rightEdgeOffset > viewportDimensions.width
+          delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
+
+      delta
+
+    getTitle: ->
+      $e = @$element
+      o  = @options
+      title = $e.attr('data-original-title') or (if typeof o.title == 'function' then o.title.call($e[0]) else  o.title)
+      title
+
+    tip: ->
+      @$tip = @$tip or $(@options.template)
+
+    arrow: ->
+      @$arrow = @$arrow or @tip().find('.tooltip-arrow')
+
+    validate: ->
+      if !@$element[0].parentNode
+        @hide()
+        @$element = null
+        @options  = null
+
+    enable: ->
+      @enabled = true
+
+    disable: ->
+      @enabled = false
+
+    toggleEnabled: ->
+      @enabled = !@enabled
+
+    toggle: (e) ->
+      self = this
+      if e
+        self = $(e.currentTarget).data('bs.' + @type)
+        if !self
+          self = new @constructor(e.currentTarget, @getDelegateOptions())
+          $(e.currentTarget).data('bs.' + @type, self)
+
+      if self.tip().hasClass('in') then self.leave(self) else self.enter(self)
+
+    destroy: ->
+      clearTimeout(@timeout)
+      @hide().$element.off('.' + @type).removeData('bs.' + @type)
+
+
+  $.fn.tooltip = (option) ->
+    @each ->
+      $this   = $(@)
+      data    = $this.data('bs.tooltip')
+      options = typeof option == 'object' and option
+
+      return if !data and option == 'destroy'
+      $this.data('bs.tooltip', (data = new Tooltip(@, options))) if !data
+      data[option]() if typeof option == 'string'
+
+  old = $.fn.tooltip
+
+  $.fn.tooltip.Constructor = Tooltip
+
+  $.fn.tooltip.noConflict = ->
+    $.fn.tooltip = old
+    @

   $.fn.tooltip.defaults =
     animation: true


fixed mouse capture for FF, but not for Chrome

--- client/src/app/io.coffee
+++ client/src/app/io.coffee
@@ -63,8 +63,8 @@
           rect = event.target.getBoundingClientRect()
           target = $(event.target)
           mouse.position = [
-            Math.floor((event.clientX - rect.left) * target.attr("width") / rect.width)
-            Math.floor((event.clientY - rect.top) * target.attr("height") / rect.height)
+            Math.floor((event.clientX - rect.left) * options.size[0] / rect.width)
+            Math.floor((event.clientY - rect.top) * options.size[1] / rect.height)
           ]

     return {


Disallow typing a dot after a dash.  optimize for performance

--- app/js/directives/misc.coffee
+++ app/js/directives/misc.coffee
@@ -43,17 +43,20 @@

     $scope.ku = ->
         return unless $scope.ngModel
+        if ($scope.ngModel.length>=63)
+            $scope.ngModel=oldname
+            return
         subnames=$scope.ngModel.split('.')
         i = 0
         last = subnames.length - 1
         while i < last
-            valid = /^[a-z]+(?:[a-z0-9\-])*$/.test(subnames[i]) && $scope.ngModel.length<63
+            valid = /^[a-z]+(?:[a-z0-9\-])*[a-z0-9]$/.test(subnames[i])
             if(!valid)
                 $scope.ngModel=oldname
                 break
             ++i
         if(subnames[last] != '')
-            valid = /^[a-z]+(?:[a-z0-9\-])*$/.test(subnames[last]) && $scope.ngModel.length<63
+            valid = /^[a-z]+(?:[a-z0-9\-])*$/.test(subnames[last])
             if(!valid)
                 $scope.ngModel=oldname


Revert "games: use previous day instead of current day for game filter"

This reverts commit b223da0e2c62e5e8fd941bd276edeefd55f483bd.

--- src/components/games/index.coffee
+++ src/components/games/index.coffee
@@ -57,7 +57,7 @@
     appNames = model.event.query {
       select: 'count(userId)'
       from: 'game_play'
-      where: 'time >= now() - 2d AND time < now() - 1d'
+      where: 'time >= now() - 1d'
       groupBy: 'game'
     }
     .map (result) ->


Use coffee for pouch views

Use coffee for pouch views

--- backend/pouch.coffee
+++ backend/pouch.coffee
@@ -19,6 +19,7 @@
     constructor: (@config) ->
         @db = new PouchDB @config.dbPath
         @db.setMaxListeners 100
+        @db.on 'error', (err) -> log.debug err
         @updater = async.queue (task, callback) =>
             @db.get task._id, (err, doc) =>
                 if err?.status is 404
@@ -107,37 +108,31 @@
     # The path for a file/folder in root will be '',
     # not '.' as with node's path.dirname
     addByPathView: (callback) =>
-        query = """
-            function (doc) {
-                if ('docType' in doc) {
-                    var parts = doc._id.split('#{path.sep}');
-                    parts.pop();
-                    emit(parts.join('#{path.sep}'), { _id: doc._id });
-                }
-            }
-            """
+        query = (
+            (doc) ->
+                if 'docType' of doc
+                    parts = doc._id.split '/'
+                    parts.pop()
+                    emit parts.join('/'), _id: doc._id
+        ).toString()
         @createDesignDoc "byPath", query, callback

     # Create a view to find files by their checksum
     addByChecksumView: (callback) =>
-        query = """
-            function (doc) {
-                if ('checksum' in doc) {
-                    emit(doc.checksum);
-                }
-            }
-            """
+        query = (
+            (doc) ->
+                if 'checksum' of doc
+                    emit doc.checksum
+        ).toString()
         @createDesignDoc "byChecksum", query, callback

     # Create a view to find file/folder by their _id on a remote cozy
     addByRemoteIdView: (callback) =>
-        query = """
-            function (doc) {
-                if ('remote' in doc) {
-                    emit(doc.remote._id);
-                }
-            }
-            """
+        query = (
+            (doc) ->
+                if 'remote' of doc
+                    emit doc.remote._id
+        ).toString()
         @createDesignDoc "byRemoteId", query, callback

     # Create or update given design doc


Fix playthrough for changing media types

--- app/assets/javascripts/avalon_player.js.coffee
+++ app/assets/javascripts/avalon_player.js.coffee
@@ -212,5 +212,8 @@
           $.getJSON uri, params.join('&'), (data) =>
             @player.options.autostart = autostart
             @setStreamInfo(data)
+      else
+        @click()
+      return

 (exports ? this).AvalonPlayer = AvalonPlayer


removes log output

removes log output

--- app/assets/javascripts/components/modal.coffee
+++ app/assets/javascripts/components/modal.coffee
@@ -24,29 +24,24 @@
   connect((state) -> state)(
     React.createClass
       componentDidMount: ->
-        console.log('modal mounted')
         $(@refs.modal).modal 'show'
         $(@refs.modal).on 'hidden.bs.modal', @handleClose

       componentWillUnmount: ->
-        console.log('modal unmount')

       close: (e, callback) ->
-        console.log('modal close', e, callback)
         e.preventDefault() if e
         # bug in react. Instead of one parameter the onClick callback provides
-        # two paraeters. The first is a Proxy object (don't know what it is).
+        # two parameters. The first is a Proxy object (don't know what it is).
         # Deactivate the callback feature until fixed.
         # $(@refs.modal).on('hidden.bs.modal', callback) if callback
         $(@refs.modal).modal 'hide'

       handleClose: () ->
-        console.log('modal handle close')
         if @props.dispatch
           @props.dispatch(type: ReactModal.HIDE_MODAL, modalType: @props.modalType)

       render: ->
-        console.log('modal render')
         options = ReactHelpers.mergeObjects({closeButton: true},options)
         modalProps = (@props.modalProps || {})
         childProps = ReactHelpers.mergeObjects(@props,{close: @close})


Updates dynamic url params fields generation

--- app/assets/javascripts/sabisu/sabisu.js.coffee
+++ app/assets/javascripts/sabisu/sabisu.js.coffee
@@ -28,10 +28,10 @@
     e.preventDefault()
     $urlParamsInputsWrapper.append("

-    <input type='text' name='explorer[url_params][#{urlParamFieldId}][url_param_name_#{urlParamFieldId}]' id='explorer_header_name_#{urlParamFieldId}' placeholder='Param name'/>×
+    <input type='text' name='explorer[url_params][#{urlParamFieldId}][url_params_name_#{urlParamFieldId}]' id='explorer_url_params_name_#{urlParamFieldId}' placeholder='Param name'/>×

-    <input type='text' name='explorer[url_params][#{urlParamFieldId}][url_param_value_#{urlParamFieldId}]' id='explorer_header_value_#{urlParamFieldId}' placeholder='Param value'/>×
+    <input type='text' name='explorer[url_params][#{urlParamFieldId}][url_params_value_#{urlParamFieldId}]' id='explorer_url_params_value_#{urlParamFieldId}' placeholder='Param value'/>×

")
     urlParamFieldId++


sort tags list by lowercase name

sort tags list by lowercase name

--- app/coffee/modules/admin/project-values.coffee
+++ app/coffee/modules/admin/project-values.coffee
@@ -730,8 +730,10 @@
             @.loading = false

     filterAndSortTags: =>
+        @scope.projectTags = _.sortBy @scope.projectTagsAll, (it) -> it.name.toLowerCase()
+
         @scope.projectTags = _.filter(
-            _.sortBy(@scope.projectTagsAll, "name"),
+            @scope.projectTags,
             (tag) => tag.name.indexOf(@scope.tagsFilter.name) != -1
         )


tidying up

tidying up

--- app/controllers/bankaccesses_controller.coffee
+++ app/controllers/bankaccesses_controller.coffee
@@ -130,41 +130,6 @@
                                             console.log "Bank Accounts created successfully"
                                             send ba, 201

-
-
-            ###
-            # scaffolding:
-            BankAccount = compound.models.BankAccount
-            BankOperation = compound.models.BankOperation
-
-            for num in [1..3]
-                body =
-                    title: "Compte bancaire "+ num
-                    bankAccess: ba.id
-                    bank: ba.bank
-                    amount: Math.floor(Math.random() * 10000)
-                    initialAmount: 0
-                    accountNumber: "FR 123 31321 41421 23"
-
-                BankAccount.create body, (err, baccount) ->
-                    if not err
-                        for i in [1..Math.floor(Math.random() * 100)]
-                            d = new Date()
-                            d.setFullYear(1990 + Math.floor(Math.random() * 23))
-                            d.setMonth(Math.floor(Math.random() * 11) + 1)
-                            d.setDate(Math.floor(Math.random() * 27) + 1)
-                            body =
-                                bankAccount: baccount.id
-                                title: Math.random().toString(36).slice(5) + " " + Math.random().toString(36).slice(10)
-                                date: d
-                                amount: (Math.floor(Math.random() * 10000) - Math.floor(Math.random() * 20000))
-                                category: "whatever"
-                            BankOperation.create body, () ->
-
-            # scaffolding end
-            send ba, 201
-            ###
-
 action 'destroy', ->
     @ba.destroy (err) ->
         if err?


don't return a value from showNotification

--- app/js/jail_iframe/classes/notification.coffee
+++ app/js/jail_iframe/classes/notification.coffee
@@ -41,3 +41,4 @@
     setTimeout (-> $el.remove()), removal_delay

   render()
+  return


Add wobbling stalker to give player a fair warning

--- app/scripts/lazerscripts/stage1/stalker.coffee
+++ app/scripts/lazerscripts/stage1/stalker.coffee
@@ -8,7 +8,7 @@
       health: 100
       x: 680
       y: 400
-      speed: 300
+      speed: 600
     )

   execute: ->
@@ -20,6 +20,16 @@
         @moveTo(@targetLocation(), y: 450, speed: 200)
         @wait 100
       ))
+      @moveTo(y: 340, speed: 200)
+
+      # Wobble once before moving up
+      @repeat(1, @sequence(
+        @wait 100
+        @moveTo(y: 350, speed: 100)
+        @wait 100
+        @moveTo(y: 340, speed: 100)
+      ))
+
       @moveTo(y: -50)
     )


support non-public broadcasts

support non-public broadcasts

--- src/plugins/request-broadcasts.coffee
+++ src/plugins/request-broadcasts.coffee
@@ -25,29 +25,33 @@
 bootstrap = (callback) ->

     if argv.new or argv.n
-        Chat.prepareNew ->
+        Chat.prepareNew Chat.TYPE_PUBLIC, ->
             Chat.start callback
     else
-        Chat.prepareFromDatabase ->
+        Chat.prepareFromDatabase Chat.TYPE_PUBLIC, ->
             Chat.start callback

 Chat =

-    tasks: {}
+    TYPE_PUBLIC:  'all'
+    TYPE_FACTION: 'faction'
+
+    tasks:  {}
     length: 0

-    createTasks: (timestampMin, callback) ->
+    createTasks: (type, timestampMin, callback) ->

         timestampMax = new Date().getTime()

         preparedTasks = []

         for TSmin in [timestampMin..timestampMax] by Config.Chat.SplitTimespanMS
-
+
             TSmax = Math.min(timestampMax, TSmin + Config.Chat.SplitTimespanMS - 1)
             continue if TSmax is TSmin

             preparedTasks.push
+                type:  type
                 data:
                     desiredNumItems: Config.Chat.FetchItemCount
                     minLatE6:        Math.round(Config.Region.SouthWest.Lat * 1e6)
@@ -56,7 +60,7 @@
                     maxLngE6:        Math.round(Config.Region.NorthEast.Lng * 1e6)
                     minTimestampMs:  TSmin
                     maxTimestampMs:  TSmax
-                    chatTab:         'all'
+                    chatTab:         type
                 status: STATUS_PENDING
                 _id:    new ObjectID()

@@ -65,25 +69,25 @@
             Chat.tasks[task._id.toString()] = task
             Chat.length++

-            Database.db.collection('Chat._queue').insert task, callback
+            Database.db.collection('chat_queue').insert task, callback

         , ->

-            Database.db.collection('Chat._data').update
+            Database.db.collection('chat_data').update
                 _id: 'last_task'
             ,
                 $set:
-                    timestamp: timestampMax
+                    'timestamp_' + type: timestampMax
             ,
                 upsert: true
             , (err) ->

-                logger.info "[Broadcasts] Created #{preparedTasks.length} tasks (all #{Chat.length} tasks)."
+                logger.info "[Broadcasts.#{type}] Created #{preparedTasks.length} tasks (all #{Chat.length} tasks)."
                 callback && callback()

-    prepareFromDatabase: (callback) ->
-
-        logger.info "[Broadcasts] Continue: [#{Config.Region.SouthWest.Lat},#{Config.Region.SouthWest.Lng}]-[#{Config.Region.NorthEast.Lat},#{Config.Region.NorthEast.Lng}]"
+    prepareFromDatabase: (type, callback) ->
+
+        logger.info "[Broadcasts.#{type}] Continue: [#{Config.Region.SouthWest.Lat},#{Config.Region.SouthWest.Lng}]-[#{Config.Region.NorthEast.Lat},#{Config.Region.NorthEast.Lng}]"

         # TODO: Maybe should take TimeOffset into consideration?
         timestampMin    = new Date().getTime() - Config.Chat.TraceTimespanMS
@@ -95,7 +99,7 @@

                 # query queued tasks

-                Database.db.collection('Chat._queue').find().toArray (err, tasks) ->
+                Database.db.collection('chat_queue').find({type: type}).toArray (err, tasks) ->

                     if tasks?
                         for task in tasks
@@ -108,9 +112,9 @@

                 # get last timestamp

-                Database.db.collection('Chat._data').findOne {_id: 'last_task'}, (err, data) ->
-
-                    timestampMin = data.timestamp + 1 if data?.timestamp?
+                Database.db.collection('chat_data').findOne {_id: 'last_task'}, (err, data) ->
+
+                    timestampMin = data[type].timestamp + 1 if data?[type]?.timestamp?
                     timestampMin = timestampMinMax if timestampMin < timestampMinMax

                     callback()
@@ -119,16 +123,16 @@

                 # create tasks

-                Chat.createTasks timestampMin, callback
+                Chat.createTasks type, timestampMin, callback

         ], callback

-    prepareNew: (callback) ->
-
-        logger.info "[Broadcasts] New: [#{Config.Region.SouthWest.Lat},#{Config.Region.SouthWest.Lng}]-[#{Config.Region.NorthEast.Lat},#{Config.Region.NorthEast.Lng}]"
+    prepareNew: (type, callback) ->
+
+        logger.info "[Broadcasts.#{type}] New: [#{Config.Region.SouthWest.Lat},#{Config.Region.SouthWest.Lng}]-[#{Config.Region.NorthEast.Lat},#{Config.Region.NorthEast.Lng}]"

         timestampMin = new Date().getTime() - Config.Chat.TraceTimespanMS
-        Chat.createTasks timestampMin, callback
+        Chat.createTasks type, timestampMin, callback

     start: (callback) ->

@@ -138,15 +142,27 @@

             (callback) ->

-                Database.db.collection('Chat').ensureIndex {time: -1}, callback
-
-            , (callback) ->
-
-                Database.db.collection('Chat').ensureIndex {'markup.player1.guid': 1}, callback
-
-            , (callback) ->
-
-                Database.db.collection('Chat').ensureIndex {'markup.portal1.guid': 1}, callback
+                Database.db.collection('Chat.all').ensureIndex {time: -1}, callback
+
+            , (callback) ->
+
+                Database.db.collection('Chat.all').ensureIndex {'markup.player1.guid': 1}, callback
+
+            , (callback) ->
+
+                Database.db.collection('Chat.all').ensureIndex {'markup.portal1.guid': 1}, callback
+
+            ,(callback) ->
+
+                Database.db.collection('Chat.faction').ensureIndex {time: -1}, callback
+
+            , (callback) ->
+
+                Database.db.collection('Chat.faction').ensureIndex {'markup.player1.guid': 1}, callback
+
+            , (callback) ->
+
+                Database.db.collection('Chat.faction').ensureIndex {'markup.portal1.guid': 1}, callback

         ], ->

@@ -162,7 +178,7 @@

                 Chat.tasks[taskId].status = STATUS_PENDING

-                Database.db.collection('Chat._queue').update
+                Database.db.collection('chat_queue').update
                     _id:    new ObjectID(taskId)
                 ,
                     $set:
@@ -178,35 +194,37 @@

     request: (taskId) ->

+        chatType = Chat.tasks[taskId].type
+
         request.push

             action: 'getPaginatedPlexts'
             data:   Chat.tasks[taskId].data
             onSuccess: (response, callback) ->

-                parseChatResponse taskId, response.result, callback
+                parseChatResponse chatType, taskId, response.result, callback

             onError: (err, callback) ->

                 # TODO: Recover tasks

-                logger.error "[Broadcasts] #{err.message}"
+                logger.error "[Broadcasts.#{chatType}] #{err.message}"
                 callback()

             afterResponse: (callback) ->

-                logger.info "[Broadcasts] " +
+                logger.info "[Broadcasts.#{chatType}] " +
                     Math.round(request.done / request.max * 100).toString() +
                     "%\t[#{request.done}/#{request.max}]" +
                     "\t#{messageCount} messages (#{dbQueue.length()} in buffer)"

                 callback()

-parseChatResponse = (taskId, response, callback) ->
+parseChatResponse = (type, taskId, response, callback) ->

     async.each response, (rec, callback) ->

-        insertMessage rec[0], rec[1], rec[2], callback
+        insertMessage type, rec[0], rec[1], rec[2], callback

     , ->

@@ -217,7 +235,7 @@
             delete Chat.tasks[taskId]
             Chat.length--

-            Database.db.collection('Chat._queue').remove
+            Database.db.collection('chat_queue').remove
                 _id: new ObjectID(taskId)
             ,
                 single: true
@@ -231,7 +249,7 @@
             Chat.tasks[taskId].data.maxTimestampMs = maxTimestamp
             Chat.tasks[taskId].status = STATUS_NOTCOMPLETE

-            Database.db.collection('Chat._queue').update
+            Database.db.collection('chat_queue').update
                 _id: new ObjectID(taskId)
             ,
                 $set:
@@ -252,68 +270,73 @@

 , Config.Database.MaxParallel

-insertMessage = (id, timestamp, data, _callback) ->
-
-    main = ->
-
-        insertCount++
-
-        data2 = data.plext
-
-        # parse markup
-        markup = {}
-        count = {}
-
-        for m in data.plext.markup
-            count[m[0]] = 0 if not count[m[0]]?
-            count[m[0]]++
-            markup[m[0]+count[m[0]].toString()] = m[1]
-
-        data2.markup = markup
-
-        dbQueue.push (callback) ->
-
-            doc = data2
-            doc._id = id
-            doc.time = timestamp
-
-            async.series [
-
-                (callback) ->
+insertMessage = (type, id, timestamp, data, callback) ->
+
+    async.series [
+
+        (callback) ->
+
+            if insertCount % 100 is 0

-                    Database.db.collection('Chat').insert doc, callback
-
-                , (callback) ->
-
-                    # resove player names
-                    if doc.markup.PLAYER1?
-
-                        level = null
-
-                        if doc.markup.TEXT1.plain is ' deployed an '
-                            level = parseInt doc.markup.TEXT2.plain.substr(1)
-
-                        Agent.resolved doc.markup.PLAYER1.plain,
-                            team:  Agent.strToTeam(doc.markup.PLAYER1.team)
-                            level: level
-                        , callback
-
-                    else
-
-                        callback()
+                Database.db.collection('Chat.' + type).count {}, (err, count) ->
+                    messageCount = count
+                    callback()
+
+            else
+
+                callback()
+
+        , (callback) ->
+
+            insertCount++
+
+            data2 = data.plext
+
+            # parse markup
+            markup = {}
+            count = {}
+
+            for m in data.plext.markup
+                count[m[0]] = 0 if not count[m[0]]?
+                count[m[0]]++
+                markup[m[0]+count[m[0]].toString()] = m[1]
+
+            data2.markup = markup
+
+            dbQueue.push (done) ->
+
+                doc = data2
+                doc._id = id
+                doc.time = timestamp
+
+                async.series [
+
+                    (callback) ->

-            ], ->
-
-                callback()
-                _callback()
-
-    # update count
-    if insertCount % 100 is 0
-
-        Database.db.collection('Chat').count {}, (err, count) ->
-            messageCount = count
-            main()
-
-    else
-
-        main()
+                        Database.db.collection('Chat.' + type).insert doc, callback
+
+                    , (callback) ->
+
+                        # resove player names
+                        if doc.markup.PLAYER1?
+
+                            level = 0
+
+                            if doc.markup.TEXT1.plain is ' deployed an '
+                                level = parseInt doc.markup.TEXT2.plain.substr(1)
+
+                            Agent.resolved doc.markup.PLAYER1.plain,
+                                team:  Agent.strToTeam(doc.markup.PLAYER1.team)
+                                level: level
+                            , callback
+
+                        else
+
+                            callback()
+
+                ], ->
+
+                    done()
+                    callback()
+
+    ], callback


Jax.World is now an event emitter

Jax.World is now an event emitter

It can fire the following events:
  ambientChanged - ambient light color changed
  numCamerasChanged - number of scene cameras changed
  lightAdded - light added to scene
  lightRemoved - light removed from scene
  objectAddedToOctree - object added to octree
  objectAdded - object added to scene, but not
                necessarily to the octree
  objectRemovedFromOctree - object removed from octree
  objectRemoved - object removed from scene, but not
                  necessarily removed from octree
  disposed - scene disposed

--- lib/assets/javascripts/jax/world.js.coffee
+++ lib/assets/javascripts/jax/world.js.coffee
@@ -1,3 +1,4 @@
+#= require "jax/core"
 #= require "jax/partition/octree"

 ###
@@ -7,6 +8,8 @@
 controllers and views as the +this.world+ property.
 ###
 class Jax.World
+  @include Jax.EventEmitter
+
   constructor: (@context) ->
     @renderOctree = false
     @lights = []
@@ -43,7 +46,9 @@

   @define 'ambientColor',
     get: -> @_ambientColor
-    set: (c) -> @_ambientColor = Jax.Color.parse c
+    set: (c) ->
+      @_ambientColor = Jax.Color.parse c
+      @fireEvent 'ambientChanged'

   @getter 'objects', ->
     console.log """The getter `objects` is deprecated; please use `getObjects()` instead.
@@ -64,6 +69,7 @@
       if i > num
         for j in [num...i]
           @_cameras[j] = new Jax.Camera
+      @fireEvent 'numCamerasChanged'

   ###
   Renders this World to its Jax context. If a material (or its name) is not
@@ -185,6 +191,7 @@
   addLight: (light) ->
     light = Jax.Light.find(light) unless light instanceof Jax.Light
     @lights.push light
+    @fireEvent 'lightAdded'
     light

   ###
@@ -192,6 +199,7 @@
   ###
   removeLight: (light) ->
     @lights.splice @lights.indexOf(light)
+    @fireEvent 'lightRemoved'
     light

   ###
@@ -220,9 +228,11 @@
     if addToOctree
       @octree.add object
       object.addEventListener 'transformed', @updateOctree
+      @fireEvent 'objectAddedToOctree'
     else
       @_objects[object.__unique_id] = object
     @getObjects().push object
+    @fireEvent 'objectAdded'
     if object.castShadow isnt false
       # immediately invalidate shadow maps so that this object doesn't
       # not have a shadow
@@ -251,7 +261,10 @@
     @invalidateShadowMaps.apply obj
     obj.removeEventListener 'transformed', @invalidateShadowMaps
     obj.removeEventListener 'transformed', @updateOctree
-    @octree.find(obj)?.remove obj
+    if node = @octree.find(obj)
+      node.remove obj
+      @fireEvent 'objectRemovedFromOctree'
+    @fireEvent 'objectRemoved'
     obj

   pickDataBuffers = {}
@@ -365,4 +378,5 @@
     for light in @lights.splice(0)
       @removeLight light
       light.dispose @context
+    @fireEvent 'disposed'
     true


Don't delete cached data when no reply occurs

When the network times out or is otherwise interrupted the status
is 0, indicating "no status".

In this case we should not clear the data we previously may have loaded.

--- src/flux/helpers.coffee
+++ src/flux/helpers.coffee
@@ -35,9 +35,10 @@

     FAILED: (status, msg, id) ->
       @_asyncStatus[id] = FAILED
-      delete @_local[id]
       @_errors[id] = msg
-      @emitChange()
+      unless status is 0 # indicates network failure
+        delete @_local[id]
+        @emitChange()

     load: (id) ->
       # Add a shortcut for unit testing


Don't mark discussions without readers as unread

--- lineman/app/models/discussion_model.coffee
+++ lineman/app/models/discussion_model.coffee
@@ -74,10 +74,7 @@
       proposal.lastVoteAt if proposal?

     isUnread: ->
-      if @lastReadAt?
-        @unreadActivityCount() > 0
-      else
-        true
+      @discussionReaderId? and (!@lastReadAt? or @unreadActivityCount() > 0)

     isImportant: ->
       @starred or @hasActiveProposal()


Kontrol/Base: fix version, linter issues

--- src/kontrol/base.coffee
+++ src/kontrol/base.coffee
@@ -4,7 +4,7 @@

 module.exports = class Kontrol extends Emitter

-  @version = '0.4.0-rc1'
+  @version = '1.0.0'

   @Kite = Kite

@@ -142,16 +142,21 @@
     changes.emit eventName, kite
     return

+  getPath = (query) ->
+    {
+      username = ''
+      environment = ''
+      name = ''
+      version = ''
+      region = ''
+      hostname = ''
+      id  = ''
+    } = query
+
+    "#{username}/#{environment}/#{name}/#{version}/#{region}/#{hostname}/#{id}"
+
   createKiteNotFoundError: (query) ->
-    { username, environment, name, version, region, hostname, id } = query
-    new KiteError "No kite found! query: #{
-      username    ? '' }/#{
-      environment ? '' }/#{
-      name        ? '' }/#{
-      version     ? '' }/#{
-      region      ? '' }/#{
-      hostname    ? '' }/#{
-      id          ? '' }"
+    new KiteError "No kite found for query: #{getPath query}"

   connect: -> @kite.connect()

@@ -163,4 +168,3 @@
   @actions      =
     REGISTER    : 'register'
     DEREGISTER  : 'deregister'
-


added some timeupdate listen for not support seeked event

--- src/mediaMonitor.coffee
+++ src/mediaMonitor.coffee
@@ -8,6 +8,9 @@
     @addEvent_ @el, "seeking", @
     @addEvent_ @el, "seeked", @
     @addEvent_ @el, "timeupdate", @
+
+    @isEnabledSeeking_ = "onseeking" of @el or "onseeked" of @el or
+                         (/(ipad|iphone)/i.test(navigator.userAgent) && /Version\/8/i.test(navigator.userAgent))

     if @el.duration
       @setDuration_ @el.duration
@@ -90,15 +93,21 @@

   onScaleChange:->

-    lastStatus = @getLastStatus_()
-    startScale = if lastStatus then lastStatus.endScale else parseInt( @startTime_ * @scale / @duration_ )
+    if @isEnabledSeeking_
+      lastStatus = @getLastStatus_()
+      startScale = if lastStatus then lastStatus.endScale else parseInt( @startTime_ * @scale / @duration_ )

-    end = @el.currentTime
-    endScale   = parseInt( end * @scale / @duration_ )
-
-    if @listenCB and ( !lastStatus or endScale != lastStatus.endScale ) then @listenCB startScale:startScale, endScale:endScale
+      end = @el.currentTime
+      endScale   = parseInt( end * @scale / @duration_ )
+
+      if @listenCB and ( !lastStatus or endScale != lastStatus.endScale ) then @listenCB startScale:startScale, endScale:endScale

-    @setLastStatus_ startScale, endScale
+      @setLastStatus_ startScale, endScale
+    #listen not support seeked
+    else
+      scale = parseInt( @el.currentTime * @scale / @duration_ )
+      if scale != @lastScale_ then @listenCB startScale:scale, endScale:scale
+      @lastScale_ = scale

   #@event


Port functionality of Hooks from Repo's to Orgs.

--- src/octonode/org.coffee
+++ src/octonode/org.coffee
@@ -139,5 +139,26 @@
       return cb(err)  if err
       if s isnt 204 then cb(new Error('Org addTeamRepo error')) else cb null, b, h

+  # List hooks
+  # '/orgs/pksunkara/hub/hooks' GET
+  hooks: (cb) ->
+    @client.get "/orgs/#{@name}/hooks", (err, s, b, h) ->
+      return cb(err) if (err)
+      if s isnt 200 then cb(new Error("Repo hooks error")) else cb null, b, h
+
+  # Create a hook
+  # '/orgs/pksunkara/hub/hooks' POST
+  hook: (hookInfo, cb) ->
+    @client.post "/orgs/#{@name}/hooks", hookInfo, (err, s, b, h) ->
+      return cb(err) if err
+      if s isnt 201 then cb(new Error("Repo createHook error")) else cb null, b, h
+
+  # Delete a hook
+  # '/orgs/pksunkara/hub/hooks/37' DELETE
+  deleteHook: (id, cb) ->
+    @client.del "/orgs/#{@name}/hooks/#{id}", {}, (err, s, b, h) ->
+      return cb(err) if err
+      if s isnt 204 then cb(new Error("Repo deleteHook error")) else cb null, b, h
+
 # Export module
 module.exports = Org


Fix error with model Permissions

Fix error with model Permissions

--- packages/rocketchat-authorization/server/models/Permissions.coffee
+++ packages/rocketchat-authorization/server/models/Permissions.coffee
@@ -6,6 +6,6 @@
 	# FIND
 	findByRole: (role, options) ->
 		query =
-			role: role
+			roles: role

-		return @findOne query, options
+		return @find query, options


Use jquery .filter.

Use jquery .filter.

--- resources/assets/coffee/store.coffee
+++ resources/assets/coffee/store.coffee
@@ -80,7 +80,7 @@
 #checkout checks
 checkCheckoutConfirmations = ->
   $checkboxes = $('.js-checkout-confirmation-step')
-  $checkboxesChecked = $('.js-checkout-confirmation-step:checked')
+  $checkboxesChecked = $checkboxes.filter(':checked')
   $('#checkout-with-paypal').toggleClass('disabled', $checkboxesChecked.length < $checkboxes.length)

 $(document).on 'ready page:load', checkCheckoutConfirmations


[mechanics] remove outdated avalanche tests

--- test/bw/moves.coffee
+++ test/bw/moves.coffee
@@ -1636,11 +1636,6 @@
       @p2.currentHP = 1
       @battle.performMove(@p1, @battle.getMove('Super Fang'))
       @p2.currentHP.should.equal 0
-
-  describe 'Avalanche', ->
-    it "doubles base power if moving after target"
-    it "has normal base power if moving before target"
-    it "doesn't double base power if moving after target, then before target"

   describe "A weather-based recovery move", ->
     it "heals 50% HP in no weather, rounded half down", ->


Updated $xhr and $response.

Updated $xhr and $response.

--- src/directives/file_upload.coffee
+++ src/directives/file_upload.coffee
@@ -32,9 +32,10 @@
       callbackFn = $parse attrs["macUpload#{action}"]
       if callbackFn?
         $scope.$apply ->
-          $status        = $data.jqXHR?.status
-          args           = {$event, $data, $status}
-          args.$response = $data.jqXHR
+          $xhr      = $data.jqXHR
+          $status   = $data.jqXHR?.status
+          $response = $data.jqXHR?.responseText
+          args      = {$event, $data, $status, $xhr, $response}
           callbackFn $scope, args

     options =


Update plugin registration function name.

--- api/index.coffee
+++ api/index.coffee
@@ -26,6 +26,6 @@
 """

 fs = require 'fs'
-SupAPI.addPlugin 'typescript', 'dom', {
+SupAPI.registerPlugin 'typescript', 'dom', {
   defs: fs.readFileSync(__dirname + '/dom.d.ts', encoding: 'utf8') + fix
 }


only process the render queue after we stop moving

--- services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
+++ services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
@@ -81,6 +81,8 @@
 				@resetState()

 			triggerRenderQueue: (interval = @JOB_QUEUE_INTERVAL) ->
+				if @queueTimer?
+					clearTimeout @queueTimer
 				@queueTimer = setTimeout () =>
 					@queueTimer = null
 					@processRenderQueue()


consistent new_

consistent new_

--- api/server.coffee
+++ api/server.coffee
@@ -29,8 +29,8 @@
 # Post a measurement from a device
 app.post '/measurements.json', (req, res) ->
     {device_id, kind, value, unit} = req.body
-    new_measurement = {device_id, kind, value, unit}
-    DataService 'createMeasurement', new_measurement, (err, new_measurement) ->
+    measurement = {device_id, kind, value, unit}
+    DataService 'createMeasurement', measurement, (err, new_measurement) ->
         announce 'maia:create-measurement', {measurement: new_measurement} if !config.LOCAL
         res.json new_measurement


Fix linux release packages names

Fix linux release packages names

--- tasks/pack.coffee
+++ tasks/pack.coffee
@@ -211,7 +211,7 @@
         '--iteration'
         process.env.CIRCLE_BUILD_NUM || '1'
         '--package'
-        './dist/' + manifest.name + '-linux-VERSION-ARCH.' + target
+        './dist/' + manifest.name + '-VERSION-linux-ARCH.' + target
         '-C'
         './build/linux' + arch
         '.'


easier debugging

easier debugging

--- tests/harness.coffee
+++ tests/harness.coffee
@@ -14,7 +14,7 @@
         .append('<br />')
         .appendTo('body')

-    addEventListener = (combo, eventId, list) ->
+    addEventListener = (combo, eventId, list) ->
       combo.input.on eventId, (e, data...) => list.push({ name: eventId, data: data })

     window.render_combo_setup = (id, msg, data) ->
@@ -47,7 +47,7 @@
       { id: i++, text: "my special number is 0.626", true: no },
       { id: i++, text: "my special number is 0.627", true: yes }
     ]
-    window.data_1275.push(id: i++, text: uuid(i), true: j%2 isnt 0) for j in [8..1265]
+    window.data_1275.push(id: i++, text: i+":"+uuid(i), true: j%2 isnt 0) for j in [8..1265]
     window.data_1275.push(id: i++, text: text, true: yes) for text in [
         "Massiv væg mod uopvarmet rum, 12 cm tegl, 250 mm udvendig isolering.  (U: 0.14)",
         "Massiv ydervæg, 12 cm tegl, 250 mm udvendig isolering.  (U: 0.14)",


sending body instead of body.fieldValues

--- server/controllers/konnectors.coffee
+++ server/controllers/konnectors.coffee
@@ -58,7 +58,7 @@
             res.send error: true, msg: 'konnector is already importing', 400
         else
             fields = konnectorHash[req.konnector.slug].fields
-            req.konnector.import req.body.fieldValues, fields, (err) ->
+            req.konnector.import req.body, fields, (err) ->
                 if err
                     next err
                 else


[neo4j] added koding slug into neo4j queries

--- server/lib/server/graph/graph.coffee
+++ server/lib/server/graph/graph.coffee
@@ -4,7 +4,9 @@
 module.exports = class Graph
   constructor:(config)->
     # todo remove hardcoded id
-    @groupId = KD?.getSingleton('groupsController')?.getCurrentGroup()?.getId() or "5150c743f2589b107d000007"
+
+    @groupId   = "5150c743f2589b107d000007"
+    @groupName = "koding"
     @db = new neo4j.GraphDatabase(config.host + ":" + config.port);

   objectify = (incomingObjects, callback)->
@@ -26,7 +28,6 @@
   fetchAll:(startDate, callback)->
     start = new Date().getTime()
     query = [
-      # 'start koding=node:koding(\'id:5150c743f2589b107d000007\')'
       'START koding=node:koding(id={groupId})'
       'MATCH koding-[:member]->members<-[:author]-content'
       'WHERE (content.name = "JTutorial"'
@@ -34,6 +35,8 @@
       ' or content.name = "JDiscussion"'
       ' or content.name = "JBlogPost"'
       ' or content.name = "JStatusUpdate")'
+      ' and has(content.group)'
+      ' and content.group = "' + @groupName + '"'
       ' and has(content.`meta.createdAtEpoch`)'
       ' and content.`meta.createdAtEpoch` < {startDate}'
       'return *'
@@ -46,7 +49,6 @@
       startDate : startDate

     @db.query query, params, (err, results)=>
-
       tempRes = []
       if err then callback err
       else if results.length is 0 then callback null, []
@@ -183,6 +185,7 @@
       'MATCH koding-[:member]->followees<-[r:follower]-follower'
       'where followees.name="JAccount"'
       'and follower.name="JTag"'
+      'and follower.name="' + @groupName + '"'
       'and r.createdAtEpoch < {startDate}'
       'return r,followees, follower'
       'order by r.createdAtEpoch DESC'


workspace: add ability to remove root folder of workspace

--- client/Main/workspacesettingspopup.coffee
+++ client/Main/workspacesettingspopup.coffee
@@ -36,7 +36,7 @@

           return  if KD.showError err

-          KD.getSingleton('mainController').emit "WorkspaceFilesNeedToBeDeleted", workspace.data.machineUId, workspace.data.rootPath if deleteRelatedFiles
+          KD.getSingleton('appManager').tell 'IDE', "deleteWorkspaceRootFolder", workspace.data.machineUId, workspace.data.rootPath if deleteRelatedFiles

           navItem.destroy()
           @destroy()


testrunner: removed unused variables

testrunner: removed unused variables

--- client/testrunner/lib/routehandler.coffee
+++ client/testrunner/lib/routehandler.coffee
@@ -49,7 +49,7 @@

   mocha.ui('bdd')

-  mochaTests = require './require-tests'
+  require './require-tests'

   runner = mocha.run()


Add mappings for sideloading annotations

--- assets/scripts/app/app.coffee
+++ assets/scripts/app/app.coffee
@@ -20,6 +20,8 @@
       accounts:     Travis.Account
       worker:       Travis.Worker
       workers:      Travis.Worker
+      annotation:   Travis.Annotation
+      annotations:  Travis.Annotation
     ).property()

     modelClasses: (->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -95,3 +95,8 @@
   equals color.g(), 34
   equals color.b(), 102
   equals color.a(), 0.4
+
+test "Color should equal colors with the same rbga values", ->
+  color1 = Color(4, 20, 100, 0.5)
+  color2 = Color('rgba(4, 20, 100, 0.5)')
+  ok(color1.equals(color2))


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -17,6 +17,12 @@
   equals color.g(), 66, 'green channel should equal input green value'
   equals color.b(), 134, 'blue channel should equal input blue value'
   equals color.a(), 0.45, 'alpha channel should equal input alpha value'
+
+  color = Color('rgb(4, 66, 134, .45)')
+  equals color.r(), 4, 'red channel should equal input red value'
+  equals color.g(), 66, 'green channel should equal input green value'
+  equals color.b(), 134, 'blue channel should equal input blue value'
+  equals color.a(), 0.45, 'alpha channel should equal input alpha value'

 test "Color should parse hex starting with #", ->
   color = Color('#ffffff')
@@ -24,7 +30,6 @@
   equals color.g(), 255, 'green channel should equal numeric equivalent of ff'
   equals color.b(), 255, 'blue channel should equal numeric equivalent of ff'

-test "Color should parse hex starting without #", ->
   color = Color('ffffff')
   equals color.r(), 255, 'red channel should equal numeric equivalent of ff'
   equals color.g(), 255, 'green channel should equal numeric equivalent of ff'


don't print calendar event location if null

--- app/assets/javascripts/angular_files/components/calendar.api.coffee
+++ app/assets/javascripts/angular_files/components/calendar.api.coffee
@@ -90,7 +90,15 @@
           volunteersNeeded: rawNeed.attributes['volunteers-needed']

         #need.title = need.start.format("H:mm") + " – " + need.end.format("H:mm")
-        need.title = "#{need.volunteersNeeded} #{need.category} volunteers at #{need.location}, #{need.city}"
+        need.title = "#{need.volunteersNeeded} #{need.category}"
+        if need.volunteersNeeded == 1
+          need.title += " volunteer"
+        else
+          need.title += " volunteers"
+        if need.location
+          need.title += " at #{need.location}, #{need.city}."
+        else
+          need.title += ". (no location)"

         need
     new ApiCalendar


sort /select/:program/:courseId classes by time, and return a message that we are doing so.

--- apis/v0p1.coffee
+++ apis/v0p1.coffee
@@ -31,6 +31,10 @@
 data = global.etabits.data
 log = global.etabits.log

+daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
+sortTimedClass = (a, b)->
+	"#{daysOfWeek.indexOf(a.time.day)}#{a.time.hour}".localeCompare("#{daysOfWeek.indexOf(b.time.day)}#{b.time.hour}")
+
 studentsRouter.param 'program', (req, res, next)->
 	req.svu.program = data.programsByCode[req.params.program]
 	next()
@@ -63,7 +67,13 @@
 			_.assign(tt, available[tt.number])
 		res.json {
 			success: true
-			data: results.tutorTime
+			data: results.tutorTime.sort(sortTimedClass)
+			messages: [
+				{
+					type: 'info'
+					text: 'Classes are now sorted by date (Sunday through Saturday), not by class number.'
+				}
+			]
 		}

 studentsRouter.post '/select/:program/:courseId', etabits.jsonMiddleware, (req, res)->


Fixes invalid path.

Fixes invalid path.

--- apps/application/client/model.coffee
+++ apps/application/client/model.coffee
@@ -1,4 +1,4 @@
-Util = require '../../components/Util/index.coffee'
+Util = require '../../../components/Util/index.coffee'

 components = {}


revert apply method

revert apply method

--- src/layout/mixins/programs.coffee
+++ src/layout/mixins/programs.coffee
@@ -84,8 +84,8 @@

     apply: (world, args) ->
       {target, action} = args
-      return unless sprite == target
-      world.call 'store', action
+      return unless world.get 'editable'
+      world.call('store', action) if sprite == target

     store: (world, action) ->
       actions_container = world.find_child('actions')


fix(grunt): wrong `amsf-update` task

fix(grunt): wrong `amsf-update` task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -591,9 +591,13 @@
       grunt.task.run [
         "gitreset:amsf__core__reset_git"
         "gitclean:amsf__core__clean_git"
+        "gitpull:amsf__core__update_remote"
+      ]
+    else
+      grunt.task.run [
+        "gitclone:amsf__core__add_remote"
       ]
     grunt.task.run [
-      "gitclone:amsf__core__add_remote"
       "copy:amsf__core__to_app"
     ]


Update release task

Update release task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -74,18 +74,13 @@
   #

   # Task to tag a version in git
-  grunt.registerTask "git-tag", "Tags a release in git", (version) ->
+  grunt.registerTask "git-tag", "Tags a release in git", ->
     done = this.async()
-    releaseVersion = process.env.RELEASE_VERSION || version
+    releaseVersion = grunt.template.process("<%= pkg.version %>")

-    done(false) unless releaseVersion
-
-    child = exec "git tag v#{releaseVersion}" (error, stdout, stderr) ->
-      if error?
-        console.log("Error running git tag: " + error);
-        done(false)
-      else
-        done()
+    child = exec "git tag v#{releaseVersion}", (error, stdout, stderr) ->
+      console.log("Error running git tag: " + error) if error?
+      done(!error?)

   # Load tasks from plugins
   grunt.loadNpmTasks "grunt-contrib-jshint"
@@ -95,8 +90,7 @@
   grunt.loadNpmTasks "grunt-s3"

   # Release meta-task
-  # lint, update version, minify, tag in git, copy to s3
-  # grunt.registerTask "release", ["jshint", "update-version", "uglify", "git-tag"]
+  grunt.registerTask "release", ["jshint", "uglify", "git-tag", "s3"]

   # Default meta-task
   grunt.registerTask "default", ["jshint", "uglify"]


Include sample theme (regression)

Include sample theme (regression)

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -142,7 +142,7 @@
                 expand: true # required for cwd
                 cwd: "src/"
                 src: [
-                    "skin-**/*.{css,gif,png,less}"
+                    "skin-**/*.{css,gif,md,png,less}"
                     "skin-common.less"
                     "*.txt"
                     ]


remove version from bower.json

remove version from bower.json

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -138,7 +138,6 @@
     bower =
       "name": pkg.name
       "description": pkg.description
-      "version": version()
       "main": bowerjson.main
       "license": pkg.licenses
       "ignore": []


Corrected Bin Reference in Version

Corrected Bin Reference in Version

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -54,12 +54,8 @@
           variables:
             version: '<%= pkg.version %>'
           prefix: '@@'
-        files: [
-          expand: true
-          src: 'build/bin/api.js'
-          dest: '.'
-        ]
-
+        files: [src: 'build/bin/kdc-plus.js', dest: 'build/bin/kdc-plus.js']
+

   grunt.loadNpmTasks 'grunt-contrib-clean'
   grunt.loadNpmTasks 'grunt-contrib-coffee'


Gruntfile passes server.

Gruntfile passes server.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,5 +1,7 @@
 module.exports = (grunt)->
-    require('rupert-grunt')(grunt, config)
+    require('rupert-grunt')(grunt, {
+        server: __dirname + '/app.js'
+    })

     grunt.registerTask 'watcher', [ 'rupert-watch' ]
     grunt.registerTask 'default', [ 'rupert-default' ]


feat(grunt): remove originals after assets being smooshed

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -237,6 +237,7 @@
         inlineSvg: true
         inlineSvgBase64: false
         assetsUrlPrefix: "<%= config.base %>/assets/"
+        deleteOriginals: true

       dist:
         files: [


Changed the js to coffeescript in Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,181 +1,126 @@
-module.exports = function(grunt){
+module.exports = (grunt) ->
+  (require 'load-grunt-tasks')(grunt)
+  growl = require 'growl'

-	require('load-grunt-tasks')(grunt);
-	var growl = require('growl');
+  grunt.initConfig
+    pkg: grunt.file.readJSON 'package.json'
+    nodemon:
+      dev:
+        script: 'build/server/app.js'
+        options:
+          env:
+            PORT: 8888
+          ext: 'js'
+          watch: ['build/server']
+          callback: (nodemon) ->
+            nodemon.on 'log', (event) ->
+              console.log  event.colour
+            nodemon.on 'config:update', ->
+              growl 'Server has started'
+              console.log 'Server has started'
+            nodemon.on 'restart', ->
+              growl 'Server has restarted'
+              console.log 'Server has restarted'
+            nodemon.on 'crash', ->
+              growl 'Server has crashed'
+              console.log 'Server has crashed'
+    copy:
+      js:
+        src: ['**/*.js']
+        dest: 'public/js/'
+        cwd: 'src/client/'
+        expand: true
+      html:
+        src: ['**/*.html']
+        dest: 'public/js/'
+        cwd: 'src/client/'
+        expand: true
+    stylus:
+      compile:
+        files:
+          'public/css/application.css': ['src/client/stylus/application.styl']
+    watch:
+      styles:
+        files: 'src/client/styles/**/*.styl'
+        tasks: ['stylus:compile']
+        options:
+          atBegin: true
+      clientCoffeeCompile:
+        files: 'src/client/**/*.coffee'
+        tasks: ['coffee:client']
+        options:
+          atBegin: true
+      clientJsCopy:
+        files: 'src/client/**/*,js'
+        tasks: ['copy:js']
+        options:
+          atBegin: true
+      clientTmplCopy:
+        files: 'src/client/**/*.html'
+        tasks: ['copy:html']
+        options:
+          atBegin: true
+      serverCoffeeCompile:
+        files: 'src/server/**/*.coffee'
+        tasks: ['coffee:server']
+        options:
+          atBegin: true
+      serverTests:
+        files: ['build/server/**/*.js', 'spec/server/**/*.js']
+        tasks: ['mochaTest:test']
+        options:
+          spawn: false
+    concurrent:
+      target:
+        tasks: ['nodemon', 'watch']
+        options:
+          logConcurrentOutput: true
+    shell:
+      mongo:
+        command: 'mongod'
+        options:
+          async: true
+    monchaTest:
+      test:
+        src: ['spec/server/**/*.js']
+        options:
+          reporter: 'spec'
+          clearRequireCache: true
+    karma:
+      unit:
+        configFile: 'spec/client/karma.conf.js'
+        runnerPort: 9999
+    notify:
+      server:
+        options:
+          message: 'Server has started'
+      restart:
+        options:
+          message: 'Server has restarted'
+      error:
+        options:
+          message: 'Server has crashed'
+    coffee:
+      client:
+        expand: true
+        cwd: 'src/client'
+        src: '**/*.coffee'
+        dest: 'public/js/'
+        ext: '.js'
+      server:
+        expand: true
+        cwd: 'src/server'
+        src: '**/*.coffee'
+        dest: 'build/server'
+        ext: '.js'

-	grunt.initConfig({
-		pkg: grunt.file.readJSON('package.json'),
-		nodemon: {
-			dev:{
-				script: 'build/server/app.js',
-				options: {
-					env: {
-						PORT: 8888
-					},
-					ext: 'js',
-					watch: ['build/server'],
-					callback: function(nodemon){
-						nodemon.on('log', function (event) {
-							console.log(event.colour);
-						});
-						nodemon.on('config:update', function () {
-							growl('Server has started');
-							console.log('Server has started');
-						});
-						nodemon.on('restart', function () {
-							// Delay before server listens on port
-							growl('Server has restarted');
-						});
-						nodemon.on('crash', function () {
-							// Delay before server listens on port
-							growl('Server has crashed');
-							console.log('Server has crashed');
-						});
-					}
-				}
-			}
-		},
-		copy: {
-      js: {
-        src: ['**/*.js'],
-        dest: 'public/js/',
-        cwd: 'src/client/',
-        expand: true
-      },
-			html: {
-				src: ['**/*.html'],
-				dest: 'public/js/',
-				cwd: 'src/client/',
-				expand: true
-			}
-		},
-		stylus: {
-			compile: {
-				files: {
-					'public/css/application.css': ['src/client/styles/application.styl']
-				}
-			}
-		},
-		watch: {
-			styles: {
-				files: 'src/client/styles/*.styl',
-				tasks: ['stylus:compile'],
-				options: {
-					atBegin: true
-				}
-			},
-			clientCoffeCompile: {
-				files: ['src/client/**/*.coffee'],
-				tasks: ['coffee:client'],
-				options: {
-					atBegin: true
-				}
-			},
-			clientJsCopy: {
-				files: ['src/client/**/*.js'],
-				tasks: ['copy:js'],
-				options: {
-					atBegin: true
-				}
-			},
-			clientTmplCopy: {
-				files: ['src/client/**/*.html'],
-				tasks: ['copy:html'],
-				options: {
-					atBegin: true
-				}
-			},
-      serverCoffeCompile: {
-        files: ['src/server/**/*.coffee'],
-        tasks: ['coffee:server'],
-        options: {
-          atBegin: true
-        }
-      },
-			serverTests: {
-				files: ['src/server/**/*.js', 'spec/server/**/*.js'],
-				tasks: ['mochaTest:test'],
-				options: {
-					spawn: false
-				}
-			}
-		},
-		concurrent: {
-			target: {
-				tasks: ['nodemon', 'watch'],
-				options: {
-					logConcurrentOutput: true
-				}
-			}
-		},
-		shell: {
-			mongo: {
-				command: 'mongod',
-				options: {
-					async: true
-				}
-			}
-		},
-		mochaTest: {
-			test: {
-				options: {
-					reporter: 'spec',
-					clearRequireCache: true
-				},
-				src: ['spec/server/**/*.js']
-			}
-		},
-		karma: {
-			unit: {
-				configFile: 'spec/client/karma.conf.js',
-				runnerPort: 9999
-			}
-		},
-		notify: {
-			server: {
-				options: {
-					message: 'Server has started'
-				}
-			},
-			restart: {
-				options: {
-					message: 'Server has restarted'
-				}
-			},
-			error: {
-				options: {
-					message: 'Server has crashed'
-				}
-			}
-		},
-    coffee: {
-      client: {
-        expand: true,
-        cwd: 'src/client/',
-        src: ['**/*.coffee'],
-        dest: 'public/js/',
-        ext: '.js'
-      },
-      server: {
-        expand: true,
-        cwd: 'src/server',
-        src: '**/*.coffee',
-        dest: 'build/server',
-        ext: '.js'
-      }
-    }
-	});

-	// On watch events, if the changed file is a test file then configure mochaTest to only
-	// run the tests from that file. Otherwise run all the tests
-	var defaultTestSrc = grunt.config('mochaTest.test.src');
-	grunt.event.on('watch', function(action, filepath) {
-		console.log('Must run da testz');
-		grunt.config('mochaTest.test.src', defaultTestSrc);
-		if (filepath.match('spec/server')) {
-			grunt.config('mochaTest.test.src', filepath);
-		}
-	});
+  # On watch events, if the changed file is a test file then configure mochaTest to only
+  # run the tests from that file. Otherwise run all the tests
+  defaultTestSrc = grunt.config 'mochaTest.test.src'
+  grunt.event.on 'watch', (action, filepath) ->
+    console.log 'Must run da testz'
+    grunt.config 'mochaTest.test.src', defaultTestSrc

-	grunt.registerTask('default', ['shell:mongo', 'concurrent:target']);
-};+    if filepath.match 'spec/server'
+      grunt.config 'mockaTest.test.src', 'filepath'


Add commented-out mocha:grep options to gruntfile.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -255,7 +255,12 @@
     # Test runner
     # -----------
     mocha:
-      index: 'test/index.html'
+      index:
+        src: ['test/index.html']
+        # options:
+        #   grep: 'autoAttach'
+        #   mocha:
+        #     grep: 'autoAttach'

     # Minify
     # ------


[Activity] added getting timestamp

[Activity] added getting timestamp

--- client/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/app/Applications/Activity.kdapplication/AppController.coffee
@@ -147,7 +147,7 @@
       #@isExempt (exempt)=>
       #if exempt or @getFilter() isnt activityTypes
       groupObj     = KD.getSingleton("groupsController").getCurrentGroup()
-      mydate = new Date((new Date()).setSeconds(0) + 60000)
+      mydate = new Date((new Date()).setSeconds(0) + 60000).getTime()
       options      =
         to         : options.to or mydate #Date.now() we cant cache if we change ts everytime.
         group      :


fixing dist in gruntfile

fixing dist in gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -33,6 +33,7 @@

 # Makes the distribution build.
 taskManager.add 'dist', [
+  'dev',
   'copy:dist'
 ]

@@ -113,7 +114,7 @@
               nonull: true,
               expand: true,
               cwd: 'build/src/',
-              src: ['**/*.html', '**/*.css',  '**/*.js',
+              src: ['**/*.html', '**/*.css',  '**/*.js', '**/*.json',
                     '!**/*.spec.js', '!**/*.spec.static.js'],
               dest: 'build/dist/',
               onlyIf: 'modified'


Add build target for grunt.

Add build target for grunt.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -71,6 +71,7 @@
   grunt.loadNpmTasks('grunt-shell')
   grunt.registerTask('run', ['coffee:compile', 'copy:app', 'package.json',
     'shell:electron'])
+  grunt.registerTask('build', ['coffee:compile', 'copy:lib'])
   grunt.registerTask('test', ['mochaTest'])
   grunt.registerTask('prepublish', ['coffee:compile', 'copy:lib'])
   grunt.registerTask('package', ['coffee:compile', 'copy:app', 'package.json',


feature($Gruntfile): Run `build` task always before testing

Build was not being run before testing. Instead the assumption was that
the tester would have already built from source before testing. This
assumption may be valid, but it's a PITA to remember to rebuild before
each test.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -485,6 +485,7 @@
     # Test
     # ----------
     grunt.registerTask 'test', 'Run headless unit tests', [
+        'build'
         'copy:test'
         'connect:test_server'
         'qunit'
@@ -493,6 +494,7 @@
     # Test on Saucelabs
     # ----------
     grunt.registerTask 'test-sauce', 'Run tests on Saucelabs', [
+        'build'
         'copy:test'
         'connect:test_server'
         'saucelabs-qunit'
@@ -501,6 +503,7 @@
     # Watcher
     # ----------
     grunt.registerTask 'test-watch', 'Run headless unit-tests and re-run on file changes', [
+        'build'
         'test'
         'watch'
     ]


Abstract out post build task in Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -110,18 +110,18 @@
         command: 'bower install'
       npm_install:
         command: 'cd dist && npm install --production && cd -'
+
+      osx: (bits) ->
+        """
+          cp assets/icons/nw.icns builds/tdd-reminder/osx#{bits}/tdd-reminder.app/Contents/Resources/
+          cp -r assets/icons/Icon builds/tdd-reminder/osx#{bits}/tdd-reminder.app/Contents/
+          cd builds/tdd-reminder/osx#{bits}/ && mv tdd-reminder.app 'TDD Reminder.app'
+          zip -r tdd-reminder-osx#{bits}-v<%= pkg.version %>.zip *.app && cd -
+        """
       osx64:
-        command: """
-          cp assets/icons/nw.icns builds/tdd-reminder/osx64/tdd-reminder.app/Contents/Resources/ &&
-          cp -r assets/icons/Icon builds/tdd-reminder/osx64/tdd-reminder.app/Contents/ &&
-          cd builds/tdd-reminder/osx64/ && zip -r tdd-reminder-osx64-v<%= pkg.version %>.zip *.app && cd -
-        """
+        command: "<%= shell.osx('64') %>"
       osx32:
-        command: """
-          cp assets/icons/nw.icns builds/tdd-reminder/osx32/tdd-reminder.app/Contents/Resources/ &&
-          cp -r assets/icons/Icon builds/tdd-reminder/osx32/tdd-reminder.app/Contents/
-          cd builds/tdd-reminder/osx32/ && zip -r tdd-reminder-osx32-v<%= pkg.version %>.zip *.app && cd -
-        """
+        command: "<%= shell.osx('32') %>"

     nodewebkit:
       options:


fix(grunt): duplicated task

fix(grunt): duplicated task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -570,17 +570,12 @@
     "theme-upgrade"
   ]

-  grunt.registerTask "amsf-update", "Upgrade AM", [
-    "gitreset:amsf__core__reset_git"
-    "gitclean:amsf__core__clean_git"
-    "gitpull:amsf__core__update_remote"
-    "copy:amsf__core__to_app"
-  ]
-
   grunt.registerTask "amsf-update", "Update ASMF", ->
     # TODO: need better implement
     if grunt.file.exists("_amsf/core/")
       grunt.task.run [
+        "gitreset:amsf__core__reset_git"
+        "gitclean:amsf__core__clean_git"
         "gitpull:amsf__core__update_remote"
         "copy:amsf__core__to_app"
       ]


Landing: send _csrf in confirm request

Landing: send _csrf in confirm request

--- client/landing/site.landing/coffee/team/tabs/teamusernametab.coffee
+++ client/landing/site.landing/coffee/team/tabs/teamusernametab.coffee
@@ -83,7 +83,8 @@
           track 'succeeded to create a team'
           KD.utils.clearTeamData()
           { protocol, host } = location
-          location.href      = "#{protocol}//#{slug}.#{host}/-/confirm?token=#{data.token}"
+          csrfToken          = encodeURIComponent Cookies.get '_csrf'
+          location.href      = "#{protocol}//#{slug}.#{host}/-/confirm?token=#{data.token}&_csrf=#{csrfToken}"
         error : ({responseText}) =>
           if /TwoFactor/.test responseText
             track 'requires two-factor authentication'


Fixed indent

Fixed indent

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,61 +1,61 @@
 module.exports = (grunt)->

-    require('load-grunt-tasks')(grunt)
+  require('load-grunt-tasks')(grunt)

-    grunt.registerTask('default', ['typescript', 'uglify', 'concat', 'clean', 'compass'])
-    grunt.registerTask('server', ['connect'])
+  grunt.registerTask('default', ['typescript', 'uglify', 'concat', 'clean', 'compass'])
+  grunt.registerTask('server', ['connect'])

-    grunt.initConfig({
-        pkg: grunt.file.readJSON('package.json')
+  grunt.initConfig({
+    pkg: grunt.file.readJSON('package.json')

-        uglify:
-            typesc:
-                files: 'qflipper.min.js': ['qflipper.js']
+    uglify:
+      typesc:
+        files: 'qflipper.min.js': ['qflipper.js']

-        concat:
-            license:
-                src: ['license/mit.js', 'qflipper.js']
-                dest: 'qflipper.js'
+    concat:
+      license:
+        src: ['license/mit.js', 'qflipper.js']
+        dest: 'qflipper.js'

-            licenseMin:
-                src: ['license/mit.min.js', 'qflipper.min.js']
-                dest: 'qflipper.min.js'
+      licenseMin:
+        src: ['license/mit.min.js', 'qflipper.min.js']
+        dest: 'qflipper.min.js'

-            options:
-                separator: ';'
+      options:
+        separator: ';'

-        typescript:
-            base:
-                src: ['src/**/*.ts']
-                dest: 'qflipper.js'
-                options:
-                    sourceMap: true
+    typescript:
+      base:
+        src: ['src/**/*.ts']
+        dest: 'qflipper.js'
+        options:
+          sourceMap: true

-        compass:
-            dist:
-                options:
-                    config: 'config.rb'
+    compass:
+      dist:
+        options:
+          config: 'config.rb'

-        watch:
-            ts:
-                files: ['src/**/*.ts']
-                tasks: ['typescript', 'uglify', 'concat', 'clean']
-                options:
-                    atBegin: true
+    watch:
+      ts:
+        files: ['src/**/*.ts']
+        tasks: ['typescript', 'uglify', 'concat', 'clean']
+        options:
+          atBegin: true

-            css:
-                files: ['sass/**/*.scss']
-                tasks: ['compass']
-                options:
-                    atBegin: true
+      css:
+        files: ['sass/**/*.scss']
+        tasks: ['compass']
+        options:
+          atBegin: true

-        clean: ['src/**/*.js']
+    clean: ['src/**/*.js']

-        connect:
-            server:
-                options:
-                    port: 8000
-                    base: './'
-                    keepalive: true
+    connect:
+      server:
+        options:
+          port: 8000
+          base: './'
+          keepalive: true

-    })
+  })


use double quotes

use double quotes

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -23,7 +23,7 @@
         banner: '/*!\n' +
           ' * <%= pkg.name %> v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
           ' * <%= pkg.homepage %>\n' +
-          ' * Copyright (c) <%= grunt.template.today(\'yyyy\') %> <%= pkg.author %>\n' +
+          ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
           ' * Released under the <%= pkg.licenses[0].type %> license\n' +
           ' * <%= pkg.licenses[0].url %>\n' +
           ' */\n'


grunt test: Allow to specify TESTS

grunt test: Allow to specify TESTS

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -38,6 +38,7 @@
         options:
           reporter: 'spec'
           require: 'coffee-script/register'
+          grep: process.env.TESTS

   # Grunt plugins used for building
   @loadNpmTasks 'grunt-exec'


fix messaging area height.

fix messaging area height.

--- chat/grails-app/assets/javascripts/chat.coffee
+++ chat/grails-app/assets/javascripts/chat.coffee
@@ -11,7 +11,7 @@
 today = moment().format("YYYY/MM/DD")
 lastNotified = startTime
 windowHeight = $(window).height() - 127 # (+ 50 34 43) 127
-areaTimelineShort = windowHeight - 328
+areaTimelineShort = windowHeight - 331
 areaTimelineLong  = windowHeight - 60


troubleshoot: DRY

troubleshoot: DRY

--- client/Main/troubleshoot/brokerrecovery.coffee
+++ client/Main/troubleshoot/brokerrecovery.coffee
@@ -15,6 +15,8 @@
     @broker.on "ready", =>
       @emit "brokerConnected"

+  brokerURL:->
+    @broker.sockURL.replace("/subscribe", "")

   checkStatus: ->
     {timeout} = @getOptions()
@@ -23,7 +25,7 @@
         @unsuccessfulAttempt++
         @emit "brokerNotResponding"

-        brokerURL = @broker.sockURL.replace("/subscribe")
+        brokerURL = @brokerURL()
         KD.utils.warnAndLog 'broker not responding', {@unsuccessfulAttempt, brokerURL}

       @broker.ping =>
@@ -35,8 +37,7 @@
     # checking for the previous unsuccessful attempts
     @unsuccessfulAttempt = 0
     @broker.disconnect no
-    brokerURL = @broker.sockURL.replace("/subscribe", "")
-    @broker.selectAndConnect [brokerURL]
+    @broker.selectAndConnect [@brokerURL()]

   recover: (callback) ->


Client: Activity: Added new actiontypes to add delete flag for selected dropdown user

--- client/app/lib/flux/actions/actiontypes.coffee
+++ client/app/lib/flux/actions/actiontypes.coffee
@@ -18,4 +18,7 @@
   'BLOCK_USER_BEGIN'
   'BLOCK_USER_SUCCESS'
   'BLOCK_USER_FAIL'
+
+  'SET_CREATE_CHANNEL_PARTICIPANT_DELETE_FLAG'
+  'UNSET_CREATE_CHANNEL_PARTICIPANT_DELETE_FLAG'
 }


Charts now utilize a string for the independent variable, thus fixing weird port stat graphs

--- client/templates/results/chart/chart.js.coffee
+++ client/templates/results/chart/chart.js.coffee
@@ -1,6 +1,10 @@
 Template.chart.helpers
   nonNumberDataColumnSpec: ->
+    for spec2 in @header.slice(0,1)
+      console.log spec2
+      spec2.chartType = "string"
     for spec in @header.slice(1)
+      console.log spec
       if spec.chartType isnt "number"
         return spec
   reactivityHack: ->
@@ -21,8 +25,12 @@
           spec = @header[index]
           if @output is "rwstats" and (spec.isPercentage or spec.name is "cumul_%")
             continue
+          if index == 0
+            value = value.toString() # guarantees that field value is a string
+            #test = typeof value
           values.push(value)
         data.addRow(values)
+        console.log values
       chartWrapper = share.chartWrappers.get(@_id)
       chartWrapper.setDataTable(data)
       chartWrapper.setChartType(@chartType)