New doc for implementing hooks

New doc for implementing hooks

--- generate-docs.coffee
+++ generate-docs.coffee
@@ -74,14 +74,16 @@

 Shrub implements message passing between packages through a hook system. Hooks
 may be invoked with [pkgman.invoke()](./client/modules/pkgman.html), and are
-implemented in packages by prefixing a `$` to the hook name.
+implemented in packages by exporting `pkgmanRegister`.

 For instance, if we are implementing a package and want to implement the
 `httpListening` hook, our code would look like:

-	exports.$httpListening = ->
-
-		# Your code goes here...
+	exports.pkgmanRegister = (registrar) ->
+
+		registrar.registerHook 'httpListening', ->
+
+			# Your code goes here...

 The list below was dynamically generated from the source code. There is a
 description and a list of implementing packages for each hook.


Wrap results in an APIResults object.

Wrap results in an APIResults object.

This adds indexing by ID, as well as an aggregateBy method. Also passes through common Array methods.

--- compiler/ContentAPI.coffee
+++ compiler/ContentAPI.coffee
@@ -146,6 +146,38 @@
         return new Model(data_copy)

+class APIResults
+    constructor: (array_of_results) ->
+        @_items = array_of_results
+        @length = @_items.length
+
+        @_items.forEach (item, i) =>
+            @[i] = item
+            @[item.id] = item if item.id
+
+
+    aggregateBy: (property) ->
+        result = {}
+        @_items.forEach (item) ->
+            if item[property]?
+                result[item[property]] ?= []
+                result[item[property]].push(item)
+        return result
+
+    forEach: -> @_items.forEach(arguments...)
+    map: -> @_items.map(arguments...)
+    slice: -> @_items.slice(arguments...)
+    shift: ->
+        _res = @_items.shift()
+        @length = @_items.length
+        return _res
+    pop: ->
+        _res = @_items.pop()
+        @length = @_items.length
+        return _res
+
+
+

 # Content API wrapper
@@ -218,8 +250,10 @@
         num_last_batch = -1
         _makeRequest = =>
             if num_last_batch is 0
-                deferred_result.resolve(results)
-                cb?(results)
+                _results = new APIResults(results)
+
+                deferred_result.resolve(_results)
+                cb?(_results)
             else
                 @_sendRequest
                     url         : ENDPOINTS[query.type]


Pass onDelete to newly created ModuleEdit views

--- cms/static/coffee/src/views/unit.coffee
+++ cms/static/coffee/src/views/unit.coffee
@@ -59,6 +59,7 @@
     event.preventDefault()

     editor = new CMS.Views.ModuleEdit(
+      onDelete: @deleteComponent
       model: new CMS.Models.Module()
     )


refactor

refactor

--- app/assets/javascripts/downloads.js.coffee
+++ app/assets/javascripts/downloads.js.coffee
@@ -50,24 +50,14 @@
     tbody = $("#code_rows")
     tbody.empty()
     for code in codes
-      stock = stocks[code]
+      stock = stocks[code] || {name: "-"}

-      tr = $("

")
-      tbody.append(tr)
+      tr = $("

").appendTo(tbody)

       $("	").text(code).appendTo(tr)
-
-      td = $("	").appendTo(tr)
-      if stock
-        td.text(stock.name)
-
-      td = $("	").appendTo(tr)
-      if stock
-        td.text(stock.price)
-
-      td = $("	").appendTo(tr)
-      if stock
-        td.text(stock.date)
+      $("	").text(stock.name).appendTo(tr)
+      $("	").text(stock.price).appendTo(tr)
+      $("	").text(stock.date).appendTo(tr)

       del = $("<button class='btn btn-danger'>削除</button>")
       # code の値をイベントハンドラ内で使用するため、クロージャにする


fix response JSON for error in importer

fix response JSON for error in importer

--- app/assets/javascripts/employees.js.coffee
+++ app/assets/javascripts/employees.js.coffee
@@ -89,7 +89,7 @@
         data: {employee: attrs}
         success: (data) ->
         error: (data) ->
-          $('#import-errors').append("
" + data.responseText + "
")
+          $('#import-errors').append("
" + data.responseJSON.errors + "
")
         complete: () ->
           rows.shift()
           if rows.length == 0


Tab stop support

Tab stop support

--- app/assets/javascripts/player/vt.js.coffee
+++ app/assets/javascripts/player/vt.js.coffee
@@ -166,6 +166,7 @@
     "\x1bP([^\\\\])*?\\\\": (data) -> # DCS, Device Control String
     "\x1bD": -> @index()
     "\x1bE": -> @newLine()
+    "\x1bH": -> @setHorizontalTabStop()
     "\x1bM": -> @reverseIndex()

     "\x1b7": (data) -> # save cursor pos and char attrs
@@ -224,7 +225,6 @@
         @eraseCharacters @n
       when "Z"
         @goToPriorHorizontalTabStop @n
-
       when "b"
         @repeatLastCharacter @n
       when "d" # VPA - Vertical Position Absolute
@@ -484,15 +484,43 @@
     @goToColumn col

   setHorizontalTabStop: ->
-    # @tabStops << @cursorX
-
-  goToNextHorizontalTabStop: ->
-
-  goToPriorHorizontalTabStop: ->
+    unless _(@tabStops).include(@cursorX)
+      pos = _(@tabStops).sortedIndex(@cursorX)
+      @tabStops.splice(pos, 0, @cursorX)
+
+  goToNextHorizontalTabStop: (n) ->
+    x = @getNextTabStop()
+    @goToRowAndColumn(@cursorY + 1, x + 1)
+    @updateLine()
+
+  goToPriorHorizontalTabStop: (n) ->
+    x = @getPriorTabStop()
+    @goToRowAndColumn(@cursorY + 1, x + 1)
+    @updateLine()
+
+  getNextTabStop: ->
+    for x in @tabStops
+      if x > @cursorX
+        return x
+
+    @cols
+
+  getPriorTabStop: ->
+    ret = 0
+
+    for x in @tabStops
+      if x > @cursorX
+        break
+
+      ret = x
+
+    ret

   clearHorizontalTabStop: ->
+    console.log 'clearHorizontalTabStop'

   clearAllHorizontalTabStops: ->
+    console.log 'clearAllHorizontalTabStops'

   saveCursor: ->
     @savedCol = @cursorX
@@ -605,6 +633,7 @@
     @lineData = @normalBuffer
     @dirtyLines = {}
     @brush = AsciiIo.Brush.create({})
+    @tabStops = (x for x in [0...@cols] when x % 8 is 0)

     @fg = @bg = undefined
     @bright = false


Form submission now checks for minimal signatures for required and recommended signature fields

--- app/assets/javascripts/variables.js.coffee
+++ app/assets/javascripts/variables.js.coffee
@@ -77,6 +77,13 @@
       $(this).is(':checked')
     )
     value.size() == 0
+  else if element.data('required-type') == 'signature'
+    try
+      result = JSON.parse($("[name='#{element.data('required-field')}']").val()).length < 20
+    catch
+      result = true
+    finally
+      result
   else
     value = $("[name='#{element.data('required-field')}']").val()
     $.trim(value) == ''


unselect all on render

unselect all on render

--- app/assets/javascripts/zvg/verbatim.coffee
+++ app/assets/javascripts/zvg/verbatim.coffee
@@ -24,6 +24,7 @@
       )

     rows.attr('id', (d) -> "respondent_#{d.id}")
+    @selectNoResponses()

     rows.exit().remove()
     rows.selectAll('td').remove()


change errorStream to stdout

change errorStream to stdout

--- lib/linter-js-standard.coffee
+++ lib/linter-js-standard.coffee
@@ -15,7 +15,7 @@

   linterName: 'js-standard'

-  errorStream: 'stderr'
+  errorStream: 'stdout'

   # A regex pattern used to extract information from the executable's output.
   regex:


The map() method now works correctly for empty collections.

--- lib/indexed.coffee
+++ lib/indexed.coffee
@@ -267,7 +267,7 @@
   minusAll: (seq) -> @update_ Sequence.reduce seq, @root, @constructor.minusOne

   # Creates a mapped collection of the same type
-  map: (fun) -> new @constructor().plusAll @entries.map fun
+  map: (fun) -> new @constructor().plusAll Sequence.map @entries, fun

   # The sequence of entries.
   toSeq: -> @entries


limit new calendar event <select /> to active calendars.

fixes #8683

test plan:
  * in calendar 2, uncheck all calendars in the right sidebar
    but one;
  * click on a day in the calendar to create a new event, and
    verify that the only active calendar is selected in the
    dropdown;
  * verify that the unchecked calendars are unavailable in the
    select menu;
  * clear your localStorage and verify that without local storage
    all calendars are included in the <select />.

Change-Id: Ide4c5cd930bc66a8a5d44b81c13dbcba12e92f91
Reviewed-on: https://gerrit.instructure.com/14707
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Marc LeGendre <d33c80bc45d65303e33ca83108a9952b745af9ef@instructure.com>
Reviewed-by: Mark Ericksen <776ab05fc6b68e82d2b2c63873bf56edde131809@instructure.com>

--- app/coffeescripts/calendar/Calendar.coffee
+++ app/coffeescripts/calendar/Calendar.coffee
@@ -8,6 +8,7 @@
   'i18n!calendar'
   'jquery'
   'underscore'
+  'compiled/userSettings'
   'compiled/util/hsvToRgb'
   'jst/calendar/calendarApp'
   'compiled/calendar/EventDataSource'
@@ -22,7 +23,7 @@
   'jquery.instructure_misc_plugins'
   'vendor/jquery.ba-tinypubsub'
   'jqueryui/button'
-], (I18n, $, _, hsvToRgb, calendarAppTemplate, EventDataSource, commonEventFactory, ShowEventDetailsDialog, EditEventDetailsDialog, Scheduler, calendarDefaults) ->
+], (I18n, $, _, userSettings, hsvToRgb, calendarAppTemplate, EventDataSource, commonEventFactory, ShowEventDetailsDialog, EditEventDetailsDialog, Scheduler, calendarDefaults) ->

   class Calendar
     constructor: (selector, @contexts, @manageContexts, @dataSource, @options) ->
@@ -242,7 +243,9 @@
         return

       # create a new dummy event
-      event = commonEventFactory(null, @contexts)
+      allowedContexts = userSettings.get('checked_calendar_codes') or _.pluck(@contexts, 'asset_string')
+      activeContexts  = _.filter @contexts, (c) -> _.contains(allowedContexts, c.asset_string)
+      event = commonEventFactory(null, activeContexts)
       event.allDay = allDay
       event.date = date


don't include <init> and <clinit> in reflection method lookup

--- src/natives.coffee
+++ src/natives.coffee
@@ -191,7 +191,7 @@
             rs.init_object('[Ljava/lang/reflect/Field;',(f.reflector(rs) for f in fields))
         o 'getDeclaredMethods0(Z)[Ljava/lang/reflect/Method;', (rs, _this, public_only) ->
             methods = rs.class_lookup(_this.$type).methods
-            methods = (m for sig, m of methods when m.access_flags.public or not public_only)
+            methods = (m for sig, m of methods when sig[0] != '<' and (m.access_flags.public or not public_only))
             rs.init_object('[Ljava/lang/reflect/Method;',(m.reflector(rs) for m in methods))
         o 'getDeclaredConstructors0(Z)[Ljava/lang/reflect/Constructor;', (rs, _this, public_only) ->
             methods = rs.class_lookup(_this.$type).methods


Pass in settings value from ocelot. Remove header from GET request.

--- js/src/http/UserDirectoryApi.coffee
+++ js/src/http/UserDirectoryApi.coffee
@@ -27,8 +27,6 @@
   log = Logger.get('UserDirectoryApi')

   DEFAULT_HEADERS = { 'Content-Type' : 'application/json' }
-  MUTED_CHANNEL = 'SILENCED_CHANNELS'
-

   { validateUuids } = Validators

@@ -168,32 +166,31 @@
         })
       ))

-    getMutedChannels: ->
+    getMutedChannels: (setting) ->
       Promise.resolve(axios(
         Requests.wrapCredentials({
-          url: getUserUrl() + '/setting/' + MUTED_CHANNEL
-          headers: DEFAULT_HEADERS
+          url: getUserUrl() + '/setting/' + setting
           method: 'GET'
         })
       ))

-    addMutedChannel: (channelId) ->
+    addMutedChannel: (uuid, setting) ->
       Promise.resolve(axios(
         Requests.wrapCredentials({
-          url: getUserUrl() + '/setting/' + MUTED_CHANNEL
+          url: getUserUrl() + '/setting/' + setting
           headers: DEFAULT_HEADERS
           method: 'PUT'
-          data: [channelId]
+          data: [uuid]
         })
       ))

