:art: Add describe for PatchIterator::splice

--- spec/patch-spec.coffee
+++ spec/patch-spec.coffee
@@ -30,180 +30,181 @@
         expect(iterator.getSourcePosition()).toEqual sourcePosition
       expect(iterator.next()).toEqual {value: null, done: true}

-    describe "splicing with a positive delta", ->
-      beforeEach ->
-        iterator = patch.buildIterator()
-        iterator.seek(Point(0, 4))
-        iterator.splice(Point(0, 3), "abcde")
-
-      it "inserts new content into the map", ->
-        expect(iterator.getPosition()).toEqual(Point(0, 9))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expectHunks(
-          [null, Point(0, 4), Point(0, 4)]
-          ["abcde", Point(0, 9), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-        iterator.seek(Point(0, 8))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expect(iterator.next()).toEqual(value: "e", done: false)
-        expect(iterator.getPosition()).toEqual(Point(0, 9))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-      it "can apply a second splice that precedes the existing splice", ->
-        iterator.seek(Point(0, 1))
-        iterator.splice(Point(0, 2), "fgh")
-        expect(iterator.getPosition()).toEqual(Point(0, 4))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 3))
-
-        expectHunks(
-          [null, Point(0, 1), Point(0, 1)]
-          ["fgh", Point(0, 4), Point(0, 3)]
-          [null, Point(0, 5), Point(0, 4)]
-          ["abcde", Point(0, 10), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that ends at the beginning of the existing splice", ->
-        iterator.seek(Point(0, 2))
-        iterator.splice(Point(0, 2), "fgh")
-        expect(iterator.getPosition()).toEqual(Point(0, 5))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 5))
-
-        expectHunks(
-          [null, Point(0, 2), Point(0, 2)]
-          ["fghabcde", Point(0, 10), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that spans the beginning of the existing splice", ->
-        iterator.seek(Point(0, 3))
-        iterator.splice(Point(0, 2), "fghi")
-        expect(iterator.getPosition()).toEqual(Point(0, 7))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expectHunks(
-          [null, Point(0, 3), Point(0, 3)]
-          ["fghibcde", Point(0, 11), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that starts at the beginning of the existing splice", ->
-        iterator.seek(Point(0, 4))
-        iterator.splice(Point(0, 2), "fghi")
-        expect(iterator.getPosition()).toEqual(Point(0, 8))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expectHunks(
-          [null, Point(0, 4), Point(0, 4)]
-          ["fghicde", Point(0, 11), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that is contained within the existing splice", ->
-        iterator.seek(Point(0, 6))
-        iterator.splice(Point(0, 2), "fghi")
-        expect(iterator.getPosition()).toEqual(Point(0, 10))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expectHunks(
-          [null, Point(0, 4), Point(0, 4)]
-          ["abfghie", Point(0, 11), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that encompasses the existing splice", ->
-        iterator.seek(Point(0, 2))
-        iterator.splice(Point(0, 8), "fghijklmno")
-        expect(iterator.getPosition()).toEqual(Point(0, 12))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 8))
-
-        expectHunks(
-          [null, Point(0, 2), Point(0, 2)]
-          ["fghijklmno", Point(0, 12), Point(0, 8)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that ends at the end of the existing splice", ->
-        iterator.seek(Point(0, 6))
-        iterator.splice(Point(0, 3), "fghij")
-        expect(iterator.getPosition()).toEqual(Point(0, 11))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expectHunks(
-          [null, Point(0, 4), Point(0, 4)]
-          ["abfghij", Point(0, 11), Point(0, 7)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that spans the end of the existing splice", ->
-        iterator.seek(Point(0, 8))
-        iterator.splice(Point(0, 4), "fghijk")
-        expect(iterator.getPosition()).toEqual(Point(0, 14))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 10))
-
-        expectHunks(
-          [null, Point(0, 4), Point(0, 4)]
-          ["abcdfghijk", Point(0, 14), Point(0, 10)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-      it "can apply a second splice that follows the existing splice", ->
-        iterator.seek(Point(0, 12))
-        iterator.splice(Point(0, 3), "fghij")
-        expect(iterator.getPosition()).toEqual(Point(0, 17))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 13))
-
-        expectHunks(
-          [null, Point(0, 4), Point(0, 4)]
-          ["abcde", Point(0, 9), Point(0, 7)]
-          [null, Point(0, 12), Point(0, 10)]
-          ["fghij", Point(0, 17), Point(0, 13)]
-          [null, Point.infinity(), Point.infinity()]
-        )
-
-    describe "splicing with a negative delta", ->
-      iterator = null
-
-      beforeEach ->
-        iterator = patch.buildIterator()
-        iterator.seek(Point(0, 2))
-        iterator.splice(Point(0, 5), "abc")
-
-      it "inserts new content into the map", ->
-        expect(iterator.getPosition()).toEqual(Point(0, 5))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        expectHunks(
-         [null, Point(0, 2), Point(0, 2)]
-         ["abc", Point(0, 5), Point(0, 7)]
-         [null, Point.infinity(), Point.infinity()]
-        )
-
-        iterator.seek(Point(0, 5))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-        iterator.seek(Point(0, 3))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 3))
-
-        expect(iterator.next()).toEqual(value: "bc", done: false)
-        expect(iterator.getPosition()).toEqual(Point(0, 5))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
-
-      it "can apply a second splice that is contained within the existing splice", ->
-        iterator.seek(Point(0, 3))
-        iterator.splice(Point(0, 1), "")
-        expect(iterator.getPosition()).toEqual(Point(0, 3))
-        expect(iterator.getSourcePosition()).toEqual(Point(0, 3))
-
-        expectHunks(
-         [null, Point(0, 2), Point(0, 2)]
-         ["ac", Point(0, 4), Point(0, 7)]
-         [null, Point.infinity(), Point.infinity()]
-        )
+    describe "::splice(extent, content)", ->
+      describe "splicing with a positive delta", ->
+        beforeEach ->
+          iterator = patch.buildIterator()
+          iterator.seek(Point(0, 4))
+          iterator.splice(Point(0, 3), "abcde")
+
+        it "inserts new content into the map", ->
+          expect(iterator.getPosition()).toEqual(Point(0, 9))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expectHunks(
+            [null, Point(0, 4), Point(0, 4)]
+            ["abcde", Point(0, 9), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+          iterator.seek(Point(0, 8))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expect(iterator.next()).toEqual(value: "e", done: false)
+          expect(iterator.getPosition()).toEqual(Point(0, 9))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+        it "can apply a second splice that precedes the existing splice", ->
+          iterator.seek(Point(0, 1))
+          iterator.splice(Point(0, 2), "fgh")
+          expect(iterator.getPosition()).toEqual(Point(0, 4))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 3))
+
+          expectHunks(
+            [null, Point(0, 1), Point(0, 1)]
+            ["fgh", Point(0, 4), Point(0, 3)]
+            [null, Point(0, 5), Point(0, 4)]
+            ["abcde", Point(0, 10), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that ends at the beginning of the existing splice", ->
+          iterator.seek(Point(0, 2))
+          iterator.splice(Point(0, 2), "fgh")
+          expect(iterator.getPosition()).toEqual(Point(0, 5))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 5))
+
+          expectHunks(
+            [null, Point(0, 2), Point(0, 2)]
+            ["fghabcde", Point(0, 10), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that spans the beginning of the existing splice", ->
+          iterator.seek(Point(0, 3))
+          iterator.splice(Point(0, 2), "fghi")
+          expect(iterator.getPosition()).toEqual(Point(0, 7))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expectHunks(
+            [null, Point(0, 3), Point(0, 3)]
+            ["fghibcde", Point(0, 11), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that starts at the beginning of the existing splice", ->
+          iterator.seek(Point(0, 4))
+          iterator.splice(Point(0, 2), "fghi")
+          expect(iterator.getPosition()).toEqual(Point(0, 8))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expectHunks(
+            [null, Point(0, 4), Point(0, 4)]
+            ["fghicde", Point(0, 11), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that is contained within the existing splice", ->
+          iterator.seek(Point(0, 6))
+          iterator.splice(Point(0, 2), "fghi")
+          expect(iterator.getPosition()).toEqual(Point(0, 10))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expectHunks(
+            [null, Point(0, 4), Point(0, 4)]
+            ["abfghie", Point(0, 11), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that encompasses the existing splice", ->
+          iterator.seek(Point(0, 2))
+          iterator.splice(Point(0, 8), "fghijklmno")
+          expect(iterator.getPosition()).toEqual(Point(0, 12))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 8))
+
+          expectHunks(
+            [null, Point(0, 2), Point(0, 2)]
+            ["fghijklmno", Point(0, 12), Point(0, 8)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that ends at the end of the existing splice", ->
+          iterator.seek(Point(0, 6))
+          iterator.splice(Point(0, 3), "fghij")
+          expect(iterator.getPosition()).toEqual(Point(0, 11))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expectHunks(
+            [null, Point(0, 4), Point(0, 4)]
+            ["abfghij", Point(0, 11), Point(0, 7)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that spans the end of the existing splice", ->
+          iterator.seek(Point(0, 8))
+          iterator.splice(Point(0, 4), "fghijk")
+          expect(iterator.getPosition()).toEqual(Point(0, 14))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 10))
+
+          expectHunks(
+            [null, Point(0, 4), Point(0, 4)]
+            ["abcdfghijk", Point(0, 14), Point(0, 10)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+        it "can apply a second splice that follows the existing splice", ->
+          iterator.seek(Point(0, 12))
+          iterator.splice(Point(0, 3), "fghij")
+          expect(iterator.getPosition()).toEqual(Point(0, 17))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 13))
+
+          expectHunks(
+            [null, Point(0, 4), Point(0, 4)]
+            ["abcde", Point(0, 9), Point(0, 7)]
+            [null, Point(0, 12), Point(0, 10)]
+            ["fghij", Point(0, 17), Point(0, 13)]
+            [null, Point.infinity(), Point.infinity()]
+          )
+
+      describe "splicing with a negative delta", ->
+        iterator = null
+
+        beforeEach ->
+          iterator = patch.buildIterator()
+          iterator.seek(Point(0, 2))
+          iterator.splice(Point(0, 5), "abc")
+
+        it "inserts new content into the map", ->
+          expect(iterator.getPosition()).toEqual(Point(0, 5))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          expectHunks(
+           [null, Point(0, 2), Point(0, 2)]
+           ["abc", Point(0, 5), Point(0, 7)]
+           [null, Point.infinity(), Point.infinity()]
+          )
+
+          iterator.seek(Point(0, 5))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+          iterator.seek(Point(0, 3))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 3))
+
+          expect(iterator.next()).toEqual(value: "bc", done: false)
+          expect(iterator.getPosition()).toEqual(Point(0, 5))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 7))
+
+        it "can apply a second splice that is contained within the existing splice", ->
+          iterator.seek(Point(0, 3))
+          iterator.splice(Point(0, 1), "")
+          expect(iterator.getPosition()).toEqual(Point(0, 3))
+          expect(iterator.getSourcePosition()).toEqual(Point(0, 3))
+
+          expectHunks(
+           [null, Point(0, 2), Point(0, 2)]
+           ["ac", Point(0, 4), Point(0, 7)]
+           [null, Point.infinity(), Point.infinity()]
+          )

     describe "::seek(position)", ->
       it "stops at the first hunk that starts at or contains the given position", ->


Minor improvements on the params handling

--- library/membrane/bilateral.coffee
+++ library/membrane/bilateral.coffee
@@ -112,15 +112,12 @@
             return unless reference of (@uplinks or {})
             assert mangled = "#{@location}/#{reference}"
             mangled += "/#{nsp}" if _.isString nsp = @nsp
-            @socket.on mangled, (args..., callback) =>
-                args.push c unless _.isFunction c = callback
-                callback = null unless _.isFunction callback
+            assert value; return @socket.on mangled, =>
                 logger.info uplinking, reference, nsp
-                assert args; return value i(args)..., =>
-                    return c(o(arguments)...) if c = callback
+                return value i(arguments)..., (parameters...) =>
                     id = @socket.sacks = (@socket.sacks ?= 0) + 1
                     packet = type: "ack", name: mangled, ack: "data"
-                    _.extend packet, ackId: id, args: o(arguments)
+                    _.extend packet, ackId: id, args: o(parameters)
                     assert packet.ackId; @socket.packet packet

     # This is a complementary part of the bilateral implementation.


Still one bug: Japanese / Chinese Input Method

--- jquery.colorfy.coffee
+++ jquery.colorfy.coffee
@@ -2,6 +2,7 @@
 #
 # For the Japanese / Chinese input method issue
 # http://stackoverflow.com/questions/1391278/contenteditable-change-events
+# https://github.com/makesites/jquery-contenteditable
 #
 # For the cursor issue
 # ;(
@@ -90,7 +91,6 @@
         newNode = createNode(remainder, @htmlfier, @descriptor.slice(0), null)
         @subnodes.push(newNode)
     node.processed = true
-
   return node

 htmlfy = (dataText) ->
@@ -101,7 +101,8 @@
   dataText = dataText.replace(/'/g, ''')   # ' -> '
   dataText = dataText.replace(/\//g, '/')  # / -> /
   dataText = dataText.replace(/\n/g, '
')    # \n ->

-  dataText = dataText.replace(/ /g, ' ')   # ' ' ->
+  # Coffee cannot compile if use literal '/ /g'
+  dataText = dataText.replace(new RegExp(' ', 'g'), ' ')   # ' ' ->
   return dataText

 colorfy = (dataText, descriptor, htmlfier) ->
@@ -125,27 +126,36 @@
   formattedText = formattedText.replace(/ /g, ' ')           #   -> ' '
   return formattedText

-cursorLocationFromNodeAndOffset = (rootNode, anchorNode, anchorOffset) ->
-  location = 0
-  if rootNode == anchorNode
-    if rootNode.nodeType == Node.TEXT_NODE
-      location += anchorOffset
-    else if rootNode.tagName == "SPAN"
-      for i in [0..anchorOffset] by 1
-        location += cursorLocationFromNodeAndOffset(rootNode.childNodes[i], anchorNode, anchorOffset)
-  else
-    if rootNode.nodeType == Node.TEXT_NODE
-      location += rootNode.nodeValue.length
-    else if rootNode.tagName == "BR"
-      location += 1
-    else
-      for childNode in rootNode.childNodes
-        if childNode == anchorNode
-          location += cursorLocationFromNodeAndOffset(childNode, anchorNode, anchorOffset)
-          return location
-        else
-          location += cursorLocationFromNodeAndOffset(childNode, anchorNode, anchorOffset)
-  return location
+# Common ancestor of two nodes, algorithm from
+# http://stackoverflow.com/questions/3960843/how-to-find-the-nearest-common-ancestors-of-two-or-more-nodes
+parentsOfNode = (node) ->
+  nodes = [node]
+  while node = node.parentNode
+    nodes.unshift(node)
+  return nodes
+
+commonAncestorOfTwoNodes = (node1, node2) ->
+  parents1 = parentsOfNode(node1)
+  parents2 = parentsOfNode(node2)
+  return null if parents1[0] != parents2[0]
+  for i in [0...parents1.length] by 1
+    if parents1[i] != parents2[i]
+      return parents1[i - 1]
+
+cursorLocationForRootNodeFromAnchorNodeAndOffset = (rootNode, anchorNode, anchorOffset, currentNode) ->
+  if !currentNode
+    currentNode = rootNode
+  if currentNode == anchorNode
+    return lengthOfNodeToOffset(anchorNode, anchorOffset)
+  else if !currentNode.contains(anchorNode) && rootNode.contains(commonAncestorOfTwoNodes(currentNode, anchorNode)) && (currentNode.compareDocumentPosition(anchorNode) == Node.DOCUMENT_POSITION_FOLLOWING) # if currentNode is before anchorNode
+    return lengthOfNode(currentNode)
+  else if !currentNode.contains(anchorNode) && rootNode.contains(commonAncestorOfTwoNodes(currentNode, anchorNode)) && (currentNode.compareDocumentPosition(anchorNode) == Node.DOCUMENT_POSITION_PRECEDING) # If rootNode is after anchor node, not make sence
+    return 0
+  else if currentNode.contains(anchorNode)
+    location = 0
+    for childNode in currentNode.childNodes
+      location += cursorLocationForRootNodeFromAnchorNodeAndOffset(rootNode, anchorNode, anchorOffset, childNode)
+    return location

 lengthOfNode = (node) ->
   if node.nodeType == Node.TEXT_NODE
@@ -156,6 +166,19 @@
     length = 0
     for childNode in node.childNodes
       length += lengthOfNode(childNode)
+    return length
+
+lengthOfNodeToOffset = (node, offset) ->
+  if node.nodeType == Node.TEXT_NODE
+    return offset
+  else if node.tagName == "BR"
+    # Hope this never happens
+    console.log("Is this correct behavior?")
+    return offset
+  else if (node.tagName == "SPAN") || (node.tagName == "DIV")
+    length = 0
+    for i in [0...offset] by 1
+      length += lengthOfNode(node.childNodes[i])
     return length

 nodeAndOffsetFromCursorLocation = (location, node) ->
@@ -179,8 +202,8 @@
   anchorNode = sel.anchorNode
   anchorOffset = sel.anchorOffset
   return unless plainObject.contains(anchorNode)
-  cursorLo = cursorLocationFromNodeAndOffset(plainObject, anchorNode, anchorOffset)
-  console.log("CursorLo: #{cursorLo}")
+  cursorLo = cursorLocationForRootNodeFromAnchorNodeAndOffset(plainObject, anchorNode, anchorOffset)
+#  cursorLo = cursorLocationFromNodeAndOffset(plainObject, anchorNode, anchorOffset)
   jObject.data("cursorlocation", cursorLo)

@@ -191,8 +214,6 @@
   return unless sel.isCollapsed
   # return unless plainObject.contains(sel.anchorNode)
   [anchorNode, anchorOffset] = nodeAndOffsetFromCursorLocation(jObject.data("cursorlocation"), plainObject)
-  console.log(anchorNode)
-  console.log(anchorOffset)
   if anchorNode && anchorOffset >= 0
     sel.collapse(anchorNode, anchorOffset)

@@ -214,7 +235,7 @@
   # Insert the fake text area
   this.after(div)
   # Hide the original and real one
-  this.css("display", "none")
+  # this.css("display", "none")

   # Event Binding
   # to keep form and fake div in sync


Actually call method :P

Actually call method :P

--- src/trix/views/object_view.coffee
+++ src/trix/views/object_view.coffee
@@ -64,7 +64,7 @@
     @getViewCache()?[object.toKey()] = view

   garbageCollectCachedViews: ->
-    if cache = @getViewCache
+    if cache = @getViewCache()
       views = @getAllChildViews().concat(this)
       objectKeys = (view.object.toKey() for view in views)
       delete cache[key] for key of cache when key not in objectKeys


Possible fix to double history slider (#355)

--- client/message.coffee
+++ client/message.coffee
@@ -1129,24 +1129,24 @@
   @diffsub = @subscribe 'messages.diff', @data._id

 Template.messageHistory.onRendered ->
-  @autorun =>
-    diffs = MessagesDiff.find
-        id: @data._id
-      ,
-        sort: ['updated']
-      .fetch()
-    return if diffs.length < 2  ## don't show a zero-length slider
-    ## Accumulate diffs
-    for diff, i in diffs
-      if i >= 0
-        for own key, value of diffs[i-1]
-          unless key of diff
-            diff[key] = value
-      ## Remove diff IDs
-      delete diff._id
-    @slider?.destroy()
-    `import('bootstrap-slider')`.then (Slider) =>
-      Slider = Slider.default
+  `import('bootstrap-slider')`.then (Slider) =>
+    Slider = Slider.default
+    @autorun =>
+      @slider?.destroy()
+      diffs = MessagesDiff.find
+          id: @data._id
+        ,
+          sort: ['updated']
+        .fetch()
+      return if diffs.length < 2  ## don't show a zero-length slider
+      ## Accumulate diffs
+      for diff, i in diffs
+        if i >= 0
+          for own key, value of diffs[i-1]
+            unless key of diff
+              diff[key] = value
+        ## Remove diff IDs
+        delete diff._id
       @slider = new Slider @$('input')[0],
         #min: 0                 ## min and max not needed when using ticks
         #max: diffs.length-1
@@ -1164,7 +1164,7 @@
       #@slider.off 'change'
       @slider.on 'change', (e) =>
         messageHistory.set @data._id, diffs[e.newValue]
-    messageHistory.set @data._id, diffs[diffs.length-1]
+      messageHistory.set @data._id, diffs[diffs.length-1]

 uploader = (template, button, input, callback) ->
   Template[template].events {


Fix zero rotation case

Fix zero rotation case

--- client/message.coffee
+++ client/message.coffee
@@ -489,8 +489,8 @@
         image.style.transform = "translateY(#{(height - image.height)/2}px) scale(#{scale}) rotate(#{-settings.rotate}deg)"
         image.parentNode.style.height = "#{height}px"
       else
-        image.style.transform = undefined
-        image.parentNode.style.height = undefined
+        image.style.transform = null
+        image.parentNode.style.height = null
   @autorun =>
     data = Template.currentData()
     @imageTransform()


Fix on Android

Fix on Android

--- client/message.coffee
+++ client/message.coffee
@@ -290,7 +290,9 @@
       switch sharejsEditor
         when 'cm'
           editor.getInputField().setAttribute 'tabindex', 1 + 20 * ti.count + 19
-          editor.setOption 'styleActiveLine', true
+          ## styleActiveLine is currently buggy on Android.
+          if 0 > navigator.userAgent.toLowerCase().indexOf 'android'
+            editor.setOption 'styleActiveLine', true
           editor.setOption 'matchBrackets', true
           editor.setOption 'lineWrapping', true
           editor.setOption 'lineNumbers', true
@@ -311,9 +313,6 @@
                 theme()
           editor.setOption 'extraKeys',
             Enter: 'newlineAndIndentContinueMarkdownList'
-          #editor.setShowFoldWidgets true
-          #editor.setOption 'mode', 'javascript'
-          #require 'codemirror/mode/javascript/javascript'
         when 'ace'
           editor.textInput.getElement().setAttribute 'tabindex', 1 + 20 * ti.count + 19
           #editor.meteorData = @  ## currently not needed, also dunno if works


Fix routing

Fix routing

--- client/project.coffee
+++ client/project.coffee
@@ -5,7 +5,7 @@
     logger.debug('Getting active tab')
     tabNameMatch = /^.+#([^#]+)$/.exec(@url)
     tabName = if tabNameMatch? then tabNameMatch[1] else 'description'
-    if tabName not in ['description', 'instructions']
+    if tabName not in ['description', 'instructions', 'files']
       tabName = 'description'
     logger.debug("Current tab name: '#{tabName}'")
     @state.set('activeTab', tabName)


make test runnable on a different port from default

--- test/phabs_feeds_test.coffee
+++ test/phabs_feeds_test.coffee
@@ -34,6 +34,7 @@
     process.env.PHABRICATOR_URL = 'http://example.com'
     process.env.PHABRICATOR_API_KEY = 'xxx'
     process.env.PHABRICATOR_BOT_PHID = 'PHID-USER-xxx'
+    process.env.PORT = 80800
     room = helper.createRoom()
     room.robot.brain.userForId 'user', {
       name: 'user'
@@ -296,10 +297,9 @@
     context 'with invalid payload', ->
       beforeEach (done) ->
         do nock.enableNetConnect
-        process.env.EXPRESS_PORT = 8080
         options = {
           host: 'localhost',
-          port: 8080,
+          port: process.env.PORT,
           path: '/hubot/phabs/feeds',
           method: 'POST',
           headers: {
@@ -317,10 +317,9 @@
     context 'with valid payload', ->
       beforeEach (done) ->
         do nock.enableNetConnect
-        process.env.EXPRESS_PORT = 8080
         options = {
           host: 'localhost',
-          port: 8080,
+          port: process.env.PORT,
           path: '/hubot/phabs/feeds',
           method: 'POST',
           headers: {


Added scrape function with header

Added scrape function with header

--- source/Downloader.coffee
+++ source/Downloader.coffee
@@ -53,3 +53,8 @@
     source = 'http://123savemp3.net' + downloads[index]
     winston.info('Should download: ' + name)
     downloadTrack(name, source)
+
+# Function to scrape the given page. Note
+# that it should be one on 123savemp3 with
+# a query format.
+scrape = (source) ->


Added inquirer prompt

Added inquirer prompt

--- source/Downloader.coffee
+++ source/Downloader.coffee
@@ -41,3 +41,9 @@
 # the given selection.
 askForTrack = (titles, links) ->
   winston.info('Ask user to select track.')
+  inquirer.prompt [
+    type: 'list'
+    name: 'song'
+    message: 'Choose a song to download:'
+    choices: titles
+  ], (result) ->


add more methods to the dal SchemaFactory mock

--- test/server/mocks/dal.coffee
+++ test/server/mocks/dal.coffee
@@ -6,4 +6,10 @@
   crudFactory: crudFactory
   modelFactory: modelFactory
   schemaFactory: (def) ->
-    def.schemaDefinition+    res = def.schemaDefinition
+    res.virtual = () ->
+      get: ->
+      set: ->
+    res.methods = {}
+    res.pre = () ->
+    res


Suppress selection sounds in the level editor.

--- app/views/editor/level/thangs/ThangsTabView.coffee
+++ app/views/editor/level/thangs/ThangsTabView.coffee
@@ -213,6 +213,7 @@
     }
     @surface.playing = false
     @surface.setWorld @world
+    @surface.lankBoss.suppressSelectionSounds = true
     @centerCamera()

   centerCamera: ->


[data] use templating to form images path for twitter and facebook images; fix non-root-relative path

--- source/data/index.coffee
+++ source/data/index.coffee
@@ -39,10 +39,10 @@
       # Add any other social services following same pattern
       twitter:
         handle: pkg.twitter
-        image: imagesPath + '/twitter.png'
+        image: "/#{imagesPath}/twitter.png"
         url: "https://twitter.com/#{pkg.twitter}"
       facebook:
-        image: imagesPath + '/facebook.png'
+        image: "/#{imagesPath}/facebook.png"
         url: 'https://www.facebook.com/Lotus-TM-647393298791066/'
     env:
       production: config('env.production')


Guard against desctructed view.

Guard against desctructed view.

--- src/views/position-details.coffee
+++ src/views/position-details.coffee
@@ -84,7 +84,7 @@
                 @rescueUnits[serviceId] = coll
                 deferreds.push @fetchRescueUnits(coll, serviceId, coords)
             $.when(deferreds...).done =>
-                @render()
+                unless @isDestroyed then @render()
         fetchRescueUnits: (coll, sid, coords) ->
             coll.pageSize = 5
             distance = 1000


Sanitize support for upsert

Sanitize support for upsert

--- src/Collections.coffee
+++ src/Collections.coffee
@@ -284,7 +284,7 @@
   simulateModifierUpdate: (doc, modifier) ->
     # TODO(aramk) If non-modifier properties are passed, this can result in them being merged at
     # times, though it should be throwing an error in mongo.
-    if Object.keys(modifier).length > 1 && !modifier.$set? && !modifier.$unset?
+    if _.keys(modifier).length > 1 && !modifier.$set? && !modifier.$unset?
       throw new Error('Unexpected keys in modifier.')
     tmpCollection = @createTemporary()
     doc = Setter.clone(doc)
@@ -430,7 +430,7 @@
         updatedDoc = @simulateModifierUpdate(doc, modifier)
         Setter.merge(doc, updatedDoc)

-    collection.before.update (userId, doc, fieldNames, modifier) ->
+    beforeUpdate = (userId, doc, fieldNames, modifier) ->
       updatedDoc = Collections.simulateModifierUpdate(doc, modifier)
       context = {userId: userId}
       # sanitizedDoc = Setter.clone(updatedDoc)
@@ -441,8 +441,14 @@
       # Ensure no fields exist in $unset from $set.
       $unset = modifier.$unset
       if $unset
-        _.each $set, (value, key) ->
+        _.each modifier.$set, (value, key) ->
           delete $unset[key]
+
+    collection.before.update ->
+      beforeUpdate.apply(@, arguments)
+
+    collection.before.upsert (userId, doc, modifier) ->
+      beforeUpdate.call(@, userId, doc, [], modifier)

 ####################################################################################################
 # SCHEMAS


Uncomment AJAX posty bit of report form

Uncomment AJAX posty bit of report form

Reverts small part of 1aa556a6983adf61d52c770f3cf98e265d876cb2

--- _coffee/report.coffee
+++ _coffee/report.coffee
@@ -16,19 +16,19 @@
   save: (opts) ->
     FORM_URL = @url
     console.log @buildJSON()
-    # $.ajax
-    #   url : FORM_URL
-    #   type: "POST"
-    #   data : @buildJSON()
-    #   success: (data, textStatus, jqXHR) ->
-    #     if data.status is "success"
-    #       opts.success(data)
-    #     else
-    #       alert('There was a problem with the data your provided')
-    #       opts.error()
-    #   error: (jqXHR, textStatus, errorThrown) ->
-    #     alert('Oops, something went wrong')
-    #     error()
+    $.ajax
+      url : FORM_URL
+      type: "POST"
+      data : @buildJSON()
+      success: (data, textStatus, jqXHR) ->
+        if data.status is "success"
+          opts.success(data)
+        else
+          alert('There was a problem with the data your provided')
+          opts.error()
+      error: (jqXHR, textStatus, errorThrown) ->
+        alert('Oops, something went wrong')
+        error()

 $(document).ready ->


allow and track multiline changes in the session

Like in the ODE, only changes and insertions of lines are allowed.
If you select m lines and paste n lines, and m > n, the pasted content
is padded with empty lines.

QT can restore both changed and inserted lines.

--- client/session.coffee
+++ client/session.coffee
@@ -1,9 +1,10 @@
 autocompletion = require './autocompletion'
 module.exports = (e, opts = {}) ->

-  # keep track of which lines have been modified and preserve the original content
-  mod = {} # line number -> original content
+  dirty = {} # modified lines: line number -> original content
+             # inserted lines: line number -> 0

+  # history management ("session log"):
   hist = ['']
   histIndex = 0
   histAdd = (lines) -> hist[0] = ''; hist[1...1] = lines; histIndex = 0; return
@@ -29,21 +30,35 @@
     FD: -> histMove -1; return # Forward or Redo
     QT: QT = -> # Quit (and lose changes)
       c = cm.getCursor(); l = c.line
-      if mod[l]?
-        cm.replaceRange mod[l], {line: l, ch: 0}, {line: l, ch: cm.getLine(l).length}, 'D'
-        delete mod[l]; cm.removeLineClass l, 'background', 'modified'; cm.setCursor l + 1, c.ch
+      if dirty[l] == 0
+        if l == cm.lineCount() - 1
+          cm.replaceRange '', {line: l - 1, ch: cm.getLine(l - 1).length}, {line: l, ch: cm.getLine(l).length}, 'D'
+        else
+          cm.replaceRange '', {line: l, ch: 0}, {line: l + 1, ch: 0}, 'D'
+        delete dirty[l]; h = dirty; dirty = {}; for x, y of h then dirty[x - (x > l)] = y
+      else if dirty[l]?
+        cm.replaceRange dirty[l], {line: l, ch: 0}, {line: l, ch: cm.getLine(l).length}, 'D'
+        delete dirty[l]; cm.removeLineClass l, 'background', 'modified'; cm.setCursor l + 1, c.ch
       return
     EP: QT # Exit (and save changes); in this case we are deliberately making QT and EP the same; Esc is an easier shortcut to discover than Shift-Esc
     ER: -> exec 0 # Enter
     TC: -> exec 1 # Trace

   exec = (trace) ->
-    a = [] # pairs of [lineNumber, contentToExecute]
-    for l, s of mod # l: line number, s: original content
-      l = +l; cm.removeLineClass l, 'background', 'modified'; a.push [l, (e = cm.getLine l)]
-      cm.replaceRange s, {line: l, ch: 0}, {line: l, ch: e.length}, 'D'
-    if !a.length then a = [[(l = cm.getCursor().line), cm.getLine l]]
-    a.sort((x, y) -> x[0] - y[0]); es = (for [_, e] in a when e then e); opts.exec? es, trace; mod = {}; histAdd es; return
+    ls = for l of dirty then +l
+    if ls.length
+      ls.sort (x, y) -> x - y
+      es = ls.map (l) -> cm.getLine l # strings to execute
+      ls.reverse().forEach (l) ->
+        cm.removeLineClass l, 'background', 'modified'
+        if dirty[l] == 0
+          cm.replaceRange '', {line: l, ch: 0}, {line: l + 1, ch: 0}, 'D'
+        else
+          cm.replaceRange dirty[l], {line: l, ch: 0}, {line: l, ch: cm.getLine(l).length}, 'D'
+        return
+    else
+      es = [cm.getLine cm.getCursor().line]
+    opts.exec es, trace; dirty = {}; histAdd es; return

   # CodeMirror supports 'dblclick' events but they are unreliable and seem to require rather a short time between the two clicks
   # So, let's track clicks manually:
@@ -54,10 +69,18 @@

   cm.on 'beforeChange', (_, c) ->
     if c.origin != 'D'
-      if (l = c.from.line) != c.to.line || c.text.length > 1
-        c.cancel(); $.alert 'Cannot make changes across multiple lines in session', 'Error'
-      else
-        mod[l] ?= cm.getLine l; cm.addLineClass l, 'background', 'modified'
+      l0 = c.from.line; l1 = c.to.line; m = l1 - l0 + 1; n = c.text.length
+      if n < m then c.update c.from, c.to, c.text.concat(for [0...m - n] then ''); n = m # pad shrinking changes with empty lines
+      if m < n then h = dirty; dirty = {}; for x, y of h then dirty[x + (n - m) * (x > l1)] = y
+      l = l0
+      while l <= l1    then dirty[l] ?= cm.getLine l; l++
+      while l < l0 + n then dirty[l] = 0;             l++
+    return
+
+  cm.on 'change', (_, c) ->
+    if c.origin != 'D'
+      l0 = c.from.line; l1 = c.to.line; m = l1 - l0 + 1; n = c.text.length
+      for l of dirty then cm.addLineClass +l, 'background', 'modified'
     return

   add: (s) ->
@@ -75,7 +98,7 @@
     cm.setOption 'readOnly', false
     cm.setOption 'cursorHeight', 1
     l = cm.lineCount() - 1
-    if (why == 1 && !mod[l]) || why !in [1, 4]
+    if (why == 1 && !dirty[l]?) || why !in [1, 4]
       cm.replaceRange '      ', {line: l, ch: 0}, {line: l, ch: cm.getLine(l).length}, 'D'
     cm.setCursor l, cm.getLine(l).length
     return


accept AppendSessionOutput in chunks

accept AppendSessionOutput in chunks

--- client/session.coffee
+++ client/session.coffee
@@ -60,14 +60,10 @@
     return

   add: (s) ->
-    l = cm.lineCount() - 1
-    cm.replaceRange s, {line: l, ch: 0}, {line: l, ch: cm.getLine(l).length}, 'D'
+    l = cm.lineCount() - 1; s0 = cm.getLine l
+    cm.replaceRange (if cm.getOption 'readOnly' then s0 + s else s), {line: l, ch: 0}, {line: l, ch: s0.length}, 'D'
     cm.setCursor cm.lineCount() - 1, 0
-
-  set: (s) ->
-    l = cm.lineCount() - 1
-    cm.replaceRange s, {line: 0, ch: 0}, {line: l, ch: cm.getLine(l).length}, 'D'
-    cm.setCursor cm.lineCount() - 1, 0
+    return

   noPrompt: ->
     cm.setOption 'readOnly', true


Expose moment

Expose moment

--- client/src/app.coffee
+++ client/src/app.coffee
@@ -4,6 +4,8 @@
 window.$ = jquery;
 lodash = require 'lodash'
 window._ = lodash
+moment = require 'moment'
+window.moment = moment

 require 'angular-material/angular-material.min.css'
 require 'angular-material-data-table/dist/md-data-table.min.css'
@@ -27,9 +29,8 @@
 angularDateInterceptor = require 'angular-date-interceptor'
 trello = require 'angular-trello'

+require 'angular-aria'

-require 'angular-aria'
-require 'moment'
 require 'd3'
 require 'd3-bdc'
 require 'c3'


Trick model into returning image URL for thumb in inquiry modal

--- apps/artwork_2/components/inquiry/bootstrap.coffee
+++ apps/artwork_2/components/inquiry/bootstrap.coffee
@@ -1,7 +1,12 @@
-{ pick } = require 'underscore'
+{ tap, pick } = require 'underscore'

 module.exports = (sd, { artwork }) ->
-  __artwork__ = pick artwork, 'id', 'title', 'partner', 'fair', 'image'
+  __artwork__ = pick artwork, 'id', 'title', 'partner', 'fair', 'images'
   __artwork__.fair = pick __artwork__.fair, 'id', 'name'
   __artwork__.partner = pick __artwork__.partner, 'id', 'name'
+  __artwork__.images = __artwork__.images.map (image) ->
+    tap {}, (x) ->
+      x.versions = ['medium'] # Trick model into returning image URL
+      x.image_url = image.url
+
   sd.INQUIRY = artwork: __artwork__


Fix api has_session listener

Fix api has_session listener

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

   @init: ->
     @_has_session = false
-    ledger.app.on 'wallet:authenticated', ->
+    ledger.app.on 'dongle:unlocked', ->
       Api._has_session = true

   @listener: (event) ->
@@ -18,7 +18,7 @@
   @hasSession: (data) ->
     chrome.runtime.sendMessage {
       command: 'has_session',
-      success: Api._has_session
+      success: @_has_session
     }

   @sendPayment: (data) ->


Tests: IDE: Search: Added openAnExistingFileAndSave test.

--- client/test/lib/ide/search.coffee
+++ client/test/lib/ide/search.coffee
@@ -81,3 +81,34 @@
     ideHelpers.createAndSaveNewFile(browser)

     browser.end()
+
+
+  openAnExistingFileAndSave: (browser) ->
+
+    activeEditorSelector = '.pane-wrapper .kdsplitview-panel.panel-1 .kdtabpaneview.active'
+
+
+    user = helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
+
+    text     = helpers.getFakeText()
+    fileName = ideHelpers.createAndSaveNewFile(browser, text)
+
+    ideHelpers.closeFile(browser, fileName)
+    ideHelpers.openAnExistingFile(browser, user, fileName, text)
+
+    text = helpers.getFakeText()
+
+    ideHelpers.setTextToEditor(browser, text)
+
+    browser
+      .assert.containsText    activeEditorSelector, text # Assertion
+
+    ideHelpers.saveFile(browser)
+    ideHelpers.closeFile(browser, fileName)
+    ideHelpers.openAnExistingFile(browser, user, fileName, text)
+
+    browser
+      .assert.containsText    activeEditorSelector, text # Assertion
+
+    browser.end()


Add end_of_line fixtures to fixtures test helper

--- test/support/fixtures.coffee
+++ test/support/fixtures.coffee
@@ -11,3 +11,6 @@
 	'indent_size/2-spaces.txt': readFixture 'indent_size/2-spaces.txt'
 	'indent_size/4-spaces.txt': readFixture 'indent_size/4-spaces.txt'
 	'indent_size/none.txt': readFixture 'indent_size/none.txt'
+	'end_of_line/lf.txt': readFixture 'end_of_line/lf.txt'
+	'end_of_line/crlf.txt': readFixture 'end_of_line/crlf.txt'
+	'end_of_line/none.txt': readFixture 'end_of_line/none.txt'


Kobler til mongodb replica set "ntb"

Kobler til mongodb replica set "ntb"

--- coffee/turbase.coffee
+++ coffee/turbase.coffee
@@ -4,8 +4,19 @@

 mongodb = require 'mongodb'
 #MongoDB kobling
-server  = new mongodb.Server "127.0.0.1", 27017, {safe:true, auto_reconnect: true}, {}
-db      = new mongodb.Db 'dnt', server, {safe:true}
+#server  = new mongodb.Server "127.0.0.1", 27017, {safe:true, auto_reconnect: true}, {}
+#db      = new mongodb.Db 'ntb', server, {safe:true}
+#db.open(()->)
+
+replSet = new mongodb.ReplSet( [
+  new mongodb.Server( '127.0.0.1', 27017, {}),
+  new mongodb.Server( '127.0.0.1', 27018, {}),
+  new mongodb.Server( '127.0.0.1', 27019, {})
+  ]
+)
+
+db = new mongodb.Db('ntb_03', replSet, {native_parser: true});
+console.log "Kobler seg til mongodb replica set ntb og database ntb_<versjon>"
 db.open(()->)

 exports.get = (req, res) ->
@@ -13,7 +24,6 @@
     res.jsonp "{'error':#{err}}" if err
     res.jsonp "{'error': 'Mangler id'}" if not req.params.id
     collection.find({_id: mongodb.ObjectID(req.params.id), 'eier':req.eier}).toArray (err, result) ->
-      console.log err
       res.jsonp result if result
       res.jsonp err if err


Refactored EntityUtils to separate logic for adding entity json.

--- src/EntityUtils.coffee
+++ src/EntityUtils.coffee
@@ -436,33 +436,33 @@
     args = @_getProjectAndScenarioArgs(args)
     projectId = args.projectId
     scenarioId = args.scenarioId
-    entitiesJson = []
-    jsonIds = []
-
-    addEntity = (entity) ->
-      id = entity.getId()
-      return if jsonIds[id]
-      json = jsonIds[id] = entity.toJson()
-      entitiesJson.push(json)
-
     df = Q.defer()
     entities = @_getEntitiesForJson(args)
     ids = args.ids = _.map entities, (entity) -> entity._id
     unrenderPromises = @_unrenderEntitiesBeforeJson(ids: ids)
     Q.all(unrenderPromises).then Meteor.bindEnvironment =>
       renderPromise = @_renderEntitiesBeforeJson(args)
-      renderPromise.then ->
-        _.each ids, (id) ->
-          entity = AtlasManager.getEntity(id)
-          return unless entity
-          addEntity(entity)
-          _.each entity.getRecursiveChildren(), addEntity
-        _.each entitiesJson, (json) -> json.type = json.type.toUpperCase()
-        df.resolve(c3mls: entitiesJson)
+      renderPromise.then Meteor.bindEnvironment =>
+        Q.when(@_getEntitiesAsJson(args)).then Meteor.bindEnvironment (entitiesJson) ->
+          _.each entitiesJson, (json) -> json.type = json.type.toUpperCase()
+          df.resolve(c3mls: entitiesJson)
       # Unrender all entities when on the server to prevent using old rendered data.
       renderPromise.fin Meteor.bindEnvironment =>
         if Meteor.isServer then _.each ids, (id) => @unrender(id)
     df.promise
+
+  _getEntitiesAsJson: (args) ->
+    jsonMap = {}
+    addEntity = (entity) ->
+      id = entity.getId()
+      return if jsonMap[id]
+      jsonMap[id] = entity.toJson()
+    _.each args.ids, (id) ->
+      entity = AtlasManager.getEntity(id)
+      return unless entity
+      addEntity(entity)
+      _.each entity.getRecursiveChildren(), addEntity
+    _.values(jsonMap)

   _getEntitiesForJson: (args) ->
     if Entities.findByProjectAndScenario?
@@ -470,7 +470,7 @@
     else
       entities = Entities.findByProject(projectId).fetch()

-  _renderEntitiesBeforeJson: (args) -> @_renderBulk(args)
+  _renderEntitiesBeforeJson: (args) -> EntityUtils.renderAll(args)

   _unrenderEntitiesBeforeJson: (args) ->
     if Meteor.isServer


Update es-ES.coffee

Update es-ES.coffee

Traslate: loading_error, concepts and legal

--- app/locale/es-ES.coffee
+++ app/locale/es-ES.coffee
@@ -1506,14 +1506,14 @@
   loading_error:
     could_not_load: "Error al cargar desde el servidor."
     connection_failure: "Fallo en la conexión."
-#    connection_failure_desc: "It doesn’t look like you’re connected to the internet! Check your network connection and then reload this page."
-#    login_required: "Login Required"
-#    login_required_desc: "You need to be logged in to access this page."
+    connection_failure_desc: "No parece que estés conectado a Internet! Comprueba tu conexión de red y vuelva a cargar esta página."
+    login_required: "Necesario iniciar sesión"
+    login_required_desc: "Tiene que estar registrado para acceder a esta página."
     unauthorized: "Tienes que haber iniciado sesión. ¿No permites la instalación de cookies?"
     forbidden: "No tienes autorización."
-#    forbidden_desc: "Oh no, there’s nothing we can show you here! Make sure you’re logged into the correct account, or visit one of the links below to get back to programming!"
+    forbidden_desc: "Oh, no, no hay nada que le podemos mostrar aquí! Asegúrese de que está conectado a la cuenta correcta, o visita uno de los enlaces siguientes para volver a la programación!"
     not_found: "No encontrado."
-#    not_found_desc: "Hm, there’s nothing here. Visit one of the following links to get back to programming!"
+    not_found_desc: "Hm, no hay nada aquí. Visita uno de los siguientes enlaces para volver a la programación!"
     not_allowed: "Método no permitido."
     timeout: "Tiempo de espera del servidor superado." # {change}
     conflict: "Conflicto de recursos."
@@ -1521,7 +1521,7 @@
     server_error: "Error del servidor."
     unknown: "Error desconocido." # {change}
     error: "ERROR"
-#    general_desc: "Something went wrong, and it’s probably our fault. Try waiting a bit and then refreshing the page, or visit one of the following links to get back to programming!"
+    general_desc: "Algo salió mal, y es probable que sea nuestra culpa. Espera un poco y luego actualiza la página, o visita uno de los siguientes enlaces para volver a la programación!"

   resources:
     level: "Nivel"
@@ -1542,16 +1542,16 @@
     arrays: "Vectores"
     basic_syntax: "Sintáxis básica"
     boolean_logic: "Lógica booleanos"
-#    break_statements: "Break Statements"
+    break_statements: "Sentencias Break"
     classes: "Clases"
-#    continue_statements: "Continue Statements"
+    continue_statements: "Sentencias Continue"
     for_loops: "Bucles For"
     functions: "Funciones"
     graphics: "Gráficos"
-#    if_statements: "If Statements"
-#    input_handling: "Input Handling"
+    if_statements: "Sentencias if"
+    input_handling: "Manejo de entradas"
     math_operations: "Operaciones matemáticas"
-#    object_literals: "Object Literals"
+    object_literals: "Objetos literales"
     parameters: "Parámetros"
     strings: "Cadenas"
     variables: "Variables"
@@ -1598,7 +1598,7 @@
     email_settings_url: "tus ajustes de correo electrónico"
     email_description_suffix: "o a través de los enlaces en los correos que te enviemos, puedes cambiar tus preferencias y darte de baja fácilmente en cualquier momento."
     cost_title: "Precio"
-#    cost_description: "CodeCombat is free to play for all of its core levels, with a ${{price}} USD/mo subscription for access to extra level branches and {{gems}} bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
+    cost_description: "CodeCombat es un juego gratuito para todos sus niveles básicos, con una suscripción de ${{price}} USD al mes con acceso a niveles adicionales y un bonus de {{gems}} gemas al mes. Puedes cancelar con un clic, y ofrecemos una garantía de reembolso del 100%."
     copyrights_title: "Copyrights y Licencias"
     contributor_title: "Acuerdo de Licencia del Colaborador"
     contributor_description_prefix: "Todas las colaboraciones, tanto en la web como en nuestro repositorio de GitHub, están sujetas a nuestro"
@@ -1633,8 +1633,8 @@
     nutshell_title: "En una palabra"
     nutshell_description: "Todos los recursos que ofrecemos en el editor de niveles son libres de ser utilizados para crear niveles. Pero nos reservamos el derecho de restringir la distribución de los propios niveles (que se crean en codecombat.com) de modo que se pueda cobrar por ellos en el futuro, si eso es lo que termina sucediendo."
     canonical: "La versión inglesa de este documento es la canónica, la definitiva. Si hay alguna diferencia con lo que pueda aparecer en las traducciones, la versión inglesa es la que prevalece sobre las demás."
-#    third_party_title: "Third Party Services"
-#    third_party_description: "CodeCombat uses the following third party services (among others):"
+    third_party_title: "Servicios de terceros"
+    third_party_description: "CodeCombat utiliza los siguientes servicios de terceros (entre otros):"

   ladder_prizes:
     title: "Premios del Torneo" # This section was for an old tournament and doesn't need new translations now.


Update pt-BR.coffee

Update pt-BR.coffee

--- app/locale/pt-BR.coffee
+++ app/locale/pt-BR.coffee
@@ -1362,12 +1362,12 @@
 #    student_age_range_to: "to"
     create_class: "Criar Classe"
     class_name: "Nome da Turma"
-#    teacher_account_restricted: "Your account is a teacher account and cannot access student content."
-#    account_restricted: "A student account is required to access this page."
-#    update_account_login_title: "Log in to update your account"
-#    update_account_title: "Your account needs attention!"
-#    update_account_blurb: "Before you can access your classes, choose how you want to use this account."
-#    update_account_current_type: "Current Account Type:"
+    teacher_account_restricted: "Sua conta é uma conta de professor e não pode acessar o conteúdo de aluno."
+    account_restricted: "Uma conta de aluno é necessária para acessar esta página."
+    update_account_login_title: "Faça Log in para atualizar sua conta."
+    update_account_title: "Sua conta precisa de atenção!"
+    update_account_blurb: "BAntes de poder acessar suas turmas, escolha como você deseja usar essa conta."
+    update_account_current_type: "Tipo de Conta Atual:"
     update_account_account_email: "Email/Usuário da Conta:"
     update_account_am_teacher: "Eu sou um professor"
 #    update_account_keep_access: "Keep access to classes I've created"


Fix ObjectID parsing

Fix ObjectID parsing

--- coffee/document.coffee
+++ coffee/document.coffee
@@ -4,7 +4,7 @@

 exports.param = (req, res, next, id) ->
   return res.json 400, error: 'Invalid ObjectID' if not /^[a-f0-9]{24}$/.test id
-  req.id = ObjectID.createFromHexString id
+  req.id = new ObjectID id
   next()

 exports.options = (req, res, next) ->


Fixed a bug in resetting the hash back state to empty

--- static/js/vendor/hashstate.coffee
+++ static/js/vendor/hashstate.coffee
@@ -7,6 +7,9 @@

 deep_extend = (target, update) ->
     for subname, value of update
+        if not value?
+            delete target[subname]
+            continue
         if not _.isObject value
             target[subname] = value
             continue
@@ -23,14 +26,15 @@
         handler get_path_val(@state, var_path), undefined

     resetState: (new_state) =>
+        old_state = @state
+        @state = new_state
         for [var_path, handler] in @listeners
-            new_val = get_path_val new_state, var_path
-            old_val = get_path_val @state, var_path
+            new_val = get_path_val @state, var_path
+            old_val = get_path_val old_state, var_path
             if _.isEqual new_val, old_val
                 continue
             # TODO: Don't stop processing on exception
             handler(new_val, old_val)
-        @state = new_state

 # TODO: Hack for old browsers?
 get_state_string = ->
@@ -39,6 +43,11 @@
 set_state_string = (new_hash, history=false) ->
     if new_hash
         new_hash = '?' + new_hash
+    else
+        # History.js (or HTML state API?) won't change
+        # the state at all if we give in an empty string, so we'll
+        # have to resort to having an empty question mark
+        new_hash = '?'
     if history
         History.pushState null, null, new_hash
     else
@@ -63,30 +72,30 @@
         new_hash = saneparam new_state
         set_state_string new_hash, history

-    _onChild: (var_path, handler) ->
+    _onChild: (var_path, handler) =>
         @binder.on var_path, handler

-    get: (var_path=[]) ->
+    get: (var_path=[]) =>
         get_path_val @binder.state, var_path

-    sub: (varname) -> new SubState @, varname
+    sub: (varname) => new SubState @, varname

 class SubState
     constructor: (@parent, @varname) ->

-    update: (changes, history=false) ->
+    update: (changes, history=false) =>
         subchanges = {}
         subchanges[@varname] = changes
         @parent.update subchanges, history

-    get: (var_path=[]) ->
+    get: (var_path=[]) =>
         var_path.unshift(@varname)
         @parent.get var_path

-    on: (handler) ->
+    on: (handler) =>
         @parent._onChild [@varname], handler

-    _onChild: (var_path, handler) ->
+    _onChild: (var_path, handler) =>
         var_path.unshift(@varname)
         @parent._onChild var_path, handler


Save volume between program runs

Save volume between program runs

--- coffee/_player.coffee
+++ coffee/_player.coffee
@@ -274,7 +274,12 @@
   playerContainer
     .find('.controls .volume-icon .action i.fa-volume-down')
     .hide()
-
+  volume_percentage = Settings.get('volume') || 1
+  playerContainer.
+    attr("data-volume", volume_percentage)
+  playerContainer.
+    find('.volume-current').css('width', volume_percentage * 100 + '%')
+  videojs('video_player').volume(volume_percentage)
   playerContainer
     .find('.controls .volume-icon #action i.fa-volume-off')
     .hide()
@@ -305,7 +310,7 @@

   videojs('video_player')
     .volume(percentage)
-
+  Settings.set('volume', percentage)
   playerContainer
     .find('.volume-current')
     .animate({'width': (percentage) * 100 + '%'}, 100, 'easeOutExpo')


removed comment

removed comment

--- coffee/atTable.coffee
+++ coffee/atTable.coffee
@@ -33,11 +33,6 @@
     if not attributes.pagination and not attributes.list
       throw "Either a list or pagination must be specified."

-  # Earlier versions of angular table used 'pagination' instead of 'at-pagination'
-  # to wire a table to a pagination instance. However, this declaration can become
-  # ambiguous (ui-bootstrap for example has a directive called 'pagination'). So
-  # right now, you can either use 'pagination' or 'at-pagination'. The support
-  # for 'pagination' should be dropped some time.
   normalizeInput = (attributes) ->
     if attributes.atPagination
       attributes.pagination = attributes.atPagination


Improve cancel button look

Improve cancel button look

Make cancel button look like other buttons (start, next step, step in, step out).

--- lib/Components/CancelButton.coffee
+++ lib/Components/CancelButton.coffee
@@ -13,6 +13,6 @@
   })

 exports.render = (state) ->
-  h('button.btn.btn-error', {
+  h('button.btn.btn-error.icon-primitive-square', {
     'ev-click': hg.send state.channels.cancel
-  }, ['x'])
+  }, [])


Social: only create group channel

Social: only create group channel

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -1612,7 +1612,7 @@
         callback()

   createSocialApiChannelId: (callback) ->
-    if @socialApiChannelId and @socialApiDefaultChannelId
+    if @socialApiChannelId
       return callback null, @socialApiChannelId
     @fetchOwner (err, owner)=>
       return callback err if err
@@ -1627,17 +1627,9 @@
           creatorId: socialApiId
           privacyConstant: privacy

-        @createDefaultChannel options, (err, defaultChannelId) =>
+        @createGroupChannel options, (err, groupChannelId) =>
           return callback err if err
-          @createGroupChannel options, (err, groupChannelId) =>
-            return callback err if err
-            return callback null, groupChannelId
-
-  createDefaultChannel:(options, callback)->
-    options.varName = "socialApiDefaultChannelId"
-    options.typeConstant = "default"
-
-    return @createSocialAPIChannel options, callback
+          return callback null, groupChannelId

   createGroupChannel:(options, callback)->


Social: added a new relationship for storing an info about the blocked account for that group

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -329,6 +329,9 @@
         owner         :
           targetType  : JAccount
           as          : 'owner'
+        blockedAccount:
+          targetType  : JAccount
+          as          : 'blockedAccount'
         subgroup      :
           targetType  : 'JGroup'
           as          : 'parent'


add image callback

add image callback

--- app/scripts/main.coffee
+++ app/scripts/main.coffee
@@ -31,6 +31,19 @@
         $('#server-status').text ''
         console.log 'unable to autosave'
         editor.setConnecting false
+  imageCallback: (editor)->
+    editor.setConnecting true
+    $.ajax
+      url: 'http://localhost:3000'
+      success: (data)->
+        console.log 'upload success'
+        editor.setConnecting false
+        setTimeout ->
+          $('#server-status').text 'Saved'
+          setTimeout ->
+            $('#server-status').text ''
+          , 3000
+        , 500

 $(document).ready =>
   @Clean.editor.init()


remove stuff from util

remove stuff from util

git-svn-id: 00f01ee4f729ccc52c3e336777c3e81b5adc88db@252893 ef392f78-ee14-0410-84e5-9db46baae5bb

--- app/scripts/util.coffee
+++ app/scripts/util.coffee
@@ -157,12 +157,6 @@
         within = _(output).compact().join()
         return { defaultwithin : defaultWithin, within : within }

-    getMorphology: ->
-        _(@selected).map((corpus) ->
-            morf = corpus.morf or "saldom"
-            morf.split "|"
-        ).flatten().unique().join "|"
-
     getTimeInterval : ->
         all = _(@selected)
             .pluck("time")
@@ -203,7 +197,6 @@
         else
             to = _.max tos, toUnix

-        # c.log "first", infoGetter("FirstDate")
         [from, to]

@@ -427,12 +420,6 @@

     getTitle : (corpus) ->
         @struct[corpus.split("|")[1]].title
-
-
-window.applyTo = (ctrl, f) ->
-    s = getScope(ctrl)
-    s.$apply f(s)
-

 # TODO never use this, remove when sure it is not used
 window.search = (obj, val) ->
@@ -577,22 +564,6 @@
         infixIndex
     ]

-util.sblexArraytoString = (idArray, labelFunction) ->
-    labelFunction = labelFunction or util.lemgramToString
-    return _.map idArray, (lemgram) -> labelFunction lemgram, true
-
-    # TODO: remove this if all is well with the lemgram dropdown
-    # tempArray = $.map(idArray, (lemgram) ->
-    #     labelFunction lemgram, false
-    # )
-    # out = $.map idArray, (lemgram) ->
-    #     isAmbigous = $.grep(tempArray, (tempLemgram) ->
-    #         tempLemgram is labelFunction(lemgram, false)
-    #     ).length > 1
-    #     labelFunction lemgram, isAmbigous
-    # return out
-
-
 util.lemgramRegexp = /\.\.\w+\.\d\d?(\:\d+)?$/
 util.isLemgramId = (lemgram) ->
     lemgram.search(util.lemgramRegexp) isnt -1
@@ -603,10 +574,6 @@
     keys = ["morph", "form", "pos", "index", "startIndex"]
     splitArray = lemgram.match(/((\w+)--)?(.*?)\.\.(\w+)\.(\d\d?)(\:\d+)?$/).slice(2)
     _.object keys, splitArray
-
-util.splitSaldo = (saldo) ->
-    saldo.match util.saldoRegExp
-

 # Add download links for other formats, defined in
 # settings.downloadFormats (Jyrki Niemi <jyrki.niemi@helsinki.fi>
@@ -707,7 +674,6 @@

     return

-# $(window).trigger("hashchange")
 added_corpora_ids = []
 util.loadCorporaFolderRecursive = (first_level, folder) ->
     outHTML = undefined
@@ -742,14 +708,6 @@
             outHTML += "
#{settings.corpora[val].title}
"
     outHTML += "
"
     outHTML
-
-# Helper function to turn 1.2345 into 1,2345 (locale dependent)
-# Use "," instead of "." if Swedish, if mode is
-# Split the string into two parts
-
-#return x.replace(".",'' + decimalSeparator + '');
-
-#return x.replace(".", decimalSeparator);

 # Helper function to turn "8455999" into "8 455 999"
 util.prettyNumbers = (numstring) ->
@@ -787,7 +745,6 @@
             baseLang = settings.corpora[corpusID]?.linked_to
             if baseLang
                 lang = " (" + util.getLocaleString(settings.corpora[corpusID].lang) + ")"
-                #baseLangTag = " (" + settings.corpora[baseLang].lang + ")"
                 baseLangTokenHTML = """#{util.getLocaleString("corpselector_numberoftokens")}: #{util.prettyNumbers(settings.corpora[baseLang].info.Size)}
                  (#{util.getLocaleString(settings.corpora[baseLang].lang)})

                 """
@@ -872,15 +829,6 @@
 window.unregescape = (s) ->
     s.replace /\\/g, ""

-util.localizeFloat = (float, nDec) ->
-    lang = $("#languages").radioList("getSelected").data("lang")
-    sep = null
-    nDec = nDec or float.toString().split(".")[1].length
-    if lang is "sv"
-        sep = ","
-    else sep = "." if lang is "en"
-    $.format("%." + nDec + "f", float).replace ".", sep
-
 util.formatDecimalString = (x, mode, statsmode, stringOnly) ->
     if _.contains(x, ".")
         parts = x.split(".")
@@ -895,25 +843,6 @@
             x
         else
             util.prettyNumbers x
-
-util.makeAttrSelect = (groups) ->
-    arg_select = $("<select/>")
-    $.each groups, (lbl, group) ->
-        return if $.isEmptyObject(group)
-        optgroup = $("<optgroup/>",
-            label: util.getLocaleString(lbl).toLowerCase()
-            rel: $.format("localize[%s]", lbl)
-        ).appendTo(arg_select)
-        $.each group, (key, val) ->
-            return if val.displayType is "hidden"
-            $("<option/>",
-                rel: $.format("localize[%s]", val.label)
-            ).val(key).text(util.getLocaleString(val.label) or "").appendTo(optgroup).data "dataProvider", val
-            return
-
-        return
-
-    arg_select

 util.browserWarn = ->
     $.reject
@@ -971,58 +900,3 @@
             expires: 100000 # Expiration Date (in seconds), 0 (default) means it ends with the current session

     return
-
-util.convertLMFFeatsToObjects = (structure, key) ->
-
-    # Recursively traverse a tree, expanding each "feat" array into a real object, with the key "feat-[att]":
-    if structure?
-        output = null
-        theType = util.findoutType(structure)
-        if theType is "object"
-            output = {}
-            $.each structure, (inkey, inval) ->
-                if inkey is "feat"
-                    innerType = util.findoutType(inval)
-                    if innerType is "array"
-                        $.each inval, (fkey, fval) ->
-                            keyName = "feat_" + fval["att"]
-                            if not output[keyName]?
-                                output[keyName] = fval["val"]
-                            else
-                                if $.isArray(output[keyName])
-                                    output[keyName].push fval["val"]
-                                else
-                                    output[keyName] = [output[keyName], fval["val"]]
-                            return
-
-                    else
-                        keyName = "feat_" + inval["att"]
-                        if not output[keyName]?
-                            output[keyName] = inval["val"]
-                        else
-                            if $.isArray(output[keyName])
-                                output[keyName].push inval["val"]
-                            else
-                                output[keyName] = [output[keyName], inval["val"]]
-                else
-                    output[inkey] = util.convertLMFFeatsToObjects(inval)
-                return
-
-        else if theType is "array"
-            dArr = new Array()
-            $.each structure, (inkey, inval) ->
-                dArr.push util.convertLMFFeatsToObjects(inval)
-                return
-
-            output = dArr
-        else
-            output = structure
-        output
-    else
-        null
-
-util.findoutType = (variable) ->
-    if $.isArray(variable)
-        "array"
-    else
-        typeof (variable)


Rename function to _replaceArrow()

Rename function to _replaceArrow()

--- bootstrap-tour.coffee
+++ bootstrap-tour.coffee
@@ -247,13 +247,13 @@

       # reposition the arrow
       if step.placement == 'bottom' or step.placement == 'top'
-        @_bootstrap_tooltip_replaceArrow(tip, (tipOffset.left-original_left)*2, original_offsetWidth, 'left') if original_left != tipOffset.left
-      else
-        @_bootstrap_tooltip_replaceArrow(tip, (tipOffset.top-original_top)*2, original_offsetHeight, 'top') if original_top != tipOffset.top
+        @_replaceArrow(tip, (tipOffset.left-original_left)*2, original_offsetWidth, 'left') if original_left != tipOffset.left
+      else
+        @_replaceArrow(tip, (tipOffset.top-original_top)*2, original_offsetHeight, 'top') if original_top != tipOffset.top

     # copy pasted from bootstrap-tooltip.js
     # with some alterations
-    _bootstrap_tooltip_replaceArrow: (tip, delta, dimension, position)->
+    _replaceArrow: (tip, delta, dimension, position)->
       tip
         .find(".arrow")
         .css(position, if delta then (50 * (1 - delta / dimension) + "%") else '')


Don't throw when checking for compiler

Don't throw when checking for compiler

--- lib/ghc-mod/ghc-modi-process.coffee
+++ lib/ghc-mod/ghc-modi-process.coffee
@@ -99,21 +99,21 @@
       return opts1
     .then (opts) ->
       stackghc =
-        new Promise (resolve) ->
+        new Promise (resolve, reject) ->
           CP.execFile 'stack', ['ghc', '--', '--version'], opts, (error, stdout, stderr) ->
             if error?
               error.stack = (new Error).stack
-              throw error
+              return reject error
             resolve /version (.+)$/.exec(stdout.trim())[1]
         .catch (error) ->
           Util.warn error
           return null
       pathghc =
-        new Promise (resolve) ->
+        new Promise (resolve, reject) ->
           CP.execFile 'ghc', ['--version'], opts, (error, stdout, stderr) ->
             if error?
               error.stack = (new Error).stack
-              throw error
+              return reject error
             resolve /version (.+)$/.exec(stdout.trim())[1]
         .catch (error) ->
           Util.warn error


animations fall back to default style if invalid animation is passed in

--- lib/mixins/animation-support.coffee
+++ lib/mixins/animation-support.coffee
@@ -8,8 +8,9 @@
     return unless style and component

     style = "#{component}Open#{style}"
+    backupStyle = "#{component}OpenDefault"

-    if $.velocity.Sequences.hasOwnProperty style then style else return
+    if $.velocity.Sequences.hasOwnProperty style then style else backupStyle

   closeAnimation: Em.computed 'computedAnimationStyle', ->
@@ -18,8 +19,9 @@
     return unless style and component

     style = "#{component}Close#{style}"
+    backupStyle = "#{component}CloseDefault"

-    if $.velocity.Sequences.hasOwnProperty style then style else return
+    if $.velocity.Sequences.hasOwnProperty style then style else backupStyle

   computedAnimationStyle: Em.computed 'animationStyle', 'style', ->


Fix marks for deletion

Fix marks for deletion

--- coffee/views/dialog_delete.coffee
+++ coffee/views/dialog_delete.coffee
@@ -15,6 +15,9 @@
       "click .delete-selected": "deleteSelected"
       "click .cancel-delete": "cancelDelete"

+    # XXX: We use here Slow selectors and there is some code
+    # duplication in post/comment views.
+    # Hope this will not be slow as shit.
     MARKED_POST_CLASS: "post-delete-marked"
     MARKED_COMMENT_CLASS: "comment-delete-marked"
     MARKED_SELECTOR:
@@ -24,7 +27,8 @@

     initialize: (options) ->
       super options
-      @pluralForms = if options?.singlePost
+      @singlePost = options?.singlePost
+      @pluralForms = if @singlePost
         ["комментарий", "комментария", "комментариев"]
       else
         ["пост", "поста", "постов"]
@@ -49,17 +53,22 @@
     hide: ->
       @modal.modal "hide"

-    updateMarked: (singlePost = false) ->
-      marked = $(@MARKED_SELECTOR)
-      if marked.length
+    updateMarked: (singlePostMarked = false) ->
+      marked = $(@MARKED_SELECTOR).length
+      if marked
         @show() unless @isVisible()
-        forDelete = if singlePost
-          # Special case: close button on the entire post at the
-          # single post page.
-          $(".#{@MARKED_COMMENT_CLASS}").removeClass(@MARKED_COMMENT_CLASS)
+        # Special case: at the single post page allow select either
+        # engire post or comments.
+        if @singlePost
+          if singlePostMarked
+            $(".#{@MARKED_COMMENT_CLASS}").removeClass(@MARKED_COMMENT_CLASS)
+          else
+            post = $(".#{@MARKED_POST_CLASS}").removeClass(@MARKED_POST_CLASS)
+            marked -= post.length  # Always will be 0 or 1
+        forDelete = if singlePostMarked
           "пост"
         else
-          utils.pluralForm marked.length, @pluralForms
+          utils.pluralForm marked, @pluralForms
         @$(".for-delete").text(forDelete)
       else
         @hide()


Tactician is now limited.

Tactician is now limited.

--- coffeescripts/cards-common.coffee
+++ coffeescripts/cards-common.coffee
@@ -3585,6 +3585,7 @@
             id: 61
             slot: "Crew"
             points: 2
+            limited: true
         }
         {
             name: "R2-D2 (Crew)"


Fix remove symbol behavior

Fix remove symbol behavior

--- assets/javascripts/application.coffee
+++ assets/javascripts/application.coffee
@@ -54,7 +54,7 @@

   $(document).on 'click', '.remove', (event) ->
     event.preventDefault()
-    stock = $(e.currentTarget).attr('name')
+    stock = $(event.currentTarget).attr('name')
     window.stocks = _.reject(window.stocks, (symbol) -> symbol is stock)
     localStorage.setItem('stocks', window.stocks)
     $('li[name=' + stock + ']').remove()
@@ -78,6 +78,7 @@

       $.ajax({
         contentType: 'application/json',
+        type: 'GET',
         url: '/stocks',
         data: { symbol: symbol },
         dataType: 'json',


:construction: Even more logs

:construction: Even more logs

--- lib/tasks/load-paths-handler.coffee
+++ lib/tasks/load-paths-handler.coffee
@@ -30,14 +30,18 @@
       return true if sourceName.match(relativePath)

   isIgnored: (loadedPath, stats) ->
+    console.log "in isIgnored: #{loadPaths}, #{stats}"
     relativePath = path.relative(@rootPath, loadedPath)
     if @repo?.isPathIgnored(relativePath)
+      console.log "is git ignored"
       true
     else
       for ignoredName in @ignoredNames
+        console.log "is ignored using patterns"
         return true if ignoredName.match(relativePath)

       if stats and @knownPaths? and @timestamp? and loadedPath in @knownPaths
+        console.log "is ignored using timestamp: #{stats.ctime <= @timestamp}"
         stats.ctime <= @timestamp
       else
         false


Basic classifiers working.

Basic classifiers working.

--- picoCTF-web/web/coffee/management-problems.coffee
+++ picoCTF-web/web/coffee/management-problems.coffee
@@ -7,6 +7,8 @@
 Label = ReactBootstrap.Label
 PanelGroup = ReactBootstrap.PanelGroup
 Row = ReactBootstrap.Row
+ListGroup = ReactBootstrap.ListGroup
+ListGroupItem = ReactBootstrap.ListGroupItem

 update = React.addons.update

@@ -64,10 +66,52 @@
       </Col>
     </Panel>

+ProblemClassifierList = React.createClass
+  render: ->
+    fakeAll = [
+      {
+        name: "Show all problems",
+        size: 20,
+        classifier: (problem) -> true
+      },
+      {
+        name: "Show none of the problems",
+        size: 0,
+        classifier: (problem) -> false
+      }
+    ]
+
+    <PanelGroup className="problem-classifier" collapsible>
+      <ProblemClassifier name="All" data={fakeAll} {...@props}/>
+      <ProblemClassifier name="Categories" data={[]} {...@props}/>
+      <ProblemClassifier name="Organizations" data={[]} {...@props}/>
+    </PanelGroup>
+
+ClassifierItem = React.createClass
+  getInitialState: ->
+    active: false
+
+  handleClick: (e) ->
+    activeState = !@state.active
+    @setState {active: activeState}
+    console.log @props
+    @props.setClassifier activeState, @props.classifier
+
+  render: ->
+    <ListGroupItem
+      onClick={@handleClick}
+      active={if @state.active then "active" else ""}>
+        {@props.name}
<Badge>{@props.size}</Badge>

+    </ListGroupItem>
+
 ProblemClassifier = React.createClass
   render: ->
-    <Panel header="Category" defaultExpanded collapsible>
-      test
+    <Panel header={@props.name} defaultExpanded collapsible>
+      <ListGroup fill>
+        {@props.data.map ((data, i) ->
+          <ClassifierItem key={i} setClassifier={@props.setClassifier} {...data}/>
+        ).bind this}
+      </ListGroup>
     </Panel>

 Problem = React.createClass
@@ -150,6 +194,10 @@
     @setState update @state,
       activeSort: $set: {name: name, ascending: ascending}

+  setClassifier: (state, classifier) ->
+    @setState update @state,
+      problemClassifier: $set: classifier
+
   filterProblems: (problems) ->
     visibleProblems = _.filter problems, ((problem) ->
       (@state.filterRegex.exec problem.name) != null and @state.problemClassifier problem
@@ -168,7 +216,7 @@
           <ProblemFilter onSortChange={@onSortChange} filter="" onFilterChange={@onFilterChange}/>
         </Row>
         <Row>
-          <ProblemClassifier/>
+          <ProblemClassifierList setClassifier={@setClassifier}/>
         </Row>
       </Col>
       <Col xs={9} md={9}>


tweak search form style

tweak search form style

--- lib/carte/client/views/header.cjsx
+++ lib/carte/client/views/header.cjsx
@@ -72,7 +72,7 @@

           <form className="navbar-form navbar-left" role="search">

-              <input type="text" className="form-control" value={@state.searchText} onChange={@onChangeSearchText} onKeyPress={@onKeyPressSearchText} placeholder='Search ...' style={width:"344px"} />
+              <input type="text" className="form-control" value={@state.searchText} onChange={@onChangeSearchText} onKeyPress={@onKeyPressSearchText} placeholder='Search ...' />

           </form>


optimize set selection, fix for firefox

optimize set selection, fix for firefox

--- src/selection.coffee
+++ src/selection.coffee
@@ -9,6 +9,7 @@
     this.initListeners()
     @editor.renderer.runWhenLoaded( =>
       @nativeSelection = @editor.contentWindow.getSelection()
+      this.setRange(new Scribe.Range(@editor, 0, 0))    # Range gets set to end of doc in Firefox by default
     )

   initListeners: ->
@@ -47,15 +48,19 @@
     else
       return new Scribe.Range(@editor, end, start)

-  setRange: (@range, silent = false) ->
+  setRange: (range, silent = false) ->
     return unless @nativeSelection?
+    this.update(true)
+    return if range == @range or @range?.equals(range)
+    @range = range
     @nativeSelection.removeAllRanges()
     if @range?
       nativeRange = @editor.root.ownerDocument.createRange()
-      [startNode, startOffset] = Scribe.DOM.findDeepestNode(@range.start.leafNode, @range.start.offset)
-      nativeRange.setStart(startNode, startOffset)
-      [endNode, endOffset] = Scribe.DOM.findDeepestNode(@range.end.leafNode, @range.end.offset)
-      nativeRange.setEnd(endNode, endOffset)
+      _.each([@range.start, @range.end], (pos, i) ->
+        [node, offset] = Scribe.DOM.findDeepestNode(pos.leafNode, pos.offset)
+        fn = if i == 0 then 'setStart' else 'setEnd'
+        nativeRange[fn].call(nativeRange, node, offset)
+      )
       @nativeSelection.addRange(nativeRange)
     @editor.emit(Scribe.Editor.events.SELECTION_CHANGE, @range) unless silent


Trim some comments.

Trim some comments.

--- lib/Watchers/Asset/mirror.coffee
+++ lib/Watchers/Asset/mirror.coffee
@@ -7,7 +7,6 @@
 # Watch = require('gaze').Gaze
 Watch = require('sane')
 minimatch = require 'minimatch'
-

 eventMap =
     add: 'added'
@@ -30,11 +29,6 @@
         unless @pattern? and @pattern instanceof Array
             throw new Error "No sane pattern; have #{@pattern}, need Array."

-        # if noGlob then extensions = @pattern
-
-        # @pond = GetPool root, extensions, noGlob
-        # @gazeAt gaze for gaze in @pond
-        # @gazeAt @gaze = new Gaze @pattern
         @gazeAt @pool = (GetPool(root) for root in @root)

     gazeAt: (pool)->
@@ -77,16 +71,6 @@

     watched: (cb)->
         cb = nb cb
-        # @gaze.watched (err, matched = {})=>
-        #     return cb err if err
-        #     filelist = {}
-        #     for _, files of matched
-        #         files
-        #         .filter(@toWatch())
-        #         .forEach (filepath)=>
-        #             filelist[filepath] = yes
-        #     cb null, filelist
-
         filelist = {}
         @pool.forEach (pond)=>
             files = Object.keys(pond.dirRegistery).forEach (dirname)=>


auto browser tests in good shape

auto browser tests in good shape

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -104,7 +104,7 @@
         cb()
   null

-gulp.task 'travis', [ 'test', 'coveralls', 'browsers' ], ->
+gulp.task 'travis', [ 'test', 'coveralls' ], ->

 gulp.task 'testwatch', ['test' ], ->
   gulp.watch ['./src/*', './test/*'], ['test', 'default']


only define window.Page if undefined

only define window.Page if undefined

--- lib/assets/javascripts/page.coffee
+++ lib/assets/javascripts/page.coffee
@@ -1,4 +1,4 @@
-window.Page = {}
+window.Page = {} if !window.Page

 Page.visit = (url, opts={}) ->
   if opts.reload


fix(lib/apply-configuration.coffee): Fix default option values

Added default values for several missing options. Removed `reporters` which is handled by the CLI.

--- lib/apply-configuration.coffee
+++ lib/apply-configuration.coffee
@@ -31,7 +31,9 @@
     options:
       server: null
       schemas: null
-      reporters: false
+      'generate-hooks': false
+      template: null
+      timeout: 2000
       reporter: null
       header: null
       names: false


Don't rely on 'webkitMatchesSelector', find out prefix on load instead

--- src/space-pen.coffee
+++ src/space-pen.coffee
@@ -127,7 +127,7 @@
           element = $(element)
           element.on eventName, (event) -> view[methodName](event, element)

-      if view[0].webkitMatchesSelector(selector)
+      if view.matching(selector)
         methodName = view[0].getAttribute(eventName)
         do (methodName) ->
           view.on eventName, (event) -> view[methodName](event, view)
@@ -239,6 +239,9 @@

 jQuery.fn.view = -> @data('view')
 jQuery.fn.views = -> @toArray().map (elt) -> $(elt).view()
+docEl = document.documentElement
+matches = docEl.matchesSelector || docEl.mozMatchesSelector || docEl.webkitMatchesSelector || docEl.oMatchesSelector || docEl.msMatchesSelector
+jQuery.fn.matching = if matches then ((selector) -> matches.call(@[0], selector)) else jQuery.fn.is

 # Trigger attach event when views are added to the DOM
 callAttachHook = (element) ->


re-adding accidental removal - these are used for hiding/showing entire panel

--- lib/features/typechecking.coffee
+++ lib/features/typechecking.coffee
@@ -33,6 +33,11 @@
       if(not file.includes('dep-src')) # TODO: put under flag
         addNoteToMessageView note for note in notes

+  hide: ->
+    @messages.hide()
+
+  show: ->
+    @messages.show()

   clearScalaNotes: ->
     @messages.clear()


Fix: handlers re-registered on package reinstall

Fixes #711

--- lib/package-detail-view.coffee
+++ lib/package-detail-view.coffee
@@ -58,6 +58,7 @@
     super
     @disposables = new CompositeDisposable()
     @loadPackage()
+    @handleButtonEvents()

   completeInitialzation: ->
     unless @packageCard # Had to load this from the network
@@ -69,7 +70,6 @@
     @buttons.removeClass('hidden')
     @activateConfig()
     @populate()
-    @handleButtonEvents()
     @updateFileButtons()
     @subscribeToPackageManager()
     @renderReadme()


have default opts in inc

have default opts in inc

--- libraries/metrics/metrics.coffee
+++ libraries/metrics/metrics.coffee
@@ -52,7 +52,7 @@

 	set : (key, value, sampleRate = 1)->

-	inc : (key, sampleRate = 1, opts)->
+	inc : (key, sampleRate = 1, opts = {})->
 		key = Metrics.buildPromKey(key)
 		if !promMetrics[key]?
 			promMetrics[key] = new prom.Counter({


add up a bit of doc

add up a bit of doc

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -53,6 +53,8 @@
 # unique ability of half duplex communications between the external
 # code that is executed on the call site via `Screenplay` facility
 # and an instance of the service that resides on the server site.
+# The component itself is built heavily on top of a code emission
+# and delivery platform, as implemented by `Screenplay` service.
 module.exports.Duplex = class Duplex extends Preflight

     # This is a marker that indicates to some internal subsystems


fix the docs

fix the docs

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -296,6 +296,7 @@
     # is a good idea for this HTTP method to be idempotent. As the
     # rule, this method does not have to alter any contents or data
     # of the resource. Use for unobtrusive retrieval of resources.
+    # The method is an HTTP verb, coherent with the REST interface.
     GET: (request, response, resource, domain, session) ->
         assert symbol = "$root".toString().toLowerCase()
         assert args = [symbol, request, yes, undefined]


Added: FreshCut color for Atom terminal

Added: FreshCut color for Atom terminal

--- dotfiles/atom/config.cson
+++ dotfiles/atom/config.cson
@@ -39,7 +39,24 @@
   "merge-conflicts": {}
   "terminal-panel": {}
   term2:
-    colors: {}
+    colors:
+      normalBlack: "#222"
+      normalRed: "#c75646"
+      normalGreen: "#8fbe00"
+      normalYellow: "#e9ee00"
+      normalBlue: "#00a8c6"
+      normalPurple: "#b656ff"
+      normalCyan: "#4ecdc4"
+      normalWhite: "#f8f8f2"
+      brightBlack: "#556270"
+      brightRed: "#d3796d"
+      brightGreen: "#c0e763"
+      brightYellow: "#fafd24"
+      brightBlue: "#00d4ff"
+      brightPurple: "#cc89ff"
+      brightCyan: "#74d6ce"
+      brightWhite: "#fdfdfb"
+      background: "#222"
     openPanesInSameSplit: true
   react: {}
   "isotope-ui":


allow reminders for others [fixes [fixed]

--- commands/timer.coffee
+++ commands/timer.coffee
@@ -76,7 +76,7 @@

 brain.addPattern 'reminder',
     match: [/remind\s(.+)\s(to|of|about)\s(.+)\sin\s(.+)/i]
-    fn: (user, m, cb) -> setReminder m[4], m[3], if m[2] == 'me' then user else m[2], cb
+    fn: (user, m, cb) -> setReminder m[4], m[3], (if m[1] == 'me' then user else m[1]), cb

 brain.addPattern 'sayTimer',
     match: [/say\s(.+)\sin\s(.+)/i]


parallax: allowing layer to be positioned on subpixels with 2 decimals rounding

--- app/_src/components/parallax-scroll.coffee
+++ app/_src/components/parallax-scroll.coffee
@@ -85,15 +85,18 @@
     @init()

+  round: (number) =>
+    Math.round(number*100) / 100;
+
   updatePosition: =>
     if @direction is DIRECTION_VERTICAL
       prop =
-        y: Math.round(-1 * (@contentHeight - @contextHeight) * @percentageScrolled)
+        y: @round(-1 * (@contentHeight - @contextHeight) * @percentageScrolled)
         force3D: true

     else if @direction is DIRECTION_HORIZONTAL
       prop =
-        x: Math.round(-1 * (@contentWidth - @contextWidth) * @percentageScrolled)
+        x: @round(-1 * (@contentWidth - @contextWidth) * @percentageScrolled)
         force3D: true

     TweenLite.set(@$content[0], prop);


Account: fixed password modal's buttons

Account: fixed password modal's buttons

--- client/Main/CommonViews/VerifyPasswordModal.coffee
+++ client/Main/CommonViews/VerifyPasswordModal.coffee
@@ -21,9 +21,10 @@
             buttons               :
               Submit              :
                 title             : buttonTitle
-                cssClass          : "solid green medium"
+                style             : "solid green medium"
                 type              : "submit"
               Forgot              :
+                style             : "solid light-gray medium"
                 title             : "Forgot Password?"
                 callback          : =>
                   {entryPoint} = KD.config


vms: create new vm modal WIP

vms: create new vm modal WIP

--- client/app/MainApp/VirtualizationController.coffee
+++ client/app/MainApp/VirtualizationController.coffee
@@ -98,8 +98,10 @@

   createNewVM:->
     return  if @dialogIsOpen
-
-    vmCreateCallback = (err, vm)->
+    canCreateSharedVM   = "owner" in KD.config.roles or "admin" in KD.config.roles
+    canCreatePersonalVM = "member" in KD.config.roles
+
+    vmCreateCallback  = (err, vm)->
       if err
         warn err
         return new KDNotificationView
@@ -111,39 +113,97 @@

     group = KD.singletons.groupsController.getGroupSlug()

-    buttons =
-      'Create a Personal VM' :
-        style    : "modal-clean-gray"
-        callback : =>
-          KD.singletons.vmController.createGroupVM 'personal', vmCreateCallback
-
-    if "owner" in KD.config.roles or "admin" in KD.config.roles
+    if canCreateSharedVM
       content = """You can create a Personal or Shared VM for
                    #{group}. If you prefer to create a shared VM, all
                    members in #{group} will be able to use that VM.
                 """
-      buttons['Create a Shared VM'] =
-        style      : "modal-clean-gray"
-        callback   : =>
-          KD.singletons.vmController.createGroupVM 'shared', vmCreateCallback
-
-    else if "member" in KD.config.roles
+    else if canCreatePersonalVM
       content = """You can create a Personal VM in #{group}."""
-
     else
       return new KDNotificationView
         title : "You are not authorized to create VMs in #{group} group"

-    buttons.Cancel =
-      style      : "modal-cancel"
-      callback   : -> modal.destroy()
-
-    modal     = new KDModalView
-      title   : "Create a new VM"
-      content : "

#{content}

"
-      height  : "auto"
-      overlay : yes
-      buttons : buttons
+    modal = new KDModalViewWithForms
+      title                       : "Create a new VM"
+      height                      : "auto"
+      width                       : 500
+      overlay                     : yes
+      tabs                        :
+        navigable                 : no
+        forms                     :
+          "Create VM"             :
+            callback              : (formData)=>
+              form                = modal.modalTabs.forms["Create VM"]
+              {personal, shared}   = form.buttons
+              log formData
+            buttons               :
+              personal            :
+                title             : "Create a personal VM"
+                style             : "modal-clean-gray"
+                type              : "submit"
+                loader            :
+                  color           : "#ffffff"
+                  diameter        : 12
+                callback          : ->
+                  KD.singletons.vmController.createGroupVM 'personal', vmCreateCallback
+              shared              :
+                title             : "Create a shared VM"
+                style             : "modal-clean-gray hidden"
+                type              : "submit"
+                loader            :
+                  color           : "#ffffff"
+                  diameter        : 12
+                callback          : ->
+                  KD.singletons.vmController.createGroupVM 'shared', vmCreateCallback
+              cancel              :
+                style             : "modal-cancel"
+                callback          : -> modal.destroy()
+            fields                :
+              "intro"             :
+                itemClass         : KDCustomHTMLView
+                partial           : "
#{content}
"
+              "VM Type"           :
+                label             : "VM Host"
+                itemClass         : KDSelectBox
+                type              : "select"
+                name              : "vm-host"
+                defaultValue      : "1"
+                selectOptions     : [
+                  { title : "2GHz, 4GB RAM, 20GB Disk",      value : "1" }
+                  { title : "4GHz, 4GB RAM, 40GB Disk",      value : "2" }
+                  { title : "8GHz, 8GB RAM, 60GB Disk",      value : "3" }
+                  { title : "8GHz, 16GB RAM, 60GB Disk",     value : "4" }
+                  { title : "16GHz, 32GB RAM, 100GB Disk",   value : "5" }
+                ]
+              "Usage Policy"      :
+                label             : "Usage Policy"
+                itemClass         : KDSelectBox
+                type              : "select"
+                name              : "vm-policy"
+                defaultValue      : "single"
+                selectOptions     : [
+                  { title : "All users share same VM host.",                  value : "single" }
+                  { title : "Limit users per VM, create new when necessary.", value : "multiple" }
+                ]
+              "Users per VM"      :
+                label             : "Users per VM"
+                itemClass         : KDSelectBox
+                type              : "select"
+                name              : "vm-users"
+                defaultValue      : "25"
+                selectOptions     : [
+                  { title : "5",   value : "5" }
+                  { title : "10",  value : "10" }
+                  { title : "25",  value : "25" }
+                  { title : "50",  value : "50" }
+                  { title : "100", value : "100" }
+                ]
+
+
+
+    if canCreateSharedVM
+      modal.modalTabs.forms["Create VM"].buttons.shared.show()

     @dialogIsOpen = yes
     modal.once 'KDModalViewDestroyed', => @dialogIsOpen = no


Update atom package sync

Update atom package sync

--- atom/packages.cson
+++ atom/packages.cson
@@ -1,35 +1,52 @@
 packages: [
-  "atom-ember-author-kit"
-  "atom-emblem"
-  "atom-handlebars"
-  "atom-sort-sass"
-  "cobalt2-syntax"
-  "colorful-json"
-  "count-word"
-  "dracula-ui"
-  "dracula-theme"
-  "electron-light-syntax"
-  "electron-syntax"
-  "ember-snippets"
+  "an-old-hope-light-syntax"
+  "apex-syntax"
+  "apex-ui"
+  "ariake-dark-syntax"
+  "atom-beautify"
+  "atom-focus-mode"
+  "atom-ide-ui"
+  "atom-material-syntax"
+  "atom-material-syntax-dark"
+  "atom-material-syntax-light"
+  "atom-material-ui"
+  "atom-typescript"
+  "autoclose-html"
+  "busy-signal"
+  "chester-atom-syntax"
+  "dash-syntax"
+  "dash-ui"
+  "dracula-syntax"
+  "editorconfig"
   "emmet"
-  "file-icons"
-  "highlight-selected"
+  "gloom"
+  "hey-pane"
+  "htmlbars-beautify"
+  "ide-typescript"
+  "intentions"
+  "language-docker"
   "language-ember-htmlbars"
-  "language-nginx"
-  "language-slim"
+  "language-javascript-jsx"
   "linter"
+  "linter-docker"
   "linter-eslint"
-  "linter-ruby"
+  "linter-rubocop"
   "linter-sass-lint"
-  "linter-slim"
-  "maximize-active-pane"
-  "move-panes"
-  "native-ui"
+  "linter-scss-lint"
+  "linter-stylelint"
+  "linter-ui-default"
+  "loved-syntax"
+  "northem-light-atom-syntax"
+  "northem-light-atom-ui"
   "package-sync"
+  "php-twig"
   "pigments"
-  "pretty-json"
+  "prettier-atom"
+  "pristine-ui"
   "project-manager"
+  "pure-syntax"
+  "rails-transporter"
+  "ramda-syntax"
   "sort-lines"
-  "svg-preview"
-  "travis-ci-status"
+  "textmate-funky_dashboard-syntax"
 ]


remove delay complete

remove delay complete

--- app/assets/javascripts/client/index.coffee
+++ app/assets/javascripts/client/index.coffee
@@ -82,7 +82,6 @@
     this

   start: (name) =>
-    # Choose a random variant
     if previous = @getVariantCookie()
       # Use the same variant as before, don't record anything
       variant = (v for v in @variants when v.name is previous)[0]
@@ -120,18 +119,14 @@
     else
       @reset()

-    # Request complete now, or on the next request
-    if @options.delay
-      @setDelayCompleteCookie(name)
-    else
-      @recordComplete(name)
-
+    @recordComplete(name)
     this

   reset: =>
     @removeVariantCookie()
     @removePersistCompleteCookie()
     @result = null
+    this

   # Private

@@ -164,9 +159,6 @@

   # Complete Cookie

-  setDelayCompleteCookie: (value) =>
-    setCookie('abbaDelayComplete', "#{@name}##{value}")
-
   setPersistCompleteCookie: =>
     setCookie("abbaPersistComplete_#{@name}", '1', expires: 600)

@@ -176,16 +168,8 @@
   removePersistCompleteCookie: =>
     setCookie("abbaPersistComplete_#{@name}", '', expires: true)

-  @delayComplete: =>
-    if result = getCookie('abbaDelayComplete')
-      [name, value] = result.split('#')
-      new this(name).requestComplete(value)
-      setCookie('abbaDelayComplete', '', expires: true)
-
 do ->
   # Find Abba's endpoint from the script tag
   scripts = document.getElementsByTagName('script')
   scripts = (script.src for script in scripts when /\/abba\.js$/.test(script.src))
-  Abba.endpoint = "//#{host(scripts[0])}" if scripts[0]
-
-bind(window, 'load', Abba.delayComplete)+  Abba.endpoint = "//#{host(scripts[0])}" if scripts[0]


Re order views.

Re order views.

--- js/views/views.coffee
+++ js/views/views.coffee
@@ -1,24 +1,5 @@

-class Panel_2 extends Backbone.View
-
-  el: $('#panel-2')
-
-  initialize: (options) ->
-
-  render: (id) ->
-    compiledTemplate = _.template( $('#panel-2-template').html(), {header:"panel " + id}  )
-    @$el.html( compiledTemplate )
-
-    flight.goToPage({targetPanel:'#panel-' + id})
-
-    this
-
-
-@app = window.app ? {}
-@app.Views.Panel_2 = Panel_2
-
-
-
+# First panel to be displayed
 class Home extends Backbone.View

   el: $('#panel-1')
@@ -30,7 +11,26 @@
     @$el.html( compiledTemplate )

     this
-

 @app = window.app ? {}
 @app.Views.Home = Home
+
+# second panel
+class Panel_2 extends Backbone.View
+
+  el: $('#panel-2')
+
+  initialize: (options) ->
+
+  render: (id) ->
+    compiledTemplate = _.template( $('#panel-2-template').html(), {header:"panel " + id}  )
+    @$el.html( compiledTemplate )
+
+    # use flight for page load.
+    flight.goToPage({targetPanel:'#panel-' + id})
+
+    this
+
+@app = window.app ? {}
+@app.Views.Panel_2 = Panel_2
+


no longer rely on consolidateSelections to remove cursors :tulip:

--- spec/bracket-matcher-spec.coffee
+++ spec/bracket-matcher-spec.coffee
@@ -105,12 +105,12 @@
           expectHighlights([8, 42], [8, 54])

     describe "when the cursor is destroyed", ->
-      it "updates the highlights to use the editor's last cursor", ->
+      it "updates the highlights to use the editor's first cursor", ->
         editor.setCursorBufferPosition([0, 29])
         editor.addCursorAtBufferPosition([9, 0])
         expectHighlights([0, 28], [12, 0])

-        editor.consolidateSelections()
+        editor.getCursors()[0].destroy()
         expectNoHighlights()

         editor.setCursorBufferPosition([0, 29])


Fixed subject ids array when uploading media

--- lib/panoptes-subject-uploader.iced
+++ lib/panoptes-subject-uploader.iced
@@ -193,6 +193,7 @@
         log "Saved subject #{subject.id}"

         # Locations array has been transformed into [{"mime type": "URL to upload"}]
+        successCount = 0
         for location, ii in subject.locations
           for type, url of location
             headers = {'Content-Type': mime.lookup imageFileNames[ii]}
@@ -203,7 +204,8 @@
             if response?
               if 200 <= response.statusCode < 400
                 log "Uploaded image #{imageFileNames[ii]}"
-                newSubjectIDs.push subject.id
+                # Deal with multi-image subjects. Track if image upload is successful
+                successCount++
               else
                 error = response.body

@@ -213,6 +215,9 @@
               await subject.delete().then(defer _).catch(console.error.bind console)
               break

+          # Deal with multi-image subject. Only add new subject id once and if all images put successfully.
+          newSubjectIDs.push subject.id if successCount is subject.locations.length
+
 if newSubjectIDs.length is 0
   log 'No subjects to link'
 else


Remove unnecessary code

Remove unnecessary code

--- scripts/views/workspace/content/edit-roles.coffee
+++ scripts/views/workspace/content/edit-roles.coffee
@@ -45,7 +45,7 @@
           update: -> $el.select2 'onSortEnd'

     setupSelect2: ($el, attr) ->
-      $el.val(@model.get(attr).join('|') or '')
+      $el.val(@model.get(attr).join('|'))

       $el.select2
         tags: @model.get(attr) or []
@@ -64,14 +64,7 @@
       @setupSelect2(@$el.find('[name=editors]'), 'editors')
       @setupSelect2(@$el.find('[name=translators]'), 'translators')

-      # Populate the multiselect widgets with data from the backbone model
-      @_updateAuthors()
-      @_updateCopyrightHolders()
-
       @delegateEvents()
-
-    _updateAuthors: -> @$el.find('*[name=authors]').select2 'val', (@model.get('authors') or [])
-    _updateCopyrightHolders: -> @$el.find('*[name=copyrightHolders]').select2 'val', (@model.get('copyrightHolders') or [])

     attrsToSave: () ->
       # Grab the authors from the multiselect input element.


fixed bug with notifications.

fixed bug with notifications.

--- client/coffeescript/templates/notifications.coffee
+++ client/coffeescript/templates/notifications.coffee
@@ -1,6 +1,6 @@
 _.extend( Template.notifications,
   count: ->
-    Notifications.find( timestamp: { '$gt': Meteor.user().notify } ).count()
+    Notifications.find( timestamp: { '$gt': Meteor.user()?.notify } ).count()
   notifications: ->
     groups = {}
     notifs = Notifications.find({},{sort:{timestamp:-1}}).fetch()


fixed error handling in Request

fixed error handling in Request

--- app/assets/javascripts/libs/request.coffee
+++ app/assets/javascripts/libs/request.coffee
@@ -130,7 +130,7 @@
     if error instanceof Response
       error.json().then(
         (json) ->
-          Toast.message(json)
+          Toast.message(json.messages)
           Promise.reject(json)
         (error) ->
           Toast.error(error)


Define new method for check of this rule "can user start video collaboration".

--- client/ide/lib/videocollaborationcontroller.coffee
+++ client/ide/lib/videocollaborationcontroller.coffee
@@ -3,6 +3,8 @@
 socialHelpers                 = require './collaboration/helpers/social'
 isVideoFeatureEnabled         = require 'app/util/isVideoFeatureEnabled'
 showError                     = require 'app/util/showError'
+LimitedVideoCollaborationFree = require './views/collaboration/limitedvideocollaborationfree'
+

 generatePayloadFromModel = (model) ->
   return {
@@ -172,3 +174,18 @@
     @chat?.emit args...

+  canFreeUserStartVideo: (callback = kd.noop, isVideoActive = no) ->
+
+    { paymentController } = kd.singletons
+
+    paymentController.subscriptions (err, plan) =>
+
+      return showError err if err?
+
+      pLength = @participants.asArray().length
+
+      if plan.planTitle is 'free' and (pLength > 2 or (pLength is 2 and isVideoActive))
+        return new LimitedVideoCollaborationFree
+
+      callback()
+