Add sections to Grammar

Add sections to Grammar

--- src/grammar.coffee
+++ src/grammar.coffee
@@ -47,6 +47,10 @@
     @fileTypes ?= []
     @includedGrammarScopes = []

+  ###
+  Section: Event Subscription
+  ###
+
   # Public: Invoke the given callback when this grammar is updated due to a
   # grammar it depends on being added or removed from the registry.
   #
@@ -63,6 +67,10 @@
       Grim.deprecate("Call explicit event subscription methods instead")

     EmitterMixin::on.apply(this, arguments)
+
+  ###
+  Section: Tokenizing
+  ###

   # Public: Tokenize all lines in the given text.
   #


Implement custom tool tip for heatmap

Implement custom tool tip for heatmap

--- src/heatmap.coffee
+++ src/heatmap.coffee
@@ -106,20 +106,6 @@
         xlabels = myframe.xlabels()
         ylabels = myframe.ylabels()

-        celltip = d3.tip()
-                    .attr('class', "d3-tip #{tipclass}")
-                    .html((d) ->
-                            x = d3panels.formatAxis(data.x)(d.x)
-                            y = d3panels.formatAxis(data.y)(d.y)
-                            z = d3panels.formatAxis([0, zmax/100])(d.z)
-                            return "#{z}" if data.xcat? and data.ycat?
-                            return "(#{y}) → #{z}" if data.xcat?
-                            return "(#{x}) → #{z}" if data.ycat?
-                            "(#{x}, #{y}) → #{z}")
-                    .direction('e')
-                    .offset([0,10])
-        svg.call(celltip)
-
         # scaled x and y midpoints
         xmid_scaled = (xscale(xv) for xv in xmid)
         ymid_scaled = (yscale(yv) for yv in ymid)
@@ -142,20 +128,28 @@
                     .attr("stroke", "none")
                     .attr("stroke-width", "1")
                     .attr("shape-rendering", "crispEdges")
-                    .on("mouseover.paneltip", (d) ->
+                    .on("mouseover", (d) ->
                                                   d3.select(this).attr("stroke", hilitcolor).raise()
-                                                  celltip.show(d)
                                                   if data.xcat? # show categorical scales
                                                       svg.select("text#xlab#{d.x}").attr("opacity", 1)
                                                   if data.ycat?
                                                       svg.select("text#ylab#{d.y}").attr("opacity", 1))
-                    .on("mouseout.paneltip", (d) ->
+                    .on("mouseout", (d) ->
                                                   d3.select(this).attr("stroke", "none")
-                                                  celltip.hide()
                                                   if data.xcat? # hide categorical scales
                                                       svg.select("text#xlab#{d.x}").attr("opacity", 0)
                                                   if data.ycat?
                                                       svg.select("text#ylab#{d.y}").attr("opacity", 0))
+
+        tooltipfunc = (d,i) ->
+                            x = d3panels.formatAxis(data.x)(d.x)
+                            y = d3panels.formatAxis(data.y)(d.y)
+                            z = d3panels.formatAxis([0, zmax/100])(d.z)
+                            return "#{z}" if data.xcat? and data.ycat?
+                            return "(#{y}) → #{z}" if data.xcat?
+                            return "(#{x}) → #{z}" if data.ycat?
+                            "(#{x}, #{y}) → #{z}"
+        celltip = d3panels.tooltip_create(d3.select("body"), cellrect.selectAll("rect"), {tipclass: tipclass}, tooltipfunc)

         # handle categorical scales:
         #    replace text with category labels, add IDs, and hide them initially
@@ -182,7 +176,7 @@
     # function to remove chart
     chart.remove = () ->
                       svg.remove()
-                      celltip.destroy()
+                      d3panels.tooltip_destroy(celltip)
                       return null

     # return the chart function


Remove default strict append.

Remove default strict append.

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -323,7 +323,7 @@
   modules              = config.modules      ?= {}
   modules.wrapper     ?= 'commonjs'
   modules.definition  ?= 'commonjs'
-  modules.strict      ?= yes
+  modules.strict      ?= no

   config.server       ?= {}
   config.server.base  ?= ''


handle multiple badges in "I have" command

--- src/ingress.coffee
+++ src/ingress.coffee
@@ -118,12 +118,13 @@

 module.exports = (robot) ->
   badges =
-    add: (user, badgeName) ->
-      toRemove = badgeList.filter (x) ->
-        (x.replace /\d+/, '') is (badgeName.replace /\d+/, '')
-      @del user, badge for badge in toRemove
-      userBadges = robot.brain.data.ingressBadges[user.id] ?= []
-      userBadges.push ":#{badgeName}:"
+    add: (user, badgeNames...) ->
+      for badgeName in badgeNames
+        toRemove = badgeList.filter (x) ->
+          (x.replace /\d+/, '') is (badgeName.replace /\d+/, '')
+        @del user, badge for badge in toRemove
+        userBadges = robot.brain.data.ingressBadges[user.id] ?= []
+        userBadges.push ":#{badgeName}:"
     del: (user, badgeName) ->
       robot.brain.data.ingressBadges[user.id] = (badges.forUser user).filter (x) ->
         x isnt ":#{badgeName}:"
@@ -151,23 +152,36 @@
       "\nL#{lv} = #{lvl.ap} AP#{if badgeReq? then ' ' + badgeReq.join ' ' else ''}"
     msg.send lvls.join ""

-  robot.respond /(I|@?\w+) (?:have|has|got|earned)(?: the)? :?(\w+):? badge/i, (msg) ->
+  robot.respond /(I|@?\w+) (?:have|has|got|earned)(?: the)? :?([\w,\s]+):? badges?/i, (msg) ->
     who = msg.match[1].replace '@', ''
-    badgeName = msg.match[2]
+    badgeNames = (msg.match[2].replace /\s*/g, '').split ','

     if who.toLowerCase() == 'i'
       who = msg.envelope.user
     else
       who = robot.brain.userForName who

-    if badgeName in badgeList
+    invalidNames = []
+    for badgeName in badgeNames
+      if badgeName not in badgeList
+        invalidNames.push badgeName
+
+    msg.reply "Invalid badge name(s): #{invalidNames.join ', '}." if invalidNames.length > 0
+    badgeNames = badgeNames.filter (x) -> x not in invalidNames
+
+    for badgeName in badgeNames
       badges.add who, badgeName
-      if who.name == msg.envelope.user.name
-        msg.reply "congrats on earning the :#{badgeName}: badge!"
-      else
-        msg.send "@#{who.name}: congrats on earning the :#{badgeName}: badge!"
-    else
-      msg.reply "Invalid badge name. Available badges are: #{badgeList.join ', '}"
+
+    userBadges = badges.forUser who
+    badgeNames = badgeNames.filter (x) ->
+      ":#{x}:" in userBadges
+
+    if who.name == msg.envelope.user.name
+      msg.reply "congrats on earning the :#{badgeNames.join ': :'}:
+ badge#{if badgeNames.length > 1 then 's' else ''}!"
+    else
+      msg.send "@#{who.name}: congrats on earning the :#{badgeNames.join ': :'}:
+ badge#{if badgeNames.length > 1 then 's' else ''}!"

   robot.respond /wh(?:at|ich) badges? do(?:es)? (I|@?\w+) have/i, (msg) ->
     who = msg.match[1].replace '@', ''


Add uninstall instructions

Add uninstall instructions

--- src/install.coffee
+++ src/install.coffee
@@ -440,7 +440,10 @@

         @isBundledPackage name, (isBundledPackage) =>
           if isBundledPackage
-            console.error "The #{name} package is bundled with Atom and should not be explicitly installed.".yellow
+            console.error """
+              The #{name} package is bundled with Atom and should not be explicitly installed.
+              You can run `apm uninstall #{name}` to uninstall it.
+            """.yellow
           @installPackage({name, version}, options, callback)

     if packagesFilePath


Add CSON snippets for language-grammar development

--- .config/snippets.cson
+++ .config/snippets.cson
@@ -52,3 +52,13 @@
 	"Comment":
 		prefix: "c"
 		body: "/** $1 */"
+
+
+# CoffeeScript/CSON
+".source.coffee":
+	"Patterns":
+		prefix: "pat"
+		body: "patterns: [{\n\t$1\n}]"
+	"Include":
+		prefix: "inc"
+		body: "include: \"#$1\""


Update course invite email subject and body with course name

--- server/courses/course_instance_handler.coffee
+++ server/courses/course_instance_handler.coffee
@@ -121,20 +121,25 @@
       return @sendNotFoundError(res) unless courseInstance
       return @sendForbiddenError(res) unless @hasAccessToDocument(req, courseInstance)

-      Prepaid.findById courseInstance.get('prepaidID'), (err, prepaid) =>
+      Course.findById courseInstance.get('courseID'), (err, course) =>
         return @sendDatabaseError(res, err) if err
-        return @sendNotFoundError(res) unless prepaid
-        return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length
-        for email in req.body.emails
-          context =
-            email_id: sendwithus.templates.course_invite_email
-            recipient:
-              address: email
-            email_data:
-              class_name: courseInstance.get('name')
-              join_link: "https://codecombat.com/courses?_ppc=" + prepaid.get('code')
-          sendwithus.api.send context, _.noop
-        return @sendSuccess(res, {})
+        return @sendNotFoundError(res) unless course
+
+        Prepaid.findById courseInstance.get('prepaidID'), (err, prepaid) =>
+          return @sendDatabaseError(res, err) if err
+          return @sendNotFoundError(res) unless prepaid
+          return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length
+          for email in req.body.emails
+            context =
+              email_id: sendwithus.templates.course_invite_email
+              recipient:
+                address: email
+              subject: course.get('name')
+              email_data:
+                class_name: course.get('name')
+                join_link: "https://codecombat.com/courses?_ppc=" + prepaid.get('code')
+            sendwithus.api.send context, _.noop
+          return @sendSuccess(res, {})

   redeemPrepaidCodeAPI: (req, res) ->
     return @sendUnauthorizedError(res) if not req.user? or req.user?.isAnonymous()


Utility methods to implement healing-over-time.

--- src/challenger.coffee
+++ src/challenger.coffee
@@ -7,19 +7,44 @@

   constructor: (@user, @storage) ->
     @storage.hp ?= 50
+    @storage.maxHP ?= 50
     @storage.exp ?= 0

   id: -> @user.id

   hp: -> @storage.hp

+  maxHP: -> @storage.maxHP
+
   exp: -> @storage.exp
+
+  # Public: amount of HP recovered per second.
+  #
+  healingRate: -> 2

   damage: (amount) -> @storage.hp -= amount

   heal: (amount) -> @storage.hp += amount

   levelUp: (amount) -> @storage.exp += amount
+
+  # Public: Revive this Challenger with full HP.
+  #
+  respawn: -> @storage.hp = @maxHP()
+
+  # Public: Begin the healing process.
+  #
+  healOverTime: ->
+    fn = =>
+      @healInt = null
+      @storage.hp = Math.min(@maxHP(), @hp() + @healingRate())
+      @healInt = setInterval(fn, 1000) if @hp() < @maxHP()
+    @healInt = setInterval fn, 1000
+
+  # Public: Halt the healing process begun with `healOverTime`. Important so you don't heal during
+  # a Match!
+  #
+  stopHealing: -> clearInterval(@healInt) if @healInt?

   displayName: -> "@#{@user.name}"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/keydown.coffee
+++ src/keydown.coffee
@@ -1,36 +0,0 @@
-$ ->
-  ###*
-  The global keydown property lets your query the status of keys.
-
-  <pre>
-  # Examples:
-
-  if keydown.left
-    moveLeft()
-
-  if keydown.a or keydown.space
-    attack()
-
-  if keydown.return
-    confirm()
-
-  if keydown.esc
-    cancel()
-
-  </pre>
-
-  @name keydown
-  @namespace
-  ###
-  window.keydown = {}
-
-  keyName = (event) ->
-    jQuery.hotkeys.specialKeys[event.which] ||
-    String.fromCharCode(event.which).toLowerCase()
-
-  $(document).bind "keydown", (event) ->
-    keydown[keyName(event)] = true
-
-  $(document).bind "keyup", (event) ->
-    keydown[keyName(event)] = false
-


clearing views dict when empting

clearing views dict when empting

--- src/mixins/list.coffee
+++ src/mixins/list.coffee
@@ -119,6 +119,7 @@

   empty: () ->
     @getListElement().empty()
+    @views = {}

   added: (model, container) ->
     if @filterFunc model, @filters


Remove unnecessary checks

Remove unnecessary checks

--- resources/assets/coffee/react/profile-page/header-extra.coffee
+++ resources/assets/coffee/react/profile-page/header-extra.coffee
@@ -210,8 +210,6 @@

   rankChartHover: (_e, {data} = {}) =>
-    return if @props.user.isBot
-
     if data?
       hoverLine1 = "##{(-data.y).toLocaleString()}"
       hoverLine2 =
@@ -226,8 +224,6 @@

   rankChartUpdate: =>
-    return if @props.user.isBot
-
     if !@rankChart?
       options =
         scales:


make code clearer when compilation is per-user

--- services/web/app/coffee/Features/Compile/CompileManager.coffee
+++ services/web/app/coffee/Features/Compile/CompileManager.coffee
@@ -37,8 +37,9 @@
 							return callback(error) if error?
 							for key, value of limits
 								options[key] = value
-							user_id = undefined if not options.isolated
-							ClsiManager.sendRequest project_id, user_id, options, (error, status, outputFiles, clsiServerId) ->
+							# only pass user_id down to clsi if this is a per-user compile
+							compileAsUser = if options.isolated then user_id else undefined
+							ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId) ->
 								return callback(error) if error?
 								logger.log files: outputFiles, "output files"
 								callback(null, status, outputFiles, clsiServerId, limits)


Add test

Add test

--- src/api/apps/articles/test/model/index/persistence.test.coffee
+++ src/api/apps/articles/test/model/index/persistence.test.coffee
@@ -261,6 +261,10 @@
             height: '400'
             mobile_height: '300'
           }