-    removeMutedChannel: (channelId) ->
+    removeMutedChannel: (uuid, setting) ->
       Promise.resolve(axios(
         Requests.wrapCredentials({
-          url: getUserUrl() + '/setting/' + MUTED_CHANNEL
+          url: getUserUrl() + '/setting/' + setting
           headers: DEFAULT_HEADERS
           method: 'DELETE'
-          data: [channelId]
+          data: [uuid]
         })
       ))


new endpoint to get user UUID from email

--- js/src/http/UserDirectoryApi.coffee
+++ js/src/http/UserDirectoryApi.coffee
@@ -22,6 +22,7 @@
   getUsersUrl  = -> Configuration.get('heraclesUrlV2') + '/directory/users'
   usersInRealmUrl = -> Configuration.get('heraclesUrlV2') + '/directory'
   setFirstLoginUrl = -> Configuration.get('heraclesUrlV2') + '/directory/setlogin'
+  getUserIdFromEmail = (email) -> Configuration.get('heraclesUrlV2') + '/directory/validate/sharing/email/' + email

   log = Logger.get('UserDirectoryApi')

@@ -49,6 +50,39 @@
       @getUser( uuid )
       .then (user) ->
         return user.name
+
+    getUserIdFromEmail: (email) ->
+
+      if _.isEmpty(email)
+        return Promise.resolve(null)
+
+      Promise.resolve(
+        axios(
+          Requests.wrapCredentials({
+            method : 'GET',
+            url    : getUserIdFromEmail(email)
+          })
+        )
+      )
+      .then (axiosResponse) ->
+        if axiosResponse and axiosResponse.data
+          # axiosResponse.data == java.util.UUID
+          return axiosResponse.data
+        else
+          return null
+      .catch (axiosError) ->
+        kjsError = {
+          status: axiosError.status
+        }
+        if axiosError.status == 404
+          kjsError.message = 'USER_DOES_NOT_EXIST'
+          return Promise.reject(kjsError)
+        else if axiosError.status == 403
+          kjsError.message = 'SHARING_WITH_USER_BLOCKED'
+          return Promise.reject(kjsError)
+        else
+          kjsError.message = JSON.stringify(axiosError.data)
+          return Promise.reject(kjsError)

     resolve: ({ email }) ->
       Promise.resolve()


Regard .tikz files as TeX files as well

Regard .tikz files as TeX files as well

--- lib/manager.coffee
+++ lib/manager.coffee
@@ -44,7 +44,7 @@

   isTexFile: (name) ->
     @latex.manager.loadLocalCfg()
-    if path.extname(name) == '.tex' or \
+    if path.extname(name) == '.tex' or path.extname(name) == '.tikz' or \
         @latex.manager.config?.latex_ext?.indexOf(path.extname(name)) > -1
       return true
     return false


much nicer approach for this

much nicer approach for this

--- lib/runtime/evaluation.coffee
+++ lib/runtime/evaluation.coffee
@@ -13,20 +13,19 @@
     editor = atom.workspace.getActiveTextEditor()
     mod = modules.current()
     edpath = editor.getPath() || 'untitled-' + editor.getBuffer().inkId
-    for {range, line, text} in blocks.get editor
-      do (range, line, text) =>
-        [[start], [end]] = range
-        @ink.highlight editor, start, end
-        evaluate({text, line: line+1, mod, path: edpath}).then (result) =>
-          error = result.type == 'error'
-          view = if error then result.view else result
-          r = new @ink.Result editor, [start, end],
-            content: views.render view
-            error: error
-          r.view.classList.add 'julia'
-          if error and result.highlights?
-            @showError r, result.highlights
-          notifications.show "Evaluation Finished"
+    blocks.get(editor).forEach ({range, line, text}) ->
+      [[start], [end]] = range
+      @ink.highlight editor, start, end
+      evaluate({text, line: line+1, mod, path: edpath}).then (result) =>
+        error = result.type == 'error'
+        view = if error then result.view else result
+        r = new @ink.Result editor, [start, end],
+          content: views.render view
+          error: error
+        r.view.classList.add 'julia'
+        if error and result.highlights?
+          @showError r, result.highlights
+        notifications.show "Evaluation Finished"

   # get documentation or methods for the current word
   toggleMeta: (type) ->


Use prev/next swipe functionality of photoview and add preview of photo messages

--- client.coffee
+++ client.coffee
@@ -17,14 +17,13 @@
 	contactUserId = 3-myUserId

 exports.render = !->
-	log 'render'
-
+
 	Dom.style
 		fontSize: '90%'

 	Chat.renderMessages
 		newCount: Obs.peek -> Event.getUnread() || 0
-		content: (msg, num) !->
+		content: messageContent = (msg, num) !->
 			return if !msg.isHash()
 			byUserId = msg.get('by')
 			name = Plugin.userName byUserId
@@ -59,8 +58,16 @@

 				Dom.div !->
 					Dom.cls 'chat-content'
-					photoKey = msg.get('photo')
-					if photoKey
+					if  (photoUpload = msg.get("photoUpload"))
+						Dom.div !->
+							Dom.style
+								background: "url(#{msg.get('thumb')}) 50% 50% no-repeat"
+								backgroundSize: 'cover'
+								width: "90px"
+								height: "70px"
+								padding: "40px 0 0 60px"
+							Ui.spinner 30
+					else if (photoKey = msg.get('photo'))
 						Dom.div !->
 							Dom.cls 'img'
 							Dom.style
@@ -73,7 +80,6 @@
 							Dom.onTap !->
 								Page.nav !->
 									renderPhoto msg, num
-
 					else if photoKey is ''
 						Dom.div !->
 							Dom.cls 'chat-nophoto'
@@ -94,15 +100,24 @@
 							Dom.text tr("sending")
 							Ui.dots()

-					Dom.onTap !->
-						msgModal msg, num
+					if !photoUpload
+						Dom.onTap !->
+							msgModal msg, num
+
+	Obs.observe !->
+		Photo.uploads.iterate (newPhoto) !-> # Previews of image uploads
+			Page.scroll 'down', true # Scroll to the preview of the image that is getting uploaded
+			preview = Obs.create newPhoto.get()
+			preview.set "photoUpload", true
+			preview.set "by", Plugin.userId()
+			messageContent(preview)

 	typingSub = Obs.create {}
 	Server.send 'typingSub', (delta) !-> typingSub.patch delta
 	Dom.div !->
 		wasNearBottom = Page.nearBottom()
 		users = (Plugin.userName(userId) for userId of typingSub.get() when +userId isnt myUserId)
-
+
 		Dom.style
 			fontSize: '80%'
 			padding: '4px 0'
@@ -123,42 +138,55 @@
 			placeholder: tr("Send a message...")

 renderPhoto = (msg, num) !->
-
-	byUserId = msg.get('by')
-	photoKey = msg.get('photo')
-
-	Page.setTitle tr("Photo")
-	Page.setSubTitle tr("added by %1", Plugin.userName(byUserId))
-	opts = []
-	if Photo.share
-		opts.push
-			label: tr("Share")
-			icon: 'share'
-			action: !-> Photo.share photoKey
-	if Photo.download
-		opts.push
-			label: tr("Download")
-			icon: 'boxdown'
-			action: !-> Photo.download photoKey
-	if byUserId is myUserId or Plugin.userIsAdmin()
-		opts.push
-			label: tr("Remove")
-			icon: 'trash'
-			action: !->
-				require('modal').confirm null, tr("Remove photo?"), !->
-					Server.sync 'removePhoto', num, !->
-						msg.set('photo', '')
-					Page.back()
-	Page.setActions opts
-
 	Dom.style
 		padding: 0
-		backgroundColor: '#444'
-	#Dom.img !->
-	#	Dom.prop src: Photo.url(photoKey, 800)
-
+
+	content = (identifier) !->
+		message = Db.shared.ref(0|identifier/100, identifier%100)
+		return if !message?
+		byUserId = message.get('by')
+		photoKey = message.get('photo')
+		return if !byUserId? or !photoKey?
+		Page.setTitle tr("Posted by %1", Plugin.userName(byUserId))
+		opts = []
+		if Photo.share
+			opts.push
+				label: tr("Share")
+				icon: 'share'
+				action: !-> Photo.share photoKey
+		if Photo.download
+			opts.push
+				label: tr("Download")
+				icon: 'boxdown'
+				action: !-> Photo.download photoKey
+		if byUserId is myUserId or Plugin.userIsAdmin()
+			opts.push
+				label: tr("Remove")
+				icon: 'trash'
+				action: !->
+					require('modal').confirm null, tr("Remove photo?"), !->
+						Server.sync 'removePhoto', identifier, !->
+							message.set('photo', '')
+						Page.back()
+		Page.setActions opts
+
 	(require 'photoview').render
-		key: photoKey
+		current: num
+		getNeighbourIds: (id) ->
+			max = Db.shared.peek('maxId')||0
+			right = parseInt(id)-1
+			while !((rightValue = Db.shared.peek(0|right/100, right%100, "photo"))?) && right > 0
+				right--
+			left = parseInt(id)+1
+			while !((leftValue = Db.shared.peek(0|left/100, left%100, "photo"))?) && left <= max
+				left++
+			left = undefined if !leftValue? or leftValue.length is 0
+			right = undefined if !rightValue? or rightValue.length is 0
+			[left,right]
+		idToPhotoKey: (id) ->
+			Db.shared.get 0|id/100, id%100, 'photo'
+		fullHeight: true
+		content: content

 msgModal = (msg, num) !->
 	time = msg.get('time')


Framework for failed updates

Framework for failed updates

--- lib/package-updates-status-view.coffee
+++ lib/package-updates-status-view.coffee
@@ -11,10 +11,12 @@
   initialize: (@statusBar, packageManager, @updates) ->
     @destroyed = true
     @updatingPackages = []
+    @failedUpdates = []

     packageManager.on 'package-updated theme-updated', ({error, pack}) => @onDidUpdatePackage(pack)
     packageManager.on 'package-update-available theme-update-available', ({error, pack}) => @onPackageUpdateAvailable(pack)
     packageManager.on 'package-updating theme-updating', ({error, pack}) => @onPackageUpdating(pack)
+    packageManager.on 'package-update-failed theme-update-failed', ({error, pack}) => @onPackageUpdateFailed(pack)

     @updateTile()

@@ -44,6 +46,14 @@
     @updatingPackages.push(pack)
     @updateTile()

+  onPackageUpdateFailed: (pack) ->
+    for update in @failedUpdates
+      if update.name is pack.name
+        return
+
+    @failedUpdates.push(pack)
+    # @updateTile()
+
   updateTile: ->
     if @updates.length
       if @destroyed


Fix blinking cursor CSS content

Fix blinking cursor CSS content

