Organize HTMLParser

Organize HTMLParser

--- src/trix/models/html_parser.coffee
+++ src/trix/models/html_parser.coffee
@@ -16,6 +16,8 @@

   getDocument: ->
     Trix.Document.fromJSON(@blocks)
+
+  # HTML parsing

   parse: ->
     try
@@ -28,12 +30,6 @@
     finally
       @removeHiddenContainer()

-  nodeFilter = (node) ->
-    if tagName(node) is "style"
-      NodeFilter.FILTER_REJECT
-    else
-      NodeFilter.FILTER_ACCEPT
-
   createHiddenContainer: ->
     if @referenceElement
       @containerElement = @referenceElement.cloneNode(false)
@@ -47,6 +43,41 @@

   removeHiddenContainer: ->
     @containerElement.parentNode.removeChild(@containerElement)
+
+  sanitizeHTML = (html) ->
+    doc = document.implementation.createHTMLDocument("")
+    doc.documentElement.innerHTML = html
+    {body, head} = doc
+
+    for style in head.querySelectorAll("style")
+      body.appendChild(style)
+
+    nodesToRemove = []
+    walker = walkTree(body)
+
+    while walker.nextNode()
+      node = walker.currentNode
+      switch node.nodeType
+        when Node.ELEMENT_NODE
+          if tagName(node) is "script"
+            nodesToRemove.push(node)
+          else
+            for {name} in [node.attributes...]
+              unless name in allowedAttributes or name.indexOf("data-trix") is 0
+                node.removeAttribute(name)
+        when Node.COMMENT_NODE
+          nodesToRemove.push(node)
+
+    for node in nodesToRemove
+      node.parentNode.removeChild(node)
+
+    body.innerHTML
+
+  nodeFilter = (node) ->
+    if tagName(node) is "style"
+      NodeFilter.FILTER_REJECT
+    else
+      NodeFilter.FILTER_ACCEPT

   processNode: (node) ->
     switch node.nodeType
@@ -120,6 +151,8 @@
           unless element.parentNode.firstChild is element
             @appendStringWithAttributes(" | ")

+  # Document construction
+
   appendBlockForAttributesWithElement: (attributes, element) ->
     @blockElements.push(element)
     block = blockForAttributes(attributes)
@@ -157,6 +190,21 @@
       piece.string = string + piece.string
     else
       text.unshift(pieceForString(string))
+
+  pieceForString = (string, attributes = {}) ->
+    type = "string"
+    string = normalizeSpaces(string)
+    {string, attributes, type}
+
+  pieceForAttachment = (attachment, attributes = {}) ->
+    type = "attachment"
+    {attachment, attributes, type}
+
+  blockForAttributes = (attributes = {}) ->
+    text = []
+    {text, attributes}
+
+  # Attribute parsing

   getTextAttributes: (element) ->
     attributes = {}
@@ -199,102 +247,8 @@
       element = element.parentNode
     ancestors

-  getMarginOfBlockElementAtIndex: (index) ->
-    if element = @blockElements[index]
-      unless tagName(element) in getBlockTagNames() or element in @processedElements
-        getBlockElementMargin(element)
-
-  getMarginOfDefaultBlockElement: ->
-    element = makeElement(Trix.config.blockAttributes.default.tagName)
-    @containerElement.appendChild(element)
-    getBlockElementMargin(element)
-
-  translateBlockElementMarginsToNewlines: ->
-    defaultMargin = @getMarginOfDefaultBlockElement()
-
-    for block, index in @blocks when margin = @getMarginOfBlockElementAtIndex(index)
-      if margin.top > defaultMargin.top * 2
-        @prependStringToTextAtIndex("\n", index)
-
-      if margin.bottom > defaultMargin.bottom * 2
-        @appendStringToTextAtIndex("\n", index)
-
-  pieceForString = (string, attributes = {}) ->
-    type = "string"
-    string = normalizeSpaces(string)
-    {string, attributes, type}
-
-  pieceForAttachment = (attachment, attributes = {}) ->
-    type = "attachment"
-    {attachment, attributes, type}
-
-  blockForAttributes = (attributes = {}) ->
-    text = []
-    {text, attributes}
-
   getAttachmentAttributes = (element) ->
     JSON.parse(element.dataset.trixAttachment)