+          {
+            type: 'social_embed'
+            url: 'some-link.com'
+          }
         ]
       }, 'foo', {}, (err, article) ->
         article.lead_paragraph.should.equal '
abcd abcd
<svg onload="alert(1)"/>'
@@ -272,6 +276,7 @@
         article.sections[4].url.should.equal 'http://maps.google.com'
         article.sections[4].height.should.equal '400'
         article.sections[4].mobile_height.should.equal '300'
+        article.sections[5].url.should.equal 'http://some-link.com'
         done()

     it 'doesnt escape smart quotes', (done) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -18,7 +18,7 @@
         width
       } = meterData

-      if not I[name] || not
+      if not I[name] || not I[]
       ratio = I[name] / I["max#{name.capitalize()}"]

       if showName || text


namespaced Movable

namespaced Movable

--- src/movable.coffee
+++ src/movable.coffee
@@ -43,7 +43,7 @@
   I.velocity = Point(I.velocity.x, I.velocity.y)

   self.bind 'update.Movable', (dt) ->
-    I.velocity = I.velocity.add(I.acceleration)
+    I.velocity = I.velocity.add(I.acceleration.scale(d))

     if I.maxSpeed?
       currentSpeed = I.velocity.magnitude()


adding dummy OS name

adding dummy OS name

--- src/natives.coffee
+++ src/natives.coffee
@@ -89,11 +89,12 @@
   rs.pop()

 # properties to set:
-#  java.version,java.vendor.url,java.class.version,java.class.path,os.name,os.arch,os.version
+#  java.version,java.vendor.url,java.class.version,java.class.path,os.arch,os.version
 system_properties = {
   'java.home':'third_party/java_home/', 'file.encoding':'US_ASCII','java.vendor':'DoppioVM',
   'line.separator':'\n', 'file.separator':'/', 'path.separator':':',
   'user.dir':'.','user.home':'.','user.name':'DoppioUser',
+  'os.name':'Doppio',
   # we don't actually load from this file, but it has to look like a valid filename
   'sun.boot.class.path': '/System/Library/Frameworks/JavaVM.framework/Classes/classes.jar',
   # this doesn't actually exist in our classes.jar, but no other GraphicsEnvironment does either


Ensure more opcodes eat the correct number of arguments.

lookupswitch and tableswitch remain to be done.

--- src/opcodes.coffee
+++ src/opcodes.coffee
@@ -51,8 +51,8 @@
   13: new Opcode 'fconst_2'
   14: new Opcode 'dconst_0'
   15: new Opcode 'dconst_1'
-  16: new Opcode 'bipush'
-  17: new Opcode 'sipush'
+  16: new Opcode 'bipush', 1
+  17: new Opcode 'sipush', 2
   18: new LoadOpcode 'ldc'
   19: new LoadOpcode 'ldc_w', 2
   20: new LoadOpcode 'ldc2_w', 2
@@ -222,7 +222,7 @@
   184: new InvokeOpcode 'invokestatic'
   184: new InvokeOpcode 'invokeinterface'
   187: new ClassOpcode 'new'
-  188: new Opcode 'newarray'
+  188: new Opcode 'newarray', 1
   189: new ClassOpcode 'anewarray'
   190: new Opcode 'arraylength'
   191: new Opcode 'athrow'
@@ -231,7 +231,7 @@
   194: new Opcode 'monitorenter'
   195: new Opcode 'monitorexit'
   196: new Opcode 'wide'
-  197: new Opcode 'multianewarray'
+  197: new Opcode 'multianewarray', 3
   198: new BranchOpcode 'ifnull'
   199: new BranchOpcode 'ifnonnull'
   200: new Opcode 'goto_w'


Defer requiring of $ in Package

Defer requiring of $ in Package

This is needed in the case that the package manager is being used
in the browser process to open a URL.

Closes #1869

--- src/package.coffee
+++ src/package.coffee
@@ -7,7 +7,7 @@
 {Emitter} = require 'emissary'
 Q = require 'q'

-{$} = require './space-pen-extensions'
+$ = null # Defer require in case this is in the window-less browser process
 ScopedProperties = require './scoped-properties'

 # Loads and activates a package's main module and resources such as
@@ -286,6 +286,7 @@
   handleActivationEvent: (event) =>
     bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
     @activateNow()
+    $ ?= require('./space-pen-extensions').$
     $(event.target).trigger(event)
     @restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
     @unsubscribeFromActivationEvents()
@@ -303,6 +304,7 @@
   disableEventHandlersOnBubblePath: (event) ->
     bubblePathEventHandlers = []
     disabledHandler = ->
+    $ ?= require('./space-pen-extensions').$
     element = $(event.target)
     while element.length
       if eventHandlers = element.handlers()?[event.type]


Keep old folder names in api preview mode

--- src/package.coffee
+++ src/package.coffee
@@ -277,7 +277,7 @@
       [stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)]

   getStylesheetsPath: ->
-    if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'stylesheets'))
+    if fs.isDirectorySync(path.join(@path, 'stylesheets'))
       deprecate("Store package style sheets in the `styles/` directory instead of `stylesheets/` in the `#{@name}` package", packageName: @name)
       path.join(@path, 'stylesheets')
     else
@@ -353,7 +353,7 @@

     deferred = Q.defer()

-    if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'scoped-properties'))
+    if fs.isDirectorySync(path.join(@path, 'scoped-properties'))
       settingsDirPath = path.join(@path, 'scoped-properties')
       deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name)
     else


Fix invalid usage of activation disposable in Package::load

Like deserializers and config schema, directory providers are added
at package load time, and *not* removed when the package is deactivated

--- src/package.coffee
+++ src/package.coffee
@@ -298,9 +298,7 @@
       for version, methodName of directoryProviderService.versions
         if typeof @mainModule[methodName] is 'function'
           servicesByVersion[version] = @mainModule[methodName]()
-
-      disposable = @packageManager.serviceHub.provide('atom.directory-provider', servicesByVersion)
-      @activationDisposables.add(disposable)
+      @packageManager.serviceHub.provide('atom.directory-provider', servicesByVersion)

   registerViewProviders: ->
     if @metadata.viewProviders? and not @registeredViewProviders


Add destroy

Add destroy

--- admin/static/coffee/namespaces/replicas.coffee
+++ admin/static/coffee/namespaces/replicas.coffee
@@ -861,3 +861,6 @@
             else
                 @state = 'show_primary'
             @render()
+
+        destroy: =>
+            @model.off 'change:primary_uuid', @change_pin


use XHR.abort() instead of .cancel()

use XHR.abort() instead of .cancel()

--- app/assets/javascripts/_open_street_map.coffee
+++ app/assets/javascripts/_open_street_map.coffee
@@ -67,7 +67,7 @@

   fetch_data: (query, handler) ->
     if @fetch_request
