Add an ember test for editable transition.

--- test/javascripts/integration/edit_paper_test.js.coffee
+++ test/javascripts/integration/edit_paper_test.js.coffee
@@ -188,3 +188,12 @@
       click '#task_completed'
       click '.overlay-close-button:first'
   .then -> ok !find('a:contains("Submit")').hasClass 'button--disabled'
+
+test 'on paper.edit when paper.editable changes, user transitions to paper.index', ->
+  visit '/papers/93412/edit'
+  .then ->
+    Ember.run ->
+      getStore().getById('paper', 93412).set('editable', false)
+  andThen ->
+    ok !exists find('.button-primary:contains("Submit")')
+    equal currentRouteName(), "paper.index"


Tweak code-of-conduct message (trying to link to #presentaciones channel)

--- scripts/welcome-to-zihub.coffee
+++ scripts/welcome-to-zihub.coffee
@@ -61,5 +61,5 @@
       add_nicks user.name

   robot.respond /la bienvenida/i, (msg) ->
-#    msg.reply welcomeMsg('')
+    msg.reply welcomeMsg('')
     robot.adapter.client.chat.postMessage(msg.room, welcomeMsg(''), {as_user: true, link_names: true})


StaticPage: get post, topic, channel, and profile content in different methods

--- server/lib/crawler/index.coffee
+++ server/lib/crawler/index.coffee
@@ -1,63 +1,95 @@
-{ isLoggedIn, error_404, error_500 } = require '../server/helpers'
-{htmlEncode}                         = require 'htmlencode'
-kodinghome                           = require './staticpages/kodinghome'
-activity                             = require './staticpages/activity'
-crawlableFeed                        = require './staticpages/feed'
-profile                              = require './staticpages/profile'
-{Relationship}                       = require 'jraphical'
+{ error_404, error_500 } = require '../server/helpers'
+{htmlEncode}             = require 'htmlencode'
+kodinghome               = require './staticpages/kodinghome'
+activity                 = require './staticpages/activity'
+feed                     = require './staticpages/feed'
+profile                  = require './staticpages/profile'
+{argv}                   = require 'optimist'
+{uri}                    = require('koding-config-manager').load("main.#{argv.c}")
+{createActivityContent}  = require './helpers'

-{
-  forceTwoDigits
-  formatDate
-  getFullName
-  getNickname
-  getUserHash
-  createActivityContent
-  decorateComment
-}          = require './helpers'
+notFoundError = (name) ->
+  {description: "invalid #{name}", error: 'koding.NotFoundError'}

-fetchLastStatusUpdatesOfUser = (account, Relationship, JNewStatusUpdate, callback) ->
-  {daisy} = require "bongo"
-  return callback null, null  unless account?._id
-  originId = account._id

-  feedOptions  =
-    sort       : 'timestamp' : -1
-    limit      : 20
+fetchProfileContent = (models, options, callback) ->
+  {client, name} = options
+  {JAccount, SocialChannel} = models
+  JAccount.one "profile.nickname": name, (err, account) ->
+    return callback err  if err or not account

-  selector     =
-    targetId   : originId
-    targetName : "JAccount"
-    sourceName : "JNewStatusUpdate"
-    as         : "author"
-    data       :          # we should filter by group because when the group is
-      group    : "koding" # private publishing on profile page will cause data leak ~EA
+    fetchOptions =
+      targetId : account.socialApiId
+      limit    : 5

-  Relationship.some selector, feedOptions, (err, relationships)->
-    return callback err, null  if err
-    return callback null, null  unless relationships?.length > 0
-    queue = [0..relationships.length - 1].map (index)=>=>
-      rel = relationships[index]
-      queue.next  unless rel?.sourceId?
-      sel =
-        _id        : rel.sourceId
-        originType : "JAccount"
-      JNewStatusUpdate.one sel, {}, (error, statusUpdate)=>
-        queue.next()  if error
-        queue.next()  unless statusUpdate
-        queue.statusUpdates or= []
-        queue.statusUpdates.push statusUpdate
-        queue.next()
-    queue.push =>
-      return callback null, queue.statusUpdates
-    daisy queue
+    SocialChannel.fetchProfileFeed client, fetchOptions, (err, result) ->
+      return callback err  if err or not result
+      feed.buildContent models, result, options, (err, content) ->
+        return callback err  if err or not content
+        callback null, profile account, content

-isInAppRoute = (firstLetter)->
-  # user nicknames can start with numbers
-  intRegex = /^\d/
-  return false if intRegex.test firstLetter
-  return true  if firstLetter.toUpperCase() is firstLetter
-  return false
+
+fetchPostContent = (models, options, callback) ->
+  {SocialMessage} = models
+  {client, entrySlug} = options
+  SocialMessage.bySlug client, slug: entrySlug, (err, activity) ->
+    return callback err  if err or not activity
+
+    createActivityContent models, activity, (err, content, activityContent)->
+      return callback err  if err or not content
+      graphMeta =
+        title    : "Post on koding.com by #{activityContent.fullName}"
+        body     : "#{activityContent.body}"
+        shareUrl : "#{uri.address}/Activity/Post/#{activityContent.slug}"
+      fullPage = feed.putContentIntoFullPage content, "", graphMeta
+      callback null, fullPage
+
+
+fetchTopicContent = (models, options, callback) ->
+  {client, entrySlug} = options
+
+  {SocialChannel} = models
+  SocialChannel.byName client, name: entrySlug, (err, channel) ->
+    return callback err  if err or not channel
+
+    options.channelId = channel.channel.id
+    feed.createFeed models, options, callback
+
+
+fetchGroupContent = (models, options, callback) ->
+  {entrySlug, client, page} = options
+  entrySlug |= "koding"
+  {JGroup} = models
+
+  JGroup.one slug: "koding", (err, group) ->
+    return callback err  if err
+    return callback notFoundError "group"  unless group
+
+    options.channelId = group.socialApiChannelId
+    feed.createFeed models, options, callback
+
+
+fetchContent = (models, options, callback) ->
+  {section, entrySlug, client} = options
+
+  switch section
+    when "Public"
+      options.entrySlug = "koding"
+      fetchGroupContent models, options, callback
+    when "Topic"
+      fetchTopicContent models, options, callback
+    when "Post"
+      fetchPostContent models, options, callback
+    else
+      return fetchProfileContent models, options, callback  if options.isProfile
+      return callback notFoundError "section"
+
+
+getPage = (query) ->
+  {page}  = query
+  page = parseInt( page, 10 );
+  page   or= 1
+

 module.exports =
   crawl: (bongo, {req, res, slug, isProfile}) ->


Upgrade to analytics.js

Upgrade to analytics.js

--- src/scripts/helpers/handlers/analytics.coffee
+++ src/scripts/helpers/handlers/analytics.coffee
@@ -8,25 +8,25 @@
   return new class AnalyticsHandler
     constructor: () ->
       # Setup temporary analytics.js objects
-      # window.GoogleAnalyticsObject = 'ga'
-      # window.ga = () -> (window.ga.q ?= []).push(arguments)
-      # window.ga.l = 1 * new Date()
+      window.GoogleAnalyticsObject = 'ga'
+      window.ga = () -> (window.ga.q ?= []).push(arguments)
+      window.ga.l = 1 * new Date()

       # Initialize analytics.js account
-      # window.ga('create', settings.analyticsID, 'auto')
+      window.ga('create', settings.analyticsID, 'auto')

       # ## Setup ga.js
-      window._gaq ?= []
+      #window._gaq ?= []

       # Asynchronously load analytics.js.
-      # require(['https://www.google-analytics.com/analytics.js'])
+      require(['https://www.google-analytics.com/analytics.js'])

       # Asynchronously load ga.js
-      require(['https://www.google-analytics.com/ga.js'])
+      #require(['https://www.google-analytics.com/ga.js'])

     # Wrapper functions to add analytics events
-    # ga: () -> window.ga?.apply(@, arguments) # analytics.js
-    gaq: () -> window._gaq?.push(arguments[0], arguments[1]) # ga.js
+    ga: () -> window.ga?.apply(@, arguments) # analytics.js
+    #gaq: () -> window._gaq?.push(arguments[0], arguments[1]) # ga.js

     # Send the current page to every analytics service
     send: (account, fragment = Backbone.history.fragment) ->
@@ -35,5 +35,5 @@
       # Use the default analytics ID in settings if no account is specified
       account ?= settings.analyticsID

-      # @ga('send', 'pageview')
-      @gaq(['_setAccount', account], ['_trackPageview', fragment])
+      @ga('send', 'pageview')
+      #@gaq(['_setAccount', account], ['_trackPageview', fragment])


Server: removed test log

Server: removed test log

--- servers/lib/server/index.coffee
+++ servers/lib/server/index.coffee
@@ -589,7 +589,7 @@

       return next()  if err

-      return res.status(200)#.send content
+      return res.status(200).send content


Remove unused method

Remove unused method

--- src/trix/controllers/editor_controller.coffee
+++ src/trix/controllers/editor_controller.coffee
@@ -154,9 +154,6 @@

   compositionControllerWillUpdateAttachment: (attachment) ->
     @editor.recordUndoEntry("Edit Attachment", context: attachment.id, consolidatable: true)
-
-  compositionControllerWillUpdateAttachments: (attachments) ->
-    @editor.recordUndoEntry("Edit Attachments", context: attachments.map((a) -> a.id), consolidatable: true)

   compositionControllerDidRequestRemovalOfAttachment: (attachment) ->
     @removeAttachment(attachment)


minor enhances

minor enhances

--- router.coffee
+++ router.coffee
@@ -47,6 +47,31 @@
     '.rb':   'text/x-ruby'

+  dispatch = (req, res) ->
+    done = false
+    parsed = urlparse(req.url)
+    pathname = parsed.pathname
+    req.get = if parsed.query? then querystring.parse(parsed.query) else {}
+    req.body = _extend {}, req.get
+    method = req.method.toLowerCase()
+    for route in dispatch.routes[method]
+      m = pathname.match(route.pattern)
+      if m isnt null
+        if route.params
+          req.params = {}
+          args = m.slice(1)
+          for param, index in route.params
+            req.params[param] = args[index]
+        return route.handler(req, res)
+    if pathname is "/"
+      return dispatch.static "/index.html", res
+    if dispatch.serve_static
+      return dispatch.static pathname, res
+    else
+      return dispatch._404 req, res, pathname
+
+  dispatch.version = '0.1.4'
+
   _dirlist_template = """
       <!DOCTYPE  html>

@@ -63,7 +88,7 @@
               <%= @cwd_contents %>

-
Served by Node Simple Router v#{dispatch.version}

+
Served by Node Simple Router v#{dispatch.version}

       """
@@ -117,32 +142,6 @@
         req.body = _extend req.body, req.post
         cb(req, res)
     wrapper
-
-  dispatch = (req, res) ->
-    done = false
-    parsed = urlparse(req.url)
-    pathname = parsed.pathname
-    req.get = if parsed.query? then querystring.parse(parsed.query) else {}
-    req.body = _extend {}, req.get
-    method = req.method.toLowerCase()
-
-    for route in dispatch.routes[method]
-      m = pathname.match(route.pattern)
-      if m isnt null
-        if route.params
-          req.params = {}
-          args = m.slice(1)
-          for param, index in route.params
-            req.params[param] = args[index]
-        return route.handler(req, res)
-    if pathname is "/"
-       return dispatch.static "/index.html", res
-    if dispatch.serve_static
-      return dispatch.static pathname, res
-    else
-      return dispatch._404 req, res, pathname
-
-  dispatch.version = '0.1.4'

   dispatch.static = (pathname, res) ->
     full_path = "#{dispatch.static_route}#{pathname}"


#11 #done 测试环境下，有一个默认的用户

#11 #done 测试环境下，有一个默认的用户

--- router.coffee
+++ router.coffee
@@ -100,7 +100,7 @@
 requestPermission = (method, router, req, res)->
   #非产品环境下不检查权限
   #不要加这行，如果使用这行，用户退出后还能登录
-  #return true if process.env.NODE_ENV isnt 'production'
+  return true if process.env.NODE_ENV isnt 'production'

   #检查是否忽略权限检查
   return true if  _.indexOf(router.anonymity || [], method) >= 0
@@ -109,6 +109,14 @@
   return req.session.member_id || _common.response401(res)

 module.exports = (app)->
+  ###
+  #测试环境下，在所有路由之前，设置一个session id为1
+  if process.env.NODE_ENV is 'development'
+    app.all '*', (req, res, next) ->
+      req.session.member_id = 1
+      next()
+  ###
+
   #首页
   app.get '/', (req, res, next)->
     res.sendfile 'static/index.html'


Reduce sizes of inspector charts to prevent scrollbars in win/ff.

--- h2o-web/src/main/steam/scripts/frame-view.coffee
+++ h2o-web/src/main/steam/scripts/frame-view.coffee
@@ -1,3 +1,6 @@
+localConfig =
+  chartWidth: 275
+
 significantDigitsBeforeDecimal = (value) -> 1 + Math.floor Math.log(Math.abs value) / Math.LN10

 formatToSignificantDigits = (digits, value) ->
@@ -398,7 +401,7 @@
         histogram = computeHistogram column, 32
         appendHistogram = ($element) ->
           $element.empty().append renderHistogram histogram,
-            width: 300
+            width: localConfig.chartWidth
             height: 100
             margin:
               top: 0
@@ -414,7 +417,7 @@
         boxplot = computeBoxplot frame.default_pctiles, column
         appendBoxplot = ($element) ->
           $element.empty().append renderBoxplot boxplot,
-            width: 300
+            width: localConfig.chartWidth
             height: 70
             margin:
               top: 5
@@ -450,7 +453,7 @@

     appendCharacteristics = ($element) ->
       $element.empty().append renderCharacteristics characteristics,
-        width: 300
+        width: localConfig.chartWidth
         height: 10
         margin:
           top: 0


added Haggler

added Haggler

--- basicAI.coffee
+++ basicAI.coffee
@@ -297,6 +297,7 @@
     "Remake"   # has a low priority so it'll mostly be played early in the game
     "Adventurer"
     "Harvest"
+    "Haggler" # probably needs to make sure the gained card will be wanted
     "Explorer"
     "Woodcutter"
     "Nomad Camp"


Oghma/app.coffee: Minor cleanup.

Oghma/app.coffee: Minor cleanup.

--- Oghma/app.coffee
+++ Oghma/app.coffee
@@ -211,9 +211,9 @@

     # Allow right clicks to hit nodes.
     window.oncontextmenu = ( event ) ->
-        event.preventDefault()
-        event.stopPropagation()
-        false
+      event.preventDefault()
+      event.stopPropagation()
+      false

     # Console
     @console = Ext.create( 'Oghma.Ext.Console',


Fix multiple ^{username} mentions

Fix multiple ^{username} mentions

--- assets/javascripts/helpers/url.js.coffee
+++ assets/javascripts/helpers/url.js.coffee
@@ -72,25 +72,26 @@
         _entities[entity] = true

     if TentStatus.config.tent_host_domain
-      m = text.match(
-        new RegExp("[\\^]([a-z0-9]+(?:\.#{TentStatus.config.tent_host_domain})?)(?=[\\W]|\\$)")
-      )
-      if entity = m?[1]
-        original_text = entity
-        _length = original_text.length
-        _indices = TentStatus.Helpers.substringIndices(text, original_text)
-        if entity.match(new RegExp(TentStatus.config.tent_host_domain))
-          entity = "https://#{entity}"
-        else
-          entity = "https://#{entity}.#{TentStatus.config.tent_host_domain}"
+      _regex = new RegExp("[\\^]([a-z0-9]{2,}(?:\.#{TentStatus.config.tent_host_domain})?)(?=[\\W]|$)")
+      _offset = 0
+      while m = _regex.exec(text.substr(_offset, text.length))
+        if entity = RegExp.$1
+          _offset += text.substr(_offset, text.length).indexOf(entity)
+          original_text = entity
+          _length = original_text.length
+          _indices = TentStatus.Helpers.substringIndices(text, original_text)
+          if entity.match(new RegExp(TentStatus.config.tent_host_domain))
+            entity = "https://#{entity}"
+          else
+            entity = "https://#{entity}.#{TentStatus.config.tent_host_domain}"

-        _mentions.push {
-          entity: entity
-          text: original_text
-          url: entity
-          indices: _indices
-        }
-        _entities[entity] = true
+          _mentions.push {
+            entity: entity
+            text: original_text
+            url: entity
+            indices: _indices
+          }
+          _entities[entity] = true

     _mentions


Use pre-wrap for displaing plain text

Use pre-wrap for displaing plain text

Testing a couple of mail clients it appears that consecutive whitespace
is usually respected, so using pre-wrap seems sensible. Also means we
don't need to replace newlines with explicit break tags.

--- assets/javascripts/mailcatcher.js.coffee
+++ assets/javascripts/mailcatcher.js.coffee
@@ -292,11 +292,11 @@
         text = text.replace(/</g, "<")
         text = text.replace(/>/g, ">")
         text = text.replace(/"/g, """)
-        text = text.replace(/\n/g, "
")

         # Autolink text
         text = text.replace(/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?)/g, """$1""")
-        message_iframe.find("html").html("#{text}")
+
+        message_iframe.find("html").html("""#{text}""")

   refresh: ->
     $.getJSON "messages", (messages) =>


No use in catching and hiding the error as of now

--- cilantro/static/cilantro/scripts/coffeescript/mediator.coffee
+++ cilantro/static/cilantro/scripts/coffeescript/mediator.coffee
@@ -22,10 +22,7 @@
             for handler in handlers
                 # Catch any errors, allow all handlers to finish prior to
                 # throwing the exception.
-                try
-                    if handler then handler args...
-                catch error
-                    setTimeout -> throw error
+                if handler then handler args...
             setTimeout -> channels[channel] = _.compact handlers
             return


Document loadHistory

Document loadHistory

--- src/adapters/shell.coffee
+++ src/adapters/shell.coffee
@@ -75,6 +75,9 @@
        else
          @shutdown()

+  # Private: load history from .hubot_history.
+  #
+  # callback - A Function that is called with the loaded history items (or an empty array if there is no history)
   loadHistory: (callback) ->
     fs.exists historyPath, (exists) ->
       if exists


Cleanup unused code and coffeescript formatting

--- homedisplay/info_timers/static/js/custom_timers.coffee
+++ homedisplay/info_timers/static/js/custom_timers.coffee
@@ -18,10 +18,13 @@
     c = duration_elem.data("content")
     seconds = parseInt(c.substr(0, 3)) * 3600 + parseInt(c.substr(3, 2)) * 60 + parseInt(c.substr(5, 2))
     if seconds == 0
+      # If no time is set, try getting duration for fixed-length items
       seconds = jq(this).data("duration")

     if seconds?
-      jq.post("/homecontroller/timer/create", {name: name, duration: seconds})
+      jq.post "/homecontroller/timer/create",
+        name: name
+        duration: seconds

     modal_elem.find(".close").click()

@@ -29,14 +32,14 @@
   processLabels = (data) ->
     clearLabels()
     jq.each data.labels, ->
-      jq(".custom-timer-labels").append "
#{this}
"
+      jq(".custom-timer-labels").append "
#{@}
"

     jq.each data.timed_labels, ->
-      jq(".timed-custom-timer-labels").append "
#{this.label}
"
+      jq(".timed-custom-timer-labels").append "
#{@label}
"

     modal_elem.find(".timer-description-button").on "click", ->
       content_switch.userActivity()
-      name = jq(this).html()
+      name = jq(@).html()
       submitTimer(name)

   updateLabels = ->
@@ -57,75 +60,62 @@
     d = content.substring(0, index_start) + data + content.substring(index_start + length)
     return d

-  nullTimeField = (content, field) ->
-    replace_string = ""
-    #TODO: validate this
-    for i in [0..time_spec["#{field}_l"]]
-      replace_string += "0"
-
-    return setTimeField(content, field, replace_string)
-
   processButton = (content) ->
     current_content = duration_elem.data("content")
-    if content.substring(0, 6) == "clear-"
-      # backspace
-      clear_field = content.substring(6)
-      current_content = nullTimeField(current_content, clear_field)
-    else
-      data = parseInt(content.substring(0, content.length - 1))
-      field = content.substring(content.length - 1)
-      field_data = parseInt(getTimeField(current_content, field))
-      field_data += data
-      if field_data < 0
-        hours_field = parseInt(getTimeField(current_content, "h"))
-        minutes_field = parseInt(getTimeField(current_content, "m"))
-        if field == "s"
-          if minutes_field == 0 and hours_field > 0
-            # Take 1h to minutes field.
-            hours_field -= 1
-            current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))
-            minutes_field += 60
+    data = parseInt(content.substring(0, content.length - 1))
+    field = content.substring(content.length - 1)
+    field_data = parseInt(getTimeField(current_content, field))
+    field_data += data
+    if field_data < 0
+      hours_field = parseInt(getTimeField(current_content, "h"))
+      minutes_field = parseInt(getTimeField(current_content, "m"))
+      if field == "s"
+        if minutes_field == 0 and hours_field > 0
+          # Take 1h to minutes field.
+          hours_field -= 1
+          current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))
+          minutes_field += 60