-
-  sanitizeHTML = (html) ->
-    doc = document.implementation.createHTMLDocument("")
-    doc.documentElement.innerHTML = html
-    {body, head} = doc
-
-    for style in head.querySelectorAll("style")
-      body.appendChild(style)
-
-    nodesToRemove = []
-    walker = walkTree(body)
-
-    while walker.nextNode()
-      node = walker.currentNode
-      switch node.nodeType
-        when Node.ELEMENT_NODE
-          if tagName(node) is "script"
-            nodesToRemove.push(node)
-          else
-            for {name} in [node.attributes...]
-              unless name in allowedAttributes or name.indexOf("data-trix") is 0
-                node.removeAttribute(name)
-        when Node.COMMENT_NODE
-          nodesToRemove.push(node)
-
-    for node in nodesToRemove
-      node.parentNode.removeChild(node)
-
-    body.innerHTML
-
-  squishWhitespace = (string) ->
-    string
-      # Replace all breaking whitespace characters with a space
-      .replace(///[^\S#{Trix.NON_BREAKING_SPACE}]///g, " ")
-      # Replace two or more spaces with a single space
-      .replace(/\ {2,}/g, " ")
-
-  isBlockElement = (element) ->
-    return unless element?.nodeType is Node.ELEMENT_NODE
-    return if findClosestElementFromNode(element, matchingSelector: "td")
-    tagName(element) in getBlockTagNames() or window.getComputedStyle(element).display is "block"
-
-  isInsignificantTextNode = (node) ->
-    return unless node?.nodeType is Node.TEXT_NODE
-    return if /\S/.test(node.data)
-    return if elementCanDisplayPreformattedText(node.parentNode)
-    not node.previousSibling or isBlockElement(node.previousSibling) or not node.nextSibling or isBlockElement(node.nextSibling)
-
-  isExtraBR = (element) ->
-    tagName(element) is "br" and
-      isBlockElement(element.parentNode) and
-      element.parentNode.lastChild is element
-
-  elementCanDisplayPreformattedText = (element) ->
-    {whiteSpace} = window.getComputedStyle(element)
-    whiteSpace in ["pre", "pre-wrap", "pre-line"]
-
-  getBlockElementMargin = (element) ->
-    style = window.getComputedStyle(element)
-    if style.display is "block"
-      top: parseInt(style.marginTop), bottom: parseInt(style.marginBottom)

   getImageDimensions = (element) ->
     width = element.getAttribute("width")
@@ -303,3 +257,61 @@
     dimensions.width = parseInt(width, 10) if width
     dimensions.height = parseInt(height, 10) if height
     dimensions
+
+  # Element inspection
+
+  isBlockElement = (element) ->
+    return unless element?.nodeType is Node.ELEMENT_NODE
+    return if findClosestElementFromNode(element, matchingSelector: "td")
+    tagName(element) in getBlockTagNames() or window.getComputedStyle(element).display is "block"
+
+  isInsignificantTextNode = (node) ->
+    return unless node?.nodeType is Node.TEXT_NODE
+    return if /\S/.test(node.data)
+    return if elementCanDisplayPreformattedText(node.parentNode)
+    not node.previousSibling or isBlockElement(node.previousSibling) or not node.nextSibling or isBlockElement(node.nextSibling)
+
+  isExtraBR = (element) ->
+    tagName(element) is "br" and
+      isBlockElement(element.parentNode) and
+      element.parentNode.lastChild is element
+
+  elementCanDisplayPreformattedText = (element) ->
+    {whiteSpace} = window.getComputedStyle(element)
+    whiteSpace in ["pre", "pre-wrap", "pre-line"]
+
+  # Margin translation
+
+  translateBlockElementMarginsToNewlines: ->
+    defaultMargin = @getMarginOfDefaultBlockElement()
+
+    for block, index in @blocks when margin = @getMarginOfBlockElementAtIndex(index)
+      if margin.top > defaultMargin.top * 2
+        @prependStringToTextAtIndex("\n", index)
+
+      if margin.bottom > defaultMargin.bottom * 2
+        @appendStringToTextAtIndex("\n", index)
+
+  getMarginOfBlockElementAtIndex: (index) ->
+    if element = @blockElements[index]
+      unless tagName(element) in getBlockTagNames() or element in @processedElements
+        getBlockElementMargin(element)
+
+  getMarginOfDefaultBlockElement: ->
+    element = makeElement(Trix.config.blockAttributes.default.tagName)
+    @containerElement.appendChild(element)
+    getBlockElementMargin(element)
+
+  getBlockElementMargin = (element) ->
+    style = window.getComputedStyle(element)
+    if style.display is "block"
+      top: parseInt(style.marginTop), bottom: parseInt(style.marginBottom)
+
+  # Helpers
+
+  squishWhitespace = (string) ->
+    string
+      # Replace all breaking whitespace characters with a space
+      .replace(///[^\S#{Trix.NON_BREAKING_SPACE}]///g, " ")
+      # Replace two or more spaces with a single space
+      .replace(/\ {2,}/g, " ")


perform clip before projecting

perform clip before projecting

--- src/ui/canvas.coffee
+++ src/ui/canvas.coffee
@@ -34,7 +34,7 @@
   clip = d3.geo.clipExtent()

   map.path = d3.geo.path()
-      .projection(stream: (s) -> simplify.stream(projection.stream(clip.stream(s))))
+      .projection(stream: (s) -> simplify.stream(clip.stream(projection.stream(s))))

   svg = d3.select('body').append('svg')

@@ -53,8 +53,6 @@
   resize = ->
     map.width = parseInt(d3.select('body').style('width'))
     map.height = parseInt(d3.select('body').style('height'))
-
-    clip.extent([[0, 0], [map.width, map.height]])

     projection.scale(map.s0 = d3.min([map.width / boxWidth, map.height / boxHeight]))
     f0 = (bbox[2] - bbox[0]) / boxWidth / map.s0
@@ -79,6 +77,7 @@
     projection
         .scale(map.s0 * map.sc)
         .translate([t0[0] * map.sc + tr[0], t0[1] * map.sc + tr[1]])
+    clip.extent([projection.invert([0, map.height]), projection.invert([map.width, 0])])

   map.centerAt = (pos) ->
     new_sc = 8000000 / map.s0


:non-potable_water: Register config observers subscriptions as disposable

--- lib/minimap-render-view.coffee
+++ lib/minimap-render-view.coffee
@@ -59,16 +59,17 @@
     @charHeight = atom.config.get 'minimap.charHeight'
     @textOpacity = atom.config.get 'minimap.textOpacity'

-    atom.config.observe 'minimap.interline', (@interline) =>
+    @subscriptions.add @asDisposable atom.config.observe 'minimap.interline', (@interline) =>
       @emit 'minimap:scaleChanged'
       @forceUpdate()
-    atom.config.observe 'minimap.charWidth', (@charWidth) =>
+    @subscriptions.add @asDisposable atom.config.observe 'minimap.charWidth', (@charWidth) =>
       @emit 'minimap:scaleChanged'
       @forceUpdate()
-    atom.config.observe 'minimap.charHeight', (@charHeight) =>
+    @subscriptions.add @asDisposable atom.config.observe 'minimap.charHeight', (@charHeight) =>
       @emit 'minimap:scaleChanged'
       @forceUpdate()
-    atom.config.observe 'minimap.textOpacity', (@textOpacity) => @forceUpdate()
+    @subscriptions.add @asDisposable atom.config.observe 'minimap.textOpacity', (@textOpacity) =>
+      @forceUpdate()

   # Destroys the {MinimapRenderView} instance, unsubscribes from the listened
   # events and releases its resources.
@@ -656,3 +657,19 @@
         intactRanges.splice(i--, 1)
       i++
     intactRanges.sort (a, b) -> a.domStart - b.domStart
+
+  #     #######  ######## ##     ## ######## ########
+  #    ##     ##    ##    ##     ## ##       ##     ##
+  #    ##     ##    ##    ##     ## ##       ##     ##
+  #    ##     ##    ##    ######### ######   ########
+  #    ##     ##    ##    ##     ## ##       ##   ##
+  #    ##     ##    ##    ##     ## ##       ##    ##
+  #     #######     ##    ##     ## ######## ##     ##
+
+  # Convert a subscription on the deprecated model with a `::off` method into a
+  # `Disposable`.
+  #
+  # subscription - The subscription {Object} to wrap in a `Disposable`.
+  #
+  # Returns a `Disposable`.
+  asDisposable: (subscription) -> new Disposable -> subscription.off()


:fire: Remove redundant updates container on detail view

--- lib/package-detail-view.coffee
+++ lib/package-detail-view.coffee
@@ -30,10 +30,6 @@

       @section class: 'section', =>
         @form class: 'section-container package-detail-view', =>
-          @div outlet: 'updateArea', class: 'alert alert-success package-update', =>
-            @span outlet: 'updateLabel', class: 'icon icon-squirrel update-message'
-            @span outlet: 'updateLink', class: 'alert-link update-link icon icon-cloud-download', 'Install'
-
           @div class: 'container package-container', =>
             @div class: 'row', =>
               @subview 'packageCard', new PackageCard(pack.metadata, packageManager, onSettingsView: true)
@@ -143,7 +139,6 @@

       @loadPackage()
       @updateFileButtons()
-      @updateArea.hide()
       @populate()

   handleButtonEvents: ->
@@ -201,30 +196,21 @@
     loadTime + activateTime

   installUpdate: ->
-    return if @updateLink.prop('disabled')
     return unless @availableVersion

-    @updateLink.prop('disabled', true)
-    @updateLink.text('Installing\u2026')

     @packageManager.update @pack, @availableVersion, (error) =>
       if error?
-        @updateLink.prop('disabled', false)
-        @updateLink.text('Install')
         @errors.append(new ErrorView(@packageManager, error))

   checkForUpdate: ->
-    @updateArea.hide()
     return if atom.packages.isBundledPackage(@pack.name)

-    @updateLink.on 'click', => @installUpdate()

     @packageManager.getOutdated().then (packages) =>
       for pack in packages when pack.name is @pack.name
         if @packageManager.canUpgrade(@pack, pack.latestVersion)
           @availableVersion = pack.latestVersion
-          @updateLabel.text("Version #{@availableVersion} is now available!")
-          @updateArea.show()

   # Even though the title of this view is hilariously "PackageDetailView",
   # the package might not be installed.


Prevent double posting

Prevent double posting

--- resources/assets/coffee/react/_components/comment-editor.coffee
+++ resources/assets/coffee/react/_components/comment-editor.coffee
@@ -27,6 +27,7 @@
     super props

     @textarea = null
+    @throttledPost = _.throttle @post, 1000

     @state =
       message: @props.message ? ''
@@ -39,6 +40,7 @@

   componentWillUnmount: =>
+    @throttledPost.cancel()
     @xhr?.abort()

@@ -77,7 +79,7 @@
               text: @buttonText()
               icon: @buttonIcon()
               props:
-                onClick: @post
+                onClick: @throttledPost
                 disabled: @state.posting || !@isValid()
         else
           div className: "#{bn}__footer-item",


flush detection, maybe

flush detection, maybe

--- src/valueHand.coffee
+++ src/valueHand.coffee
@@ -15,9 +15,10 @@
   .filter (group) -> group.length > 1
   .value()

-  suits = _ hand.all
+  ofSuit = _ hand.all
   .groupBy (card) -> card.suit
-  .value()
+  .map (group, suit) -> suit
+  .max()

   myPairs = pairs.length - cpairs.length

@@ -40,8 +41,12 @@
   if hand.all.length < 7 and detectStraight hand.all, 4
     value = 0.5

-  if suits.length == 1
+  if ofSuit == hand.all.length
     value = value + (1 - value) * 0.5
+  if ofSuit >= 4 and hand.all.length < 7
+    value = 0.5
+  if ofSuit >= 5
+    value = 1

   if myPairs > 0
     if hand.all.length < 4


Reload view on html set.

Reload view on html set.

--- src/view/view.coffee
+++ src/view/view.coffee
@@ -144,7 +144,12 @@
       source = Batman.Navigator.normalizePath(source)
       @html = @constructor.store.get(source)

-    set: Batman.Property.defaultAccessor.set
+    set: (key, html) ->
+      @html = html
+      @isBound = false
+
+      @loadView(@node) if @node and html?
+      @initializeBindings() if @bindImmediately

   @accessor 'node',
     get: ->


override TOC-index-title too

override TOC-index-title too

--- lib/utils/style_helpers.coffee
+++ lib/utils/style_helpers.coffee
@@ -57,7 +57,7 @@
             type:  'folder'
             data:
               path:  targetChunks[0..i].join '/'
-              title: targetChunks[i]
+              title: if chunk == 'index' and i is 0 then file.pageTitle else targetChunks[i]
             depth: i + 1

           prevPath = [] # Make sure that we don't match directories several levels in, after a fail.
@@ -78,7 +78,7 @@

       # Otherwise we just fall back to the file's target path...
       else
-        fileData.title = path.basename file.targetPath
+        fileData.title = if file.targetPath == 'index' then file.pageTitle else path.basename file.targetPath

       nodes.push
         type:   'file'


Call updateState when required

Call updateState when required

--- lib/views/analysis_view.coffee
+++ lib/views/analysis_view.coffee
@@ -20,10 +20,12 @@

     @subscribe atom.workspace, 'dart-tools:analysis', (result) =>
       @items.push(result)
+      @updateState()
       return null

     @subscribe atom.workspace, 'dart-tools:refresh', (fullPath) =>
       _.remove @items, (item) => item.fullpath == fullPath
+      @updateState()
       return null
     this


squash bug for state not updating url correctly

--- _source/client/lib/components/component.coffee
+++ _source/client/lib/components/component.coffee
@@ -670,6 +670,7 @@
                   neededUrls.push obj

+
               paths = current.split("/")
               paths = paths.filter Boolean

@@ -688,6 +689,7 @@
                 else
                   paths = paths.splice(0, isAtState)

+
               # build each route
               for state in neededUrls

@@ -696,7 +698,6 @@

                 # rebuild the path array
                 route = encodeURI(route.join("/"))
-

                 # if there is not already a path for this url lets make one
                 if Apollos.Router.isPath("#{state.name}")
@@ -923,10 +924,8 @@

               # @TODO - break out of class and call as method
               card = component.getCard(component.componentName())
-
-
-
               card.states or= {}
+

               # track the current state prior to doing state
               # calculations
@@ -970,7 +969,6 @@
                       Apollos.Router.redirect cleanedPath
                       return

-
                   Apollos.Router.redirect encodeURI(_stateUrl)
                   oldRoute = Apollos.Router.current()

@@ -988,12 +986,12 @@

               ###
               Tracker.autorun ->
+

                 # current state of the app
                 currentState = self.component.state.get()
                 stateUrl = Apollos.Router.path(currentState)

-
                 # state has been updated so all we need to do is update
                 # the route
                 if currentState isnt state
@@ -1003,7 +1001,7 @@
                   if window.location.search
                     stateUrl += window.location.search

-                  # Apollos.Router.redirect encodeURI(stateUrl)
+                  Apollos.Router.redirect encodeURI(stateUrl)

                   return

@@ -1020,7 +1018,6 @@

                 # current route
                 currentRoute = Apollos.Router.current()
-

                 # no change in path
                 if currentRoute.path is oldRoute.path


ui hook fix?

ui hook fix?

--- _source/client/lib/components/component.coffee
+++ _source/client/lib/components/component.coffee
@@ -327,7 +327,7 @@
       parentElement._uihooks = createUIHooks component, parentElement
       return originalDOMRangeAttach.apply @, arguments

-    finally
+    catch

       parentElement._uihooks = oldUIHooks if oldUIHooks


Send game started to only late joining player

--- models/game.coffee
+++ models/game.coffee
@@ -18,9 +18,9 @@
       @topic = topic
       @io.sockets.emit "new topic", @topic

-  sendGameStarted: () ->
+  sendGameStarted: (socket) ->
       elapsed = Date.now() - @startTime
-      @io.sockets.emit "game started",
+      socket.emit "game started",
           gameLength: @gameLength
           elapsed: elapsed
           players: (player.name for player in @players)
@@ -35,7 +35,7 @@
       @DbHelper.createGame @topic, (res)=>
         @gameId = res

-      @sendGameStarted()
+      @sendGameStarted(@io.sockets)
       if @gameLength
           cb = () =>
               @gameOver()
@@ -45,7 +45,7 @@
   addPlayer: (newPlayer) ->
     @players.push(newPlayer)
     if @start
-      @sendGameStarted()
+      @sendGameStarted(newPlayer.socket)

   addWord: (player, word) ->
     # if moderator has not started the game


add vote counts by category to team model

--- models/team.coffee
+++ models/team.coffee
@@ -45,11 +45,14 @@
     contestant_design: Number
     contestant_innovation: Number
     contestant_completeness: Number
+    contestant_count: Number
     judge_utility: Number
     judge_design: Number
     judge_innovation: Number
     judge_completeness: Number
+    judge_count: Number
     popularity: Number
+    popularity_count: Number
     overall: Number
 TeamSchema.plugin require('mongoose-types').useTimestamps
 TeamSchema.index updatedAt: -1
@@ -75,60 +78,49 @@
 TeamSchema.static 'updateAllSavedScores', (next) ->
   map = ->
     ret =
-      contestant_utility:
-        sum: 0
-        count: 0
-      contestant_design:
-        sum: 0
-        count: 0
-      contestant_innovation:
-        sum: 0
-        count: 0
-      contestant_completeness:
-        sum: 0
-        count: 0
-      judge_utility:
-        sum: 0
-        count: 0
-      judge_design:
-        sum: 0
-        count: 0
-      judge_innovation:
-        sum: 0
-        count: 0
-      judge_completeness:
-        sum: 0
-        count: 0
-      popularity: 0
+      contestant_utility: 0
+      contestant_design: 0
+      contestant_innovation: 0
+      contestant_completeness: 0
+      contestant_count: 0
+      judge_utility: 0
+      judge_design: 0
+      judge_innovation: 0
+      judge_completeness: 0
+      judge_count: 0
+      popularity_count: 0
     if this.type == 'contestant' or this.type == 'judge'
-      ret[this.type + '_utility'] = { sum: this.utility, count: 1 }
-      ret[this.type + '_design'] = { sum: this.design, count: 1 }
-      ret[this.type + '_innovation'] = { sum: this.innovation, count: 1 }
-      ret[this.type + '_completeness'] = { sum: this.completeness, count: 1 }
+      ret[this.type + '_utility'] = this.utility
+      ret[this.type + '_design'] = this.design
+      ret[this.type + '_innovation'] = this.innovation
+      ret[this.type + '_completeness'] = this.completeness
+      ret[this.type + '_count'] = 1
     else if this.type == 'voter'
-      ret.popularity = 1
+      ret.popularity_count = 1
     emit this.teamId, ret

   reduce = (key,vals) ->
     ret = vals.shift()
     vals.forEach (val) ->
       for field of ret
-        if field == 'popularity'
-          ret[ field ] += val[ field ]
-        else
-          ret[ field ].sum += val[ field ].sum
-          ret[ field ].count += val[ field ].count
+        ret[ field ] += val[ field ]
     ret

   finalize = (key,val) ->
     ret = {}
-    for field of val
-      if field != 'popularity'
-        if val[ field ].count == 0
-          ret[ field ] = 0
-        else
-          ret[ field ] = val[ field ].sum / val[ field ].count
-    ret[ 'popularity' ] = val.popularity
+    [ 'contestant_utility', 'contestant_design', 'contestant_innovation', 'contestant_completeness' ].forEach (field) ->
+      if val.contestant_count != 0
+        ret[ field ] = val[ field ] / val.contestant_count
+      else
+        ret[ field ] = 0
+    ret[ 'contestant_count' ] = val.contestant_count
+    [ 'judge_utility', 'judge_design', 'judge_innovation', 'judge_completeness' ].forEach (field) ->
+      if val.judge_count != 0
+        ret[ field ] = val[ field ] / val.judge_count
+      else
+        ret[ field ] = 0
+    ret[ 'judge_count' ] = val.judge_count
+    ret[ 'popularity_count' ] = val.popularity_count
     ret

   mrCommand =
@@ -144,10 +136,10 @@
       console.log result
       return next [err,result]

-    maxPopularity = 0
+    max_popularity_count = 0
     computedScores = result.documents[0].results
     computedScores.forEach (computedScore) ->
-      maxPopularity = Math.max maxPopularity, computedScore.value.popularity
+      max_popularity_count = Math.max max_popularity_count, computedScore.value.popularity_count
     Team.find {}, (err,teams) ->
       teams.forEach (team) ->
         id = team._id
@@ -156,13 +148,13 @@
         if computedScore
           overall = 0
           for field of computedScore.value
-            if field != 'popularity'
-              team.scores[ field ] = computedScore.value[ field ]
+            team.scores[ field ] = computedScore.value[ field ]
+            if field != 'contestant_count' and field != 'judge_count' and field != 'popularity_count'
               overall += computedScore.value[ field ]
-          if maxPopularity == 0
+          if max_popularity_count == 0
             team.scores.popularity = 0
           else
-            team.scores.popularity = computedScore.value.popularity / maxPopularity * 10
+            team.scores.popularity = computedScore.value.popularity_count / max_popularity_count * 10
           overall += team.scores.popularity
           team.scores.overall = overall
         else


Use $ to match end of string

Use $ to match end of string

It seems like some JS RE engines don't support \z

--- src/plugins/jsonparser.coffee
+++ src/plugins/jsonparser.coffee
@@ -2,7 +2,7 @@
   processResponse: (res) ->
     # Check to see if the contentype is "something/json" or
     # "something/somethingelse+json"
-    if res.contentType and (/^.*\/(?:.*\+)?json(;|\z)/i).test res.contentType
+    if res.contentType and (/^.*\/(?:.*\+)?json(;|$)/i).test res.contentType
       # If the body hasn't been parsed yet, parse it.
       raw = if typeof res.body is 'string' then res.body else res.text
       res.body = JSON.parse raw if raw


Create back binding for dynamic service

Create back binding for dynamic service

--- library/gearing/augment.coffee
+++ library/gearing/augment.coffee
@@ -77,6 +77,7 @@
             namespace[method] = (resource, implementation) =>
                 augment = Augment.augmentForResource resource
                 augment.service::[method] = implementation
+                implementation.service = service

     # Obtain the augment object for the specified resource. If such
     # an object does not exist, it will be automatically created and


Explicitly call out the duplex hooks

Explicitly call out the duplex hooks

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -59,6 +59,20 @@
     # can not be treated as a complete class implementation. This
     # mainly is used to exclude or account for abstract classes.
     @abstract yes
+
+    # A usable hook that gets asynchronously invoked once a new
+    # channel (socket) gets connected and acknowledges by the server.
+    # The method gets a set of parameters that maybe be useful to
+    # have by the actual implementation. Please remember thet the
+    # method is asynchronously wired, so be sure to call `next`.
+    connected: (context, socket, next) -> next()
+
+    # A usable hook that gets asynchronously invoked once a new
+    # channel (socket) gets connected to the Socket.IO hub in the
+    # context of the current service. The channel will be considered
+    # established only when prescreen calls the next procedure. If
+    # you wish to decline, just don't call `next` and close socket.
+    prescreen: (context, socket, next) -> next()

     # An internal, static method that is used to obtain gurading
     # domains for each of the declared server site providers. Please


Add configurable compression

Add configurable compression

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -170,6 +170,7 @@
     # JS sources in the context and minifies & compresses those into
     # one blob using the library called UglifyJS2. See it for info.
     compressSources: (context) ->
+        compression = "visual:compression"
         sources = _.toArray context.sources
         scripts = _.toArray context.scripts
         emptySources = "the JS sources are empty"
@@ -177,6 +178,7 @@
         context.styles = _.unique context.styles, u
         assert context.scripts = _.unique scripts, u
         assert _.isArray sources = _.unique sources
+        return context unless nconf.get compression
         assert hasher = crypto.createHash "md5"
         joined = context.sources.join new String
         digest = hasher.update(joined).digest "hex"


Minor changes in the visual

Minor changes in the visual

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -62,10 +62,11 @@
     # method is wired in an synchronous way for greater functionality.
     # This is the place where you would be importing the dependencies.
     prelude: (context, request, next) ->
+        context.uuid = request: request.uuid
         context.headers = request.headers
         context.params = request.params
+        context.uuid.service = @uuid
         context.url = request.url
-        context.uuid = @uuid
         return next()

     # Use this method in the `predule` scope to bring dependencies into


Make possible to directly specify context ecc

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -179,11 +179,12 @@
     # creates a proper empty context that conforms to the necessary
     # restrictions. Then it runs the internal machinery to fill the
     # context with the service and request related data and events.
-    assembleContext: (symbol, request, asm, receive) ->
+    assembleContext: (symbol, request, asm, ecc, receive) ->
         noPrelude = "no prelude method detected"
         assert _.isFunction(@prelude), noPrelude
         context = new Object scripts: [], sources: []
         assert _.extend context, styles: [], sheets: []
+        assert _.isObject(context.caching = ecc) if ecc
         pusher = context.sources.push.bind context.sources
         context.inline = (f) -> pusher "(#{f}).apply(this)"
         context.doctype = "<!DOCTYPE html>"
@@ -201,7 +202,7 @@
     # of the resource. Use for unobtrusive retrieval of resources.
     GET: (request, response) ->
         symbol = "service".toString().toLowerCase()
-        assert _.isArray args = [symbol, request, yes]
+        assert args = [symbol, request, yes, undefined]
         @assembleContext args..., (context, compiled) ->
             length = compiled.length or undefined
             response.setHeader "Content-Length", length


More goodies for the visual module

More goodies for the visual module

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -232,10 +232,11 @@
         noPrelude = "no prelude method detected"
         assert _.isFunction(@prelude), noPrelude
         assert _.isObject context = stock or {}
-        assert @extendContext.call this, context
+        @extendContext.call this, context, symbol
         prelude = @upstreamAsync "prelude", =>
             assert @deployContext context, symbol
             assert @inlineAutocalls context, symbol
+            context.inline -> @emit "installed", this
             context = @compressContext context if asm
             compiled = @compileContext context if asm
             return receive context, compiled or null
@@ -246,17 +247,18 @@
     # with all the commodities that should be present on contexts.
     # This includes utilitiy methods and member definitions that all
     # of the internal and external codebase depends and relies on.
-    extendContext: (context) ->
+    extendContext: (context, symbol) ->
         append = -> _.extend context, arguments...
         append styles: [], sheets: [], changes: []
         append externals: [], invokes: [], sources: []
         append scripts: [], cargo: [], reserved: {}
-        assert context.doctype = "<!DOCTYPE html>"
-        assert t = "(%s).call(this, (%s))".toString()
-        assert a = "(%s).apply(this, (%s))".toString()
         v = (f, s) -> format a, f, try JSON.stringify s
+        assert context.doctype = type = "<!DOCTYPE html>"
+        assert t = "(%s).call(#{symbol or "this"}, (%s))"
+        assert a = "(%s).apply(#{symbol or "this"}, (%s))"
+        assert i = (f) -> "(#{f}).call(#{symbol or "this"})"
         pusher = context.sources.push.bind context.sources
-        context.inline = (f) -> pusher "(#{f}).apply(this)"
+        context.inline = (implement) -> pusher i(implement)
         context.varargs = (s...) -> pusher v(_.last(s), s)
         context.transit = (x, f) -> pusher format t, f, x


play a little bit more with the naming

play a little bit more with the naming

--- library/semantic/modals.coffee
+++ library/semantic/modals.coffee
@@ -33,7 +33,7 @@
 # auxiliary services upon it. Basically, if you have thhe service
 # that needs to be presented as a window (modal) and is used like
 # zombie, then this class can be used to reduce the boilerplate.
-module.exports.ModalHosted = class ModalHosted extends Zombie
+module.exports.WithinModal = class WithinModal extends Zombie

     # This is a marker that indicates to some internal subsystems
     # that this class has to be considered abstract and therefore


added @_input.data('action')

added @_input.data('action')

--- modules/gui/lib/widgets/ComboBox.coffee
+++ modules/gui/lib/widgets/ComboBox.coffee
@@ -103,11 +103,14 @@

                     else
                         @__root.addClass 'activeBox'
-
-                        @_dropdown._updateData (err)=>
-                            return alert(err.message) if err
-                            dropdown.show()
-
+
+                        if @_dropdown._updateData
+                            @_dropdown._updateData (err)=>
+                                return alert(err.message) if err
+                                dropdown.show()
+
+                        else dropdown.show()
+
                     return false

                 $(document).click ()=>
@@ -130,8 +133,10 @@
         _setAction: (@_action, @_value)->

             @_input.focus().val @_value
-
+            @_input.data 'action', @_action
+
             @_emit 'change',
                 name: @data.name
                 action: @_action
                 value: @_value
+


URLify for assignments and group assignments

--- app/assets/javascripts/assignments-slug.coffee
+++ app/assets/javascripts/assignments-slug.coffee
@@ -344,10 +344,15 @@
       result
     ), '').replace(/^\s+|\s+$/g, '').replace(/[-\s]+/g, replacement or '-').replace '#{replacement}$', ''