-      @fetch_request.cancel()
+      @fetch_request.abort()

     @fetch_request = $.ajax(
       url: "/api/circles.json",


Call jquery.dotdotdot() after update.

Call jquery.dotdotdot() after update.

--- app/assets/javascripts/flow_managers.js.coffee
+++ app/assets/javascripts/flow_managers.js.coffee
@@ -25,10 +25,6 @@
     Tahi.overlay.display event, @props.task.cardName

 PaperProfile = React.createClass
-  componentDidMount: (DOMElement, rootNode) ->
-    $('h4', rootNode).dotdotdot
-      height: 40
-
   render: ->
     {div, h4, a} = React.DOM

@@ -55,6 +51,12 @@
     $.getJSON 'flow_manager', (data,status) =>
       @setProps flows: data.flows

+  componentDidUpdate: ->
+    $('.paper-profile h4').dotdotdot
+      height: 40
+
+
+
   render: ->
     {ul} = React.DOM
     (ul {className: 'columns'},


Remove json call to load assets

Remove json call to load assets

--- app/assets/javascripts/georgia/media.js.coffee
+++ app/assets/javascripts/georgia/media.js.coffee
@@ -11,15 +11,4 @@
   $('#fileupload').fileupload('option',
     maxFileSize: 2000000,
     maxNumberOfFiles: 50
-  )
-
-  # Load existing files:
-  $.getJSON($('#fileupload').prop('action'), (files) ->
-    fu = $('#fileupload').data('fileupload')
-    fu._adjustMaxNumberOfFiles(-files.length)
-    template = fu._renderDownload(files).appendTo($('#fileupload .files'))
-    # Force reflow:
-    fu._reflow = fu._transition && template.length && template[0].offsetWidth
-    template.addClass('in')
-    $('#loading').remove()
   )


charts: fixed table switching

charts: fixed table switching

--- app/assets/javascripts/lib/models/chart.coffee
+++ app/assets/javascripts/lib/models/chart.coffee
@@ -215,16 +215,20 @@
       @load chart_id, chart_holder
       close_fancybox()

-    $("a.table_format").live 'click', =>
-      $("a.table_format").hide()
-      $("a.chart_format").show()
-      @current().view.toggle_format()
-      false
-
-    $("a.chart_format").live 'click', =>
-      $("a.chart_format").hide()
-      $("a.table_format").show()
-      @current().view.toggle_format()
+    $(document).on "click", "a.table_format", (e) =>
+      holder_id = $(e.target).parents(".chart_holder").data('holder_id')
+      root = $('#' + holder_id).parents('.chart_holder')
+      root.find("a.table_format").hide()
+      root.find("a.chart_format").show()
+      @chart_holders[holder_id].view.toggle_format()
+      false
+
+    $(document).on "click", "a.chart_format", (e) =>
+      holder_id = $(e.target).parents(".chart_holder").data('holder_id')
+      root = $('#' + holder_id).parents('.chart_holder')
+      root.find("a.chart_format").hide()
+      root.find("a.table_format").show()
+      @chart_holders[holder_id].view.toggle_format()
       false

     $(document).on 'click', "a.pin_chart", (e) =>


make self-edges more prominent in graphs

- Edges in graphs which have the same
  source and target need to be displayed
  a little bit differently in order for them
  to be prominent and clickable.

--- app/assets/javascripts/plugins/graph.js.coffee
+++ app/assets/javascripts/plugins/graph.js.coffee
@@ -235,9 +235,14 @@
       path.attr("d", (d) ->
         dx = d.target.x - d.source.x
         dy = d.target.y - d.source.y
-        dr = Math.sqrt(dx*dx + dy*dy)*2
-        "M#{d.source.x},#{d.source.y}" +
-        "A#{dr},#{dr} 0 0,1 #{d.target.x},#{d.target.y}")
+        dr = Math.sqrt(dx+dx + dy*dy)*2
+        if d.source != d.target
+          "M#{d.source.x},#{d.source.y}" +
+          "A#{dr},#{dr} 0 0,1 #{d.target.x},#{d.target.y}"
+        else
+          "M#{d.source.x},#{d.source.y-0.6}" +
+          "A-30,-15 0 1,0 #{d.target.x-4},#{d.target.y}"
+      )
     force.on('tick', tick)

   drawGraph(nodes, edgesForMode(mode))


Improve error logging when executable not found

--- src/program.coffee
+++ src/program.coffee
@@ -201,7 +201,7 @@
     ensureExecutable: ->
         unless @isGuessed or @hasExecFile or @hasBinaryInPath
             unless @hasSrcFile
-                logger.e "Program #{styler.id @execFile} doesn't exist or isn't readable"
+                logger.e "Program #{styler.id @execFile} doesn't exist or isn't executable"
             else
                 logger.e "Program #{styler.id @execFile} isn't compiled"


Use Q for promise in Project::scan

Use Q for promise in Project::scan

--- src/project.coffee
+++ src/project.coffee
@@ -4,7 +4,6 @@
 Q = require 'q'

 _ = require './underscore-extensions'
-$ = require './jquery-extensions'
 telepath = require 'telepath'
 {Range} = telepath
 TextBuffer = require './text-buffer'
@@ -303,7 +302,7 @@
       iterator = options
       options = {}

-    deferred = $.Deferred()
+    deferred = Q.defer()

     searchOptions =
       ignoreCase: regex.ignoreCase
@@ -322,7 +321,7 @@
       task.on 'scan:paths-searched', (numberOfPathsSearched) ->
         options.onPathsSearched(numberOfPathsSearched)

-    deferred
+    deferred.promise

   # Private:
   buildEditSessionForBuffer: (buffer, editSessionOptions) ->


directly send the help message instead of use a function

--- src/sm-cmds.coffee
+++ src/sm-cmds.coffee
@@ -99,7 +99,7 @@
      robot.logger.debug( 'sm get incident callback function is called with:'+resp.message+ ' \n with auth.secrets:'+auth.secrets)
      match = /([\w\d]+)(?:\s+on\s+([\w\d]+))?/i.exec resp.match[1]
      if not match
-       sendHelp resp, room, ["Please use the correct syntax: 'sm get incident [ID]'.","To learn more about all supported commands, enter 'sm'."]
+       resp.send ["Please use the correct syntax: 'sm get incident [ID]'.","To learn more about all supported commands, enter 'sm'."].join("\r\n")
        return
      id=match[1]
      ins = match[2] or Config.get "sm.servers.default"


add a global _flux object (mainly useful for debugging)

--- salvus/page/flux.cjsx
+++ salvus/page/flux.cjsx
@@ -161,6 +161,9 @@

 flux = new AppFlux()

+if window?
+    window._flux = flux  # for convenience in the browser (mainly for debugging)
+
 FluxComponent = require('flummox/component')

 Flux = React.createClass


twid & twrc

twid & twrc

It should still work the same due to mod 10 for remainder calculation.

--- src/validid.coffee
+++ src/validid.coffee
@@ -163,16 +163,15 @@
       idLen = id.length
       # each letter represents value from [10..35]
       letters = 'ABCDEFGHJKLMNPQRSTUVXYWZIO'
-      letterIndex = letters.indexOf(id[0]) + 10
-      letterValue = Math.floor(letterIndex / 10) + (letterIndex % 10) * (idLen - 1)
+      letterIndex = letters.indexOf(id[0])
+      weightedSum = Math.floor(letterIndex / 10 + 1) + letterIndex * (idLen - 1)
       idTail = id.slice(1) # drop the letter
       weight = idLen - 2 # minus letter digit and check digit
-      weightedSum = 0
       for char in idTail
         weightedSum += +char * weight
         weight--
       # note: the check digit of 'id' is weighted 0
-      remainder = (letterValue + weightedSum + +id.slice(-1)) % 10
+      remainder = (weightedSum + +id.slice(-1)) % 10
       remainder is 0

     id = @tools.normalize(id)
@@ -192,10 +191,9 @@
       idLen = id.length
       # each letter represents value from [10..35]
       letters = 'ABCDEFGHJKLMNPQRSTUVXYWZIO'
-      letterIndex = letters.indexOf(id[0]) + 10
-      weightedSum = Math.floor(letterIndex / 10) + (letterIndex % 10) * (idLen - 1)
-      letter2Index = letters.indexOf(id[1])
-      weightedSum += letter2Index * (idLen - 2)
+      letterIndex = letters.indexOf(id[0])
+      weightedSum = Math.floor(letterIndex / 10 + 1) + letterIndex * (idLen - 1)
+      weightedSum += letters.indexOf(id[1]) * (idLen - 2)
       idTail = id.slice(2) # drop the letters
       weight = idLen - 3 # minus letter digit and check digit
       for char in idTail


Don't wrap JavaScript files inside assets/ dir.

--- src/watcher.coffee
+++ src/watcher.coffee
@@ -64,6 +64,10 @@
       # Find a compiler plugin that can handle this file extension
       ext = sysPath.extname(path)
       compiler = @compilerForExt(ext)
+
+      # Don't wrap JavaScript files inside the assets dir
+      if ext is '.js' and (path.indexOf(project.clientAssetsDir) > -1)
+        compiler = null

       if compiler
         # Let the compiler plugin handle it.


Compiler can find inline calls.

Compiler can find inline calls.

--- tools/Compiler.coffee
+++ tools/Compiler.coffee
@@ -1,23 +1,123 @@
-Parser  = require("coffee-script/lib/coffee-script/parser").parser
+Parser  = require "coffee-script/lib/coffee-script/parser"
 Command = require "coffee-script/lib/coffee-script/command"
+Nodes   = require "coffee-script/lib/coffee-script/nodes"

-parse = Parser.parse
-Parser.parse = (source) ->
+Access  = Nodes.Access
+Assign  = Nodes.Assign
+Base    = Nodes.Base
+Block   = Nodes.Block
+Call    = Nodes.Call
+Class   = Nodes.Class
+Code    = Nodes.Code
+Literal = Nodes.Literal
+Obj     = Nodes.Obj
+Value   = Nodes.Value
+
+parse = Parser.parser.parse
+baseContext = "__no_class__"
+
+Base::getChild = (id) ->
+    result = null
+    position = 0
+    @eachChild (child) ->
+        if position is id or child instanceof id
+            result = child
+            false
+        else
+            ++position
+    result
+
+Base::hasChild = (id) ->
+    @getChild(id) isnt null
+
+Base::getLiteralValue = ->
+    @getChild(Literal).value
+
+Base::isClass = ->
+    this instanceof Class
+
+Class::getName = ->
+    @getChild(Value).getLiteralValue()
+
+Class::getBody = ->
+    @getChild(Block).getChild(Value).getChild(Obj)
+
+Base::isFunction = ->
+    this instanceof Assign and @hasChild(Code)
+
+Assign::getName = ->
+    @getChild(Value).getLiteralValue()
+
+Assign::getBody = ->
+    @getChild(Code)
+
+Base::isCall = ->
+    this instanceof Call
+
+Call::isInlineCall = ->
+    (@isFunctionCall() or @isSelfMethodCall()) and @getBareName()[0] is "_"
+
+Call::getName = ->
+    if @isInlineCall()
+        @getBareName()[1..]
+    else
+        @getBareName()
+
+Call::getBareName = ->
+    if @isMethodCall()
+        @getMethodName()
+    else
+        @getFunctionName()
+
+Call::isFunctionCall = ->
+    @isCall() and @getMethod() is null
+
+Call::isMethodCall = ->
+    @isCall() and @getMethod() isnt null
+
+Call::isSelfMethodCall = ->
+    @isMethodCall() and @getMethodTarget() is "this"
+
+Call::getFunctionName = ->
+    @getFunction().getLiteralValue()
+
+Call::getFunction = ->
+    @getChild(0)
+
+Call::getMethodName = ->
+    @getMethod().getLiteralValue()
+
+Call::getMethodTarget = ->
+    @getChild(0).getLiteralValue()
+
+Call::getMethod = ->
+    @getChild(0).getChild(Access)
+
+Parser.parser.parse = (source) ->
     modifyAST parse.call this, source

 modifyAST = (ast) ->
+    functions = findFunctions ast
     level = 0
-    functions = findFunctions ast
     level++ while level < 10 and inlineFunctions ast, functions
     ast

-findFunctions = (ast) ->
-    functions = []
+findFunctions = (ast, functions = {}, context = baseContext) ->
+    functions[context] ?= {}
     ast.eachChild (node) ->
-        functions.push node.variable.base.value if node.value?.params?
+        if node.isClass()
+            findFunctions node.getBody(), functions, node.getName()
+        else if node.isFunction()
+            functions[context][node.getName()] = node.getBody()
     functions

-inlineFunctions = (ast, functions) ->
+inlineFunctions = (ast, functions, context = baseContext) ->
+    ast.traverseChildren true, (node) ->
+        if node.isClass()
+            inlineFunctions node.getBody(), functions, node.getName()
+        else if node.isCall() and node.isInlineCall()
+            context = baseContext unless node.isMethodCall()
+            # TODO inline it!
     false

 Command.run()


Avoid scrollbar mousedowns.

Avoid scrollbar mousedowns.

--- src/controller.coffee
+++ src/controller.coffee
@@ -222,33 +222,40 @@

       # ## Tracker Events
       # We allow binding to the tracker element.
+      dispatchEvent = (event) =>
+        # ignore mouse clicks that are not the left-button, and ignore
+        # them if they are on the scrollbars
+        if event.type in ['mousedown', 'dblclick', 'mouseup']
+          if event.which isnt 1 then return
+          if event.target in [@mainScroller, @paletteScroller] then return
+
+        trackPoint = @getPointRelativeToTracker event
+
+        # We keep a state object so that handlers
+        # can know about each other.
+        state = {}
+
+        # Call all the handlers.
+        for handler in editorBindings[event.type]
+          handler.call this, trackPoint, event, state
+
+        # Stop event propagation so that
+        # we don't get bad selections
+        event.stopPropagation?()
+        event.preventDefault?()
+
+        event.cancelBubble = true
+        event.returnValue = false
+
+        return false
+
       for eventName, elements of {
           mousedown: [@iceElement, @paletteElement, @dragCover]
           dblclick: [@iceElement, @paletteElement, @dragCover]
           mouseup: [window]
           mousemove: [window] } then do (eventName, elements) =>
-
         for element in elements
-          element.addEventListener eventName, (event) =>
-            trackPoint = @getPointRelativeToTracker event
-
-            # We keep a state object so that handlers
-            # can know about each other.
-            state = {}
-
-            # Call all the handlers.
-            for handler in editorBindings[eventName]
-              handler.call this, trackPoint, event, state
-
-            # Stop event propagation so that
-            # we don't get bad selections
-            event.stopPropagation?()
-            event.preventDefault?()
-
-            event.cancelBubble = true
-            event.returnValue = false
-
-            return false
+          element.addEventListener eventName, dispatchEvent

       # ## Document initialization
       # We start of with an empty document


Make TextBuffer::isModified public and improve docs

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -590,7 +590,10 @@
     @emitModifiedStatusChanged(false)
     @emit 'saved', this

-  # Identifies if the buffer was modified.
+  # Public: Determine if the in-memory contents of the buffer differ from its
+  # contents on disk.
+  #
+  # If the buffer is unsaved, always returns `true` unless the buffer is empty.
   #
   # Returns a {Boolean}.
   isModified: ->


Bind TextEditor behaviors to deprecated API flag

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -74,10 +74,6 @@
     'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
     toProperty: 'languageMode'

-  @delegatesProperties '$lineHeightInPixels', '$defaultCharWidth', '$height', '$width',
-    '$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft',
-    toProperty: 'displayBuffer'
-
   constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, @gutterVisible}) ->
     super

@@ -108,10 +104,10 @@

     @setEncoding(atom.config.get('core.fileEncoding', scope: @getRootScopeDescriptor()))

-    @subscribe @$scrollTop, (scrollTop) =>
+    @subscribe @displayBuffer.$scrollTop, (scrollTop) =>
       @emit 'scroll-top-changed', scrollTop if includeDeprecatedAPIs
       @emitter.emit 'did-change-scroll-top', scrollTop
-    @subscribe @$scrollLeft, (scrollLeft) =>
+    @subscribe @displayBuffer.$scrollLeft, (scrollLeft) =>
       @emit 'scroll-left-changed', scrollLeft if includeDeprecatedAPIs
       @emitter.emit 'did-change-scroll-left', scrollLeft

@@ -2837,6 +2833,10 @@
   logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)

 if includeDeprecatedAPIs
+  TextEditor.delegatesProperties '$lineHeightInPixels', '$defaultCharWidth', '$height', '$width',
+    '$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft',
+    toProperty: 'displayBuffer'
+
   TextEditor::getViewClass = ->
     require './text-editor-view'


Update: 必要ないModグループは作成しないように

Update: 必要ないModグループは作成しないように

--- src/core/plist.coffee
+++ src/core/plist.coffee
@@ -106,14 +106,14 @@
       plat = config.get("platform")
       obj = {}
       for k1, v1 of parsedObj
-        obj[k1] = {}
         for k2, v2 of v1
-          obj[k1][k2] = {}
           for k3, v3 of v2
             if (
               v3.version is ver and
               v3.platform.includes(plat)
             )
+              obj[k1] = {} if !obj[k1]?
+              obj[k1][k2] = {} if !obj[k1][k2]?
               obj[k1][k2][k3] = v3.name
       resolve(obj)
       return