-          if minutes_field > 0
-            # Take 1 minute to seconds field.
-            minutes_field -= 1
-            field_data += 60
-            current_content = setTimeField(current_content, "m", zeroPad(minutes_field, time_spec.m_l))
-          else
-            field_data = 0
-
-        else if field == "m"
-          if hours_field > 0
-            # Take 1 hour to minutes field.
-            hours_field -= 1
-            field_data += 60
-            current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))
-          else
-            field_data = 0
+        if minutes_field > 0
+          # Take 1 minute to seconds field.
+          minutes_field -= 1
+          field_data += 60
+          current_content = setTimeField(current_content, "m", zeroPad(minutes_field, time_spec.m_l))
         else
           field_data = 0

-      if (field == "s" or field == "m") and field_data >= 60
-        if field == "s"
-          minutes_field = parseInt(getTimeField(current_content, "m"))
-          minutes_field += 1
-          field_data -= 60
-          if minutes_field >= 60
-            minutes_field -= 60
-            hours_field = parseInt(getTimeField(current_content, "h"))
-            hours_field += 1
-            current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))
+      else if field == "m"
+        if hours_field > 0
+          # Take 1 hour to minutes field.
+          hours_field -= 1
+          field_data += 60
+          current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))
+        else
+          field_data = 0
+      else
+        field_data = 0

-          current_content = setTimeField(current_content, "m", zeroPad(minutes_field, time_spec.m_l))
-
-        if field == "m"
-          field_data -= 60
-          hours_field = parseInt getTimeField(current_content, "h")
+    if (field == "s" or field == "m") and field_data >= 60
+      if field == "s"
+        minutes_field = parseInt(getTimeField(current_content, "m"))
+        minutes_field += 1
+        field_data -= 60
+        if minutes_field >= 60
+          minutes_field -= 60
+          hours_field = parseInt(getTimeField(current_content, "h"))
           hours_field += 1
           current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))