-ready = ->
-  $('#assignment_title').on('keyup', ->
+$('.assignments').ready( ->
+  $('#assignment_title').on('change paste keyup click', ->
     $('#assignment_slug').val(urlify($('#assignment_title').val()));
   )
+)

-$('.assignments').ready(ready)
+$('.group_assignments').ready( ->
+  $('#group_assignment_title').on('change paste keyup click', ->
+    $('#group_assignment_slug').val(urlify($('#group_assignment_title').val()));
+  )
+)


Improved urlify function

Improved urlify function


IE: Fix search placeholder bugging out when clicking a dashboard item.

How is this even possible?!

--- app/assets/javascripts/common_interface.coffee
+++ app/assets/javascripts/common_interface.coffee
@@ -36,7 +36,7 @@
     search      = $("#header_inside input[name=q]")
     placeholder = search.attr('placeholder')

-    if search.val().length is 0
+    if search.val()?.length is 0
       search.val(placeholder)

     search.focus ->
@@ -45,7 +45,7 @@
       search.val('') if search.val() is placeholder

     search.blur ->
-      value = search.val()
+      value = search.val() or ''

       # Similarly, only add the placeholder back if the user did not enter
       # a value.


re-fail test after refactor

re-fail test after refactor

--- regression/src/matchers-test.coffee
+++ regression/src/matchers-test.coffee
@@ -88,7 +88,7 @@
       Then -> @matches(td.matchers.contains(true, 5, null, undefined), [true, 5, null]) == false

       describe 'elements containing matchers', ->
-        xThen -> @matches(td.matchers.contains('b', td.matchers.isA(Number)), ['a', 3, 'b']) == true
+        Then -> @matches(td.matchers.contains('b', td.matchers.isA(Number)), ['a', 3, 'b']) == true

     context 'objects', ->
       Then -> @matches(td.matchers.contains(foo: 'bar', baz: 42), foo: 'bar', baz: 42, stuff: this) == true


adds fallback display when not logged in on contact-info

--- app/assets/javascripts/project/projects.coffee
+++ app/assets/javascripts/project/projects.coffee
@@ -11,6 +11,13 @@
       .done (r, status, xhr) ->
         $(button).hide()
         $('#contact-info').html(r)
+      .fail (xhr, textStatus, errorThrown) ->
+        if xhr.status == 401
+          $('#contact-info').html(
+            '<div data-alert class="alert-box alert radius">' +
+              'Please log in to view private contact info.' +
+            '</div>'
+          )

   PB.projects.form.init_tags = (element) ->
     $(element).selectize


問い合わせ件数表示スクリプト　生成URLをセット  $.get window.path_for("count_staff_messages"), (data) ->

--- app/assets/javascripts/staff/message.js.coffee
+++ app/assets/javascripts/staff/message.js.coffee
@@ -3,5 +3,5 @@
     window.setInterval(update_number_of_unprocessed_messages, 1000 * 60)

 update_number_of_unprocessed_messages = ->
-  $.get window.paths.number_of_unprocessed_messages, (data) ->
+  $.get window.path_for("count_staff_messages"), (data) ->
     $('#number-of-unprocessed-messages').text "(#{data})"


make info copy before apply crop (this breaks everything)

--- app/assets/javascripts/uploadcare/utils.coffee
+++ app/assets/javascripts/uploadcare/utils.coffee
@@ -155,6 +155,7 @@
       modifiers += "-/preview/"

     file.then (info) =>
+      info = $.extend({}, info)
       info.cdnUrlModifiers = modifiers
       info.cdnUrl = "#{info.originalUrl}#{modifiers or ''}"
       info.crop = coords


Use correct executedFrom value for subsequent invocations

--- src/browser/atom-application.coffee
+++ src/browser/atom-application.coffee
@@ -65,7 +65,7 @@
   exit: (status) -> app.exit(status)

   constructor: (options) ->
-    {@resourcePath, @devResourcePath, @executedFrom, @version, @devMode, @safeMode, @socketPath} = options
+    {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath} = options

     global.atomApplication = this

@@ -86,11 +86,11 @@
     else
       @loadState() or @openPath(options)

-  openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, profileStartup}) ->
+  openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, profileStartup}) ->
     if test
       @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile})
     else if pathsToOpen.length > 0
