fixes #207

fixes #207

--- app/assets/javascripts/libs/core_ext.coffee
+++ app/assets/javascripts/libs/core_ext.coffee
@@ -6,7 +6,7 @@
 ###

 # Applies an affine transformation matrix on an array of points.
-M4x4.transformPointsAffine = (m, points, r = new MJS_FLOAT_ARRAY_TYPE(points.length)) ->
+M4x4.transformPointsAffine = (m, points, r = new Float32Array(points.length)) ->

   m00 = m[0]
   m01 = m[1]
@@ -34,7 +34,7 @@

 # Applies a transformation matrix on an array of points.
-M4x4.transformPoints = (m, points, r = new MJS_FLOAT_ARRAY_TYPE(points.length)) ->
+M4x4.transformPoints = (m, points, r = new Float32Array(points.length)) ->

   for i in [0...points.length] by 3
     v0 = points[i]
@@ -54,7 +54,7 @@
   r

-M4x4.inverse = (mat, dest = new MJS_FLOAT_ARRAY_TYPE(16)) ->
+M4x4.inverse = (mat, dest = new Float32Array(16)) ->

   # cache matrix values
   a00 = mat[0]
@@ -110,7 +110,7 @@
   return dest

-M4x4.extractTranslation = (m, r = new MJS_FLOAT_ARRAY_TYPE(3)) ->
+M4x4.extractTranslation = (m, r = new Float32Array(3)) ->

   r[0] = m[12]
   r[1] = m[13]
@@ -118,7 +118,7 @@
   r

-V3.round = (v, r = new MJS_FLOAT_ARRAY_TYPE(3)) ->
+V3.round = (v, r = new Float32Array(3)) ->

   r[0] = Math.round(v[0])
   r[1] = Math.round(v[1])


LMS-721: Upload files on course file manager are not working

--- app/coffeescripts/bundles/full_files.coffee
+++ app/coffeescripts/bundles/full_files.coffee
@@ -1,4 +1,4 @@
-require ['jquery', 'full_files', 'jquery.google-analytics'], ($) ->
+require ['jquery', 'full_files', 'jquery.google-analytics', 'uploadify'], ($) ->

   $ ->
     $('.manage_collaborations').on 'click', ->


remove uneeded html escaping

remove uneeded html escaping

fixes CNVS-31681

Test Plan:
- Create a discussion
- Reply to discussion
- Either inspect the time label on the reply
  or paste $('.discussion-pubdate .screenreader-only')
  into console
- notice time looks fine

Change-Id: Icd55d13c50ca2250fb79d0e0d5ae70219a89e743
Reviewed-on: https://gerrit.instructure.com/90472
Product-Review: Aaron Cannon <25919c673621c006b735dcd5a28df63325948e2b@instructure.com>
Tested-by: Jenkins
Reviewed-by: Joel Hough <4816dabf8db1bc6cac35b3a24cab2ff844b5b0c7@instructure.com>
QA-Review: David Tan <6283e310a2278fc51bdc4fb5974f70dd9c3366b9@instructure.com>

--- app/coffeescripts/handlebars_helpers.coffee
+++ app/coffeescripts/handlebars_helpers.coffee
@@ -76,16 +76,16 @@
       return unless datetime?
       datetime = tz.parse(datetime) unless _.isDate datetime
       fudged = $.fudgeDateForProfileTimezone(tz.parse(datetime))
-      timeTitle = ""
+      timeTitleHtml = ""
       if contextSensitive and ENV and ENV.CONTEXT_TIMEZONE
-        timeTitle = Handlebars.helpers.contextSensitiveDatetimeTitle(datetime, hash: {justText: true})
-      else
-        timeTitle = htmlEscape $.datetimeString(datetime)
+        timeTitleHtml = Handlebars.helpers.contextSensitiveDatetimeTitle(datetime, hash: {justText: true})
+      else
+        timeTitleHtml = $.datetimeString(datetime)

       new Handlebars.SafeString """
-        <time data-tooltip data-html-tooltip-title='#{htmlEscape timeTitle}' datetime='#{datetime.toISOString()}' #{$.raw('pubdate' if pubdate)}>
+        <time data-tooltip data-html-tooltip-title='#{htmlEscape timeTitleHtml}' datetime='#{datetime.toISOString()}' #{$.raw('pubdate' if pubdate)}>
           #{$.friendlyDatetime(fudged)}
-          #{htmlEscape timeTitle}
+          #{timeTitleHtml}
         </time>
       """


Enlarge dropzones in sidebar

Enlarge dropzones in sidebar

--- scripts/views/workspace/sidebar/toc-branch.coffee
+++ scripts/views/workspace/sidebar/toc-branch.coffee
@@ -25,7 +25,7 @@
         @$el.removeClass('editor-node-expanded')

       # Add DnD options to content
-      enableContentDragging(@model, @$el.children().children('*[data-media-type]'))
+      enableContentDragging(@model, @$el.children().children('*[data-media-type]').parent())

       return result


大小写冲突

大小写冲突

--- lib/QDT-Builder/builder-view.coffee
+++ lib/QDT-Builder/builder-view.coffee
@@ -43,13 +43,13 @@

   createProject: (options) ->
     info = options.projectInfo
-    Util.createDir info.appPath, (err) =>
+    util.createDir info.appPath, (err) =>
       if err
         console.error err
         alert "应用创建失败#{':权限不足' if err.code is 'EACCES'}"
       else
         appConfigPath = pathM.join info.appPath,desc.projectConfigFileName
-        appConfig = Util.formatAppConfigToObj(info)
+        appConfig = util.formatAppConfigToObj(info)
         util.createModule options, (err) =>
           return console.error err if err?
           console.log 'success'
@@ -57,12 +57,12 @@
           moduleId = moduleInfo.identifier
           appConfig.mainModule = moduleId
           appConfig.modules[moduleId] = desc.minVersion
-          Util.writeJson appConfigPath, appConfig, (err) =>
+          util.writeJson appConfigPath, appConfig, (err) =>
             throw err if err
             atom.workspace.open appConfigPath
             aft = =>
-              Util.rumAtomCommand('tree-view:reveal-active-file')
+              util.rumAtomCommand('tree-view:reveal-active-file')
             _.debounce(aft,300)
           alert desc.createAppSuccess
           atom.project.addPath(info.appPath)
-          Util.rumAtomCommand 'tree-view:toggle' if $('.tree-view-resizer').length is 0
+          util.rumAtomCommand 'tree-view:toggle' if $('.tree-view-resizer').length is 0


Added ui element for hiding a group.

Added ui element for hiding a group.

--- picoCTF-web/web/coffee/classroom-management.coffee
+++ picoCTF-web/web/coffee/classroom-management.coffee
@@ -122,6 +122,7 @@
     name: ""
     settings:
       email_filter: []
+      hidden: false
     member_information: []
     teacher_information: []
     current_user: {}
@@ -171,8 +172,41 @@
           memberInformation={@state.member_information} gid={@props.gid} refresh={@refreshSettings}/>
       </Col>
       <Col xs={6}>
+        <GroupOptions pushUpdates={@pushUpdates} settings={@state.settings} gid={@props.gid}/>
         <GroupEmailWhitelist emails={@state.settings.email_filter} pushUpdates={@pushUpdates} gid={@props.gid}/>
       </Col>
+

+
+GroupOptions = React.createClass
+  propTypes:
+    settings: React.PropTypes.object.isRequired
+    pushUpdates: React.PropTypes.func.isRequired
+    gid: React.PropTypes.string.isRequired
+
+  promptGroupHide: ->
+    window.confirmDialog "Hiding your group from the scoreboard is an irrevocable change. You won't be able to change this later.", "Hidden Group Change",
+    "Okay", "Cancel", (() ->
+      @props.pushUpdates ((data) -> update data, {hidden: {$set: true}})
+    ).bind this, () -> false
+
+  render: ->
+    if @props.settings.hidden
+      hiddenGroupDisplay =
+
This group is hidden from the general scoreboard.

+    else
+      hiddenGroupDisplay =
+

+          This group is visible on the scoreboard.
+          Click here to hide it.
+

+
+

+
Group Options

+      <Panel>
+        <form>
+          {hiddenGroupDisplay}
+        </form>
+      </Panel>

 EmailWhitelistItem = React.createClass
@@ -232,14 +266,12 @@

Email Domain Whitelist

-      <form onSubmit={@addEmailDomain}>
-        <Row>
-          <Input type="text" addonBefore="@ Domain" valueLink={@linkState "emailDomain"}/>
-        </Row>
-        <Row>
-          {if @props.emails.length > 0 then @createItemDisplay() else emptyItemDisplay}
-        </Row>
-      </form>
+        <Panel>
+          <form onSubmit={@addEmailDomain}>
+            <Input type="text" addonBefore="@ Domain" valueLink={@linkState "emailDomain"}/>
+            {if @props.emails.length > 0 then @createItemDisplay() else emptyItemDisplay}
+          </form>
+        </Panel>

 TeacherManagement = React.createClass


Fix ThreadListParticipants spec

Fix ThreadListParticipants spec

--- internal_packages/thread-list/spec/thread-list-participants-spec.cjsx
+++ internal_packages/thread-list/spec/thread-list-participants-spec.cjsx
@@ -6,6 +6,9 @@
 ThreadListParticipants = require '../lib/thread-list-participants'

 describe "ThreadListParticipants", ->
+
+  beforeEach ->
+    @account = AccountStore.accounts()[0]

   it "renders into the document", ->
     @participants = ReactTestUtils.renderIntoDocument(
@@ -181,7 +184,7 @@

     describe "when getTokens() called and current user is only sender", ->
       beforeEach ->
-        @me = AccountStore.current().me()
+        @me = @account.me()
         @ben = new Contact(email: 'ben@nylas.com', name: 'ben')
         @evan = new Contact(email: 'evan@nylas.com', name: 'evan')
         @evanCapitalized = new Contact(email: 'EVAN@nylas.com', name: 'evan')
@@ -263,7 +266,7 @@

     describe "when thread.messages is not available", ->
       it "correctly produces items for display in a wide range of scenarios", ->
-        me = AccountStore.current().me()
+        me = @account.me()
         scenarios = [{
           name: 'one participant'
           in: [@ben]


fix context menu

fix context menu

--- menus/open-in-browsers.cson
+++ menus/open-in-browsers.cson
@@ -6,12 +6,12 @@
         {
           label: 'Open in BrowserPlus',
           submenu: [
-            {label: 'Open in BrowserPlus', command:  'open-in-browsers:bp'}
-            {label: 'Open in Safari', command:  'open-in-browsers:safari'}
-            {label: 'Open in Opera', command:  'open-in-browsers:opera'}
-            {label: 'Open in FireFox', command:  'open-in-browsers:ff'}
-            {label: 'Open in Chrome', command:  'open-in-browsers:chrome'}
-            {label: 'Open in IE', command:  'open-in-browsers:ie'}
+            {label: 'Open in BrowserPlus', command:  'open-in-browsers:BP'}
+            {label: 'Open in Safari', command:  'open-in-browsers:Safari'}
+            {label: 'Open in Opera', command:  'open-in-browsers:Opera'}
+            {label: 'Open in FireFox', command:  'open-in-browsers:FF'}
+            {label: 'Open in Chrome', command:  'open-in-browsers:Chrome'}
+            {label: 'Open in IE', command:  'open-in-browsers:IE'}
           ]
         }
       ]
@@ -21,12 +21,12 @@
         {
           label: 'Open in BrowserPlus',
           submenu: [
-            {label: 'Open in BrowserPlus', command:  'open-in-browsers:bp'}
-            {label: 'Open in Safari', command:  'open-in-browsers:safari'}
-            {label: 'Open in Opera', command:  'open-in-browsers:opera'}
-            {label: 'Open in FireFox', command:  'open-in-browsers:ff'}
-            {label: 'Open in Chrome', command:  'open-in-browsers:chrome'}
-            {label: 'Open in IE', command:  'open-in-browsers:ie'}
+            {label: 'Open in BrowserPlus', command:  'open-in-browsers:BP'}
+            {label: 'Open in Safari', command:  'open-in-browsers:Safari'}
+            {label: 'Open in Opera', command:  'open-in-browsers:Opera'}
+            {label: 'Open in FireFox', command:  'open-in-browsers:FF'}
+            {label: 'Open in Chrome', command:  'open-in-browsers:Chrome'}
+            {label: 'Open in IE', command:  'open-in-browsers:IE'}
           ]
         }
       ]


Comments

Comments

--- cosmo.coffee
+++ cosmo.coffee
@@ -1,7 +1,8 @@
 window.Cosmo =
   version: '0.1.0'

-# FOOBAR
+# comment
+# another
 # All apps should be an extended instance of this class
 class Cosmo.Router
   # app regions, assumes main content goes in a div#container


Direction test

Direction test

--- test/studio/LayerDraggableDirection.framer/app.coffee
+++ test/studio/LayerDraggableDirection.framer/app.coffee
@@ -1,2 +1,8 @@
 layer = new Layer
-layer.draggable.enabled = true+layer.center()
+
+layer.draggable.enabled = true
+layer.draggable.constraints = {x:0, y:0, width:Screen.width, height:Screen.height}
+
+layer.on Events.DragMove, ->
+	print layer.draggable.direction


[rename] WeatherMoodCollection

[rename] WeatherMoodCollection

--- app/scripts/collections/weather_mood.coffee
+++ app/scripts/collections/weather_mood.coffee
@@ -3,6 +3,6 @@
   'models/weather_mood_item'
 ], (BaseCollection, WeatherMoodItem) ->