Make markdown links always jump to the top (#5262)

--- app/views/teachers/MarkdownResourceView.coffee
+++ app/views/teachers/MarkdownResourceView.coffee
@@ -35,7 +35,7 @@
           else
             align = if me.get('preferredLanguage') in ['he', 'ar', 'fa', 'ur'] then 'left' else 'right'
             buttonText = $.i18n.t 'teacher.back_to_top'
-            "#{buttonText}</h5"
+            "#{buttonText}</h5"

       if @name is 'cs1'
         $('body').append($(""))


Further bounties debugging

Further bounties debugging

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -42,7 +42,7 @@

     else if inputFirst is 'vendor'
       for vendorHash in Constants.VENDORS
-        getVendorDetails(res, vendorHash).then (saleItemCategories) ->
+        getVendorDetails(res, vendorHash).then (vendorDetails) ->
           return
         , (err) ->
           sendError(robot, res, err)
@@ -59,6 +59,9 @@
       remainingIteration = vendors.length
       for vendorHash in vendors
         getVendorDetails(res, vendorHash).then (vendorDetails) ->
+
+          console.log vendorDetails.saleItemCategories
+
           for category in vendorDetails.saleItemCategories
             if category.categoryIndex in Constants.BOUNTIES_CATEGORY_INDICES
               bountyItems.push category.saleItems...


Add description

Add description

--- scripts/yamada.coffee
+++ scripts/yamada.coffee
@@ -1,3 +1,34 @@
+# Description:
+#   Yamada bot helps to enjoy slack life
+#
+# Dependencies:
+#   $ npm install
+#
+# Configuration:
+#   You need to set following environment variables
+#     HUBOT_SLACK_TOKEN
+#     HUBOT_TWITTER_CONSUMER_KEY
+#     HUBOT_TWITTER_CONSUMER_SECRET
+#     HUBOT_TWITTER_ACCESS_TOKEN_KEY
+#     HUBOT_TWITTER_ACCESS_TOKEN_SECRET
+#     HUBOT_TWITTER_MENTION_QUERY
+#     HUBOT_TWITTER_MENTION_ROOM
+#
+# Commands:
+#   yamada-bot help          -- Display this help
+#   yamada-bot ping          -- Check whether a bot is alive
+#   yamada-bot weather       -- Ask today's weather
+#   yamada-bot yahoo-news    -- Display current yahoo news highlight
+#   yamada-bot kindle        -- Display daily kindle sale book
+#   yamada-bot how are you?  -- Ask condition of the bot
+#   yamada-bot who are you?  -- Ask a bot name
+#   yamada-bot ping [IPADDR]         -- Execute ping [IPADDR] from bot server
+#   yamada-bot traceroute [IPADDR]   -- Execute traceroute [IPADDR] from bot server
+#
+# Author:
+#   noralife
+#
+
 cheerio = require('cheerio')
 cheerio-httpcli = require('cheerio-httpcli')


+meuh

+meuh

--- meteor/channels.coffee
+++ meteor/channels.coffee
@@ -31,7 +31,7 @@
     'fip': { url: 'http://audio.scdn.arkena.com/11016/fip-midfi128.mp3'}
     # 'tsfjazz': { url: 'http://tsfjazz.ice.infomaniak.ch:80/tsfjazz-high', tags: ['jazz', 'ads'] }
     'ledjam' : { url: 'http://ledjamradio.ice.infomaniak.ch/ledjamradio.mp3', fb: 'https://www.facebook.com/ledjamradio' }
-
+    'meuh' : { url: 'http://radiomeuh.ice.infomaniak.ch/radiomeuh-128.mp3' }
     'radiopanik': { url: 'http://streaming.domainepublic.net:8000/radiopanik.ogg', tags: ['libre', 'bx'] }
     'radioairlibre': { url: 'http://streaming.domainepublic.net:8000/radioairlibre.ogg', tags: ['libre', 'bxl'] }
     'radiocampusbxl': { url: 'http://streamer.radiocampusbruxelles.org:8000/stream.ogg', tags: ['bxl'], twitter: 'radiocampusbxl' }


Reuse Mongoose Connection For Sessions

Reuse Mongoose Connection For Sessions

--- server/middlewares.coffee
+++ server/middlewares.coffee
@@ -28,6 +28,7 @@
       secret: app.get('app config').express.sessionSecret
       store: new MongoStore
         db: app.get('app config').mongo.db
+        mongoose_connection: mongoose.connection

     app.use passport.initialize()
     app.use passport.session()


reset @initialized on package deactivation

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -36,6 +36,7 @@
       @initialize()

   deactivate: ->
+    @initialized = false
     @disable() if @enabled
     @subs.dispose()


use Maps in MarkLocalAndGlobalVariables

use Maps in MarkLocalAndGlobalVariables

--- lib/closure-conversion.coffee
+++ lib/closure-conversion.coffee
@@ -1208,12 +1208,11 @@
 class MarkLocalAndGlobalVariables extends TreeVisitor
         constructor: ->
                 # initialize the scope stack with the global (empty) scope
-                @scope_stack = new Stack Object.create null
+                @scope_stack = new Stack new Map

         findIdentifierInScope: (ident) ->
                 for scope in @scope_stack.stack
-                        if hasOwnProperty.call scope, ident.name
-                                return true
+                        return true if scope.has(ident.name)
                 return false

         intrinsicForIdentifier: (id) ->
@@ -1230,11 +1229,11 @@
                 rv

         visitVariableDeclarator: (n) ->
-                @scope_stack.top[n.id.name] = true
+                @scope_stack.top.set(n.id.name, true)
                 n

         visitImportSpecifier: (n) ->
-                @scope_stack.top[n.id.name] = true
+                @scope_stack.top.set(n.id.name, true)
                 n

         visitObjectExpression: (n) ->
@@ -1268,7 +1267,7 @@
                         n

         visitBlock: (n) ->
-                n.body = @visitWithScope (Object.create null), n.body
+                n.body = @visitWithScope(new Map(), n.body)
                 n

         visitCallExpression: (n) ->
@@ -1301,10 +1300,10 @@
                 n

         visitFunction: (n) ->
-                new_scope = Object.create null
-                new_scope[p.name] = true for p in n.params
-                new_scope[n.rest.name] = true if n.rest?
-                new_scope["arguments"] = true
+                new_scope = new Map
+                new_scope.set(p.name, true) for p in n.params
+                new_scope.set(n.rest.name, true) if n.rest?
+                new_scope.set("arguments", true)
                 new_body = @visitWithScope new_scope, n.body
                 n.body = new_body
                 n
@@ -1314,8 +1313,8 @@
                 n

         visitCatchClause: (n) ->
-                new_scope = Object.create null
-                new_scope[n.param.name] = true
+                new_scope = new Map
+                new_scope.set(n.param.name, true)
                 n.guard = @visit n.guard
                 n.body = @visitWithScope new_scope, n.body
                 n


Follow mixin

Follow mixin

--- test/follow.coffee
+++ test/follow.coffee
@@ -16,7 +16,7 @@
   enemy.include(Follow)
   enemy.follow(player)

-  ok enemy.I.velocity.equal(Point(1, 0)), 'enemy should head '
+  ok enemy.I.velocity.equal(Point(1, 0)), 'enemy should head toward '

   fastEnemy = GameObject
     x: 50


Follow mixin

Follow mixin

--- test/follow.coffee
+++ test/follow.coffee
@@ -17,6 +17,6 @@

   enemy.follow(player)

-  equ
+  equals

 module()


Increased the default test timeout.

Increased the default test timeout.

--- test/runner.coffee
+++ test/runner.coffee
@@ -27,7 +27,7 @@
   console.log 'ERROR: ' + msg

 # used for waiting until the tests finish running
-waitFor = (testFn, onReady, timeout=3000) ->
+waitFor = (testFn, onReady, timeout=4000) ->
   start = new Date()
   interval = setInterval ->
     if testFn()


Robust downloading

Robust downloading

--- server/methods.coffee
+++ server/methods.coffee
@@ -24,16 +24,30 @@
   })
   [s3Bucket, s3Client]

+downloadFile = (url) ->
+  logger.debug("Downloading file '#{file.url}'...")
+  numTries = 0
+  while true
+    numTries += 1
+    try
+      result = HTTP.get(file.url)
+      if result.statusCode != 200
+        throw new Error("Couldn't download '#{file.url}', status code: #{result.statusCode}")
+      else
+        break
+    catch
+      if numTries >= 3
+        throw
+
+  result.content
+
 createZip = (files, user, id, s3Bucket, s3Client) ->
   logger.debug("Generating zip...")
   zip = new JSZip()
   for file in files
-    logger.debug("Downloading file '#{file.url}'...")
-    result = Meteor.http.get(file.url)
-    if result.statusCode != 200
-      throw new Error("Couldn't download '#{file.url}', status code: #{result.statusCode}")
+    content = downloadFile(file)
     logger.debug("Adding file '#{file.name}' to zip")
