fix require for lead.js modules

fix require for lead.js modules

--- app/compat.coffee
+++ app/compat.coffee
@@ -4,14 +4,16 @@
 CodeMirror = require 'codemirror'
 d3 = require('d3')

+Context = require('./context')
 colors = require './colors'
 modules = require './modules'

 requireables = {q: Q, _: _, moment, colors, d3}

-modules.export exports, 'compat', ({doc, component_fn, cmd, fn} ) ->
+modules.export exports, 'compat', ({doc, fn} ) ->
+  fn 'require', (ctx, moduleName) ->
+    Context.value(requireables[moduleName] ? ctx.modules[moduleName])
+
   context_vars:
     moment: moment
     CodeMirror: CodeMirror
-    require: (module_name) ->
-      requireables[module_name] ? modules.get_module module_name


if validation error, set global.cobudgetApp.currentUserId to null

--- app/boot.coffee
+++ app/boot.coffee
@@ -27,8 +27,9 @@
     work(user)

   $rootScope.$on 'auth:validation-error', () ->
+    userValidatedDeferred.reject()
+    membershipsLoadedDeferred.reject()
+    global.cobudgetApp.currentUserId = null
     console.log('validation-error')
     $location.path('/')
     Toast.show('Please log in to continue')
-    userValidatedDeferred.reject()
-    membershipsLoadedDeferred.reject()


Remove dedupe

Remove dedupe

--- src/webpack.config.coffee
+++ src/webpack.config.coffee
@@ -540,7 +540,6 @@
     console.log "production mode: enabling compression"
     # https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
     # plugins.push new webpack.optimize.CommonsChunkPlugin(name: "lib")
-    plugins.push new webpack.optimize.DedupePlugin()
     # configuration for the number of chunks and their minimum size
     plugins.push new webpack.optimize.LimitChunkCountPlugin(maxChunks: 5)
     plugins.push new webpack.optimize.MinChunkSizePlugin(minChunkSize: 30000)
@@ -617,6 +616,9 @@
         chunkFilename : if PRODMODE then '[id]-[hash].cacheme.js'   else '[id].nocache.js'
         hashFunction  : 'sha256'