--- helper.coffee
+++ helper.coffee
@@ -38,7 +38,7 @@
       }

       .eager-typist-element:after {
-        content: "\00a0";
+        content: "\\00a0";
         display: inline-block;
         margin-left: .125em;
         width: .125em;


fix user admin user interaction in setting up the checkbox

--- plugins/010-users/app/assets/javascripts/users.js.coffee
+++ plugins/010-users/app/assets/javascripts/users.js.coffee
@@ -84,15 +84,14 @@
 		(if element then $(element).parents("form:first") else $("#new-user-form"))

 	initialize: () ->
+		_this = this
 		SmartLinks.initialize
 			open_selector: ".open-username-edit"
 			close_selector: ".close-username-edit"
 			onShow: (open_link) ->
-				user = $(this).user(open_link)
-				user_id = $(this).parse_id(user.attr("id"))
-				open_link.after Templates.run("updateUsername",
-					user_id: user_id
-				)
+				user = _this.user(open_link)
+				user_id = _this.parse_id(user.attr("id"))
+				open_link.after Templates.run("updateUsername", user_id: user_id)
 				form = open_link.next()
 				FormHelpers.update_first form, open_link.text()
 				FormHelpers.focus_first form
@@ -100,8 +99,8 @@
 			selector: ".user_admin_checkbox"
 			parentSelector: "span:first"
 			success: (rc, checkbox) ->
-				$(this).user(checkbox).find(".user_icons:first").toggleClass "user_admin"
-				$(this).delete_area_toggle checkbox
+				_this.user(checkbox).find(".user_icons:first").toggleClass "user_admin"
+				_this.delete_area_toggle checkbox

 $(document).ready ->
 	Users.initialize()


Fix an express warning

Fix an express warning

--- server/controllers/leavegoogle.coffee
+++ server/controllers/leavegoogle.coffee
@@ -21,7 +21,7 @@

 module.exports.lg = (req, res, next) ->

-    res.send 200
+    res.sendStatus 200

     {auth_code, scope} = req.body
     #scope is like this :


HLS: Fix Content-Length for segments with PTS tag

We weren't adding the length of our tag to the Content-Length, which was
causing players to lose 73 bytes from the end of the audio data,
corrupting an audio frame.

--- src/streammachine/outputs/live_streaming.coffee
+++ src/streammachine/outputs/live_streaming.coffee
@@ -24,18 +24,8 @@
                 @opts.res.status(404).end "Segment not found."
                 return false

-            headers =
-                "Content-Type":
-                    if @stream.opts.format == "mp3"         then "audio/mpeg"
-                    else if @stream.opts.format == "aac"    then "audio/aacp"
-                    else "unknown"
-                "Connection":           "close"
-                "Content-Length":       info.length
-
-            # write out our headers
-            @opts.res.writeHead 200, headers
-
             # write our PTS tag
+            tag = null
             if info.pts
                 tag = new Buffer(PTS_TAG)
                 # node 0.10 doesn't know how to write ints over 32-bit, so
@@ -46,7 +36,18 @@
                 else
                     tag.writeUInt32BE(info.pts,0x45)

-                @opts.res.write tag
+            headers =
+                "Content-Type":
+                    if @stream.opts.format == "mp3"         then "audio/mpeg"
+                    else if @stream.opts.format == "aac"    then "audio/aac"
+                    else "unknown"
+                "Connection":           "close"
+                "Content-Length":       info.length + tag?.length||0
+
+            # write out our headers
+            @opts.res.writeHead 200, headers
+
+            @opts.res.write tag if tag

             # send our pump buffer to the client
             playHead.pipe(@opts.res)


Fix logs

Fix logs

--- server/testSystems/Informatics.coffee
+++ server/testSystems/Informatics.coffee
@@ -168,7 +168,7 @@
         informaticsProblemId = @_informaticsProblemId(problemId)
         success = false
         for i in [1..8]
-            logger.info "Try submit #{user}, #{problemId}, attempt #{i}"
+            logger.info "Try submit #{user.username}, #{user.informaticsId} #{problemId}, attempt #{i}"
             try
                 oldSubmits = await Submit.findByUserAndProblem(user.informaticsId, problemId)
                 try
@@ -177,14 +177,14 @@
                 finally
                     await downloadSubmits.runForUser(user.informaticsId, 5, 1)
             catch e
-                logger.info "Error submitting #{user} #{problemId} attempt #{i}: ", e.message
+                logger.info "Error submitting #{user.username}, #{user.informaticsId} #{problemId} attempt #{i}: ", e.message
             newSubmits = await Submit.findByUserAndProblem(user.informaticsId, problemId)
             if oldSubmits.length != newSubmits.length
-                logger.info "However, submit #{user} #{problemId} appeared after downloadSubmits from attempt #{i}"
+                logger.info "However, submit #{user.username}, #{user.informaticsId} #{problemId} appeared after downloadSubmits from attempt #{i}"
                 success = true
                 break
         if not success
-            throw "Can't submit #{user} #{problemId}"
+            throw "Can't submit #{user.username}, #{user.informaticsId} #{problemId}"

     registerUser: (user) ->
         logger.info "Moving user #{user._id} to unknown group"


Declare options for --help usage.

Declare options for --help usage.

--- slide/index.coffee
+++ slide/index.coffee
@@ -4,6 +4,20 @@
 semver = require 'semver'

 module.exports = class SlideGenerator extends yeoman.generators.NamedBase
+    constructor: (args, options, config) ->
+        super args, options, config
+        @option 'notes',
+            desc: 'Include speaker notes'
+            type: Boolean
+            default: false
+        @option 'markdown',
+            desc: 'Use markdown'
+            type: Boolean
+            default: false
+        @option 'attributes',
+            desc: 'Include (data-)attributes on markdown section. This only works combinded with --markdown.'
+            type: Boolean
+            default: false

     files: ->
         appPath = process.cwd()


increase saving alert message from 1 second to 3 seconds

--- services/web/public/coffee/ide.coffee
+++ services/web/public/coffee/ide.coffee
@@ -207,7 +207,7 @@
 			@timeOut = setTimeout((=>
 				ga?('send', 'event', 'editor-interaction', 'notification-shown', "saving")
 				$("#savingProblems").show()
-			), 1000)
+			), 3000)

 		clearTimeout:->
 			if @timeOut?


minor kernel changes

minor kernel changes

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -198,13 +198,15 @@
         assert ident = try kinded.identify() or null
         error = "could not find a %s in the registry"
         noKinded = "the supplied arg has to be class"
+        success = "Successfully accquired %s service"
         formatted = try format error, ident.toString()
         assert _.isArray registry = @router?.registry
         assert _.isObject(kinded.__super__), noKinded
         look = (fxc) -> try fxc.objectOf kinded, yes
         spoted = _.find(registry, look) or undefined
         assert _.isObject(spoted) or silent, formatted
-        try spotted.accuired? kinded, silent; spoted
+        logger.debug success.grey, try ident.underline
+        try spoted.accquired? kinded, silent; spoted

     # This routines sets up the infrastructure necessary for kernel
     # to properly intercept and process errors and exceptions. This


Remove some leftovers in the kernel

Remove some leftovers in the kernel

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -162,23 +162,21 @@
     # the determined scope and the router, which is then are wired
     # in with the located and instantiated services. Please refer
     # to the implementation on how and what is being done exactly.
-    setupRoutableServices: (services...) ->
+    setupRoutableServices: ->
         tag = nconf.get "NODE_ENV"
         missing = "No NODE_ENV variable found"
-        c = (s) => @router.register new s @
-        throw new Error missing unless _.isString tag
+        assert _.isString(tag), missing
         @scope = scoping.Scope.lookupOrFail tag
         @scope.incorporate this
         @router = new routing.Router this
         @middleware = @router.middleware
         @middleware = @middleware.bind @router
-        c(s) for s in (services or [])

     # Setup the Connect middleware framework along with the default
     # pipeline of middlewares necessary for the Flames framework to
     # operate correctly. You are encouraged to override this method
     # to provide a Connect setup procedure to your own liking, etc.
-    setupConnectPipeline: (middlewares...) ->
+    setupConnectPipeline: ->
         @connect = connect()
         @connect.use connect.query()
         @connect.use connect.favicon()
@@ -190,5 +188,4 @@
         @connect.use plumbs.accepts this
         @connect.use plumbs.sender this
         @connect.use plumbs.logger this
-        @connect.use m for m in middlewares
         @connect.use @middleware


minor change in the plumbing

minor change in the plumbing

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -51,7 +51,7 @@
     writer = (d) -> logger.log level, filter d
     assert options = stream: write: writer
     assert options.format = "#{format}"
-    return connect.logger options
+    return try connect.logger options

 # This middleware is really a wrapper around the `Connect` session
 # middleware. The reason it wraps it is to automatically configure


Main.default: add default secretkey for jwt

we may need to auto generate this

--- config/main.default.coffee
+++ config/main.default.coffee
@@ -309,7 +309,7 @@
     contentRotatorUrl              : 'http://koding.github.io'
     collaboration                  : {timeout: 1 * 60 * 1000}
     client                         : {watch: yes                                                     , version: version                                              , includesPath:'client' , indexMaster: "index-master.html" , index: "default.html" , useStaticFileServer: no , staticFilesBaseUrl: "#{customDomain.public}:#{customDomain.port}"}
-    jwt                            : {secret: ""                     , confirmExpiresInMinutes: 10080  } # 7 days
+    jwt                            : {secret: 'somesecretkeyhere'       , confirmExpiresInMinutes: 10080  } # 7 days
     papertrail                     : {destination: ''                   , groupId: 2199093                                              , token: '' }
     sendEventsToSegment            : options.sendEventsToSegment
     mailgun                        : mailgun


Fix URL in callback

Fix URL in callback

--- plugins/jira/index.coffee
+++ plugins/jira/index.coffee
@@ -53,6 +53,6 @@
         callback null,
           id: res.body.id
           key: res.body.key
-          url: "#{config.host}/browse/#{res.body.key}"
+          url: "#{host}/browse/#{res.body.key}"

 module.exports = Jira


oneliners are nice :)

oneliners are nice :)

--- core/app/backbone/models/channel.coffee
+++ core/app/backbone/models/channel.coffee
@@ -13,8 +13,7 @@
   setNotActive: ->
     @isActive = false

-  user: ->
-    new User(@get("created_by"))
+  user: -> new User(@get("created_by"))

   cached: (field, retrieval) ->
     @cached ||= {}


wip: redesigning sessions

wip: redesigning sessions

--- lib/session.coffee
+++ lib/session.coffee
@@ -1,4 +1,5 @@
 utils = require('./utils')
+UserSession = require('./user_session').UserSession

 class exports.Session

@@ -52,7 +53,8 @@
   # Users
   assignUser: (data) ->
     return null unless data.user_id
-    @user = JSON.parse data.attributes
+    @user = new UserSession(data.user_id, @)
+    @user.attributes = JSON.parse(data.attributes) if data.attributes

   loggedIn: ->
     @user?
@@ -61,12 +63,12 @@
     @user = null
     @create (err, new_session) -> cb(null, new_session)

-  setUserAndAttributes: (id, user_data) ->
-    return null if id is undefined or user_data is undefined
-    R.hset @key(), 'user_id', id
+  setUserAndAttributes: (user_id, user_data) ->
+    return null if user_id is undefined or user_data is undefined
+    R.hset @key(), 'user_id', user_id
     @assignUser(user_data)
-    R.hset @key(), 'attributes', JSON.stringify Object.extend @getAttributes(), user_data # Set attributes
+    R.hset @key(), 'attributes', JSON.stringify(Object.extend(@getAttributes(), user_data)) # Set attributes

   getAttributes: ->
     attr = R.hget @key(), 'attributes'
-    JSON.parse if attr is undefined then '{}' else attr+    JSON.parse(attr || '{}')


document ruby 2.1.0

document ruby 2.1.0


changed spaces to tabs

changed spaces to tabs

--- src/reports.coffee
+++ src/reports.coffee
@@ -11,204 +11,194 @@
 # Function Sends the reports

 sendReports = (job, flag, done) ->
-  reportMap = {}
-  channelReportMap = {}
-  channelMap = {}
-
-  #Select the right subscribers for the report
-  if flag == 'dailyReport'
-    fetchUsers = fetchDailySubscribers
-  if flag == 'weeklyReport'
-    fetchUsers = fetchWeeklySubscribers
-
-  fetchUsers (err, users) ->
-    promises = []
-    userKey = ''
-    userIndex = 0
-    usersArray = []
-    for user in users
-      do (user) ->
-        deferred = Q.defer()
-        userKey = user.email
-        metrics.getAllowedChannels user
-        .then (result) ->
-          usersArray[userIndex] = user
-          usersArray[userIndex].allowedChannels = result
-          for channel in usersArray[userIndex].allowedChannels
-            channelMap[channel._id] =
-              user: user
-              channel: channel
-
-          userIndex++
-          deferred.resolve()
-
-        promises.push deferred.promise
+	reportMap = {}
+	channelReportMap = {}
+	channelMap = {}
+
+	#Select the right subscribers for the report
+	if flag == 'dailyReport'
+		fetchUsers = fetchDailySubscribers
+	if flag == 'weeklyReport'
+		fetchUsers = fetchWeeklySubscribers
+
+	fetchUsers (err, users) ->
+		promises = []
+		userKey = ''
+		userIndex = 0
+		usersArray = []
+		for user in users
+			do (user) ->
+				deferred = Q.defer()
+				userKey = user.email
+				metrics.getAllowedChannels user
+				.then (result) ->
+					usersArray[userIndex] = user
+					usersArray[userIndex].allowedChannels = result
+					for channel in usersArray[userIndex].allowedChannels
+						channelMap[channel._id] =
+							user: user
+							channel: channel
+
+					userIndex++
+					deferred.resolve()
+
+				promises.push deferred.promise

 #   Loop through the enriched user array
-    innerPromises = []
-    (Q.all promises).then ->
+		innerPromises = []
+		(Q.all promises).then ->
 #     Pre-Fetch report data into Channel Map
-      for key , obj of channelMap
-        innerDeferred = Q.defer()
-        fetchChannelReport obj.channel,obj.user,flag, (item) ->
-          channelReportMap[key] = item
-          innerDeferred.resolve()
-        innerPromises.push innerDeferred.promise
-
-      (Q.all innerPromises).then ->
-        for user in usersArray
-          userKey = user.email
-          for channel in user.allowedChannels
-            do (channel) ->
-              if (reportMap[userKey])
-                # Do nothing since object already exists
-              else
-                # Create the object
-                reportMap[userKey] =
-                  email: user.email
-                  data: []
-
-              # If report has been fetched get it from the map
-              if channelReportMap[channel._id]
-                reportMap[userKey].data.push channelReportMap[channel._id]
-              else
-                logger.info 'should never be here since channels have been pre-fetched'
+			for key , obj of channelMap
+				innerDeferred = Q.defer()
+				fetchChannelReport obj.channel, obj.user, flag, (item) ->
+					channelReportMap[key] = item
+					innerDeferred.resolve()
+				innerPromises.push innerDeferred.promise
+
+			(Q.all innerPromises).then ->
+				for user in usersArray
+					userKey = user.email
+					for channel in user.allowedChannels
+						do (channel) ->
+							if (reportMap[userKey])
+								# Do nothing since object already exists
+							else
+								# Create the object
+								reportMap[userKey] =
+									email: user.email
+									data: []
+
+							# If report has been fetched get it from the map
+							if channelReportMap[channel._id]
+								reportMap[userKey].data.push channelReportMap[channel._id]
+							else
+								logger.info 'should never be here since channels have been pre-fetched'

 #       Iterate over reports and send the emails