-  class WeatherMood extends BaseCollection
+  class WeatherMoodCollection extends BaseCollection

     model: WeatherMoodItem


typos fixed

typos fixed

--- src/sphere.coffee
+++ src/sphere.coffee
@@ -10,7 +10,7 @@
   # @var {Number} tilMin tilt arc start point in radians
   # @var {Number} tilMax tilt arc end point in radians
   # @var {Number} panArc panMin to panMax length in radians
-  # @var {Number} tiltArc tilMin to tiltMax length in radians
+  # @var {Number} tiltArc panMin to panMax length in radians

   ###*
   # Constructor-Description
@@ -28,11 +28,11 @@
     # compute horizontal circumference
     @panArc = @panMax - @panMin
     # computrtical circumference
-    @tiltArc iltMax - @tiltMin
-    # count picture-rows  @countRows = Math.ceil(@tiltArc/@pictureSizeght)
+    @tiltArc = @tiltMax - @tiltMin
+    # count picture-rows  @countRows = Math.ceil(@tiltArc/@pictureSize.height)
     # compute delta  @deltaTilt = @tiltArc/@countRows
     # create rows
-@rows = []
+    @rows = []
     for i in [0..@countRows]
       tilt = @tiltMin + i*@deltaTilt
       # create SphereRow


sprite

sprite

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -86,10 +86,8 @@

   @name load
   @methodOf Sprite
-
   @param {String} url
   @param {Function} [loadedCallback]
-
   @returns {Sprite} A sprite object
   ###
   Sprite.load = (url, loadedCallback) ->
@@ -113,10 +111,8 @@

   @name fromPixieId
   @methodOf Sprite
-
   @param {Number} id Pixie Id of the sprite to load
   @param {Function} [callback] Function to execute once the image is loaded. The sprite proxy data is passed to this as a parameter.
-
   @returns {Sprite}
   ###
   Sprite.fromPixieId = (id, callback) ->
@@ -145,10 +141,8 @@

   @name fromURL
   @methodOf Sprite
-
   @param {String} url The url where the image to load is located
   @param {Function} [callback] Function to execute once the image is loaded. The sprite proxy data is passed to this as a parameter.
-
   @returns {Sprite}
   ###
   Sprite.fromURL = Sprite.load
@@ -158,10 +152,8 @@

   @name loadByName
   @methodOf Sprite
-
   @param {String} name The name of the image in your images directory
   @param {Function} [callback] Function to execute once the image is loaded. The sprite proxy data is passed to this as a parameter.
-
   @returns {Sprite}
   ###
   Sprite.loadByName = (name, callback) ->


Increase post rollback window for unread threads

--- lineman/app/components/thread_page/activity_card/activity_card.coffee
+++ lineman/app/components/thread_page/activity_card/activity_card.coffee
@@ -22,7 +22,7 @@

     $scope.initialLoadSequenceId = ->
       if $scope.discussion.isUnread()
-        $scope.discussion.lastReadSequenceId - 1
+        $scope.discussion.lastReadSequenceId - 5
       else
         $scope.discussion.lastSequenceId - $scope.pageSize + 1


Slight speedup

Slight speedup

Still slow.

--- resources/assets/coffee/react/profile-page/extra.coffee
+++ resources/assets/coffee/react/profile-page/extra.coffee
@@ -50,10 +50,11 @@
     for page in elements by -1
       continue unless page.getBoundingClientRect().top <= 0

-      @setState mode: page.getAttribute('id')
+      @setSingleState 'mode', page.getAttribute('id')
+
       return

-    @setState mode: page.getAttribute('id')
+    @setSingleState 'mode', page.getAttribute('id')

   _modeSwitch: (_e, mode) =>
@@ -67,7 +68,11 @@

   _tabsStick: (_e, target) =>
-    @setState tabsSticky: (target == 'profile-extra-tabs')
+    @setSingleState 'tabsSticky', (target == 'profile-extra-tabs')
+
+
+  setSingleState: (key, value) =>
+    @setState "#{key}": value if @state[key] != value

   render: =>


log out of order events now that the rate is lower

--- services/real-time/app/coffee/EventLogger.coffee
+++ services/real-time/app/coffee/EventLogger.coffee
@@ -35,12 +35,11 @@
 			return # order is ok
 		if (count == previous)
 			metrics.inc "event.#{channel}.duplicate"
-			if Math.random() < 0.01
-				logger.warn {channel:channel, message_id:message_id}, "duplicate event (sampled at 1%)"
+			logger.warn {channel:channel, message_id:message_id}, "duplicate event"
 			return "duplicate"
 		else
 			metrics.inc "event.#{channel}.out-of-order"
-			# logger.warn {channel:channel, message_id:message_id, key:key, previous: previous, count:count}, "out of order event"
+			logger.warn {channel:channel, message_id:message_id, key:key, previous: previous, count:count}, "out of order event"
 			return "out-of-order"

 	_storeEventCount: (key, count) ->


return done in smoke test

return done in smoke test

--- services/web/test/smoke/coffee/SmokeTests.coffee
+++ services/web/test/smoke/coffee/SmokeTests.coffee
@@ -20,8 +20,8 @@
 				if err? then done(err)
 				csrfMatches = stdout.match("<input name=\"_csrf\" type=\"hidden\" value=\"(.*?)\">")
 				if !csrfMatches?
-					logger.err stdout:stdout, "smoke test: does not hace csrf token"
-					return("smoke test: does not hace csrf token")
+					logger.err stdout:stdout, "smoke test: does not have csrf token"
+					return done("smoke test: does not have csrf token")
 				csrf = csrfMatches[1]

 				# Change cookie to be non secure so curl will send it


Popover coffeescript optimized.

Popover coffeescript optimized.

--- webapp/public/pbsfinder/javascripts/jobs/table_header.coffee
+++ webapp/public/pbsfinder/javascripts/jobs/table_header.coffee
@@ -4,7 +4,7 @@
   s = 8
   t = $('#protein-table')
   c = t.find('tr').size() - 1
-  show_popover = false
+  show_popover = null

   # Auxiliary methods.
   isFootableNeeded = () ->
@@ -26,12 +26,8 @@
       nextText: "Next →",
       lastText: "Last ⇒"
     }).bind({'footable_sorting': (e) ->
-      if show_popover
-        show_popover = false
-        $('[data-toggle="popover"]').each(() ->
-          if (!$(this).is(e.target) && $(this).has(e.target).length == 0 && $('.popover').has(e.target).length == 0)
-            $(this).popover('hide')
-    )
+      if(show_popover?)
+        $(show_popover).popover('hide')
     })

   # Page select change.
@@ -53,25 +49,21 @@
           content: cont.html()
         }).on("show.bs.popover", ->
           $(this).data("bs.popover").tip().css(maxWidth: "600px")
-          show_popover = true
+          if(show_popover?)
+            $(show_popover).popover('hide')
+          show_popover = this
         )

   # Disable multiple popovers.
   $('html').on('click', (e) ->
-    if show_popover == true
-      $('[data-toggle="popover"]').each(() ->
-        if (!$(this).is(e.target) && $(this).has(e.target).length == 0 && $('.popover').has(e.target).length == 0)
-          $(this).popover('hide')
-      )
+    if(show_popover? && !$(show_popover).is($(e.target)))
+      $(show_popover).popover('hide')
   )

   # Disable popovers on scroll.
   $('.table-container').scroll( ->
-    if show_popover == true
-      show_popover = false
-      $('[data-toggle="popover"]').each(() ->
-        $(this).popover('hide')
-      )
+    if(show_popover?)
+      $(show_popover).popover('hide')
   )

   # Enable header tooltips.


Editor: remove CMD+L key combo

Editor: remove CMD+L key combo

--- client/Ace/ace.coffee
+++ client/Ace/ace.coffee
@@ -104,6 +104,7 @@
       @addKeyCombo "preview", "Ctrl-Shift-P", => @getDelegate().preview()
       @addKeyCombo "fullscreen", "Ctrl-Enter", => @getDelegate().toggleFullscreen()
       @addKeyCombo "gotoLine", "Ctrl-G", @bound "showGotoLine"
+      @addKeyCombo "gotoLineL", "Ctrl-L", noop
       @addKeyCombo "saveAll", "Ctrl-Alt-S", @bound "saveAllFiles"
       @addKeyCombo "closeTab", "Ctrl-W", "Ctrl-W", @bound "closeTab"
       @addKeyCombo "settings", "Ctrl-,", noop # ace creates a settings view for this shortcut, overriding it.


JPermissionSet: fetchGroupAndPermissionSet: helper for inline usages

--- workers/social/lib/social/models/group/permissionset.coffee
+++ workers/social/lib/social/models/group/permissionset.coffee
@@ -69,6 +69,18 @@
     [{ permission, validateWith: require('./validators').any }]

   @checkPermission = (client, advanced, target, args, callback) ->
+
+  fetchGroupAndPermissionSet = (groupName, callback) ->
+
+    JGroup = require '../group'
+    JGroup.one { slug: groupName }, (err, group) ->
+      if err then callback err
+      else unless group?
+        callback new KodingError "Unknown group! #{groupName}"
+      else
+        group.fetchPermissionSetOrDefault (err, permissionSet) ->
+          if err then callback err
+          else callback null, { group, permissionSet }
     JGroup = require '../group'
     advanced = wrapPermission advanced  if 'string' is typeof advanced
     kallback = (group, permissionSet) ->


Checkbox to connect to spawned interpreter

Added checkbos to connect to a spawned interpreter if it's checked

--- client/connect.coffee
+++ client/connect.coffee
@@ -16,6 +16,10 @@
     else [x.host, x.port] = s.split ':' # IPv4 host:port or just host
     x.port = +(x.port or DEFAULT_PORT)
     x
+
+  sleep = (ms) ->
+    start = new Date().getTime()
+    continue while new Date().getTime() - start < ms

   ipAddresses = [] # of the proxy.  Used in the "Waiting for connections" dialogue.

@@ -56,7 +60,8 @@

Linux-only.  Requires a <tt>dyalog</tt> executable on your <tt>$PATH</tt>.

           Port <input id="spawn-port" class="text-field" value="#{DEFAULT_PORT}" size="5">
-          Spawn
+          Spawn

+          Connect on spawn <input type="checkbox" id="spawn-connect" checked>

       </fieldset>
@@ -75,6 +80,7 @@
     $spawn       = $ '#spawn'        ; $cancel      = $ '#fav-cancel'
     $spawnPort   = $ '#spawn-port'   ; $listen      = $ '#listen'
     $spawnStatus = $ '#spawn-status' ; $listenPort  = $ '#listen-port'
+    $spawnConn   = $ '#spawn-connect'
     $listenDialog = null

     $connect.add($new).add($delete).add($save).add($cancel).add($spawn).add($listen).button()
@@ -126,6 +132,9 @@
       else
         $spawnStatus.text 'Spawning...'; $spawn.button 'disable'; $spawnPort.attr 'disabled', true
         Dyalog.socket.emit '*spawn', {port}
+        if $spawnConn.is(':checked')
+          sleep(500)
+          Dyalog.socket.emit '*connect', host: '127.0.0.1', port: port or DEFAULT_PORT
       false
     $spawnPort.on 'keydown', null, 'return', -> $spawn.click(); false
     $listen.click ->


:art: If only I could spell

:art: If only I could spell

--- spec/path-scanner-spec.coffee
+++ spec/path-scanner-spec.coffee
@@ -62,7 +62,7 @@
           expect(paths.length).toBe 1
           expect(paths).toContain path.join(rootPath, 'dir.with.dots', 'parent-has-dots.txt')

-      it "returns nothing when a non-existant directory is passed in", ->
+      it "returns nothing when a non-existent directory is passed in", ->
         scanner = new PathScanner(rootPath, inclusions: ['thisdoesntexist'])
         scanner.on('path-found', pathHandler = createPathCollector())
         scanner.on('finished-scanning', finishedHandler = jasmine.createSpy())


Bigger spinner

Bigger spinner

--- client/loading.coffee
+++ client/loading.coffee
@@ -1,2 +1,6 @@
+Meteor.Spinner.options.length = 15
+Meteor.Spinner.options.radius = 10
+Meteor.Spinner.options.width = 5
+
 Template.registerHelper 'ready', ->
   Router.current().ready()