-    zip.file(file.name, result.content)
+    zip.file(file.name, content)
   output = zip.generate({
     type: "nodebuffer",
     compression: "DEFLATE",


fixed the parent_id of undefined bug, now you can put in a child post id in the browser and see the entire thread

--- server/publish.coffee
+++ server/publish.coffee
@@ -96,14 +96,14 @@

   user_id = @userId()
   if user_id?
-  #all the parents and clindren if you have replied.
+  #all the parents of the post
     ids = []
     ids.push( post_id )
     this_post = post_id
     while Posts.findOne( this_post ).parent_id?
-      this_post = Posts.findOne( post_id ).parent_id
+      this_post = Posts.findOne( this_post ).parent_id
       if this_post not in ids
-        ids.push( parent_id )
+        ids.push( this_post )

     #also add child posts if user has replied
     children = Posts.find( 'parent_id': post_id ).fetch()


remove todo

remove todo

--- lib/runtime/evaluation.coffee
+++ lib/runtime/evaluation.coffee
@@ -12,7 +12,6 @@

 module.exports =

-  # TODO: make the mark first and attach the result later
   eval: ({move}={}) ->
     editor = atom.workspace.getActiveTextEditor()
     mod = modules.current() # TODO: may not work in all cases


use prefix to reduce redundancies

use prefix to reduce redundancies

--- lib/status-bar-manager.coffee
+++ lib/status-bar-manager.coffee
@@ -13,7 +13,7 @@
 class StatusBarManager
   constructor: ->
     @element = document.createElement("div")
-    @element.id = "status-bar-vim-mode"
+    @prefix = @element.id = "status-bar-vim-mode"

     @container = document.createElement("div")
     @container.className = "inline-block"
@@ -25,7 +25,7 @@
     mode += ".#{submode}" if submode?
     if newContents = ContentsByMode[mode]
       [klass, text] = newContents
-      @element.className = "status-bar-vim-mode-#{klass}"
+      @element.className = "{@prefix}-#{klass}"
       @element.textContent = text

   # Private


unessential code changes in the plumbs.accepts

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -180,18 +180,26 @@
 # very useful to use this method to negiotate the content type field.
 # This is a very dummy way of asking if a client supports something,
 # for a propert content negotiation please see the `send` plumbing.
-module.exports.accepts = (kernel) ->
-    (request, response, next) ->
-        response.accepts = (mimes...) ->
-            handles = (pattern) -> pattern.test accept
-            patternize = (s) -> new RegExp RegExp.escape s
-            accept = do -> request?.headers?.accept or ""
-            regexps = _.filter mimes, (sx) ->_.isRegExp sx
-            strings = _.filter mimes, (sx) ->_.isString sx
-            strings = _.map strings, patternize.bind this
-            assert merged = _.merge(regexps, strings) or []
-            return _.find(merged, handles) or undefined
-        next() unless request.headersSent
+module.exports.accepts = (kernel) -> (request, response) ->
+    noHeaders = "unable to locate request headers"
+    terribles = "got no valid response object found"
+    assert _.isObject(kernel), "no kernel supplied"
+    assert _.isObject(request?.headers), noHeaders
+    assert _.isObject(response or null), terribles
+    response.accepts = (mimes...) -> # response func
+        handles = (pattern) -> try pattern.test accept
+        patternize = (s) -> new RegExp RegExp.escape s
+        accept = do -> request?.headers?.accept or ""
+        regexps = _.filter mimes, (sx) ->_.isRegExp sx
+        strings = _.filter mimes, (sx) ->_.isString sx
+        strings = _.map strings, patternize.bind this
+        assert merged = try _.merge(regexps, strings)
+        return _.find(merged, handles) or undefined
+    assert unix = moment().unix().toString().bold
+    message = "Running accepting middleware at U=%s"
+    logger.debug message.toString(), unix.toString()
+    assert _.isFunction next = _.last arguments
+    return next() unless request.headersSent

 # A middeware that makes possible external specification of session
 # bearer via HTTP headers. This basically means - it allows for you


Gave db.models a default in case of bad ORM lib

--- node_modules/radedit/lib/db.coffee
+++ node_modules/radedit/lib/db.coffee
@@ -1,7 +1,7 @@
 orm = require 'orm'
 global.db  = orm.connect config.db
-
-
+if not db.models
+	db.models = {}

 # Allow callbacks to run on successful connect.
 db.connect = (callback) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/animated.coffee
+++ src/animated.coffee
@@ -36,6 +36,7 @@
       }
       frames: [0]
     currentFrameIndex: 0
+    debugAnimation: true
     lastUpdate: new Date().getTime()
     useTimer: false
     transform: Matrix.IDENTITY
@@ -67,7 +68,7 @@
       I.data.tileset.each (spriteData, i) ->
         I.spriteLookup[i] = Sprite.fromURL(spriteData.src)
   else
-    throw "No animation data provided. Either use animationName to specify an animation to load from the project or pass in raw JSON to the data key."
+    throw "No animation data provided. Use animationName to specify an animation to load from the project or pass in raw JSON to the data key."

   advanceFrame = ->
     frames = I.activeAnimation.frames
@@ -79,16 +80,19 @@

       if nextState
         I.activeAnimation = find(nextState) || I.activeAnimation
-        I.width = I.spriteLookup[I.activeAnimation.frames.first()].width
-        I.height = I.spriteLookup[I.activeAnimation.frames.first()].height
+        sprite = I.spriteLookup[I.activeAnimation.frames.first()]
+
+        I.width = sprite.width
+        I.height = sprite.height

     I.currentFrameIndex = I.activeAnimation.frames[(frames.indexOf(I.currentFrameIndex) + 1) % frames.length]

   find = (name) ->
     result = null
+    nameLower = name.toLowerCase()

     I.data.animations.each (animation) ->
-      result = animation if animation.name.toLowerCase() == name.toLowerCase()
+      result = animation if animation.name.toLowerCase() == nameLower

     return result

@@ -98,7 +102,9 @@

   transition: (newState) ->
     return if newState == I.activeAnimation.name
-    return unless I.activeAnimation.interruptible
+    unless I.activeAnimation.interruptible
+      warn "Cannot transition to '#{newState}' because '#{I.activeAnimation.name}' is locked" if I.debugAnimation
+      return

     nextState = find(newState)

@@ -110,26 +116,25 @@
       I.currentFrameIndex = firstFrame
       I.width = firstSprite.width
       I.height = firstSprite.height
+    else
+      warn "Could not find animation state '#{newState}'. The current transition will be ignored" if I.debugAnimation

-  transform: ->
-    I.transform
+  transform: -> I.transform

   before:
-    update: () ->
+    update: ->
       if I.useTimer
         time = new Date().getTime()

-        updateFrame = (time - I.lastUpdate) >= I.activeAnimation.speed
-
-        if updateFrame
+        if updateFrame = (time - I.lastUpdate) >= I.activeAnimation.speed
           I.lastUpdate = time
-          if I.activeAnimation.triggers && I.activeAnimation.triggers[I.currentFrameIndex]
+          if I.activeAnimation.triggers?[I.currentFrameIndex]
             I.activeAnimation.triggers[I.currentFrameIndex].each (event) ->
               self.trigger(event)

           advanceFrame()
       else
-        if I.activeAnimation.triggers && I.activeAnimation.triggers[I.currentFrameIndex]
+        if I.activeAnimation.triggers?[I.currentFrameIndex]
           I.activeAnimation.triggers[I.currentFrameIndex].each (event) ->
             self.trigger(event)


fix(model): show category for lookup

fix(model): show category for lookup

--- src/api/mods.coffee
+++ src/api/mods.coffee
@@ -113,7 +113,7 @@
       # Validate options
       Mod = mongoose.model "Mod"
       query = Mod.findOne({slug: slug})
-      query.select("name slug body description summary comments logo created updatedAt author")
+      query.select("name slug body description summary comments logo created updatedAt author category")
       query.populate("author", "name")
       query.populate("comments.author", "username")
       query.lean()
@@ -246,6 +246,7 @@

 exports.put = (userid, slug, body) ->
   deferred = Q.defer()
+  console.log body
   body = _.pick body, ['name', 'body', 'summary', 'category']
   canThis(userid, "mod", "browse").then (can)->
     # Validate options


add basic patch upload

add basic patch upload

--- src/api/pool.coffee
+++ src/api/pool.coffee
@@ -2,7 +2,7 @@

 #=====================================================================

-exports.set = ->
+exports.set = (params) ->
   try
     pool = @getObject params.id, 'pool'
   catch
@@ -29,3 +29,32 @@
   name_description:
     type: 'string'
     optional: true
+
+# FIXME
+exports.patch = ({pool}) ->
+  try
+    pool = @getObject pool, 'pool'
+  catch
+    @throw 'NO_SUCH_OBJECT'
+
+  {sessionId} = @getXAPI pool
+  host = @getObject pool.master, 'host'
+
+  url = $wait @registerProxyRequest {
+    # Receive a POST but send a PUT.
+    method: 'put'
+    proxyMethod: 'post'
+    hostname: host.address
+    pathname: '/pool_patch_upload'
+    query: {
+      session_id: sessionId
+    }
+  }
+
+  return {
+    $sendTo: url
+  }
+exports.patch.permission = 'admin'
+exports.patch.params = {
+  pool: { type: 'string' }
+}


button changes

button changes

--- Framework/Views/Controls/Button_Framework.coffee
+++ Framework/Views/Controls/Button_Framework.coffee
@@ -71,7 +71,9 @@
         shadowOffset: { x: 1, y: 1 }
         shadowColor: @startStyle("labelShadowColor")
         textAlign: "center"
-        height: Ti.UI.FILL
+        height: Ti.UI.SIZE
+        width: Ti.UI.SIZE
+        backgroundColor: "transparent"
       })
       @content.add(@label)


Clear button is hidden on readonly, resolves #244

--- wzk/ui/ClearableInput.coffee
+++ wzk/ui/ClearableInput.coffee
@@ -40,7 +40,8 @@
     goog.style.setElementShown @clrBtn.getElement(), false

   showClearButton: ->
-    goog.style.setElementShown @clrBtn.getElement(), true
+    unless @getElement().getAttribute("readonly")
+      goog.style.setElementShown @clrBtn.getElement(), true

   ###*
     @override


Dont require exporting of sibling module

--- src/compiler.coffee
+++ src/compiler.coffee
@@ -6117,8 +6117,9 @@
           toTypedModulePath
         else if i < fromModulePath.length
           # From parent
-          types: join fromTypes[i - 1...fromModulePath.length], toTypes[i...]
-          names: join ('..' for [i..fromModulePath.length]), toModulePath[i...]
+          parentTypes = fromTypes[i - 1...fromModulePath.length - 1]
+          types: join parentTypes, toTypes[i...]
+          names: join ('..' for _ in parentTypes), toModulePath[i...]
         else
           # From current
           types: toTypes[i - 1...]
@@ -6129,13 +6130,16 @@
     if (from = fromModulePath[i]) and from isnt moduleName
       if i is 0 # Top modules are always public
         return false
-      checkedModuleName = (modulePathToName toModulePath[0..i])
-      module = (lookupInMap moduleGraph, checkedModuleName)
-      if not module
-        throw new Error "missing module #{checkedModuleName}
-         that should have been compiled for #{moduleName}"
-      if not module.typedModulePath.exported
-        return checkedModuleName
+      if insideSiblings
+        checkedModuleName = (modulePathToName toModulePath[0..i])
+        module = (lookupInMap moduleGraph, checkedModuleName)
+        if not module
+          throw new Error "missing module #{checkedModuleName}
+           that should have been compiled for #{moduleName}"
+        if not module.typedModulePath.exported
+          return checkedModuleName
+      else
+        insideSiblings = yes
   false

 lookupCompiledModule = (name) ->


Histories is no longer promise-gated

Histories is no longer promise-gated

--- src/coffee/controllers/starting-points.coffee
+++ src/coffee/controllers/starting-points.coffee
@@ -30,9 +30,9 @@

     scope.anyToolDocked = -> L.some scope.startingPoints, state: 'DOCKED'

-    scope.$on 'start-history', (evt, step) -> Histories.then (histories) ->
-      history = histories.create {title: "Un-named history"}, ->
-        step = histories.append {id: history.id}, step, ->
+    scope.$on 'start-history', (evt, step) ->
+      history = Histories.create {title: "Un-named history"}, ->
+        step = Histories.append {id: history.id}, step, ->
           console.log("Created history " + history.id + " and step " + step.id)
           location.url "/history/#{ history.id }/1"


update existing exercise component tests for multipart changes

--- test/components/exercise/index.spec.coffee
+++ test/components/exercise/index.spec.coffee
@@ -8,7 +8,6 @@
 props = null

 FREE_RESPONSE_PROPS =
-  id: '1'
   taskId: '1'
   onStepCompleted: sinon.spy()
   onNextStep: sinon.spy()
@@ -42,17 +41,17 @@
 resetProps = ->
   step = _.clone(STEP)
   props = _.clone(FREE_RESPONSE_PROPS)
-  props.step = step
+  props.parts = [step]

 exerciseActionsAndChecks =
   enterFreeResponse: ({dom, wrapper, element}, freeResponse = 'HELLO') ->
     {textarea} = Testing.actions._fillTextarea('textarea', freeResponse, {div: dom})
     expect(textarea.value).equals(freeResponse)
-    expect(props.onFreeResponseChange).to.have.been.calledWith(freeResponse)
+    expect(props.onFreeResponseChange).to.have.been.calledWith(step.id, freeResponse)

   continueOnFreeResponse: ({dom, wrapper, element}, freeResponse = 'HELLO') ->
     Testing.actions._clickMatch('.continue', {div: dom})
-    expect(props.setFreeResponseAnswer).to.have.been.calledWith('1', freeResponse)
+    expect(props.setFreeResponseAnswer).to.have.been.calledWith(step.id, freeResponse)
     expect(step.free_response).equals(freeResponse)

     wrapper.setProps({})
@@ -74,11 +73,11 @@

     Testing.actions._changeDOMNode(choicesDOMs[FIRST_CHOICE_INDEX])
     expect(_.pluck(dom.querySelectorAll('.answer-checked'), 'textContent')).to.deep.equal([CHOICES[FIRST_CHOICE_INDEX].content_html])
-    expect(props.setAnswerId).to.have.been.calledWith('1', CHOICES[FIRST_CHOICE_INDEX].id)
+    expect(props.setAnswerId).to.have.been.calledWith(step.id, CHOICES[FIRST_CHOICE_INDEX].id)

     Testing.actions._changeDOMNode(choicesDOMs[SECOND_CHOICE_INDEX])
     expect(_.pluck(dom.querySelectorAll('.answer-checked'), 'textContent')).to.deep.equal([CHOICES[SECOND_CHOICE_INDEX].content_html])
-    expect(props.setAnswerId).to.have.been.calledWith('1', CHOICES[SECOND_CHOICE_INDEX].id)
+    expect(props.setAnswerId).to.have.been.calledWith(step.id, CHOICES[SECOND_CHOICE_INDEX].id)

   setCorrectAnswerAndFeedback: (renderedData, choiceIndex = 0) ->
     {wrapper, dom} = renderedData


cooldown finilazation

cooldown finilazation

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -1,5 +1,5 @@
 ###*
-The Cooldown module provides a declarative way to manage cooldowns
+The Cooldown module provides a declarative way to manage cooldowns on
 GameObjects based on the velocity and acceleration. It does not check
 collisions so is probably best suited to particle effect like things.


Export ConsoleReporter

Export ConsoleReporter

--- ConsoleReporter.coffee
+++ ConsoleReporter.coffee
@@ -12,9 +12,6 @@
     try
       log.enter('constructor')
       super(@clientRunner, @serverRunner, @options)
-
-      @registerRunnerEvents('client')
-      @registerRunnerEvents('server')
       MochaRunner.on "end all", => @finishAndPrintTestsSummary()

     finally
@@ -90,5 +87,8 @@
     finally
       log.return()

+
+module.exports.ConsoleReporter = ConsoleReporter
 module.exports.runTests = () ->
   MochaRunner.setReporter(ConsoleReporter)
+


period should be enabled for checking and unchecking unless it is visible to students

--- src/components/task-plan/builder/tasking.cjsx
+++ src/components/task-plan/builder/tasking.cjsx
@@ -43,12 +43,13 @@
         <TaskingDateTimes {...@props} taskingIdentifier={taskingIdentifier}/>
     else
       if period?