+    resolveLoader: # Not reccommended. TODO: Track down missing -loader instances
+        moduleExtensions: ["-loader"]
+
     module:
         rules: [
             { test: /pnotify.*\.js$/, use: "imports?define=>false,global=>window" },


增强cola.util.findWidget(), 使其支持跨框架的查找

增强cola.util.findWidget(), 使其支持跨框架的查找

--- src/widget/support.coffee
+++ src/widget/support.coffee
@@ -208,16 +208,42 @@

 cola.findWidget = (dom, type) ->
 	if type and typeof type == "string"
-		type = cola.resolveType("widget", {$type: type})
+		typeName = type
+		type = WIDGET_TAGS_REGISTRY[typeName]
+		type = cola.resolveType("widget", { $type: typeName }) unless type
 		return null unless type

-	while dom
-		widget = cola.util.userData(dom, cola.constants.DOM_ELEMENT_KEY)
-		if widget
-			if not type or widget instanceof type
-				return widget
-		dom = dom.parentNode
-	return null
+	if dom instanceof cola.Widget
+		dom = dom.getDom()
+
+	find = (win, dom, type) ->
+		parentDom = dom.parentNode
+		while parentDom
+			dom = parentDom
+			widget = cola.util.userData(dom, cola.constants.DOM_ELEMENT_KEY)
+			if widget
+				if not type or widget instanceof type
+					return widget
+			parentDom = dom.parentNode
+
+		if win.parent
+			try
+				parentFrames = win.parent.jQuery("iframe,frame")
+			catch
+				# do nothing
+
+			if parentFrames
+				frame = null
+				parentFrames.each () ->
+					if @contentWindow is win
+						frame = @
+						return false
+
+				if frame
+					widget = find(win.parent, frame, type)
+		return widget
+
+	return find(window, dom, type)

 cola.Model::widget = (config) -> cola.widget(config, null, @)


Added some population stats functions.

Added some population stats functions.

I added functions to compute the proportion of late nesters and proportion of wild born birds in a population.

--- src/lib/population.coffee
+++ src/lib/population.coffee
@@ -57,6 +57,14 @@

   size: -> @_unpairedBirds.length + 2*@_pairings.length

+  proportionLateNesters: ->
+    return 0 if @size() == 0
+    @birds().filter((b) -> b.isLate()).length / @size()
+
+  proportionWildBorn: ->
+    return 0 if @size() == 0
+    @birds().filter((b) -> b.isWild()).length / @size()
+
   mateUnpairedBirds: ->
     toMate = @_unpairedBirds.filter((b) -> b.canMate())
     if toMate.length % 2 == 1


remove permalink fn

remove permalink fn

--- app/builtins.coffee
+++ app/builtins.coffee
@@ -184,11 +184,6 @@
   fn 'dynamic', 'Returns a Bacon.Model bound to a global variable', (ctx, name) ->
     Context.value Context.make_prop_var ctx, name

-  component_cmd 'permalink', 'Create a link to the code in the input cell above', (ctx, code) ->
-    code ?= ctx.previously_run()
-    uri = App.raw_cell_url code
-    React.DOM.a {href: uri}, uri
-
   PromiseResolvedComponent = React.createClass
     displayName: 'PromiseResolvedComponent'
     getInitialState: ->


recompact example

recompact example

--- src/smc-hub/rethink.coffee
+++ src/smc-hub/rethink.coffee
@@ -511,6 +511,8 @@
     ###
     Sometimes RethinkDB tables waste huge amounts of disk space.  A trick that Daniel Mewes suggests to
     fix this is to write and remove a dummy field from all records.  This function implements that idea.
+
+    db._error_thresh=1e6; db.recompact_table(table:'blobs', cb:done())
     ###
     recompact_table: (opts) =>
         opts = defaults opts,


cocalc: minor r_help tweaks

cocalc: minor r_help tweaks

--- src/smc-webapp/r_help.cjsx
+++ src/smc-webapp/r_help.cjsx
@@ -142,6 +142,9 @@

                 {@render_active_users_stats()}
                 {# @render_active_projects_stats()}
+

+                    Recent user activity
+

                 {@render_recent_usage_stats()}
                 <Icon name='line-chart' fixedWidth />{' '}

@@ -213,7 +216,7 @@
         icon : 'superscript'
         href : 'http://www.sagemath.org/help.html'
         link : 'SageMath'
-        text : 'help and support for SageMath'
+        text : 'documentation, help, support and books'
     r :
         icon : 'table'
         href : 'https://cran.r-project.org/doc/manuals/r-release/R-intro.html'
@@ -229,6 +232,11 @@
                     SymPy,{' '}
                     Scikit Learn and many more

+    julia :
+        icon : 'building-o'
+        href : 'http://docs.julialang.org/en/stable/manual/introduction/'
+        link : 'Julia'
+        text : 'programming language for numerical computing'
     tensorflow :
         icon : 'lightbulb-o'
         href : 'https://www.tensorflow.org/get_started/get_started'


webapp: adding an react HTML component which is 90% of the already existing Markdown component

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -709,15 +709,19 @@

-exports.Markdown = rclass
-    displayName : 'Misc-Markdown'
-
-    propTypes :
-        value      : rtypes.string
-        style      : rtypes.object
-        project_id : rtypes.string   # optional -- can be used to improve link handling (e.g., to images)
-        file_path  : rtypes.string   # optional -- ...
-        className  : rtypes.string   # optional class
+exports.HTML = rclass
+    displayName : 'Misc-HTML'
+
+    propTypes :
+        value       : rtypes.string
+        style       : rtypes.object
+        has_mathjax : rtypes.bool
+        project_id  : rtypes.string   # optional -- can be used to improve link handling (e.g., to images)
+        file_path   : rtypes.string   # optional -- ...
+        className   : rtypes.string   # optional class
+
+    getDefaultProps: ->
+        has_mathjax : true

     shouldComponentUpdate: (newProps) ->
         return @props.value != newProps.value or not underscore.isEqual(@props.style, newProps.style)
@@ -731,8 +735,7 @@
     _update_mathjax: (cb) ->
         if not @_isMounted  # see https://github.com/sagemathinc/smc/issues/1689
             return
-        #if DEBUG then console.log('Markdown._update_mathjax: @_x?.has_mathjax', @_x?.has_mathjax, @_x)
-        if @_x?.has_mathjax
+        if @props.has_mathjax
             $(ReactDOM.findDOMNode(@)).mathjax
                 cb : () =>
                     # Awkward code, since cb may be called more than once.
@@ -768,23 +771,49 @@
         # and https://github.com/sagemathinc/smc/issues/1689
         @_isMounted = false

+    render_html: ->
+        if @props.value
+            {__html: require('./misc_page').sanitize_html(@props.value)}
+        else
+            {__html: ''}
+
+    render: ->
+
+
+
+exports.Markdown = rclass
+    displayName : 'Misc-Markdown'
+
+    propTypes :
+        value      : rtypes.string
+        style      : rtypes.object
+        project_id : rtypes.string   # optional -- can be used to improve link handling (e.g., to images)
+        file_path  : rtypes.string   # optional -- ...
+        className  : rtypes.string   # optional class
+
     to_html: ->
         if @props.value
             # change escaped characters back for markdown processing
             v = @props.value.replace(/>/g, '>').replace(/</g, '<')
-            @_x = markdown.markdown_to_html(v)
-            html_sane = require('./misc_page').sanitize_html(@_x.s)
-            #if DEBUG then console.log('Markdown.to_html @_x', @_x)
-            {__html: html_sane}
-        else
-            {__html: ''}
-
-    render: ->
-
-
+            return markdown.markdown_to_html(v)
+        else
+            ''
+
+    render: ->
+        HTML = exports.HTML
+        value = @to_html()
+        #if DEBUG then console.log('Markdown.to_html value', value.s, value.has_mathjax)
+
+

 activity_style =
     float           : 'right'


fix bug where moving/copying/selecting directory names with space at the end is broken by removing a "terrible hack"

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -950,9 +950,12 @@
             @props.redux.getActions('projects').fetch_directory_tree(@props.project_id)
         tree = x?.tree
         if tree?
-            # TODO: spaces below are a terrible hack to get around weird design of Combobox.
-            tree = (x + ' ' for x in tree)
-            group = (s) -> s[0 ... s.indexOf('/')]
+            group = (s) ->
+                i = s.indexOf('/')
+                if i == -1
+                    return s
+                else
+                    return s.slice(0, i)
         else
             group = (s) -> s
         <Combobox
@@ -963,7 +966,7 @@
             defaultValue = {@props.default_value}
             placeholder  = {@props.placeholder}
             messages     = {emptyFilter : '', emptyList : ''}
-            onChange     = {(value) => @props.on_change(value.trim())}
+            onChange     = {(value) => @props.on_change(value)}
             onKeyDown    = {@props.on_key_down}
             onKeyUp      = {@props.on_key_up}
         />


increase height of mobile file info button

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -35,6 +35,7 @@
 misc        = require('smc-util/misc')
 immutable   = require('immutable')
 underscore  = require('underscore')
+{IS_MOBILE} = require('./feature')

 markdown    = require('./markdown')

@@ -1159,9 +1160,14 @@
             @render_menu_item(name, icon)

     render : ->
-        <DropdownButton style={marginRight:'2px'} id='file_info_button' bsStyle='info' title={<Icon name='info-circle' />} className='pull-left'>
-            {@render_menu_items()}
-        </DropdownButton>
+        if IS_MOBILE
+            <DropdownButton style={marginRight:'2px', height:'45px'} id='file_info_button' bsStyle='info' title={<Icon name='info-circle' />} className='pull-left'>
+                {@render_menu_items()}
+            </DropdownButton>
+        else
+            <DropdownButton style={marginRight:'2px'} id='file_info_button' bsStyle='info' title={<Icon name='info-circle' />} className='pull-left'>
+                {@render_menu_items()}
+            </DropdownButton>

 exports.render_file_info_dropdown = (filename, actions, dom_node, is_public) ->
     ReactDOM.render(<EditorFileInfoDropdown filename={filename} actions={actions} is_public={is_public}/>, dom_node)


Make error boundaries more user friendly

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -2056,10 +2056,11 @@
                     <Icon name='exclamation-triangle'/> Oh noes!! Well this is embarrasing... 😬
                 </h2>
                 <h4>
-                    {"We've already been notified of this error and are hard at work fixing it."}
+                    {"We have been notified of this error. You must refresh your browser."}
                 </h4>
-                {"If you'd like to expidite the bug fixing, please file a support ticket describing what you were trying to do when the error occured."}
-                <Button onClick={console.log "TODO"}>
+                {"If you'd like to expedite the bug fixing, please file a support ticket describing what you were trying to do when the error occured."}
+                </br>
+                <Button onClick={=>redux.getActions('support').show(true)}>
                     Create Ticket
                 </Button>
                 <details style={whiteSpace:'pre-wrap', cursor:'pointer'} >


fixed duplicate code

fixed duplicate code

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -1160,14 +1160,15 @@
             @render_menu_item(name, icon)

     render : ->
+        drop_down_style =
+            marginRight : '2px'
+
         if IS_MOBILE
-            <DropdownButton style={marginRight:'2px', height:'45px'} id='file_info_button' bsStyle='info' title={<Icon name='info-circle' />} className='pull-left'>
-                {@render_menu_items()}
-            </DropdownButton>
-        else
-            <DropdownButton style={marginRight:'2px'} id='file_info_button' bsStyle='info' title={<Icon name='info-circle' />} className='pull-left'>
-                {@render_menu_items()}
-            </DropdownButton>
+            drop_down_style.height = '45px'
+
+        <DropdownButton style={drop_down_style} id='file_info_button' bsStyle='info' title={<Icon name='info-circle' />} className='pull-left'>
+            {@render_menu_items()}
+        </DropdownButton>

 exports.render_file_info_dropdown = (filename, actions, dom_node, is_public) ->
     ReactDOM.render(<EditorFileInfoDropdown filename={filename} actions={actions} is_public={is_public}/>, dom_node)


fix for marking issue

fix for marking issue

--- app/lib/mark.coffee
+++ app/lib/mark.coffee
@@ -49,11 +49,11 @@
     # no overlapping of marks
     markLeftX = Math.max markLeftX,
                         (@closestXBelow + @handleWidth()/2 || markLeftX),
-                        (@canvasGraph.toPixels(@canvasGraph.smallestX))
+                        (@canvasGraph.toPixels(@canvasGraph.smallestX)),@canvasGraph.leftPadding

     markRightX = Math.min markRightX,
                          (@closestXAbove - @handleWidth()/2 || markRightX),
-                         (@canvasGraph.toPixels(@canvasGraph.largestX))
+                         (@canvasGraph.toPixels(@canvasGraph.largestX)) + @canvasGraph.leftPadding

     # max and min width on creating / resizing marks
     width = Math.min (Math.max (Math.abs parseFloat(markRightX) - parseFloat(markLeftX)), parseFloat(@minWidth())), parseFloat(@maxWidth())
@@ -66,11 +66,11 @@

   move: (e) ->
     markWidth = parseFloat @element.style.width
-
     # no overlapping of marks or moving out of canvas bounds
     leftXPos = Math.max (@toCanvasXPoint(e) - @moveOffset),
                         (@closestXBelow || -@handleWidth()/2) + @handleWidth()/2,
                         @canvasGraph.leftPadding
+
     leftXPos = Math.min leftXPos,
                         ((@closestXAbove || @canvas.width + @handleWidth()/2) - markWidth) - @handleWidth()/2

@@ -83,7 +83,7 @@
     #data coords
     @dataXMinRel = @canvasGraph.toDays(@canvasXMin-@canvasGraph.leftPadding)
     @dataXMaxRel = @canvasGraph.toDays(@canvasXMax-@canvasGraph.leftPadding)
-    @dataXMinGlobal = @dataXMinRel + @originalMin
+    @dataXMinGlobal = @dataXMinRel + @originalMin
     @dataXMaxGlobal = @dataXMaxRel + @originalMin

   onMouseDown: (e) =>
@@ -120,7 +120,7 @@
     window.removeEventListener 'mouseup', @onMouseUp
     window.removeEventListener 'touchmove', @onMouseMove
     window.removeEventListener 'touchend', @onTouchEnd
-
+
     @canvasGraph.marks.add(@) unless (@ in @canvasGraph.marks.all)
     @canvasGraph.marks.remove(@) if e.target.className is "top-bar" or e.target.className is "close-icon"
     for mark in @canvasGraph.marks.all


Updated App.directory attribute due to repo case change

--- assets/app.coffee
+++ assets/app.coffee
@@ -4,7 +4,7 @@
     # load htmlify object
     @htmlify = new Htmlify()
     # set base directory
-    @dir = "/CSS-Reference"
+    @dir = "/css-reference"
     # query history
     @history = []
     # initialize current position in query history
@@ -200,4 +200,4 @@
     # write to document
     $(element).append(@htmlify.htmlify(window.data.document))
     # show element
-    $(element).fadeIn('fast')+    $(element).fadeIn('fast')


Remove keyup event listener.

Remove keyup event listener.

--- app/assets/javascripts/channels/room.coffee
+++ app/assets/javascripts/channels/room.coffee
@@ -17,14 +17,6 @@

 $(document).on 'turbolinks:load', ->
   scroll_bottom()
-  submit_chat()
-
-submit_chat = () ->
-  $('#chat_body').on 'keydown', (event) ->
-    if event.keyCode is 13
-      event.preventDefault()
-      $('input').click()
-      event.target.value = " "

 scroll_bottom = () ->
   $('#chats').scrollTop($('#chats')[0].scrollHeight)


send page height on Turbolinks page load

--- app/assets/javascripts/embedded.init.coffee
+++ app/assets/javascripts/embedded.init.coffee
@@ -2,6 +2,7 @@

 App.init = ->
   pymChild = new pym.Child()
+  pymChild.sendHeight()

   $(document).foundation()


fixed weekly, daily, and one-time events in week view

--- app/coffee/drm-calendar/drm-calendar.coffee
+++ app/coffee/drm-calendar/drm-calendar.coffee
@@ -67,6 +67,10 @@

             self.hours = [
                 {
+                    name: 'All Day Event'
+                    time: null
+                }
+                {
                     name: '12am'
                     time: 0
                 }
@@ -564,7 +568,12 @@
             @events.push obj
             @addEventsToCalendar @events[obj.id]

-        addNewCalendarEvent: (events, calendarItem) =>
+        addNewCalendarEvent: (events, dates) =>
+            calendarInner = @calendar.find "div.#{@calendarInnerClass}"
+            if @view is 'month'
+                calendarItem = calendarInner.find "td[data-date=#{dates}]"
+            else if @view is 'week'
+                calendarItem = calendarInner.find "td[data-date=#{dates}][data-hour='All Day Event']"
             eventList = calendarItem.find "ul.#{@eventClass}"
             length = eventList.length

@@ -721,7 +730,7 @@
             else
                 parseInt(dayNum, 10) - 1

-        addYearlyEvents: (events, eventDays) =>
+        addYearlyEvents: (events, eventDates) =>
             self = @
             calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
             currentMonth = calendarInner.data 'month'
@@ -731,7 +740,6 @@
             numberDays = self.getDaysInMonth (currentMonth + 1), currentYear
             dayShift = if firstDay is self.daysPerWeek then 0 else firstDay
             month = $.inArray events.month, self.months
-            eventDate = []

             # add yearly events
             if events.day
@@ -741,13 +749,11 @@

                     if currentMonth is month
                         eventWeek = if dayShift <= day then eventWeek = weeks.eq eventWeekNum else eventWeek = weeks.eq eventWeekNum + 1
-                        eventDate.push eventWeek.find("td.drm-date[data-day=#{day}]").data 'date'
+                        eventDates.push eventWeek.find("td.drm-date[data-day=#{day}]").data 'date'
             else
-                eventDate.push parseInt(events.eventDate, 10)
-            $.each eventDate, (key, value) ->
-                eventDays.push calendarInner.find "td.drm-date[data-date=#{value}]"
+                eventDates.push parseInt(events.eventDate, 10)

-        addMonthlyEvents: (events, eventDays) =>
+        addMonthlyEvents: (events, eventDates) =>
             self = @
             calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
             currentMonth = calendarInner.data 'month'
@@ -756,7 +762,6 @@
             firstDay = self.getDayOfWeek currentMonth, currentYear, 1
             numberDays = self.getDaysInMonth (currentMonth + 1), currentYear
             dayShift = if firstDay is self.daysPerWeek then 0 else firstDay
-            eventDate = []

             # add monthly events
             if events.day
@@ -764,20 +769,16 @@
                     day = $.inArray value, self.days
                     eventWeekNum = self.getEventWeekNum events.dayNum, day, numberDays, dayShift
                     eventWeek = if dayShift <= day then eventWeek = weeks.eq eventWeekNum else eventWeek = weeks.eq eventWeekNum + 1
-                    eventDate.push eventWeek.find("td.drm-date[data-day=#{day}]").data 'date'
+                    eventDates.push eventWeek.find("td.drm-date[data-day=#{day}]").data 'date'
             else
-                eventDate.push parseInt(events.eventDate, 10)
-
-            $.each eventDate, (key, value) ->
-                eventDays.push calendarInner.find "td.drm-date[data-date=#{value}]"
-
-        addBiWeeklyEvents: (events, eventDays) =>
+                eventDates.push parseInt(events.eventDate, 10)
+
+        addBiWeeklyEvents: (events, eventDates) =>
             self = @
             calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
             currentMonth = calendarInner.data 'month'
             currentYear = calendarInner.data 'year'
             weeks = if self.view is 'month' then calendarInner.find('tbody').find "tr.drm-week" else calendarInner.find "tbody.drm-week"
-            eventDate = []

             # events that occur every 2 weeks
             if events.day
@@ -791,12 +792,9 @@
                     $.each eventWeeks, (key, value) ->
                         that = $ value
                         weekLength = that.find("td:not(.#{self.classes.muted})").length
-                        eventDate.push that.find("td.drm-date[data-day=#{day}]").data('date')
-
-            $.each eventDate, (key, value) ->
-                eventDays.push calendarInner.find "td.drm-date[data-date=#{value}]"
-
-        addWeeklyEvents: (events, eventDays) =>
+                        eventDates.push that.find("td.drm-date[data-day=#{day}]").data('date')
+
+        addWeeklyEvents: (events, eventDates) =>
             self = @
             calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
             currentMonth = calendarInner.data 'month'
@@ -804,7 +802,6 @@
             firstDay = self.getDayOfWeek currentMonth, currentYear, 1
             numberDays = self.getDaysInMonth (currentMonth + 1), currentYear
             dayShift = if firstDay is self.daysPerWeek then 0 else firstDay
-            eventDate = []
             weeks = if self.view is 'month' then calendarInner.find('tbody').find 'tr.drm-week' else calendarInner.find 'tbody.drm-week'

             # weekly events
@@ -814,29 +811,29 @@
                     length = weeks.length
                     $.each weeks, (key, value) ->
                         that = $ value
-                        weekLength = that.find("td:not(.#{self.classes.muted})").length
-                        if key is 0
-                            if dayShift <= day then eventDate.push that.find("td.drm-date[data-day=#{day}]").data('date')
-                        else if key is (length - 1)
-                            if day < weekLength then eventDate.push that.find("td.drm-date[data-day=#{day}]").data('date')
+                        days = if self.view is 'month' then that.find "td.drm-date" else that.find "td.drm-date[data-hour='All Day Event']"
+                        weekLength = days.length
+                        if key is 0 and length isnt 1
+                            if dayShift <= day then eventDates.push that.find("td.drm-date[data-day=#{day}]").data('date')
+                        else if key is (length - 1) and length isnt 1
+                            if day < weekLength then eventDates.push that.find("td.drm-date[data-day=#{day}]").data('date')
                         else
-                            eventDate.push that.find("td.drm-date[data-day=#{day}]").data('date')
-
-            $.each eventDate, (key, value) ->
-                eventDays.push calendarInner.find "td.drm-date[data-date=#{value}]"
-
-        addDailyEvents: (events, eventDays) =>
+                            eventDates.push that.find("td.drm-date[data-day=#{day}]").data('date')
+
+            eventDates
+
+        addDailyEvents: (events, eventDates) =>
             self = @
             calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
-            days = calendarInner.find('tbody').find "td.drm-date"
+            days = if self.view is 'month' then calendarInner.find "tbody td.drm-date" else calendarInner.find "td.drm-date[data-hour='All Day Event']"
             days.each ->
-                eventDays.push $(@)
-
-        addOneTimeEvents: (events, eventDays) =>
-            self = @
-            calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
-            eventDate = parseInt(events.eventDate, 10)
-            eventDays.push calendarInner.find "td.drm-date[data-date=#{eventDate}]"
+                eventDates.push $(@).data 'date'
+
+        addOneTimeEvents: (events, eventDates) =>
+            self = @
+            calendarInner = self.calendar.find "div.#{@calendarInnerClass}"
+            eventDates.push parseInt(events.eventDate, 10)
+            eventDates

         addEventsToCalendar: (events) =>
             self = @
@@ -844,25 +841,25 @@
             currentMonth = calendarInner.data 'month'
             currentYear = calendarInner.data 'year'
             month = $.inArray events.month, self.months
-            eventDays = []
+            eventDates = []

             # add yearly events
             if events.recurrance is 'yearly' and currentMonth is month
-                self.addYearlyEvents events, eventDays
+                self.addYearlyEvents events, eventDates
             else if events.recurrance is 'monthly'
-                self.addMonthlyEvents events, eventDays
+                self.addMonthlyEvents events, eventDates
             else if events.recurrance is 'biweekly'
-                self.addBiWeeklyEvents events, eventDays
+                self.addBiWeeklyEvents events, eventDates
             else if events.recurrance is 'weekly'
-                self.addWeeklyEvents events, eventDays
+                self.addWeeklyEvents events, eventDates
             else if events.recurrance is 'daily'
-                self.addDailyEvents events, eventDays
+                self.addDailyEvents events, eventDates
             else if events.recurrance is 'none' and currentMonth is month and currentYear is parseInt(events.year, 10)
-                self.addOneTimeEvents events, eventDays
-            if eventDays.length > 0
-                $.each eventDays, (key, calendarItem) ->
+                self.addOneTimeEvents events, eventDates
+            if eventDates.length > 0
+                $.each eventDates, (key, date) ->
                     # add css classes here
-                    self.addNewCalendarEvent events, calendarItem
+                    self.addNewCalendarEvent events, date

         highlightWeekends: =>
             self = @
@@ -1109,6 +1106,7 @@
             weekInfo = self.getDatesInWeek currentMonth, currentDate, currentYear
             datesInWeek = weekInfo.datesInWeek
             weekNumber = self.getWeekNumber currentMonth, currentDate, currentYear
+            weekClass = if weekNumber % 2 is 0 then 'even-week' else 'odd-week'

             getDates = (datesInWeek, key) ->
                 dates = {}
@@ -1143,20 +1141,15 @@
                 weekdays += "#{value}
#{self.months[dates.month]} #{dates.date}	"
             weekdays += '

'

-            week = "
"
-            week += "
All Day Events	"
-
-            $.each self.days, (key, value) ->
-                dates = getDates datesInWeek, key
-                week += "	"
-            week += "
"
+            week = "
"

             $.each self.hours, (key, value) ->
+                hour = value.name
                 week += '
'
-                week += "#{value.name}	"
+                week += "#{hour}	"
                 $.each self.days, (key, value) ->
                     dates = getDates datesInWeek, key
-                    week += "	"
+                    week += "	"
                 week += '
'
             week += '

'

@@ -1179,7 +1172,6 @@
             $("div.#{self.calendarInnerClass}").find("tbody td[data-month=#{nextMonth}]").addClass(self.classes.muted).removeClass 'drm-date'
             self.highlightCurrentDay()
             self.highlightWeekends()
-            self.addWeekNumbers()

             $.each self.events, (key, value) ->
                 self.addEventsToCalendar value


prevent issue submit after click in the floppy icon

--- app/coffee/modules/common/components.coffee
+++ app/coffee/modules/common/components.coffee
@@ -505,7 +505,9 @@
             $el.find('.view-subject').hide()
             $el.find('input').focus()

-        $el.on "click", ".save", ->
+        $el.on "click", ".save", (e) ->
+            e.preventDefault()
+
             subject = $scope.item.subject
             save(subject)


Remove function extra params

Remove function extra params

--- app/coffee/modules/common/lightboxes.coffee
+++ app/coffee/modules/common/lightboxes.coffee
@@ -779,7 +779,7 @@

             $scope.$apply ->
                 $scope.usersSearch = null
-                $scope.$broadcast("assigned-user:added", target.data("user-id"), selectedItem)
+                $scope.$broadcast("assigned-user:added", target.data("user-id"))

         $el.on "click", ".remove-assigned-to", (event) ->
             event.preventDefault()
@@ -788,7 +788,7 @@

             $scope.$apply ->
                 $scope.usersSearch = null
-                $scope.$broadcast("assigned-user:deleted", target.data("user-id"), selectedItem)
+                $scope.$broadcast("assigned-user:deleted", target.data("user-id"))
             closeLightbox()

         $el.on "click", ".close", (event) ->


performance increase for highlighting users in bars

Former-commit-id: 27e2918c0acfd1d114b4b038d7cbda504fdb811e [formerly 27d6ef0b1a708c2df38f620cbb9bf527839a163a] [formerly ae01f2e276e7e4386ccc65a69f2e92128033ba48 [formerly ae01f2e276e7e4386ccc65a69f2e92128033ba48 [formerly 91a8572f795559fc45fb38ae339058cff1b497da]]]
Former-commit-id: 1aaa3bb0db6431252f4e1150b083ee2fccd80005 [formerly 969640810956224e7420b1b47f0bf72e420638b9]
Former-commit-id: 62f81bb65458e85143cdd628fd426ffb28b8faa6
Former-commit-id: a0c3d23eaa7b8b68a972e0bb6405fb0cb3f617e0

--- app/assets/javascripts/backbone/apps/franklin/proposal/aggregate/aggregate_view.coffee
+++ app/assets/javascripts/backbone/apps/franklin/proposal/aggregate/aggregate_view.coffee
@@ -21,6 +21,7 @@
     template : '#tpl_aggregate_histogram'
     className : 'm-histogram'
     bar_state : null
+    highlight_state : []

     serializeData : ->

@@ -45,20 +46,33 @@
       $(ev.currentTarget).parent().trigger('mouseenter')

     highlightUsers : (users, highlight = true) ->
+
       if Modernizr.opacity
         selector = ("#avatar-#{uid}" for uid in users).join(',')

         @$el.hide()

         if highlight
-          @$el.addClass 'm-histogram-segment-selected'
+          if @highlight_state.length > 0
+            restore_opacity = ("#avatar-#{uid}" for uid in @highlight_state).join(',')
+            @$el.find(".avatar:not(#{restore_opacity})").css
+              opacity: ''
+            @highlight_state = []
+
           @$el.find(".avatar:not(#{selector})").css
             opacity: 0

         else
-          @$el.removeClass 'm-histogram-segment-selected'
-          @$el.find(".avatar:not(#{selector})").css
-            opacity: ''
+          @highlight_state = _.union users, @highlight_state
+          # this delay is a performance enhancement for skipping unhighlight when unnecessary
+          _.delay =>
+            if @highlight_state.length > 0
+              restore_opacity = ("#avatar-#{uid}" for uid in @highlight_state).join(',')
+              @$el.find(".avatar:not(#{restore_opacity})").css
+                opacity: ''
+
+              @highlight_state = []
+          , 10

         @$el.show()


Fix issue where paginator wasn't set for non-markdown index pages. Fixes #9.

--- src/plugins/jekyll-extensions.coffee
+++ src/plugins/jekyll-extensions.coffee
@@ -76,7 +76,7 @@
       # As in Jekyll, pagination only works in index.html files
       for page in site.pages
         # Only paginate at root
-        unless (page.url is 'index')
+        unless /^\/index(\.html)?$/.test page.url
           continue

         page_number = 1


Update path when move events occur

Update path when move events occur

--- src/stdlib/fs-utils.coffee
+++ src/stdlib/fs-utils.coffee
@@ -285,5 +285,8 @@

   watchPath: (path, callback) ->
     path = @absolute(path)
-    id = $native.watchPath(path, callback)
+    watchCallback = (eventType, eventPath) =>
+      path = @absolute(eventPath) if eventType is 'move'
+      callback(arguments...)
+    id = $native.watchPath(path, watchCallback)
     unwatch: -> $native.unwatchPath(path, id)


:lipstick:

:lipstick:

--- src/stdlib/fs-utils.coffee
+++ src/stdlib/fs-utils.coffee
@@ -77,11 +77,13 @@
   list: (rootPath, rest...) ->
     extensions = rest.shift() if rest.length > 1
     done = rest.shift()
-    fs.readdir rootPath, (err, paths) =>
-      return done(err) if err
-      paths = @filterExtensions(paths, extensions) if extensions
-      paths = paths.map (childPath) -> path.join(rootPath, childPath)
-      done(null, paths)
+    fs.readdir rootPath, (error, paths) =>
+      if error?
+        done(error)
+      else
+        paths = @filterExtensions(paths, extensions) if extensions
+        paths = paths.map (childPath) -> path.join(rootPath, childPath)
+        done(null, paths)

   filterExtensions: (paths, extensions) ->
     extensions = extensions.map (ext) ->
@@ -277,12 +279,14 @@

   readPlist: (plistPath, done) ->
     plist = require 'plist'
-    fs.readFile plistPath, 'utf8', (err, contents) ->
-      return done(err) if err
-      try
-        done(null, plist.parseStringSync(contents))
-      catch err
-        done(err)
+    fs.readFile plistPath, 'utf8', (error, contents) ->
+      if error?
+        done(error)
+      else
+        try
+          done(null, plist.parseStringSync(contents))
+        catch parseError
+          done(parseError)

   readObjectSync: (objectPath) ->
     CSON = require 'season'


managedvm: minor text change for success modal of managed vm

--- client/app/lib/components/sidebarmachineslistitem/connectedmanagedmachinewidget.coffee
+++ client/app/lib/components/sidebarmachineslistitem/connectedmanagedmachinewidget.coffee
@@ -75,7 +75,7 @@
           <div className='description'>
             You may now use this machine just like you use your Koding VM. You can open files, terminals and even initiate collaboration session.
           </div>
-          <button className='GenericButton' onClick={@bound 'handleButtonClick'}>Start Coding</button>
+          <button className='GenericButton' onClick={@bound 'handleButtonClick'}>START CODING</button>
         </div>
       </div>


Fixed incorrect variable

Fixed incorrect variable

--- app/js/jail_iframe/classes/highlight.coffee
+++ app/js/jail_iframe/classes/highlight.coffee
@@ -2,8 +2,8 @@
   constructor: (@id, @elements) ->
     @show_button = new FactlinkJailRoot.ShowButton @elements, @id

-  highlight:   -> @$elements.addClass('fl-core-highlight')
-  dehighlight: -> @$elements.removeClass('fl-core-highlight')
+  highlight:   -> $(@elements).addClass('fl-core-highlight')
+  dehighlight: -> $(@elements).removeClass('fl-core-highlight')

   destroy: ->
     for el in @elements


Minor: code styling

Minor: code styling

--- client/Login/AppView.coffee
+++ client/Login/AppView.coffee
@@ -364,6 +364,8 @@
                 name                  : 'password'
                 placeholder           : 'password'
                 validate              :
+                  events              :
+                    passwordCheck     : 'keyup'
                   rules               :
                     passwordCheck     : (input, event)=>

@@ -373,9 +375,6 @@
                       else
                         unless input.getValue() is passwordForm.inputs.confirm.getValue()
                           modal.setTitle "Looks good, please confirm it."
-
-                  events              :
-                    passwordCheck     : "keyup"

                 nextElement           :
                   confirm             :
@@ -384,21 +383,19 @@
                     name              : 'passwordConfirm'
                     placeholder       : 'confirm password'
                     validate          :
+                      events          :
+                        passwordCheck : 'keyup'
                       rules           :
                         passwordCheck : (input, event)=>

                           passwordForm = modal.modalTabs.forms.password
                           if passwordForm.inputs.password.getValue().length >= 8
                             if input.getValue() is passwordForm.inputs.password.getValue()
-                              modal.setTitle "Both look good!"
+                              modal.setTitle 'Both look good!'
                               passwordForm.buttons.submit.enable()
                             else
-                              modal.setTitle "Passwords should match."
+                              modal.setTitle 'Passwords should match.'
                               passwordForm.buttons.submit.disable()
-
-                      events          :
-                        passwordCheck : "keyup"
-

             buttons           :
               submit          :


Close ide pane quietly in collaboration when close request came from owner.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -1237,7 +1237,7 @@

         switch type
           when 'TabChanged'  then tabView.showPane paneView
-          when 'PaneRemoved' then tabView.removePane paneView
+          when 'PaneRemoved' then tabView.removePane paneView, no, yes
           when 'TerminalRenamed'
             ideView.renameTerminal paneView, @mountedMachine, context.session


Dependancy injection of RTCPeerConnection to make testing easier

--- src/public/models/remote.peer.coffee
+++ src/public/models/remote.peer.coffee
@@ -40,9 +40,8 @@
 		# @param id [String] the id of the peer to connect to
 		# @param instantiate [Booelean] wether to instantiate the connection or wait for the remote
 		#
-		initialize: ( @id, instantiate = true ) ->
-			@_connection = new RTCPeerConnection(@_serverConfiguration, @_connectionConfiguration)
-
+		initialize: ( @id, instantiate = true, webRTCObject = RTCPeerConnection ) ->
+			@_connection = new webRTCObject(@_serverConfiguration, @_connectionConfiguration)
 			@_connection.onicecandidate = @_onIceCandidate
 			@_connection.oniceconnectionstatechange = @_onIceConnectionStateChange
 			@_connection.ondatachannel = @_onDataChannel


taking out logging call

taking out logging call

--- static/coffee/tools.coffee
+++ static/coffee/tools.coffee
@@ -347,7 +347,6 @@
     for own k,v of datasource_selections
       selected = _.intersection.apply(_, v)
       datasources[k].set('selected', selected)
-      console.log('selected', selected)
       datasources[k].save()
     return null


fix see https://github.com/sagemathinc/smc/issues/918 -- "jupyter: be more careful when checking for unsaved changes when load not quite done yet"

--- src/smc-webapp/editor_jupyter.coffee
+++ src/smc-webapp/editor_jupyter.coffee
@@ -209,7 +209,8 @@

     # save notebook file from DOM to disk
     save: (cb) =>
-        @nb.save_notebook(false).then(cb)
+        # could be called when notebook is being initialized before nb is defined.
+        @nb?.save_notebook(false).then(cb)

     disable_autosave: () =>
         # We have our own auto-save system
@@ -1096,7 +1097,9 @@
     # this way too expensive (and any changes there will quickly get saved
     # to the syncstring, or don't matter).
     has_unsaved_changes: () =>
-        return @syncstring._syncstring.has_unsaved_changes()
+        # The question mark is necessary since @syncstring might not be defined when this gets called
+        # (see https://github.com/sagemathinc/smc/issues/918).
+        return @syncstring?._syncstring?.has_unsaved_changes()

     update_save_state: () =>
         if not @save_button? or @state != 'ready'


Improve documentation for the events module Simplify the module to simply define an object, rather than provide a callback, etc.

--- tapestry-core/src/main/coffeescript/META-INF/modules/core/events.coffee
+++ tapestry-core/src/main/coffeescript/META-INF/modules/core/events.coffee
@@ -12,19 +12,25 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.

+# ## core/events
+#
 # This module defines logical names for all events that Tapestry-controlled elements
 # trigger or listener for. Prototype requires that all custom events have a namespace prefix; jQuery appears to
 # allow it without issue.
-define [], ->
-  zone: # Invoked on a zone element to force an update to its content. The event memo is the
-  # new content (an Element, or a string containing HTML markup). A top-level handler
-  # is responsible for the actual update; it fires an event on the element just before,
-  # and just after, the content is changed.
+define
+  # Defines a number of event names specific to Tapestry Zones. Zones are Tapestry components that are structured
+  # to correctly support dynamic updates from the server via an Ajax request, and a standard response
+  # (the partial page render reponse). More details are available in the `core/zone` module.
+  zone:
+    # Invoked on a zone element to force an update to its content. The event memo is the new content (an Element, or a
+    # `core/spi:ElementWrapper`, or a string containing HTML markup). A standard top-level handler is defined by module
+    # `core/zone`, and is responsible for the actual update; it triggers the `events.zone.willUpdate` and
+    # `events.zone.didUpdate` events just before and just after changing the element's content.
     update: "t5:zone-update"

-    # Triggered just before the content in a Zone will be updated.
+    # Triggered (by the standard handler) just before the content in a Zone will be updated.
     willUpdate: "t5:zone-will-update"

-    # Triggered just afer the content in a Zone has updated. If the zone was not visible,
-    # it is made visible after its content is changed, and before this event is triggered.
+    # Triggered (by the standard hanndler) just after the content in a Zone has updated. If the zone was not visible, it
+    # is made visible after its content is changed, and before this event is triggered.
     didUpdate: "t5:zone-did-update"


Update boards on user changes

Update boards on user changes

- Closes  #1375

--- app/talk/init.cjsx
+++ app/talk/init.cjsx
@@ -24,6 +24,7 @@

   propChangeHandlers:
     'section': 'setBoards'
+    'user': 'setBoards'

   getInitialState: ->
     boards: []


Add referrer grouping, trying to make it remember progress and run quickly

--- scripts/buildSchoolGraph.coffee
+++ scripts/buildSchoolGraph.coffee
@@ -17,8 +17,7 @@
 UserHandler = require '../server/users/user_handler'
 User = require '../server/users/User'

-#startDate = new Date 2015, 11, 1
-startDate = new Date 2015, 11, 8  # Testing
+startDate = new Date 2015, 11, 1

 query = dateCreated: {$gt: startDate}, emailLower: {$exists: true}
 selection = 'name emailLower schoolName courseInstances clans ageRange dateCreated referrer points lastIP hourOfCode preferredLanguage lastLevel'
@@ -33,10 +32,10 @@
   return console.log('Done.') or process.exit() unless [userToSchool, suggestions] = findUserToSchool users
   question ?= formatSuggestions userToSchool, suggestions
   prompt question, (answer) ->
+    answer = answer.trim()
     return console.log('Bye.') or process.exit() if answer in ['q', 'quit']
-    answer = answer.trim()
     if answer is ''
-      users = _.without users, userToSchool
+      return nextPrompt _.without users, userToSchool
     else unless _.isNaN(num = parseInt(answer, 10))
       schoolName = if num then suggestions[num - 1]?.schoolName else userToSchool.schoolName
       return finalizePrompt userToSchool, suggestions, schoolName, users
@@ -45,13 +44,13 @@
       return nextPrompt users, "> "
     else
       return finalizePrompt userToSchool, suggestions, answer, users
-    nextPrompt users

 finalizePrompt = (userToSchool, suggestions, schoolName, users) ->
   console.log "Selected schoolName: \"#{schoolName}\""
-  question = "Also apply this to other users? Ex.: 'all', '0 1 2 5 9-14', 'all but 38 59-65', '0' to just do this one, or blank to retype school name.\n> "
+  question = "Also apply this to other users? Ex.: 'all', '0 1 2 5 9-14', 'all but 38 59-65', '0' to just do this one, q to quit, or blank to retype school name.\n> "
   prompt question, (answer) ->
     answer = answer.trim()
+    return console.log('Bye.') or process.exit() if answer in ['q', 'quit']
     if answer is ''
       console.log "Should just do", userToSchool._id, userToSchool.emailLower, userToSchool.schoolName
       targets = [userToSchool]
@@ -69,8 +68,8 @@
       numbers = findNumbers answer, suggestions.length
       targets = _.filter ((if number then suggestions[number - 1].user else userToSchool) for number in numbers)
       console.log "Doing #{targets.length} users for #{numbers}..."
-    #User.update {_id: {$in: (_.map targets, '_id')}}, {schoolName: schoolName}, {multi: true}, (err, result) ->
-    User.update {_id: {$in: []}}, {schoolName: schoolName}, {multi: true}, (err, result) ->
+    User.update {_id: {$in: (_.map targets, '_id')}}, {schoolName: schoolName}, {multi: true}, (err, result) ->
+    #User.update {_id: {$in: []}}, {schoolName: schoolName}, {multi: true}, (err, result) ->
       if err
         console.error "Ran into error doing the save:", err
         return finalizePrompt userToSchool, suggestions, schoolName, users
@@ -92,11 +91,11 @@
   numbers

 formatUser = (user) ->
-  # TODO: replace date string with relative time since signup compared to target user
+  # TODO: replace date string with relative time since signup compared to target user, and actually make suggestions based on students that signed up at almost the same time
   _.values(_.pick(user, ['name', 'emailLower', 'ageRange', 'dateCreated', 'lastLevel', 'points', 'referrer', 'hourOfCode'])).join('  ')

 formatSuggestions = (userToSchool, suggestions) ->
-  suggestionPrompts = ("#{_.str.rpad(i + 1, 3)}  #{_.str.rpad(s.schoolName, 50)} #{s.reasons.join(' + ')}\tfrom user: #{formatUser(s.user)}" for s, i in suggestions).join('\n')
+  suggestionPrompts = ("#{_.str.rpad(i + 1, 3)}  #{_.str.rpad(s.schoolName, 50)} #{s.reasons.length} #{if s.reasons.length > 1 then 'Matches' else 'Match'}: #{s.reasons.join(', ')}\tfrom user: #{formatUser(s.user)}" for s, i in suggestions).join('\n')
   """
   What should the school for this user be?
   0    #{_.str.rpad(userToSchool.schoolName, 50)} #{formatUser(userToSchool)}
@@ -107,35 +106,60 @@

 findUserToSchool = (users) ->
   # We find the top user from the top group that we can make the most reasoned suggestions about what the school name would be.
-  # TODO: don't show users where everyone in the suggestion already has the same school (because we have already done this group)
   [bestTarget, bestTargetSuggestions, mostReasons] = [null, [], 0]
   for field, groups of topGroups
-    largestGroup = groups[0]
-    target = userCategories[field][largestGroup][0]
-    suggestions = findSuggestions target
-    reasons = _.reduce suggestions, ((sum, suggestion) -> sum + (if suggestion.schoolName then suggestion.reasons.length else 0)), 0
-    if reasons > mostReasons
-      bestTarget = target
-      bestTargetSuggestions = suggestions
-      mostReasons = reasons
+    for nextLargestGroup in groups
+      possibleTargets = userCategories[field][nextLargestGroup]
+      schoolNames = _.uniq possibleTargets, 'schoolName'
+      # TODO: better method to avoid showing users where everyone in the suggestion already has the same school (because we have already done this group)
+      for schoolName in schoolNames
+        if _.filter(possibleTargets, schoolName: schoolName).length > 0.5 * possibleTargets.length
+          alreadyDone = true
+      continue if alreadyDone
+      nSamples = Math.min 15, Math.max(4, Math.floor possibleTargets.length / 20)
+      console.log 'Checking', nSamples, 'samples of', possibleTargets.length, 'players in the biggest', field, 'group:', nextLargestGroup
+      for i in [0 ... nSamples]
+        target = possibleTargets[Math.floor i * possibleTargets.length / (nSamples + 1)]
+        suggestions = findSuggestions target
+        reasons = _.reduce suggestions, ((sum, suggestion) ->
+          for suggestion in suggestions
+            for reason in suggestion.reasons
+              sum += switch reason
+                when 'Course instances' then 50
+                when 'IP' then 40
+                when 'Name' then 30
+                when 'Referrer' then 20
+                when 'Domain' then (if getDomain(target) is 'cps.edu' then 1 else 10)
+                when 'Clans' then 0.1
+          sum
+        ), 0
+        if reasons > mostReasons
+          bestTarget = target
+          bestTargetSuggestions = suggestions
+          mostReasons = reasons
+      break
   return [bestTarget, bestTargetSuggestions]

 findSuggestions = (target) ->
   # Look for other users with the same IP, course instances, clans, or similar school names or non-common shared email domains.
   suggestions = []
+  t0 = new Date()
+  console.log '  Checking suggestions for', target.emailLower, target.schoolName, (new Date()) - t0
   if target.lastIP
-    for otherUser in userCategories.lastIP[target.lastIP] when otherUser isnt target
-      suggestions.push schoolName: otherUser.schoolName, reasons: ["IP match"], user: otherUser
+    for otherUser in (userCategories.lastIP[target.lastIP] ? []) when otherUser isnt target
+      suggestions.push schoolName: otherUser.schoolName, reasons: ['IP'], user: otherUser
   for leagueType in ['courseInstances', 'clans']
+    console.log '    Now checking', leagueType, (new Date()) - t0
     if target[leagueType]?.length
       for league in target[leagueType]
-        for otherUser in userCategories[leagueType][league] when otherUser isnt target
-          reason = "#{_.str.humanize(leagueType)} match"
+        for otherUser in (userCategories[leagueType][league] ? []) when otherUser isnt target
+          reason = _.str.humanize(leagueType)
           if existingSuggestion = _.find(suggestions, user: otherUser)
             existingSuggestion.reasons.push reason
           else
             suggestions.push schoolName: otherUser.schoolName, reasons: [reason], user: otherUser
   if target.schoolName?.length > 5
+    console.log '    Now checking schoolName', (new Date()) - t0
     nameMatches = []
     for otherSchoolName in topGroups.schoolName
       score = stringScore otherSchoolName, target.schoolName, 0.8
@@ -143,20 +167,31 @@
       nameMatches.push schoolName: otherSchoolName, score: score
     nameMatches = (match.schoolName for match in (_.sortBy nameMatches, (match) -> -match.score))
     for match in nameMatches.slice(0, 10)
-      reason = "Name match"
-      for otherUser in userCategories.schoolName[match] when otherUser isnt target
+      reason = "Name"
+      for otherUser in (userCategories.schoolName[match] ? []) when otherUser isnt target
         if existingSuggestion = _.find(suggestions, user: otherUser)
           existingSuggestion.reasons.push reason
         else
           suggestions.push schoolName: match, reasons: [reason], user: otherUser
+  console.log '    Now checking domain', (new Date()) - t0
   if domain = getDomain target
-    for otherUser in userCategories.domain[domain] when otherUser isnt target
-      reason = "Domain match"
+    for otherUser in (userCategories.domain[domain] ? []) when otherUser isnt target
+      reason = "Domain"
       if existingSuggestion = _.find(suggestions, user: otherUser)
         existingSuggestion.reasons.push reason
       else
         suggestions.push schoolName: otherUser.schoolName, reasons: [reason], user: otherUser
-  return _.uniq suggestions, 'user'
+  console.log '    Now checking referrer', (new Date()) - t0
+  if referrer = getReferrer target
+    for otherUser in (userCategories.referrer[referrer] ? []) when otherUser isnt target
+      reason = "Referrer"
+      if existingSuggestion = _.find(suggestions, user: otherUser)
+        existingSuggestion.reasons.push reason
+      else
+        suggestions.push schoolName: otherUser.schoolName, reasons: [reason], user: otherUser
+  suggestions = _.sortBy suggestions, 'schoolName'
+  suggestions = _.sortBy suggestions, (s) -> -s.reasons.length
+  return suggestions

 userCategories = {}
 topGroups = {}
@@ -165,7 +200,7 @@
 sortUsers = (users) ->
   users = _.sortBy users, (u) -> -u.points
   users = _.sortBy users, ['schoolName', 'lastIP']
-  for field in ['courseInstances', 'lastIP', 'schoolName', 'domain', 'clans']
+  for field in ['courseInstances', 'lastIP', 'schoolName', 'domain', 'clans', 'referrer']
     userCategories[field] = categorizeUsers users, field
     topGroups[field] = _.sortBy _.keys(userCategories[field]), (key) -> -userCategories[field][key].length
     topGroups[field] = (group for group in topGroups[field] when 2 < userCategories[field][group].length < (if field is 'clans' then 30 else 5000))
@@ -175,6 +210,8 @@
   for user in users
     if field is 'domain'
       value = getDomain user
+    else if field is 'referrer'
+      value = getReferrer user
     else
       value = user[field]
     continue unless value
@@ -185,12 +222,26 @@
       categories[value].push user
   categories

+typoCache = {}
 getDomain = (user) ->
-  domain = user.emailLower.split('@')[1]
+  return null unless domain = user.emailLower.split('@')[1]
   return null if commonEmailDomainMap[domain]
+  typo = typoCache[domain]
+  return null if typo
+  return domain if typo is false
   typo = _.find commonEmailDomains, (commonDomain) -> stringScore(commonDomain, domain, 0.8) > 0.9
+  typoCache[domain] = Boolean(typo)
   return null if typo
   domain
+
+commonReferrersRegex = /(google|bing\.|yahoo|duckduckgo|jobs\.lever|code\.org|twitter|facebook|dollarclick|stumbleupon|vk\.com|playpcesor|reddit|lifehacker|favorite|bnext|freelance|taringa|blogthinkbig|graphism|inside\.com|korben|habrahabr|iplaysoft|geekbrains|playground|ycombinator|github\.com)/
+getReferrer = (user) ->
+  return null unless referrer = user.referrer?.toLowerCase().trim()
+  referrer = referrer.replace /^https?:\/\//, ''
+  return null if commonReferrersRegex.test referrer
+  return classCode if classCode = referrer.match(/\?_cc=(\S+)/)?[1]
+  return null if /codecombat/.test referrer
+  referrer

 # https://github.com/joshaven/string_score
 stringScore = (_a, word, fuzziness) ->


handle Android Chrome error

handle Android Chrome error

Former-commit-id: 311bd7be724f84c11c83fb7a8ca52078b448f571 [formerly ca18c959ee58c7d688db2037909c9459e1ed8857] [formerly 80825d75385f8bed0c9db6f1d9fecc02bc2f3a98 [formerly 913cbfcaedff09660c1065d67dcfe02f4e276358]] [formerly 459ae681dbd2be270e75d321b791cd0e398a2e8e [formerly 1d2680964ea10ae4b6d10889acd1aadccccb5aba] [formerly 92ab667925e020c19dcf86b4e3d83309e582837a [formerly 92ab667925e020c19dcf86b4e3d83309e582837a [formerly 002e80a88a826cb744464632a3b9736a65f9223c]]]]
Former-commit-id: 6cc5cb4a0c1a11abcac2d362d3a75d4e7ce9f721 [formerly 1a26170d6e3435c54a9ef5125d1611cf3ff265b0] [formerly 36d198e0fa3cc1052da524949859903ba24e10a9 [formerly 6e252ca6f7402ad3880fea4d8f42268f330e623d]]
Former-commit-id: f9dee983b6a44469284ba165513505d79a9e19e9 [formerly 31cc0735cd56c48b1b57fac8b331c26f488c596c]
Former-commit-id: 838293d20924fab1b15eb0af3a8c0a880e02979a
Former-commit-id: 4de8cb8e3828084605db45da9b373ee66b5bad49

--- @client/assets/javascripts/vendor_extensions/jquery_extensions.js.coffee
+++ @client/assets/javascripts/vendor_extensions/jquery_extensions.js.coffee
@@ -59,6 +59,11 @@
     #    if scrolled past bottom of element, position bottom of element at bottom of viewport
     #      (stick to bottom)
     update : ->
+
+      if !@$el
+        console.error('sticky element has disappeared!!')
+        @destroy()
+        return

       container_top = @options.container.offset().top
       viewport_top = (document.documentElement.scrollTop || document.body.scrollTop)
@@ -100,7 +105,6 @@

       # Don't need to do anything if we're not actually stuck
       return if !@is_stuck
-

       ######
       # HORIZONTAL ADJUSTMENTS
@@ -235,7 +239,7 @@

     destroy : ->
       $(window).off ".plugin_stickytopbottom-#{@my_id}"
-      $.removeData @$el[0], "plugin_stickytopbottom"
+      $.removeData @$el[0], "plugin_stickytopbottom" if @$el
       @$el = null


fix getuser

fix getuser

--- scripts/pagerv2_commands.coffee
+++ scripts/pagerv2_commands.coffee
@@ -425,7 +425,7 @@
     [ _, who ] = res.match
     pagerv2.getPermission(res.envelope.user, 'pageruser')
     .then ->
-      pagerv2.setOverride(res.envelope.user, who)
+      pagerv2.setOverride(res.envelope.user, { name: who })
     .then (data) ->
       res.send "Rejoice #{data.over.from}! #{data.over.name} is now on call."
     .catch (e) ->
@@ -437,7 +437,7 @@
     [ _, who ] = res.match
     pagerv2.getPermission(res.envelope.user, 'pageruser')
     .then ->
-      pagerv2.dropOverride(res.envelope.user, who)
+      pagerv2.dropOverride(res.envelope.user, { name: who })
     .then (data) ->
       if data
         if who is 'me'
@@ -454,7 +454,7 @@
     [ _, who, duration ] = res.match
     pagerv2.getPermission(res.envelope.user, 'pageruser')
     .then ->
-      pagerv2.setOverride(res.envelope.user, who, duration)
+      pagerv2.setOverride(res.envelope.user, { name: who }, duration)
     .then (data) ->
       res.send "Rejoice #{data.over.from}! #{data.over.name} is now on call."
     .catch (e) ->


Clearer error message

Clearer error message

--- components/GetSource.coffee
+++ components/GetSource.coffee
@@ -73,7 +73,7 @@
         return callback()

       if rounds <= 0
-        return callback new Error 'Runtime didn\'t provide source in time'
+        return callback new Error "Runtime didn't provide source for #{name} in time"

       setTimeout poll, 100
     setTimeout poll, 200


Finder: finder made a viewless controller

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -6,7 +6,6 @@

   constructor:(options, data)->

-    # options.view    = new FinderView
     options.appInfo = name : "Finder"

     super options, data


Summary helper is more flexible, can specify seperator and trimmer

--- src/typhoon/helpers.coffee
+++ src/typhoon/helpers.coffee
@@ -29,7 +29,7 @@

 module.exports = class Helpers
   @markdown: (str) -> markdown str
-  @summary: (body) -> body.split('<!-- more -->')[0].replace /\.$/, '…'
+  @summary: (body, seperator = '<!-- more -->', trimmer = '…') -> body.split(seperator)[0].replace /\.$/, trimmer
   @gravatar: (email, size = 50) ->
     'http://www.gravatar.com/avatar/' +
     crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex') +


Reduce crazy number of test case asserts.

--- test/integration/connection-test.coffee
+++ test/integration/connection-test.coffee
@@ -61,8 +61,9 @@

 exports.execSqlWithLotsOfRowsReturned = (test) ->
   numberOfRows = 1000
+  rowsReceived = 0

-  test.expect(numberOfRows + 5)
+  test.expect(6)

   connection = new Connection(config.server, config.userName, config.password, config.options, (err, loggedIn) ->
     test.ok(!err)
@@ -71,6 +72,7 @@
     connection.execSql("select top #{numberOfRows} object_id, name from sys.all_columns", (err, rowCount) ->
       test.ok(!err)
       test.strictEqual(rowCount, numberOfRows)
+      test.strictEqual(rowsReceived, numberOfRows)
       test.done()
     )
   )
@@ -80,7 +82,7 @@
   )

   connection.on('row', (columns) ->
-    test.strictEqual(columns.length, 2)
+    rowsReceived++
   )

   connection.on('debug', (message) ->


refactor database query helpers

refactor database query helpers

--- backend/db.coffee
+++ backend/db.coffee
@@ -12,6 +12,20 @@
 db.setMaxListeners 100

 fs.ensureDirSync config.dir
+
+
+# TODO add tests
+getByKey = (query, key, callback) ->
+    params =
+        include_docs: true
+        key: key
+    db.query query, params, (err, docs) ->
+        if err
+            callback err
+        else if docs.rows.length is 0
+            callback()
+        else
+            callback null,  docs.rows[0].value

 module.exports = dbHelpers =
@@ -32,16 +46,7 @@
                 params = {}
             dbHelpers.db.query 'file/all', params, callback
         get: (key, callback) ->
-            params =
-                include_docs: true
-                key: key
-            db.query 'file/byFullPath', params, (err, docs) ->
-                if err
-                    callback err
-                else if docs.rows.length is 0
-                    callback()
-                else
-                    callback null,  docs.rows[0].value
+            getByKey 'file/byFullPath', key, callback

     folders:
         rows: []
@@ -53,16 +58,7 @@
             dbHelpers.db.query 'folder/all', params, callback

         get: (key, callback) ->
-            params =
-                include_docs: true
-                key: key
-            db.query 'folder/byFullPath', params, (err, docs) ->
-                if err
-                    callback err
-                else if docs.rows.length is 0
-                    callback()
-                else
-                    callback null,  docs.rows[0].value
+            getByKey 'folder/byFullPath', key, callback

         upsert: (newDoc, callback) ->
             key = "#{newDoc.path}/#{newDoc.name}"
@@ -91,6 +87,8 @@
                 callback = params
                 params = {}
             dbHelpers.db.query 'binary/all', params, callback
+        get: (key, callback) ->
+            getByKey 'binary/byChecksum', key, callback

     # Create all required views in the database.


Spec: Added a bunch of new specs to Exo.Spine.Controller

--- test/specs/spine/controller_spec.coffee
+++ test/specs/spine/controller_spec.coffee
@@ -15,6 +15,10 @@
   beforeEach ->
     test = new Exo.Spine.Controller

+  it 'should be possible to set nodeId', ->
+    test.setNodeId('banana')
+    expect(test.nodeId()).to.equal 'banana'
+
   it 'should be deactivated', ->
     expect(test.isActivated()).to.equal false

@@ -47,6 +51,32 @@
     test.removeChild(test.children()[0])
     expect(test.children().length).to.equal 4

+  it 'should not allow passing self to addChild', ->
+    expect( ->
+      test.addChild(test)
+    ).to.throw /ExoReferenceError/
+
+  it 'should not allow passing null to addChild', ->
+    expect( ->
+      test.addChild(null)
+    ).to.throw /ExoReferenceError/
+
+  it 'should not allow passing undefined to addChild', ->
+    expect( ->
+      test.addChild()
+    ).to.throw /ExoReferenceError/
+
+  it 'should not allow a child to activate it if opts.childrenCanActivate = false', ->
+    test = new Exo.Spine.Controller
+      childrenCanActivate: false
+
+    c = new Exo.Spine.Controller
+    test.addChild c
+    c.activate()
+
+    expect(c.isActivated()).to.equal false
+    expect(test.isActivated()).to.equal false
+
   it 'should throw "Exo: Incompatible object" when invalid objects are passed to node.addChild'

   it 'should trigger "onActivated" and pass itself when activated', (done)->
@@ -105,6 +135,14 @@
     s.activate()
     test.activate()
     expect(test.isActivated() and p.isActivated() and not s.isActivated()).to.equal true
+
+  it "should be possible to fetch a child by id", ->
+    expect(p.childById(s.nodeId())).to.be.an('object')
+    expect(p.childById(s.nodeId()).nodeId()).to.equal s.nodeId()
+
+  it "should be possible to fetch a descendant by id", ->
+    expect(p.descendantById(s.nodeId())).to.be.an('object')
+    expect(p.descendantById(s.nodeId()).nodeId()).to.equal s.nodeId()

 describe "Exo.Spine.Controller as a parent and child", ->
   p = c = test = null


Fixed requirement of one comment per user

--- server/controllers/deals.coffee
+++ server/controllers/deals.coffee
@@ -74,30 +74,36 @@
       res.send JSON.stringify(deal)

 # functions about Comments
+hasWritten = (req, user) ->
+  i = 0
+  while i < req.deal.comments.length
+    return true  if req.deal.comments[i].author._id.equals user._id
+    i++
+  return false
+
 exports.addComment = (req, res) ->
   if req.query.author and req.query.description and req.query.rating
     User.findOne(_id: req.query.author).exec (err, user) ->
       if user
-        Deal.findOne("comments.author": req.query.author).exec (err, user) ->
-          if !user
-            comment =
-              author     : req.query.author
-              description: req.query.description
-              rating     : Number(req.query.rating)
+        if !(hasWritten(req, user))
+          comment =
+            author     : req.query.author
+            description: req.query.description
+            rating     : Number(req.query.rating)

-            sumOfRatings = req.deal.average * (req.deal.comments.length)
-            average      = (sumOfRatings + comment.rating) / (req.deal.comments.length + 1)
+          sumOfRatings = req.deal.average * (req.deal.comments.length)
+          average      = (sumOfRatings + comment.rating) / (req.deal.comments.length + 1)

-            req.deal.comments.push comment
+          req.deal.comments.push comment

-            req.deal = _.extend(req.deal, "average": average)
-            req.deal.save (err) ->
-              if err
-                res.status(500).send("error at add a comment")
-              else
-                res.jsonp req.deal
-          else
-            res.status(500).send("this user has written a comment in this deal")
+          req.deal = _.extend(req.deal, "average": average)
+          req.deal.save (err) ->
+            if err
+              res.status(500).send("error at add a comment")
+            else
+              res.jsonp req.deal
+        else
+          res.status(500).send("this user has written a comment in this deal")
       else
         res.status(404).send("the user does not exist")
   else


Check for stopErrorDisplay flag on failedData

That's what's passed to components

--- src/concept-coach/error-notification.cjsx
+++ src/concept-coach/error-notification.cjsx
@@ -15,8 +15,8 @@
   componentWillUnmount: ->
     api.channel.off 'error', @onError

-  onError: ({response}) ->
-    return if response.stopErrorDisplay # someone else is handling displaying the error
+  onError: ({response, failedData}) ->
+    return if failedData.stopErrorDisplay # someone else is handling displaying the error
     errors = ["#{response.status}: #{response.statusText}"]
     if response.data?.errors # we have something from server to display
       errors = errors.concat(


removed kitesubscription handler route

removed kitesubscription handler route

--- servers/lib/server/index.coffee
+++ servers/lib/server/index.coffee
@@ -72,7 +72,6 @@
 app.get  '/-/google-api/authorize/drive'         , require './handlers/authorizedrive'
 app.post '/-/video-chat/session'                 , require './handlers/videosession'
 app.post '/-/video-chat/token'                   , require './handlers/videotoken'
-app.get  '/-/subscription/check/:kiteToken?/:user?/:groupId?' , require './handlers/kitesubscription'
 app.get  '/-/auth/check/:key'                    , require './handlers/authkeycheck'
 app.post '/-/support/new', bodyParser.json()     , require './handlers/supportnew'
 app.get  '/-/auth/register/:hostname/:key'       , require './handlers/authregister'


substantial upgrades to the feature exchange

--- library/gearbox/exchange.coffee
+++ library/gearbox/exchange.coffee
@@ -55,6 +55,77 @@
     # Once inherited from, the inheritee is not abstract anymore.
     @abstract yes

+    # Declare the designated feature in the current class or ABC.
+    # Every feature has a string designating its name, followed by
+    # an arbitrary amount of arguments. It is typically to supply
+    # strings, numbers and functions as the arguments. This method
+    # is using the `transferred` mechanism, therefore all the args
+    # including functions are automatically going to client site.
+    @feature: @transferred (designate, parameters...) ->
+        unknown = "no feature designation is supplied"
+        eintern = "an internal error with feature set"
+        brokeService = "the service seems to be broken"
+        assert n = "Service %s is providing %s feature"
+        assert identify = this.service.toString().bold
+        assert _.isString(designate or null), unknown
+        assert _.isArray(parameters or null), eintern
+        assert _.isFunction(@broadcast), brokeService
+        assert _.isObject this.$features = new Object
+        assert _.isString sfeature = (designate.bold)
+        assert designate = try designate.toLowerCase()
+        assert this.$features[designate] = parameters
+        logger.info n, identify.green, sfeature.green
+        this.emit "feature", designate, parameters...
+        @broadcast "exchange-feature-x", arguments...
+
+    # Declare the aligning of the current featured service. This
+    # means the absolute numeric value of the service ordering as
+    # compared to other services. The argument has to be either a
+    # number (integral or decimal) or a string that select one of
+    # the predefined numbers: `normal`, `early`, `late`. Please
+    # refer to this method source code to get more information.
+    @aligning: @transferred (ordinal) ->
+        nonum = "the ordinal argument has to be a number"
+        message = "Featured service %s aligning set to %s"
+        brokeService = "the service seems like is broken"
+        stop = -> throw new Error "cannot lookup ordinal"
+        lookup = (n) -> stop() unless n of ords; ords[n]
+        ords = normal: 0.00, early: -1.00, late: +1.00
+        ordinal = lookup ordinal if _.isString ordinal
+        assert _.isNumber(ordinal), nonum # only number
+        assert _.isFunction(@broadcast), brokeService
+        assert identify = this.service.toString().bold
+        assert stringed = try (ordinal.toString().bold)
+        assert _.isNumber this.$aligning = ordinal or 0
+        logger.debug message.grey, identify, stringed
+        @broadcast "exchange-disposition", arguments...
+        @emit "disposition", arguments...; return this
+
+    # Query all the services present in the established ecosystem
+    # for the feature designated by the supplied string. In case if
+    # there is no services with this feature, the exception thrown.
+    # Otherwise, pick the last (according to the aligning) matching
+    # service, and invoke the `action` with the designated feature.
+    # Please refer to the method source code for more infromation.
+    providesFeature: external (designate, action) ->
+        noEco = "service ecosystem is missing of root"
+        unknown = "no feature designation is supplied"
+        eintern = "missing an feature action function"
+        empty = "feature #{designate} is not provided"
+        assert _.isArray(@root.ecosystem or 0), noEco
+        assert _.isString(designate or null), unknown
+        assert _.isFunction(action or false), eintern
+        assert fp = (srv) -> _.isObject srv.$features
+        assert normal = try (designate.toLowerCase())
+        hits = (sx) -> try _.has sx.$features, normal
+        getAligningsOrd = (srv) -> srv.$aligning or 0
+        services = _.filter @root.ecosystem or [], fp
+        services = _.sortBy services, getAligningsOrd
+        filtered = _.filter services or Array(), hits
+        throw new Error(empty) if _.isEmpty(filtered)
+        assert _.isObject sx = serv = _.last filtered
+        return action.apply sx, sx.$features[normal]
+
     # Query all the services present in the established ecosystem
     # for the feature designated by the supplied string. For every
     # service (feature) that matches the criteria, action function
@@ -70,33 +141,12 @@
         assert _.isFunction(action or false), eintern
         assert fp = (srv) -> _.isObject srv.$features
         assert normal = try (designate.toLowerCase())
+        getAligningsOrd = (srv) -> srv.$aligning or 0
         services = _.filter @root.ecosystem or [], fp
+        services = _.sortBy services, getAligningsOrd
         for service in services # iterate the services
             assert features = service.$features or {}
-            continue unless _.has features, designate
+            continue unless try _.has features, normal
             assert parameters = features[normal] or 0
             assert _.isArray(parameters), "no params"
             action.apply service, parameters # invoke
-
-    # Declare the designated feature in the current class or ABC.
-    # Every feature has a string designating its name, followed by
-    # an arbitrary amount of arguments. It is typically to supply
-    # strings, numbers and functions as the arguments. This method
-    # is using the `transferred` mechanism, therefore all the args
-    # including functions are automatically going to client site.
-    @feature: @transferred (designate, parameters...) ->
-        unknown = "no feature designation is supplied"
-        eintern = "an internal error with feature set"
-        brokeService = "the service seems to be broke"
-        assert n = "Service %s is providing %s feature"
-        assert identify = this.service.toString().bold
-        assert _.isString(designate or null), unknown
-        assert _.isArray(parameters or null), eintern
-        assert _.isFunction(@broadcast), brokeService
-        assert _.isObject this.$features = new Object
-        assert _.isString sfeature = (designate.bold)
-        assert designate = try designate.toLowerCase()
-        assert this.$features[designate] = parameters
-        logger.info n, identify.green, sfeature.green
-        this.emit "feature", designate, parameters...
-        @broadcast "exchange-feature-x", arguments...


extend cakefile for future changes

extend cakefile for future changes

--- library/nucleus/cakefile.coffee
+++ library/nucleus/cakefile.coffee
@@ -68,6 +68,7 @@
     # testing. It requires and bootstraps the kernel of the framework
     # as if it was an end user application, built with the framework.
     # This task can be used as a template for your end application!
+    # In terms of scalability - it starts the application instance.
     task "boot", "bootstrap the framework kernel", (options) ->
         library = options.library or DEFAULT_LIBRARY
         scoping = options.scoping or DEFAULT_SCOPING
@@ -75,13 +76,14 @@
         process.env["NODE_ENV"] = scoping.toString()
         process.env["log:level"] = logging.toString()
         granite = require "#{__dirname}/../../index"
-        assert resolved = paths.resolve library
+        assert resolved = paths.resolve library or null
         missingLibrary = "missing library: #{resolved}"
         assert _.isObject(granite), "framework failed"
         assert fs.existsSync(library), missingLibrary
         compiled = granite.collectPackages no, library
         assert _.isObject(compiled), "invalid library"
-        granite.cachedKernel(library).bootstrap()
+        conf = new Object master: no, instance: yes
+        granite.cachedKernel(library).bootstrap conf

     # This is one of the major tasks in this Cakefile, it implements
     # the cleanup of everything that could have been generated by the


added 1 to javascript index so that the right card expands

--- app/assets/javascripts/card.js.coffee
+++ app/assets/javascripts/card.js.coffee
@@ -7,6 +7,6 @@
 jQuery ->
   $(".request-btn").click(->
     if $("#sign_out").length > 0
-      index = $(this).index(".request-btn")
+      index = $(this).index(".request-btn") + 1
       $(".request").eq(index).toggleClass("hidden")
   )


fix typo

fix typo

--- app/assets/javascripts/game.js.coffee
+++ app/assets/javascripts/game.js.coffee
@@ -18,7 +18,7 @@
     glasses: 'has glasses'
     hair: 'haired'
     hair_color: {
-      black: 'black'.
+      black: 'black',
       blonde: 'blonde',
       brown: 'brown',
       pink: 'pink',
@@ -30,7 +30,7 @@
     teeth: 'visible teeth'
     beard_or_mustaches: 'visible beard or mustaches'
   }
-
+
   faces = {
     0: {
       sex: face_values['female'],


Add a Brazilian Portuguese to i18n.js.coffee

--- app/assets/javascripts/i18n.js.coffee
+++ app/assets/javascripts/i18n.js.coffee
@@ -9,6 +9,16 @@
     checked:
       pre: 'You\'ve marked '
       post: ' entries to be billed'
+  "pt-BR":
+    date:
+      today: 'hoje'
+      yesterday: 'ontem'
+      future: 'dias no futuro'
+      dateAgo: 'daias atrás'
+      format: 'DD/MM/YYYY'
+    checked:
+      pre: 'Você marcou '
+      post: ' entradas a serem faturadas'
   nl:
     date:
       today: 'vandaag'


shows title

shows title

--- app/assets/javascripts/maps.js.coffee
+++ app/assets/javascripts/maps.js.coffee
@@ -16,7 +16,8 @@
       vLatLng = new google.maps.LatLng obj.violation.latitude, obj.violation.longitude
       markers[obj.violation.id] = new google.maps.Marker
         map: organization_map,
-        position: vLatLng
+        position: vLatLng,
+        title: obj.violation.title
       violatorBounds.extend vLatLng

     organization_map.setCenter violatorBounds.getCenter()
@@ -36,7 +37,8 @@
       vLatLng = new google.maps.LatLng obj.violation.latitude, obj.violation.longitude
       markers[obj.violation.id] = new google.maps.Marker
         map: violator_map,
-        position: vLatLng
+        position: vLatLng,
+        title: obj.violation.title
       violatorBounds.extend vLatLng

     violator_map.setCenter violatorBounds.getCenter()


experiment with different settings for cytoscape quiz rendering

--- app/assets/javascripts/quizzes.coffee
+++ app/assets/javascripts/quizzes.coffee
@@ -84,8 +84,6 @@
     cy = cytoscape(
       container: $cyContainer
       elements: $cyContainer.data('elements')
-      userPanningEnabled: false
-      autoungrabify: true
       style: [
         {
           selector: 'node'
@@ -101,8 +99,8 @@
             'line-color': 'data(color)'
             'curve-style': 'bezier'
             'control-point-distances' : '-50 50 -50'
-            'target-arrow-color': 'data(color)'
-            'target-arrow-shape': 'triangle'
+            'mid-target-arrow-color': 'data(color)'
+            'mid-target-arrow-shape': 'triangle'
             'arrow-scale': 2
         }
         {
@@ -121,7 +119,14 @@
         }
       ]
       layout:
-        name: 'breadthfirst'
+        name: 'circle'
+        nodeDimensionsIncludeLabels: false
+#        circle: true
+#        grid: true
+#        roots: '1'
+#        rows: 5
+#        columns: 5
+        fit: true
         directed: false)


Fixed hide compete button when there is an error submitting the form

--- app/assets/javascripts/user.js.coffee
+++ app/assets/javascripts/user.js.coffee
@@ -1,6 +1,8 @@
 window.hidePrizesCheckbox = () ->
-  if $('#user_state').val() == "NA" or $('#user_year_in_school').val() == "0"
-    $('#compete-for-prizes').hide()
-    $('#user_compete_for_prizes').prop("checked", false);
-  else
-    $('#compete-for-prizes').show()
+    if $('#user_state').val() == "NA" or $('#user_year_in_school').val() == "0"
+      $('#compete-for-prizes').hide()
+      $('#user_compete_for_prizes').prop("checked", false);
+    else
+     $('#compete-for-prizes').show()
+$(document).ready(window.hidePrizesCheckbox)
+$(document).on('page:load', window.hidePrizesCheckbox)


radar axis defaults to using x axis styles

--- src/viz/types/radar.coffee
+++ src/viz/types/radar.coffee
@@ -29,8 +29,10 @@
   labels = (fetchText(vars, c, nextDepth)[0] for c in children)
   labels = uniques(d3.merge(labels))
   textStyle = {
-    "fill": "#444"
-    "font-size": "12px"
+    "fill": vars.x.ticks.font.color
+    "font-family": vars.x.ticks.font.family.value
+    "font-weight": vars.x.ticks.font.weight
+    "font-size":   vars.x.ticks.font.size+"px"
   }
   sizes = fontSizes labels, textStyle,
     mod: (elem) ->


fix assigned to change on issues list

fix assigned to change on issues list

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -791,7 +791,7 @@
                 imgurl: "/#{window._version}/images/unnamed.png"
             }

-            member = issue.assigned_to_extra_info
+            member = $scope.usersById[issue.assigned_to]
             if member
                 ctx.name = member.full_name_display
                 ctx.imgurl = member.photo


Fix Bug #1143: Add or remove blocked class to the kanban card when the US is updated

--- app/coffee/modules/kanban/main.coffee
+++ app/coffee/modules/kanban/main.coffee
@@ -324,14 +324,18 @@
     link = ($scope, $el, $attrs, $model) ->
         $el.disableSelection()

+        $scope.$watch "us", (us) ->
+            if us.is_blocked and not $el.hasClass("blocked")
+                $el.addClass("blocked")
+            else if not us.is_blocked and $el.hasClass("blocked")
+                $el.removeClass("blocked")
+
         $el.find(".icon-edit").on "click", (event) ->
             if $el.find(".icon-edit").hasClass("noclick")
                 return

             $scope.$apply ->
                 $rootscope.$broadcast("usform:edit", $model.$modelValue)
-        if $scope.us.is_blocked
-            $el.addClass("blocked")

         $scope.$on "$destroy", ->
             $el.off()


Add KEY_WOW64_32KEY and KEY_WOW64_64KEY

Add KEY_WOW64_32KEY and KEY_WOW64_64KEY

--- src/win32/constants.coffee
+++ src/win32/constants.coffee
@@ -20,6 +20,8 @@
   @KEY_WRITE: @STANDARD_RIGHTS_WRITE | @KEY_SET_VALUE | @KEY_CREATE_SUB_KEY
   @KEY_EXECUTE: @KEY_READ
   @KEY_ALL_ACCESS: @KEY_READ | @KEY_WRITE | @KEY_CREATE_LINK
+  @KEY_WOW64_32KEY = 0x0200 # Windows 2000: This flag is not supported.
+  @KEY_WOW64_64KEY = 0x0100 #Windows 2000: This flag is not supported.

   # winerror.h
   @ERROR_SUCCESS = 0


get how many days ago

get how many days ago

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -16,7 +16,7 @@
     @storage = new LocalStorage()
     today = new Date()
     comparison = today - @storage.created_at
-    $("#created_at").append(@storage.created_at)
+    $("#created_at").append(moment.duration(comparison).days)
     @current = ""
     @fail = 0
     @reveal = ""


better clearing of project cache

better clearing of project cache

--- src/smc-hub/client.coffee
+++ src/smc-hub/client.coffee
@@ -930,11 +930,10 @@
             else
                 project = hub_projects.new_project(mesg.project_id, @database, @compute_server)
                 @database.touch_project(project_id:mesg.project_id)
-                if not @_project_cache?
-                    @_project_cache = {}
+                @_project_cache ?= {}
                 @_project_cache[key] = project
                 # cache for a while
-                setTimeout((()=>delete @_project_cache[key]), CACHE_PROJECT_AUTH_MS)
+                setTimeout((()=>delete @_project_cache?[key]), CACHE_PROJECT_AUTH_MS)
                 dbg("got project; caching and returning")
                 cb(undefined, project)
         )


Can now remove an event

Can now remove an event

--- source/coffee/Enumerable.coffee
+++ source/coffee/Enumerable.coffee
@@ -54,6 +54,10 @@
   register : (event, fn) ->
     @events[event] = fn

+  deregister : (event) ->
+    if @events[event]?
+      delete @events[event]
+
   trigger : (event, args...) ->
     if event? && @events[event]?
       @events[event](args)


Bypass attestation for old generation dongles (< LW 1.0.0)

--- app/src/wallet/hardware_wallet.coffee
+++ app/src/wallet/hardware_wallet.coffee
@@ -214,6 +214,9 @@

   _checkDongleCertification: (Attestation, callback) ->
     completion = new CompletionClosure(callback)
+    if @getIntFirmwareVersion() < ledger.wallet.Firmware.V_LW_1_0_0
+      completion.success(true)
+      return completion.readonly()
     randomValues = new Uint32Array(2)
     crypto.getRandomValues(randomValues)
     random = _.str.lpad(randomValues[0].toString(16), 8, '0') + _.str.lpad(randomValues[1].toString(16), 8, '0')


Specs for running keydown commands when remainder is only keyup events

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -359,20 +359,39 @@
             "ctrl-y ^ctrl": "y-command-ctrl-up"
             "ctrl-x ^ctrl": "x-command-ctrl-up"
             "ctrl-y ^y ^ctrl": "y-command-y-up-ctrl-up"
+            "a": "a-command"
             "a b c ^b ^a ^c": "abc-secret-code-command"
+
+      it "dispatches the command for a binding containing only keydown events immediately even when there is a corresponding multi-stroke binding that contains only other keyup events", ->
+        keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
+        expect(events).toEqual ['y-keydown']

       it "dispatches the command when a matching keystroke precedes it", ->
         keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
+        expect(events).toEqual ['y-keydown']
         keymapManager.handleKeyboardEvent(buildKeyupEvent('y', ctrl: true, cmd: true, shift: true, alt: true, target: elementA))
+        expect(events).toEqual ['y-keydown']
         keymapManager.handleKeyboardEvent(buildKeyupEvent('ctrl', target: elementA))
         advanceClock(keymapManager.getPartialMatchTimeout())
-        expect(events).toEqual ['y-up-ctrl-keyup']
+        expect(events).toEqual ['y-keydown', 'y-up-ctrl-keyup']
+
+      it "dispatches the command multiple times when keydown is pressed", ->
+        keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
+        expect(events).toEqual ['y-keydown']
+        keymapManager.handleKeyboardEvent(buildKeyupEvent('y', ctrl: true, target: elementA))
+        expect(events).toEqual ['y-keydown']
+        keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
+        expect(events).toEqual ['y-keydown', 'y-keydown']
+        advanceClock(keymapManager.getPartialMatchTimeout())
+        expect(events).toEqual ['y-keydown', 'y-keydown']

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

       it "dispatches the command when extra user-generated keyup events are not specified in the binding", ->
         keymapManager.handleKeyboardEvent(buildKeydownEvent('x', ctrl: true, target: elementA))
@@ -383,7 +402,9 @@

       it "does _not_ dispatch the command when extra user-generated keydown events are not specified in the binding", ->
         keymapManager.handleKeyboardEvent(buildKeydownEvent('y', ctrl: true, target: elementA))
+        expect(events).toEqual ['y-keydown']
         keymapManager.handleKeyboardEvent(buildKeydownEvent('z', ctrl: true, target: elementA)) # not specified in binding
+        expect(events).toEqual ['y-keydown']
         keymapManager.handleKeyboardEvent(buildKeyupEvent('ctrl', target: elementA))
         advanceClock(keymapManager.getPartialMatchTimeout())
         expect(events).toEqual ['y-keydown']
@@ -406,6 +427,15 @@
         keymapManager.handleKeyboardEvent(buildKeyupEvent('c', target: elementA))
         advanceClock(keymapManager.getPartialMatchTimeout())
         expect(events).toEqual ['abc-secret-code']
+
+      it "dispatches the command when multiple keyup keystrokes are specified", ->
+        keymapManager.handleKeyboardEvent(buildKeydownEvent('a', target: elementA))
+        keymapManager.handleKeyboardEvent(buildKeyupEvent('a', target: elementA))
+        advanceClock(keymapManager.getPartialMatchTimeout())
+        advanceClock(keymapManager.getPartialMatchTimeout())
+        advanceClock(keymapManager.getPartialMatchTimeout())
+        advanceClock(keymapManager.getPartialMatchTimeout())
+        expect(events).toEqual []

     it "only counts entire keystrokes when checking for partial matches", ->
       element = $$ -> @div class: 'a'


Add a failing test

Add a failing test

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -722,6 +722,10 @@
         currentKeymap = null
         stub(KeyboardLayout, 'getCurrentKeymap', -> currentKeymap)

+      it "correctly resolves to AltGraph to ctrl-alt when key is AltGraph on Windows", ->
+        mockProcessPlatform('win32')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'AltGraph', code: 'AltRight', ctrlKey: true, altKey: true})), 'ctrl-alt')
+
       it "allows ASCII characters (<= 127) to be typed via an option modifier on macOS", ->
         mockProcessPlatform('darwin')


Add some keyup events to the multi stroke spec

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -225,9 +225,13 @@
       describe "when subsequent keystrokes yield an exact match", ->
         it "dispatches the command associated with the matched multi-keystroke binding", ->
           keymapManager.handleKeyboardEvent(buildKeydownEvent('v', target: editor))
+          keymapManager.handleKeyboardEvent(buildKeyupEvent('v', target: editor))
           keymapManager.handleKeyboardEvent(buildKeydownEvent('i', target: editor))
+          keymapManager.handleKeyboardEvent(buildKeyupEvent('i', target: editor))
           keymapManager.handleKeyboardEvent(buildKeydownEvent('v', target: editor))
+          keymapManager.handleKeyboardEvent(buildKeyupEvent('v', target: editor))
           keymapManager.handleKeyboardEvent(buildKeydownEvent('a', target: editor))
+          keymapManager.handleKeyboardEvent(buildKeyupEvent('a', target: editor))
           expect(events).toEqual ['viva!']

       describe "when subsequent keystrokes yield no matches", ->


Test that destroyed panes aren't included in `PaneContainer::getPanes()`

--- spec/pane-container-spec.coffee
+++ spec/pane-container-spec.coffee
@@ -255,7 +255,9 @@
     it "invokes the given callback when panes are destroyed", ->
       container = new PaneContainer(params)
       events = []
-      container.onDidDestroyPane (event) -> events.push(event)
+      container.onDidDestroyPane (event) ->
+        expect(event.pane in container.getPanes()).toBe false
+        events.push(event)

       pane1 = container.getActivePane()
       pane2 = pane1.splitRight()
@@ -265,6 +267,21 @@
       pane3.destroy()

       expect(events).toEqual [{pane: pane2}, {pane: pane3}]
+
+    it "invokes the given callback when the container is destroyed", ->
+      container = new PaneContainer(params)
+      events = []
+      container.onDidDestroyPane (event) ->
+        expect(event.pane in container.getPanes()).toBe false
+        events.push(event)
+
+      pane1 = container.getActivePane()
+      pane2 = pane1.splitRight()
+      pane3 = pane2.splitDown()
+
+      container.destroy()
+
+      expect(events).toEqual [{pane: pane1}, {pane: pane2}, {pane: pane3}]

   describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", ->
     it "invokes the given callbacks when an item will be destroyed on any pane", ->


Add missing subscription event

Add missing subscription event

--- src/application-delegate.coffee
+++ src/application-delegate.coffee
@@ -182,6 +182,9 @@
     new Disposable ->
       ipcRenderer.removeListener('message', outerCallback)

+  onDidBeginDownloadingUpdate: (callback) ->
+    @onUpdateAvailable(callback)
+
   onDidBeginCheckingForUpdate: (callback) ->
     outerCallback = (message, detail) ->
       if message is 'checking-for-update'


IE drag & drop fix

IE drag & drop fix

--- app/assets/javascripts/ckeditor/plugins/effective_menus/plugin.js.coffee
+++ app/assets/javascripts/ckeditor/plugins/effective_menus/plugin.js.coffee
@@ -95,7 +95,7 @@
         @menu.children('.actions').children('.add-item').hide()
         item.removeClass('open').find('.open').removeClass('open')

-        event.originalEvent.dataTransfer.setData('text/html', item[0].outerHTML)
+        event.originalEvent.dataTransfer.setData('Text', item[0].outerHTML)

         item.css('opacity', '0.4') # Show it slightly removed from the DOM
         @menu.addClass('dragging')
@@ -133,7 +133,7 @@
         # Don't allow to drop into myself or my own children
         return false if !@draggable? || @draggable.is(item) || @draggable.find(item).length > 0

-        new_item = $(event.originalEvent.dataTransfer.getData('text/html'))
+        new_item = $(event.originalEvent.dataTransfer.getData('Text'))

         item.before(new_item)
         @draggable.remove()


tidy (rm comment)

tidy (rm comment)

--- app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee
+++ app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee
@@ -1,8 +1,3 @@
 Darkswarm.controller "GroupsCtrl", ($scope, Groups, $anchorScroll, $rootScope) ->
   $scope.Groups = Groups
   $scope.order = 'position'
-
-  #$rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) ->
-    #$anchorScroll()
-    #
-      #


[skip] Removed special sorting from Estimate Effort view (1m)

--- app/assets/javascripts/houston-scheduler/views/_edit_tickets_view.coffee
+++ app/assets/javascripts/houston-scheduler/views/_edit_tickets_view.coffee
@@ -35,19 +35,13 @@
     @$el.delegate 'input', 'blur', -> $(@).closest('.ticket').removeClass('focus')

     $list = $('#tickets').empty()
-    @tickets
-      .sortBy (ticket)=>
-        (+ticket.get('estimatedEffort') > 0) * 99999999 + # put tickets _with_ estimates at the bottom,
-        (+!!ticket.get('unableToSetEstimatedEffort') > 0) * 99999999 +
-        (+ticket.get('sequence') <= 0) * 9999999 +        # then tickets with no priority,
-         +ticket.get('sequence')                          # finally sort by priority
-      .each (ticket)=>
-        view = new Scheduler.EditTicketView
-          ticket: ticket
-          template: @ticketTemplate
-          isValid: _.bind(@isValid, @)
-          attribute: @attribute
-        $list.appendView(view)
+    @tickets.each (ticket)=>
+      view = new Scheduler.EditTicketView
+        ticket: ticket
+        template: @ticketTemplate
+        isValid: _.bind(@isValid, @)
+        attribute: @attribute
+      $list.appendView(view)

     @$el.loadTicketDetailsOnClick()


Update join state in RoomUserState

Update join state in RoomUserState

--- assets/javascripts/controllers/room_user_state_item_controller.js.coffee
+++ assets/javascripts/controllers/room_user_state_item_controller.js.coffee
@@ -4,14 +4,11 @@

   actions:
     join: ->
-      console.log("hi", arguments)
       room_item_state = @get("model")
-      console.log room_item_state.get("joined")
       room_item_state.set("joined", true)
-      console.log room_item_state.get("joined")

-      # successCallback  = =>
-      #   console.log("deleted")
-      # errorCallback = =>
-      #   console.log("error whatever...")
-      # # user.save().then(successCallback, errorCallback)
+      successCallback  = =>
+        console.log("saved")
+      errorCallback = =>
+        console.log("error whatever...")
+      room_item_state.save().then(successCallback, errorCallback)


client: spelling fix

client: spelling fix

--- client/app/Applications/Account.kdapplication/views/referrermodal.coffee
+++ client/app/Applications/Account.kdapplication/views/referrermodal.coffee
@@ -55,7 +55,7 @@
     shareLinks.addSubView new LinkedInShareLink url: options.url

     rightColumn.addSubView gmail = new KDButtonView
-      title    : "Invite Gmail Contacs"
+      title    : "Invite Gmail Contacts"
       style    : "invite-button gmail"
       icon     : yes
       callback : @bound "checkGoogleLinkStatus"


Fix application page link for now

Fix application page link for now

--- client/app/Applications/Apps.kdapplication/Views/AppsListItemView.coffee
+++ client/app/Applications/Apps.kdapplication/Views/AppsListItemView.coffee
@@ -34,33 +34,6 @@
     @setTemplate @pistachio()
     @template.update()

-  #   if @getData().installedA
-  #     @alreadyInstalledText()
-  #   else
-  #     @createInstallButton()
-
-  # createInstallButton:->
-  #   app = @getData()
-
-  #   @installButton.destroy() if @installButton?
-  #   @installButton = new KDButtonView
-  #     title : "Install"
-  #     icon  : no
-  #     callback: =>
-  #       list = @getDelegate()
-  #       list.propagateEvent KDEventType : "AppWantsToExpand", app
-
-  #   @addSubView @installButton, '.button-container'
-
-  # alreadyInstalledText:->
-
-  #   @installButton.destroy() if @installButton?
-  #   @installButton = new KDButtonView
-  #     title     : "Installed"
-  #     icon      : no
-  #     disabled  : yes
-  #   @addSubView @installButton, '.button-container'
-
   pistachio:->
     """

@@ -82,6 +55,6 @@

{a[href="#"]{#(title)}}

{{@utils.shortenText #(body)}}

-      Application Page →
+      Application Page →

     """


Also close the channel when leave requested for conversation

--- client/app/Applications/Chat.kdapplication/Views/conversationmenu.coffee
+++ client/app/Applications/Chat.kdapplication/Views/conversationmenu.coffee
@@ -37,7 +37,8 @@
               'Leave'      :
                 style      : "modal-clean-red"
                 callback   : =>
-                  {conversation} = @getData()
+                  {conversation, chatChannel} = @getData()
+                  chatChannel?.close()?.off()
                   conversation.leave (err)=>
                     warn err  if err
                     @emit 'DestroyConversation'