-      field_data = zeroPad field_data, time_spec[field + "_l"]
-      current_content = setTimeField current_content, field, field_data
+        current_content = setTimeField(current_content, "m", zeroPad(minutes_field, time_spec.m_l))
+
+      if field == "m"
+        field_data -= 60
+        hours_field = parseInt getTimeField(current_content, "h")
+        hours_field += 1
+        current_content = setTimeField(current_content, "h", zeroPad(hours_field, time_spec.h_l))
+
+    field_data = zeroPad field_data, time_spec[field + "_l"]
+    current_content = setTimeField current_content, field, field_data

     duration_elem.data "content", current_content
     c = current_content
@@ -144,7 +134,7 @@

   modal_elem.find(".add-timer-button").on "click", ->
     content_switch.userActivity()
-    content = jq(this).data("content").trim()
+    content = jq(@).data("content").trim()
     processButton content

   jq(".add-custom-timer-plus").on "click", ->
@@ -157,17 +147,16 @@
   ge_refresh.register "timer-labels", updateLabels
   ws_generic.register "timer-labels", processLabels

-  this.showCustomTimer = showCustomTimer
-  this.closeCustomTimer = closeCustomTimer
-  this.submitTimer = submitTimer
-  this.processButton = processButton
-  this.setTimeField = setTimeField
-  this.getTimeField = getTimeField
-  this.nullTimeField = nullTimeField
+  @showCustomTimer = showCustomTimer
+  @closeCustomTimer = closeCustomTimer
+  @submitTimer = submitTimer
+  @processButton = processButton
+  @setTimeField = setTimeField
+  @getTimeField = getTimeField

   return this

 jq =>
-  this.custom_timer = new CustomTimer
+  @.custom_timer = new CustomTimer
     duration_elem: "#custom-timer-duration"
     modal_elem: "#add-custom-timer"


changed color configuration read by shorter statement

--- src/alerts/hipchat.coffee
+++ src/alerts/hipchat.coffee
@@ -17,7 +17,7 @@
   sendToHipChat: (message) ->
     options =
       message: message
-      color: if @config.color then @config.color else 'yellow'
+      color: @config.color or 'yellow'
     @api.notify @config.room, options, (err, response) ->
       if err
         throw new Error "[Hipchat] Couldn't send message to room: #{err}"


Pass clientId to doReset to make login after password recovery work.

--- client/landing/app/MainApp/login/loginview.coffee
+++ client/landing/app/MainApp/login/loginview.coffee
@@ -114,7 +114,9 @@

     @resetForm = new ResetInlineForm
       cssClass : "login-form"
-      callback : (formData)=> @doReset formData
+      callback : (formData)=>
+        formData.clientId = $.cookie('clientId')
+        @doReset formData

     @requestForm = new RequestInlineForm
       cssClass : "login-form"