+        # if isVisibleToStudents, we cannot re-enable this task for the period.
         <BS.Row key="tasking-disabled-#{period.id}" className="tasking-plan disabled">
           <BS.Col sm=12>
             <input
               id={"period-toggle-#{period.id}"}
               type='checkbox'
-              disabled={not isVisibleToStudents}
+              disabled={isVisibleToStudents}
               onChange={_.partial(togglePeriodEnabled, period)}
               checked={false}/>
             <label className="period" htmlFor={"period-toggle-#{period.id}"}>{period.name}</label>


should update defaults to true if undefined

--- src/components/task-teacher-review/index.cjsx
+++ src/components/task-teacher-review/index.cjsx
@@ -60,7 +60,10 @@
     # @setStepKey()

   shouldComponentUpdate: (nextProps) ->
-    nextProps.shouldUpdate
+    {shouldUpdate} = nextProps
+    shouldUpdate ?= true
+
+    shouldUpdate

   goToStep: (stepKey) ->
     params = _.clone(@context.router.getCurrentParams())


Fixes applySpawn in task utils

Fixes applySpawn in task utils

--- tasks/utils.coffee
+++ tasks/utils.coffee
@@ -8,18 +8,21 @@
       .then (results...) -> cb null, results...
       .catch cb

-applySpawn = (cmd, args, opts = {}) ->
+applySpawn = (cmd, params, opts = {}) ->
   (cb) ->
     opts.stdio = if args.verbose then 'inherit' else 'ignore'
-    proc = cp.spawn cmd, args, opts
+    child = cp.spawn cmd, params, opts
     if cb
       errored = false
-      proc.on 'error', (err) ->
+      child.on 'error', (err) ->
         errored = true
         cb(err)
-      proc.on 'close', (code) ->
+      child.on 'close', (code) ->
         unless errored
-          cb (if code then "`#{cmd}` exited with code #{code}" else null)
+          if code
+            cb "`#{cmd} #{params.join(' ')}` exited with code #{code}"
+          else
+            cb null

 platform = () ->
   if process.platform == 'win32'


ListView

ListView

- Fixed "abortLoading" and "removeFromDOM" during load.

--- src/elements/ListView/ListViewTreeNode.coffee
+++ src/elements/ListView/ListViewTreeNode.coffee
@@ -302,6 +302,10 @@
 			return
 		CUI.error("ListViewTreeNode.abortLoading: Aborting chunk loading.")
 		@__loadingDeferred.reject()
+		@__loadingDeferred = null
+		return
+
+	__open_counter: 0

 	# resolves with the opened node
 	open: ->
@@ -309,8 +313,17 @@
 		# we could return loading_deferred here
 		assert(not @isLoading(), "ListViewTreeNode.open", "Cannot open node #{@getUniqueId()}, during opening. This can happen if the same node exists multiple times in the same tree.", node: @)

+		@__open_counter++
+
+		open_counter = @__open_counter
+
+		# console.error @getUniqueId(), "opening...", "is open:", @is_open, open_counter
+
 		if @is_open
 			return CUI.resolvedPromise()
+
+		@is_open = true
+		@do_open = false

 		dfr = @__loadingDeferred = new CUI.Deferred()

@@ -330,8 +343,6 @@
 			# console.debug @._key, @getUniqueId(), "children loaded", @children.length

 			if @children.length == 0
-				@is_open = true
-				@do_open = false
 				if not @isRoot()
 					@replaceSelf()
 				do_resolve()
@@ -349,17 +360,25 @@
 						chunk_size: 1
 						timeout: -1
 						call: (_items) =>
-							if @__loadingDeferred.state() == "rejected"
+							# console.error @getUniqueId(), open_counter, @__open_counter, "chunking work"
+							if open_counter < @__open_counter
+								# we are already in a new run, exit
 								return false
 							@__appendNode(_items[0], true) # , false, true))

 			.done =>
-				@is_open = true
-				@do_open = false
+				# console.error @getUniqueId(), open_counter, @__open_counter, "chunking work DONE"
+				if open_counter < @__open_counter
+					return
+
 				if not @isRoot()
 					@replaceSelf()
 				do_resolve()
 			.fail =>
+				# console.error @getUniqueId(), open_counter, @__open_counter, "chunking work FAIL"
+				if open_counter < @__open_counter
+					return
+
 				for c in @children
 					c.removeFromDOM()
 				do_reject()
@@ -379,8 +398,9 @@
 					assert(isPromise(ret), "#{getObjectClass(@)}.open", "returned children are not of type Promise or Array", children: ret)
 					ret
 					.done (@children) =>
-						if @__loadingDeferred.state() == "pending"
-							load_children()
+						if open_counter < @__open_counter
+							return
+						load_children()
 						return
 					.fail(do_reject)
 			else


Support for having the recordTypeName be either a function or string

--- lib/generators/nas_extjs/templates/public/app/model/Base.coffee
+++ lib/generators/nas_extjs/templates/public/app/model/Base.coffee
@@ -37,8 +37,17 @@
         else
             prx.setAssociations = names.slice(0)

+    recordTypeName: ->
+        Util.baseClassName(this)
+
+    getTitle:->
+        this.toString()
+
+    _getRecordTypeName:->
+        if Ext.isFunction( this.getRecordTypeName ) then this.getRecordTypeName() else this.getRecordTypeName
+
     toString:->
-        Util.baseClassName(this) + ' ' + ( if this.phantom then "(new)" else String(this.recordIdentifier()) )
+        this._getRecordTypeName() + ' ' + ( if this.phantom then "(new)" else String(this.recordIdentifier()) )

     copyFrom: (sourceRecord) ->
         this.callParent( arguments )


test syncType in RequestParser

test syncType in RequestParser

--- test/unit/coffee/RequestParserTests.coffee
+++ test/unit/coffee/RequestParserTests.coffee
@@ -242,3 +242,13 @@
 		it "should return an error", ->
 			@callback.calledWith("relative path in root resource")
 				.should.equal true
+
+	describe "with an unknown syncType", ->
+		beforeEach ->
+			@validRequest.compile.options.syncType = "unexpected"
+			@RequestParser.parse @validRequest, @callback
+			@data = @callback.args[0][1]
+
+		it "should return an error", ->
+			@callback.calledWith("syncType attribute should be one of: full, incremental")
+				.should.equal true


added test to check locationChangeSuccess triggers selected item

--- test/unit/factory/menu-factory-spec.coffee
+++ test/unit/factory/menu-factory-spec.coffee
@@ -4,6 +4,7 @@
   'factory/menu-factory'
 ], (cfg, A) ->
   describe 'Menu factory', () ->
+    $location = null
     $q = null
     $rootScope = null
     factory = null
@@ -11,6 +12,7 @@
     beforeEach module cfg.ngApp

     beforeEach inject ($injector) ->
+      $location = $injector.get '$location'
       $q = $injector.get '$q'
       $rootScope = $injector.get '$rootScope'
       factory = $injector.get 'menuFactory'
@@ -55,4 +57,21 @@

       factory.set dfd, data

+      $rootScope.$digest()
+
+    it 'should update the selected item if the location changes', () ->
+      dfd = $q.defer()
+
+      dfd.promise.then () ->
+        items = factory.get()
+        item = items[2]
+
+        $location.path item.href
+
+        $rootScope.$emit '$locationChangeSuccess'
+
+        expect(factory.selectedItem.href).toBe item.href
+
+      factory.set dfd, data
+
       $rootScope.$digest()


Client: Sidebar: Added TODO for deleting forceUpdate parameter when SharedMachineInvitation and CollaborationInvitation event listeners multi-trigger problem fixed.

--- client/app/lib/flux/environment/actions.coffee
+++ client/app/lib/flux/environment/actions.coffee
@@ -37,6 +37,13 @@
       return loadMachines()  unless newMachine

       reactor.dispatch actions.MACHINE_UPDATED, { id, machine: newMachine }
+
+
+  # TODO: when a machine shared/collaborate, SharedMachineInvitation and
+  # CollaborationInvitation event listeners trigger two times.
+  # szkl can you check it why trigger these events two times?
+  # When we fix it this problem, gokhansongul will create a new pr for deleting
+  # forceUpdate parameter from ActiveInvitationMachineIdStore setMachineId event handler

   kd.singletons.notificationController


hub : do a better job of starting querying the db for stats (only after connecting to the db)

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -1398,9 +1398,6 @@
             if not err
                 _server_stats_cache = stats

-# Update stats every minute
-setInterval(update_server_stats, 60*1000)
-setTimeout(update_server_stats, 5000)  # and a few seconds after startup (no harm if this fails)

 ##-------------------------------
 #
@@ -4248,11 +4245,22 @@
     init_http_server()
     winston.info("Using Cassandra keyspace #{program.keyspace}")
     hosts = program.database_nodes.split(',')
-    database = new cass.Salvus(hosts:hosts, keyspace:program.keyspace)
-    init_sockjs_server()
-    init_stateless_exec()
-    http_server.listen(program.port, program.host)
-    winston.info("Started hub. HTTP port #{program.port}; keyspace #{program.keyspace}")
+
+    # Once we connect to the database, start serving.
+
+    new cass.Salvus
+        hosts    : hosts
+        keyspace : program.keyspace
+        cb       : (err, _db) ->
+            database = _db
+
+            # start updating stats every minute
+            update_server_stats(); setInterval(update_server_stats, 60*1000)
+
+            init_sockjs_server()
+            init_stateless_exec()
+            http_server.listen(program.port, program.host)
+            winston.info("Started hub. HTTP port #{program.port}; keyspace #{program.keyspace}")

 #############################################
 # Process command line arguments


Use notifications in the "postmessage" controller

--- testApp/app/controllers/postmessage.coffee
+++ testApp/app/controllers/postmessage.coffee
@@ -9,7 +9,7 @@

     window.addEventListener "message", receiveMessage

-    alert "Listening for messages and appending them to this document."
+    notification "Listening for messages and appending them to this document."

   @testSendingUncommonCharacters: ->
     msg = {text: "%=!=€%&hay'jay  ____dolan"}


EDSC-317: keep gibs_accuracy popover from going off the top of the window

--- app/assets/javascripts/modules/help.js.coffee
+++ app/assets/javascripts/modules/help.js.coffee
@@ -28,7 +28,7 @@
                 In either case, we may present imagery collected from an overlapping granule collected within
                 approximately one day of the indicated granule.
                 '