-      @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
+      @openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
     else if urlsToOpen.length > 0
       @openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
     else
@@ -369,8 +369,8 @@
   #   :safeMode - Boolean to control the opened window's safe mode.
   #   :windowDimensions - Object with height and width keys.
   #   :window - {AtomWindow} to open file paths in.
-  openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
-    locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen)
+  openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
+    locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
     pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)

     unless pidToKillWhenClosed or newWindow
@@ -517,7 +517,7 @@
     devMode = true
     new AtomWindow({bootstrapScript, @resourcePath, exitWhenDone, isSpec, specDirectory, devMode})

-  locationForPathToOpen: (pathToOpen) ->
+  locationForPathToOpen: (pathToOpen, executedFrom='') ->
     return {pathToOpen} unless pathToOpen

     pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
@@ -531,7 +531,7 @@
       initialLine = initialColumn = null

     unless url.parse(pathToOpen).protocol?
-      pathToOpen = path.resolve(@executedFrom, fs.normalize(pathToOpen))
+      pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))

     {pathToOpen, initialLine, initialColumn}


fixed post list sort active state bug

fixed post list sort active state bug

--- lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
+++ lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
@@ -130,8 +130,8 @@
       @trigger("thread:removed", thread_id)

     setActiveThread: (thread_id) ->
-      @$("a[data-id!='#{thread_id}']").removeClass("active")
-      @$("a[data-id='#{thread_id}']").addClass("active")
+      @$(".post-list a[data-id!='#{thread_id}']").removeClass("active")
+      @$(".post-list a[data-id='#{thread_id}']").addClass("active")

     showSearch: ->
       @$(".browse").removeClass('is-dropped')


add differential objects to possible blacklists

--- scripts/phabs_commands.coffee
+++ scripts/phabs_commands.coffee
@@ -101,7 +101,7 @@
     msg.finish()

   #   hubot bl <id> - blacklists <id> from auto-resopnses
-  robot.respond /ph(?:ab)? bl ((?:T|F|P|M|B|Q|L|V)(?:[0-9]+)|(?:r[A-Z]+[a-f0-9]{10,}))/, (msg) ->
+  robot.respond /ph(?:ab)? bl ((?:T|F|P|M|B|Q|L|V|D)(?:[0-9]+)|(?:r[A-Z]+[a-f0-9]{10,}))/, (msg) ->
     phab.getPermission(msg.envelope.user, 'phuser')
     .then ->
       phab.blacklist msg.match[1]
@@ -111,7 +111,7 @@
     msg.finish()

   #   hubot bl <id> - blacklists <id> from auto-resopnses
-  robot.respond /ph(?:ab)? unbl ((?:T|F|P|M|B|Q|L|V)(?:[0-9]+)|(?:r[A-Z]+[a-f0-9]{10,}))/, (msg) ->
+  robot.respond /ph(?:ab)? unbl ((?:T|F|P|M|B|Q|L|V|D)(?:[0-9]+)|(?:r[A-Z]+[a-f0-9]{10,}))/, (msg) ->
     phab.getPermission(msg.envelope.user, 'phuser')
     .then ->
       phab.unblacklist msg.match[1]


Add test for new view property in context

--- test/app/frimfram/View.spec.coffee
+++ test/app/frimfram/View.spec.coffee
@@ -112,6 +112,10 @@
       FrimFram.View.extendGlobalContext({'someLib':someLib})
       expect(view.initContext().someLib).toBe(someLib)

+    it 'includes the property "view", which is the View itself', ->
+      view = new FrimFram.View()
+      expect(view.initContext().view).toBe(view)
+

   describe '.getContext()', ->
     it 'proxies to .initContext() by default', ->
@@ -308,4 +312,4 @@
     it 'clears all properties from the object, except for "destroyed" and "destroy"', ->
       view = new FrimFram.View()
       view.destroy()
-      expect(_.isEqual(_.keys(view), ['destroyed', 'destroy'])).toBe(true)+      expect(_.isEqual(_.keys(view), ['destroyed', 'destroy'])).toBe(true)


make tooltip container into a div

make tooltip container into a div

--- app/assets/javascripts/components/tooltip.cjsx
+++ app/assets/javascripts/components/tooltip.cjsx
@@ -8,14 +8,14 @@
     }

   render: =>
-

+

            Show help text

       {@renderTooltip()}
-

+

   renderTooltip: =>
     if @state.show then
{@props.text}
 else null


populate even if there are no options.  Fixes #91.

--- app/assets/javascripts/instructables.js.coffee
+++ app/assets/javascripts/instructables.js.coffee
@@ -85,7 +85,7 @@
     update_select = ->
       track = $('#instructable_track')[0].value
       options = window.thing_tracks[track]
-      if track and options.length > 0
+      if track
         repopulate_targets(options)
       else
         hide_targets()


-Saving progress

-Saving progress

--- app/javascripts/app.coffee
+++ app/javascripts/app.coffee
@@ -1,9 +1,11 @@
+storage = {}
 document.addEventListener("DOMContentLoaded", (e) ->
-    stored_val = SimpleStorage.get()
-    store_el = document.getElementById("storedValue")
-    store_el.innerHTML = stored_val
-    window.store = ->
-        el = document.getElementById("toStoreInput")
-        console.warn "store triggerred" + el.value
-    console.warn SimpleStorage.new()
-)+    storage = SimpleStorage.at(SimpleStorage.deployed_address)
+    storage.storedData().then((tx)->
+        store_el = document.getElementById("storedValue")
+        store_el.innerHTML = tx
+    )
+)
+window.store = ->
+    el = document.getElementById("toStoreInput")
+    storage.set(el)


Simplify lab project wrapper

Simplify lab project wrapper

--- app/pages/lab/project.cjsx
+++ app/pages/lab/project.cjsx
@@ -17,7 +17,10 @@
 EditProjectPage = React.createClass
   displayName: 'EditProjectPage'

-  mixins: [Navigation]
+  mixins: [TitleMixin, Navigation]
+
+  title: ->
+    @props.project.display_name

   getDefaultProps: ->
     project: id: '2'
@@ -64,6 +67,7 @@

             }</PromiseRenderer>

+

             Subject sets
             <PromiseRenderer promise={@props.project.get 'subject_sets'}>{(subjectSets) =>
@@ -86,12 +90,15 @@
             }</PromiseRenderer>

-        <button type="button" className="minor-button" onClick={@deleteProject}>Delete this project <LoadingIndicator off={not @state.deletionInProgress} /></button>{' '}
+
+        <button type="button" className="minor-button" disabled={@state.deletionInProgress} onClick={@deleteProject}>Delete this project <LoadingIndicator off={not @state.deletionInProgress} /></button>{' '}
         {if @state.deletionError?

{@state.deletionError.message}
}

-        <RouteHandler {...@props} />
+        <ChangeListener target={@props.project} handler={=>
+          <RouteHandler {...@props} />
+        } />

@@ -142,8 +149,7 @@
         @setState subjectSetCreationInProgress: false

   deleteProject: ->
-    @setState
-      deletionError: null
+    @setState deletionError: null

     confirmed = prompt("""
       You are about to delete this project and all its data!
@@ -151,8 +157,7 @@
     """) is DELETE_CONFIRMATION_PHRASE

     if confirmed
-      @setState
-        deletionInProgress: true
+      @setState deletionInProgress: true

       this.props.project.delete()
         .then =>
@@ -166,39 +171,25 @@
 module.exports = React.createClass
   displayName: 'EditProjectPageWrapper'

-  mixins: [TitleMixin, HandlePropChanges, PromiseToSetState]
+  mixins: [TitleMixin]

-  title: ->
-    "Edit #{@state.project?.display_name ? '(Loading)'}"
+  title: 'Edit'

   getDefaultProps: ->
-    params: null
-
-  getInitialState: ->
-    project: null
-
-  propChangeHandlers:
-    'params.projectID': 'fetchProject'
-
-  componentDidMount: ->
-    auth.listen 'change', @fetchProject
-
-  componentWillUnmount: ->
-    auth.stopListening 'change', @fetchProject
-
-  fetchProject: (_, props = @props) ->
-    @promiseToSetState project: auth.checkCurrent().then ->
-      apiClient.type('projects').get props.params.projectID
+    params:
+      projectID: '0'

   render: ->
-    if @state.project?
-      <ChangeListener target={@state.project}>{=>
-        <EditProjectPage {...@props} project={@state.project} />
-      }</ChangeListener>
-    else
-

-        {if @state.rejected.project?
-          {@state.rejected.project.toString()}
-        else
-          Loading}
-

+    getProject = auth.checkCurrent().then =>
+      apiClient.type('projects').get @props.params.projectID
+
+    <ChangeListener target={auth} handler={=>
+      <PromiseRenderer promise={getProject} pending={=>
+
Loading project

+      } then={(project) =>
+        @_project = project
+        <EditProjectPage {...@props} project={project} />
+      } catch={(error) =>
+
{error.toString()}

+      } />
+    } />


w/e just uncache

w/e just uncache

--- apps/profile/routes.coffee
+++ apps/profile/routes.coffee
@@ -14,8 +14,6 @@
   return next() if res.locals.profile
   new Profile(id: req.params.id).fetch
     data: data
-    cache: true
-    cacheTime: 300 # 5 mins
     success: (profile) ->
       res.locals.profile = profile
       res.locals.sd.PROFILE = profile.toJSON()


OX-120 debug

OX-120 debug

--- app/assets/javascripts/model/binary.coffee
+++ app/assets/javascripts/model/binary.coffee
@@ -155,6 +155,10 @@
     unless position == @lastPosition and direction == @lastDirection and _.isEqual(zoomSteps, @lastZoomSteps)

       lastPosition = @lastPosition
+
+      unless lastPosition
+        lastPosition = [0, 0, 0]
+
       @lastPosition = position
       @lastZoomSteps = zoomSteps.slice(0)
       @lastDirection = direction


Rename set_class_name function

Rename set_class_name function

--- app/assets/javascripts/models/setup.coffee
+++ app/assets/javascripts/models/setup.coffee
@@ -24,7 +24,7 @@
       @template = @template.replace(/\b(data-id)\.?[^\s|>]+/g, "data-id=" + @id)
       @_build_attrs_template()

-  @set_class_name_variables: (class_name) ->
+  @set_class_name: (class_name) ->
     @class_name = class_name
     @dash_name =  @_get_dash_name(class_name)
     @snake_name = @_get_snake_name(class_name)


request != baseRequest, didn't save.

request != baseRequest, didn't save.

--- app/coffee/AuthenticationController.coffee
+++ app/coffee/AuthenticationController.coffee
@@ -36,7 +36,7 @@
 		})
 		if req.session?.token?
 			res.locals.loggedIn = req.loggedIn = true