@@ -217,12 +219,12 @@
     {{> @backToHomeLink}}
     """

-  doReset:({recoveryToken, password})->
+  doReset:({recoveryToken, password, clientId})->
     KD.remote.api.JPasswordRecovery.resetPassword recoveryToken, password, (err, username)=>
       @resetForm.button.hideLoader()
       @resetForm.reset()
       @animateToForm 'login'
-      @doLogin {username, password}
+      @doLogin {username, password, clientId}

   doRecover:(formData)->
     KD.remote.api.JPasswordRecovery.recoverPassword formData['username-or-email'], (err)=>


focus selected row while loading

focus selected row while loading

--- client/lanes/components/record-finder/Dialog.cjsx
+++ client/lanes/components/record-finder/Dialog.cjsx
@@ -11,12 +11,19 @@
     contextTypes:
         viewport: Lanes.PropTypes.State.isRequired

+    getInitialState: -> {}
+
     warning: ->
         <BS.Alert bsStyle='warning'>
              <strong>{@model.lastServerMessage}</strong>
         </BS.Alert>

-    onRecordSelect: (model) ->
+    isGridRowFocused: (row, index) ->
+        @state.selectedRow is index
+
+    onRecordSelect: (model, selectedRow) ->
+        @setState({selectedRow})
+        @props.query.markModified()
         return unless model
         @props.query.loadModel(model).then @props.onRecordSelect
         _.delay( =>
@@ -47,6 +54,7 @@
             </div>
             <LC.Grid
                 onColumnClick={@onColumnSort}
+                isRowFocused={@isGridRowFocused}
                 query={@props.query} height=200 autoLoadQuery
                 onSelectionChange={@onRecordSelect}
             />


changing js files location 7

changing js files location 7

--- app/assets/javascripts/actions/beacons_map.js.coffee
+++ app/assets/javascripts/actions/beacons_map.js.coffee
@@ -33,5 +33,5 @@
     eventHandler.addEvent('#beacon_floor', 'change')
     eventHandler.addEvent('#beacon_name', 'change keyup')
     eventHandler.addEvent('.map-view-sidebar input:checkbox', 'switchChange.bootstrapSwitch')
-    eventHandler.addOberver(fetcher)
+#    eventHandler.addOberver(fetcher)
     map = new MicelloMap('rrVDMQe2qjBRuJP5dXv59kAeK77SD7','beacons-map')


Remove unnecessary models for auto sluggify by title

--- app/assets/javascripts/admin/form_translation.coffee
+++ app/assets/javascripts/admin/form_translation.coffee
@@ -21,7 +21,7 @@

 slugify = ->
-  categories = ['home', 'about', 'career', 'photography', 'webdesign', 'print']
+  categories = ['home', 'about', 'content']
   $.each categories, (i, val) ->
     j = 0
     while j < 2


Remove log

Remove log

--- select.coffee
+++ select.coffee
@@ -80,7 +80,6 @@

         @$target.on 'blur', (e) =>
             if @dropSelect.isOpened()
-                log e.relatedTarget, $(e.relatedTarget).parents(@dropSelect.$drop).length
                 if e.relatedTarget and not $(e.relatedTarget).parents('.drop:first').is(@dropSelect.$drop)
                     @dropSelect.close()
             else


made some translations serious. make addresses same (Du/Sie). add missing translations

--- t9n/de.coffee
+++ t9n/de.coffee
@@ -1,5 +1,5 @@
 #Language: German
-#Translators: softwarerero, robhunt3r
+#Translators: softwarerero, robhunt3r, sclausen

 de =

@@ -43,8 +43,8 @@

   info:
-    emailSent: "Email gesendet"
-    emailVerified: "Email verifiziert"
+    emailSent: "E-Mail gesendet"
+    emailVerified: "E-Mail verifiziert"
     PasswortChanged: "Passwort geändert"
     PasswortReset: "Passwort zurückgesetzt"

@@ -67,48 +67,48 @@
       #---- accounts-base
       #"@" + domain + " email required":
       #"A login handler should return a result or undefined":
-      "Email already exists.": "Die Email gibt es schon."
-      "Email doesn't match the criteria.": "Emails stimmt nicht mit den Kriterien überein."
-#>    "Invalid login token":
-#>    "Login forbidden":
+      "Email already exists.": "Die E-Mail Adresse ist bereits vergeben."
+      "Email doesn't match the criteria.": "E-Mail Adresse erfüllt nicht die Anforderungen."
+      "Invalid login token": "Ungültiger Login Token"
+      "Login forbidden": "Anmeldung verboten"
       #"Service " + options.service + " already configured":
-#>    "Service unknown":
-#>    "Unrecognized options for login request":
+      "Service unknown": "Dienst unbekannt"
+      "Unrecognized options for login request": "Unbekannte Optionen für Login Request"
       "User validation failed": "Die Benutzerdaten scheinen nicht korrekt"
       "Username already exists.": "Den Benutzer gibt es schon."
       "You are not logged in.": "Sie sind nicht eingeloggt."
       "You've been logged out by the server. Please log in again.": "Der Server hat Dich ausgeloggt. Bitte melde Dich erneut an."
-      "Your session has expired. Please log in again.": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an."
+      "Your session has expired. Please log in again.": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an."

       #---- accounts-oauth
-#>    "No matching login attempt found":
+      "No matching login attempt found": "Kein passender Loginversuch gefunden."

       #---- accounts-Passwort-client
-      "Passwort is old. Please reset your Passwort.": "Das Passwort ist abgelaufen, bitte zurücksetzen."
+      "Passwort is old. Please reset your Passwort.": "Passwort ist abgelaufen. Bitte setzen Sie es zurück."

       #---- accounts-Passwort
       "Incorrect Passwort": "Falschen Passwort"
-      "Invalid email": "Falschen Email"
-      "Must be logged in": "Da muss man sich aber erst anmelden"
-      "Need to set a username or email": "Benutzername oder Email sollte man schon angeben"
+      "Invalid email": "Ungültige E-Mail Adresse"
+      "Must be logged in": "Sie müssen sich anmelden"
+      "Need to set a username or email": "Benutzername oder E-Mail Adresse müssen angegeben werden"
 #>    "old Passwort format":
-      "Passwort may not be empty": "Das Passwort darf nicht leer bleiben"
+      "Passwort may not be empty": "Das Passwort darf nicht leer sein"
       "Signups forbidden": "Anmeldungen sind verboten"
-      "Token expired": "Das Token ist abgelaufen"
-      "Token has invalid email address": "Für des Token stimmt die Email-Adresse nicht"
+      "Token expired": "Token ist abgelaufen"
+      "Token has invalid email address": "E-Mail Adresse passt nicht zum Token"
       "User has no Passwort set": "Kein Passwort für den Benutzer angegeben"
       "User not found": "Benutzer nicht gefunden"
-      "Verify email link expired": "Token zur Verifizierung der Email ist abgelaufen"
-      "Verify email link is for unknown address": "Token zur Verifizierung der Email ist für eine unbekannte Adresse"
+      "Verify email link expired": "Link zur E-Mail Verifizierung ist abgelaufen"
+      "Verify email link is for unknown address": "Link zur Verifizierung ist für eine unbekannte E-Mail Adresse"

       #---- match
-#>    "Match failed":
+      "Match failed": "Abgleich fehlgeschlagen"

       #---- Misc...
-#>    "Unknown error":
+      "Unknown error": "Unbekannter Fehler"

-T9n.map "de", de+T9n.map "de", de


Autocomplete now supports a 'placeholder' property

--- packages/emberella/lib/views/autocomplete.coffee
+++ packages/emberella/lib/views/autocomplete.coffee
@@ -213,6 +213,15 @@
     @default ''
   ###
   value: ''
+
+  ###
+    The placeholder attribute to assign to the autocomplete's input field.
+
+    @property placeholder
+    @type String
+    @default ''
+  ###
+  placeholder: ''

   ###
     The dot-delimited "Ember.get" path for finding a string value to display
@@ -1014,7 +1023,7 @@
 Emberella.AutocompleteInputView = Ember.TextField.extend Emberella.FocusableMixin, Emberella.KeyboardControlMixin, Emberella.MembershipMixin,
   attributeBindings: ['autocomplete']
   autocomplete: 'off' #disable browser autocomplete
-  inherit: ['value']
+  inherit: ['value', 'placeholder']

 ###############################################################################


add urlencoded parser

add urlencoded parser

--- server.coffee
+++ server.coffee
@@ -15,6 +15,7 @@
 app
   .use cookies()
   .use parser.json()
+  .use parser.urlencoded {extended: yes}
   .use routes

 app.listen port, ->


Delete wrong error messages

Delete wrong error messages

--- server.coffee
+++ server.coffee
@@ -1315,8 +1315,6 @@
       if (match = /client_port=(\d+)-(\d+)/.exec req.headers.transport)?
         client.clientAudioRTPPort = parseInt match[1]
         client.clientAudioRTCPPort = parseInt match[2]
-      else
-        console.log "error: malformed transport header for audio: #{req.headers.transport}"
     else  # video
       track = 'video'
       if client.useHTTP
@@ -1327,8 +1325,6 @@
       if (match = /client_port=(\d+)-(\d+)/.exec req.headers.transport)?
         client.clientVideoRTPPort = parseInt match[1]
         client.clientVideoRTCPPort = parseInt match[2]
-      else
-        console.log "error: malformed transport header for video: #{req.headers.transport}"

     if /\bTCP\b/.test req.headers.transport
       useTCPTransport = true


Converted tabs to spaces.

Converted tabs to spaces.

--- server.coffee
+++ server.coffee
@@ -5,150 +5,150 @@
 queue = new Meteor._SynchronousQueue()

 _.extend Tracker,
-	currentComputationVar: new Meteor.EnvironmentVariable()
-
-	flush: ->
-		if not queue.safeToRunTask()
-			throw new Error("Can't call Tracker.flush while flushing, or inside Tracker.autorun")
-
-		queue.drain()
-
-	_postRun: ->
-		while (queue._taskHandles.length == 0) and (afterFlushCallbacks.length > 0)
-			f = afterFlushCallbacks.shift()
-			try
-				f()
-			catch e
-				console.log "Exception from Tracker afterFlush function:", e.stack || e.message
-
-	autorun: (f) ->
-		c = new Tracker.Computation(f, Tracker.currentComputation, privateObj)
-
-		if Tracker.active
-			Tracker.onInvalidate -> c.stop()
-
-		c
-
-	nonreactive: (f) ->
-		Tracker.currentComputationVar.withValue null, f
-
-	_makeNonreactive: (f) ->
-		if f.$isNonreactive
-			return f
-		result = (args...) ->
-			Tracker.nonreactive =>
-				f.apply(@, args)
-		result.$isNonreactive = true
-		result
-
-	onInvalidate: (f) ->
-		if not Tracker.active
-			throw new Error("Tracker.onInvalidate requires a currentComputation")
-
-		Tracker.currentComputation.onInvalidate(f)
-
-	afterFlush: (f) ->
-		afterFlushCallbacks.push(f)
+  currentComputationVar: new Meteor.EnvironmentVariable()
+
+  flush: ->
+    if not queue.safeToRunTask()
+      throw new Error("Can't call Tracker.flush while flushing, or inside Tracker.autorun")
+
+    queue.drain()
+
+  _postRun: ->
+    while (queue._taskHandles.length == 0) and (afterFlushCallbacks.length > 0)
+      f = afterFlushCallbacks.shift()
+      try
+        f()
+      catch e
+        console.log "Exception from Tracker afterFlush function:", e.stack || e.message
+
+  autorun: (f) ->
+    c = new Tracker.Computation(f, Tracker.currentComputation, privateObj)
+
+    if Tracker.active
+      Tracker.onInvalidate -> c.stop()
+
+    c
+
+  nonreactive: (f) ->
+    Tracker.currentComputationVar.withValue null, f
+
+  _makeNonreactive: (f) ->
+    if f.$isNonreactive
+      return f
+    result = (args...) ->
+      Tracker.nonreactive =>
+        f.apply(@, args)
+    result.$isNonreactive = true
+    result
+
+  onInvalidate: (f) ->
+    if not Tracker.active
+      throw new Error("Tracker.onInvalidate requires a currentComputation")
+
+    Tracker.currentComputation.onInvalidate(f)
+
+  afterFlush: (f) ->
+    afterFlushCallbacks.push(f)

 # Compatibility with client-side Tracker
 Object.defineProperties Tracker,
-	currentComputation:
-		get: ->
-			Tracker.currentComputationVar.get()
-	active:
-		get: ->
-			!!Tracker.currentComputationVar.get()
+  currentComputation:
+    get: ->
+      Tracker.currentComputationVar.get()
+  active:
+    get: ->
+      !!Tracker.currentComputationVar.get()

 class Tracker.Computation
-	constructor: (f, @_parent, p)->
-		if p != privateObj
-			throw new Error("Tracker.Computation constructor is private; use Tracker.autorun")
-
-		@stopped = false
-		@invalidated = false
-		@firstRun = true
-		@_id = nextId++
-		@_onInvalidateCallbacks = []
-		@_recomputing = false
-
-		Tracker.currentComputationVar.withValue @, =>
-			@_func = Meteor.bindEnvironment(f, null, @)
-
-		errored = true
-		try
-			@._compute()
-			errored = false
-		finally
-			@firstRun = false
-			if errored
-				@stop()
-
-	onInvalidate: (f) ->
-		if typeof f != "function"
-			throw new Error("onInvalidate requires a function")
-
-		f = Tracker._makeNonreactive(Meteor.bindEnvironment(f, null, @))
-
-		if @invalidated
-			f()
-		else
-			@_onInvalidateCallbacks.push(f)
-
-	invalidate: ->
-		if not @invalidated
-			if not @_recomputing and not @stopped
-				queue.queueTask =>
-					@._recompute()
-					Tracker._postRun()
-
-			@invalidated = true
-
-			for callback in @_onInvalidateCallbacks
-				callback()
-			@_onInvalidateCallbacks = []
-
-	stop: ->
-		if not @stopped
-			@stopped = true
-			@invalidate()
-
-	_compute: ->
-		@invalidated = false
-		@._func(@)
-
-	_recompute: ->
-		@_recomputing = true
-		while @invalidated and not @stopped
-			try
-				@._compute()
-			catch e
-				console.log e
-		@_recomputing = false
-
+  constructor: (f, @_parent, p)->
+    if p != privateObj
+      throw new Error("Tracker.Computation constructor is private; use Tracker.autorun")
+
+    @stopped = false
+    @invalidated = false
+    @firstRun = true
+    @_id = nextId++
+    @_onInvalidateCallbacks = []
+    @_recomputing = false
+
+    Tracker.currentComputationVar.withValue @, =>
+      @_func = Meteor.bindEnvironment(f, null, @)
+
+    errored = true
+    try
+      @._compute()
+      errored = false
+    finally
+      @firstRun = false
+      if errored
+        @stop()
+
+  onInvalidate: (f) ->
+    if typeof f != "function"
+      throw new Error("onInvalidate requires a function")
+
+    f = Tracker._makeNonreactive(Meteor.bindEnvironment(f, null, @))
+
+    if @invalidated
+      f()
+    else
+      @_onInvalidateCallbacks.push(f)
+
+  invalidate: ->
+    if not @invalidated
+      if not @_recomputing and not @stopped
+        queue.queueTask =>
+          @._recompute()
+          Tracker._postRun()
+
+      @invalidated = true
+
+      for callback in @_onInvalidateCallbacks
+        callback()
+      @_onInvalidateCallbacks = []
+
+  stop: ->
+    if not @stopped
+      @stopped = true
+      @invalidate()
+
+  _compute: ->
+    @invalidated = false
+    @._func(@)
+
+  _recompute: ->
+    @_recomputing = true
+    while @invalidated and not @stopped
+      try
+        @._compute()
+      catch e
+        console.log e
+    @_recomputing = false
+
 class Tracker.Dependency
-	constructor: ->
-		@_dependentsById = {}
-
-	depend: (computation = Tracker.currentComputation) ->
-		if not computation
-			return false
-
-		id = computation._id
-
-		if not (id of @_dependentsById)
-			@_dependentsById[id] = computation
-			computation.onInvalidate =>
-				delete @_dependentsById[id]
-
-			return true
-		false
-
-	changed: ->
-		for id, computation of @_dependentsById
-			computation.invalidate()
-
-	hasDependents: ->
-		for id, computation of @_dependentsById
-			return true
-		false
-
+  constructor: ->
+    @_dependentsById = {}
+
+  depend: (computation = Tracker.currentComputation) ->
+    if not computation
+      return false
+
+    id = computation._id
+
+    if not (id of @_dependentsById)
+      @_dependentsById[id] = computation
+      computation.onInvalidate =>
+        delete @_dependentsById[id]
+
+      return true
+    false
+
+  changed: ->
+    for id, computation of @_dependentsById
+      computation.invalidate()
+
+  hasDependents: ->
+    for id, computation of @_dependentsById
+      return true
+    false
+


minor updates

minor updates

--- server.coffee
+++ server.coffee
@@ -82,19 +82,12 @@
 createGitRepo = (repo, callback) ->
   {name} = repo
   gitty.create name, repo.description, __dirname + "/repos", (err, data) ->
-    # gitty.add __dirname + "/repos/#{name}", ["LICENSE"], (err, data) ->
-    #   console.log "added file", err, data
-    #   if err
-    #     callback err
-    #     return
-    gitty.commit __dirname + "/repos/#{name}", "initial", (err, data) ->
-      gitty.remote.add __dirname + "/repos/#{name}", "origin", "https://github.com/communities/#{name}.git", (err, data) ->
-        console.log "origin was added", err, data
-        gitty.push __dirname + "/repos/#{name}", "origin", "master", (err, data) ->
-          if err
-            callback err
-            return
-          callback undefined, data
+    gitty.add __dirname + "/repos/#{name}", ["LICENSE"], (err, data) ->
+      gitty.commit __dirname + "/repos/#{name}", "initial", (err, data) ->
+        gitty.remote.add __dirname + "/repos/#{name}", "origin", "https://github.com/communities/#{name}.git", (err, data) ->
+          console.log "origin was added", err, data
+          gitty.push __dirname + "/repos/#{name}", "origin", "master", (err, data) ->
+            callback undefined, data

 passport = require "passport"
@@ -194,7 +187,8 @@
             return
           hash = {}
           _.each repos, (repo) -> hash[repo.name] = JSON.stringify(repo)
-          rc.hmset "communities", hash, callback
+          rc.hmset "communities", hash, (err) ->
+            callback err, repos

 app.get "/api/communities/:community", (req, res) ->
   community = req.params.community


Updated placeholder

Updated placeholder

--- core/app/backbone/views/facts/auto_complete_facts_view.coffee
+++ core/app/backbone/views/facts/auto_complete_facts_view.coffee
@@ -19,7 +19,7 @@
         fact_id: @options.fact_id
         recent_collection: @_recent_collection
       filtered_search_collection: new FilteredFactSearchResults
-      placeholder: 'Search...'
+      placeholder: 'Search discussion link to insert...'

     @listenTo @model, 'change', @queryChanges


Improved copy on sending factlinks to other factlink users

Textual change to improve understanding of sending messages/factlinks
to other factlink users.

--- core/app/backbone/views/users/auto_complete_users_view.coffee
+++ core/app/backbone/views/users/auto_complete_users_view.coffee
@@ -14,7 +14,7 @@
       search_list_view: (options) -> new AutoCompleteSearchUsersView(options)
       search_collection: new UserSearchResults
       filtered_search_collection: new UserSearchResults
-      placeholder: 'Name...'
+      placeholder: 'Factlink user...'

     @_results_view = new AutoCompleteResultsUsersView(collection: @collection)


pagination service tests improved

pagination service tests improved

--- test/coffee/spec/pagination/services/pagination.coffee
+++ test/coffee/spec/pagination/services/pagination.coffee
@@ -10,4 +10,23 @@
 		pagination = $injector.get 'pagination'

 	it 'should be something', ->
-		expect(!!pagination).toBe true+		expect(!!pagination).toBe true
+
+	it 'should have a valid getFixedOffset method', ->
+		expect(!!pagination.getFixedOffset).toBe true
+		expect(pagination.getFixedOffset 20, 20, 35 ).toBe 15
+
+	it 'should have a valid getNumberOfPages method', ->
+		expect(!!pagination.getNumberOfPages).toBe  true
+		expect(pagination.getNumberOfPages 50, 210).toBe 5
+
+	it 'should have a valid getCurrentPage method', ->
+		expect(!!pagination.getCurrentPage).toEqual true
+		expect(pagination.getCurrentPage 50, 50, 4).toEqual 2
+
+	it 'should have a valid getPages method', ->
+		expect(!!pagination.getPages).toEqual true
+		expect(pagination.getPages 14, 50, 5).toEqual [9,10,11,12,13,14,15,16,17,18,19]
+		# should return 2*offset pages except the active one in offset edges too
+		expect(pagination.getPages 3, 15, 5).toEqual [1,2,3,4,5,6,7,8,9,10,11]
+		expect(pagination.getPages 18, 20, 5).toEqual [10,11,12,13,14,15,16,17,18,19,20]


Fix CKEditor reset issue when pressing 'Add new' button of nested form

--- app/assets/javascripts/rails_admin/ra.widgets.coffee
+++ app/assets/javascripts/rails_admin/ra.widgets.coffee
@@ -124,7 +124,7 @@

     # ckeditor

-    $('form [data-richtext=ckeditor]').each ->
+    $('form [data-richtext=ckeditor]:not(.ckeditored)').each ->
       window.CKEDITOR_BASEPATH = '/assets/ckeditor/'
       options = $(this).data('options')
       if not window.CKEDITOR
@@ -132,3 +132,4 @@
       if instance = window.CKEDITOR.instances[this.id]
         instance.destroy(true)
       window.CKEDITOR.replace(this, options['options'])
+      $(this).addClass('ckeditored')


fixbug

fixbug

--- core/router/ticket.coffee
+++ core/router/ticket.coffee
@@ -105,7 +105,7 @@
       unless mAccount.inGroup req.account, 'root'
         return res.error 'forbidden'

-    status = if mAccount.inGroupr(req.account, 'root') then 'open' else 'pending'
+    status = if mAccount.inGroup(req.account, 'root') then 'open' else 'pending'
     mTicket.createReply ticket, req.account, req.body.content, status, (reply) ->
       return res.json
         id: reply._id


Sort files by size but with directories on top

--- SingularityUI/app/collections/TaskLogFiles.coffee
+++ SingularityUI/app/collections/TaskLogFiles.coffee
@@ -19,6 +19,11 @@
             taskLogFile.taskId = @taskId
             taskLogFile

-    comparator: 'size'
+    comparator: (a, b) ->
+        if a.get('isDirectory') and not b.get('isDirectory')
+            return 1
+        else if not a.get('isDirectory') and b.get('isDirectory')
+            return -1
+        return a.get('size') - b.get('size')

 module.exports = TaskLogFiles


app.coffee: tweaked the controller logging.

--- src/awraamba/coffee/app.coffee
+++ src/awraamba/coffee/app.coffee
@@ -81,17 +81,17 @@
       @_show 'intro_view'

     explore: (location) =>
-      console.log 'Controller.explore'
+      console.log "Controller.explore #{location}"
       @_show 'explore_view', (view) =>
         view.model.set 'value', location

     watch: (theme) =>
-      console.log 'Controller.watch'
+      console.log "Controller.watch #{theme}"
       @_show 'watch_view', (view) =>
         view.model.set 'value', theme

     interact: (reaction) =>
-      console.log 'Controller.interact'
+      console.log "Controller.interact #{reaction}"
       @_show 'interact_view', (view) =>
         view.model.set 'value', reaction


Invitations: Updated the tests.

Invitations: Updated the tests.

--- workers/social/lib/social/models/invitation.test.coffee
+++ workers/social/lib/social/models/invitation.test.coffee
@@ -151,87 +151,90 @@

   describe 'workers.social.invitation.some', ->

-    describe 'if user does not have permission', ->
-
-      it 'should fail', (done) ->
-
-        expectAccessDenied JInvitation, 'some$', {}, {}, done
-
-      it 'should have default group name if user is not an admin', (done) ->
-
-        withConvertedUser { createGroup: yes }, (data) ->
-          { client: adminClient } = data
-          dummyGroupName = 'Foo'
-          selector = { groupSlug: dummyGroupName }
-
-          email = generateRandomEmail()
+    it 'should fail if user does not have permission ', (done) ->
+
+      expectAccessDenied JInvitation, 'some$', {}, {}, done
+
+
+    it 'should have default group name if user is not an admin', (done) ->
+
+      withConvertedUser { createGroup: yes }, (data) ->
+        { client: adminClient } = data
+        dummyGroupName = 'Foo'
+        selector = { groupSlug: dummyGroupName }
+
+        email = generateRandomEmail()
+        invitationReq = { invitations:[ { email } ] }
+        JInvitation.create adminClient, invitationReq, (err) ->
+          JInvitation.some$ adminClient, selector, {}, (err, invitations) ->
+
+            invitationGroupName = invitations[0].data.groupName
+            expect(invitationGroupName).to.not.equal dummyGroupName
+            expect(invitationGroupName).to.be.equal data.group.slug
+
+            done()
+
+    it 'should update groupSlug and fetch other group invitations', (done) ->
+
+      groupSlug1   = null
+      groupSlug2   = null
+      adminClient1 = null
+      adminClient2 = null
+      adminAccount = null
+      invitedUser1 = null
+      invitedUser2 = null
+
+      queue = [
+
+        # create first group
+        (next) ->
+          withConvertedUser { createGroup: yes }, ({ client, group }) ->
+            groupSlug1   = group.slug
+            adminClient1 = client
+            next()
+
+        # invite a user to first group
+        (next) ->
+          email = invitedUser1 = generateRandomEmail()
           invitationReq = { invitations:[ { email } ] }
-          JInvitation.create adminClient, invitationReq, (err) ->
-            JInvitation.some$ adminClient, selector, {}, (err, invitations) ->
-
-              invitationGroupName = invitations[0].data.groupName
-              expect(invitationGroupName).to.not.equal dummyGroupName
-              expect(invitationGroupName).to.be.equal data.group.slug
-
-              done()
-
-      it 'should update groupSlug and fetch other group invitations', (done) ->
-
-        groupSlug1   = null
-        groupSlug2   = null
-        adminClient1 = null
-        adminClient2 = null
-        adminAccount = null
-        invitedUser1 = null
-        invitedUser2 = null
-
-        queue = [
-
-          # create first group
-          (next) ->
-            withConvertedUser { createGroup: yes }, ({ client, group }) ->
-              groupSlug1   = group.slug
-              adminClient1 = client
-              next()
-
-          # invite a user to first group
-          (next) ->
-            email = invitedUser1 = generateRandomEmail()
-            invitationReq = { invitations:[ { email } ] }
-            JInvitation.create adminClient1, invitationReq, (err) ->
-              expect(err).to.not.exist
-              next()
-
-          # create second group
-          (next) ->
-            withConvertedUser { createGroup: yes }, ({ client, account, group }) ->
-              groupSlug2   = group.slug
-              adminClient2 = client
-              adminAccount = account
-              next()
-
-          # invite a user to second group
-          (next) ->
-            email = invitedUser2 = generateRandomEmail()
-            invitationReq = { invitations:[ { email } ] }
-            JInvitation.create adminClient2, invitationReq, (err) ->
-              expect(err).to.not.exist
-              next()
-
-          # add super-admin global flag to second converted account
-          (next) ->
-            adminAccount.update { $set: { globalFlags: [ 'super-admin' ] } }, ->
-              next()
-
-          # try to hijack groupSlug with first group admin
-          # returned result set should be for admin's own group
-          (next) ->
-            JInvitation.some$ adminClient1, { groupSlug: groupSlug2 }, {}, (err, invitations) ->
-              expect(invitations[0].data.groupName).to.equal groupSlug1
-              next()
-        ]
-
-        async.series queue, done
+          JInvitation.create adminClient1, invitationReq, (err) ->
+            expect(err).to.not.exist
+            next()
+
+        # create second group
+        (next) ->
+          options = { createGroup: yes, role: 'admin' }
+          withConvertedUser options, ({ client, account, group }) ->
+            groupSlug2   = group.slug
+            adminClient2 = client
+            adminAccount = account
+            next()
+
+        # invite a user to second group
+        (next) ->
+          email = invitedUser2 = generateRandomEmail()
+          invitationReq = { invitations:[ { email } ] }
+          JInvitation.create adminClient2, invitationReq, (err) ->
+            expect(err).to.not.exist
+            next()
+
+        # add super-admin global flag to second converted account
+        (next) ->
+          adminAccount.update { $set: { globalFlags: [ 'super-admin' ] } }, next
+
+        # try to hijack groupSlug with first group admin
+        # returned result set should be for admin's own group
+        (next) ->
+          JInvitation.some$ adminClient1, { groupSlug: groupSlug2 }, {}, (err, invitations) ->
+            expect(err).to.not.exist
+            expect(invitations.length).to.above 0
+            expect(invitations[0].data).to.exist
+            expect(invitations[0].data.groupName).to.equal groupSlug1
+            next()
+
+      ]
+
+      async.series queue, done

 beforeTests()


referrable email: forgot a dependency

referrable email: forgot a dependency

--- workers/social/lib/social/models/referrableemail.coffee
+++ workers/social/lib/social/models/referrableemail.coffee
@@ -1,14 +1,14 @@
 jraphical = require "jraphical"
 module.exports = class JReferrableEmail extends jraphical.Module
   JAccount           = require "./account"
-  {ObjectId, secure} = require "bongo"
+  {ObjectId, secure, signature} = require "bongo"

   @share()

   @set

     sharedMethods :
-
+
       static      :
         invite    :
           (signature String, Function)


working on transition configuration

working on transition configuration

--- lib/assets/javascripts/nice_imp_jquery.js.coffee
+++ lib/assets/javascripts/nice_imp_jquery.js.coffee
@@ -35,6 +35,12 @@
 	@perform_transition_animation: (event) ->
 		$("[data-state][data-state-transition!='none']").fadeTo(0,0).delay(200).fadeTo(500,1.0)

+document.nice =
+	transitions:
+		def:
+			duration: 200
+			properties:
+				alpha: 0

 ## add event listener
 document.addEventListener "nice.dom.InsertAfterEvent", NiceJquery.insert_after, false
@@ -42,4 +48,4 @@
 document.addEventListener "nice.dom.RemoveStateEvent", NiceJquery.remove_state_elements, false
 document.addEventListener "nice.hist.ChangeURLEvent", NiceJquery.move_to_url, false
 document.addEventListener "nice.hist.PopHistoryEvent", NiceJquery.insert_or_update_back_listener, false
-document.addEventListener "nice.trsn.AnimateEvent", NiceJquery.perform_transition_animation, false+document.addEventListener "nice.trsn.AnimateEvent", NiceJquery.perform_transition_animation, false


Removed GOPATH from Atom's config.cson

Removed GOPATH from Atom's config.cson

Don't think it's necessary(?) as $GOPATH is set in https://github.com/holman/dotfiles/blob/master/go/path.zsh

https://github.com/joefitzgerald/go-config/wiki/GOPATH

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -26,8 +26,6 @@
     invisibles: {}
     showIndentGuide: true
     softWrapAtPreferredLineLength: true
-  "go-plus":
-    goPath: "/Users/holman/Code/go"
   "linter-eslint":
     useGlobalEslint: true
   "release-notes":


Atom updates

Atom updates

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -1,8 +1,8 @@
 "*":
   editor:
     invisibles: {}
+    showInvisibles: true
     fontFamily: "Source Code Pro"
-    showInvisibles: true
   core:
     ignoredNames: [
       ".bundle"
@@ -27,3 +27,21 @@
     showOnStartup: false
   "atom-lint": {}
   linter: {}
+  "tree-view":
+    showOnRightSide: true
+  "linter-rubocop": {}
+  react:
+    enabledForAllJavascriptFiles: true
+  "linter-flow-plus": {}
+  "file-watcher":
+    promptWhenFileHasChangedOnDisk: false
+  "linter-eslint":
+    disableWhenNoEslintrcFileInPath: true
+  nuclide:
+    remoteProjectsConfig: []
+  "one-light-ui": {}
+  "find-and-replace": {}
+  "linter-jscs":
+    onlyConfig: true
+    fixOnSave: true
+  "go-plus": {}


Updating test to reflect client side building done

--- test/1-overall_spec.coffee
+++ test/1-overall_spec.coffee
@@ -5,5 +5,5 @@
     it "can parse timezone files", -> true
     it "can process timezone rules", -> true
     it "can convert a UTC TimeStamp to a Wall Time for America/Chicago during daylight savings time", -> true
-    it "can convert a Wall Time for America/Chicago during daylight savings time to a UTC TimeStamp"
-    it "can build a client side script version"+    it "can convert a Wall Time for America/Chicago during daylight savings time to a UTC TimeStamp", -> true
+    it "can build a client side script version", -> true


Using Object.extend for Number.prototype extensions

--- source/number_extensions.coffee
+++ source/number_extensions.coffee
@@ -61,337 +61,338 @@
   Number::[method] = ->
     Math[method](this)

-###*
-Get a bunch of points equally spaced around the unit circle.
-
-    4.circularPoints (p) ->
-
-    # p gets Point(1, 0), Point(0, 1), Point(-1, 0), Point(0, -1)
-
-@name circularPoint
-@methodOf Number#
-
-###
-Number::circularPoints = (block) ->
-  n = this
-
-  n.times (i) ->
-    block(Point.fromAngle((i/n).turns), i)
-
-###*
-Returns a number whose value is limited to the given range.
-
-    # limit the output of this computation to between 0 and 255
-    (2 * 255).clamp(0, 255)
-    # => 255
-
-@name clamp
-@methodOf Number#
-@param {Number} min The lower boundary of the output range
-@param {Number} max The upper boundary of the output range
-@returns {Number} A number in the range [min, max]
-###
-Number::clamp = (min, max) ->
-  if min? and max?
-    Math.min(Math.max(this, min), max)
-  else if min?
-    Math.max(this, min)
-  else if max?
-    Math.min(this, max)
-  else
-    this
-
-###*
-A mod method useful for array wrapping. The range of the function is
-constrained to remain in bounds of array indices.
-
-    (-1).mod(5)
-    # => 4
-
-@name mod
-@methodOf Number#
-@param {Number} base
-@returns {Number} An integer between 0 and (base - 1) if base is positive.
-###
-Number::mod = (base) ->
-  result = this % base;
-
-  if result < 0 && base > 0
-    result += base
-
-  return result
-
-###*
-Get the sign of this number as an integer (1, -1, or 0).
-
-    (-5).sign()
-    # => -1
-
-    0.sign()
-    # => 0
-
-    5.sign()
-    # => 1
-
-@name sign
-@methodOf Number#
-@returns {Number} The sign of this number, 0 if the number is 0.
-###
-Number::sign = ->
-  if this > 0
-    1
-  else if this < 0
-    -1
-  else
-    0
-
-###*
-Returns true if this number is even (evenly divisible by 2).
-
-    2.even()
-    # => true
-
-    3.even()
-    # => false
-
-    0.even()
-    # => true
-
-@name even
-@methodOf Number#
-@returns {Boolean} true if this number is an even integer, false otherwise.
-###
-Number::even = ->
-  @mod(2) is 0
-
-###*
-Returns true if this number is odd (has remainder of 1 when divided by 2).
-
-    2.odd()
-    # => false
-
-    3.odd()
-    # => true
-
-    0.odd()
-    # => false
-
-@name odd
-@methodOf Number#
-@returns {Boolean} true if this number is an odd integer, false otherwise.
-###
-Number::odd = ->
-  @mod(2) is 1
-
-###*
-Calls iterator the specified number of times, passing in the number of the
-current iteration as a parameter: 0 on first call, 1 on the second call, etc.
-
-    output = []
-
-    5.times (n) ->
-      output.push(n)
-
-    output
-    # => [0, 1, 2, 3, 4]
-
-@name times
-@methodOf Number#
-@param {Function} iterator The iterator takes a single parameter, the number of the current iteration.
-@param {Object} [context] The optional context parameter specifies an object to treat as this in the iterator block.
-@returns {Number} The number of times the iterator was called.
-###
-Number::times = (iterator, context) ->
-  i = -1
-
-  while ++i < this
-    iterator.call context, i
-
-  return i
-
-###*
-Returns the the nearest grid resolution less than or equal to the number.
-
-    7.snap(8)
-    # => 0
-
-    4.snap(8)
-    # => 0
-
-    12.snap(8)
-    # => 8
-
-@name snap
-@methodOf Number#
-@param {Number} resolution The grid resolution to snap to.
-@returns {Number} The nearest multiple of resolution lower than the number.
-###
-Number::snap = (resolution) ->
-  n = this / resolution; 1/1; # This is to fix broken regex in doc parser
-  n.floor() * resolution
-
-###*
-In number theory, integer factorization or prime factorization is the
-breaking down of a composite number into smaller non-trivial divisors,
-which when multiplied together equal the original integer.
-
-Floors the number for purposes of factorization.
-
-    60.primeFactors()
-    # => [2, 2, 3, 5]
-
-    37.primeFactors()
-    # => [37]
-
-@name primeFactors
-@methodOf Number#
-@returns {Array} An array containing the factorization of this number.
-###
-Number::primeFactors = ->
-  factors = []
-
-  n = @floor()
-
-  if n == 0
-    return undefined
-
-  if n < 0
-    factors.push(-1)
-    n /= -1
-
-  i = 2
-  iSquared = i * i
-
-  while iSquared < n
-    while (n % i) == 0
-        factors.push i
-        n /= i
-
-    i += 1
+Object.extend Number.prototype,
+  ###*
+  Get a bunch of points equally spaced around the unit circle.
+
+      4.circularPoints (p) ->
+
+      # p gets Point(1, 0), Point(0, 1), Point(-1, 0), Point(0, -1)
+
+  @name circularPoint
+  @methodOf Number#
+
+  ###
+  circularPoints: (block) ->
+    n = this
+
+    n.times (i) ->
+      block(Point.fromAngle((i/n).turns), i)
+
+  ###*
+  Returns a number whose value is limited to the given range.
+
+      # limit the output of this computation to between 0 and 255
+      (2 * 255).clamp(0, 255)
+      # => 255
+
+  @name clamp
+  @methodOf Number#
+  @param {Number} min The lower boundary of the output range
+  @param {Number} max The upper boundary of the output range
+  @returns {Number} A number in the range [min, max]
+  ###
+  clamp: (min, max) ->
+    if min? and max?
+      Math.min(Math.max(this, min), max)
+    else if min?
+      Math.max(this, min)
+    else if max?
+      Math.min(this, max)
+    else
+      this
+
+  ###*
+  A mod method useful for array wrapping. The range of the function is
+  constrained to remain in bounds of array indices.
+
+      (-1).mod(5)
+      # => 4
+
+  @name mod
+  @methodOf Number#
+  @param {Number} base
+  @returns {Number} An integer between 0 and (base - 1) if base is positive.
+  ###
+  mod: (base) ->
+    result = this % base;
+
+    if result < 0 && base > 0
+      result += base
+
+    return result
+
+  ###*
+  Get the sign of this number as an integer (1, -1, or 0).
+
+      (-5).sign()
+      # => -1
+
+      0.sign()
+      # => 0
+
+      5.sign()
+      # => 1
+
+  @name sign
+  @methodOf Number#
+  @returns {Number} The sign of this number, 0 if the number is 0.
+  ###
+  sign: ->
+    if this > 0
+      1
+    else if this < 0
+      -1
+    else
+      0
+
+  ###*
+  Returns true if this number is even (evenly divisible by 2).
+
+      2.even()
+      # => true
+
+      3.even()
+      # => false
+
+      0.even()
+      # => true
+
+  @name even
+  @methodOf Number#
+  @returns {Boolean} true if this number is an even integer, false otherwise.
+  ###
+  even: ->
+    @mod(2) is 0
+
+  ###*
+  Returns true if this number is odd (has remainder of 1 when divided by 2).
+
+      2.odd()
+      # => false
+
+      3.odd()
+      # => true
+
+      0.odd()
+      # => false
+
+  @name odd
+  @methodOf Number#
+  @returns {Boolean} true if this number is an odd integer, false otherwise.
+  ###
+  odd: ->
+    @mod(2) is 1
+
+  ###*
+  Calls iterator the specified number of times, passing in the number of the
+  current iteration as a parameter: 0 on first call, 1 on the second call, etc.
+
+      output = []
+
+      5.times (n) ->
+        output.push(n)
+
+      output
+      # => [0, 1, 2, 3, 4]
+
+  @name times
+  @methodOf Number#
+  @param {Function} iterator The iterator takes a single parameter, the number of the current iteration.
+  @param {Object} [context] The optional context parameter specifies an object to treat as this in the iterator block.
+  @returns {Number} The number of times the iterator was called.
+  ###
+  times: (iterator, context) ->
+    i = -1
+
+    while ++i < this
+      iterator.call context, i
+
+    return i
+
+  ###*
+  Returns the the nearest grid resolution less than or equal to the number.
+
+      7.snap(8)
+      # => 0
+
+      4.snap(8)
+      # => 0
+
+      12.snap(8)
+      # => 8
+
+  @name snap
+  @methodOf Number#
+  @param {Number} resolution The grid resolution to snap to.
+  @returns {Number} The nearest multiple of resolution lower than the number.
+  ###
+  snap: (resolution) ->
+    n = this / resolution; 1/1; # This is to fix broken regex in doc parser
+    n.floor() * resolution
+
+  ###*
+  In number theory, integer factorization or prime factorization is the
+  breaking down of a composite number into smaller non-trivial divisors,
+  which when multiplied together equal the original integer.
+
+  Floors the number for purposes of factorization.
+
+      60.primeFactors()
+      # => [2, 2, 3, 5]
+
+      37.primeFactors()
+      # => [37]
+
+  @name primeFactors
+  @methodOf Number#
+  @returns {Array} An array containing the factorization of this number.
+  ###
+  primeFactors: ->
+    factors = []
+
+    n = @floor()
+
+    if n == 0
+      return undefined
+
+    if n < 0
+      factors.push(-1)
+      n /= -1
+
+    i = 2
     iSquared = i * i

-  if n != 1
-    factors.push n
-
-  return factors
-
-###*
-Returns the two character hexidecimal
-representation of numbers 0 through 255.
-
-    255.toColorPart()
-    # => "ff"
-
-    0.toColorPart()
-    # => "00"
-
-    200.toColorPart()
-    # => "c8"
-
-@name toColorPart
-@methodOf Number#
-@returns {String} Hexidecimal representation of the number
-###
-Number::toColorPart = ->
-  s = parseInt(@clamp(0, 255), 10).toString(16)
-
-  if s.length == 1
-    s = '0' + s
-
-  return s
-
-###*
-Returns a number that is maxDelta closer to target.
-
-    255.approach(0, 5)
-    # => 250
-
-    5.approach(0, 10)
-    # => 0
-
-@name approach
-@methodOf Number#
-@returns {Number} A number maxDelta toward target
-###
-Number::approach = (target, maxDelta) ->
-  (target - this).clamp(-maxDelta, maxDelta) + this
-
-###*
-Returns a number that is closer to the target by the ratio.
-
-    255.approachByRatio(0, 0.1)
-    # => 229.5
-
-@name approachByRatio
-@methodOf Number#
-@returns {Number} A number toward target by the ratio
-###
-Number::approachByRatio = (target, ratio) ->
-  @approach(target, this * ratio)
-
-###*
-Returns a number that is closer to the target angle by the delta.
-
-    Math.PI.approachRotation(0, Math.PI/4)
-    # => 2.356194490192345 # this is (3/4) * Math.PI, which is (1/4) * Math.PI closer to 0 from Math.PI
-
-@name approachRotation
-@methodOf Number#
-@returns {Number} A number toward the target angle by maxDelta
-###
-Number::approachRotation = (target, maxDelta) ->
-  while target > this + Math.PI
-    target -= Math.TAU
-
-  while target < this - Math.PI
-    target += Math.TAU
-
-  return (target - this).clamp(-maxDelta, maxDelta) + this
-
-###*
-Constrains a rotation to between -PI and PI.
-
-    (9/4 * Math.PI).constrainRotation()
-    # => 0.7853981633974483 # this is (1/4) * Math.PI
-
-@name constrainRotation
-@methodOf Number#
-@returns {Number} This number constrained between -PI and PI.
-###
-Number::constrainRotation = ->
-  target = this
-
-  while target > Math.PI
-    target -= Math.TAU
-
-  while target < -Math.PI
-    target += Math.TAU
-
-  return target
-
-# TODO Test and document
-Number::truncate = ->
-  if this > 0
-    @floor()
-  else if this < 0
-    @ceil()
-  else
-    this
-
-###*
-The mathematical d operator. Useful for simulating dice rolls.
-
-@name d
-@methodOf Number#
-@returns {Number} The sum of rolling this many sides-sided dice
-###
-Number::d = (sides) ->
-  sum = 0
-
-  @times ->
-    sum += rand(sides) + 1
-
-  return sum
+    while iSquared < n
+      while (n % i) == 0
+          factors.push i
+          n /= i
+
+      i += 1
+      iSquared = i * i
+
+    if n != 1
+      factors.push n
+
+    return factors
+
+  ###*
+  Returns the two character hexidecimal
+  representation of numbers 0 through 255.
+
+      255.toColorPart()
+      # => "ff"
+
+      0.toColorPart()
+      # => "00"
+
+      200.toColorPart()
+      # => "c8"
+
+  @name toColorPart
+  @methodOf Number#
+  @returns {String} Hexidecimal representation of the number
+  ###
+  toColorPart: ->
+    s = parseInt(@clamp(0, 255), 10).toString(16)
+
+    if s.length == 1
+      s = '0' + s
+
+    return s
+
+  ###*
+  Returns a number that is maxDelta closer to target.
+
+      255.approach(0, 5)
+      # => 250
+
+      5.approach(0, 10)
+      # => 0
+
+  @name approach
+  @methodOf Number#
+  @returns {Number} A number maxDelta toward target
+  ###
+  approach: (target, maxDelta) ->
+    (target - this).clamp(-maxDelta, maxDelta) + this
+
+  ###*
+  Returns a number that is closer to the target by the ratio.
+
+      255.approachByRatio(0, 0.1)
+      # => 229.5
+
+  @name approachByRatio
+  @methodOf Number#
+  @returns {Number} A number toward target by the ratio
+  ###
+  approachByRatio: (target, ratio) ->
+    @approach(target, this * ratio)
+
+  ###*
+  Returns a number that is closer to the target angle by the delta.
+
+      Math.PI.approachRotation(0, Math.PI/4)
+      # => 2.356194490192345 # this is (3/4) * Math.PI, which is (1/4) * Math.PI closer to 0 from Math.PI
+
+  @name approachRotation
+  @methodOf Number#
+  @returns {Number} A number toward the target angle by maxDelta
+  ###
+  approachRotation: (target, maxDelta) ->
+    while target > this + Math.PI
+      target -= Math.TAU
+
+    while target < this - Math.PI
+      target += Math.TAU
+
+    return (target - this).clamp(-maxDelta, maxDelta) + this
+
+  ###*
+  Constrains a rotation to between -PI and PI.
+
+      (9/4 * Math.PI).constrainRotation()
+      # => 0.7853981633974483 # this is (1/4) * Math.PI
+
+  @name constrainRotation
+  @methodOf Number#
+  @returns {Number} This number constrained between -PI and PI.
+  ###
+  constrainRotation: ->
+    target = this
+
+    while target > Math.PI
+      target -= Math.TAU
+
+    while target < -Math.PI
+      target += Math.TAU
+
+    return target
+
+  # TODO Test and document
+  truncate: ->
+    if this > 0
+      @floor()
+    else if this < 0
+      @ceil()
+    else
+      this
+
+  ###*
+  The mathematical d operator. Useful for simulating dice rolls.
+
+  @name d
+  @methodOf Number#
+  @returns {Number} The sum of rolling this many sides-sided dice
+  ###
+  d: (sides) ->
+    sum = 0
+
+    @times ->
+      sum += rand(sides) + 1
+
+    return sum

 ###*
 Utility method to convert a number to a duration of seconds.


add linewise change operation (cc) specs

Again, basically the delete specs with checking the mode afterwards.

--- spec/vim-mode-spec.coffee
+++ spec/vim-mode-spec.coffee
@@ -202,6 +202,42 @@
           expect(editor.getCursorScreenPosition()).toEqual([0,0])

     describe "the c keybinding", ->
+      describe "when followed by a c", ->
+        it "deletes the current line and enters insert mode", ->
+          editor.setText("12345\nabcde\nABCDE")
+          editor.setCursorScreenPosition([1,1])
+
+          keydown('c', element: editor[0])
+          keydown('c', element: editor[0])
+          expect(editor.getText()).toBe "12345\nABCDE"
+          expect(editor.getCursorScreenPosition()).toEqual([1,0])
+          expect(editor).not.toHaveClass 'command-mode'
+          expect(editor).toHaveClass 'insert-mode'
+
+        it "deletes the last line and enters insert mode", ->
+          editor.setText("12345\nabcde\nABCDE")
+          editor.setCursorScreenPosition([2,1])
+          keydown('c', element: editor[0])
+          keydown('c', element: editor[0])
+          expect(editor.getText()).toBe "12345\nabcde"
+          expect(editor.getCursorScreenPosition()).toEqual([1,0])
+          expect(editor).not.toHaveClass 'command-mode'
+          expect(editor).toHaveClass 'insert-mode'
+
+        describe "when the second c is prefixed by a count", ->
+          it "deletes n lines, starting from the current, and enters insert mode", ->
+            editor.setText("12345\nabcde\nABCDE\nQWERT")
+            editor.setCursorScreenPosition([1,1])
+
+            keydown('c', element: editor[0])
+            keydown('2', element: editor[0])
+            keydown('c', element: editor[0])
+
+            expect(editor.getText()).toBe "12345\nQWERT"
+            expect(editor.getCursorScreenPosition()).toEqual([1,0])
+            expect(editor).not.toHaveClass 'command-mode'
+            expect(editor).toHaveClass 'insert-mode'
+
       describe "when followed by an h", ->
         it "deletes the previous letter on the current line and enters insert mode", ->
           editor.setText("abcd\n01234")


Don't prevent renderer-side reloads

Don't prevent renderer-side reloads

--- src/browser/atom-window.coffee
+++ src/browser/atom-window.coffee
@@ -146,7 +146,9 @@
         when 0 then @browserWindow.destroy()
         when 1 then @browserWindow.restart()

-    @browserWindow.webContents.on 'will-navigate', (event) -> event.preventDefault()
+    @browserWindow.webContents.on 'will-navigate', (event, url) =>
+      unless url is @browserWindow.webContents.getUrl()
+        event.preventDefault()

     @setupContextMenu()


change alert close method

change alert close method

--- JSKit/03_JSAlertView.coffee
+++ JSKit/03_JSAlertView.coffee
@@ -54,8 +54,8 @@
                             @delegate.clickedButtonAtIndex(text, @_self)
                         when "JSAlertViewStyleDefault"
                             @delegate.clickedButtonAtIndex(1, @_self)
-                    if (@delegate? && typeof @delegate.closedDialog == "function")
-                        @delegate.closedDialog(@_self)
+                if (@delegate? && typeof @delegate.closedDialog == "function")
+                    @delegate.closedDialog(@_self)
                 $(@_viewSelector+"_form").dialog("close")
                 @_self.removeFromSuperview()
         if (@cancel == true)


JSTableView select bug fix

JSTableView select bug fix

--- JSKit/04_JSTableView.coffee
+++ JSKit/04_JSTableView.coffee
@@ -10,7 +10,7 @@
         super(frame)
         @_rowHeight = 32
         @_clipToBounds = true
-        @_bgColor = JSColor("clearColor")
+        @_bgColor = JSColor("white")
         @_scroll = true
         @_titlebarColor = JSColor("#d0d8e0")
         @_titleColor = JSColor("black")


Fix previously broken data-route tests

Fix previously broken data-route tests

--- tests/batman/view/route_rendering_test.coffee
+++ tests/batman/view/route_rendering_test.coffee
@@ -8,6 +8,7 @@
     class @App extends Batman.App
       @layout: null
       @route '/test', ->
+      @route '/', ->
     Batman.redirect = @redirect = createSpy()

   teardown: ->
@@ -22,28 +23,20 @@
   @App.run()

 asyncTest 'should redirect when clicked', 2, ->
-  sinon.spy(Batman.DOM.RouteBinding.prototype, 'routeClick')
-
   @App.on 'run', =>
     helpers.render 'click', {}, (node) =>
       helpers.triggerClick(node[0])
-      ok Batman.DOM.RouteBinding.prototype.routeClick.called
-      Batman.DOM.RouteBinding.prototype.routeClick.restore()
-
-      delay =>
-        deepEqual @redirect.lastCallArguments['/test']
+      delay =>
+        ok @redirect.called
+        deepEqual @redirect.lastCallArguments, ['/test']
   @App.run()

 asyncTest 'should not redirect when clicked if target attribute is set', 1, ->
-  sinon.spy(Batman.DOM.RouteBinding.prototype, 'routeClick')
-
   @App.on 'run', =>
     helpers.render 'click', {}, (node) =>
       helpers.triggerClick(node[0])
-      ok not Batman.DOM.RouteBinding::routeClick.called
-      Batman.DOM.RouteBinding::routeClick.restore()
-
-      QUnit.start()
+      delay =>
+        ok not @redirect.called
   @App.run()

 asyncTest 'should set "#" href for undefined keypath', 1, ->
@@ -247,7 +240,6 @@
 asyncTest 'should allow you to nested elements with route declarations', 6, ->
   @App.resources 'products', ->
     @collection 'search'
-  @App.root ->

   @App.on 'run', =>
     source = '''
@@ -280,7 +272,6 @@

 asyncTest 'should not stop events from bubbling', 2, ->
   @App.resources 'products'
-  @App.root ->

   @App.on 'run', =>
     source = '''


Boyscouting fat arrow

Boyscouting fat arrow

--- core/app/backbone/views/notifications_view.coffee
+++ core/app/backbone/views/notifications_view.coffee
@@ -39,10 +39,10 @@
     TitleManager.set "notificationsCount", @_unreadTitleCount

   markAsRead: ->
-    self = this
-    @collection.markAsRead success: ->
-      self.markViewsForUnreadification()
-      self.setUnreadCount 0
+    @collection.markAsRead
+      success: =>
+        @markViewsForUnreadification()
+        @setUnreadCount 0

   markViewsForUnreadification: ->
     @_shouldMarkUnread = true


:memo: Fix spec titles

:memo: Fix spec titles

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -373,13 +373,13 @@
         advanceClock(keymapManager.getPartialMatchTimeout())
         expect(events).toEqual ['x-ctrl-keyup']

-      it "dispatches the y-up-ctrl-keyup command when a matching keystroke precedes it", ->
+      it "dispatches a command when modifier is lifted before the character", ->
         keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
         keymapManager.handleKeyboardEvent(buildKeyupEvent('ctrl', target: elementA))
         advanceClock(keymapManager.getPartialMatchTimeout())
         expect(events).toEqual ['y-ctrl-keyup']

-      it "doesn't dispatch the y-up-ctrl-keyup command when a non-matching keystroke precedes it", ->
+      it "does not dispatch the keyup command when a non-matching keydown event is made between matching keystrokes", ->
         keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
         keymapManager.handleKeyboardEvent(buildKeydownEvent('z', ctrl: true, target: elementA))
         keymapManager.handleKeyboardEvent(buildKeyupEvent('ctrl', target: elementA))


Increase code coverage

Increase code coverage

--- test/generate-query.coffee
+++ test/generate-query.coffee
@@ -11,7 +11,188 @@
     res = CustomModel.__buildQuery filter
     expect(res.text).to.eql 'SELECT Post.* FROM Post'

-  describe 'join tables', ->
+  context 'postgres connector', ->
+
+    describe 'join tables', ->
+
+      it 'should build a query with missing relation', ->
+        filter =
+          join: [
+            {
+              relation: 'no_relation'
+              scope:
+                where:
+                  content: 'coucou'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.text).to.eql 'SELECT Post.* FROM Post'
+
+
+      it 'should build a query with a single join', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content: 'coucou'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content = $1)'
+        expect(res.values[0]).to.eql 'coucou'
+
+      it 'should build a query with two joins', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content: 'coucou'
+            }
+            {
+              relation: 'author'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) INNER JOIN User ON (User.id = Post.authorId) WHERE (Comment.content = $1)'
+        expect(res.values[0]).to.eql 'coucou'
+
+      it 'should build a query with two joins and two where', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content: 'coucou'
+            }
+            {
+              relation: 'author'
+              scope:
+                where:
+                  email: 'user@example.com'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.values[0]).to.eql 'coucou'
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) INNER JOIN User ON (User.id = Post.authorId) WHERE (Comment.content = $1) AND (User.email = $2)'
+
+    describe 'where', ->
+
+      it 'should handle gte', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content:
+                    gte: 'coucou'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.values[0]).to.eql 'coucou'
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content >= $1)'
+
+      it 'should handle lte', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content:
+                    lte: 'coucou'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.values[0]).to.eql 'coucou'
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content <= $1)'
+
+      it 'should handle neq', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content:
+                    neq: 'coucou'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.values[0]).to.eql 'coucou'
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content <> $1)'
+
+      it 'should handle like', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content:
+                    like: 'coucou'
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.values[0]).to.eql 'coucou'
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content LIKE $1)'
+
+      it 'should handle null', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  content: null
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content IS NULL)'
+
+
+      it 'should handle or', ->
+        filter =
+          join: [
+            {
+              relation: 'comments'
+              scope:
+                where:
+                  or: [
+                    {
+                      content:
+                        like: 'coucou'
+                    }
+                    {content: 'caca'}
+                  ]
+            }
+          ]
+        res = CustomModel.__buildQuery filter
+        expect(res.values).to.be.instanceof Array
+        expect(res.values[0]).to.eql 'coucou'
+        expect(res.values[1]).to.eql 'caca'
+        expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE ((Comment.content LIKE $1 OR Comment.content = $2))'
+
+
+  context 'default connector', ->
+
+    before ->
+      application.datasources.db.connector.settings.connector = 'sqlite'
+

     it 'should build a query with a single join', ->
       filter =
@@ -25,134 +206,7 @@
         ]
       res = CustomModel.__buildQuery filter
       expect(res.values).to.be.instanceof Array
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content = $1)'
+      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content = ?)'
       expect(res.values[0]).to.eql 'coucou'

-    it 'should build a query with two joins', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                content: 'coucou'
-          }
-          {
-            relation: 'author'
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) INNER JOIN User ON (User.id = Post.authorId) WHERE (Comment.content = $1)'
-      expect(res.values[0]).to.eql 'coucou'
-
-    it 'should build a query with two joins and two where', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                content: 'coucou'
-          }
-          {
-            relation: 'author'
-            scope:
-              where:
-                email: 'user@example.com'
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.values[0]).to.eql 'coucou'
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) INNER JOIN User ON (User.id = Post.authorId) WHERE (Comment.content = $1) AND (User.email = $2)'
-
-  describe 'where', ->
-
-    it 'should handle gte', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                content:
-                  gte: 'coucou'
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.values[0]).to.eql 'coucou'
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content >= $1)'
-
-    it 'should handle lte', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                content:
-                  lte: 'coucou'
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.values[0]).to.eql 'coucou'
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content <= $1)'
-
-    it 'should handle neq', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                content:
-                  neq: 'coucou'
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.values[0]).to.eql 'coucou'
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content <> $1)'
-
-    it 'should handle like', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                content:
-                  like: 'coucou'
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.values[0]).to.eql 'coucou'
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE (Comment.content LIKE $1)'
-
-    it 'should handle or', ->
-      filter =
-        join: [
-          {
-            relation: 'comments'
-            scope:
-              where:
-                or: [
-                  {
-                    content:
-                      like: 'coucou'
-                  }
-                  {content: 'caca'}
-                ]
-          }
-        ]
-      res = CustomModel.__buildQuery filter
-      expect(res.values).to.be.instanceof Array
-      expect(res.values[0]).to.eql 'coucou'
-      expect(res.values[1]).to.eql 'caca'
-      expect(res.text).to.eql 'SELECT Post.* FROM Post INNER JOIN Comment ON (Post.id = Comment.postId) WHERE ((Comment.content LIKE $1 OR Comment.content = $2))'
-
-
+


Sorting, filtering, and paging.

Sorting, filtering, and paging.

--- teeble.coffee
+++ teeble.coffee
@@ -1,70 +1,79 @@
 # Teeble: a teeny table plugin
+
+safeParseInt = (value) ->
+    parseInt value, 10
+
+testPrint = (input) ->
+    $('div').html JSON.stringify input
+

 class Backbone.Teeble extends Backbone.View

     initialize: ->
-        # Render the table when the data changes. We could use a backbone model here in the future.
-        @onUpdate = @render
+        if not @options.collection?
+            throw "Sorry bro, we can't render a table without a collection."

-        _setOptions()
-        @data = _.extend {}, @initialData
-
-        if @pagination
-            @data.page = parseInt(@data.page) or @page ? 1
-            @data.size = parseInt(@data.size) or @size ? 10
+        @setOptions()

         return @

-    _setOptions: ->
-        for key, val in @options
-            @[key] = val
+    setOptions: ->
+        validOptions = [
+            'title'  # Table title
+            'collection'  # The canonical data model
+            'pagination'  # Boolean
+            'page'  # Pagination control
+            'size'  # Number of rows to display
+        ]

-    data: (newData) ->
+        for option in validOptions
+            @[option] = @options[option]
+
+        # @data is the subset of data displayed in the table
+        @data = @collection
+
+        if @pagination
+            @page ?= 1
+            @size ?= 10
+            @numPages = _.size(@collection) / @size
+
+        return @
+
+    set: (newData, silent=false) =>
         if newData?
             @data = newData

-        return @data
+        # Re-render every time the data changes unless told otherwise
+        if not silent
+            @render()

-    filter: (input) ->
+        return @

-    create: ->
-        # Build the table
+    render: =>
+        # Draw/redraw the table
+        # marc.render @data

-    draw: ->
-        # Draw/redraw the table
+        testPrint @data

-    destroy: (target) ->
+    sort: (sortOp) =>
+        @set _(@collection).sortBy sortOp

+        return @

-# Usage
+    toPage: (pageIndex) =>
+        if pageIndex < 1
+            pageIndex = 0

-MOCK_DATA = $.getJSON('mock.json')
+        @set @collection.slice (pageIndex * @size), (pageIndex + 1) * @size
+        @page = pageIndex

-class MyTable extends Backbone.Teeble
+    nextPage: =>
+        @toPage @page + 1

-    title: 'My Awesome Table'
-    data: MOCK_DATA
+    prevPage: =>
+        if @page > 0
+            @toPage @page - 1

-    columns:
-        platform:
-            header: "Platform"
-            template: handlebar_template
-        name:
-            header: "Name"
-            template: handlebar_template
-        os:
-            header: "Operating System"
-            template: handlebar_template
-
-        version:
-            header: "Version"
-            template: handlebar_template
-
-        grade:
-            header: "Grade"
-            template: handlebar_template
-
-
-
-
-
+    destroy: =>
+        # marc.destroy()
+        return


Restore headers to DYK list

Restore headers to DYK list

--- app/assets/javascripts/components/activity/did_you_know_handler.cjsx
+++ app/assets/javascripts/components/activity/did_you_know_handler.cjsx
@@ -22,18 +22,6 @@
   setCourseScope: (e) ->
     scoped = e.target.checked
     ServerActions.fetchDYKArticles(scoped: scoped)
-  clearAllSortableClassNames: ->
-    Array.prototype.map.call document.getElementsByClassName('sortable'), (el) ->
-      el.classList.remove('asc')
-      el.classList.remove('desc')
-  sortArticles: (e) ->
-    sortOrder = if e.target.classList.contains('asc') then 'desc' else 'asc'
-    @clearAllSortableClassNames()
-    e.target.classList.add(sortOrder)
-    key = e.target.dataset.sortKey
-    articles = _.sortByOrder(@state.articles, [key])
-    articles = articles.reverse() if sortOrder is 'desc'
-    @setState articles: articles

   render: ->
     headers = [
@@ -45,11 +33,6 @@

     noActivityMessage = 'There are not currently any DYK-eligible articles.'

-    ths = headers.map (header) =>
-
-        {header.title}
-
-

         <input ref='myCourses' type='checkbox' onChange={@setCourseScope} />
@@ -57,7 +40,7 @@

       <ActivityTable
         activity={@state.articles}
-        headers={ths}
+        headers={headers}
         noActivityMessage={noActivityMessage}
       />