-      placement: 'top'
+      placement: 'left'
       element: '#map-center'

   tour = [{


[app][main] removed stripTemplate method

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -55,13 +55,6 @@
       @emit 'GroupChanged', group
       appManager.tell app, 'setGroup', group
     appManager.tell app, 'handleQuery', query
-
-  stripTemplate =(str, konstructor)->
-    {slugTemplate} = konstructor
-    slugStripPattern = /^(.+)?(#\{slug\})(.+)?$/
-    re = RegExp slugTemplate.replace slugStripPattern,
-      (tmp, begin, slug, end)-> "^#{begin ? ''}(.*)#{end ? ''}$"
-    str.match(re)?[1]

   handleNotFound:(route)->


still set originalStart when start is absent

fixes CNVS-25122

set it to null, instead of skipping the assignment

test-plan:
 - create an dated event (start is present)
 - edit the event to make it undated (start becomes absent)
 - after event is saved to server, it should properly move to the
   undated section of the page

Change-Id: I6a7dc6d60d6ef072b6785f373feb13bb01cebcfa
Reviewed-on: https://gerrit.instructure.com/67548
Reviewed-by: Matthew Wheeler <ce71fbafda91d6c8f12582e1837687d5fe592dd9@instructure.com>
Tested-by: Jenkins
QA-Review: Derek Hansen <cb1116771307a93b98b6babf698410ed3846296d@instructure.com>
Product-Review: Jacob Fugal <f862f167b85d41b225785c70d70808bc7337c1fe@instructure.com>

--- app/coffeescripts/calendar/CommonEvent.coffee
+++ app/coffeescripts/calendar/CommonEvent.coffee
@@ -90,7 +90,7 @@
       @start && @start < fcUtil.now()

     copyDataFromObject: (data) ->
-      @originalStart = fcUtil.clone(@start) if @start
+      @originalStart = (fcUtil.clone(@start) if @start)
       @midnightFudged = false # clear out cached value because now we have new data
       if @isDueAtMidnight()
         @midnightFudged = true


Update ArgumentVotes when the argument changes

--- core/app/backbone/models/argument_votes.coffee
+++ core/app/backbone/models/argument_votes.coffee
@@ -7,6 +7,9 @@

   initialize: (attributes, options) ->
     @_argument = options.argument
+
+    @_argument.on 'change:argument_votes', =>
+      @set @_argument.get('argument_votes')

   url: ->
     @_argument.url() + '/opinion'


fixed typo

fixed typo

--- core/app/backbone/views/activities_view.coffee
+++ core/app/backbone/views/activities_view.coffee
@@ -58,7 +58,7 @@
     childView.close() for childView in @childViews
     @childViews = []

-  beforeClose: -> @closeChildviews()
+  beforeClose: -> @closeChildViews()

   appendHtml: (collectionView, childView, index) ->


passport: dropbox support

passport: dropbox support

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -449,7 +449,6 @@
                     callbackURL  : "#{auth_url}/#{strategy}/return"

                 verify = (accessToken, refreshToken, profile, done) ->
-                    console.log("#{strategy} auth: accessToken=",accessToken, " profile=", profile, "refreshToken=", refreshToken)
                     done(undefined, {profile:profile})
                 passport.use(new PassportStrategy(opts, verify))

@@ -461,7 +460,6 @@
                 app.get "/auth/#{strategy}", passport.authenticate(strategy, {'scope': 'openid email profile'})

                 app.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
-                    console.log("/auth/#{strategy}/return")
                     res.json(req.user)

                 cb()
@@ -487,18 +485,15 @@
                     callbackURL  : "#{auth_url}/#{strategy}/return"

                 verify = (accessToken, refreshToken, profile, done) ->
-                    console.log("#{strategy} auth: accessToken=",accessToken, " profile=", profile, "refreshToken=", refreshToken)
                     done(undefined, {profile:profile})
                 passport.use(new PassportStrategy(opts, verify))

                 app.get "/auth/#{strategy}", passport.authenticate(strategy)

                 app.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
-                    console.log("/auth/#{strategy}/return")
                     res.json(req.user)

                 cb()
-

         init_facebook = (cb) ->
             dbg("init_facebook")
@@ -525,19 +520,52 @@
                     enableProof  : false

                 verify = (accessToken, refreshToken, profile, done) ->
-                    console.log("#{strategy} auth: accessToken=",accessToken, " profile=", profile, "refreshToken=", refreshToken)
                     done(undefined, {profile:profile})
                 passport.use(new PassportStrategy(opts, verify))

                 app.get "/auth/#{strategy}", passport.authenticate(strategy)

                 app.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
-                    console.log("/auth/#{strategy}/return")
                     res.json(req.user)

                 cb()

-        async.parallel([init_local, init_google, init_github, init_facebook], cb)
+        init_dropbox = (cb) ->
+            dbg("init_dropbox")
+            PassportStrategy = require('passport-dropbox-oauth2').Strategy
+            strategy = 'dropbox'
+            get_conf strategy, (err, conf) ->
+                if err or not conf?
+                    cb(err)
+                    return
+                # Get these by:
+                #   (1) creating a dropbox account, then going to this url: https://www.dropbox.com/developers/apps
+                #   (2) make a dropbox api app that only access the datastore (not user files -- for now, since we're just doing auth!).
+                #   (3) You'll see an "App key" and an "App secret".
+                #   (4) Add the redirect URL on the dropbox page as well, which will be like https://cloud.sagemath.com/auth/dropbox/return
+                # This might (or might not) be relevant when we support dropbox sync: https://github.com/dropbox/dropbox-js
+                #
+                # You must then put them in the database, via
+                #   update passport_settings set conf['clientID']='...'     where strategy='dropbox';
+                #   update passport_settings set conf['clientSecret']='...' where strategy='dropbox';
+
+                opts =
+                    clientID     : conf.clientID
+                    clientSecret : conf.clientSecret
+                    callbackURL  : "#{auth_url}/#{strategy}/return"
+
+                verify = (accessToken, refreshToken, profile, done) ->
+                    done(undefined, {profile:profile})
+                passport.use(new PassportStrategy(opts, verify))
+
+                app.get "/auth/#{strategy}", passport.authenticate("dropbox-oauth2")
+
+                app.get "/auth/#{strategy}/return", passport.authenticate("dropbox-oauth2", {failureRedirect: '/auth/local'}), (req, res) ->
+                    res.json(req.user)
+
+                cb()
+
+        async.parallel([init_local, init_google, init_github, init_facebook, init_dropbox], cb)


fix tests for slow browsers (chrome 40 regression?)

--- test/zorium.coffee
+++ test/zorium.coffee
@@ -768,6 +768,12 @@

 describe 'router', ->
+  beforeEach (done) ->
+    # Allow routes to settle
+    setTimeout ->
+      done()
+    , 100
+
   describe 'route()', ->
     it 'renders updated DOM', ->
       class App


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine.coffee
+++ test/engine.coffee
@@ -181,7 +181,7 @@
   engine.collides(Rectangle(0, 0, 10, 10), null)

 test "Integration", ->
-  engine = Engin
+  engine = Engine()
 test "#setState", 3, ->
   engine = Engine()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine.coffee
+++ test/engine.coffee
@@ -36,6 +36,7 @@
   engine.add
     test: "test"

+###
 test "zSort", ->
   engine = Engine
     backgroundColor: false
@@ -59,6 +60,7 @@

   equals o1.I.drawnAt, 1, "Object with zIndex #{o1.I.zIndex} should be drawn first"
   equals o2.I.drawnAt, 2, "Object with zIndex #{o2.I.zIndex} should be drawn second"
+###

 test "invalid module throws error", ->
   raises ->
@@ -72,6 +74,7 @@
   engine = Engine
     excludedModules: ["Developer"]

+###
 test "draw events", 2, ->
   engine = Engine
     backgroundColor: false
@@ -95,6 +98,7 @@
     ok true, "remove called"

   engine.frameAdvance()
+###

 test "#find", ->
   engine = Engine()
@@ -146,6 +150,7 @@
   equal attr, "baz"
   equal value, false

+###
 asyncTest "Running", ->
   fps = 30
   milliseconds = 1000
@@ -199,5 +204,7 @@
     start()
   , milliseconds

+###
+
 module()


Added a dictRemoveFileConfirmation option that will force a confirmation dialog before removing a file.

#changelog closes #227

--- src/dropzone.coffee
+++ src/dropzone.coffee
@@ -157,6 +157,9 @@
     # If used, the text to be used to remove a file.
     dictRemoveFile: "Remove file"

+    # If this is not null, then the user will be prompted before removing a file.
+    dictRemoveFileConfirmation: null
+
     # If `done()` is called without argument the file is accepted
     # If you call it with an error message, the file is rejected
     # (This allows for asynchronous validation).
@@ -279,7 +282,10 @@
           if file.status == Dropzone.UPLOADING
             @removeFile file if window.confirm @options.dictCancelUploadConfirmation
           else
-            @removeFile file
+            if @options.dictRemoveFileConfirmation
+              @removeFile file if window.confirm @options.dictRemoveFileConfirmation
+            else
+              @removeFile file

         file.previewElement.appendChild file._removeLink


Extend “sendMessage” function of API to support sending messages to any room.

See wbbaddons/Tims-Chat@c2fe53fdb026d9f0bd047cd12836ea0802171d5e

--- api.coffee
+++ api.coffee
@@ -125,13 +125,16 @@

 # sends a message with `message` as the content and the given
 # smiley status and calls the callback without any parameters afterwards
-sendMessage = (message, enableSmilies = yes, callback) ->
+sendMessage = (message, options..., callback) ->
+	enableSmilies = options[0] ? yes
+
 	common.request.post config.host + '/index.php/AJAXProxy/',
 	form:
 		actionName: 'send'
 		className: 'chat\\data\\message\\MessageAction'
 		'parameters[text]': message
 		'parameters[enableSmilies]': if enableSmilies then 1 else 0
+		'parameters[roomID]': options[1] if options[1]?
 		t: config.securityToken
 	, -> callback?()


fixed typo

fixed typo

--- app.coffee
+++ app.coffee
@@ -18,7 +18,7 @@
 			callback(null, require('/mnt/config.json'))
 		catch error
 			callback(error)
-	# bootstraping
+	# bootstrapping
 	(config, callback) ->
 		request.post("#{API_ENDPOINT}/associate", {
 			user: config.id


Add in /status end point

Add in /status end point

--- app.coffee
+++ app.coffee
@@ -9,6 +9,9 @@
 app.use express.logger()

 app.post "/doc/:doc_id/flush", HttpController.flushUpdatesWithLock
+
+app.get "/status", (req, res, next) ->
+	res.send "track-changes is alive"

 app.use (error, req, res, next) ->
 	logger.error err: error, "an internal error occured"


Use port 3005

Use port 3005

--- app.coffee
+++ app.coffee
@@ -4,4 +4,4 @@
 app.get '/', (req, res) =>
 	res.send('hello world');

-app.listen(3000);+app.listen(3005);


add route parameters

add route parameters

--- app.coffee
+++ app.coffee
@@ -26,6 +26,18 @@
 	app.use app.router

 DispatchManager.createAndStartDispatchers(Settings.dispatcherCount || 10)
+
+app.param 'project_id', (req, res, next, project_id) ->
+	if project_id?.match /^[0-9a-f]{24}$/
+		next()
+	else
+		next new Error("invalid project id")
+
+app.param 'doc_id', (req, res, next, doc_id) ->
+	if doc_id?.match /^[0-9a-f]{24}$/
+		next()
+	else
+		next new Error("invalid doc id")

 app.get    '/project/:project_id/doc/:doc_id',       HttpController.getDoc
 app.post   '/project/:project_id/doc/:doc_id',       HttpController.setDoc


add local admin user auth to validate_doc_update

--- app.coffee
+++ app.coffee
@@ -83,8 +83,9 @@
       # Better error reporting with HTTP codes?
       JSON.stringify {error: 'query_error', reason: "no such template #{doc.type}"}

-app.ddoc.validate_doc_update = (newDoc, oldDoc, userCtx) ->
-  if (userCtx.roles.indexOf('_admin') == -1)
-    throw unauthorized: 'Only admin can modify documents on this database.'
+app.ddoc.validate_doc_update = (newDoc, oldDoc, userCtx, secObj) ->
+  if (userCtx.roles.indexOf('_admin') == -1) and
+    ((secObj?.admins?.names or []).indexOf(userCtx.name) == -1)
+      throw unauthorized: 'Only admin can modify documents in this database.'

 module.exports = app.ddoc


added heroku port param

added heroku port param

--- app.coffee
+++ app.coffee
@@ -7,7 +7,7 @@
 # Add Logging Support
 meryl.p connect.logger()
 # Add Basic Auth
-meryl.p connect.basicAuth(process.env.APIKEY,process.env.SECRETKEY) if process.env.APIKEY? and process.env.SECRETKEY
+# meryl.p connect.basicAuth(process.env.APIKEY,process.env.SECRETKEY) if process.env.APIKEY? and process.env.SECRETKEY
 # Create Web Server
 meryl
   .get '/', (req, resp) ->
@@ -29,4 +29,4 @@
     cloudq.remove req.params.id, (status) ->
       resp.end JSON.stringify({ status: status})

-  .run({ port: Number(process.env.VMC_APP_PORT) || 8000})
+  .run({ port: Number(process.env.PORT || process.env.VMC_APP_PORT) || 8000})


Fixed another bug in clean action

Fixed another bug in clean action

--- aspax.iced
+++ aspax.iced
@@ -203,8 +203,10 @@
     console.log clc.red 'failed with: ', err if err
     await ft.mkdir @dst, defer err
     console.log clc.red 'failed with: ', err if err
-    await ft.remove @dst, defer err
-    console.log clc.red 'failed with: ', err if err
+    await fs.exists @out, defer exists
+    if exists
+      await fs.unlink @out, defer err
+      console.log clc.red 'failed with: ', err if err
     console.log clc.green 'done...'
     callback() if callback


Add mobile detection

Add mobile detection

--- SingularityUI/app/application.coffee
+++ SingularityUI/app/application.coffee
@@ -14,6 +14,8 @@
 class Application

     initialize: =>
+        app.isMobile = touchDevice = 'ontouchstart' of document.documentElement
+
         @views = {}
         @collections = {}


This should actually fix the run now button on the scheduled tasks page

--- SingularityUI/app/models/Task.coffee
+++ SingularityUI/app/models/Task.coffee
@@ -1,5 +1,4 @@
 Model = require './model'
-Request = require './Request'

 endsWith = (needle, haystack) ->
     position = haystack.length - needle.length
@@ -68,6 +67,7 @@
             id = request.id
         else # Pending tasks don't have a request attribute
             id = @get('pendingTask').pendingTaskId.requestId
+        Request = require './Request' # Why can't I do this at the top of the page?
         requestModel = new Request id: id
         requestModel.promptRun => callback()


Dont sync user session on every page load for mobile

--- src/mobile/components/layout/bootstrap.coffee
+++ src/mobile/components/layout/bootstrap.coffee
@@ -43,7 +43,8 @@

   setupErrorReporting()
   setupHeaderView()
-  syncAuth()
+  # TODO: Look into why this fails.
+  # syncAuth()
   checkForAfterSignUpAction()
   if globalClientSetup
     globalClientSetup()


tunisian

tunisian

--- @client/customizations.coffee
+++ @client/customizations.coffee
@@ -1005,8 +1005,28 @@
   slider_pole_labels : effective_ineffective

+
 customizations['tunisia'] =
   lang: 'tun_ar'
+  slider_pole_labels:
+    individual:
+      support: 'أوافق'
+      oppose: 'أخالف'
+      # support: 'ادعم'
+      # oppose: 'اعارض'
+    group:
+      support: 'أوافق'
+      oppose: 'أخالف'
+
+  point_labels:
+    pro: 'نقطة إجابية'
+    pros: 'نقاط إجابية'
+    con: 'نقطة سلبية'
+    cons: 'نقاط سلبية'
+    your_header: "--valences-- أبد"
+    other_header: "--valences--  أخرى"
+    top_header: "--valences--  الرئيسية"
+

 portuguese = ['sintaj', 'delegados_sintaj']


Add support to get a page by id as well as page number

--- src/scripts/models/contents/collection.coffee
+++ src/scripts/models/contents/collection.coffee
@@ -29,7 +29,7 @@

       return length

-    getPage: (num) ->
+    _getPageNum: (num) ->
       page = 0

       for node in @get('contents').models
@@ -41,6 +41,22 @@
           return node
         else
           return node.getPage(num-page)
+
+    _getPageId: (id) ->
+      for node in @get('contents').models
+        if node.get('id') is id
+          return node
+        else if node.isSection()
+          result = node.getPage(id)
+          return result if result
+
+      return
+
+    getPage: (page) ->
+      if typeof page is 'number'
+        return @_getPageNum(page)
+
+      return @_getPageId(page)

     toJSON: (options = {}) ->
       results = super(arguments...)


the dao header

the dao header

--- @client/customizations.coffee
+++ @client/customizations.coffee
@@ -2686,6 +2686,217 @@

+customizations.dao = _.extend {},
+  show_proposer_icon: true
+  collapse_descriptions_at: 300
+
+  proposal_filters: true
+
+  civility_pledge: true
+
+
+  'cluster/Scrapped proposals':
+    archived: true
+
+  'cluster/Drafts':
+    archived: true
+
+
+  HomepageHeader: ReactiveComponent
+    displayName: 'HomepageHeader'
+
+    render: ->
+      homepage = true # fetch('location').url == '/'
+
+      DIV
+        style:
+          position: 'relative'
+          background: "linear-gradient(-45deg, #7474BF, #348AC7)"
+          paddingBottom: 20
+
+
+        onMouseEnter: => @local.hover=true;  save(@local)
+        onMouseLeave: => @local.hover=false; save(@local)
+
+
+
+
+        STYLE null,
+          '''.profile_anchor.login {font-size: 26px; padding-top: 16px;}
+             p {margin-bottom: 1em}'''
+
+        # LINK
+        #   href: "http://fonts.googleapis.com/css?family=Montserrat:200,300,400,500,700"
+        #   rel: 'stylesheet'
+        #   type: 'text/css'
+
+        DIV
+          style:
+            marginLeft: 70
+          SPAN
+            style:
+              display: 'inline-block'
+              visibility: if fetch('location').url == '/' then 'hidden'
+              color: '#ccc'
+              position: 'relative'
+              left: -60
+              top: 4
+              fontSize: 43
+              fontWeight: 400
+              paddingLeft: 25 # Make the clickable target bigger
+              paddingRight: 25 # Make the clickable target bigger
+              cursor: if fetch('location').url != '/' then 'pointer'
+            onClick: if fetch('location').url != '/' then => loadPage('/')
+
+            '<'
+
+
+          # Logo
+          A
+            href: if homepage then 'https://forum.daohub.org/c/theDAO' else '/'
+
+
+            IMG
+              style:
+                height: 30
+                width: 30
+                marginLeft: -44
+                marginRight: 10
+                marginTop: -10
+                verticalAlign: 'middle'
+
+              src: asset('ethereum/the_dao.jpg')
+
+            SPAN
+              style:
+                #fontFamily: "Montserrat, 'Avenir Next W01', 'Avenir Next', 'Lucida Grande', 'Helvetica Neue', Helvetica, Verdana, sans-serif"
+                fontSize: 24
+                color: 'white'
+                fontWeight: 500
+
+              "The DAO"
+
+        SPAN
+          style:
+            display: 'inline-block'
+            visibility: if fetch('location').url == '/' then 'hidden'
+            color: '#ccc'
+            position: 'absolute'
+            left: 0
+            bottom: -70
+            fontSize: 43
+            fontWeight: 400
+            paddingLeft: 25 # Make the clickable target bigger
+            paddingRight: 25 # Make the clickable target bigger
+            cursor: if fetch('location').url != '/' then 'pointer'
+          onClick: if fetch('location').url != '/' then => loadPage('/')
+
+          '<'
+
+
+
+        # The top bar with the logo
+        DIV
+          style:
+            width: HOMEPAGE_WIDTH()
+            margin: 'auto'
+
+
+
+          if homepage
+
+            tips = [
+              "Read a proposal carefully before voting strongly; otherwise only indicate slight support or opposition."
+              "If you vote strongly, add pro and con points that explain your reasoning, especially if you oppose."
+              "Proposals lacking sufficient detail to evaluate should be mildly opposed."
+              #"Take advantage of range voting. Don't go all-for or all-against something unless it's really the most important thing on the whole page."
+              "If a proposal is updated, please update your opinion."
+              "Proposing far-out stuff you have no intention of working on will only clutter the interface for others."
+              "Votes here are not official."
+            ]
+
+            DIV null,
+
+              DIV
+                style:
+                  #backgroundColor: '#eee'
+                  # marginTop: 10
+                  padding: "0 8px"
+                  fontSize: 42
+                  fontWeight: 300
+                  color: 'white'
+                  marginTop: 10
+
+                'Vet Proposals for The DAO'
+
+              DIV
+                style:
+                  #backgroundColor: '#eee'
+                  # marginTop: 10
+                  padding: "0 8px"
+                  fontSize: 18
+                  color: 'white'
+                  marginTop: 40
+                  opacity: .7
+
+                "Tips for keeping this space productive: "
+
+                UL
+                  style:
+                    listStyle: 'outside'
+                    paddingLeft: 40
+                    marginTop: 10
+
+                  for tip in tips
+                    LI
+                      style: {}
+                      tip
+
+              DIV
+                style:
+                  marginTop: 10
+                  padding: 8
+                  fontSize: 18
+                  color: '#eee'
+                  opacity: .7
+
+                  #fontFamily: "Montserrat, 'Avenir Next W01', 'Avenir Next', 'Lucida Grande', 'Helvetica Neue', Helvetica, Verdana, sans-serif"
+
+                "Moderators are (1) categorizing proposals based on their maturity and (2) archiving older proposals."
+
+
+              DIV
+                style:
+                  backgroundColor: 'white'
+                  marginTop: 20
+                  marginBottom: 20
+                  padding: 8
+                  fontSize: 18
+                  color: 'black'
+                  opacity: .5
+                  display: 'inline-block'
+
+                "Meta discussion about dao.consider.it at "
+
+                A
+                  href: 'https://thedao.slack.com/messages/dao_consider_it/'
+                  target: '_blank'
+                  style:
+                    #textDecoration: 'underline'
+                    color: '#EC4246'
+                    fontWeight: 600
+
+                  "#dao_consider_it"
+                ' on The Dao\'s Slack.'
+
+
+
+        ProfileMenu()
+
+
+customizations.dao.NonHomepageHeader = customizations.dao.HomepageHeader
+
+


.

Former-commit-id: 199a28263e077555d98f1127d299056a4538c201 [formerly 30a8f59af7c1d990812f339386941235866bdd33] [formerly 648e6c236b04995d45b6061a866f85718bba52c1 [formerly 39df58aa2699f6ec31ac4d173038e80239641783]] [formerly 46522f92af6c108f7a316539b20f7f32c94ab957 [formerly 6cc09bb87e733b51420632b6c32444914391b5a9] [formerly 580ba55665b792d425297d5536f0fa4f7472a8ee [formerly 580ba55665b792d425297d5536f0fa4f7472a8ee [formerly 68dc787a2fa5dc81d158514b614bb6febb59712b]]]]
Former-commit-id: e538804bf0fa0465352054e7e2bc666ba9cd6e12 [formerly fd3204b3e7118b32529eec3e2c19fb498e79f9a9] [formerly 0a638a09de348c06bc2aa96b5ec25a9e4dad5571 [formerly 1511702115cce3432d172b69c44a2eed934d265c]]
Former-commit-id: 99f185d592bc9b4d336606e124ed187975f4ad29 [formerly 127309be17748a5878941b22a319111f731bc9f7]
Former-commit-id: b60d473f03aad6c62960fc8d755efb3c22f43101
Former-commit-id: ec1ca9aa81d0c2c1477728bebfa0618b027be6c2

--- @client/shared.coffee
+++ @client/shared.coffee
@@ -281,6 +281,7 @@
   top: 0;
   position: absolute;
   color: #aaa;
+  pointer-events: none;
 }
 """


Verkehrte Zeitangaben anzeigen

Verkehrte Zeitangaben anzeigen

closes #61

--- app/assets/javascripts/lib/enter_times.js.coffee
+++ app/assets/javascripts/lib/enter_times.js.coffee
@@ -15,3 +15,19 @@

     $('.time-entries input', context).on('change keydown paste', selectRadioButton)
     $($('.edit-time .time-entries input')[0]).focus()
+
+  $('input.numeric.second_time').each () ->
+    input = $(@)
+    group = input.parent()
+    tr = group.closest('tr')
+    label = group.find('label')
+    text = label.text()
+    input.on('change keyup paste', () ->
+      value = input.val()
+      if (!input[0].validity? || !input[0].validity.badInput) && (value.match(/^\d+[.,]\d{1,2}$/) || value.match(/^\s*$/))
+        label.text(text)
+        tr.removeClass('danger')
+      else
+        label.text('Format: SS,MM')
+        tr.addClass('danger')
+    )


update uiActive

update uiActive

--- app/assets/javascripts/monster/directives.coffee
+++ app/assets/javascripts/monster/directives.coffee
@@ -26,43 +26,23 @@
   (scope, element, attrs) ->
     element.removeClass 'not_ready'

-directive = ($state, $stateParams, $interpolate) ->
-  restrict: 'A'
-  controller: ['$scope','$element','$attrs',($scope, $element, $attrs) ->
-      equalForKeys = (a, b, keys) ->
-        unless keys
-          keys = []
-          for n of a # Used instead of Object.keys() for IE8 compatibility
-            continue
-        i = 0
-
-        while i < keys.length
-          k = keys[i]
-          return false  unless a[k] is b[k] # Not '===', values aren't necessarily normalized
-          i++
-        true
-      matchesParams = ->
-        not params or equalForKeys(params, $stateParams)
+$directive 'uiActive', '$state',
+  ($state)->
+    (scope,element,attrs)->
       update = ->
-        actives      = $attrs.uiActive.split ' '
-        active_class = false
-        for active in actives
-          if $state.current.controller.match active
-            active_class = true
-
-        if active_class
-          $element.addClass 'active'
-        else
-          $element.removeClass 'active'
+        if $state.current.templateUrl
+          name = $state.current.templateUrl.replace /\//g, '_'
+          matched = name is attrs.uiActive
+          if matched
+            element.addClass 'active'
+          else
+            element.removeClass 'active'
         return
-      @$$setStateInfo = (newState, newParams) ->
-        state  = $state.get(newState, stateContext($element))
-        params = newParams
+      @$$setStateInfo = (newState) ->
+        state = $state.get newState, stateContext(element)
         update()
         return
-      $scope.$on '$stateChangeSuccess', update
-  ]
-app.directive 'uiActive', ['$state', '$stateParams', '$interpolate',directive]
+      scope.$on '$stateChangeSuccess', update

 Link = ($state, $stateParams, $interpolate) ->
   restrict: 'A'


OX-401 Bug fix for comment deletion

OX-401 Bug fix for comment deletion

--- app/assets/javascripts/oxalis/model/route.coffee
+++ app/assets/javascripts/oxalis/model/route.coffee
@@ -348,6 +348,7 @@
       if(@comments[i].node == nodeID)
         @comments.splice(i, 1)
         @stateLogger.push()
+        break

   nextCommentNodeID : (forward) ->