-			req.ghclient = github.client(req.session.token, {request:request})
+			req.ghclient = github.client(req.session.token, {request:baseRequest})
 		else
 			res.locals.loggedIn = req.loggedIn = false


Sanitize full name on calendar to prevent xss

you can no longer be xss'd by a users name on a calendar item
We're not currently using the tooltip plugin for this element but if we
ever did we would have the same problem with the malicious code getting
injected into the page upon hovering over the event name.

Fixes CNVS-16638

Test Plan:

- modify you username to be '"><img src=x onerror=alert(2) />
- create a calendar event
- view the calendar
- You shouldnt see an alert

Change-Id: I81c3b89af44386f9eba71df00feb65ca3542d1e7
Reviewed-on: https://gerrit.instructure.com/44950
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
QA-Review: Steven Shepherd <e794458448443812fb2685675aab3bbd1a3891bc@instructure.com>
Reviewed-by: Mark Severson <cdf1f90aee9d0d073a2a24141ebc71887f3ba533@instructure.com>
Product-Review: Cosme Salazar <d37f5e06cd46b8fd23ce5553334e09ede5394232@instructure.com>

--- app/coffeescripts/calendar/Calendar.coffee
+++ app/coffeescripts/calendar/Calendar.coffee
@@ -21,13 +21,14 @@
   'compiled/views/calendar/AgendaView'
   'compiled/calendar/CalendarDefaults'
   'compiled/util/deparam'
+  'str/htmlEscape'

   'vendor/fullcalendar'
   'jquery.instructure_misc_helpers'
   'jquery.instructure_misc_plugins'
   'vendor/jquery.ba-tinypubsub'
   'jqueryui/button'
-], (I18n, $, _, userSettings, hsvToRgb, colorSlicer, calendarAppTemplate, EventDataSource, commonEventFactory, ShowEventDetailsDialog, EditEventDetailsDialog, Scheduler, CalendarNavigator, AgendaView, calendarDefaults, deparam) ->
+], (I18n, $, _, userSettings, hsvToRgb, colorSlicer, calendarAppTemplate, EventDataSource, commonEventFactory, ShowEventDetailsDialog, EditEventDetailsDialog, Scheduler, CalendarNavigator, AgendaView, calendarDefaults, deparam, htmlEscape) ->

   class Calendar
     constructor: (selector, @contexts, @manageContexts, @dataSource, @options) ->
@@ -273,8 +274,8 @@
         else
           I18n.t('event_event_title', 'Event Title:')

-      $element.attr('title', $.trim("#{timeString}\n#{$element.find('.fc-event-title').text()}\n\n#{I18n.t('calendar_title', 'Calendar:')} #{event.contextInfo.name}"))
-      $element.find('.fc-event-inner').prepend($("#{I18n.t('calendar_title', 'Calendar:')} #{event.contextInfo.name}"));
+      $element.attr('title', $.trim("#{timeString}\n#{$element.find('.fc-event-title').text()}\n\n#{I18n.t('calendar_title', 'Calendar:')} #{htmlEscape(event.contextInfo.name)}"))
+      $element.find('.fc-event-inner').prepend($("#{I18n.t('calendar_title', 'Calendar:')} #{htmlEscape(event.contextInfo.name)}"))
       $element.find('.fc-event-title').prepend($("#{screenReaderTitleHint} "))
       $element.find('.fc-event-title').toggleClass('calendar__event--completed', event.isCompleted())
       element.find('.fc-event-inner').prepend($('', {'class': "icon-#{event.iconType()}"}))


fix js error you sometimes get when editing an appointment group

fixes CNVS-12558

test plan:
 * enable calendar2 and scheduler
 * go to calendar, then scheduler view
 * ensure you have an appointment group (if not, create one)
 * ensure appointment_group_id=null is *not* in the url
 * click the appointment group to view the calendar for it
 * appointment_group_id=x should be in the url
 * click "Go back to list"
 * appointment_group_id= should *not* be in the url
 * reload the page
 * try to edit the appointment group
 * it should work

Change-Id: Idc15a14a1957b8322b1f48ee577770072309b114
Reviewed-on: https://gerrit.instructure.com/33647
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Nick Cloward <9cc571ed222c7ca8c35f018ff48a612f8cadeb73@instructure.com>
QA-Review: August Thornton <9adc7a1161ddf32ff608de792a7e50179545f026@instructure.com>
Product-Review: Cody Cutrer <c4fe2b1d90ef8f2548c8aeabfa633434316f0305@instructure.com>

--- app/coffeescripts/calendar/Calendar.coffee
+++ app/coffeescripts/calendar/Calendar.coffee
@@ -376,7 +376,10 @@
       changed = false
       for k, v of opts
         changed = true if data[k] != v
-        data[k] = v
+        if v
+          data[k] = v
+        else
+          delete data[k]
       location.href = "#" + $.param(data) if changed

     viewDisplay: (view) =>