-        for key, report of reportMap
-          if flag == 'dailyReport'
-            report.type = 'Daily'
-          else
-            report.type = 'Weekly'
-
-          sendUserEmail report
-
-      done()
-
-
-
-
+				for key, report of reportMap
+					if flag == 'dailyReport'
+						report.type = 'Daily'
+					else
+						report.type = 'Weekly'
+
+					sendUserEmail report
+
+			done()

 sendUserEmail = (report) ->
-  contact.contactUser 'email', report.email, report.type + ' report for ' + report.email , plainTemplate(report), htmlTemplate(report) , afterEmail
-
-
-
-fetchChannelReport = (channel,user,flag,callback) ->
-  if flag == 'dailyReport'
-    from = moment().startOf('day').toDate()
-    to = moment().endOf('day').toDate()
-    period = 'day'
-  else
-    from = moment().startOf('week').toDate()
-    to = moment().endOf('week').toDate()
-    period = 'week'
-
-  item = {}
-
-  logger.info 'fetching ' + flag + ' for #' + channel.name + ' ' + user.email + ' ' + channel._id
-  metrics.fetchChannelMetrics period ,channel._id,user,
-    startDate:  from
-    endDate: to
-
-  .then (data) ->
-    item.channel = channel
-    item.data = data
-    #Then fetch status metrics
-
-    metrics.fetchChannelMetrics 'status',channel._id,user,
-      startDate:from
-      endDate: to
-
-    .then (statusData) ->
-      item.statusData = statusData
-      callback item
+	contact.contactUser 'email', report.email, report.type + ' report for ' + report.email, plainTemplate(report), htmlTemplate(report), afterEmail
+
+
+fetchChannelReport = (channel, user, flag, callback) ->
+	if flag == 'dailyReport'
+		from = moment().startOf('day').toDate()
+		to = moment().endOf('day').toDate()
+		period = 'day'
+	else
+		from = moment().startOf('week').toDate()
+		to = moment().endOf('week').toDate()
+		period = 'week'
+
+	item = {}
+
+	logger.info 'fetching ' + flag + ' for #' + channel.name + ' ' + user.email + ' ' + channel._id
+	metrics.fetchChannelMetrics period, channel._id, user,
+		startDate: from
+		endDate: to
+
+	.then (data) ->
+		item.channel = channel
+		item.data = data
+		#Then fetch status metrics
+
+		metrics.fetchChannelMetrics 'status', channel._id, user,
+			startDate: from
+			endDate: to
+
+		.then (statusData) ->
+			item.statusData = statusData
+			callback item

 fetchDailySubscribers = (callback) ->
-  User.find { dailyReport: true }, callback
+	User.find { dailyReport: true }, callback

 fetchWeeklySubscribers = (callback) ->
-  User.find { weeklyReport: true }, callback
+	User.find { weeklyReport: true }, callback

 plainTemplate = (report) ->
-  for data in report.data
-    do (data) ->
-      " \r\n \r\n <---------- Start Channel  #{data.channel.name} ---------------------------> \r\n \r\n
-          Channel Name: #{data.channel.name} \r\n
-          Channel Load: #{ data.data[0].load } transactions  \r\n
-          Ave response time: #{ data.data[0].avgResp } \r\n
-          Failed:  #{ data.statusData[0].failed }  \r\n
-          Successful:  #{ data.statusData[0].successful }  \r\n
-          Processing: #{ data.statusData[0].processing }  \r\n
-          Completed:  #{ data.statusData[0].completed }  \r\n
-          Completed with errors: #{ data.statusData[0].completedWErrors } \r\n \r\n
-          <---------- End Channel -------------------------------------------------> \r\n \r\n
-        \r\n
-        \r\n
-      "
+	for data in report.data
+		do (data) ->
+			" \r\n \r\n <---------- Start Channel  #{data.channel.name} ---------------------------> \r\n \r\n
+			          Channel Name: #{data.channel.name} \r\n
+			          Channel Load: #{ data.data[0].load } transactions  \r\n
+			          Ave response time: #{ data.data[0].avgResp } \r\n
+			          Failed:  #{ data.statusData[0].failed }  \r\n
+			          Successful:  #{ data.statusData[0].successful }  \r\n
+			          Processing: #{ data.statusData[0].processing }  \r\n
+			          Completed:  #{ data.statusData[0].completed }  \r\n
+			          Completed with errors: #{ data.statusData[0].completedWErrors } \r\n \r\n
+			          <---------- End Channel -------------------------------------------------> \r\n \r\n
+			        \r\n
+			        \r\n
+			      "
 htmlTemplate = (report) ->
-  text = "
-
-
-
-
#{report.type} OpenHIM Transactions Summary

-

-
on the OpenHIM instance running on #{config.alerts.himInstance}:

-

-

-    Channel Name
-    Channel Load
-    Ave response time
-    Failed
-    Successful
-    Processing
-    Completed
-    Completed with errors
-

-      "
-  for data in report.data
-    do (data) ->
-      text += "
#{data.channel.name}	"
-      text += " #{ data.data[0].load } transactions 	"
-      text += " #{ data.data[0].avgResp } 	"
-      text += " #{ data.statusData[0].failed }  	"
-      text += " #{ data.statusData[0].successful }  	"
-      text += " #{ data.statusData[0].processing }  	"
-      text += " #{ data.statusData[0].completed }  	"
-      text += " #{ data.statusData[0].completedWErrors }
"
-  text += "
-

-

-
-
-  "
-  text
-
-
-
-
-
+	text = "
+
+
+
+
#{report.type} OpenHIM Transactions Summary

+

+
on the OpenHIM instance running on #{config.alerts.himInstance}:

+

+

+	    Channel Name
+	    Channel Load
+	    Ave response time
+	    Failed
+	    Successful
+	    Processing
+	    Completed
+	    Completed with errors
+

+	      "
+	for data in report.data
+		do (data) ->
+			text += "
#{data.channel.name}	"
+			text += " #{ data.data[0].load } transactions 	"
+			text += " #{ data.data[0].avgResp } 	"
+			text += " #{ data.statusData[0].failed }  	"
+			text += " #{ data.statusData[0].successful }  	"
+			text += " #{ data.statusData[0].processing }  	"
+			text += " #{ data.statusData[0].completed }  	"
+			text += " #{ data.statusData[0].completedWErrors }
"
+	text += "
+

+

+
+
+	  "
+	text

 afterEmail = (callback) ->
-  logger.info 'email sent..'
+	logger.info 'email sent..'

 setupAgenda = (agenda) ->
-  agenda.define 'send weekly channel metrics', (job, done) ->
-    sendReports job,'weeklyReport', done
-
-  agenda.define 'send daily channel metrics', (job, done) ->
-    sendReports job,'dailyReport', done
-
-  agenda.every "1 weeks", 'send weekly channel metrics'
-  agenda.every "1 days", 'send daily channel metrics'
+	agenda.define 'send weekly channel metrics', (job, done) ->
+		sendReports job, 'weeklyReport', done
+
+	agenda.define 'send daily channel metrics', (job, done) ->
+		sendReports job, 'dailyReport', done
+
+	agenda.every "1 weeks", 'send weekly channel metrics'
+	agenda.every "1 days", 'send daily channel metrics'

 exports.setupAgenda = setupAgenda

 if process.env.NODE_ENV == "test"
-  exports.sendReports = sendReports
-  exports.fetchDailySubscribers = fetchDailySubscribers
-  exports.fetchWeeklySubscribers = fetchWeeklySubscribers
-  exports.fetchChannelReport = fetchChannelReport
+	exports.sendReports = sendReports
+	exports.fetchDailySubscribers = fetchDailySubscribers
+	exports.fetchWeeklySubscribers = fetchWeeklySubscribers
+	exports.fetchChannelReport = fetchChannelReport


include the reset time in rate limit data

--- src/request.coffee
+++ src/request.coffee
@@ -58,9 +58,8 @@
   clientOptions.useETags ?= true
   clientOptions.usePostInsteadOfPatch ?= false

-  # These are updated whenever a request is made
-  emitter = clientOptions.emitter or
-    emit: ->
+  # These are updated whenever a request is made (optional)
+  emitter = clientOptions.emitter

   # Cached responses are stored in this object keyed by `path`
   _cachedETags = {}
@@ -147,14 +146,24 @@
         204: () => cb(null, true)
         404: () => cb(null, false)

+    emitter?.emit('start', method, path, data, options)
+
     ajax ajaxConfig, (err, val) ->

       jqXHR = err or val
       # Fire listeners when the request completes or fails
-      rateLimit = parseFloat(jqXHR.getResponseHeader 'X-RateLimit-Limit')
-      rateLimitRemaining = parseFloat(jqXHR.getResponseHeader 'X-RateLimit-Remaining')
-
-      emitter.emit('request', rateLimitRemaining, rateLimit, method, path, data, options)
+
+      if emitter
+        rateLimit = parseFloat(jqXHR.getResponseHeader('X-RateLimit-Limit'))
+        rateLimitRemaining = parseFloat(jqXHR.getResponseHeader('X-RateLimit-Remaining'))
+        rateLimitReset = parseFloat(jqXHR.getResponseHeader('X-RateLimit-Reset'))
+
+        emitterRate =
+          rate:   # to match the JSON format of `GET /rate_limit`
+            remaining: rateLimitRemaining
+            limit: rateLimit
+            reset: rateLimitReset # Reset time is in seconds, not milliseconds
+        emitter.emit('request', emitterRate, method, path, data, options, jqXHR.status)

       unless err
         # Return the result and Base64 encode it if `options.isBase64` flag is set.


fixing scroll top offset so chapter section is visible when scrolling up

--- src/components/exercises/sectionizer.cjsx
+++ src/components/exercises/sectionizer.cjsx
@@ -32,7 +32,7 @@

   # the below properties are read by the ScrollTo mixin
   scrollingTargetDOM: -> window.document
-  getScrollTopOffset: -> 80 # 70px high control bar and a bit of padding
+  getScrollTopOffset: -> 160 # 70px high control bar and a bit of padding

   componentDidMount: ->
     @calculateAvailableSpace(@state.windowEl or @_getWindowSize())


minor changes to the context assembly

minor changes to the context assembly


Fix deprecated selectors in keymap

Fix deprecated selectors in keymap

--- keymaps/encoding-selector.cson
+++ keymaps/encoding-selector.cson
@@ -1,8 +1,8 @@
-'.platform-darwin .editor':
+'.platform-darwin atom-text-editor':
   'ctrl-shift-u': 'encoding-selector:show'

-'.platform-win32 .editor':
+'.platform-win32 atom-text-editor':
   'ctrl-shift-u': 'encoding-selector:show'

-'.platform-linux .editor':
+'.platform-linux atom-text-editor':
   'ctrl-u': 'encoding-selector:show'


ie scroll fix

ie scroll fix

--- app/lib/tutorial.cjsx
+++ app/lib/tutorial.cjsx
@@ -95,7 +95,11 @@
     media: {}

   render: ->