Fix copy/paste from Raw view (#276)

Fix copy/paste from Raw view (#276)

Parse &, <, > when stripping lone <pre> block

--- client/message.coffee
+++ client/message.coffee
@@ -615,8 +615,15 @@
               .replace /(<li[^<>]*>)<p>/ig, '$1'
               .replace /<(li|ul|\/ul|br|p)\b/ig, '\n$&'
               .replace /"/ig, '"'
-              if match = /^\s*<pre[^<>]*>([^]*)<\/pre>\s*$/.exec paste
-                paste = match[1].split /\r\n?|\n/
+              if (match = /^\s*<pre[^<>]*>([^]*)<\/pre>\s*$/.exec paste) and
+                 0 > match[1].indexOf '<pre>'
+                ## Treat a single <pre> block (such as pasted from Raw view)
+                ## like a text paste, after parsing basic &chars;
+                paste = match[1]
+                .replace /</g, '<'
+                .replace />/g, '>'
+                .replace /&/g, '&'
+                .split /\r\n?|\n/
               else
                 paste = paste.split /\r\n?|\n/
                 ## Remove blank lines


Solve overly large padding at the bottom, for #507

--- src/resources/forms/orbeon/builder/resources/cell/control-resources-editor/actions.coffee
+++ src/resources/forms/orbeon/builder/resources/cell/control-resources-editor/actions.coffee
@@ -79,6 +79,7 @@
         mceConfig = _.clone(YAHOO.xbl.fr.Tinymce.DefaultConfig)
         mceConfig.plugins += ',autoresize'
         mceConfig.autoresize_min_height = 100
+        mceConfig.autoresize_bottom_margin = 16 # Default padding for autoresize adds too much empty space at the bottom

         tinymceObject = new window.tinymce.Editor(tinymceAnchor.attr('id'), mceConfig)
         tinymceObject.render()


Add new webinars

Add new webinars

--- app/views/courses/TeacherClassesView.coffee
+++ app/views/courses/TeacherClassesView.coffee
@@ -23,9 +23,10 @@

 # TODO: if this proves useful, make a simple admin page with a Treema for editing office hours in db
 officeHours = [
-  {time: moment('2018-02-15 12:30-08').toDate(), link: 'https://zoom.us/meeting/register/4eb53ae71a6813fd34538d7d4481ef37', host: 'Sean', name: 'CodeCombat: Moving from Block-Based to Text-Based Coding'}
-  {time: moment('2018-02-20 12:30-08').toDate(), link: 'https://zoom.us/meeting/register/b8fc258742ce6947cde7dc3c8da9331e', host: 'Liz', name: 'CodeCombat for Students New to Coding'}
   {time: moment('2018-02-28 12:00-08').toDate(), link: 'https://zoom.us/meeting/register/307c335ddb1ee6ef7510d14dfea9e911', host: 'David', name: 'CodeCombat for Beginner Teachers'}
+  {time: moment('2018-03-07 12:00-08').toDate(), link: 'https://zoom.us/meeting/register/a1a6f5f4eb7a0a387c24e00bf0acd2b8', host: 'Nolan', name: 'CodeCombat: Beyond Block-Based Coding'}
+  {time: moment('2018-03-15 12:30-08').toDate(), link: 'https://zoom.us/meeting/register/16f0a6b4122087667c24e00bf0acd2b8', host: 'Sean', name: 'Building Student Engagement with CodeCombat'}
+  {time: moment('2018-03-21 12:00-08').toDate(), link: 'https://zoom.us/meeting/register/4e7eb093f8689e21c5b9141539e44ee6', host: 'Liz', name: 'CodeCombat for Beginner Teachers'}
 ]

 module.exports = class TeacherClassesView extends RootView


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -19,7 +19,7 @@
   player.update()

   # x, y position is based on the center point so
-  # the player's x,
+  # the player's x, y
   player.I.x
   # => 2.5
 </pre></code>


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -17,7 +17,7 @@
   # his position back inside the viewport.
   player.update()

-  player
+  player.I.


Tweak messages.

Tweak messages.

--- src/coffee/quine.coffee
+++ src/coffee/quine.coffee
@@ -2,10 +2,13 @@
   if window.mozillaSaveFile
     mozillaSaveFile path, contents
     inform "Saved to #{path}"
+  else if window.localStorage isnt window.localData
+      error 'No localStorage. Unable to save changes!', 'no-local-storage'
   else
     localData.value = contents
     warn 'Cannot write to local file system.
-          Saved to browser local storage instead.'
+          Saved to browser local storage instead.

+          Changes will not be persisted outside this browser.'

 constructHelpText = () ->
     msg = ''
@@ -17,10 +20,10 @@
     if location.protocol isnt 'file:'
       msg += '
Open from your local computer'

-    if msg then warn "This page may not work well in this browser.
-                      (Did you know this file can save itself?)

-                      To ensure full functionality:

-
#{msg}
"
+    if msg then inform "Features may be missing or broken in this browser.

+                        (Did you know this file can save itself?)
+                        To ensure full functionality:

+
#{msg}
"

 adjustSize = () ->
   codemirror?.setSize null, body.offsetHeight - noticeContainer.offsetHeight
@@ -99,10 +102,11 @@
     window.localData = window.localStorage
 else
     window.localData = {}
-    error 'No localStorage. All changes will be lost!'

 loadFileReady = () ->
   constructHelpText()
+  if localData isnt window.localStorage
+    warn 'No localStorage. All changes will be lost!', 'no-local-storage'

   value = ''
   if window.mozillaLoadFile
@@ -116,7 +120,9 @@
       value = window.mozillaLoadFile path
   else if localData.value
     warn 'Cannot read from local file system.
-          Loaded from browser local storage instead.'
+          Loaded from browser local storage instead.

+          The editor contents may differ from
+          the actual file contents.'
     value = localData.value
     window.documentWritten ?= false
     if not window.documentWritten
@@ -125,7 +131,9 @@
       return
   else
     warn 'Cannot read from local file system.
-          Loaded from browser DOM instead.'
+          Loaded from browser DOM instead.

+          The editor contents may slightly differ
+          from the actual file contents.'
     value = document.documentElement.outerHTML
     value = value.replace /^.*/, ''  # Remove auto-inserted tags


small bugfix

small bugfix

--- src/coffee/stats.coffee
+++ src/coffee/stats.coffee
@@ -321,8 +321,10 @@
           subj.onCompleted()
       .fail (error) ->
         if error instanceof ErrorStatusCode and (error.code is 404)
-          subj.onNext null
-          subj.onCompleted()
+          (promise or Q())
+          .then ->
+            subj.onNext null
+            subj.onCompleted()
         else
           subj.onError error
       .done()


fix: test case

fix: test case

--- tests/integration/components/file-details-test.coffee
+++ tests/integration/components/file-details-test.coffee
@@ -14,4 +14,4 @@
   @render hbs """{{file-details}}"""

-  assert.equal @$().text().trim(), 'High: Medium: Low: Passed: Unknown: Package: Version: PDF Password: PDF ReportManual'
+  assert.equal @$().text().trim(), 'High: Medium: Low: Passed: Unknown: Package: Version: PDF Password:   PDF ReportManual'


Show compile-time errors as program output

--- coffee/compile.coffee
+++ coffee/compile.coffee
@@ -1,9 +1,15 @@
 compile = (code) ->
   try
     sexp = Opal.Opal.Parser.$new().$parse(code)
-    return Opal.Object.__proto__.$statements_to_pos_to_result(sexp)
-  catch e
-    console.error e.stack
-    throw e
+    hash = Opal.Object.__proto__.$statements_to_pos_to_result(sexp)
+  catch ruby_exception
+    hash =
+      map:
+        start: 1
+        1:
+          map:
+            output: "#{ruby_exception.name}: #{ruby_exception.message}"
+            next: 'finish'
+  hash

 module.exports = { compile: compile }


Encapsulates key binding for suraido slider on a function

--- coffee/suraido.coffee
+++ coffee/suraido.coffee
@@ -10,7 +10,7 @@
     init: 0
     pause: false
     loop: false
-    keys: false
+    enableKeys: false
     dots: false
     arrows: false
     prev: '<<'
@@ -85,16 +85,8 @@
                 event.type is 'mouseout' && @play()
         , @options.init | 0)

-      #keypresses
-      if @options.keys
-        $(document).keydown (event) =>
-          key = event.which
-          if key is 37
-            @prev()
-          else if key is 39
-            @next()
-          else if key is 27
-            @stop()
+      #keypresses binding
+      @_enableBindKeys() if @options.enableKeys

       @options.dots and @_createPagination('dot')
       @options.arrows and @_createPagination('arrow')
@@ -117,6 +109,13 @@
       if $.event.special['swipe'] or $.Event 'swipe'
         @$el.on 'swipeleft swiperight swipeLeft swipeRight', (e) =>
          if e.type.toLowerCase() is 'swipeleft' then @next() else @prev()
+
+    _enableBindKeys: =>
+      $(document).on 'keydown', (event) =>
+        switch event.which
+          when 37 then @prev()
+          when 39 then @next()
+          when 27 || 32 then @stop()

     _createPagination: (name, html) =>
       if name is 'dot'


change basedir option handling for local packages and cleanup rest of its implementation

--- plugin/browserify.coffee
+++ plugin/browserify.coffee
@@ -34,27 +34,45 @@

 # helps getNpmDir by searching even deeper into the InputFile's properties
 getNpmDirForPackage = (isopackCache, name, basedirOption) ->
+
+  # get the deep property object for the named package
   pkg = isopackCache._packageMap._map[name]
-  if pkg.kind is 'local' then pkg.packageSource.npmCacheDirectory
-
-  else # it's a versioned package... doesn't have a packageSource object.
+
+  if pkg.kind is 'local'
+    # where the official Meteor npm storage is
+    npmCache = pkg.packageSource.npmCacheDirectory
+
+    # if we have a basedirOption then strip off the '.npm/package' part
+    # TODO: this assumes the basedir is 'packages/packageName' and the
+    # 'node_modules' is at the root of the packages.
+    # could do a lot of inspection into basedir to find node_modules in a
+    # subdirectory of the package. For now, require it at the root.
+    # NOTE: this makes sense, however, before I added this line it still worked
+    # with .npm/package on the end and the node_modules was in root of package. Weird.
+    if basedirOption? then npmCache[...-12] else npmCache
+
+  else # it's a published (versioned) package... so, no packageSource object.
+
+    # if user specified a basedir option...
+    # without any Npm.depends() in package.js there's no nodeModulesPath into
+    # the ./meteor/packages so I can't use that to get an absolute path to that area.
+    # there is a `_tropohouse.root` property with the equivalent of ~/.meteor
+    # so I'll use that.
+    if basedirOption?
+      # get `~/.meteor` from deep property
+      root = isopackCache._tropohouse.root
+
+      # package name uses a colon, file system uses an underscore
+      packageName = name.replace ':', '_'
+
+      # combine this all into a directory  # TODO: search web.cordova too?
+      return path.join root, 'packages', packageName, pkg.version, 'web.browser', basedirOption
+
+    # otherwise, let's search the builds for this package
     builds = isopackCache._isopacks[name].unibuilds

-    for build in builds when build.arch is 'os' # the 'os' one has nodeModulesPath
-      # if user specified a basedir option
-      if basedirOption?
-        # we need the home directory to find ~/.meteor/packages
-        home = process.env.HOME ? process.env.USERPROFILE
-        meteorPackages = home + '/.meteor/packages/'
-        # package name uses a colon, file system uses an underscore
-        packageName = name.replace ':', '_'
-        # the build info has the package's version
-        version = '/' + build.pkg.version
-        # combine this all into a directory and convert it to OS specific path
-        dir = Plugin.convertToOSPath meteorPackages + packageName + version + '/web.browser/' + basedirOption
-        # lastly, return this directory only if it has `node_modules` in it
-        return dir if fs.existsSync path.join dir, 'node_modules'
-      # if we didn't return above, then, return this:
+    # the 'os' one has nodeModulesPath
+    for build in builds when build.arch is 'os'
       return build.nodeModulesPath[...-12]

 getNpmDir = (file, basedirOption) ->
@@ -69,7 +87,9 @@
   else if isopackCache._packageMap._map?['npm-container']?
     getNpmDirForPackage isopackCache, 'npm-container'

-  else '.' # else, they'd better have `node_modules` in the root of the app
+  # else, they'd better have `node_modules` in the root of the app
+  # or, they set a basedir option pointing at its location in the app
+  else basedirOption ? '.' # TODO: check both for 'node_modules' ?

 # extend standard compiler class to make implementation easier.
@@ -217,17 +237,7 @@

   getBrowserifyOptions: (file, option) ->

-    # empty user options to fill from file, if it exists
-    userOptions = {}
-
-    if option?.input?
-      userOptions = JSON.parse option.input.getContentsAsString()
-
-    # check for `basedir` from options file or package.js file options
-    basedirOption = userOptions.basedir ? option?.package?.basedir
-
-    # sane defaults for options
-    # NOTE: no longer putting basedir right here
+    # sane defaults for options # NOTE: no longer putting basedir right here
     defaultOptions =

       # must be true to produce source map which we extract via exorcist and
@@ -240,6 +250,16 @@
         envify:
           NODE_ENV: if @getDebug() then 'development' else 'production'
           _:'purge'
+
+
+    # empty user options to fill from file, if it exists
+    userOptions = {}
+
+    if option?.input?
+      userOptions = JSON.parse option.input.getContentsAsString()
+
+    # check for `basedir` from options file and package.js file options
+    basedirOption = userOptions.basedir ? option?.package?.basedir

     # Browserify will look in `basedir` for npm modules
     # 1. place the result into `userOptions` so it's last in the hierarchy


Restore Atom windows to last state.

Restore Atom windows to last state.

--- .atom/config.cson
+++ .atom/config.cson
@@ -6,6 +6,8 @@
     disabledPackages: [
       "linter"
     ]
+    openEmptyEditorOnStart: false
+    restorePreviousWindowsOnStart: "always"
     telemetryConsent: "no"
     themes: [
       "zenburn-ui"


Update atom settings.

Update atom settings.

--- .atom/config.cson
+++ .atom/config.cson
@@ -43,13 +43,13 @@
       "native-ui"
       "solarized-light-syntax"
     ]
-    useCustomTitleBar: true
+    titleBar: "custom"
   "cursor-indicator":
     displayPrefix: "|"
   editor:
     autoIndentOnPaste: false
     fontFamily: "Inconsolata"
-    fontSize: 20
+    fontSize: 19
     invisibles:
       cr: "↩"
       eol: " "
@@ -190,7 +190,7 @@
 ".css.less.source":
   editor:
     softTabs: true
-    tabLength: 2
+    tabLength: 4
 ".dockerfile.source":
   editor:
     softTabs: true


atom: tablength for raw CSV files

atom: tablength for raw CSV files

--- .atom/config.cson
+++ .atom/config.cson
@@ -45,6 +45,12 @@
   "linter-python-pep257": {}
   latex:
     alwaysOpenResultInAtom: true
+".csv.semicolon.text":
+  editor:
+    tabLength: 8
+".csv.text":
+  editor:
+    tabLength: 8
 ".python.source":
   whitespace:
     removeTrailingWhitespace: true


provide a close button for the about dialog

--- vendor/assets/javascripts/mercury/statusbar.js.coffee
+++ vendor/assets/javascripts/mercury/statusbar.js.coffee
@@ -20,7 +20,7 @@
       @setPath(options.region.path()) if options.region && jQuery.type(options.region.path) == 'function'

     @aboutElement.click =>
-      Mercury.lightview('/mercury/lightviews/about.html', {title: "About Mercury Editor v#{Mercury.version}"})
+      Mercury.lightview('/mercury/lightviews/about.html', {title: "About Mercury Editor v#{Mercury.version}", closeButton: true})

   height: ->


NEW: Namespace.getVersions()

NEW: Namespace.getVersions()

--- src/Runtime/Neptune.coffee
+++ src/Runtime/Neptune.coffee
@@ -61,6 +61,13 @@
         namespace.getInspectedObjects includeModules

       modules: @getModuleNames().join ', ' if includeModules && @getModuleNames().length > 0
+
+  @getVersions: ->
+    out = {}
+    for key, subNamespace of @namespaces
+      out[key] = recurse if 0 < Object.keys(recurse = subNamespace.getVersions()).length
+      (out[key] ||= {}).version = subNamespace.version if subNamespace.version?
+    out

   ################################################
   # BUILD UP NAMESPACES


Add DrawingArea#size

Add DrawingArea#size

--- samples/util/drawing_area.coffee
+++ samples/util/drawing_area.coffee
@@ -6,10 +6,14 @@
     $(window).on 'resize', => @resize()
     $ => @resize()

+  size: ->
+    width: window.innerWidth - container.offsetLeft * 2 - @element.offsetLeft
+    height: window.innerHeight - container.offsetTop * 2 - @element.offsetTop
+
   resize: ->
-    @svg.attr
-      width: @width = window.innerWidth - container.offsetLeft * 2 - @element.offsetLeft
-      height: @height = window.innerHeight - container.offsetTop * 2 - @element.offsetTop
+    size = @size()
+    {@width, @height} = size
+    @svg.attr size

   container = undefined
   $ => container = $('#container')[0]


Add 1 second offset to screenshots and reduce cooldown

--- modules/player/commands.coffee
+++ modules/player/commands.coffee
@@ -349,28 +349,18 @@
           msg.channel.send l.generic.success
         else
           msg.channel.send l.generic.invalidArgs
-
-<<<<<<< HEAD
+
     @registerCommand 'screenshot', { aliases: ['ss'] }, ({ m, l, player })=>
       return m.reply l.generic.cooldown if @cooldown[m.guild.id] > Date.now()
       @cooldown[m.guild.id] = Date.now() + 5000
-=======
-    @registerCommand 'screenshot', ({ m, l, player })=>
-      return m.reply l.generic.cooldown if @cooldown[m.guild.id] > Date.now()
-      @cooldown[m.guild.id] = Date.now() + 10000
->>>>>>> Screenshot Command
-
+
       { nowPlaying } = player.queue
       return m.reply l.player.notPlaying unless nowPlaying
       return m.reply l.player.screenshotError unless nowPlaying.videoPath

       try
         Core.util.sendTyping(m.channel)
-<<<<<<< HEAD
         time = if nowPlaying.duration then nowPlaying.originalTime + 2 else 0
-=======
-        time = if nowPlaying.duration then nowPlaying.time else 0
->>>>>>> Screenshot Command
         screenshot = await @util.getScreenshot nowPlaying.videoPath, time
         m.channel.send files: [{
           name: "focabot-#{nowPlaying.uid}-#{time}.jpg"


fix typo in email settings: differenct=>different (#1897)

--- app/assets/javascripts/app/controllers/_channel/email.coffee
+++ app/assets/javascripts/app/controllers/_channel/email.coffee
@@ -138,7 +138,7 @@
     template = $( '

' + App.i18n.translateContent('New') + '
' )

     description = '''
-You can define differenct signatures for each group. So you can have different email signatures for different departments.
+You can define different signatures for each group. So you can have different email signatures for different departments.

 Once you have created a signature here, you need also to edit the groups where you want to use it.
 '''


columnSelect: trigger change on the select on change

--- app/assets/javascripts/app/lib/app_post/column_select.coffee
+++ app/assets/javascripts/app/lib/app_post/column_select.coffee
@@ -50,6 +50,7 @@
     @pool.find("[data-value='#{value}']").addClass 'is-hidden'
     @values.push(value)
     @shadow.val(@values)
+    @shadow.trigger('change')

     @placeholder.addClass 'is-hidden'

@@ -64,6 +65,7 @@
     @selected.find("[data-value='#{value}']").addClass 'is-hidden'
     @values.splice(@values.indexOf(value), 1)
     @shadow.val(@values)
+    @shadow.trigger('change')

     if !@values.length
       @placeholder.removeClass 'is-hidden'


Improve description

Improve description

--- components/Log.coffee
+++ components/Log.coffee
@@ -44,8 +44,8 @@

 class Log extends noflo.Component

-  description: _s.clean "Log all packets, groups, and disconnects, to be
-  displayed on next tick."
+  description: _s.clean "Log all packets, groups, and disconnects, when
+  the entire stream is complete."

   constructor: ->
     @inPorts =


Use `print.pf` all the way

Use `print.pf` all the way

--- components/Log.coffee
+++ components/Log.coffee
@@ -14,18 +14,18 @@
   colors: false

 flush = ->
-  console.log "---------- NEW STREAM ----------"
+  print.pf "%s", "---------- NEW STREAM ----------"

   for l in log.reverse()
-    console.log ""
+    print.pf "%s", ""
     print.pf format, "CONNECT", ""
     display l
     print.pf format, "DISCONNECT", ""

-  console.log ""
-  console.log "--------------------------------"
-  console.log ""
-  console.log ""
+  print.pf "%s", ""
+  print.pf "%s", "--------------------------------"
+  print.pf "%s", ""
+  print.pf "%s", ""

   log = []


Add stamps for NGSA Objects

Add stamps for NGSA Objects

--- app/assets/javascripts/image_question_drawing_tool.js.coffee
+++ app/assets/javascripts/image_question_drawing_tool.js.coffee
@@ -16,6 +16,16 @@
         'https://interactions-resources.concord.org/stamps/medium-particle.svg'
         'https://interactions-resources.concord.org/stamps/fast-particle.svg'
         'https://interactions-resources.concord.org/stamps/low-density-particles.svg'
+      ],
+      'Objects': [
+        'https://interactions-resources.concord.org/stamps/car-facing-east.svg'
+        'https://interactions-resources.concord.org/stamps/car-facing-west.svg'
+        'https://interactions-resources.concord.org/stamps/front-crushed-east.svg'
+        'https://interactions-resources.concord.org/stamps/front-crushed-west.svg'
+        'https://interactions-resources.concord.org/stamps/N-S-magnet.svg'
+        'https://interactions-resources.concord.org/stamps/S-N-magnet.svg'
+        'https://interactions-resources.concord.org/stamps/new-phone.svg'
+        'https://interactions-resources.concord.org/stamps/broken-phone.svg'
       ]
     },
     # LARA provides simple proxy (/image-proxy?url=). It's useful even if we use


[WIP] Call functions after connection has been made

--- app/controllers/sync.coffee
+++ app/controllers/sync.coffee
@@ -1,31 +1,65 @@
 Spine = @Spine or require('spine')
 Model = Spine.Model

+# Connection states
+OFFLINE = 0
+IN_PROGRESS = 1
+ONLINE = 2
+
+
 Sync =
-  models: {}

-Model.Sync =
-  extended: ->
-    @change @saveLocal
-    @fetch @loadLocal
+  # Hold pending actions
+  pending: []

-    @connect()
+  # Run pending actions
+  _clearPending: ->
+    for [fn, that, args], i in @pending by - 1
+      fn.apply(that, args)
+      @pending.length--

-  connect: ->
+  # If connection is not ready, then we will wait until it is
+  defer: (self, fn, args...) ->
+    if @state is ONLINE
+      fn(args...)
+    else
+      @pending.push [fn, self,  args]
+      console.log @pending
+
+  state: OFFLINE
+
+  connect: (fn) ->
+
+    # Only run connect once
+    if @state isnt OFFLINE and typeof fn is 'function 'then return fn()
+    @state = IN_PROGRESS
+
     request = indexedDB.open("springseed", 1)

-    request.onupgradeneeded = (e) ->
-      Sync.db = e.target.result
+    request.onupgradeneeded = (e) =>
+      @db = e.target.result

       # We're going to just be storing all the spine stuff in one store,
       # just because I cbf seperating the Keys.
-      Sync.db.createObjectStore("meta")
+      @db.createObjectStore("meta")

       # We'll make another store to seperate the note s
-      Sync.db.createObjectStore("notes")
+      @db.createObjectStore("notes")

-    request.onsuccess = (e) ->
+    request.onsuccess = (e) =>
       Sync.db = e.target.result
+      @state = ONLINE
+      fn() if typeof fn is 'function'
+      @_clearPending()
+
+Model.Sync =
+
+  extended: ->
+
+    @fetch Sync.defer(this, @loadLocal)
+    @change Sync.defer(this, @saveLocal)
+
+    Sync.connect()

   saveLocal: ->
     result = JSON.stringify(@)
@@ -36,6 +70,9 @@
     request = store.put(result, @className)

   loadLocal: (options = {}) ->
+
+    console.log 'loading all'
+
     options.clear = true unless options.hasOwnProperty('clear')

     # Load from IndexedDB
@@ -53,5 +90,4 @@
     @extend Extend
     @include Include

-Spine.Sync = Sync
 module?.exports = Sync


Added better save error message

Added better save error message

--- src/code/providers/document-store-provider.coffee
+++ src/code/providers/document-store-provider.coffee
@@ -263,8 +263,13 @@
         if data.id then metadata.providerData.id = data.id

         callback null, data
-      error: ->
-        callback "Unable to save "+metadata.name
+      error: (jqXHR)->
+        try
+          responseJson = JSON.parse jqXHR.responseText
+          if responseJson.message is 'error.duplicate'
+            callback "Unable to create #{metadata.name}.  File already exists."
+        catch
+          callback "Unable to save #{metadata.name}"

   remove: (metadata, callback) ->
     $.ajax


Ensure concept ids are passed when context is cleared

--- src/coffee/cilantro/models/context/manager.coffee
+++ src/coffee/cilantro/models/context/manager.coffee
@@ -49,13 +49,13 @@
         toJSON: ->
             @upstream.toJSON()

-        _hasRequired: ->
-            c.config.get('query.concepts.required', []).length > 0
+        _getRequired: ->
+            c.config.get('query.concepts.required', [])

         # Returns true if a node is required
         _isRequired: (node) ->
             id = node.get('concept')
-            return c.config.get('query.concepts.required', []).indexOf(id) > -1
+            return @_getRequired().indexOf(id) > -1

         # Triggers the context invalid event given a list of invalid nodes
         _triggerRequired: (required) ->
@@ -65,7 +65,7 @@
         _checkRequired: ->
             invalid = []

-            for id in c.config.get('query.concepts.required', [])
+            for id in @_getRequired()
                 if not (n = @find(concept: id))
                     reason = 'undefined'
                 else if not n.isValid()
@@ -255,8 +255,8 @@
                     @save(u)

         clear: (options={}) ->
-            if @_hasRequired()
-                @_triggerRequired()
+            if (required = @_getRequired()).length
+                @_triggerRequired(required)
                 return false

             @upstream.clear(reset: true)


fix https://github.com/sagemathinc/smc/issues/152 -- "if loading a jupyter notebook fails..." -- by letting user interact with jupyter directly by moving the monkey patching a little earlier and making width full from start.  Looks a lot better during load and lets jupyter take care of problems however it wants without confusingly leaving SMC.

--- src/smc-webapp/editor_jupyter.coffee
+++ src/smc-webapp/editor_jupyter.coffee
@@ -137,11 +137,12 @@
         @blobs_pending = {}
         @state = 'loading'
         @iframe_uuid = misc.uuid()
-        @iframe = $("<iframe name=#{@iframe_uuid} id=#{@iframe_uuid}>")
+        @iframe = $("<iframe name=#{@iframe_uuid} id=#{@iframe_uuid} width=100%>")
             .attr('src', "#{@server_url}#{@filename}")
             .attr('frameborder', '0')
             .attr('scrolling', 'no')
         @element.html('').append(@iframe)
+        @iframe.maxheight()
         # wait until connected -- it is ***critical*** to wait until
         # the kernel is connected before doing anything else!
         start = new Date()
@@ -163,6 +164,8 @@
                 # to that event until events loads, and by then we may have missed the one event.
                 # Also, I've observed that @frame.IPython.notebook._fully_loaded does not imply the
                 # kernels are connected.
+                if @frame?.IPython?.notebook?
+                    @monkey_patch_frame()
                 if @frame?.IPython?.notebook?.kernel?.is_connected() and @frame.IPython.notebook._fully_loaded
                     # kernel is connected; now patch the Jupyter notebook page (synchronous)
                     @nb = @frame.IPython.notebook
@@ -182,14 +185,14 @@
                         @dirty_interval = setInterval(@check_dirty, 250)
                         @nb.events.on('spec_changed.Kernel', => @nb.dirty = true)
                         @init_cursor()
-                    @monkey_patch_frame()
                     @disable_autosave()
                     @state = 'ready'
                     @emit('ready')
                     cb()
                 else
                     console.log 'Jupyter -- not yet fully connected'
-                    setTimeout(f, 1000)
+                    if @state != 'closed'
+                        setTimeout(f, 1000)
         f()

     dbg: (f) =>
@@ -217,6 +220,9 @@
         @nb.set_autosave_interval(0)

     monkey_patch_frame: () =>
+        if @_already_monkey_patched
+            return
+        @_already_monkey_patched = true
         misc_page.cm_define_diffApply_extension(@frame.CodeMirror)
         misc_page.cm_define_testbot(@frame.CodeMirror)
         @monkey_patch_logo()
@@ -873,7 +879,7 @@
                     cb       : cb
             (cb) =>
                 console.log 'Jupyter: loading iframe'
-                @notebook.css('opacity',0.5)
+                @notebook.css('opacity',0.75)
                 done = (err) =>
                     @notebook.css('opacity',1)
                     if err


Cornerstone depends on EventEmitter. Hand craft the NIfTI parser.

--- app/javascripts/main.coffee
+++ app/javascripts/main.coffee
@@ -57,13 +57,12 @@
     dc: './lib/dc'
     domReady: [DOM_READY_LIB + '/domReady' + MIN, './lib/domReady']
     'error-stack-parser': './lib/error-stack-parser'
+    EventEmitter: './lib/EventEmitter'
     lodash: './lib/lodash'
     moment: './lib/moment'
-    ndarray: './lib/ndarray'
     nganimate: [NG_LIB + '/' + 'angular-animate' + MIN, './lib/angular-animate']
     ngndc: './lib/angular-dc'
     ngnvd3: './lib/angular-nvd3'
-    nifti: './lib/nifti'
     nvd3: './lib/nv.d3'
     ngresource: [NG_LIB + '/' + 'angular-resource' + MIN, './lib/angular-resource']
     ngroute: [NG_LIB + '/' + 'angular-route' + MIN, './lib/angular-route']
@@ -92,6 +91,7 @@
   # The non-AMD module dependencies.
   shim:
     angular: exports : 'angular'
+    cornerstone: deps: ['EventEmitter']
     d3: exports: 'd3'
     lodash: exports: '_'
     nganimate: deps: ['angular']


Remove click to stop.

Remove click to stop.

--- index.coffee
+++ index.coffee
@@ -30,7 +30,7 @@
   retro.stop()
   save()

-setInterval save, 10000
+setInterval save, 10000 # ideally saving would only be done on exit

 addEventListener 'beforeunload', ->
   stop() if retro.player
@@ -115,10 +115,6 @@
   if not draghint.classList.contains 'hidden'
     draghint.classList.add 'hover'
     chooser.click()
-  else if retro.running
-    retro.stop()
-  else
-    retro.start()

 addEventListener 'focus', () ->
   draghint.classList.remove 'hover'


OneOf draft implementation.

OneOf draft implementation.

--- index.coffee
+++ index.coffee
@@ -19,13 +19,14 @@
     r = @handleElement ast

   handleElement: (element) ->
-    prim = element.primitive
-    if prim
+    if element.primitive?
+      prim = element.primitive
+
       if not prim.type or not prim.value
         # TODO we don't know either type or the value
         # - should we skip the property?
         return null
-
+      # TODO and what about having only one of them?
       if prim.type is 'object'
         @format.representObject @handleProperties prim.value
       else if prim.type is 'array'
@@ -43,7 +44,10 @@
         # - should we imply string or to skip the property?
         return null

-    # TODO oneOf, ref
+    else if element?.oneOf?.length > 0
+      @handleElement element.oneOf[0]  # choose the first one
+
+    # TODO ref

   handleProperty: (property) ->
     @format.representObjectProperty property.name, @handleElement property


Update voxel-oculus to use factory export convention: https://github.com/deathcap/voxel-oculus/commit/aa7d5efd8821105497983c32acca21cc39a08a35

--- index.coffee
+++ index.coffee
@@ -53,7 +53,7 @@

   if window.location.href.indexOf("rift") != -1 ||  window.location.hash.indexOf("rift") != -1
     # Oculus Rift support TODO: allow in-game toggling
-    oculus = new createOculus(game, { distortion: 0.2, separation: 0.5 })
+    oculus = createOculus(game, { distortion: 0.2, separation: 0.5 })
     document.getElementById("logo").style.visibility = "hidden"

   container = opts.container || document.body


One more fix for relative paths.

One more fix for relative paths.

--- index.coffee
+++ index.coffee
@@ -1,7 +1,7 @@
 co = require 'co'
 {readFileSync, writeFileSync, readdirSync} = require 'fs'
 {readFile, writeFile, readdir} = require 'co-fs'
-{dirname, basename, extname, sep, isAbsolute, join} = require 'path'
+{dirname, basename, extname, sep, isAbsolute, join, resolve} = require 'path'
 {parse, load, dump} = yaml = require 'js-yaml'

 PARSER_SCHEMA =
@@ -14,6 +14,7 @@
 YAML_EXT = ['.yaml', '.yml']

 PARENT_DIRNAME = dirname module.parent.filename
+delete require.cache[__filename]

 ###
 # Normalize path to YAML file
@@ -22,7 +23,7 @@
 ###
 normalizePath = (filename) ->
   unless isAbsolute filename
-    filename = join PARENT_DIRNAME, filename
+    filename = resolve PARENT_DIRNAME, filename

   base = basename filename
   dir = dirname filename
@@ -43,7 +44,7 @@
 ###
 normalizePathSync = (filename) ->
   unless isAbsolute filename
-    filename = join PARENT_DIRNAME, filename
+    filename = resolve PARENT_DIRNAME, filename

   base = basename filename
   dir = dirname filename


fix missing module

fix missing module

--- index.coffee
+++ index.coffee
@@ -1,4 +1,5 @@
 fs = require 'fs'
+path = require 'path'
 debug = require('debug') 'tastetyper'
 express = require 'express'
 morgan = require 'morgan'


pipe fix

pipe fix

--- index.coffee
+++ index.coffee
@@ -112,9 +112,11 @@
         when 'selection'
           editor.getSelectedText()

-      if stream
+      if stream and @focusedTerminal
         @focusedTerminal.focus()
-        @focusedTerminal?.pty?.write stream.trim()
+        @focusedTerminal.click()
+        @focusedTerminal.term?.focus()
+        @focusedTerminal.pty?.write stream.trim()

     handleRemoveTerm: (termView)->
       @termViews.splice @termViews.indexOf(termView), 1


Move binding of scroll event

Move binding of scroll event

--- app/js/views/threads.coffee
+++ app/js/views/threads.coffee
@@ -10,6 +10,10 @@
     @listenTo @collection, 'add', @add
     @listenTo @collection, 'reset', @addAll
     @listenToOnce @collection, 'sync', =>
+      # FIXME: this should be bound in #render, but for some reason the scroll
+      # event doesn't work when this is there.
+      @$content = @$('.content').on('scroll', _.debounce(@paginate, 50))
+
       # Fetch more if filtered notifications don't fill the screen.
       setTimeout(@paginate, 1)

@@ -19,7 +23,6 @@
   render: ->
     @$el.html @template()
     app.trigger 'render', @
-    @$content = @$('.content').on('scroll', _.debounce(@paginate, 50))
     @

   add: (notification) ->


Add Geordi integration and basic interventions support.

--- app/lib/intervention.coffee
+++ app/lib/intervention.coffee
@@ -21,7 +21,7 @@

 # method to check for a new intervention, and set it into variable ready for delivery. Intended to be polled.
 checkForAndProcessIntervention = =>
-  if nextIntervention?
+  if !nextIntervention?
     $.ajax "#{EXPERIMENT_SERVER_URL}/users/#{User.current?.zooniverse_id}/interventions",
         success  : (res, status, xhr) ->
           if res.length > 0


Added extend to add class methods

Added extend to add class methods

--- app/models/baseClass.coffee
+++ app/models/baseClass.coffee
@@ -1,14 +1,30 @@
 PublishSubscribe = require './modules/pubSub.coffee'

 class Base
-  @include = (args...) ->
-    _prototype = @prototype
+  @extend = (args...) ->
+    _class = this
     args.forEach (module) ->
       if module.constructor != Object
         throw new Error('Arguments not an object or an array of objects')
       for prop of module
-        _prototype[prop] = module[prop] unless prop == 'moduleName'
-      afterInclude = _prototype.afterInclude
+        _class[prop] = module[prop] unless prop == 'moduleName'
+      afterExtend = _klass.afterExtend
+      afterExtend(module) if afterExtend
+
+  afterExtend: (module) ->
+    if !module.moduleName
+      throw new Error('Module defined without a name')
+    @extendedModules = [] if !@extendedModules
+    @extendedModules.push(module.moduleName)
+
+  @include = (args...) ->
+    _class = this
+    args.forEach (module) ->
+      if module.constructor != Object
+        throw new Error('Arguments not an object or an array of objects')
+      for prop of module
+        _class.prototype[prop] = module[prop] unless prop == 'moduleName'
+      afterInclude = _class.afterInclude
       afterInclude(module) if afterInclude

   afterInclude: (module) ->
@@ -17,7 +33,7 @@
     @includedModules = [] if !@includedModules
     @includedModules.push(module.moduleName)

-  @include PublishSubscribe
+  @extend PublishSubscribe

 module.exports = Base


Store selected_workflow preference

Store selected_workflow preference

--- app/pages/project/home.cjsx
+++ app/pages/project/home.cjsx
@@ -28,9 +28,11 @@
   getInitialState: ->
     workflows: []

+  handleWorkflowSelection: (workflow) ->
+    @props.onChangePreferences 'preferences.selected_workflow', workflow?.id
+    undefined # Don't prevent default Link behavior.
+
   render: ->
-    [owner, name] = @props.project.slug.split('/')
-

         <FinishedBanner project={@props.project} />
@@ -45,12 +47,18 @@
             at {@props.project.redirect}

         else if @props.project.configuration?.user_chooses_workflow
-          for workflow in @state.workflows
-            <Link to={"/projects/#{owner}/#{name}/classify?workflow=#{workflow.id}"} key={workflow.id} className="call-to-action standard-button">
+          @state.workflows.map (workflow) =>
+            <Link
+              to={"/projects/#{@props.project.slug}/classify"}
+              query={workflow: workflow.id}
+              key={workflow.id + Math.random()}
+              className="call-to-action standard-button"
+              onMouseDown={@handleWorkflowSelection.bind this, workflow}
+            >
               {workflow.display_name}
             </Link>
         else
-          <Link to={"/projects/#{owner}/#{name}/classify"} className="call-to-action standard-button">
+          <Link to={"/projects/#{@props.project.slug}/classify"} className="call-to-action standard-button">
             Get started!
           </Link>}


Fixes secure WebSocket on HTTPs.

Fixes secure WebSocket on HTTPs.

--- app/scripts/services.coffee
+++ app/scripts/services.coffee
@@ -45,7 +45,7 @@
     url = do ->
       # Note: The path is ignored, the WebSocket must be relative to
       # root.
-      protocol = if $location.protocol() is 'https:' then 'wss:' else 'ws:'
+      protocol = if $location.protocol() is 'https' then 'wss:' else 'ws:'
       host = $location.host()
       port = $location.port()


Add Service factory

Add Service factory

--- app/scripts/services.coffee
+++ app/scripts/services.coffee
@@ -16,6 +16,8 @@
     apiResource $resource, 'waterpoints'
   .factory 'Facility', ($resource) ->
     apiResource $resource, 'facilities'
+  .factory 'Service', ($resource) ->
+    apiResource $resource, 'services'
   .factory 'Map', (Waterpoint) ->
     # Initially center on Dar es Salaam
     @center =


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -40,10 +40,10 @@
       I.velocity.x = -1

     if keydown.right
-      I.x += I.speed
+      I.velocity.x = 1

     if keydown.up
-      I.y -= I.speed
+      I.ve

     if keydown.down
       I.y += I.speed


Ops! Fixed in chunker

Ops! Fixed in chunker

--- app/services/chunker.coffee
+++ app/services/chunker.coffee
@@ -145,7 +145,7 @@
 			cb(null, doc)

 	aggregateInChunk: (item, latestItemData, instance, chunk, cb) ->
-		please {$model:@itemModel}, '$skip', {$model:@chunkModel}, '$isFn'
+		please {$model:@itemModel}, '$skip', '$skip', {$model:@chunkModel}, '$isFn'
 		logger.debug("UPDATE", chunk._id, item)

 		@chunkModel.findOneAndUpdate {


no fix

no fix

--- app/assets/javascripts/chapter_concept_review_order.js.coffee
+++ app/assets/javascripts/chapter_concept_review_order.js.coffee
@@ -41,6 +41,7 @@
   initialize: ->
     super JSON.parse(@$('.hidden input').val())
     _.bindAll 'save'
+
     @$el.closest('form').find('.form-actions .btn').on 'click', =>
       @save()
       @$('input[name="lesson[answer_array_json] "]').remove()
@@ -53,4 +54,4 @@

 jQuery ($) ->
   dataLoad 'ConceptReviewRoot'
-  dataLoad 'LessonAnswerRoot'+  dataLoad 'LessonAnswerRoot'


Return empty token array for zero-length matches

--- src/app/text-mate-grammar.coffee
+++ src/app/text-mate-grammar.coffee
@@ -38,16 +38,16 @@

       break if position == line.length

-      { nextTokens, tokensStartPosition, tokensEndPosition} = _.last(stack).getNextTokens(stack, line, position)
+      { nextTokens, tokensStartPosition, tokensEndPosition } = _.last(stack).getNextTokens(stack, line, position)

       if position < tokensStartPosition # unmatched text before next tokens
         tokens.push
           value: line[position...tokensStartPosition]
           scopes: scopes

-      tokens.push(nextTokens...) if nextTokens
-
-      if tokensEndPosition is undefined # unmatched text after last token
+      if nextTokens
+        tokens.push(nextTokens...)
+      else # unmatched text after last token
         tokens.push
           value: line[position...line.length]
           scopes: scopes
@@ -164,7 +164,7 @@
       [start, end] = captureIndices[1..2]
       zeroLengthMatch = end == start
       if zeroLengthMatch
-        tokens = null
+        tokens = []
       else
         tokens = [{ value: line[start...end], scopes: scopes }]
     if @pushRule


Fixing check to see if there's a merit enabled tag present in the chart

--- app/assets/javascripts/lib/views/html_table_chart_view.coffee
+++ app/assets/javascripts/lib/views/html_table_chart_view.coffee
@@ -19,7 +19,7 @@
     @fill_cells()
     @after_render()

-    if @container_node().find(".merit_order_enabled")
+    if @container_node().find(".merit_order_enabled").length > 0
       @check_merit_enabled()

   # The table HTML is provided by the rails app.


Polygon Mode exits cleanly

Polygon Mode exits cleanly

--- src/main/webapp/lib/views/PolygonDynamicView.coffee
+++ src/main/webapp/lib/views/PolygonDynamicView.coffee
@@ -2,7 +2,6 @@
 # based on mouse clicks
 class window.beats.PolygonDynamicView extends google.maps.OverlayView
   $a = window.beats
-  vertices : []
   markerIconOpts: {
         strokeColor: '#FF0000'
         strokeWeight: 1
@@ -20,6 +19,7 @@
     modeMsg = 'Polygon Mode: Click the polygon to retrieve data or '
     modeMsg += 'press \'Esc\' to exit.'
     $a.broker.trigger('app:display_message:info', modeMsg)
+    @vertices = []
     @drawPolygon()

   manageDraw : () ->
@@ -35,7 +35,6 @@

   drawPolygon: () ->
     @poly = new google.maps.Polygon({
-      path: []
       map: $a.map
       strokeColor: '#FF0000'
       strokeWeight: 2
@@ -47,15 +46,16 @@
       clickable: true
     })
     gme = google.maps.event
-    gme.addListener($a.map, 'click', (e) =>
+    @mapClickHandler = gme.addListener($a.map, 'click', (e) =>
       @vertices.push e.latLng
       @manageDraw()
     )
-    gme.addListener(@poly, 'click', (e) =>
+    @polyClickHandler = gme.addListener(@poly, 'click', (e) =>
       if !e.vertex?
         $a.map.setOptions({draggableCursor:'hand'})
         @vertices = []
         @removePolygon()
+        @_tearDownListeners()
         $a.broker.trigger('app:show_message:success', 'Retrieving Pems Data')
         $a.broker.trigger('app:display_message:info', 'Scenario Editing Mode')
       else
@@ -77,17 +77,17 @@
       icon: @_getMarkerOpts("#FFFFFF")
     })
     gme = google.maps.event
-    gme.addListener(@marker, 'click', (e) =>
+    @markerClickHandler = gme.addListener(@marker, 'click', (e) =>
       @vertices.splice(e.vertex, 1)
       @manageDraw()
     )
-    gme.addListener(@marker, 'mouseover', (e) =>
+    @markerOverHandler = gme.addListener(@marker, 'mouseover', (e) =>
       @marker.setIcon(@_getMarkerOpts("#FF0000"))
     )
-    gme.addListener(@marker, 'mouseout', (e) =>
+    @markerOutHandler = gme.addListener(@marker, 'mouseout', (e) =>
       @marker.setIcon(@_getMarkerOpts("#FFFFFF"))
     )
-    gme.addListener(@marker, 'dragend', (e) =>
+    @markerDragHandler = gme.addListener(@marker, 'dragend', (e) =>
       @vertices.pop()
       @vertices.push(e.latLng)
     )
@@ -106,7 +106,7 @@
       icon: @markerIconOpts
     })
     gme = google.maps.event
-    gme.addListener(@line, 'click', (e) =>
+    @lineClickHandler = gme.addListener(@line, 'click', (e) =>
       @vertices.splice(e.vertex, 1)
       @manageDraw()
     )
@@ -119,8 +119,19 @@
       @vertices = []
       $a.map.setOptions({draggableCursor:'hand'})
       @removePolygon()
-      $a.broker.trigger('app:display_message:info', 'Scenario Editing Mode')
-
+      @_tearDownListeners()
+      $a.broker.trigger('app:display_message:info', 'Scenario Editing Mode')
+
+  _tearDownListeners: () ->
+    gme = google.maps.event
+    gme.removeListener(@mapClickHandler) if @mapClickHandler?
+    gme.removeListener(@polyClickHandler) if @polyClickHandler?
+    gme.removeListener(@markerClickHandler) if @markerClickHandler?
+    gme.removeListener(@markerOverHandler) if @markerOverHandler?
+    gme.removeListener(@markerOutHandler) if @markerOutHandler?
+    gme.removeListener(@markerDragHandler) if @markerDragHandler?
+    gme.removeListener(@lineClickHandler) if @lineClickHandler?
+
   _getMarkerOpts : (color) ->
     @markerIconOpts.fillColor = color
     @markerIconOpts


Include snake_case named tests

Include snake_case named tests

--- services/web/test/unit_frontend/coffee/test-main.coffee
+++ services/web/test/unit_frontend/coffee/test-main.coffee
@@ -1,9 +1,10 @@
 # Set up requirejs to load the tests
-# Uses heuristic that test filenames end with Tests.js
+# Uses heuristic that test filenames end with Tests.js (existing frontend code)
+# or _tests.js (newer frontend code)
 tests = []
 for file of window.__karma__.files
   if window.__karma__.files.hasOwnProperty(file)
-    if /test\/unit_frontend\/js.+Tests.js$/.test(file)
+    if /test\/unit_frontend\/js.+(_t|T)ests\.js$/.test(file)
       tests.push(file)

 requirejs.config


dummy commit

dummy commit

--- tests/integration/components/vnc-viewer-test.coffee
+++ tests/integration/components/vnc-viewer-test.coffee
@@ -8,6 +8,7 @@
 test 'it renders', (assert) ->
   assert.expect 1

+
   # Set any properties with @set 'myProperty', 'value'
   # Handle any actions with @on 'myAction', (val) ->


fugly but should work

fugly but should work

--- scripts/import_coppermine.coffee
+++ scripts/import_coppermine.coffee
@@ -22,6 +22,10 @@
   107 # Animeunioni
 ]

+ROOT_CATEGORY_ID = 0
+
+LARGE_NUMBER = 9999999
+
 connection.connect()

 query = Q.nbind connection.query, connection
@@ -31,17 +35,28 @@
     getAlbum('/')
     query("SET NAMES 'latin1';")
   ]).spread (root) ->
-    convertSubcategories(0, root).then ->
-      finalizeAlbum root
+    convertSubcategories(ROOT_CATEGORY_ID, root).then ->
+      finalizeAlbum root, categoryId: ROOT_CATEGORY_ID

-finalizeAlbum = (edegalAlbum, parent) ->
+finalizeAlbum = (edegalAlbum, opts) ->
+  {categoryId, parent} = opts
   setThumbnail edegalAlbum

   if parent? and not _.find(parent.subalbums, (subalbum) -> subalbum.path == edegalAlbum.path)
     parent.subalbums.push _.pick edegalAlbum, 'path', 'title', 'thumbnail', '_pos'

+  # XXX FUGLY
+  # We want new categories to get positioned at the top, and new albums at the bottom.
+  # Existing albums won't have _pos, so we use a fallback value to send them at start or end.
+  # We rely on _.sortBy being stable.
+  fallbackPos =
+    if categoryId?
+      LARGE_NUMBER
+    else
+      -LARGE_NUMBER
+
   edegalAlbum.subalbums = _.chain(edegalAlbum.subalbums)
-    .sortBy((subalbum) -> subalbum._pos)
+    .sortBy((subalbum) -> subalbum._pos ? fallbackPos)
     .map((subalbum) -> _.omit(subalbum, '_pos'))
     .value()


simplify log mapper and fix logic error in nan/inf handling

--- bokehjs/src/coffee/mapper/log_mapper.coffee
+++ bokehjs/src/coffee/mapper/log_mapper.coffee
@@ -17,17 +17,10 @@

     if inter_scale == 0
       intermediate = 0
+
     else
-
-      try
-        intermediate = (Math.log(x) - inter_offset) / inter_scale
-
-        if isNaN(intermediate)
-          throw new Error "NaN"
-        if isFinite(intermediate) == false
-          throw new Error "Infinite"
-
-      catch error
+      intermediate = (Math.log(x) - inter_offset) / inter_scale
+      if isNaN(intermediate) or not isFinite(intermediate)
         intermediate = 0

     result = intermediate * scale + offset
@@ -41,30 +34,14 @@
     result = new Float64Array(xs.length)

     if inter_scale == 0
-      intermediate = xs.map (i) -> i * 0
+      intermediate = xs.map (i) -> 0
+
     else
+      intermediate = xs.map (i) -> (Math.log(i) - inter_offset) / inter_scale

-      try
-        mask1 = xs.map (i) -> i <= 0
-        mask2 = xs.map (i) -> isNaN(i)
-
-        mask = (mask1[i] | mask2[i] for i in [0...xs.length])
-
-        mask = mask.reduce (x, y) -> x || y
-
-        if mask == 1
-          xs[mask] = 1
-
-        intermediate = xs.map (i) -> (Math.log(i) - inter_offset) / inter_scale
-
-        for x, idx in intermediate
-          if isNaN(intermediate[idx])
-            throw new Error "NaN"
-          if isFinite(intermediate[idx]) == false
-            throw new Error "Infinite"
-
-      catch error
-        intermediate = xs.map (i) -> i * 0
+      for x, idx in intermediate
+        if isNaN(intermediate[idx]) or not isFinite(intermediate[idx])
+          intermediate[idx] = 0

     for x, idx in xs
       result[idx] = intermediate[idx] * scale + offset
@@ -99,8 +76,7 @@

     if start == end
       if start == 0
-        start = 1
-        end = 10
+        [start, end] = [1, 10]
       else
         log_val = Math.log(start) / Math.log(10)
         start = Math.pow(10, Math.floor(log_val))
@@ -113,7 +89,6 @@
     return [start, end]

   _mapper_state: () ->
-
     source_start = @get('source_range').get('start')
     source_end   = @get('source_range').get('end')
     target_start = @get('target_range').get('start')
@@ -131,6 +106,7 @@

     scale = screen_range
     offset = target_start
+
     return [scale, offset, inter_scale, inter_offset]

 module.exports =


Fix DOM.replaceWithHTML()

Fix DOM.replaceWithHTML()

--- src/marbles/dom.js.coffee
+++ src/marbles/dom.js.coffee
@@ -39,10 +39,13 @@
     offset_top

   replaceWithHTML: (el, html) ->
-    fragment = document.createDocumentFragment()
-    DOM.appendHTML(fragment, html)
-    DOM.replaceWith(el, fragment)
-    fragment
+    new_el = document.createElement('div')
+    DOM.appendHTML(new_el, html)
+    DOM.replaceWith(el, new_el)
+    if new_el.childElementCount == 1
+      new_el.children[0]
+    else
+      new_el

   replaceWith: (el, new_el) ->
     el.parentNode.replaceChild(new_el, el)


Use Spree.url() to add tokens to API calls to stock_locations

On 2.0, stock_locations AJAX class are getting a 401 on http://localhost:3000/admin/stock_transfers/new since securing the stock locations API end points.

Fixes #3828

--- backend/app/assets/javascripts/admin/stock_transfer.js.coffee
+++ backend/app/assets/javascripts/admin/stock_transfer.js.coffee
@@ -30,7 +30,7 @@

       $('#transfer_receive_stock').change (event) => @receive_stock_change(event)

-      $.getJSON Spree.routes.stock_locations_api, (data) =>
+      $.getJSON Spree.url(Spree.routes.stock_locations_api), (data) =>
         @locations = (location for location in data.stock_locations)
         @force_receive_stock() if @locations.length < 2

@@ -92,13 +92,13 @@
       if @cached_variants?
         @populate_select @cached_variants
       else
-        $.getJSON Spree.routes.variants_api, (data) =>
+        $.getJSON Spree.url(Spree.routes.variants_api), (data) =>
           @cached_variants = _.map(data.variants, (variant) -> new TransferVariant(variant))
           @populate_select @cached_variants

     _refresh_transfer_stock_items: ->
       stock_location_id = $('#transfer_source_location_id').val()
-      $.getJSON Spree.routes.stock_locations_api + "/#{stock_location_id}/stock_items", (data) =>
+      $.getJSON Spree.url(Spree.routes.stock_locations_api + "/#{stock_location_id}/stock_items"), (data) =>
         @populate_select _.map(data.stock_items, (stock_item) -> new TransferStockItem(stock_item))

     populate_select: (variants) ->
@@ -177,5 +177,3 @@
     transfer_locations = new TransferLocations
     transfer_variants = new TransferVariants
     transfer_add_variants = new TransferAddVariants
-
-


update pagination view to target the change of page action to the controller

--- vendor/assets/javascripts/pagination-view.js.coffee
+++ vendor/assets/javascripts/pagination-view.js.coffee
@@ -13,7 +13,7 @@
                   {{value}}
                 {{else}}
                   {{value}}
+                     {{action changePage target="view.controller"}}>{{value}}
                 {{/if}}

             {{/each}}


use event instead of e for clarity

use event instead of e for clarity

--- src/views/themes/default/coffee/global/forms.coffee
+++ src/views/themes/default/coffee/global/forms.coffee
@@ -273,7 +273,7 @@
           $('body').append($uploader)
           $(document.getElementById(id + '-input')).fileupload({
             dataType: 'json'
-            done: (e, data) ->
+            done: (event, data) ->
               $uploaded = $(document.getElementById(id + '-uploaded'))
               if $uploaded.length is 0
                 $uploaded = $("

")
@@ -304,7 +304,7 @@
               )
               $("a[href=##{id}-uploaded]").click()
             #return
-            error: (e, data) ->
+            error: (event, data) ->
               $error = $("
500 Internal Server Error
")
               $(document.getElementById(id + '-new')).prepend($error)
               $error.slideDown('slow').delay(3000).animate({


Removed console.log

Removed console.log

--- src/coffee/bootstrap-tour.coffee
+++ src/coffee/bootstrap-tour.coffee
@@ -214,7 +214,6 @@

     # Show the specified step
     showStep: (i) ->
-      console.log "show step!"
       step = @getStep(i)
       return unless step


Remove console.logs :/

Remove console.logs :/

--- apps/sitemaps/routes.coffee
+++ apps/sitemaps/routes.coffee
@@ -46,12 +46,10 @@
       ), (err, results) ->
         return cb(err) if err
         allPages = results.map (sres) -> Math.ceil sres.headers['x-total-count'] / PAGE_SIZE
-        console.log(allPages)
         cb null, allPages
   ], (err, [articlePages, allPages]) ->
     return next(err) if err
     res.set('Content-Type', 'text/xml')
-    console.log(allPages)
     res.render('index', {
       pretty: true
       articlePages: articlePages


<controller> Adding routes for Drivers.

<controller> Adding routes for Drivers.

--- server/controllers/routes.coffee
+++ server/controllers/routes.coffee
@@ -7,7 +7,8 @@

 sensor			= require './sensor'
 actuator		= require './actuator'
-measure			= require './measure'
+measure         = require './measure'
+driver         = require './driver'
 rule			= require './rule'
 sensorRule		= require './sensorRule'
 actuatorRule	= require './actuatorRule'
@@ -19,6 +20,7 @@
     'actuatorId': 	param: actuator.fetch
     'ruleId': 		param: rule.fetch
     'measureId': 	param: measure.fetch
+    'driverId':     param: driver.fetch

     'sensors/?':
         get: sensor.all
@@ -42,6 +44,15 @@
         put: actuator.update
         delete: actuator.delete

+    'drivers/?':
+        get: driver.all
+        post: driver.create
+
+    'drivers/:driverId/?':
+        get: driver.read
+        put: driver.update
+        delete: driver.delete
+
     'rules/?':
         get: rule.all
         post: rule.create


Fix bug when unsuccessfull submit did not step attempts|

--- server/cron/submitSubmits.coffee
+++ server/cron/submitSubmits.coffee
@@ -54,6 +54,8 @@
     catch e
         if not success
             throw e
+    if not success
+        throw "Can not submit, submit did not appear in submit list..."

 submitOneSubmit = (submit) ->


Fix typo

Fix typo

--- server/cron/submitSubmits.coffee
+++ server/cron/submitSubmits.coffee
@@ -67,7 +67,7 @@
             await testSystem.submitWithObject(registeredUser, submit.problem, {source: submit.sourceRaw, language, testSystemData: submit.testSystemData})
             await sleep(1000)
         finally
-            await downloadSubmits.runFпока не заorUserAndProblem(registeredUser.userKey(), submit.problem, onNewSubmit)
+            await downloadSubmits.runForUserAndProblem(registeredUser.userKey(), submit.problem, onNewSubmit)
     catch e
         if not success
             throw e


Fixed direct message update problems

Fixed direct message update problems

--- server/methods/updateRoom.coffee
+++ server/methods/updateRoom.coffee
@@ -7,14 +7,15 @@
 			throw new Meteor.Error('invalid-user', "[methods] updateRoom -> Requires authenticated user")

 		# validate params exist
-		unless rid and name and usernames and accessPermissions
+		unless name and accessPermissions
 			throw new Meteor.Error 'invalid-argument', 'Missing required values'

 		room = ChatRoom.findOne rid

 		# don't allow the room creator to be removed
-		unless room.u.username in usernames
-			throw new Meteor.Error 'invalid-argument', 'You cannot remove the room creator'
+		if room.u?
+			unless room.u.username in usernames
+				throw new Meteor.Error 'invalid-argument', 'You cannot remove the room creator'

 		if room.t not in ['d'] and name? and name isnt room.name
 			Meteor.call 'saveRoomName', rid, name


Add explicit check for vimeo filelist being undefined

--- src/provider/vimeo.coffee
+++ src/provider/vimeo.coffee
@@ -49,6 +49,10 @@
                 240: []

             files = data.request.files.h264
+            if not files
+                console.error("vimeo::extract() was missing files for vi:#{id}")
+                return {}
+
             if 'mobile' of files
                 videos[240].push(
                     link: files.mobile.url
@@ -72,7 +76,7 @@
             else if res.data.indexOf('Because of its privacy settings, this video cannot
                                       be played here') >= 0
                 return {}
-            console.error("vimeo::lookupAndExtract() failed: #{e.stack}")
+            console.error("vimeo::extract() failed for vi:#{id} : #{e.stack}")
             return {}
     )


failed props for async button

failed props for async button

--- src/components/async-button.cjsx
+++ src/components/async-button.cjsx
@@ -11,6 +11,7 @@
     isFailed: React.PropTypes.bool
     waitingText: React.PropTypes.any # TODO: This should be a Component or array
     failedState: React.PropTypes.func
+    failedProps: React.PropTypes.object
     doneText: React.PropTypes.any

   getDefaultProps: ->


Adds global hotkey for closing a dialog (alt-w). Refs #374

--- app/assets/javascripts/alchemy/alchemy.hotkeys.js.coffee
+++ app/assets/javascripts/alchemy/alchemy.hotkeys.js.coffee
@@ -27,3 +27,6 @@
     key = $this.data('alchemy-hotkey')
     keymage key, ->
       $this.click()
+
+  keymage 'alt-w', ->
+    Alchemy.CurrentWindow.dialog('close')


What are you sending

What are you sending

--- app/assets/javascripts/components/article_form.js.coffee
+++ app/assets/javascripts/components/article_form.js.coffee
@@ -33,4 +33,4 @@
 						className: 'btn btn-danger'
 						type: 'submit'
 						disabled: !@valid()
-						'Send'+						'Send Article URL'


add comment

add comment

--- app/assets/javascripts/controllers/my_settings.js.coffee
+++ app/assets/javascripts/controllers/my_settings.js.coffee
@@ -47,6 +47,9 @@

   saveOnUpdate: (->
+    # We check for this condition to avoid recursive
+    # loops triggered by fast consecutive mouse clicks:
+    #
     state = @get('content.currentState.stateName')
     if state == 'root.loaded.updated.uncommitted'
       Ember.run.scheduleOnce 'afterRender', @, @saveCurrentUser


fetch project for todos list view

fetch project for todos list view

--- app/assets/javascripts/dashit/views/todos/list.js.coffee
+++ app/assets/javascripts/dashit/views/todos/list.js.coffee
@@ -1,6 +1,10 @@
 DashIt.ProjectTodosView = Ember.View.extend
   templateName: 'dashit/templates/todos/list'
   todosBinding: 'DashIt.todosController'
+
+  didInsertElement: ->
+    project = @get("parentView").get("project")
+    @set "project", project

   showNewTodo:
     -> @set "isNewTodoVisible", true


OX-261 reverted fix, need to updateBranches before adding particleSystem

--- app/assets/javascripts/oxalis/geometries/skeleton.coffee
+++ app/assets/javascripts/oxalis/geometries/skeleton.coffee
@@ -80,6 +80,8 @@
     @branches = new THREE.ParticleSystem(
         routeGeometryBranchPoints,
         new THREE.ParticleBasicMaterial({color: COLOR_ACTIVE * 0.7, size: 8, sizeAttenuation : false}))
+
+    @updateBranches()

     @route.on
       newActiveNode : => @setActiveNode()


fix progress indication for small files

fix progress indication for small files

--- app/assets/javascripts/uploadcare/files/object.js.coffee
+++ app/assets/javascripts/uploadcare/files/object.js.coffee
@@ -48,7 +48,7 @@
           xhr = $.ajaxSettings.xhr()
           if xhr.upload
             xhr.upload.addEventListener 'progress', (e) =>
-              @__uploadDf.notify(e.loaded / @fileSize)
+              @__uploadDf.notify(e.loaded / e.total)
             , false
           xhr
         crossDomain: true


doesn't reset collaborator picker list when loading more

fixes CNVS-8264

test plan
- have a course with more than 50 users (more than 100 is better)
- navigate to the "start a new collaboration" page
- scroll to the bottom of the "collaborate with" user picker
- the users should load in groups of about 50 when you approach
 the bottom of the list
- ensure that when all users are loaded and you've reached the
 bottom of the list, you can scroll back to the top and no users
 have been removed from the list

Change-Id: Iff29f880375872afd6a48bf606e93f39236707c8
Reviewed-on: https://gerrit.instructure.com/30130
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Braden Anderson <a6cc2cef7d25f27847876bdb81651755fcebacf0@instructure.com>
QA-Review: Steven Shepherd <e794458448443812fb2685675aab3bbd1a3891bc@instructure.com>
Product-Review: Joel Hough <4816dabf8db1bc6cac35b3a24cab2ff844b5b0c7@instructure.com>

--- app/coffeescripts/collections/PaginatedCollection.coffee
+++ app/coffeescripts/collections/PaginatedCollection.coffee
@@ -43,6 +43,7 @@
     ##
     # options.page: 'current', 'next', 'prev', 'first', 'last', 'top', 'bottom'
     fetch: (options = {}) ->
+      options = _.clone(options)
       @loadedAll = false
       exclusionFlag = "fetching#{capitalize options.page}Page"
       @[exclusionFlag] = true


Include route and query in search params

--- src/scripts/router.coffee
+++ src/scripts/router.coffee
@@ -28,7 +28,7 @@
         uuid = settings.shortcodes[uuid] if settings.shortcodes[uuid]
         @appView.render('contents', {uuid: uuid, page: Number(page)})

-      @route /^search/, 'search', () ->
+      @route /^(search)(?:\?q=)?(.*)/, 'search', () ->
         @appView.render('search')

       @route 'about-us', 'about-us', () ->


append / remove Router 実装

append / remove Router 実装

テスト各か

--- src/coffee/kazitori.coffee
+++ src/coffee/kazitori.coffee
@@ -335,9 +335,10 @@
   #メソッド名 intercept のほうがいいかな
   reject:()->
     @dispatchEvent(new KazitoriEvent(KazitoriEvent.REJECT, @.fragment))
-    @._beforeDeffer.removeEventListener KazitoriEvent.TASK_QUEUE_COMPLETE, @beforeComplete
-    @._beforeDeffer.removeEventListener KazitoriEvent.TASK_QUEUE_FAILED, @beforeFailed
-    @._beforeDeffer = null
+    if @._beforeDeffer
+      @._beforeDeffer.removeEventListener KazitoriEvent.TASK_QUEUE_COMPLETE, @beforeComplete
+      @._beforeDeffer.removeEventListener KazitoriEvent.TASK_QUEUE_FAILED, @beforeFailed
+      @._beforeDeffer = null
     return

   #処理を一時停止する
@@ -407,7 +408,7 @@

   #ルールを後から追加する
   appendRouter:(child, childRoot)->
-    if not child instanceof Kazitori or typeof child not "function"
+    if not child instanceof Kazitori and typeof child isnt "function"
       # throw new Error("引数が不正です")
       #console.warn
       # return false
@@ -431,6 +432,9 @@
   _getChildRule:(child, childRoot)->
     rule = child.root
     if childRoot
+      lne = childRoot.length
+      if childRoot.match(trailingSlash)
+        childRoot = childRoot.replace(trailingSlash, '')
       rule = childRoot
     if rule is @.root
       throw new Error("かぶってる")
@@ -438,9 +442,8 @@

   #ルールを削除する
   removeRouter:(child, childRoot)->
-    if not child instanceof Kazitori or typeof child not "function"
-      return
-
+    if not child instanceof Kazitori and typeof child isnt "function"
+      return
     if child instanceof Kazitori
       @_unbindChild(child, childRoot)
     else
@@ -454,30 +457,25 @@

   _unbindChild:(child, childRoot)->
     rule = @_getChildRule(child, childRoot)
-    dels = []
     i = 0
     len = @.handlers.length
+    newHandlers = []
     while i < len
-      ruleObj = @.handlers[i]
-      #あー match とかじゃないとだめだな
-      if ruleObj.rule.indexOf rule isnt -1
-        dels.push i
+      ruleObj = @.handlers.shift()
+      if (ruleObj.rule.match rule) is null
+        newHandlers.unshift(ruleObj)
       i++
-    for i in dels
-      @.handlers.splice(i, 1)
-
-    dels = []
+    @.handlers = newHandlers
+
     i = 0
     len = @.beforeHandlers.length
+    newBefores = []
     while i < len
-      beforeRule = @.beforeHandlers[i]
-      if beforeRule.rule.indexOf rule isnt -1
-        dels.push i
+      beforeRule = @.beforeHandlers.shift()
+      if (beforeRule.rule.match rule) is null
+        newBefores.unshift(beforeRule)
       i++
-    for i in dels
-      @.beforeHandlers.splice(i, 1)
-
-
+    @.beforeHandlers = newBefores

   #URL を読み込む


bind => on

bind => on

--- test/bindable.coffee
+++ test/bindable.coffee
@@ -4,6 +4,9 @@
   o = Core()
   o.include("Bindable")

+  o.bind("test", -> ok true)
+
+  o.trigger("test")

 test "Multiple bindings", 2, ->
   o = Core().include(Bindable)


Add back buttons

Add back buttons

--- src/config/controls.coffee
+++ src/config/controls.coffee
@@ -27,6 +27,6 @@
     click: (world, args) -> world.send world.get('value')
   }
   _CHILDREN: [
-    "step", "run"#, "stop","loop", "reset"
+    "step", "run", "stop","loop", "reset"
   ]
 }


Refactor duplicated createDomainEvent function

--- src/context/context.coffee
+++ src/context/context.coffee
@@ -35,7 +35,7 @@
     if !DomainEventClass
       throw new Error "Tried to emitDomainEvent '#{domainEventName}' which is not defined"

-    domainEvent = @_createDomainEvent domainEventName, DomainEventClass, domainEventPayload
+    domainEvent = @createDomainEvent domainEventName, DomainEventClass, domainEventPayload
     @saveAndPublishDomainEvent domainEvent
     .then =>
       @_eventric.log.debug "Created and Handled DomainEvent in Context", domainEvent
@@ -45,16 +45,20 @@
     @_eventBus.publishDomainEvent domainEvent

-  _createDomainEvent: (domainEventName, DomainEventClass, domainEventPayload) ->
+  createDomainEvent: (domainEventName, DomainEventClass, domainEventPayload, aggregate) ->
+    payload = {}
+    DomainEventClass.apply payload, [domainEventPayload]
+
     new @_eventric.DomainEvent
       id: @_eventric.generateUid()
       name: domainEventName
+      aggregate: aggregate
       context: @name
-      payload: new DomainEventClass domainEventPayload
+      payload: payload

   # TODO: Consider renaming. What store? event store? read model store?
-  addStore: (storeName, StoreClass, storeOptions={}) ->
+  addStore: (storeName, StoreClass, storeOptions = {}) ->
     @_storeClasses[storeName] =
       Class: StoreClass
       options: storeOptions
@@ -232,7 +236,7 @@
         .catch (err) ->
           reject err

-      , (err) =>
+      , (err) ->
         return reject err if err
         resolve()

@@ -383,4 +387,4 @@
         reject err

-module.exports = Context+module.exports = Context


select task based on status for tutor/student view

--- src/app/projects/partials/directives/student-project-directive.coffee
+++ src/app/projects/partials/directives/student-project-directive.coffee
@@ -72,7 +72,16 @@
           if $scope.assessingUnitRole?
             # Find first task that is Ready To Mark or Need Help
             t = _.find filteredTasks, (t) -> t.status == 'need_help' || t.status == 'ready_to_mark'
+            if not t? # else find discuss
+              t = _.find filteredTasks, (t) -> t.status == 'discuss'
+            if not t? # else find resubmit or redo
+              t = _.find filteredTasks, (t) -> t.status == 'redo' || t.status == 'fix_and_resubmit' || t.status == 'fix_and_include'
+            if not t? # else find working on it
+              t = _.find filteredTasks, (t) -> t.status == 'working_on_it'
+            if not t? # else find not_submitted
+              t = _.find filteredTasks, (t) -> t.status == 'not_submitted'
             $scope.project.selectedTask = t
+

         if not $scope.project.selectedTask?
           $scope.project.selectedTask = $scope.project.selectedTask = filteredTasks[0]


Fix #6

Fix #6

--- src/dom_text_mapper.coffee
+++ src/dom_text_mapper.coffee
@@ -308,6 +308,7 @@
     if mappings.length is 0
       throw new Error "No mappings found for [" + start + ":" + end + "]!"

+    mappings = mappings.sort (a, b) -> a.element.start - b.element.start

     # Create a DOM range object
 #    console.log "Building range..."


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -17,7 +17,7 @@
   ###*
   Get the game object matching the given selector that is closest to the given position.

-  <code
+

   player = engine.add
     x: 0
     y: 0
@@ -38,6 +38,7 @@

   equals engine.closest(".enemy", player.position()), enemy1
   equals engine.closest(".enemy", player2.position()), enemy2
+

   @param {String} selector
   @param {Point} position


Placeholder to show in table header when the mouse enters the cell, fixes #199 	We have a label in the th, but it is for a group, not for the control, which may not be a problem 	adjustContainerForRepeat (container) -> container 		container closest .fr-grid is .fr-repeat-single-row 			If it is, then get th corresponding to current cell 				Get position of container relative to parent tr 				Get child element inside tr.fr-dt-master-row at that position 				Return that header container in addition to the container 			If not, just return the container

--- src/resources/forms/orbeon/builder/resources/grid/placeholders.coffee
+++ src/resources/forms/orbeon/builder/resources/grid/placeholders.coffee
@@ -33,10 +33,12 @@
                     o = params.pop()
                     jQueryObject[m].apply o, params
         result
-    f$.findOrIs = (selector, element) ->                                                                                # Like find, but includes the current element if matching
-        result = f$.find selector, element
-        result = f$.add element, result if f$.is selector, element
+    f$.findOrIs = (selector, jQueryObject) ->                                                                                # Like find, but includes the current element if matching
+        result = f$.find selector, jQueryObject
+        result = f$.add jQueryObject, result if f$.is selector, jQueryObject
         result
+    f$.length = (jQueryObject) -> jQueryObject.length
+    f$.nth = (n, jQueryObject) -> $ jQueryObject[n - 1]

     # Low-level operations on editables
     editables =
@@ -114,7 +116,14 @@
     editDone = (f) -> editDoneCallbacks.add f

     # Return elements, maybe relative to a node
-    elementsInContainerWithSelector = (container, selectors) -> f$.find (selectors.join ', '), container
+    adjustContainerForRepeat = (container) ->
+        grid =  f$.closest '.fr-grid', container
+        if f$.is '.fr-repeat-single-row', grid
+            position = 1 + f$.length f$.prevAll container
+            thContainer = f$.nth position, f$.children f$.find 'tr.fr-dt-master-row', grid
+            f$.add thContainer, container
+        else container
+    elementsInContainerWithSelector = (container, selectors) -> f$.find (selectors.join ', '), adjustContainerForRepeat container
     elementsInContainer = (container) -> elementsInContainerWithSelector container, anyEditableSelector
     labelsInContainer = (container) -> elementsInContainerWithSelector container, _.pluck (_.pick editables, ['label', 'button', 'link']), 'selector'
     elementClosest = (element) -> f$.closest (anyEditableSelector.join ', '), element


Add original value to maxlength bump.

Add original value to maxlength bump.

--- src/input/InputView.coffee
+++ src/input/InputView.coffee
@@ -202,12 +202,13 @@
     if exceededMaxLength or
     newValue is @value or
     newValue.length is @value.length
+      originalValue = newValue
       newValue = @value

       if ((newCursorPos isnt cursorPos or exceededMaxLength) or
       invalidCharacter) and
       not @_wasBackspace
-        @bump()
+        @bump('maxlength', originalValue)

     @value = newValue


Object: Little bit code styling.

Object: Little bit code styling.

--- src/lib/core/Object.coffee
+++ src/lib/core/Object.coffee
@@ -4,13 +4,12 @@
 goog.require 'goog.events.EventTarget'

-###*
-  Base object class which provides custom event management.
-  In theory, every framework class should extend this class.
-###
 class spark.core.Object extends goog.events.EventTarget

   ###*
+    Base object class which provides custom event management.
+    In theory, every framework class should extend this class.
+
     @constructor
     @export
     @param   {Object=} options Class options.
@@ -32,6 +31,7 @@

   ###*
     Sets options object of this class.
+
     @export
     @param {!Object} options Options object.
   ###
@@ -41,6 +41,7 @@

   ###*
     Returns all options object.
+
     @export
     @return {Object} Options object.
   ###
@@ -50,6 +51,7 @@

   ###*
     Returns value of a key in options object.
+
     @export
     @return {*} Value of key or null.
   ###
@@ -59,6 +61,7 @@

   ###*
     Sets data of this class.
+
     @export
     @param {*} data Data passed to class.
   ###
@@ -68,6 +71,7 @@

   ###*
     Returns class data.
+
     @export
     @return {*} Class data.
   ###
@@ -77,6 +81,7 @@

   ###*
     Sets uid.
+
     @private
   ###
   setUid_: ->
@@ -105,8 +110,8 @@

   ###*
-    Adds an event listener.
-    See also {@link goog.events.Listenable.prototype.listen}
+    Adds an event listener. See also {@link goog.events.Listenable::listen}
+
     @export
     @param {!string} eventName Name of the event.
     @param {!Function} callback Callback function for the event.
@@ -119,6 +124,7 @@
   ###*
     Adds an event listener that is removed automatically after the listener
     fired once. See also {@link goog.events.Listenable.prototype.listenOnce}
+
     @export
     @param {!string} eventName Name of the event.
     @param {!Function} callback Callback function for the event.
@@ -131,6 +137,7 @@
   ###*
     Dispatches an event and calls all listeners listening for events of this
     event. See also {@link goog.events.Listenable.prototype.dispatchEvent}
+
     @export
     @param {!string} eventName Name of the event.
     @param {*=} data Data which will passed to listeners
@@ -142,6 +149,7 @@
   ###*
     Removes an event listener which was added with @on or @once.
     See also {@link goog.events.Listenable.prototype.unlisten}
+
     @export
     @param {!string} eventName Name of the event.
     @param {!Function} callback Callback function for the event.