Add autoclan metadata and clean up kind (#6136)

* Add autoclan metadata and clean up kind

* Reorder to match precedence

--- app/schemas/models/clan.schema.coco.coffee
+++ app/schemas/models/clan.schema.coco.coffee
@@ -49,7 +49,16 @@
   # Set to 'basic' for auto clans
   dashboardType: {type: 'string', 'enum': ['basic', 'premium']}
   # Set only for auto clans, to the origins of the programmatic creation
-  kind: { type: 'string', 'enum': ['classroom', 'teacher', 'school', 'district', 'state', 'country'], description: 'Signals an autoclan that may use different logic to look up membership'}
+  kind: {
+    type: 'string',
+    enum: ['classroom', 'teacher', 'school', 'school-subnetwork', 'school-network', 'school-district', 'administrative-region', 'country'],
+    description: 'Signals an autoclan that may use different logic to look up membership'
+  }
+  metadata: c.object({
+    title: 'Metadata',
+    description: 'Contains properties that help find autoclans'
+    additionalProperties: true
+  })
   # Set only for auto clans (yet), to display instead of Clan.name
   displayName: { type: 'string' }


style(bpConfig): don't leak  var

style(bpConfig): don't leak  var

--- modules/scripts/providers/config.coffee
+++ modules/scripts/providers/config.coffee
@@ -1,5 +1,5 @@
 # # Config
-angular.module('bp').provider 'bpConfig', class BpConfig
+angular.module('bp').provider 'bpConfig', do -> class BpConfig

   config =
     platform: 'ios'


Fix test

Fix test

--- api/apps/articles/test/model.coffee
+++ api/apps/articles/test/model.coffee
@@ -648,7 +648,7 @@
             text: 'The Title Goes Here'
             article: ''
             thumbnail_url: 'http://image.jpg'
-            hide_thumbnail: false
+            hide_image: false
           },
           {
             type: 'text'


Better data and errors handling

Better data and errors handling

--- neo4jdriver.coffee
+++ neo4jdriver.coffee
@@ -53,14 +53,24 @@
             @__cleanUpResponse response, (result) => @__proceedResult result
           else
             console.error error
+            console.trace()
       cb and cb()
   , 50

   __cleanUpResponse: (response, cb) ->
-    if response?.data
+    if response?.data and _.isObject response.data
       @__cleanUpResults response.data, cb
-    else if response?.content
-      @__cleanUpResults JSON.parse(response.content), cb
+    else if response?.content and _.isString response.content
+      try
+        @__cleanUpResults JSON.parse(response.content), cb
+      catch error
+        console.error "Neo4j response error (Check your cypher queries):", error
+        console.error "Original received data:"
+        console.log response.content
+        console.trace()
+    else
+      console.error "Empty response from Neo4j, but expecting data"
+      console.trace()

   __cleanUpResults: (results, cb) ->
     if results?.results or results?.errors
@@ -165,6 +175,7 @@
     catch error
       console.error "Error sending request to Neo4j (GrapheneDB) server:"
       console.error error
+      console.trace()

   __parseNode: (currentNode) ->
     node = _service: {}
@@ -230,7 +241,7 @@
           parsed = parsed.concat @__parseResponse result.data, result.columns, reactive
         return parsed
       else
-        return response.exception
+        console.error response.exception

     if response?.columns and response?.data
       unless _.isEmpty response.data
@@ -255,16 +266,18 @@
       return @

   __parseSettings: (settings, opts = {}, callback) ->
-    if _.isObject settings
-      {cypher, query, opts, parameters, params, callback, cb, resultDataContents, reactive, reactiveNodes} = settings
-    else
-      if _.isFunction opts
-        callback = opts
-        opts = {}
-
-    if _.isString settings
+    if _.isArray settings
       cypher = settings
       settings = {}
+    else if _.isObject settings
+      {cypher, query, opts, parameters, params, callback, cb, resultDataContents, reactive, reactiveNodes} = settings
+    else if _.isString settings
+      cypher = settings
+      settings = {}
+
+    if _.isFunction opts
+      callback = opts
+      opts = {}

     opts     ?= {}
     cypher   ?= query


save key dom id in data._id

save key dom id in data._id

--- src/scripts/graph/Keys.coffee
+++ src/scripts/graph/Keys.coffee
@@ -6,6 +6,7 @@

   class Keys
     constructor: (@timeline) ->
+      #@selectionManager = @timeline.editor.selectionManager
       @onKeyUpdated = new Signals.Signal()

     selectNewKey: (data, container) ->
@@ -123,7 +124,12 @@
         .append('g')
         .attr('class', 'key')
         # Add a unique id for SelectionManager.removeDuplicates
-        .attr('id', (d) -> Utils.guid())
+        .call((d) ->
+          d.each (item) ->
+            item._id =  Utils.guid()
+        )
+        # Use the unique id added above for the dom element id
+        .attr('id', (d) -> d._id)
         .on('mousedown', () ->
           # Don't trigger mousedown on linescontainer else
           # it create the selection rectangle
@@ -132,6 +138,7 @@
         .call(drag)

       properties.selectAll('.key')
+
         .attr('class', (d) ->
           cls = 'key'
           # keep selected class


Little fixes

Little fixes

--- views/game.coffee
+++ views/game.coffee
@@ -1,4 +1,4 @@
-WALK_TIMER = 400
+WALK_TIMER = 300
 TILE_SIZE = 16

 class window.GameState
@@ -102,7 +102,7 @@
     return player

   update: ->
-    if !@player.isMoving
+    if !@player?.isMoving
       if @game.input.activePointer.isDown
         clickX = @game.input.activePointer.x - 270
         clickY = @game.input.activePointer.y - 270


add namespace to inlined htmled

add namespace to inlined htmled

--- apps/partner2/client/artists_artist.coffee
+++ apps/partner2/client/artists_artist.coffee
@@ -90,7 +90,7 @@
       @$('.partner2-artist-blurb').html @artist.mdToHtml('blurb')
     else if not _s.isBlank(@partnerArtist.get('biography'))
       @$('.partner2-artist-blurb').html @partnerArtist.mdToHtml('biography')
-        .after "
— Submitted by #{@partner.get('name')}
"
+        .after "
— Submitted by #{@partner.get('name')}
"
     @initializeBlurb()

   initializeBlurb: ->


change tab to Now when Usage clicked.

change tab to Now when Usage clicked.

--- chat/grails-app/assets/javascripts/chat.coffee
+++ chat/grails-app/assets/javascripts/chat.coffee
@@ -23,6 +23,7 @@

 $('#usage').on 'click', (event) ->
     $('#chatMessage').val 'usage'
+    $('#AncTabNow').tab 'show'
     sendFixedMessage()


check the class

check the class

--- extensions/tree/tree-pane.coffee
+++ extensions/tree/tree-pane.coffee
@@ -24,7 +24,7 @@
       el = $(event.currentTarget)
       path = decodeURIComponent el.data 'path'

-      if fs.isDirectory path
+      if el.hasClass 'dir'
         if el.hasClass 'open'
           @tree.hideDir path
           el.removeClass 'open'


ComputeController: followUpcomingEvents: follow other events when needed

--- client/Main/providers/computecontroller.coffee
+++ client/Main/providers/computecontroller.coffee
@@ -358,7 +358,7 @@

-  followUpcomingEvents: (machine)->
+  followUpcomingEvents: (machine, followOthers = no)->

     StateEventMap =

@@ -374,7 +374,7 @@

       @eventListener.addListener stateEvent, machine._id

-      if stateEvent in ["build", "destroy"]
+      if stateEvent in ["build", "destroy"] and followOthers
         @eventListener.addListener "reinit", machine._id

       return yes
@@ -384,7 +384,7 @@

   info: (machine)->

-    if @followUpcomingEvents machine
+    if @followUpcomingEvents machine, yes
       return Promise.resolve()

     @eventListener.triggerState machine,


mixpanel: fix reference error

mixpanel: fix reference error

--- client/Teamwork/Views/teamworkworkspace.coffee
+++ client/Teamwork/Views/teamworkworkspace.coffee
@@ -264,7 +264,7 @@
         @showActivityWidget()
         @hideShareButtons()

-        KD.mixpanel "Click Teamwork Invite", {sessionKey}
+        KD.mixpanel "Click Teamwork Invite", {@sessionKey}

     panel.addSubView @exportWorkspace = new KDButtonView
       cssClass : "export-workspace tw-rounded-button hidden"


Client: Activity: Added tabIndex property to common button component

--- client/app/lib/components/common/button.coffee
+++ client/app/lib/components/common/button.coffee
@@ -3,6 +3,9 @@

 module.exports = class Button extends React.Component
+
+  defaultProps=
+    tabIndex: ''

   onClick: (event) ->

@@ -12,6 +15,7 @@

   render: ->
     <button
+      tabIndex={@props.tabIndex}
       className={kd.utils.curry 'Button', @props.className}
       onClick={@bound 'onClick'}>{@props.children}</button>


sidebar: show unverified stacks as well

sidebar: show unverified stacks as well

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -1,3 +1,4 @@
+_ = require 'lodash'
 kd = require 'kd'
 Link = require 'app/components/common/link'
 React = require 'kd-react'
@@ -32,6 +33,7 @@
       privateChannels              : getters.followedPrivateChannelThreads
       selectedThreadId             : getters.selectedChannelThreadId
       stacks                       : EnvironmentFlux.getters.stacks
+      drafts                       : EnvironmentFlux.getters.draftStackTemplates
       sharedMachines               : EnvironmentFlux.getters.sharedMachines
       collaborationMachines        : EnvironmentFlux.getters.collaborationMachines
       sharedMachineListItems       : EnvironmentFlux.getters.sharedMachineListItems
@@ -51,6 +53,9 @@
       @setState { showNoStacksWidget : yes }  unless stacks.length

     EnvironmentFlux.actions.loadMachines().then @bound 'setActiveInvitationMachineId'
+
+    EnvironmentFlux.actions.loadTeamStackTemplates()
+    EnvironmentFlux.actions.loadPrivateStackTemplates()

     actions.channel.loadFollowedPublicChannels()
     actions.channel.loadFollowedPrivateChannels()
@@ -71,9 +76,6 @@
         EnvironmentFlux.actions.loadMachines()

-  onNewStack: -> @setState { creatingNewStack: yes }
-
-
   setActiveInvitationMachineId: ->

     { setActiveInvitationMachineId } = EnvironmentFlux.actions
@@ -115,10 +117,10 @@
     # Render stacks of koding as first.
     stackList.koding.forEach (stack) => stackSections.push @renderStack stack

+    stackSections = stackSections.concat @renderDrafts()
+
     # Now render stack of managed vms last
     stackList.managed.forEach (stack) => stackSections.push @renderStack stack
-
-    stackSections.push @renderNewStackSection()

     return stackSections

@@ -133,12 +135,22 @@
       machines={stack.get 'machines'}/>

+  renderDrafts: ->
+
+    @state.drafts?.toList().toJS().map (template) ->
+      id = template._id
+      title = _.unescape template.title
+
+

+          <Link href="/Stack-Editor/#{id}">{title}</Link>
+

+
+
+
   renderStacks: ->

     if @state.stacks.size
-      <SidebarStackHeaderSection
-        onNewStack={@bound 'onNewStack'}
-        >
+      <SidebarStackHeaderSection>
         {@prepareStacks()}
       </SidebarStackHeaderSection>
     else if @state.showNoStacksWidget
@@ -169,18 +181,6 @@
   renderNewStackSection: ->

     return null  unless @state.creatingNewStack
-
-

-
-

-          <Link href='/Stack-Editor'>Default stack template</Link>
-

-
-

-

   renderLogo: ->


Environment: Set correct values to params

--- client/app/lib/flux/environment/actions.coffee
+++ client/app/lib/flux/environment/actions.coffee
@@ -283,8 +283,8 @@
         ideApp?[methodName] machineUId, rootPath

       reactor.dispatch actions.WORKSPACE_DELETED, {
-        workspaceId : machine.get '_id'
-        machineId   : workspace.get '_id'
+        workspaceId : _id
+        machineId   : machine.get '_id'
       }

       resolve()


ComputeStorage: update: extend existing instance on update

--- client/app/lib/providers/computestorage.coffee
+++ client/app/lib/providers/computestorage.coffee
@@ -1,6 +1,7 @@
 debug = (require 'debug') 'cs'

 kd = require 'kd'
+_ = require 'lodash'
 Encoder = require 'htmlencode'
 Promise = require 'bluebird'

@@ -229,5 +230,5 @@

     for item, index in (@get type)
       if item._id is key
-        @storage[type][index] = value
+        _.extend @storage[type][index], value
         return @storage[type][index]


video: emit publisher talking state changes

--- client/app/lib/videocollaboration/model.coffee
+++ client/app/lib/videocollaboration/model.coffee
@@ -212,6 +212,12 @@
     @publisher = _publisher = new ParticipantType.Publisher getNick(), publisher
     @stream    = _publisher.stream

+    _publisher.on 'TalkingDidStart', =>
+      @emit 'ParticipantStartedTalking', getNick()
+
+    _publisher.on 'TalkingDidStop', =>
+      @emit 'ParticipantStoppedTalking', getNick()
+
     return _publisher


Terminal: Remove session key from serializer method.

--- client/ide/workspace/panes/terminalpane.coffee
+++ client/ide/workspace/panes/terminalpane.coffee
@@ -72,6 +72,5 @@
     data       =
       path     : path
       machine  : { label, ipAddress, slug, uid }
-      session  : @remote.session
       paneType : paneType
       hash     : @hash


Save selected tab

Save selected tab

--- app/assets/javascripts/notebook/magic/tabs.coffee
+++ app/assets/javascripts/notebook/magic/tabs.coffee
@@ -7,6 +7,21 @@
 ], ($, Observable, ko, d3, dimple) ->
   (dataO, container, options) ->
     ulId = @genId
+
+    get_cell = () ->
+      # get cell object, like done in get_cell_elements in notebook.js
+      cell_dom_element = $(container).parents(".cell").not('.cell .cell')[0]
+      $(cell_dom_element).data("cell")
+
+    get_saved_state = () ->
+      saved_state = get_cell().metadata.presentation?.tabs_state
+      JSON.parse(saved_state || "{}") || {}
+
+    save_state = (state) ->
+      cell = get_cell()
+      cell.metadata.presentation = {} if not cell.metadata.presentation
+      cell.metadata.presentation.tabs_state = JSON.stringify(state, undefined, 2)
+
     $('#ul'+ulId+' a').click( (e) ->
       e.preventDefault()
       e.stopImmediatePropagation()
@@ -16,10 +31,18 @@
       id = $(@).attr('href')
       $(id).addClass('active')
       $(@).parent().addClass('active')
+
+      # console.log("tab id:", id)
+      save_state({tab_id: id})
     )

-    # select the first tab
-    $('#ul'+ulId+' li:first a').click()
+    saved_tab_id = get_saved_state().tab_id
+    if saved_tab_id
+      # console.log("selecting a saved tab", saved_tab_id)
+      $('a[href=' + saved_tab_id + ']').click()
+    else
+      # select the first tab
+      $('#ul'+ulId+' li:first a').click()

     dataO.subscribe( (newData) =>
       #notify tabs


Fix pre-fill submit.

Fix pre-fill submit.

--- app/assets/javascripts/preset_templates.js.coffee
+++ app/assets/javascripts/preset_templates.js.coffee
@@ -12,7 +12,7 @@
 }

 jQuery ->
-  if ($('body').attr('data-controller') == 'preset_templates' && $('body').attr('data-action') == 'new')
+  if ($('body').attr('data-controller') == 'preset_templates' && ($('body').attr('data-action') == 'new' || $('body').attr('data-action') == 'create'))
     $('#preset_template_preset_type').on 'change', changeHandler
     $('#preset_template_preset_type').change()

@@ -26,8 +26,9 @@

   $.each available_presets, (index, value) =>
     button = document.createElement('button')
-    button.className = 'btn btn-primary btn-xs'
+    button.className = 'btn btn-info btn-xs'
     button.setAttribute('data-value', value)
+    button.setAttribute('type', 'button')
     button.innerText = index
     prefill_buttons[0].appendChild(button)
     $(button).on 'click', ->


clean base controller

clean base controller

--- src/common/states/controller.coffee
+++ src/common/states/controller.coffee
@@ -2,29 +2,19 @@
 .controller 'BaseCtrl', (
   $scope
   $mdSidenav
-  $mdDialog
   $state
   Sprint
-  projectUtils
+  Project
+  sprint
   project
-  sprint
-  Project
 ) ->
+  # since views are nested, project and sprint objects will be available
+  # for all child states
   $scope.project = project
   $scope.sprint = sprint

   $scope.toggleSidenav = ->
     $mdSidenav('left').toggle()
-
-  showConfirmNewSprint = (ev) ->
-    confirm = $mdDialog.confirm()
-      .title 'Start a new sprint'
-      .textContent 'Starting a new sprint will end this one'
-      .targetEvent ev
-      .ok 'OK'
-      .cancel 'Cancel'
-    $mdDialog.show(confirm).then ->
-      $state.go 'tab.new-sprint'

   $scope.goTo = (item) ->
     $state.go item.state, item.params
@@ -59,17 +49,17 @@
     ,
       state: 'tab.sprint-list'
       params:
-        projectId: null
+        projectId: $scope.project?.objectId
       title: 'Sprints'
       icon: 'view-list'
     ,
     ]
   ,
-    title: 'Sprint'
+    title: 'Current Sprint'
     items: [
       state: 'tab.edit-sprint'
       params:
-        sprintId: null
+        sprintId: $scope.sprint?.objectId
       title: 'Settings'
       icon: 'settings'
     ,
@@ -95,4 +85,3 @@
     ,
     ]
   ]
-  updateMenuLinks()


ComputeController: triggerReviveFor given machineId

After some operations we need to revive JMachine data, to make this central we
will call this method when we need to get latest JMachine from db. which then
this method will emit `revive-machineId` to update corresponding Machine object.

--- client/Main/providers/computecontroller.coffee
+++ client/Main/providers/computecontroller.coffee
@@ -314,6 +314,16 @@

   # Utils beyond this point
   #
+
+  triggerReviveFor:(machineId)->
+
+    info "Triggering revive for #{machineId}..."
+
+    KD.remote.api.JMachine.one machineId, (err, machine)=>
+      if err? then warn "Revive failed for #{machineId}: ", err
+      else
+        @emit "revive-#{machineId}", machine
+        info "Revive triggered for #{machineId}", machine

   requireMachine: (options = {}, callback = noop)-> @ready =>


Environment: Actions: reinitStackFromWidget: use group stack if stack not provided

--- client/app/lib/flux/environment/actions.coffee
+++ client/app/lib/flux/environment/actions.coffee
@@ -384,8 +384,10 @@
 reinitStackFromWidget = (stack) ->

   { computeController } = kd.singletons
-  _stack = remote.revive stack.toJS()
-  computeController.reinitStack _stack
+
+  computeController.reinitStack if stack
+  then remote.revive stack.toJS()
+  else computeController.getGroupStack()

 createWorkspace = (machine, workspace) ->


assets_path