-    <StepThrough ref="stepThrough" className="tutorial-steps">
+
+    isIE = 'ActiveXObject' of window
+    if isIE
+      tutorialStyle = height: '85vh'
+    <StepThrough ref="stepThrough" className="tutorial-steps" style={tutorialStyle}>
       {for step, i in @props.steps
         step._key ?= Math.random()
         <MediaCard key={step._key} className="tutorial-step" src={@props.media[step.media]?.src}>


Updates diff rendering to use new minimap lines API

--- lib/minimap-git-diff-binding.coffee
+++ lib/minimap-git-diff-binding.coffee
@@ -21,7 +21,6 @@
       @scheduleUpdate()

     @subscribeToBuffer()
-    @subscribeToMinimapView()

     @updateDiffs()

@@ -38,8 +37,6 @@
     @renderDiffs()

   renderDiffs: =>
-    lines = @minimapView.find('.lines')[0].childNodes[0].childNodes
-
     @removeDiffs()

     diffs = @getDiffs()
@@ -51,7 +48,7 @@
         for row in [newStart...newStart + newLines]
           start = displayBuffer.screenRowForBufferRow(row)
           end = displayBuffer.lastScreenRowForBufferRow(row)
-          @decorateLines(lines, start, end, 'added')
+          @decorateLines(start, end, 'added')

       else if newLines is 0 and oldLines > 0
         start = displayBuffer.screenRowForBufferRow(newStart)
@@ -61,23 +58,21 @@
         if start is 0 and start is end
           start = end = 1

-        @decorateLines(lines, start, end, 'removed')
+        @decorateLines(start, end, 'removed')

       else
         for row in [newStart...newStart + newLines]
           start = displayBuffer.screenRowForBufferRow(row)
           end = displayBuffer.lastScreenRowForBufferRow(row)
-          @decorateLines(lines, start, end, 'modified')
+          @decorateLines(start, end, 'modified')

-  decorateLines: (lines, start, end, status) ->
-    return if lines.length < end or lines.len
+  decorateLines: (start, end, status) ->

     for row in [start..end]
-      lines[row - 1].className += " git-line-#{status}"
+      @minimapView.addLineClass(row, "git-line-#{status}")

   removeDiffs: ->
-    @minimapView?.find('[class*="git-line-"]')
-    .removeClass('git-line-added git-line-removed git-line-modified')
+    @minimapView?.removeAllLineClasses('git-line-added', 'git-line-removed', 'git-line-modified')

   destroy: ->
     @deactivate()
@@ -99,7 +94,3 @@

     if @buffer = @editor.getBuffer()
       @buffer.on 'contents-modified', @updateDiffs
-
-  subscribeToMinimapView: =>
-    @subscribe @minimapView.miniEditorView, 'minimap:updated', =>
-      @updateDiffs()


Ignoring getPassword if enableFrontend is false

--- handlers/core.coffee
+++ handlers/core.coffee
@@ -43,7 +43,7 @@
 		timestamp: Date.now()
 		userID: message.sender

-	if message.message is '!getPassword'
+	if config.enableFrontend and message.message is '*getPassword'
 		crypto.randomBytes 20, (ex, buf) ->
 			token = (buf.toString 'hex').substring 0, 20
 			db.run "UPDATE users SET password = ? WHERE userID = ?", token, message.sender


make REFERENCE_YEAR dynamic

make REFERENCE_YEAR dynamic

--- src/scoring.coffee
+++ src/scoring.coffee
@@ -250,7 +250,7 @@
     base_guesses * match.token.length

   MIN_YEAR_SPACE: 20
-  REFERENCE_YEAR: 2017
+  REFERENCE_YEAR: new Date().getFullYear()

   regex_guesses: (match) ->
     char_class_bases =


search bugfix: certain optimal bruteforce matches are no longer ignored

--- src/scoring.coffee
+++ src/scoring.coffee
@@ -120,26 +120,24 @@
       optimal.m[k][l] = m
       optimal.pi[k][l] = pi

-    # helper: considers whether bruteforce matches ending at position k are optimal.
-    # three cases to consider...
+    # helper: evaluate bruteforce matches ending at k.
     bruteforce_update = (k) =>
-      # case 1: a bruteforce match spanning the full prefix.
+      # see if a single bruteforce match spanning the k-prefix is optimal.
       m = make_bruteforce_match(0, k)
       update(m, 1)
-      return if k == 0
-      for l, last_m of optimal.m[k - 1]
-        l = parseInt(l) # note: js stores object keys as strings
-        if last_m.pattern == 'bruteforce'
-          # case 2: if the optimal length-l sequence up to k - 1 ended in a bruteforce match,
-          # consider whether extending it by one character is optimal up to k.
-          # this preserves the sequence length l.
-          m = make_bruteforce_match(last_m.i, k)
-          update(m, l)
-        else
-          # case 3: if the optimal length-l sequence up to k - 1 ends in a non-bruteforce match,
-          # consider whether starting a new single-character bruteforce match is optimal.
-          # this adds a new match, adding 1 to the prior sequence length l.
-          m = make_bruteforce_match(k, k)
+      for i in [1..k]
+        # generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k).
+        # see if adding these new matches to any of the sequences in optimal[i-1]
+        # leads to new bests.
+        m = make_bruteforce_match(i, k)
+        for l, last_m of optimal.m[i-1]
+          l = parseInt(l)
+          # corner: an optimal sequence will never have two adjacent bruteforce matches.
+          # it is strictly better to have a single bruteforce match spanning the same region:
+          # same contribution to the guess product with a lower length.
+          # --> safe to skip those cases.
+          continue if last_m.pattern == 'bruteforce'
+          # try adding m to this length-l sequence.
           update(m, l + 1)

     # helper: make bruteforce match objects spanning i to j, inclusive.


#145693347 remove the browser alert (pop up) that shows to the end user when there is an error with conditional logic

--- app/assets/javascripts/hanuman/hanuman-conditional-logic.js.coffee
+++ app/assets/javascripts/hanuman/hanuman-conditional-logic.js.coffee
@@ -71,7 +71,6 @@
         type: "FAILED: conditional logic => condition container or element not found or found more than once"
         details: window.location.href

-      alert("We found a problem with the hide and show rules. We've been notified and are looking into it.")
     return

   #bind conditions to question


Update spec helper function to accept options object.

--- spec/compiler.coffee
+++ spec/compiler.coffee
@@ -9,17 +9,24 @@

 # Helper function for expecting errors to be thrown when parsing.
 #
-# @param source [String] GSS statements.
-# @param name [String] The name of the expected error.
-# @param [Boolean] Whether the spec should be treated as pending.
+# @param options [Object]
+# @option options source [String] GSS statements.
+# @option options name [String] The name of the expected error.
+# @option options before [Function] Called before the spec runs.
+# @option options after [Function] Called after the spec runs.
+# @option options pending [Boolean] Whether the spec should be treated as
+# pending.
 #
-expectError = (source, name, pending) ->
+expectError = (options) ->
+  {after, before, name, pending, source} = options
   itFn = if pending then xit else it

   describe name, ->
     itFn "should throw an error named \"#{name}\"", ->
+      before?()
       exercise = -> parser.compile source
       expect(exercise).to.throw Error
+      after?()

       # Chai doesn't provide a way to verify an error's name,
       # so do it manually.
@@ -302,18 +309,36 @@

   describe 'Errors', ->

-    expectError '@', 'Preparse error'
-
-    expectError '@grid-template simple "";', 'VGL parse error'
-
-    expectError '', 'VGL generated VFL parse error', true
-
-    expectError '', 'VGL generated CCSS parse error', true
-
-    expectError '@h [];', 'VFL parse error'
-
-    expectError '@h |-[#box]-| !requirre;', 'VFL generated CCSS parse error'
-
-    expectError '@if [target] === 960 {}', 'CCSS conditional parse error'
-
-    expectError '#box[right] === #box2[left];', 'Constraint parse error'
+    expectError
+      name: 'Preparse error'
+      source: '@'
+
+    expectError
+      name: 'VGL parse error'
+      source: '@grid-template simple "";'
+
+    expectError
+      name: 'VGL generated VFL parse error'
+      source: ''
+      pending: true
+
+    expectError
+      name: 'VGL generated CCSS parse error'
+      source: ''
+      pending: true
+
+    expectError
+      name: 'VFL parse error'
+      source: '@h [];'
+
+    expectError
+      name: 'VFL generated CCSS parse error'
+      source: '@h |-[#box]-| !requirre;'
+
+    expectError
+      name: 'CCSS conditional parse error'
+      source: '@if [target] === 960 {}'
+
+    expectError
+      name: 'Constraint parse error'
+      source: '#box[right] === #box2[left];'


Fix bad CalculateAge unit test

Fix bad CalculateAge unit test

This test has been plagueing us for a while.  It passes on days 1 - 16 of the current month, but fails after that.  I *think* we were just off by one, so this fix should do it.

--- Src/coffeescript/cql-execution/test/elm/clinical/test.coffee
+++ Src/coffeescript/cql-execution/test/elm/clinical/test.coffee
@@ -171,7 +171,7 @@
   it 'should execute age in months', ->
     # what is returned will depend on whether the day in the current month has
     # made it to the 17th day of the month as declared in the birthday
-    [@full_months, @full_months-1].indexOf(@months.exec(@ctx)).should.not.equal -1
+    [@full_months, @full_months+1].indexOf(@months.exec(@ctx)).should.not.equal -1

   # Skipping because cql-to-elm in this branch does not properly translate AgeInWeeks
   it.skip 'should execute age in weeks', ->


fix(search): button animation

fix(search): button animation

--- modules/directives/search/index.coffee
+++ modules/directives/search/index.coffee
@@ -35,7 +35,9 @@
       inputWidth = element.width() - (padding)
       $search?.css 'width', "#{inputWidth - cancelWidth - padding}px"
       $cancel.show()
-      element.addClass 'focus'
+      $timeout ->
+        element.addClass 'focus'
+      , 0

       # scroll out UI before search
       if element.prev().length


Reduce false positives on wb.

Reduce false positives on wb.

Issue #6

--- kills.coffee
+++ kills.coffee
@@ -76,7 +76,7 @@
   robot.hear /blue funnelcake/i, (msg) ->
     msg.send "Yeah, fuck those guys.  Rabble rabble."

-  robot.hear /wb|welcome back/i, (msg) ->
+  robot.hear /^wb [a-z]+|welcome back/i, (msg) ->
     msg.send "http://imagemacros.files.wordpress.com/2009/06/clowntrain.jpeg"

   robot.hear /failed|derped/i, (msg) ->


Adds a spec on 'path' event

Adds a spec on 'path' event

--- spec/traverse.coffee
+++ spec/traverse.coffee
@@ -14,9 +14,17 @@
     em.on 'error', ->
       done()

+  it 'should not emit an `path` event', (done)->
+    dfd = q.defer()
+    em = traverse 'qwertyuiopasdfghjklzxcvbnm' #That one either...
+    em.on 'error', ->
+      done()
+    em.on 'path', ->
+      done('should not have been called')
+
   # @TODO How to check that the event has NOT been raised, when the only other event is raised prior to it?
   #       Use timeouts ? Seems ugly...
-  xit 'should not emit an `end` event', ()->
+  xit 'should not emit an `end` event', (done)->
     dfd = q.defer()
     em = traverse 'qwertyuiopasdfghjklzxcvbnm' #That one either...
     em.on 'error', ->


Encode link tag attributes

Encode link tag attributes

--- src/sitemap.coffee
+++ src/sitemap.coffee
@@ -16,7 +16,7 @@
     xml = xml + "<image:image><image:loc>#{url.image}</image:loc></image:image>"
   if url.linkTags?
     for linkTag in url.linkTags
-      xml = xml + "<xhtml:link rel=\"#{linkTag.rel}\" href=\"#{linkTag.href}\"/>"
+      xml = xml + "<xhtml:link rel=\"#{encodeURIComponent(linkTag.rel)}\" href=\"#{encodeURIComponent(linkTag.href)}\"/>"
   xml + "</url>"

 module.exports = class Sitemap


moved logs button to service layers

moved logs button to service layers

--- imports/ui/pages/InstanceDetailPage.cjsx
+++ imports/ui/pages/InstanceDetailPage.cjsx
@@ -85,9 +85,6 @@
     renderSsh = (ssh) =>
       iconLink "ssh #{ssh.fqdn}", (@copyToClipboard "ssh #{ssh.fqdn}"), Icons.Base.Copy

-    renderLogsButton = (service, name) =>
-      <Button label='Logs' path="/instances/#{@props.instance.name}/#{name}/logs" />
-
     instanceHelper = Helpers.withInstance @props.instance

     if @props.instanceLink
@@ -136,20 +133,28 @@

         {_.map @props.instance.services, (service, name) =>

-            <Anchor reverse={true} href={@props.serviceLinks[name]} target='_blank'
-              icon={<Icons.Base.Link style={width:20} />}
-              label={{name}} />
+
+              <Anchor reverse={true} href={@props.serviceLinks[name]} target='_blank'
+                icon={<Icons.Base.Link style={width:20} />}
+                label={{name}} />
+
+              <Box flex={true} justify='end' direction='row'>
+                <Anchor reverse={true} target='_blank'
+                  path="/instances/#{@props.instance.name}/#{name}/logs"
+                  icon={<Icons.Base.DocumentTxt style={width:20} />}
+                  label={Logs} />
+              </Box>
+
             <List>
               {li 'Created', moment(service.container?.created).fromNow()}
               {li 'State', renderStatus service}
               {li 'FQDN', service.fqdn}
               {li 'Container name', service.container?.name}
               {li 'Ports', service.ports}
-              {li 'Network', renderNetwork service.aux?.net}
+              {li 'Network', renderNetwork service.aux?.net if service.aux?.net}
               {if service.aux?.ssh
                 li 'SSH', renderSsh service.aux.ssh
               }
-              {li 'Logs', renderLogsButton service, name}
             </List>

         }


typography

typography

--- app/assets/javascripts/controllers/OverviewController.coffee
+++ app/assets/javascripts/controllers/OverviewController.coffee
@@ -1,7 +1,7 @@
 controllers = angular.module("controllers")
 controllers.controller("OverviewController", [
   "$scope", "$routeParams", "IntervalRefresh", "Resques", "GenericErrorHandling", "Monitor",
-  ($scope ,  $routeParams ,  IntervalRefresh , Resques ,  GenericErrorHandling ,  Monitor)->
+  ($scope ,  $routeParams ,  IntervalRefresh ,  Resques ,  GenericErrorHandling ,  Monitor)->

     $scope.refresh = ->
       $scope.loading = true


fixed another bug in dashboard constructor / default options.

--- app/assets/javascripts/habberdash/models/dashboard.js.coffee
+++ app/assets/javascripts/habberdash/models/dashboard.js.coffee
@@ -9,7 +9,7 @@

   constructor: (attrs = {}) ->
-    super($.extend(Habberdash.Dashboard.defaults, attrs))
+    super($.extend({}, Habberdash.Dashboard.defaults, attrs))

   validate: ->


guard against crazy problem

guard against crazy problem

--- assets/js/view.coffee
+++ assets/js/view.coffee
@@ -870,10 +870,14 @@
     scrollPages: (npages) ->
       # TODO:  find out height per line, figure out number of lines to move down, scroll down corresponding height
       line_height = do $('.node-text').height
+      if line_height == 0
+        line_height = 21 # ugly hack... 0 happens when first line is empty, currently
       page_height = do $(document).height
       height = npages * page_height

       numlines = Math.round(height / line_height)
+      numlines = Math.max(Math.min(numlines, 1000), -1000) # guard against craziness
+
       if numlines > 0
         for i in [1..numlines]
           do @cursor.down


Normalize json payload from TaskSerializer

--- app/assets/javascripts/serializers/task_serializer.js.coffee
+++ app/assets/javascripts/serializers/task_serializer.js.coffee
@@ -1,4 +1,19 @@
 ETahi.TaskSerializer = DS.ActiveModelSerializer.extend
+  normalizePayload: (primaryType, payload) ->
+    tasks = payload.tasks
+    taskHash = _.reduce(tasks, (memo, task) ->
+      memo[task.id] = task
+      memo
+    , {})
+    for phase in payload.phases
+      taskObjs = []
+      for taskId in phase.task_ids
+        taskObjs.push {id: taskId, type: taskHash[taskId].type}
+      phase.tasks = taskObjs
+      delete phase.task_ids
+
+    payload
+
   serializeIntoHash: (data, type, record, options) ->
       root = 'task'
       data[root] = this.serialize(record, options)


more lat/lon order

more lat/lon order

--- source/javascripts/recruit-map.coffee
+++ source/javascripts/recruit-map.coffee
@@ -67,7 +67,7 @@
   topPlaces = hotspots[0..14].map (spot) -> spot.place = bygid[spot.key]; spot

   for recruit in env.recruits
-    coordinates = env.projection [recruit.lat, recruit.lon]
+    coordinates = env.projection [recruit.lon, recruit.lat]
     context.beginPath()
     context.arc coordinates[0], coordinates[1], radius(recruit.stars), 0, Math.PI * 2, false
     context.fillStyle = 'rgba(255, 255, 255, 0.3)' #'rgba(223, 0, 83, 0.9)' #env.colors[recruit.stars - 1]


Attempt to suppress tracks that are in the past

--- assets/front.coffee
+++ assets/front.coffee
@@ -246,6 +246,12 @@
 					# artist0, the next new track ID go into artist1, etc; any track
 					# with the same ID as a previously-seen track will be ignored.
 					# Signed: Chris Angelico (Rosuav).
+					now = window.serverTime()/1000
+					if now > segment.time+segment.duration
+						console.log("Segment ["+id+"] is in the past")
+						continue # Voorbij is nu voorbij :)
+					else
+						console.log("Segment ["+id+"] is in the future")
 					isnew = 1
 					for t in trackids
 						if t == id


cmd-shift-d

cmd-shift-d

--- src/atom/key-bindings.coffee
+++ src/atom/key-bindings.coffee
@@ -7,7 +7,7 @@
   'cmd-r': (window) -> window.reload()

 document:
-  'cmd-ctrl-d': -> console.log 'derp'
+  'cmd-shift-d': -> console.log 'derp'

 editor:
   'cmd-w': 'removeBuffer'


fix properties

fix properties

--- src/intermediate/IntermediateModel.coffee
+++ src/intermediate/IntermediateModel.coffee
@@ -1,23 +1,22 @@
 class @IntermediateModel
-  _events = {}
-  data = []
-
   constructor: ->
+    @_events = {}
+    @data = []
     @fetch()

   getData: ->
-    data
+    @data

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

   emit: (eventName) ->
-    for callback in _events[eventName]
+    for callback in @_events[eventName]
       callback()

   onChangeData: (callback) ->
-    _events['change_data'] = _events['change_data'] or []
-    _events['change_data'].push(callback)
+    @_events['change_data'] = @_events['change_data'] or []
+    @_events['change_data'].push(callback)

   fetch: ->
     # suppose ajax


Disable atom-script (in an effort to avoid using it)

--- atom/config.cson
+++ atom/config.cson
@@ -20,6 +20,7 @@
       "deprecation-cop"
       "language-c"
       "language-objective-c"
+      "script"
     ]
     projectHome: "~"
     themes: [


Add Atom ruby-test preferred spec framework

--- atom/config.cson
+++ atom/config.cson
@@ -37,5 +37,6 @@
     rspecFileCommand: "spring rspec --tty {relative_path}"
     rspecSingleCommand: "spring rspec --tty {relative_path}:{line_number}"
     shell: "zsh"
+    specFramework: "rspec"
   welcome:
     showOnStartup: false


atom update

atom update

--- atom/config.cson
+++ atom/config.cson
@@ -4,7 +4,6 @@
       "atom-ide-diagnostics-ui": false
   "autocomplete-plus":
     confirmCompletion: "tab"
-    defaultProvider: "Fuzzy"
   "autocomplete-python":
     useKite: false
   core:


Add a hook to merge local changes into the reloaded model

--- scripts/mixins/loadable.coffee
+++ scripts/mixins/loadable.coffee
@@ -42,7 +42,7 @@

     # Force a `Backbone.Collection` or `Backbone.Model` to reload its contents.
     # If in the middle of a `load` it waits until the load completes.
-    # Returns a Promise when the reload complates.
+    # Returns a Promise when the reload completes.
     reload: () ->
       # Finish reloading if loading has already started
       if @_loading
@@ -53,6 +53,9 @@
       else
         # For collections reset the contents to nothing
         @reset?()
-        return @load()
+        return @load().then () => @onReloaded()
+
+    # Hook to merge local unsaved changes into the remotely-updated model
+    onReloaded: () -> console.warn 'BUG: onReload SHOULD be implemented by subclasses'

   return loadableMixin


Add sort-lines hotkey change

Add sort-lines hotkey change

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -8,3 +8,7 @@
   'ctrl-shift-up': 'editor:move-line-up'
   'ctrl-down': 'unset!'
   'ctrl-shift-down': 'editor:move-line-down'
+
+'atom-text-editor:not([mini])':
+  'f5': 'unset!'
+  'f9': 'sort-lines:sort'


Fixed leading whitespace before interpolation in simple strings

--- lexer.coffee
+++ lexer.coffee
@@ -186,19 +186,15 @@
   # Matches strings, including multi-line strings. Ensures that quotation marks
   # are balanced within the string's contents, and within nested interpolations.
   stringToken: ->
-    switch @chunk.charAt 0
-      when "'"
-        return 0 unless match = SIMPLESTR.exec @chunk
-        string = match[0]
-        @token 'STRING', @escapeLines(string), 0, string.length
-      when '"'
-        return 0 unless string = @balancedString @chunk, '"'
-        if 0 < string.indexOf '#{', 1
-          @interpolateString string[1...-1], strOffset: 1, lexedLength: string.length
-        else
-          @token 'STRING', @escapeLines(string), 0, string.length
-      else
-        return 0
+    switch quote = @chunk.charAt 0
+      when "'" then [string] = SIMPLESTR.exec @chunk
+      when '"' then string = @balancedString @chunk, '"'
+    return 0 unless string
+    trimmed = @removeNewlines string[1...-1]
+    if quote is '"' and 0 < string.indexOf '#{', 1
+      @interpolateString trimmed, strOffset: 1, lexedLength: string.length
+    else
+      @token 'STRING', quote + @escapeLines(trimmed) + quote, 0, string.length
     if octalEsc = /^(?:\\.|[^\\])*\\(?:0[0-7]|[1-7])/.test string
       @error "octal escape sequences #{string} are not allowed"
     string.length
@@ -686,6 +682,11 @@
     @tag() in ['\\', '.', '?.', '?::', 'UNARY', 'MATH', '+', '-', 'SHIFT', 'RELATION'
                'COMPARE', 'LOGIC', 'THROW', 'EXTENDS']

+  # Remove newlines from beginning and (non escaped) from end of string literals.
+  removeNewlines: (str) ->
+    str.replace(/^\s*\n\s*/, '')
+       .replace(/([^\\]|\\\\)\s*\n\s*$/, '$1')
+
   # Converts newlines for string literals.
   escapeLines: (str, heredoc) ->
     # Ignore escaped backslashes and remove escaped newlines
@@ -695,9 +696,7 @@
       str.replace MULTILINER, '\\n'
     else
       # Trim leading and trailing whitespace, string includes quotes
-      str.replace(/^(.)\s*\n\s*/, '$1')
-         .replace(/\s*\n\s*(.)$/, '$1')
-         .replace(/\s*\n\s*/g, ' ')
+      str.replace /\s*\n\s*/g, ' '

   # Constructs a string token by escaping quotes and newlines.
   makeString: (body, quote, heredoc) ->


using double-quotes for auto-quoted object keys and hash accesses. JSON-style. Issue #556

--- lexer.coffee
+++ lexer.coffee
@@ -91,7 +91,7 @@
     if include(JS_FORBIDDEN, id)
       if forcedIdentifier
         tag = 'STRING'
-        id  = "'#id'"
+        id  = "\"#id\""
         if forcedIdentifier is 'accessor'
           close_index = true
           @tokens.pop() if @tag() isnt '@'


update.

update.

--- src/main/public/nz/SpriteCharacter.coffee
+++ src/main/public/nz/SpriteCharacter.coffee
@@ -119,7 +119,7 @@
     distance = enemy.position.distance @position
     if distance < (cw.height + @body.width / 2)
       v = tm.geom.Vector2 enemy.x - @x, enemy.y - @y
-      r = Math.radToDeg(v.toAngle()) - @body.rotation
+      r = (Math.radToDeg(v.toAngle()) - @body.rotation + 360) % 360
       if cw.rotation.start <= r and r <= cw.rotation.end
         @attackAnimation()
         @attack = false


Add a fix for broken Node.contains() in current Safari (WebKit/534.55.3). Closes #134.

--- src/range.coffee
+++ src/range.coffee
@@ -355,7 +355,38 @@
       if not range[p + 'Offset']?
         throw new Range.RangeError("#{p}offset", "Couldn't find offset #{this[p + 'Offset']} in element #{this[p]}")

-    range.commonAncestorContainer = $(range.startContainer).parents().has(range.endContainer)[0]
+    # Here's an elegant next step...
+    #
+    #   range.commonAncestorContainer = $(range.startContainer).parents().has(range.endContainer)[0]
+    #
+    # ...but unfortunately Node.contains() is broken in Safari 5.1.5 (7534.55.3)
+    # and presumably other earlier versions of WebKit. In particular, in a
+    # document like
+    #
+    #
Hello

+    #
+    # the code
+    #
+    #   p = document.getElementsByTagName('p')[0]
+    #   p.contains(p.firstChild)
+    #
+    # returns `false`. Yay.
+    #
+    # So instead, we step through the parents from the bottom up and use
+    # Node.compareDocumentPosition() to decide when to set the
+    # commonAncestorContainer and bail out.
+
+    contains = if not document.compareDocumentPosition?
+                 # IE
+                 (a, b) -> a.contains(b)
+               else
+                 # Everyone else
+                 (a, b) -> a.compareDocumentPosition(b) & 16
+
+    $(range.startContainer).parents().reverse().each ->
+      if contains(this, range.endContainer)
+        range.commonAncestorContainer = this
+        return false

     new Range.BrowserRange(range).normalize(root)


Range fixes

Range fixes

--- src/range.coffee
+++ src/range.coffee
@@ -73,8 +73,6 @@
   #   to get the end point.
   # * `columnDelta` A {Number} indicating how many rows to columns to the start
   #   point to get the end point.
-  #
-  # Returns a {Range}
   @fromPointWithDelta: (startPoint, rowDelta, columnDelta) ->
     startPoint = Point.fromObject(startPoint)
     endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta)
@@ -105,6 +103,8 @@

   # Public: Freezes the range and its start and end point so it becomes
   # immutable and returns itself.
+  #
+  # Returns an immutable version of this {Range}
   freeze: ->
     @start.freeze()
     @end.freeze()
@@ -113,7 +113,7 @@
   # Public: Returns a {Boolean} indicating whether this range has the same start
   # and end points as the given {Range} or range-compatible {Array}.
   #
-  # * otherRange: A {Range} or range-compatible {Array}.
+  # * `otherRange` A {Range} or range-compatible {Array}.
   isEqual: (other) ->
     return false unless other?
     other = @constructor.fromObject(other)
@@ -121,7 +121,7 @@

   # Public: Compare two Ranges
   #
-  # * other: A {Range} or range-compatible {Array}.
+  # * `otherRange` A {Range} or range-compatible {Array}.
   #
   # Returns `-1` if this range starts before the argument or contains it.
   # Returns `0` if this range is equivalent to the argument.
@@ -141,7 +141,7 @@
   # Public: Returns a {Boolean} indicating whether this range starts and ends on
   # the same row as the argument.
   #
-  # * otherRange: A {Range} or range-compatible {Array}.
+  # * `otherRange` A {Range} or range-compatible {Array}.
   coversSameRows: (other) ->
     @start.row == other.start.row && @end.row == other.end.row


server: fix problem with coffee script scope

--- server/lib/server/index.coffee
+++ server/lib/server/index.coffee
@@ -38,7 +38,6 @@
 {extend}   = require 'underscore'
 express    = require 'express'
 Broker     = require 'broker'
-request    = require 'request'
 fs         = require 'fs'
 hat        = require 'hat'
 nodePath   = require 'path'
@@ -96,7 +95,7 @@

 app.get "/-/imageProxy", (req, res)->
   if req.query.url
-    request(req.query.url).pipe(res)
+    require('request')(req.query.url).pipe(res)
   else
     res.send 404

@@ -147,7 +146,7 @@
                   body    : JSON.stringify postData
                   headers : {'content-type': 'application/json'}

-                request.post options, (error, response, body) =>
+                require('request').post options, (error, response, body) =>
                   if error
                     console.log "ERROR", error
                     res.send 401, JSON.stringify {error: "unauthorized - error code 2"}
@@ -321,8 +320,8 @@
           path    : "/user?access_token=#{access_token}"
           method  : "GET"
           headers : headers
-        request = http.request options, fetchUserInfo
-        request.end()
+        r = http.request options, fetchUserInfo
+        r.end()

   fetchUserInfo = (userInfoResp) ->
     rawResp = ""
@@ -343,8 +342,8 @@
           path    : "/user/emails?access_token=#{access_token}"
           method  : "GET"
           headers : headers
-        request = http.request options, (newResp)-> fetchUserEmail newResp, resp
-        request.end()
+        r = http.request options, (newResp)-> fetchUserEmail newResp, resp
+        r.end()
       else
         renderLoginTemplate resp, res

@@ -361,8 +360,8 @@
     path   : "/login/oauth/access_token?client_id=#{github.clientId}&client_secret=#{github.clientSecret}&code=#{code}"
     method : "POST"
     headers : headers
-  request = http.request options, authorizeUser
-  request.end()
+  r = http.request options, authorizeUser
+  r.end()

 app.get '/:name/:section?*', (req, res, next)->
   {JName} = koding.models


test: added get profile related helper

test: added get profile related helper

--- servers/test/testhelper.coffee
+++ servers/test/testhelper.coffee
@@ -503,6 +503,17 @@
     return requestParams

+  @generateGetProfileRequestParams = (opts = {}) ->
+
+    url                  = generateUrl route : "-/team/#{opts.email}"
+    defaultRequestParams = generateDefaultRequestParams { url }
+    requestParams        = deepObjectExtend defaultRequestParams, opts
+    # after deep extending object, encodes body param to a query string
+    requestParams.body   = querystring.stringify requestParams.body
+
+    return requestParams
+
+
 class LoginHandlerHelper

   @generateLoginRequestBody = (opts = {}) ->


Check the typeof a required script, so can warn when a function wasn't exported instead of a scary backtrace

--- src/robot.coffee
+++ src/robot.coffee
@@ -214,8 +214,14 @@
     full = Path.join path, Path.basename(file, ext)
     if require.extensions[ext]
       try
-        require(full) @
-        @parseHelp Path.join(path, file)
+        script = require(full)
+
+        if typeof script is 'function'
+          script @
+          @parseHelp Path.join(path, file)
+        else
+          @logger.warning "Expected #{full} to assign a function to module.exports, not #{typeof script}"
+
       catch error
         @logger.error "Unable to load #{full}: #{error.stack}"
         process.exit(1)


Add options argument to robot.http

Add options argument to robot.http

--- src/robot.coffee
+++ src/robot.coffee
@@ -418,6 +418,7 @@
   # send the request.
   #
   # url - String URL to access.
+  # options - Optional options to pass on to the client
   #
   # Examples:
   #
@@ -439,9 +440,12 @@
   #       .post(data) (err, res, body) ->
   #         console.log body
   #
+  #    # Can also set options
+  #    res.http("http://example.com", {rejectUnauthorized: false})
+  #
   # Returns a ScopedClient instance.
-  http: (url) ->
-    HttpClient.create(url)
+  http: (url, options) ->
+    HttpClient.create(url, options)
       .header('User-Agent', "Hubot/#{@version}")

 module.exports = Robot


Add comments for TODOs and information.

Add comments for TODOs and information.

--- src/coffee/csvmapping.coffee
+++ src/coffee/csvmapping.coffee
@@ -72,11 +72,16 @@
   formatMoney = (money) ->
     "#{money.currencyCode} #{money.centAmount}"

+  # states are expanded
+  # TODO: Catch case that only item is present and don't show delimiter
+  # TODO: Make delimiter configurable
   formatStates = (states) ->
     _.reduce states, (cell, state) ->
       "#{state.state.obj.key}:#{state.quantity};#{cell}"
     , ''

+  # TODO: Catch case that only item is present and don't show delimiter
+  # TODO: Make delimiter configurable
   formatImages = (images) ->
     _.reduce images, (cell, image) ->
       "#{image.url};#{cell}"


direct fair partner href directly to show (closes #2412)

--- models/fair.coffee
+++ models/fair.coffee
@@ -98,7 +98,13 @@
   aToZCollection: (namespace) =>
     href = @href()
     class FairSearchResult extends Backbone.Model
-      href: -> "#{href}/browse/#{namespace}/#{@get('id')}"
+      href: ->
+        if namespace is 'show' and @get('partner_show_ids')?[0]
+          "/show/#{@get('partner_show_ids')[0]}"
+        else
+          "#{href}/browse/#{namespace}/#{@get('id')}"
+
+
       displayName: -> @get('name')
       imageUrl: ->
         url = "#{sd.API_URL}/api/v1/profile/#{@get('default_profile_id')}/image"


Split up messages if longer than 4000 chars

--- src/slack.coffee
+++ src/slack.coffee
@@ -107,7 +107,26 @@
     for msg in messages
       @robot.logger.debug "Sending to #{envelope.room}: #{msg}"

-      channel.send msg
+      # If message is greater than 4000 chars, split it into multiple msessages
+      if msg.length > 4000
+        submessages = []
+
+        while msg.length > 0
+          # Split message at last line break, if it exists
+          chunk = msg.substring(0, 4000)
+          breakIndex = if chunk.lastIndexOf('\n') isnt -1 then chunk.lastIndexOf('\n') else 4000
+
+          submessages.push msg.substring(0, breakIndex)
+
+          # Skip char if split on line break
+          breakIndex++ if breakIndex isnt 4000
+
+          msg = msg.substring(breakIndex, msg.length)
+
+        channel.send m for m in submessages
+
+      else
+        channel.send msg

   reply: (envelope, messages...) ->
     @robot.logger.debug "Sending reply"


remove logic redundancy

remove logic redundancy

--- browser.coffee
+++ browser.coffee
@@ -68,9 +68,7 @@

   for script, index in coffees
     do (script, index) ->
-      mediatype = script?.type
-      return unless mediatype in coffeetypes
-      options = {literate: mediatype is coffeetypes[1]}
+      options = literate: script.type is coffeetypes[1]
       if script.src
         CoffeeScript.load script.src,
           (run) ->


getAuthorFromRequest fixed

getAuthorFromRequest fixed

--- src/slack.coffee
+++ src/slack.coffee
@@ -115,6 +115,8 @@
     # Return an author object
     id       : req.param 'user_id'
     name     : req.param 'user_name'
+    reply_to : req.param 'channel_id'
+    room     : req.param 'channel_name'

   userFromParams: (params) ->
     # hubot < 2.4.2: params = user


Extract some helper methods

Extract some helper methods

* HTML escaping
* logging
* option parsing
* author parsing
* message parsing

--- src/slack.coffee
+++ src/slack.coffee
@@ -4,26 +4,22 @@
 HTTPS = require 'https'

 class Slack extends Adapter
+  log: console.log.bind console
+  logError: console.error.bind console
+
   send: (params, strings...) ->
-    console.log "Sending message"
+    @log "Sending message"

     user = @userFromParams(params)

     strings.forEach (str) =>
-      # Escape this
-      str = str.replace(/&/g, '&')
-      str = str.replace(/</g, '<')
-      str = str.replace(/>/g, '>')
-
-      # Linkify. We assume that the bot is well-behaved and
-      # consistently sending links with the protocol part
-      str = str.replace(/((\bhttp)\S+)/g, '<$1>')
+      str = @escapeHtml str

       args = JSON.stringify({"channel": user.reply_to, "text": str, username: @robot.name})
       @post "/services/hooks/hubot", args

   reply: (params, strings...) ->
-    console.log "Sending reply"
+    @log "Sending reply"

     user = @userFromParams(params)
     strings.forEach (str) =>
@@ -31,6 +27,29 @@

   topic: (params, strings...) ->
     # TODO: Set the topic
+
+  escapeHtml: (string) ->
+    string
+      # Escape entities
+      .replace(/&/g, '&')
+      .replace(/</g, '<')
+      .replace(/>/g, '>')
+
+      # Linkify. We assume that the bot is well-behaved and
+      # consistently sending links with the protocol part
+      .replace(/((\bhttp)\S+)/g, '<$1>')
+
+  unescapeHtml: (string) ->
+    string
+      # Unescape entities
+      .replace(/&/g, '&')
+      .replace(/</g, '<')
+      .replace(/>/g, '>')
+
+      # Convert markup into plain url string.
+      .replace(/<((\bhttps?)[^|]+)(\|(.*))+>/g, '$1')
+      .replace(/<((\bhttps?)(.*))?>/g, '$1')
+

   userFromParams: (params) ->
     # hubot < 2.4.2: params = user
@@ -45,53 +64,58 @@

     params

-  run: ->
-    self = @
-
+  parseOptions: ->
     @options =
       token:   process.env.HUBOT_SLACK_TOKEN or null
       team:    process.env.HUBOT_SLACK_TEAM or null
       name:    process.env.HUBOT_SLACK_BOTNAME or 'slackbot'
-    console.log "Slack adapter options:", @options
+
+  getMessageFromRequest: (req) ->
+    # Parse the payload
+    hubot_msg = req.param('text')
+    return unless hubot_msg
+
+    @unescapeHtml hubotMsg
+
+  getAuthorFromRequest: (req) ->
+    from = req.param('user_id')
+    from_name = req.param('user_name')
+    channel = req.param('channel_id')
+    channel_name = req.param('channel_name')
+
+    # Construct an author object
+    author = {}
+    author.id = from
+    author.name = from_name
+    author.reply_to = channel
+    author.room = channel_name
+
+    author
+
+  run: ->
+    self = @
+
+    @parseOptions()
+    @log "Slack adapter options:", @options

     unless @options.token
-      console.error "No services token provided to Hubot"
+      @logError "No services token provided to Hubot"
       return

     unless @options.team
-      console.error "No team provided to Hubot"
+      @logError "No team provided to Hubot"
       return

     # Listen to incoming webhooks from slack
     self.robot.router.post "/hubot/slack-webhook", (req, res) ->
-      console.log "Incoming message received"
+      self.log "Incoming message received"

-      # Parse the payload
-      from = req.param('user_id')
-      from_name = req.param('user_name')
-      channel = req.param('channel_id')
-      channel_name = req.param('channel_name')
-      hubot_msg = req.param('text')
+      hubot_msg = self.getMessageFromRequest req
+      author = self.getAuthorFromRequest req

-      if hubot_msg
-        # Convert markup into plain url string.
-        hubot_msg = hubot_msg.replace(/<((\bhttps?)[^|]+)(\|(.*))+>/g, '$1')
-        hubot_msg = hubot_msg.replace(/<((\bhttps?)(.*))?>/g, '$1')
-
-        # Unescape
-        hubot_msg = hubot_msg.replace(/&/g, '&')
-        hubot_msg = hubot_msg.replace(/</g, '<')
-        hubot_msg = hubot_msg.replace(/>/g, '>')
-
-        # Construct an author object
-        author = {}
-        author.id = from
-        author.name = from_name
-        author.reply_to = channel
-        author.room = channel_name
-
+      if hubot_msg and author
         # Pass to the robot
-        console.log "Received #{hubot_msg} from #{author.name}"
+        self.log "Received #{hubot_msg} from #{author.name}"
         self.receive new TextMessage(author, hubot_msg)

       # Just send back an empty reply, since our actual reply,
@@ -102,7 +126,7 @@
     self.robot.name = @options.name

     # Tell Hubot we're connected so it can load scripts
-    console.log "Successfully 'connected' as", self.robot.name
+    @log "Successfully 'connected' as", self.robot.name
     self.emit "connected"

   # Convenience HTTP Methods for sending data back to slack
@@ -113,6 +137,8 @@
     @request "POST", path, body, callback

   request: (method, path, body, callback) ->
+    self = @
+
     #console.log method, path, body
     host = @options.team + '.slack.com'
     headers = "Host": host
@@ -138,15 +164,15 @@

       response.on "end", ->
         if response.statusCode >= 400
-          console.error "Slack services error: #{response.statusCode}"
-          console.error data
+          self.logError "Slack services error: #{response.statusCode}"
+          self.logError data

         #console.log "HTTPS response:", data
         if callback
           callback null, data

         response.on "error", (err) ->
-          console.error "HTTPS response error:", err
+          self.logError "HTTPS response error:", err
           if callback
             callback err, null

@@ -156,8 +182,8 @@
       request.end()

     request.on "error", (err) ->
-      console.error "HTTPS request error:", err
-      console.error err.stack
+      self.logError "HTTPS request error:", err
+      self.logError err.stack
       if callback
         callback err


add helper for finding the earliest due date

--- tutor/src/helpers/task-plan.coffee
+++ tutor/src/helpers/task-plan.coffee
@@ -26,4 +26,9 @@
         memo[tp.target_id] = pickFn(tp)
         memo
       , {})
+
+
+  earliestDueDate: (plan) ->
+    dates = _.map(plan?.tasking_plans, 'due_at')
+    _.first(dates.sort()) or ''
 }


Activity: accept image options through constructor for embed box images.

--- client/Social/Activity/views/embedbox/embedboxlinkviewimage.coffee
+++ client/Social/Activity/views/embedbox/embedboxlinkviewimage.coffee
@@ -6,10 +6,17 @@
     options.href   = data.link_url or data.link_embed?.url
     options.target = "_blank"

+    imageOptions         = options.imageOptions or {}
+    imageOptions.width  ?= 619
+    imageOptions.height ?= 200
+    imageOptions.crop   ?= yes
+    imageOptions.grow   ?= yes
+
     super options, data

     oembed = @getData().link_embed
-    @imageLink    = @utils.proxifyUrl(oembed.images?[0]?.url, width: 619, height: 200, crop: yes, grow: yes)
+
+    @imageLink    = @utils.proxifyUrl oembed.images?[0]?.url, imageOptions
     altSuffix     = if oembed.author_name then " by #{oembed.author_name}" else ''
     @imageAltText = oembed.title + altSuffix


participants/collab: minor fixes

participants/collab: minor fixes

--- client/activity/lib/models/collaborationchannelparticipants.coffee
+++ client/activity/lib/models/collaborationchannelparticipants.coffee
@@ -1,6 +1,6 @@
 _                        = require 'lodash'
 ChannelParticipantsModel = require './channelparticipants'
-videoConstants = require 'app/videocollaboration/constants'
+videoConstants           = require 'app/videocollaboration/constants'

 module.exports = class CollaborationChannelParticipantsModel extends ChannelParticipantsModel

@@ -30,7 +30,7 @@

     if @state.videoActive
       filterList = getOrderedVideoParticipants @state
-      actives = participants.filter (participant) =>
+      actives = participants.filter (participant) ->
         participant.profile.nickname in filterList

       return actives.toList()
@@ -51,7 +51,7 @@

     if @state.videoActive
       filterList = getOrderedVideoParticipants @state
-      inactives = participants.filter (participant) =>
+      inactives = participants.filter (participant) ->
         not (participant.profile.nickname in filterList)

       return inactives.toList()


rm _getAnnotationData function

rm _getAnnotationData function

--- client/controllers/incidentReports/newIncidentFromSelection.coffee
+++ client/controllers/incidentReports/newIncidentFromSelection.coffee
@@ -5,14 +5,6 @@
 POPUP_PADDING = 5
 POPUP_PADDING_TOP = 20
 POPUP_WINDOW_PADDING = 50
-
-_getAnnotationData = (selection) ->
-  range = selection.getRangeAt(0)
-  textOffsets = [range.startOffset, range.endOffset]
-  content = selection.anchorNode.textContent
-
-  textOffsets: textOffsets
-  text: content.slice(textOffsets[0], textOffsets[1])

 Template.newIncidentFromSelection.onCreated ->
   @selection = window.getSelection()
@@ -57,7 +49,11 @@

   annotatedText: ->
     instance = Template.instance()
-    _getAnnotationData(instance.selection).text
+    selection = instance.selection
+    range = selection.getRangeAt(0)
+    textOffsets = [range.startOffset, range.endOffset]
+    content = selection.anchorNode.textContent
+    content.slice(textOffsets[0], textOffsets[1])

 Template.newIncidentFromSelection.events
   'mousedown .add-incident-from-selection': (event, instance) ->


Fix reloading

Fix reloading

--- common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -329,7 +329,7 @@
     $.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
       if response.state == "done" or response.state=="post_assessment"
         delete window.queuePollerID
-        @reload
+        location.reload()
       else
         window.queuePollerID = window.setTimeout(@poll, 10000)


Text of old file in texteare fixed

Text of old file in texteare fixed

--- common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -541,10 +541,11 @@
           @message_wrapper.html('')
           @child_state = 'initial'
           @coe.after(response.html).remove()
-          @allow_reset="False"
+          @allow_reset = "False"
           @reinitialize(@element)
           @has_been_reset = true
           @rebind()
+          @answer_area.val('')
           @reset_button.hide()
         else
           @errors_area.html(response.error)


Fix typo

Fix typo

Linkeding > LinkedIn

--- client/app/locales/fr.coffee
+++ client/app/locales/fr.coffee
@@ -9,7 +9,7 @@
     'imported data:': 'Données importées :'
     'importing...': 'importation en cours...'
     'no import performed': "Pas d'importation effectuée"
-    'import already running': "L'import est déjà en cours."
+    'import already running': "L'import est déjà en cours."link
     'firstname': 'Prénom'
     'lastname': 'Nom'
     'login': 'Identifiant'
@@ -82,7 +82,7 @@
     'konnector description nest': "Enregistrez la température actuelle mesurée par votre Nest."
     'konnector description isen': "Les étudiants de l'école d'ingénieur ISEN peuvent importer leurs supports de cours et leur agenda."
     'konnector description googlecontacts': "Importez vos contacts Google dans votre Cozy via l'API de Google."
-    'konnector description linkedin': "Importez vos contacts Linkeding dans votre Cozy."
+    'konnector description linkedin': "Importez vos contacts LinkedIn dans votre Cozy."
     'konnector customview googlecontacts 4': "Initialiser ou réinitialiser ce konnector"
     'konnector customview googlecontacts 1': "1. Cliquez sur le bouton \"Connecter votre compte google\" afin de connecter votre compte google et autoriser Cozy à y accéder. La fenêtre de Google va présenter une chaîne de caractère comlexe pour cela. Copiez la, elle sera utile à l'étape 2."
     'konnector customview googlecontacts 2': "Connecter votre compte Google"


lots of fixes and improvements

lots of fixes and improvements

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -21,25 +21,55 @@
 modulename = 'fw'
 blacklistfile = modulename + ".json"

-try
-  fwdata = fs.readFileSync blacklistfile, 'utf-8'
-  if fwdata
-    data = JSON.parse(fwdata)
-catch error
-  console.log('Unable to read file', error) unless error.code is 'ENOENT'
-
 module.exports = (robot) ->

-  robot.router.get "/#{robot.name}/bl", (req, res) ->
+  data = []
+  try
+    fwdata = fs.readFileSync blacklistfile, 'utf-8'
+    if fwdata
+      data = JSON.parse(fwdata)
+      console.log "#{modulename} blacklist loaded: " + data.length + " entries"
+  catch error
+    console.log('Unable to read file', error) unless error.code is 'ENOENT'
+
+  robot.router.get "/#{robot.name}/#{modulename}/blacklist/url", (req, res) ->
+    clientip = req.connection.remoteAddress || req.socket.remoteAddress ||
+      req.connection.socket.remoteAddress
+    logmsg = "#{modulename}: web request from #{clientip}: url blacklist"
+    robot.logger.info logmsg
+
     arr = []
     for obj in data
-      arr.push obj.val # XXX: unless expired
+      if obj.type == 'url' and moment(obj.expires).isAfter()
+        arr.push obj.val
     content = '# nothing here yet! #'
     content = arr.join "\n" if arr.length
     res.setHeader 'content-type', 'text/plain'
     res.end content

-  robot.respond /fw(?: help)$/, (msg) ->
+    logmsg = "#{modulename}: robot responded to web request: sent url blacklist"
+    robot.logger.info logmsg
+
+  robot.router.get "/#{robot.name}/#{modulename}/blacklist/cidr", (req, res) ->
+    clientip = req.connection.remoteAddress || req.socket.remoteAddress ||
+      req.connection.socket.remoteAddress
+    logmsg = "#{modulename}: web request from #{clientip}: cidr blacklist"
+    robot.logger.info logmsg
+
+    arr = []
+    for obj in data
+      if obj.type == 'cidr' and moment(obj.expires).isAfter()
+        arr.push obj.val
+    content = '# nothing here yet! #'
+    content = arr.join "\n" if arr.length
+    res.setHeader 'content-type', 'text/plain'
+    res.end content
+
+    logmsg = "#{modulename}: robot responded to web request: " +
+      "sent cidr blacklist"
+    robot.logger.info logmsg
+
+  robot.respond /fw(?: help|)$/, (msg) ->
     cmds = []
     arr = [
       modulename + " blacklist - show blacklist"
@@ -53,12 +83,24 @@

     robot.send {room: msg.message?.user?.name}, cmds.join "\n"

+    logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
+      "displayed #{modulename} help"
+    robot.logger.info logmsg
+
   robot.respond /fw (?:blacklist|b)$/i, (msg) ->
     logmsg = "#{modulename}: #{msg.envelope.user.name} requested: blacklist"
     robot.logger.info logmsg
-    data = fs.readFileSync blacklistfile, 'utf-8'

-    robot.send {room: msg.message?.user?.name}, data.join "\n"
+    arr = ['Blacklist items and expirations:']
+    for obj in data
+      expires = moment(obj.expires)
+      if expires.isAfter()
+        arr.push "`#{obj.val}` #{expires.fromNow()}"
+    robot.send {room: msg.message?.user?.name}, arr.join "\n"
+
+    logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
+      "displayed blacklist items and expirations"
+    robot.logger.info logmsg

   robot.respond /fw (?:blacklist|b) (url|cidr) ([^ ]+)(?: ([^ ]+)|)$/i, (msg) ->
     bl =
@@ -66,15 +108,15 @@
       expires: moment().add(1, 'months').format()
       type: msg.match[1]
       val: msg.match[2]
-    if msg.match.length > 2
+    if msg.match[3]?
       expires = msg.match[3]
-      extra = expires.match(/+(\d)([hdwMQy])/)
+      extra = expires.match /\+(\d)([hdwMQy])/
       if extra?
         n = extra[1]
         unit = extra[2]
         bl.expires = moment().add(n,unit).format()
       else if moment(expires).isValid()
-        bl.expires = moment(expires)
+        bl.expires = moment(expires).format()
       else
         failed = true
         logmsg = "invalid expiration date: #{expires}"
@@ -84,5 +126,10 @@
       logmsg = "#{modulename}: #{msg.envelope.user.name} requested: " +
         "blacklist #{bl.type} #{bl.val} expires #{bl.expires}"
       robot.logger.info logmsg
+
       data.push bl
       fs.writeFileSync blacklistfile, JSON.stringify(data), 'utf-8'
+
+      logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
+        "added entry to blacklist"
+      robot.logger.info logmsg


Fixes incorrect reference to commonjs dependencies

--- src/udefine.coffee
+++ src/udefine.coffee
@@ -21,10 +21,10 @@
         requireArr = []

         for dep in deps
-          if typeof root.udefine.node[dep] is 'string'
-            requireArr.push require(root.udefine.node[dep])
+          if typeof root.udefine.commonjs[dep] is 'string'
+            requireArr.push require(root.udefine.commonjs[dep])
           else
-            requireArr.push root.udefine.node[dep]
+            requireArr.push root.udefine.commonjs[dep]

         # Common JS
         result = module.exports = factory.apply @, requireArr


Added rudimentary tweening, wut!

Added rudimentary tweening, wut!

--- src/tween.coffee
+++ src/tween.coffee
@@ -23,7 +23,7 @@
         I[property] = f(t)

   ###*
-  Modify the objects
+  Modify the objects properties over time

   player = GameObject()