--- app/assets/javascripts/map.js.coffee
+++ app/assets/javascripts/map.js.coffee
@@ -1,713 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
-
-$ = jQuery
-
-
-$ ->
-
-  $map = $("#map")
-
-  if $map.length
-    southWest = new L.LatLng(-260, -180)
-    northEast = new L.LatLng(340, 560)
-    bounds = new L.LatLngBounds(southWest, northEast)
-
-    map = L.map('map', {
-      maxBounds: bounds,
-    }).setView([20, 50], 2)
-
-    cloudmade_api_key = $map.data("cloudmade-api-key")
-    L.Icon.Default.imagePath = "http://leafletjs.com/dist/images"
-
-    L.tileLayer("/assets/carte_arven/{z}/{x}/{y}.png", {
-        attribution: '© Dragonvale',
-        maxZoom: 4,
-        tms: true,
-        continuousWorld: true,
-        noWrap: true
-    }).addTo(map)
-    ###L.tileLayer("http://{s}.tile.cloudmade.com/#{cloudmade_api_key}/997/256/{z}/{x}/{y}.png", {
-        attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © CloudMade',
-        maxZoom: 18
-    }).addTo(map)###
-
-    # smallPointIcon
-    # middlePointIcon
-    smallPointIcon = L.icon
-      iconUrl: 'http://upload.wikimedia.org/wikipedia/commons/d/d7/Red_Point.gif',
-      iconSize: [10, 10],
-      iconAnchor: [5, 5],
-      popupAnchor: [0, -5],
-    mediumPointIcon = L.icon
-      iconUrl: 'http://upload.wikimedia.org/wikipedia/commons/d/d7/Red_Point.gif',
-      iconSize: [20, 20],
-      iconAnchor: [10, 10],
-      popupAnchor: [0, -10],
-    bigPointIcon = L.icon
-      iconUrl: 'http://upload.wikimedia.org/wikipedia/commons/d/d7/Red_Point.gif',
-      iconSize: [30, 30],
-      iconAnchor: [15, 15],
-      popupAnchor: [0, -15],
-      #shadowUrl: 'my-icon-shadow.png',
-      #shadowRetinaUrl: 'my-icon-shadow@2x.png',
-      #shadowSize: [68, 95],
-      #shadowAnchor: [22, 94]
-
-    # Some content from $map data
-    nodes = $map.data("nodes")
-    nodes_by_id = $map.data("nodes-by-id")
-    #nodes_by_begin_at = $map.data("nodes-by-begin-at")
-    characters = $map.data("characters")
-    console.log(characters)
-    characters_by_id = $map.data("characters-by-id")
-    characters_updated_at = $map.data("characters-updated-at")
-
-    # TODO : choose your character
-    main_character = characters[0]
-
-    ###################
-    # UTILS
-    ###################
-    # Small functions for dates
-    j_date = (str_date)->
-      new Date(Date.parse(str_date))
-    jv_date = (str_date)->
-      j_date(str_date).valueOf()
-    json_data = ($block, attr) ->
-      JSON.parse($block.data(attr))
-    blank = (str)->
-      (!str? || (str.length == 0))
-
-    ###############################
-    # Class for stock and update automitically the character list and their nodes
-    # @characters: models from the db
-    # @list: list of Character objects
-    # @updated_at: store the value of @characters_updated_at at each fetch from the db
-    # @last_date: store the value of the date of the slider
-    ###############################
-    class CharacterList
-      constructor: ->
-        # characters and characters_updated_at are data sent with the page in $map
-        @characters = characters
-        @construct_list()
-        @updated_at = characters_updated_at
-        # Refresh data by AJAX every 3 seconds # TODO Adjust
-        setInterval( =>
-          @update_from_server()
-        1000)
-
-      destroy_list: ->
-        if @list
-          for character in @list
-            character.destroy()
-        @list = []
-
-      # Destroy and recreate the list of the characters objects
-      construct_list: ->
-        @destroy_list()
-        for character in @characters
-          @list.push(new Character(character))
-        last_date = @last_date
-        if last_date
-          @last_date = null
-          update_date(last_date)
-
-      # Fetch characters and nodes from server
-      update_from_server: ->
-        $.ajax
-          type: "GET"
-          url: "/nodes"
-          dataType: "json"
-          success: (data) =>
-            nodes_by_id = data.nodes_by_id
-            characters_by_id = data.characters_by_id
-            @update(data.characters, data.characters_updated_at)
-      # Update only the characters that have been changed since the last fetch
-      update: (characters, characters_updated_at)->
-        @characters = characters
-        #console.log characters, characters_updated_at
-        if characters_updated_at != @updated_at
-          @updated_at = characters_updated_at
-          #console.log "chars updated"
-          # A character has changed
-          if characters.length == @list.length
-            for new_ch, ind in characters
-              old_ch = @list[ind].character
-              if old_ch.id != new_ch.id
-                # Not the same character at the same position as before
-                console.log "Not the same character at the same position as before"
-                @construct_list()
-                break
-              if old_ch.updated_at != new_ch.updated_at
-                # Character updated
-                #console.log "ch", new_ch, "updated"
-                @list[ind].update_character(new_ch)
-              else if old_ch.nodes_updated_at != new_ch.nodes_updated_at
-                # Character_node updated
-                #console.log "ch nodes", new_ch, "updated"
-                @list[ind].update_character_nodes(new_ch)
-          else
-            # not the same number of characters as before
-            console.log "Not the same number of characters as before"
-            #console.log characters
-            @construct_list()
-      # Updates the date for all the characters of the map
-      update_date: (new_date)->
-        if new_date != @last_date
-          @last_date = new_date
-          if !@date_blocked
-            # One modif max for 15 ms
-            @date_blocked = true
-            setTimeout( =>
-              @date_blocked = false
-              # Ensure that the last date set is the one on the slider
-              setTimeout( =>
-                if !@date_blocked && @last_date != new_date
-                  @update_date(@last_date)
-              10)
-            15)
-            for c in @list
-              c.update_date(new_date)
-
-    ###############################
-    # A character is the character and its nodes
-    # @character: Character model from server
-    # @nodes: Array of nodes model from server
-    # @date: date of the map, this character print his marker according to this date
-    # @node: Fake node model with properties calculate by @date
-    # @node_obj: class Node object corresponding to the marker, calculate and updated according to @node
-    ###############################
-    class Character
-      # Create the character
-      # He has'nt date, so he has'nt node_obj at the beginning
-      constructor: (@character) ->
-        @update_nodes()
-
-      # Update content and position of @node and @node_obj
-      # @node_obj must exists
-      change_node: ->
-        @update_node_obj_position()
-        @last_node_index = @next_node_index
-      # Update position of @node and @node_obj
-      # @node_obj must exists
-      update_node_obj_position: ->
-        @node = $.extend({}, @nodes[@next_node_index])
-        suiv_node = @nodes[@next_node_index+1]
-        cur_node = @nodes[@next_node_index]
-        cur_lat = cur_node.latitude
-        cur_lng = cur_node.longitude
-        @node.real = false
-        if !suiv_node || j_date(cur_node.end_at) >= @date
-          if j_date(cur_node.begin_at) <= @date && j_date(cur_node.end_at) >= @date
-            @node.real = true
-          else
-            @node.id = undefined
-          new_lat = cur_lat
-          new_lng = cur_lng
-        else
-          @node.id = undefined
-          # We are between two nodes
-          diff_lat = suiv_node.latitude - cur_lat
-          diff_lng = suiv_node.longitude - cur_lng
-          diff_dates = jv_date(suiv_node.begin_at) - jv_date(cur_node.end_at)
-          diff_dates_current = jv_date(@date) - jv_date(cur_node.end_at)
-          if diff_dates == 0
-            # Nodes times are touching each other (Teleportation)
-            # Whoops ! Warning: Div by 0 !
-            new_lat = cur_lat
-            new_lng = cur_lng
-          else
-            new_lat = cur_lat + diff_lat * (diff_dates_current/diff_dates)
-            new_lng = cur_lng + diff_lng * (diff_dates_current/diff_dates)
-          #console.log("char:", @character.name)
-          #console.log('latlng:', cur_lat, cur_lng, new_lat, new_lng)
-          #console.log('diffs:', diff_lat, diff_lng, diff_dates, diff_dates_current)
-        @node.latitude = new_lat
-        @node.longitude = new_lng
-        @node_obj.update_node(@node)
-      # Update @node to correspond with the date and update or recreate
-      update_node_obj: ->
-        @choose_node()
-        if @next_node_index != -1
-          if @last_node_index != @next_node_index
-            if @node_obj
-              # node has changed
-              @change_node()
-            else
-              # first time
-              @create_node_obj()
-          else
-            # Same node as before
-            @update_node_obj_position()
-        else
-          @destroy_node_obj()
-
-      # Update @next_node_index with the index of the node just before the date,
-      # or the first node if the date is before
-      choose_node: ->
-        if @date && @nodes.length > 0
-          node_index = 0
-          # Functions
-          search_forward = =>
-            while @nodes[node_index+1] && (j_date(@nodes[node_index+1].begin_at) <= @date)
-              node_index += 1
-          search_backward = =>
-            while @nodes[node_index-1] && (j_date(@nodes[node_index-1].begin_at) > @date)
-              node_index -= 1
-            if @nodes[node_index-1]
-              node_index -= 1
-          # Core
-          if @last_node_index == -1
-            search_forward()
-          else
-            node_index = @last_node_index
-            if (j_date(@nodes[node_index].begin_at) < @date)
-              search_forward()
-            else
-              search_backward()
-          @next_node_index = node_index
-        else
-          @next_node_index = -1
-
-      # Read @next_node_index and create a @node_obj
-      create_node_obj: ->
-        if @next_node_index != -1
-          # Copy the node in a new object
-          @node = $.extend({}, @nodes[@next_node_index])
-          if j_date(@node.begin_at) <= @date && j_date(@node.end_at) >= @date
-            # Date is between nood.begin_at and node.end_at
-            @node.real = true
-          else
-            @node.real = false
-            @node.id = undefined
-          @node_obj = new Node(@node)
-        @last_node_index = @next_node_index
-      # Delete the @node_obj
-      destroy_node_obj: ->
-        if @node_obj
-          @node_obj.destroy()
-          delete @node_obj
-      destroy: ->
-        @destroy_node_obj()
-        delete @node
-
-      # Change the date of the character ->
-      # it updates his node with this date
-      update_date: (new_date) ->
-        @date = new_date
-        @update_node_obj()
-      # Updates all the properties of a character, even his descriptions and nodes
-      # WARNING : For the moment, do exactly the same than the next function.
-      # Reflection needed.
-      update_character: (ch) ->
-        @character = ch
-        @update_character_nodes(ch)
-      # Refetch nodes, remove node_obj and recreate it
-      update_character_nodes: (ch) ->
-        @character = ch
-        @update_nodes()
-        @update_node_obj()
-      # Take @character.node_ids and fetch nodes from nodes_by_id
-      update_nodes: ->
-        @last_node_index = -1
-        @next_node_index = -1
-        @nodes = []
-        for node_id in @character.node_ids
-          @nodes.push(nodes_by_id[node_id])
-
-      # Update the character on the server
-      update_on_server: ->
-        $.ajax
-          type: "PUT"
-          url: "/characters/"+@character.id
-          data:
-            character: @character
-          dataType: "json"
-          success: =>
-            console.log "Character #{@id} updated on server."
-          error: =>
-            alert "Error on server ! Character #{@id} update failed."
-
-    ###########################
-    # Structure to gathering node_objs
-    ###########################
-    class NodeCollection # Not RockCollection, man
-      constructor: () ->
-        # Initiate the hash table
-        @node_objs_by_id = {}
-        @node_objs = []
-
-      # [ Called by node_obj creation, node_obj maj
-      register: (node_obj)->
-        @node_objs.push(node_obj)
-        id = node_obj.node.id
-        if id
-          @node_objs_by_id[id] = node_obj
-
-      # [ Called by node_obj_maj, node_obj_destruction
-      remove: (node_obj) ->
-        id = node_obj.node.id
-        if id
-          @node_objs_by_id[id] = null
-          delete @node_objs_by_id[id]
-        # Remove node_obj from @node_objs
-        index = -1
-        for nd_obj, ind in @node_objs
-          if nd_obj == node_obj
-            index = ind
-        if (index > -1)
-          @node_objs.splice(index, 1)
-
-      # [ Called before register
-      fetch: (id) ->
-        node_obj = @node_objs_by_id[id]
-        if node_obj
-          node_obj
-        else
-          null
-
-      # [ Called by d&d, at end, we add character1 to node_obj2
-      closest: (node_obj) ->
-        closest = null
-        dist = 40 # Set max distance before keeping a node # TODO : Adjust
-        for n_obj in @node_objs
-          if n_obj != node_obj # TODO Verify, else add new ids
-            cur_dist = node_obj.distance_from(n_obj)
-            if cur_dist < dist
-              closest = n_obj
-              dist = cur_dist
-        closest
-
-    node_collection = new NodeCollection
-
-    ###########################
-    # Class corresponding to a Marker on the map
-    # TODO : Rename Node -> Marker, node_obj -> marker
-    # @node: node model from the server. If @node.real is true, @node correspond s
-    # truly to a model in the db, else, it's a fake node.
-    # @marker: marker on the map, class from Leaflet
-    # @characters: characters models fetch by ids in @node.character_ids
-    # @character_names: Property calculated from @characters
-    ###########################
-    class Node
-      # Create a marker on the map
-      constructor: (@node) ->
-        @targeted = false
-        @update_characters()
-        @marker = L.marker([@node.latitude, @node.longitude],
-          {"draggable": true, "title": @character_names})
-
-        @on_map = false
-        if !node_collection.fetch(@node.id)
-          node_collection.register(this)
-          map.addLayer(@marker)
-          @on_map = true
-        else
-          console.log "#{@node.id} already present"
-
-        @resize_marker()
-
-        @marker.on('drag', (e) =>
-          closest = node_collection.closest(this)
-          @deselect_target()
-          if closest
-            @select_target(closest)
-        )
-        @marker.on('dragend', (e) =>
-          if @target
-            # Fuuuuusion !
-            console.log('Fuuuuusion')
-            concerned_ch_id = null
-            if @node.character_ids.length == 1
-              concerned_ch_id = @node.character_ids[0]
-            else
-              concerned_ch_id = main_character.id
-
-            # Join points and create a node
-            if @target.node.real
-              # Update it
-              if @node.real
-                # Target and source are real
-                # TODO : two solutions : transfer all or just the main character
-                # Remove character from the source
-                @remove_character(concerned_ch_id)
-                # Update target adding a character
-                @target.add_character(concerned_ch_id)
-              else
-                # Only target is real
-                # Update it adding a character
-                @target.add_character(concerned_ch_id)
-            else
-              target_ch_id = @target.node.character_ids[0]
-              if @node.real
-                # Only source is real
-                # Deplace source and update it adding a character
-                @node.latitude = @target.node.latitude
-                @node.longitude = @target.node.longitude
-                @add_character(target_ch_id)
-              else
-                # Nodes aren't real
-                # Create a new node
-                @node.latitude = @target.node.latitude
-                @node.longitude = @target.node.longitude
-                @set_node_default_values()
-                @node.character_ids.push(target_ch_id)
-                @update_characters()
-                @create_on_server()
-            @deselect_target()
-          else
-            console.log("Deplacement")
-            latlng = @marker.getLatLng()
-            @node.latitude = latlng.lat
-            @node.longitude = latlng.lng
-            if @node.real
-              @update_on_server()
-            else
-              @create_new_real_node()
-        )
-        @popup = false
-        @update_popup()
-        #console.log @node, "created"
-
-      fill_informations: ->
-        @node.title = prompt("Titre")
-        @node.resume = prompt("Résumé")
-        delete @node.topic_id
-      set_default_dates: ->
-        @node.begin_at = character_list.last_date
-        delete @node.end_at
-      set_node_default_values: ->
-        delete @node.id
-        @fill_informations()
-        @set_default_dates()
-      create_new_real_node: ->
-        @set_node_default_values()
-        @create_on_server()
-
-      # Updates the position of the marker on the map
-      update_latlng: ->
-        @marker.setLatLng([@node.latitude, @node.longitude])
-
-      # Distance fro another node_obj
-      distance_from: (node_obj)->
-        latlng = @marker.getLatLng()
-        a = latlng.lat - node_obj.node.latitude
-        b = latlng.lng - node_obj.node.longitude
-        (a*a+b*b)
-
-      select_target: (node_obj)->
-        @target = node_obj
-        node_obj.is_targeted()
-
-      deselect_target: ()->
-        if @target
-          @target.is_untargeted()
-        @target = null
-
-      is_targeted: ->
-        @targeted = true
-        @resize_marker()
-
-      is_untargeted: ->
-        @targeted = false
-        @resize_marker()
-
-      # Update whole content of node (popup, positionn, size)
-      # according to the content of node
-      update_node: (node) ->
-        #console.log("Update node content", @node.title, "->", node.title)
-        if @node.real == node.real && @node.id == node.id
-          # The node is real and hasn't change from the last time
-          # We just make it move (normally not necessary but... ?)
-          @node = node
-          @update_latlng()
-        else
-          console.log(@node.id, "->", node.id)
-
-          # Not the same node as before, update all attributes
-
-          node_collection.remove(this)
-
-          @node = node
-          @update_characters() if @node.real
-
-          if node_collection.fetch(@node.id)
-            # Already present on the map (topic)
-            if @on_map
-              map.removeLayer(@marker)
-              @on_map = false
-          else
-            # Not present, add it
-            node_collection.register(this)
-            if !@on_map
-              map.addLayer(@marker)
-              @on_map = true
-
-          @update_marker()
-
-      # Return the content of the popup of the marker.
-      # Must be null to not create a popup
-      popup_content: ->
-        if @node.real
-          str = ""
-          str += "
#{@node.title}
" if !blank(@node.title)
-          str += "
#{@node.resume}
" if !blank(@node.resume)
-          str += "#{@character_names}" if !blank(@character_names)
-        else
-          null
-
-      # Change the icon of the marker according the importance of the node
-      resize_marker: ->
-        if @targeted
-          # Fusion will come
-          @marker.setIcon(bigPointIcon)
-        else if @node.real && @node.title && @node.title.length > 0
-          # Real node with a title
-          if @node.topic_id
-            # This node include a topic
-            @marker.setIcon(bigPointIcon)
-          else
-            # This node is just an information of deplacement
-            @marker.setIcon(mediumPointIcon)
-        else
-          # Not real node (character is in a traject) or Node has'nt a title
-          @marker.setIcon(smallPointIcon)
-
-      # Remove the marker from the map
-      destroy_popup: ->
-        @marker.closePopup()
-        @marker.unbindPopup()
-        @popup = false
-      # Update the popup of the node, remove it if necessary
-      update_popup: ->
-        cont = @popup_content()
-        if cont?
-          if @popup
-            # Only update content
-            @marker.setPopupContent cont
-          else
-            # Create a new popup
-            @marker.bindPopup cont
-            @popup = true
-        else
-          # Not real node
-          @destroy_popup()
-      update_marker: ->
-        @resize_marker()
-        @update_popup()
-        @update_latlng()
-
-      # Destroy popup and marker
-      destroy: ->
-        @destroy_popup()
-        if @on_map
-          node_collection.remove(this)
-          map.removeLayer(@marker)
-      # Idem + on server
-      really_destroy: ->
-        @delete_on_server()
-        @destroy()
-
-      # Fetch characters model by @node.character_ids
-      # Calculate @character_names
-      update_characters: ->
-        @characters = []
-        for ch_id in @node.character_ids
-          @characters.push(characters_by_id[ch_id])
-        @character_names = ""
-        for character in @characters
-          @character_names += character.name + ' '
-
-      # add a character to the node (serverside too)
-      add_character: (id)->
-        @node.character_ids.push(id)
-        @update_characters()
-        @update_on_server()
-
-      # remove a character from the node
-      # if the node has only one character, it is removed
-      remove_character: (id)->
-        if @node.character_ids.length <= 1
-          @really_destroy()
-        else
-          # TODO TODO TODO
-          # Remove character id from @node.character_ids
-          @update_characters()
-          @update_on_server()
-
-      # Update @node on server
-      update_on_server: ->
-        alert("update a non real node !") unless @node.id != 0
-        @update_marker()
-        $.ajax
-          type: "PUT"
-          url: "/nodes/"+@node.id
-          data:
-            node: @node
-          dataType: "json"
-          success: =>
-            #console.log "Node #{@node.id} updated on server."
-          error: =>
-            alert "Error on server ! Node #{@node.id} can't be updated."
-      # Create @node on server
-      create_on_server: ->
-        #alert('create on server !')
-        @update_marker()
-        $.ajax
-          type: "POST"
-          url: "/nodes/"
-          data:
-            node: @node
-          dataType: "json"
-          success: (a, b, c, d) =>
-            console.log "Node created on server. Res :", a, ",", b, ",", c, ",", d
-            @node.id = null # TODO : set the @node.id
-            @node.real = true
-          error: =>
-            alert "Error on server ! Node can't be created."
-      # Update @node on server
-      delete_on_server: ->
-        alert("delete a non real node !") unless @node.id != 0
-        $.ajax
-          type: "DELETE"
-          url: "/nodes/"+@node.id
-          dataType: "json"
-          success: =>
-            console.log "Node #{@node.id} deleted on server."
-          error: =>
-            alert "Error on server ! Node #{@node.id} can't be deleted."
-
-    character_list = new CharacterList
-
-    ###############
-    # Date Slider #
-    ###############
-    $date_slider = $("#date-slider")
-    $date_value = $("#date-value")
-    from = j_date(json_data($date_slider, "from"))
-    to = j_date(json_data($date_slider, "to"))
-    hour_in_ms = 1000*60*60
-    day_in_ms = hour_in_ms*24
-
-    # Modif the sample to precise the time
-    sample = hour_in_ms
-
-    duration = (to-from)/sample
-
-    # Send the date to the character_list
-    update_date = (date)->
-      $date_value.text(date.toLocaleString())
-      character_list.update_date(date)
-    # Begin to the current date (the last)
-    update_date(to)
-    # Set the slider correctly
-    $date_slider.slider
-      min: 0
-      max: duration
-      value: duration
-      slide: (event, ui) ->
-        new_date = new Date(from.valueOf()+ui.value*sample)
-        update_date(new_date)
-


Include the lastModifiedDate property for uploaded files in AJAX request.

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -115,18 +115,19 @@
   build_content_object = ($uploadForm, file, result) ->
     content = {}
     if result # Use the S3 response to set the URL to avoid character encodings bugs
-      content.url      = $(result).find("Location").text()
-      content.filepath = $('<a />').attr('href', content.url)[0].pathname
-    else # IE <= 9 return a null result object so we use the file object instead
-      domain           = $uploadForm.attr('action')
-      content.filepath = $uploadForm.find('input[name=key]').val().replace('/${filename}', '')
-      content.url      = domain + content.filepath + '/' + encodeURIComponent(file.name)
+      content.url            = $(result).find("Location").text()
+      content.filepath       = $('<a />').attr('href', content.url)[0].pathname
+    else # IE <= 9 retu      rn a null result object so we use the file object instead
+      domain                 = $uploadForm.attr('action')
+      content.filepath       = $uploadForm.find('input[name=key]').val().replace('/${filename}', '')
+      content.url            = domain + content.filepath + '/' + encodeURIComponent(file.name)

-    content.filename   = file.name
-    content.filesize   = file.size if 'size' of file
-    content.filetype   = file.type if 'type' of file
-    content.unique_id  = file.unique_id if 'unique_id' of file
-    content.relativePath = build_relativePath(file) if has_relativePath(file)
+    content.filename         = file.name
+    content.filesize         = file.size if 'size' of file
+    content.lastModifiedDate = file.lastModifiedDate if 'lastModifiedDate' of file
+    content.filetype         = file.type if 'type' of file
+    content.unique_id        = file.unique_id if 'unique_id' of file
+    content.relativePath     = build_relativePath(file) if has_relativePath(file)
     content = $.extend content, settings.additional_data if settings.additional_data
     content