:art:

:art:

--- spec/markdown-preview-spec.coffee
+++ spec/markdown-preview-spec.coffee
@@ -4,9 +4,6 @@
 wrench = require 'wrench'
 MarkdownPreviewView = require '../lib/markdown-preview-view'

-# TODO: Remove this after atom/atom#13977 lands in favor of unguarded `getCenter()` calls
-getCenter = -> atom.workspace.getCenter?() ? atom.workspace
-
 describe "Markdown preview package", ->
   [workspaceElement, preview] = []

@@ -28,10 +25,10 @@
       atom.packages.activatePackage('language-gfm')

   expectPreviewInSplitPane = ->
-    waitsFor -> getCenter().getPanes().length is 2
+    waitsFor -> atom.workspace.getCenter().getPanes().length is 2

     waitsFor "markdown preview to be created", ->
-      preview = getCenter().getPanes()[1].getActiveItem()
+      preview = atom.workspace.getCenter().getPanes()[1].getActiveItem()

     runs ->
       expect(preview).toBeInstanceOf(MarkdownPreviewView)
@@ -44,7 +41,7 @@
       expectPreviewInSplitPane()

       runs ->
-        [editorPane] = getCenter().getPanes()
+        [editorPane] = atom.workspace.getCenter().getPanes()
         expect(editorPane.getItems()).toHaveLength 1
         expect(editorPane.isActive()).toBe true

@@ -81,12 +78,12 @@
     it "closes the existing preview when toggle is triggered a second time on the editor", ->
       atom.commands.dispatch workspaceElement, 'markdown-preview:toggle'

-      [editorPane, previewPane] = getCenter().getPanes()
+      [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
       expect(editorPane.isActive()).toBe true
       expect(previewPane.getActiveItem()).toBeUndefined()

     it "closes the existing preview when toggle is triggered on it and it has focus", ->
-      [editorPane, previewPane] = getCenter().getPanes()
+      [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
       previewPane.activate()

       atom.commands.dispatch workspaceElement, 'markdown-preview:toggle'
@@ -118,7 +115,7 @@
       describe "when the preview is in the active pane but is not the active item", ->
         it "re-renders the preview but does not make it active", ->
           markdownEditor = atom.workspace.getActiveTextEditor()
-          previewPane = getCenter().getPanes()[1]
+          previewPane = atom.workspace.getCenter().getPanes()[1]
           previewPane.activate()

           waitsForPromise ->
@@ -137,7 +134,7 @@
       describe "when the preview is not the active item and not in the active pane", ->
         it "re-renders the preview and makes it active", ->
           markdownEditor = atom.workspace.getActiveTextEditor()
-          [editorPane, previewPane] = getCenter().getPanes()
+          [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
           previewPane.splitRight(copyActiveItem: true)
           previewPane.activate()

@@ -175,12 +172,12 @@

     describe "when the original preview is split", ->
       it "renders another preview in the new split pane", ->
-        getCenter().getPanes()[1].splitRight({copyActiveItem: true})
-
-        expect(getCenter().getPanes()).toHaveLength 3
+        atom.workspace.getCenter().getPanes()[1].splitRight({copyActiveItem: true})
+
+        expect(atom.workspace.getCenter().getPanes()).toHaveLength 3

         waitsFor "split markdown preview to be created", ->
-          preview = getCenter().getPanes()[2].getActiveItem()
+          preview = atom.workspace.getCenter().getPanes()[2].getActiveItem()

         runs ->
           expect(preview).toBeInstanceOf(MarkdownPreviewView)
@@ -188,10 +185,10 @@

     describe "when the editor is destroyed", ->
       beforeEach ->
-        getCenter().getPanes()[0].destroyActiveItem()
+        atom.workspace.getCenter().getPanes()[0].destroyActiveItem()

       it "falls back to using the file path", ->
-        getCenter().getPanes()[1].activate()
+        atom.workspace.getCenter().getPanes()[1].activate()
         expect(preview.file.getPath()).toBe atom.workspace.getActivePaneItem().getPath()

       it "continues to update the preview if the file is changed on #win32 and #darwin", ->
@@ -228,12 +225,12 @@
           listener.callCount > 0

       it "allows a new split pane of the preview to be created", ->
-        getCenter().getPanes()[1].splitRight({copyActiveItem: true})
-
-        expect(getCenter().getPanes()).toHaveLength 3
+        atom.workspace.getCenter().getPanes()[1].splitRight({copyActiveItem: true})
+
+        expect(atom.workspace.getCenter().getPanes()).toHaveLength 3

         waitsFor "split markdown preview to be created", ->
-          preview = getCenter().getPanes()[2].getActiveItem()
+          preview = atom.workspace.getCenter().getPanes()[2].getActiveItem()

         runs ->
           expect(preview).toBeInstanceOf(MarkdownPreviewView)
@@ -476,7 +473,7 @@
       expectPreviewInSplitPane()

     it "saves the HTML when it is triggered and the editor has focus", ->
-      [editorPane, previewPane] = getCenter().getPanes()
+      [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
       editorPane.activate()

       outputPath = temp.path(suffix: '.html')
@@ -493,7 +490,7 @@
         expect(fs.isFileSync(outputPath)).toBe true

     it "saves the HTML when it is triggered and the preview pane has focus", ->
-      [editorPane, previewPane] = getCenter().getPanes()
+      [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
       previewPane.activate()

       outputPath = temp.path(suffix: '.html')


fix unlikely issue involving local storage

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -758,13 +758,12 @@
             @_chat_is_hidden = true

         @_layout = @local_storage("layout")
-        if not @_layout?
+        if @_layout not in [1,2]
+            # The *ONLY* allowed values for the layout are 1 and 2 here (update when this changes!)
+            # IMPORTANT: If this were anything other than 1, the user would never be able to open
+            # tex files. So it's important that this be valid.
             @_layout = 1
         @_last_layout = @_layout
-
-        layout_elt = @element.find(".salvus-editor-codemirror-input-container-layout-#{@_layout}").show()
-        elt = layout_elt.find(".salvus-editor-codemirror-input-box").find("textarea")
-        elt.text(content)

         extraKeys =
             "Alt-Enter"    : (editor)   => @action_key(execute: true, advance:false, split:false)
@@ -871,6 +870,9 @@

             return cm

+        layout_elt = @element.find(".salvus-editor-codemirror-input-container-layout-#{@_layout}").show()
+        elt = layout_elt.find(".salvus-editor-codemirror-input-box").find("textarea")
+        elt.text(content)

         @codemirror = make_editor(elt[0])
         @codemirror.name = '0'


More specs in tokenized buffer testing empty line retokenizing

--- spec/tokenized-buffer-spec.coffee
+++ spec/tokenized-buffer-spec.coffee
@@ -657,3 +657,57 @@

         buffer.setText('\n\n\n')
         expect(tokenizedBuffer.lineForScreenRow(1).indentLevel).toBe 0
+
+    describe "when the line changed is surrounded by whitespace lines", ->
+      it "updates empty line indent guides above a line that is indented", ->
+        expect(tokenizedBuffer.lineForScreenRow(12).indentLevel).toBe 0
+
+        buffer.insert([12, 0], '\n')
+
+        # The newline and he tab need to be in two different operations to surface the bug
+        buffer.insert([13, 0], '  ')
+        expect(tokenizedBuffer.lineForScreenRow(12).indentLevel).toBe 1
+
+      it "updates empty line indent guides when the empty line is the last line", ->
+        buffer.insert([12, 2], '\n')
+
+        # The newline and he tab need to be in two different operations to surface the bug
+        buffer.insert([12, 0], '  ')
+        expect(tokenizedBuffer.lineForScreenRow(13).indentLevel).toBe 1
+
+        buffer.insert([12, 0], '  ')
+        expect(tokenizedBuffer.lineForScreenRow(13).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(14)).not.toBeDefined()
+
+      it "updates empty lines surrounding a change encompasing more lines than the old text", ->
+        # create some new lines
+        buffer.insert([7, 0], '\n\n')
+        buffer.insert([5, 0], '\n\n')
+
+        expect(tokenizedBuffer.lineForScreenRow(5).indentLevel).toBe 3
+        expect(tokenizedBuffer.lineForScreenRow(6).indentLevel).toBe 3
+        expect(tokenizedBuffer.lineForScreenRow(9).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(10).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(11).indentLevel).toBe 2
+
+        buffer.setTextInRange([[7, 0], [8, 65]], '        one\n        two\n        three\n        four')
+
+        expect(tokenizedBuffer.lineForScreenRow(5).indentLevel).toBe 4
+        expect(tokenizedBuffer.lineForScreenRow(6).indentLevel).toBe 4
+        expect(tokenizedBuffer.lineForScreenRow(11).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(12).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(13).indentLevel).toBe 2
+
+      it "updates empty lines surrounding a change encompasing less lines than the old text", ->
+        # create some new lines
+        buffer.insert([7, 0], '\n\n')
+        buffer.insert([5, 0], '\n\n')
+
+        buffer.setTextInRange([[7, 0], [8, 65]], '    ok')
+
+        expect(tokenizedBuffer.lineForScreenRow(5).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(6).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(7).indentLevel).toBe 2 # new text
+        expect(tokenizedBuffer.lineForScreenRow(8).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(9).indentLevel).toBe 2
+        expect(tokenizedBuffer.lineForScreenRow(10).indentLevel).toBe 2 # }


Add initial spec of hard tabs aligning

Add initial spec of hard tabs aligning

--- spec/tokenized-buffer-spec.coffee
+++ spec/tokenized-buffer-spec.coffee
@@ -344,6 +344,20 @@

         expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand"

+      it "aligns the hard tabs to the correct tab stop column", ->
+        buffer.setText """
+          1\t2
+          12\t3
+          123\t4
+        """
+
+        tokenizedBuffer.setTabLength(4)
+        fullyTokenize(tokenizedBuffer)
+
+        expect(tokenizedBuffer.lineForScreenRow(0).text).toBe "1   2"
+        expect(tokenizedBuffer.lineForScreenRow(1).text).toBe "12  3"
+        expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "123 4"
+
   describe "when the buffer contains surrogate pairs", ->
     beforeEach ->
       waitsForPromise ->


test: piping in and callback out

test: piping in and callback out

--- test/api.pipe.coffee
+++ test/api.pipe.coffee
@@ -5,7 +5,7 @@

 describe 'api pipe', ->

-  it 'work with producer', (next) ->
+  it 'piping in and reading out', (next) ->
     finished = false
     parser = parse()
     data = []
@@ -24,6 +24,15 @@
       next()
     generator.pipe(parser)

+  it 'piping in and callback out', (next) ->
+    generator = generate length: 2, seed: 1, columns: 2, fixed_size: true
+    generator.pipe parse (err, data) ->
+      data.should.eql [
+        [ 'OMH', 'ONKCHhJmjadoA' ]
+        [ 'D', 'GeACHiN' ]
+      ]
+      next()
+
   it 'catch source error', (next) ->
     parser = parse()
     parser.on 'error', ->


fix for CI

fix for CI

--- test/bin_spec.coffee
+++ test/bin_spec.coffee
@@ -3,6 +3,9 @@

 describe "bin/cypress", ->
   it "is executable", (done) ->
-    cp.exec "cypress --version", (err, stdout, stderr) ->
+    cp.exec "bin/cypress --version", (err, stdout, stderr) ->
       expect(stdout).to.eq(pkg.version + "\n")
-      done()+      done()
+
+  it "is set in package.json", ->
+    expect(pkg.bin).to.deep.eq({cypress: "bin/cypress"})


JSTextField focus method implement

JSTextField focus method implement

--- JSKit/04_JSTextField.coffee
+++ JSKit/04_JSTextField.coffee
@@ -13,6 +13,7 @@
 		@_text = ""
 		@_editable = true
 		@_action = null
+		@_focus = false

 	getText:->
 		if (@_editable == true)
@@ -124,6 +125,14 @@

 	setDidKeyPressEvent:(@action)->

+	setFocus:(@_focus)->
+		if (!$(@_viewSelector+"_text").length)
+			return
+		if (@_focus == true)
+			$(@_viewSelector+"_text").focus()
+		else
+			$(@_viewSelector+"_text").blur()
+
 	viewDidAppear: ->
 		super()
 		@setEditable(@_editable)


Fixed bugs with the 'contains' and 'startsWith' methods.

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -1,5 +1,5 @@
 String::contains = (string) ->
-  @match "#{string}"
+  @indexOf(string) isnt -1

 String::decapitalize = ->
   first = @[0..0]
@@ -8,4 +8,4 @@
   "#{first.toLowerCase()}#{rest}"

 String::startsWith = (string) ->
-  @match "^#{string}"
+  @substring(0, string.length) is string


Converted to Universal Analytics

Converted to Universal Analytics

--- app/assets/javascripts/google_analytics.js.coffee
+++ app/assets/javascripts/google_analytics.js.coffee
@@ -9,20 +9,20 @@

     # Track all clicks in the results list
     if $a.closest(".results").length > 0
-      _gaq.push(['_trackEvent', 'SearchClickPosition', GAAction, GALabel, $(".results > ol > li").index($a.closest("li")) + 1, 10])
+      ga('send', 'event', 'SearchClickPosition', GAAction, GALabel, $(".results > ol > li").index($a.closest("li")) + 1)

     # Track clicks on breadcrumbs in the results list
     if $a.closest(".breadcrumbs").length > 0
-      _gaq.push(['_trackEvent', 'SearchClickBreadcrumb', GAAction,  GALabel])
+      ga('send', 'event', 'SearchClickBreadcrumb', GAAction,  GALabel)

     # Track clicks on editors choice in the results list
     if $a.closest(".editors_choice").length > 0
-      _gaq.push(['_trackEvent', 'SearchClickEditorsChoice', GAAction,  GALabel])
+      ga('send', 'event', 'SearchClickEditorsChoice', GAAction,  GALabel)

     # Track clicks on recommendations in the results list
     if $a.closest(".recommendations").length > 0
-      _gaq.push(['_trackEvent', 'SearchClickRecommendations', GAAction,  GALabel])
+      ga('send', 'event', 'SearchClickRecommendations', GAAction,  GALabel)

     # Track clicks on editors choich in the results list
     if $a.closest(".categories").length > 0
-      _gaq.push(['_trackEvent', 'SearchClickCategory', GAAction,  GALabel])
+      ga('send', 'event', 'SearchClickCategory', GAAction,  GALabel)


Tests: Fix some due to reordering

Tests: Fix some due to reordering

--- test/bw/moves.coffee
+++ test/bw/moves.coffee
@@ -3511,7 +3511,7 @@
       it "completely protects the user from attacks", ->
         shared.create.call(this)
         move = @battle.getMove("Tackle")
-        mock = @sandbox.mock(move).expects('use').never()
+        mock = @sandbox.mock(move).expects('hit').never()

         @battle.recordMove(@id2, move)
         @battle.determineTurnOrder()
@@ -3529,14 +3529,14 @@
         @battle.performMove(@p2, move)
         @battle.endTurn()

-        mock = @sandbox.mock(move).expects('use').once()
+        mock = @sandbox.mock(move).expects('hit').once()
         @battle.performMove(@p2, move)
         mock.verify()

       it "does not protect the user from attacks without the protect flag", ->
         shared.create.call(this)
         move = @battle.getMove("Feint")
-        mock = @sandbox.mock(move).expects('use').once()
+        mock = @sandbox.mock(move).expects('hit').once()

         @battle.recordMove(@id2, move)
         @battle.determineTurnOrder()
@@ -4650,7 +4650,7 @@
           @battle.recordMove(@id1, move)
           @battle.recordMove(@id2, tackle)

-          mock = @sandbox.mock(tackle).expects('use').never()
+          mock = @sandbox.mock(tackle).expects('hit').never()
           @battle.continueTurn()
           mock.verify()

@@ -4663,7 +4663,7 @@
           @battle.recordMove(@id1, move)
           @battle.recordMove(@id2, tackle)

-          mock = @sandbox.mock(tackle).expects('use').once()
+          mock = @sandbox.mock(tackle).expects('hit').once()
           @battle.continueTurn()
           mock.verify()

@@ -4681,7 +4681,7 @@
             @battle.recordMove(@id1, move)
             @battle.recordMove(@id2, vulnerable)

-            mock = @sandbox.mock(vulnerable).expects('use').once()
+            mock = @sandbox.mock(vulnerable).expects('hit').once()
             @battle.continueTurn()
             mock.verify()

@@ -5943,7 +5943,7 @@
         team1: [Factory("Magikarp", item: "Poison Herb")]
       protect = @battle.getMove("Protect")
       fling = @battle.getMove("Fling")
-      mock = @sandbox.mock(fling).expects('use').never()
+      mock = @sandbox.mock(fling).expects('hit').never()
       @battle.recordMove(@id2, protect)
       @battle.recordMove(@id1, fling)
       @battle.continueTurn()


OX-721 clearing pullQueue between pings

OX-721 clearing pullQueue between pings

--- app/assets/javascripts/oxalis/model/binary.coffee
+++ app/assets/javascripts/oxalis/model/binary.coffee
@@ -101,7 +101,7 @@
         if strategy.inVelocityRange(1) and strategy.inRoundTripTimeRange(@queue.roundTripTime)

           pullQueue = strategy.ping(position, @direction, zoomStep, area, activePlane) if zoomStep? and area? and activePlane?
-
+          @queue.clear
           for entry in pullQueue
             @queue.insert(entry...)


Recent changes in the Google+ and facebook share doesnot allow custom parameters to pass the way it's passed in this Js file. It only takes Url params, so no need to provide title param in the share URL. For passing custom params we have give it in the meta tag which will be crawled by Facebook/google bot and will be picked up as the params.

--- app/assets/javascripts/social-share-button.coffee
+++ app/assets/javascripts/social-share-button.coffee
@@ -20,7 +20,7 @@
       when "douban"
         SocialShareButton.openUrl("http://shuo.douban.com/!service/share?href=#{url}&name=#{title}&image=#{img}")
       when "facebook"
-        SocialShareButton.openUrl("http://www.facebook.com/sharer.php?t=#{title}&u=#{url}")
+        SocialShareButton.openUrl("http://www.facebook.com/sharer.php?u=#{url}")
       when "qq"
         SocialShareButton.openUrl("http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=#{url}&title=#{title}&pics=#{img}")
       when "tqq"
@@ -32,7 +32,7 @@
       when "renren"
         SocialShareButton.openUrl("http://widget.renren.com/dialog/share?resourceUrl=#{url}&srcUrl=#{url}&title=#{title}&pic=#{img}&description=")
       when "google_plus"
-        SocialShareButton.openUrl("https://plus.google.com/share?url=#{url}&t=#{title}")
+        SocialShareButton.openUrl("https://plus.google.com/share?url=#{url}")
       when "google_bookmark"
         SocialShareButton.openUrl("https://www.google.com/bookmarks/mark?op=edit&output=popup&bkmk=#{url}&title=#{title}")
       when "delicious"


restore ChapterSectionMixin

restore ChapterSectionMixin

it's used in defaultStateFromProps

--- tutor/src/components/reference-book/reference-book.cjsx
+++ tutor/src/components/reference-book/reference-book.cjsx
@@ -18,7 +18,7 @@
 module.exports = React.createClass
   displayName: 'ReferenceBook'

-  mixins: [ResizeListenerMixin]
+  mixins: [ResizeListenerMixin, ChapterSectionMixin]

   propTypes:
     navbarControls: React.PropTypes.element


Update tests to pass with last change to not special casing $or

--- test/compiler.coffee
+++ test/compiler.coffee
@@ -2,7 +2,7 @@

-describe 'little-matcher compiler', ->
+describe 'compile()', ->
   spec0 = { a: { b: $eq: 2 } }
   spec1 = { a: { b: $eq: 2 }, c: { $lt: 4 } }
   spec2 = { a: { b: { $gt:50, $lt:60 } } }
@@ -14,23 +14,7 @@
       name: null
       conditions: [{
         name:'$or'
-        config:[{
-          name: null
-          conditions: []
-          leafs: [{
-            name:'a'
-            conditions:[]
-            leafs: [{
-              name: 'b'
-              conditions:[{ name:'$lt', config:{value:4} }]
-              leafs: []
-            }]
-          },{
-            name:'c'
-            conditions:[{name:'$gt',config:{value:1}}]
-            leafs:[]
-          }]
-        }]
+        config:[{a:{b:{$lt:4}}, c:{$gt:1}}]
       }]
       leafs: []
     actual = compile(spec)
@@ -49,10 +33,7 @@
           name: 'b'
           conditions: [{
             name:'$or'
-            config: [
-              {name:null,conditions:[{name: '$eq', config:{value:2}}],leafs:[]}
-              {name:null,conditions:[{name:'$eq', config:{value:5}}],leafs:[]}
-            ]
+            config: [{$eq:2},{$eq:5}]
           }]
           leafs: []
         },{


update test range

update test range

--- test/diabetes.coffee
+++ test/diabetes.coffee
@@ -16,4 +16,4 @@
     expect(@robot.respond).to.have.been.calledWith(/estimate a1c (from average )?(.*)/i)

   it 'registers a hear listener for glucose readings', ->
-    expect(@robot.hear).to.have.been.calledWith(/^(\d{2,3}|\d{1,2}\.\d)$/)
+    expect(@robot.hear).to.have.been.calledWith(/^(\d{1,3}(\.\d)?)$/)


Proper user route and accept header

Proper user route and accept header

--- app/api/oauth.coffee
+++ app/api/oauth.coffee
@@ -4,11 +4,11 @@
   match = hash.match(/access_token=(\w+)/)
   !!match && match[1]

-fetch(config.host + '/me', {
+fetch(config.host + '/api/me', {
   method: 'GET'
   headers:
     'Authorization': 'Bearer ' + extractToken window.location.hash
-    'Accept': 'application/json'
+    'Accept': 'Accept: application/vnd.api+json; version=1'
   })
   .then (user) ->
     console.log user


added tile inventory management and double-click zoom

--- bokehjs/src/coffee/renderer/geo/tile_layer.coffee
+++ bokehjs/src/coffee/renderer/geo/tile_layer.coffee
@@ -1,5 +1,23 @@
 _ = require "underscore"
 Glyph = require "../glyph/glyph"
+
+class ImagePool
+
+  constructor: () ->
+    @images = []
+
+  pop: () ->
+    img = @images.pop()
+    if img?
+      return img
+    else
+      return new Image()
+
+  push: (img) ->
+    if img.constructor == Array
+      Array::push.apply(@images, img)
+    else
+      @images.push(img)

 class ProjectionUtils

@@ -30,18 +48,53 @@
     [xmax, ymax] = @meters_to_geographic(xmax, ymax)
     return [xmin, ymin, xmax, ymax]

-class MercatorTileProvider
+class GridLayer
+
+  tile_xyz_to_key: (x, y, z) ->
+    return x.toString() + ":" + y.toString() + ":" + z.toString()
+
+  key_to_tile_xyz: (key) ->
+    return (parseInt(c) for c in key.split(':'))
+
+class MercatorTileProvider extends GridLayer
+
+

   constructor: (@url, @tile_size=256) ->
     @utils = new ProjectionUtils()
+    @pool = new ImagePool()
     @origin_shift = 2 * Math.PI * 6378137 / 2.0
     @initial_resolution = 2 * Math.PI * 6378137 / @tile_size
+    @resolutions = (@get_resolution(z) for z in [0..30])
+    @tiles = {}

   get_image_url: () ->
     throw Error "Unimplemented Method"

   get_resolution: (level) ->
     return @initial_resolution / Math.pow(2, level)
+
+  get_level_by_extent: (extent, height, width) ->
+    x_rs = (extent[2] - extent[0]) / width
+    y_rs = (extent[3] - extent[1]) / height
+    res = Math.max(x_rs, y_rs)
+
+    closest = @resolutions.reduce (previous, current) ->
+      return current if (Math.abs(current - res) < Math.abs(previous - res))
+      return previous
+
+    return @resolutions.indexOf(closest)
+
+  snap_to_zoom: (extent, height, width, level) ->
+    desired_res = @resolutions[level]
+    desired_x_delta = width * desired_res
+    desired_y_delta = height * desired_res
+
+    [xmin, ymin, xmax, ymax] = extent
+    x_adjust = (desired_x_delta - (xmax - xmin)) / 2
+    y_adjust = (desired_y_delta - (ymax - ymin)) / 2
+
+    return [xmin - x_adjust, ymin - y_adjust, xmax + x_adjust, ymax + y_adjust]

   tms_to_wmts: (x, y, z) ->
     '''Note this works both ways'''
@@ -83,14 +136,25 @@
     return [minLon, minLat, maxLon, maxLat]

   get_tiles_by_extent: (extent, level) ->
+
     [xmin, ymin, xmax, ymax] = extent
     [txmin, tymin] = @meters_to_tile(xmin, ymin, level)
     [txmax, tymax] = @meters_to_tile(xmax, ymax, level)

+    center_tile_x = (txmax - txmin) // 2
+    center_tile_y = (tymax - tymin) // 2
+
     tiles = []
-    for ty in [tymin..tymax] by 1
+    for ty in [tymax..tymin] by -1
       for tx in [txmin..txmax] by 1
-        tiles.push(@get_image_url(tx, ty, level))
+        tx = tx %% 2 ** level
+        tiles.push([tx, ty, level, @get_tile_meter_bounds(tx, ty, level)])
+
+    #TOOD: sort tiles by distance from center doesn't seem to be working
+    tiles.sort (a, b) ->
+      a_distance = Math.sqrt(Math.pow(center_tile_x - a[0], 2) + Math.pow(center_tile_y - a[1], 2))
+      b_distance = Math.sqrt(Math.pow(center_tile_x - b[0], 2) + Math.pow(center_tile_y - b[1], 2))
+      return a_distance - b_distance

     return tiles

@@ -100,10 +164,6 @@
     return @url.replace("{X}", x).replace('{Y}', y).replace("{Z}", z)

 class WMTSTileProvider extends MercatorTileProvider
-
-  get_tile_meter_bounds: (x, y, z) ->
-    [x, y, z] = @tms_to_wmts(x, y, z)
-    return super(x, y, z)

   get_image_url: (x, y, z) ->
     [x, y, z] = @tms_to_wmts(x, y, z)
@@ -129,7 +189,7 @@
       else if value == '1'
         tileX |= mask

-      else if value == '2'
+      else if value == '2 '
         tileY |= mask

       else if value == '3'
@@ -161,89 +221,166 @@
     quadKey = @tile_xyz_to_quadkey(x, y, z)
     return @url.replace("{QUADKEY}", quadKey)

-
 class TileLayerView extends Glyph.View

+  trace: (msg) ->
+    console.warn "TileLayerView :: " + msg.toString()
+
+  _create_tile_provider: (provider_type, service_url, tile_size) ->
+    return new QUADKEYTileProvider(service_url, tile_size) if(provider_type.toLowerCase() == 'quadkeytileprovider')
+    return new TMSTileProvider(service_url, tile_size) if(provider_type.toLowerCase() == 'tmstileprovider')
+    return new WMTSTileProvider(service_url, tile_size)
+
   _index_data: () ->
+    @trace("._index_data()")
     @_xy_index()

+  get_extent: () ->
+    return [@x_range.get('start'), @y_range.get('start'), @x_range.get('end'), @y_range.get('end')]
+
+  _on_dblclick: (e) =>
+    extent = @get_extent()
+    [xmin, xmax, ymin, ymax] = extent
+    zoom_level = @tile_provider.get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+
+    rect = @map_canvas.canvas.getBoundingClientRect()
+    x = e.clientX - rect.left
+    #TODO: y-dim is not perfect.  zooming doesn't seem completely correct
+    y = e.clientY - rect.top - (@map_frame.get('top') - @map_frame.get('height'))
+    y = @map_frame.get("height") - y
+
+    x_meters = @x_mapper.map_from_target(x)
+    y_meters = @y_mapper.map_from_target(y)
+
+    x_offset = Math.abs((@x_range.get('end') - @x_range.get('start')) / 2)
+    y_offset = Math.abs((@y_range.get('end') - @y_range.get('start')) / 2)
+
+    updated_extent =  [x_meters - x_offset, y_meters - y_offset, x_meters + x_offset, y_meters + y_offset]
+
+    if e.shiftKey
+      zoom_level = zoom_level - 1
+    else
+      zoom_level = zoom_level + 1
+
+    new_extent = @tile_provider.snap_to_zoom(updated_extent, @map_frame.get('height'), @map_frame.get('width'), zoom_level)
+    @x_range.set('start', new_extent[0])
+    @y_range.set('start', new_extent[1])
+    @x_range.set('end', new_extent[2])
+    @y_range.set('end', new_extent[3])
+
   _set_data: () ->
-    @need_load = (true for img in @url)
-    @loaded = (false for img in @url)
-
-    @_xy_index()
+    @trace("._set_data()")
+    @tile_provider = @_create_tile_provider(@provider_type[0], @url[0], @tile_size[0])
+    @pool = new ImagePool()
+    @initial_extent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]
+    @map_plot = @renderer.plot_view.model
+    @map_canvas = @renderer.plot_view.canvas_view.ctx
+    @map_frame = @renderer.plot_view.frame
+
+    @x_range = @map_plot.get('x_range')
+    @x_mapper = this.map_frame.get('x_mappers')['default']
+
+    @y_range = @map_plot.get('y_range')
+    @y_mapper = this.map_frame.get('y_mappers')['default']
+
+    @x_range.set('start', @initial_extent[0])
+    @y_range.set('start', @initial_extent[1])
+    @x_range.set('end', @initial_extent[2])
+    @y_range.set('end', @initial_extent[3])
+
+    @renderer.plot_view.$el.dblclick(@_on_dblclick)

   _map_data: () ->
-    @sw = @sdist(@renderer.xmapper, @x, @tile_size, 'edge', @mget('dilate'))
-    @sh = @sdist(@renderer.ymapper, @y, @tile_size, 'edge', @mget('dilate'))
+    @trace("._map_data()")
+    @sw = @sdist(@renderer.xmapper, @x, @mget('tile_size'), 'edge', @mget('dilate'))
+    @sh = @sdist(@renderer.ymapper, @y, @mget('tile_size'), 'edge', @mget('dilate'))
+
+  _on_tile_load: (e) =>
+    @tile_provider.tiles[e.target.cache_key] = {img: e.target, tile_coords: e.target.tile_coords, bounds: e.target.bounds, current: true}
+    @_render_tile(e.target.cache_key)
+
+  _on_tile_error: (e) =>
+    @trace("._on_tile_error()")
+    return ''
+
+  _is_valid_tile: (x, y, z) ->
+
+    #is tile currently on screen?
+    return true
+
+  _create_tile: (x, y, z, bounds) ->
+    tile = @pool.pop()
+    tile.onload = @_on_tile_load
+    tile.onerror = @_on_tile_error
+    tile.alt = ''
+    tile.src = @tile_provider.get_image_url(x, y, z)
+    tile.tile_coords = [x, y, z]
+    tile.bounds = bounds
+    tile.x_coord = bounds[0]
+    tile.y_coord = bounds[3]
+    tile.cache_key = @tile_provider.tile_xyz_to_key(x, y, z)
+    return tile

   _render: (ctx, indices, {url, image, need_load, sx, sy, sw, sh, angle}) ->
-    for i in indices
-
-      if isNaN(sx[i]+sy[i]+angle[i])
-        continue
-
-      if need_load[i]
-        img = new Image()
-        img.onload = do (img, i) =>
-          return () =>
-            @loaded[i] = true
-            @renderer.request_render()
-
-        img.src = url[i]
-        need_load[i] = false
-      else if @loaded[i]
-        @_render_image(ctx, i, image[i], sx, sy, @tile_size, @tile_size, angle)
-
-  _final_sx_sy: (anchor, sx, sy, sw, sh) ->
-    switch anchor
-      when "top_left"      then [sx       , sy       ]
-      when "top_center"    then [sx - sw/2, sy       ]
-      when "top_right"     then [sx - sw  , sy       ]
-      when "right_center"  then [sx - sw  , sy - sh/2]
-      when "bottom_right"  then [sx - sw  , sy - sh  ]
-      when "bottom_center" then [sx - sw/2, sy - sh  ]
-      when "bottom_left"   then [sx       , sy - sh  ]
-      when "left_center"   then [sx       , sy - sh/2]
-      when "center"        then [sx - sw/2, sy - sh/2]
-
-  _render_image: (ctx, i, image, sx, sy, sw, sh, angle) ->
-
-    anchor = @mget('anchor') or "top_left"
-    [sx, sy] = @_final_sx_sy(anchor, sx[i], sy[i], 256, 256)
-
-    ctx.save()
-
-    ctx.globalAlpha = @mget("global_alpha")
-
-    if angle[i]
-      ctx.translate(sx, sy)
-      ctx.rotate(angle[i])
-      ctx.drawImage(image, 0, 0, 256, 256)
-      ctx.rotate(-angle[i])
-      ctx.translate(-sx, -sy)
-    else
-      ctx.drawImage(image, sx, sy, 256, 256)
-    ctx.restore()
+    @_update()
+
+  _render_tile: (tile_key) ->
+    tile_obj = @tile_provider.tiles[tile_key]
+    if tile_obj?
+      @map_canvas.save()
+      [sx, sy] = @renderer.plot_view.frame.map_to_screen([tile_obj.bounds[0]], [tile_obj.bounds[3]], @renderer.plot_view.canvas)
+      @map_canvas.rect(
+        @map_frame.get('left')+1, @map_frame.get('bottom')+2,
+        @map_frame.get('width')-2, @map_frame.get('height'),
+      )
+      @map_canvas.clip()
+      @map_canvas.drawImage(tile_obj.img, sx[0], sy[0], 256, 256)
+      @map_canvas.restore()
+
+  _update: () =>
+    if window.stop?
+      window.stop()
+    else if document.execCommand?
+      document.execCommand("Stop", false)
+
+    @trace("._render()")
+    extent = @get_extent()
+    zoom_level = @tile_provider.get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+    new_extent = @tile_provider.snap_to_zoom(extent, @map_frame.get('height'), @map_frame.get('width'), zoom_level)
+
+    @x_range.set('start', new_extent[0])
+    @y_range.set('start', new_extent[1])
+    @x_range.set('end', new_extent[2])
+    @y_range.set('end', new_extent[3])
+    tiles = @tile_provider.get_tiles_by_extent(new_extent, zoom_level)
+
+    for t in tiles
+      [x, y, z, bounds] = t
+      if @_is_valid_tile(x, y, z)
+        key = @tile_provider.tile_xyz_to_key(x, y, z)
+        if key of @tile_provider.tiles
+          @_render_tile(key)
+        else
+          @_create_tile(x, y, z, bounds)

 class TileLayer extends Glyph.Model
   default_view: TileLayerView
   type: 'TileLayer'
-  tile_width: 256
-  tile_height: 256
   visuals: []
   distances: ['w', 'h']
   angles: ['angle']
-  fields: ['url:string']
+  fields: ['url:string', 'provider_type:string', 'tile_size']

   defaults: ->
     return _.extend {}, super(), {
       angle: 0
+      tile_size: 256
       global_alpha: 1.0
     }

   display_defaults: ->
     return _.extend {}, super(), {
+      level: 'glyph'
     }

 module.exports =


Update ja.coffee

Update ja.coffee

--- app/locale/ja.coffee
+++ app/locale/ja.coffee
@@ -156,12 +156,12 @@
     body: "体"
     version: "バージョン"
     pending: "ペンディング"
-    accepted: "容認された"
-    rejected: "拒否された"
-    withdrawn: "取り下げられました"
-#    accept: "Accept"
-#    reject: "Reject"
-#    withdraw: "Withdraw"
+    accepted: "容認済み"
+    rejected: "拒否済み"
+    withdrawn: "取り下げ済み"
+    accept: "容認"
+    reject: "拒否"
+    withdraw: "取り下げ"
     submitter: "提出者"
     submitted: "提出されました"
     commit_msg: "コミットメッセージ"
@@ -217,8 +217,8 @@

   play_level:
     done: "完了"
-#    next_game: "Next game"
-#    show_menu: "Show game menu"
+    next_game: "次のゲーム"
+    show_menu: "ゲームメニューを見る"
     home: "ホーム" # Not used any more, will be removed soon.
     level: "ゲームレベル" # Like "Level: Dungeons of Kithgard"
     skip: "スキップ"
@@ -340,8 +340,8 @@
     tip_recurse: "繰り返しは人間、再帰は神。 - L・ピーター・ドイツ"
     tip_free_your_mind: "全ての雑念を捨てろ、恐怖、疑いも不信も 心を解き放つんだ - モーフィアス"
     tip_strong_opponents: "どんな強者にも弱点というものはあるんだ… - うちは　イタチ"
-    tip_paper_and_pen: "コーディングを始める前に, 必ず紙とペンを用意して計画を立てましょう。"
-#    tip_solve_then_write: "First, solve the problem. Then, write the code. - John Johnson"
+    tip_paper_and_pen: "コーディングを始める前に、 必ず紙とペンを用意して計画を立てましょう。"
+    tip_solve_then_write: "まず、問題を解決しよう。それから、コードを書こう。 - ジョン・ジョンソン"

   game_menu:
     inventory_tab: "インベントリー"
@@ -526,10 +526,10 @@

   skill_docs:
     writable: "書き込み可能な" # Hover over "attack" in Your Skills while playing a level to see most of this
-    read_only: "リードオンリー"
-#    action: "Action"
-#    spell: "Spell"
-    action_name: "ネーム"
+    read_only: "読み込み限定"
+    action: "アクション"
+    spell: "呪文"
+    action_name: "アクション名"
     action_cooldown: "テイク"
     action_specific_cooldown: "クールダウン"
     action_damage: "ダメージ"
@@ -966,7 +966,7 @@

   editor:
     main_title: "CodeCombatエディター"
-#    article_title: "Article Editor"
+    article_title: "アーティクル エディター"
     thang_title: "サングエディター"
     level_title: "レベルエディター"
     achievement_title: "実績エディター"


Update ms.coffee

Update ms.coffee

--- app/locale/ms.coffee
+++ app/locale/ms.coffee
@@ -199,7 +199,7 @@
   modal:
     close: "Tutup"
     okay: "Ok"
-#    cancel: "Cancel"
+    cancel: "Batal"

   not_found:
     page_not_found: "Halaman tidak ditemui"
@@ -879,7 +879,7 @@
 #    personal_sub: "Personal Subscription" # Accounts Subscription View below
 #    loading_info: "Loading subscription information..."
 #    managed_by: "Managed by"
-#    will_be_cancelled: "Will be cancelled on"
+    will_be_cancelled: "Akan dibatalkan pada"
 #    currently_free: "You currently have a free subscription"
 #    currently_free_until: "You currently have a subscription until"
 #    free_subscription: "Free subscription"
@@ -1984,7 +1984,7 @@
 #  teacher_ozaria_encouragement_modal:
 #    title: "Build Computer Science Skills to Save Ozaria"
 #    sub_title: "You are invited to try the new adventure game from CodeCombat"
-#    cancel: "Back to CodeCombat"
+    cancel: "Kembali ke CodeCombat"
 #    accept: "Try First Unit Free"
 #    bullet1: "Deepen student connection to learning through an epic story and immersive gameplay"
 #    bullet2: "Teach CS fundamentals, Python or JavaScript and 21st century skills"


Update vi.coffee

Update vi.coffee

--- app/locale/vi.coffee
+++ app/locale/vi.coffee
@@ -607,7 +607,7 @@
 #    why_paragraph_2_italic: "yay a badge"
     why_paragraph_2_center: "Nhưng nó có vẽ vui"
 #    why_paragraph_2_center: "but fun like"
-    whyparagraph__italic_caps: "CHƯA XONG MÀN NÀY THÌ CHƯA ĐƯỢC MẸ ƠI!"
+    whyparagraph__italic_caps: "LÀM XONG MÀN NÀY CON SẼ XUỐNG MẸ ƠI"
 #    why_paragraph_2_italic_caps: "NO MOM I HAVE TO FINISH THE LEVEL!"
     why_paragraph_2_suffix: "Đó là lý do tại sao CodeCombat là một trò chơi dành cho nhiều người, không phải một khóa học bằng trò chơi. Chúng tôi sẽ không dừng lại cho đến khi nào bạn không thể dừng lại được--nhưng nó là theo ý tốt nha."
 #    why_paragraph_2_suffix: "That's why CodeCombat is a multiplayer game, not a gamified lesson course. We won't stop until you can't stop--but this time, that's a good thing."
@@ -782,12 +782,12 @@
   community:
     main_title: "Cộng đồng CodeCombat"
     introduction: "Hãy coi thử bạn có thể đóng góp vào mảng nào thú vị nhất với mình. Chúng tôi đang đợi được làm việc chung với bạn!"
-#    level_editor_prefix: "Use the CodeCombat"
-#    level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!"
-#    thang_editor_prefix: "We call units within the game 'thangs'. Use the"
-#    thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites."
-#    article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the"
-#    article_editor_suffix: "and help CodeCombat players get the most out of their playtime."
+##    level_editor_prefix: "Use the CodeCombat"
+##    level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!"
+##   thang_editor_prefix: "We call units within the game 'thangs'. Use the"
+##   thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites."
+##    article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the"
+##   article_editor_suffix: "and help CodeCombat players get the most out of their playtime."
     find_us: "Tìm chúng tôi trên những trang này"
     social_github: "Hãy coi thử tất cả dòng lệnh của chúng tôi trên GitHub"
     social_blog: "Đọc Blog của CodeCombat trên Sett"
@@ -1120,7 +1120,9 @@
     ambassador_summary: "Thuần hóa những thành viện trong diễn đàn và đưa ra hướng dẫn cho những thắc mắc. Đoàn sứ giả của chúng tôi là bộ mặt của CodeCombat gửi gắm cho thế giới."

   editor:
+    main_title: "Trình soạn cấp độ CodeCombat"
 #    main_title: "CodeCombat Editors"
+
 #    article_title: "Article Editor"
 #    thang_title: "Thang Editor"
 #    level_title: "Level Editor"
@@ -1262,6 +1264,7 @@
 #    artisan_join_step4: "Post your levels on the forum for feedback."
 #    artisan_subscribe_desc: "Get emails on level editor updates and announcements."
 #    adventurer_introduction: "Let's be clear about your role: you are the tank. You're going to take heavy damage. We need people to try out brand-new levels and help identify how to make things better. The pain will be enormous; making good games is a long process and no one gets it right the first time. If you can endure and have a high constitution score, then this class might be for you."
+    adventurer_attribute_1: "Một khát khao được học hỏi. Bạn muốn học làm thế nào để lập trình và chúng tôi muốn dạy bạn điều đó. Tuy nhiên hầu hết là bạn sẽ phải tự thân vận động."
 #    adventurer_attribute_1: "A thirst for learning. You want to learn how to code and we want to teach you how to code. You'll probably be doing most of the teaching in this case, though."
 #    adventurer_attribute_2: "Charismatic. Be gentle but articulate about what needs improving, and offer suggestions on how to improve."
 #    adventurer_join_pref: "Either get together with (or recruit!) an Artisan and work with them, or check the box below to receive emails when there are new levels to test. We'll also be posting about levels to review on our networks like"


Updated formatting to 'welcome.cson' in Hebrew

--- def/he/welcome.cson
+++ def/he/welcome.cson
@@ -1,28 +1,28 @@
 Welcome:
-  tabTitle: "ברוך הבא"
+  tabTitle: "ברוך הבא!"
   subtitle: {
-    text1: "עורך טקסט עדכני עבור המאה"
-    superscript: "st"
-    text2: " העשרים ואחת"
+    text1: "עורך טקסט עדכני עבור המאה "
+    superscript: ""
+    text2: "העשרים ואחת"
   }
   help: {
     forHelpVisit: "לקבלת עזרה, בבקשה בקר"
     atomDocs: {
       text1: "את "
       link: "הדוקומנטציה הרשמית"
-      text2: " בשביל מדריכים והפנייה ל-API."
+      text2: " בשביל מדריכים והפנייה לממשק תכנות היישומים"
       _template: "${text1}${link}${text2}"
     }
     atomForum: {
-      text1: "את הפורום של אטום ב "
+      text1: ""
       link: "discuss.atom.io"
-      text2: ""   # optional
+      text2: "את הפורום של אטום ב"
       _template: "${text1}${link}${text2}"
     }
     atomOrg: {
       text1: "את "
       link: "אטום אורג"
-      text2: ". זהו המקום שבו תמצא את כל התוספים הרשמיים של אטום."
+      text2: ". זהו המקום שבו תמצא את כל התוספים הרשמיים של אטום"
       _template: "${text1}${link}${text2}"
     }
   }


Third batch of translations

Third batch of translations

--- def/nl/context.cson
+++ def/nl/context.cson
@@ -2,8 +2,8 @@
   "atom-workspace":
     "application:inspect": "Inspect Element"
   "atom-text-editor":
-    "color-picker:open": "Color Picker"
-    "minimap:toggle": "Toggle Minimap"
+    "color-picker:open": "Kleurenkiezer"
+    "minimap:toggle": "Minimap Aan/Uit"
   "atom-text-editor, .overlayer":
     "core:undo": "Ongedaan maken"
     "core:redo": "Opnieuw"


Lowered game ticker to 2 seconds

Lowered game ticker to 2 seconds

--- server/main.coffee
+++ server/main.coffee
@@ -41,7 +41,7 @@
             MapGrid :
               tiles : n.gameData.MapGrid.tiles )
     return
-  ,4000
+  ,2000

   #Delete Games if the users are no longer in them every few minutes
   Meteor.setInterval () ->


Update main.coffee

Update main.coffee

--- server/main.coffee
+++ server/main.coffee
@@ -26,11 +26,9 @@
 		attachData profileImageUrl
 		picture.name('picture' + user._id)
 		profilePicture = ProfilePictures.insert picture
-		user.profile =
-			picture: profilePicture._id
+		user.profile.picture = profilePicture._id
 	else
 		email = user.emails?[0]?.address or ''
 		url = Gravatar.imageUrl email
-		user.profile =
-			picture: url
-	user+		user.profile.picture =  url
+	user


JDomain: pass group information while creating domains

--- workers/social/lib/social/models/domain.coffee
+++ workers/social/lib/social/models/domain.coffee
@@ -301,13 +301,11 @@
             console.log err  if err?

     domains.forEach (domain) ->
-      domainObj = new JDomain
-        domain        : domain
+      domainObj = new JDomain {
+        domain, group, stack,
         hostnameAlias : [hostnameAlias]
-        proxy         : { mode: 'vm' }
-        regYears      : 0
-        loadBalancer  : { persistance: 'disabled' }
-        stack         : stack
+      }
+
       domainObj.save (err)->
         if err
         then console.error err  unless err.code is 11000
@@ -323,7 +321,10 @@
       requiredDomains = ["#{groupSlug}.#{domain}", "shared.#{groupSlug}.#{domain}"]

     {hostnameAlias} = vm
-    @createDomains {account, domains:requiredDomains, hostnameAlias, stack}
+    @createDomains {
+      account, hostnameAlias, stack,
+      domains:requiredDomains, group: groupSlug
+    }

   bindVM: (client, params, callback)->
     domainName = @domain


[market] market order expriation text: more predictable unit changes

--- app/js/services/utils.coffee
+++ app/js/services/utils.coffee
@@ -134,15 +134,15 @@
         diff = (date - Date.now()) / 1000.0
         #console.log _date,date,diff,date.toLocaleDateString() #20140930T191750 Tue Sep 30 2014 15:17:50 GMT-0400 (EDT) -71989.91 9/30/2014

-        return if diff < 0
+        return if diff <= 0
             $translate("utils.expired")
-        else if diff < 60
+        else if diff <= 60
             $translate("utils.seconds", {value: Math.round(diff)})
-        else if diff < 3600 # 1 hour
+        else if diff <= 3600 # 1 hour
             $translate("utils.minutes", {value: Math.round(diff/60.0)})
-        else if diff < 24 * 3600
+        else if diff <= 24 * 3600
             $translate("utils.hours", {value: Math.round(diff/3600.0)})
-        else if diff < 30 * 24 * 3600
+        else if diff <= 30 * 24 * 3600
             $translate("utils.days", {value: Math.round(diff/3600.0/24)})
         else
             deferred = $q.defer()


allverse/table: Fix bug in table.remove()

--- Oghma/allverse/table.coffee
+++ Oghma/allverse/table.coffee
@@ -57,7 +57,7 @@
         thingy.__

       remove: ( thingy ) ->
-        thingyverse.login.remove( thingy )
+        thingyverse.table.remove( thingy )
         null
   )
 )


Comments.

Comments.

--- src/xo.coffee
+++ src/xo.coffee
@@ -160,8 +160,6 @@
         entered = {}
         exited = {}

-      # TODO: maybe close events (500ms) could be merged to limit
-      # network consumption.
       @_xobjs.on 'any', (event, items) ->
         unless dispatcherRegistered
           dispatcherRegistered = true
@@ -275,6 +273,9 @@
       $debug 'objects inserted into the database '

       # Finally, monitors events.
+      #
+      # TODO: maybe close events (500ms) could be merged to limit
+      # CPU & network consumption.
       loop
         $wait xapi.call 'event.register', ['*']

@@ -311,6 +312,8 @@
               remove: false
             }
         catch error
+          # FIXME: The proper approach with events loss or
+          # disconnection is to redownload all objects.
           if error[0] is 'EVENTS_LOST'
             # XAPI error, the program must unregister from events and then
             # register again.


javascript apparently interprets the string to a boolean, making this check fail

--- app/assets/javascripts/components/editor.coffee
+++ app/assets/javascripts/components/editor.coffee
@@ -38,7 +38,7 @@
   }
 }

-if $('#image_upload').data('value') == 'true'
+if $('#image_upload').data('value') == true
   $("#article_content").inlineattach(options)


add copyright/acceptable policies to footer

--- client/Main/CommonViews/footerview.coffee
+++ client/Main/CommonViews/footerview.coffee
@@ -41,6 +41,8 @@
         MEETUPS
         TESTIMONIALS
         BRAND GUIDELINES
+        COPYRIGHT/DMCA GUIDELINES
+        ACCEPTABLE USER POLICY


better algolia fallback

better algolia fallback

--- client/Main/autocompletecontroller.coffee
+++ client/Main/autocompletecontroller.coffee
@@ -26,6 +26,9 @@

   searchAccounts: (seed) ->
     @search 'accounts', seed
+      .then (data) ->
+        throw new Error "No data!" if data.length is 0
+        return data
       .map ({ objectID }) -> KD.remote.cacheableAsync 'JAccount', objectID
       .catch (err) => @searchAccountsMongo seed
       .filter Boolean


Socialapi: map result of leaf channels

Socialapi: map result of leaf channels

--- client/app/lib/socialapicontroller.coffee
+++ client/app/lib/socialapicontroller.coffee
@@ -682,7 +682,9 @@
       doXhrRequest {
         endPoint : "/api/social/moderation/channel/#{options.rootId}/link?#{serialize(options)}"
         type     : 'GET'
-      }, callback
+      }, (err, result)->
+        return callback err if err
+        return callback null, mapChannels result

     blacklist: (options, callback) ->
       doXhrRequest {


Fetching text

Fetching text

--- SingularityUI/app/components/aggregateTail/Contents.cjsx
+++ SingularityUI/app/components/aggregateTail/Contents.cjsx
@@ -57,9 +57,11 @@
       @stopTailingPoll()
       @setState
         isLoading: true
+        loadingText: 'Fetching'
       @props.fetchPrevious(=>
         @setState
           isLoading: false
+          loadingText: ''
       )
     else
       @stopTailingPoll()


Finish extracting information from the row

--- app/assets/javascripts/migrate_donations.coffee
+++ app/assets/javascripts/migrate_donations.coffee
@@ -23,6 +23,34 @@

   dateField.val(dateValue)

+  depadDecomma = (x) ->
+    spacingMatch = /^\s*(.*?)(?:\s|,)*$/.exec(x)
+
+    if spacingMatch
+      spacingMatch[1]
+    else
+      x
+
+  donorSelector = $("#donor-selector")
+  name = originalNotes.replace(/\s+(\d+)\/(\d+)\/(\d+)\b/, "")
+  address = ""
+  email = ""
+  notes = ""
+  match = /Donation from: (.*?)(?:\s+address: (.*?))?(?:\s+email: (.*?))?(?:\s+notes: (.*?))?$/.exec(name)
+  name = depadDecomma(match[1]) if match
+  address = depadDecomma(match[2]) if match && match[2]
+  email = depadDecomma(match[3]) if match && match[3]
+  notes = depadDecomma(match[4]) if match && match[4]
+
+  if donorSelector.val() == ""
+    donorSelector.val("new").trigger("change")
+    $("#donor-name").val(name)
+    $("#donor-address").val(address)
+    $("#donor-email").val(email)
+
+  if notesField.val() == ""
+    notesField.val(notes)
+
 $(document).on "change", ".migrate-donation-checkbox", ->
   checkbox = $(@)
   row = checkbox.parents("tr:first")


Made the session model use the users preferred status.

--- app/assets/javascripts/models/session.js.coffee
+++ app/assets/javascripts/models/session.js.coffee
@@ -13,7 +13,7 @@

     @get('users').add(@get('user'))
-    @get('user').set('status','online')
+    @get('user').set('status',@get('user').get('preferred_status'))
     @listenToPrivateChannel()

     @bindEvents()
@@ -27,7 +27,7 @@
   bindEvents: ->

     @get('user').on 'change', (user) =>
-      user.set('status','online')
+      user.set('status',@get('user').get('preferred_status'))
       window.location.replace("/logout") if user.get('is_banned')
       @listenToPrivateChannel()


Pass token to rpc://user.reset

Pass token to rpc://user.reset

--- client/modules/packages/user/reset.coffee
+++ client/modules/packages/user/reset.coffee
@@ -7,8 +7,8 @@
 	title: 'Reset your password'

 	controller: [
-		'$location', '$scope', 'ui/notifications'
-		($location, $scope, notifications) ->
+		'$location', '$routeParams', '$scope', 'ui/notifications'
+		($location, $routeParams, $scope, notifications) ->

 			$scope.userReset =

@@ -16,6 +16,10 @@
 					type: 'password'
 					label: "New password"
 					required: true
+
+				token:
+					type: 'hidden'
+					value: $routeParams.token

 				submit:
 					type: 'submit'


Port IP tests

Port IP tests

--- spec/IP.coffee
+++ spec/IP.coffee
@@ -1,37 +1,37 @@
 if typeof process isnt 'undefined' and process.execPath and process.execPath.match /node|iojs/
   chai = require 'chai' unless chai
-  IP = require '../src/lib/IP'
+  noflo = require '../src/lib/NoFlo.coffee'
 else
-  IP = require 'noflo/src/lib/IP'
+  noflo = require 'noflo'

 describe 'IP object', ->
   it 'should create IPs of different types', ->
-    open = new IP 'openBracket'
-    data = new IP 'data', "Payload"
-    close = new IP 'closeBracket'
+    open = new noflo.IP 'openBracket'
+    data = new noflo.IP 'data', "Payload"
+    close = new noflo.IP 'closeBracket'
     chai.expect(open.type).to.equal 'openBracket'
     chai.expect(close.type).to.equal 'closeBracket'
     chai.expect(data.type).to.equal 'data'

   it 'should attach data groups explicitly', ->
-    data = new IP 'data', "Payload"
+    data = new noflo.IP 'data', "Payload"
     chai.expect(data.groups).to.be.an.instanceof Array
     chai.expect(data.groups).to.have.lengthOf 0
     data.groups = ['foo', 'bar']
     chai.expect(data.groups).to.have.lengthOf 2

   it 'should be moved to an owner', ->
-    p = new IP 'data', "Token"
+    p = new noflo.IP 'data', "Token"
     p.move 'SomeProc'
     chai.expect(p.owner).to.equal 'SomeProc'

   it 'should support sync context scoping', ->
-    p = new IP 'data', "Request-specific"
+    p = new noflo.IP 'data', "Request-specific"
     p.scope = 'request-12345'
     chai.expect(p.scope).to.equal 'request-12345'

   it 'should be able to clone itself', ->
-    d1 = new IP 'data', "Trooper",
+    d1 = new noflo.IP 'data', "Trooper",
       groups: ['foo', 'bar']
       owner: 'SomeProc'
       scope: 'request-12345'
@@ -45,7 +45,7 @@
     chai.expect(d2.scope).to.equal d1.scope

   it 'should dispose its contents when dropped', ->
-    p = new IP 'data', "Garbage"
+    p = new noflo.IP 'data', "Garbage"
     p.groups = ['foo', 'bar']
     p.drop()
     chai.expect(Object.keys(p)).to.have.lengthOf 0


not everyone has a storage

not everyone has a storage

--- client/app/MainApp/sidebar/findersettingsbuttonview.coffee
+++ client/app/MainApp/sidebar/findersettingsbuttonview.coffee
@@ -41,8 +41,7 @@

   hideFiles:(state)->
     @appStorage.fetchStorage (storage)=>
-      unless storage
-        return KD.showError "An error occured. Please try again later."
+      return  unless storage
       @appStorage.setValue "hideDotFiles", state
       @emit "dotFileStateChanged", state


Removed unused test code

Removed unused test code

--- cms/static/coffee/spec/views/feedback_spec.coffee
+++ cms/static/coffee/spec/views/feedback_spec.coffee
@@ -133,7 +133,6 @@
         @clock = sinon.useFakeTimers()

     afterEach ->
-        delete CMS.Views.Notification.active;
         @clock.restore()

     it "a minShown view should not hide too quickly", ->


env variable APP_PORT to enable running application on a custom port

--- routes/index.coffee
+++ routes/index.coffee
@@ -10,10 +10,11 @@

 # Configure google login
 protocol = if process.env.USE_SSL is '1' then 'https' else 'http'
+port = if process.env.APP_PORT then ":" + process.env.APP_PORT else ""
 strategy = new GoogleStrategy
     clientID:     process.env.GOOGLE_CLIENT_ID
     clientSecret: process.env.GOOGLE_CLIENT_SECRET
-    callbackURL: "#{protocol}://#{process.env.DOMAIN or "127.0.0.1"}:#{process.env.PORT or "3000"}/auth/google/return"
+    callbackURL: "#{protocol}://#{process.env.DOMAIN or "127.0.0.1"}#{port}/auth/google/return"
     # realm:     process.env.GOOGLE_REALM      or "http://localhost:#{process.env.PORT or 3000}/"
     # returnURL: process.env.GOOGLE_RETURN_URL or "http://localhost:#{process.env.PORT or 3000}/auth/google/return"
   , (accessToken, refreshToken, profile, done) ->


test love.run

test love.run

--- test/src/love.coffee
+++ test/src/love.coffee
@@ -60,3 +60,30 @@

         expect(love.graphics.getWidth()).to.equal(900)
         expect(love.graphics.getHeight()).to.equal(300)
+
+  describe '.run', ->
+    it 'invokes the expected functions', ->
+      @clock = sinon.useFakeTimers(0, "setTimeout", "clearTimeout", "Date")
+
+      love = new Love()
+
+      load = sinon.spy(love, 'load')
+      update = sinon.spy(love, 'update')
+      clear = sinon.spy(love.graphics, 'clear')
+      draw = sinon.spy(love, 'draw')
+
+      counter = 0
+      nextFrame = sinon.stub love.timer, 'nextFrame', (f) ->
+        return if counter != 0
+        counter += 1
+        f(0)
+
+      love.run()
+
+      expect(load).to.have.been.called
+      expect(update).to.have.been.called
+      expect(clear).to.have.been.called
+      expect(draw).to.have.been.called
+      expect(nextFrame).to.have.been.called
+
+      this.clock.restore()


Adding more snippets

Adding more snippets

--- snippets/dump.cson
+++ snippets/dump.cson
@@ -12,3 +12,6 @@
   'dump4':
     'prefix': 'dump the HAR file to "filename"'
     'body': 'dump the HAR file to "$1"'
+  'dump5':
+     'prefix': 'dump the value of the alias "alias" to the console'
+     'body': 'dump the value of the alias "$1" to the console'


Fix abilities

Fix abilities

--- app/abilities/base.coffee
+++ app/abilities/base.coffee
@@ -1,8 +1,7 @@
 class BaseAbility

-  _abilities: { }
-
   constructor: (model) ->
+    @_abilities = {}
     @intitalize(model)

   intitalize: ->


Update FlexibilityOrder to use new ETEngine API

--- app/assets/javascripts/lib/util/flexibility_order.coffee
+++ app/assets/javascripts/lib/util/flexibility_order.coffee
@@ -2,14 +2,14 @@
   constructor: (@element) ->
     @lastGood = null

-  url: (path) ->
-    "#{ App.scenario.url_path() }/flexibility_order/#{ path }"
+  url: ->
+    "#{App.scenario.url_path()}/flexibility_order"

   update: (sortable) =>
     options = sortable.toArray()

     $.ajax
-      url: @url('set'),
+      url: @url(),
       type: 'POST',
       data:
         flexibility_order:
@@ -23,7 +23,7 @@

   render: =>
     $.ajax
-      url: @url('get')
+      url: @url()
       type: 'GET'
       success: (data) =>
         Sortable.create @element,


Lazy load images after actually opening the entry.

--- app/assets/javascripts/ng-services/ng-EntrySvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-EntrySvc.js.coffee
@@ -105,12 +105,12 @@
         # User is closing the open entry
         openEntrySvc.close entry
       else
-        # lazy load images
-        lazyLoadingSvc.load_images entry
         openEntrySvc.open entry
         if !entry.read
           # User is opening an unread entry, mark it as read
           change_entry_state entry, true
+        # lazy load images
+        lazyLoadingSvc.load_images entry

     #--------------------------------------------
     # Mark a single entry as unread


Fix callback

Fix callback

--- views/components/settings/parts/plugin-config.cjsx
+++ views/components/settings/parts/plugin-config.cjsx
@@ -361,18 +361,18 @@
     npm.load npmConfig, (err) =>
       npm.commands.install [name], (er, data) ->
         callback(data, er)
-  handleReinstall: (index, callback) ->
+  handleReinstall: (index) ->
     if !@props.disabled
       failStatus = @state.failStatus
       failStatus[index] = 1
       name = path.basename fails[index]
-      @handleManuallyInstall.bind @, name, callback
+      @handleManuallyInstall name, @handleReinstallComplete
       @setState {failStatus}
   handleReinstallComplete: (data, er) ->
     failStatus = @state.failStatus
     for arr in data
       name = arr[0].split('@')[0]
-      for fail, index of fails
+      for fail, index in fails
         if name == path.basename fail
           failStatus[index] = if er then 0 else 2
     @setState {failStatus}
@@ -640,7 +640,7 @@
                     </Button>
                     <Button bsStyle='primary'
                             disabled={@state.failStatus[index] != 0}
-                            onClick={@handleReinstall.bind @, index, @handleReinstallComplete}
+                            onClick={@handleReinstall.bind @, index}
                             style={width: "33%"}
                             className="plugin-control-button">
                       <FontAwesome name={


trakttv: remove the getOrFetchEpisodeData monstrosity

--- src/js/trakttv.coffee
+++ src/js/trakttv.coffee
@@ -216,68 +216,6 @@
     (response, status, req) ->
       cb status

-trakttv.getOrFetchEpisodeData = (showID, seasonNumber, episodeNumber, callback) ->
-  # toWatchMenu.on 'select', (e) ->
-  # element = e.item
-  # console.log "getOrFetchEpisodeData for #{showID}, #{seasonNumber}, #{episodeNumber}"
-  item = i for i in shows when i.show.ids.trakt == showID
-  # console.log "item: #{JSON.stringify item}"
-  season = s for s in item.seasons when s.number == seasonNumber
-  unless season?
-    season =
-      number: seasonNumber
-      aired: 0
-      completed: 0
-      episodes: []
-  # console.log "season: #{JSON.stringify season}"
-  episode = e for e in season.episodes when e.number == episodeNumber
-  # console.log "episode: #{JSON.stringify episode}"
-  unless episode?
-    episode =
-      number: episodeNumber
-      completed: false
-    season.episodes.push episode
-  # console.log "Considering episode: #{JSON.stringify episode}"
-
-  episode.seasonNumber = seasonNumber
-  episode.episodeNumber = episodeNumber
-  episode.showID = showID
-  episode.showTitle = item.show.title
-
-  getOrFetchOverview = (success) ->
-    if episode.overview?
-      # console.log "Overview already available"
-      success(episode)
-      return
-    # console.log "fetching overview..."
-    trakttv.request "/search?id_type=trakt-episode&id=#{episode.episodeID}",
-      (response, status, req) ->
-        # console.log "Fetched overview: #{response}"
-        if response
-          episode.overview = response[0].episode.overview
-          Settings.data shows: shows
-        success? episode
-
-  fetchEpisodeIDAndTitle = (showID, seasonNumber, episodeNumber, successFetchEpisodeIDAndTitle) ->
-    trakttv.request "/shows/#{showID}/seasons/#{seasonNumber}/episodes/#{episodeNumber}",
-      (response, status, req) ->
-        # console.log "fetchEpisodeIDAndTitle response: #{response}"
-        episode.episodeID = response.ids.trakt
-        episode.episodeTitle = response.title
-        Settings.data shows: shows
-
-        successFetchEpisodeIDAndTitle(episode) if successFetchEpisodeIDAndTitle?
-
-  if episode.episodeID? and episode.episodeTitle?
-    # console.log "going to fetch overview"
-    getOrFetchOverview callback
-  else
-    # console.log "going to fetch ep id and title"
-    fetchEpisodeIDAndTitle showID, seasonNumber, episodeNumber,
-      (episode) ->
-        # console.log "fetched id and title: #{JSON.stringify episode}"
-        getOrFetchOverview callback
-
 trakttv.markEpisode = (episodeObj, seen, watched_at, cb) ->
   episode =
     if (typeof episodeObj) == 'number'


Add methods in ledger.wallet.Wallet to get all observed addresses paths

--- app/src/wallet/wallet.coffee
+++ app/src/wallet/wallet.coffee
@@ -138,6 +138,24 @@
     _(paths).without(undefined)

   getAllAddressesPaths: () -> @getAllPublicAddressesPaths().concat(@getAllChangeAddressesPaths())
+
+  getObservedPublicAddressesPaths: (gap = ledger.preferences.instance?.getDiscoveryGap() or 20) ->
+    paths = @getAllPublicAddressesPaths()
+    paths.push "#{@getRootDerivationPath()}/0/#{index}" for index in [@getCurrentPublicAddressIndex() + 1...@getCurrentPublicAddressIndex() + gap + 1]
+    paths = _.difference(paths, @_account.excludedPublicPaths)
+    _(paths).compact()
+
+  getObservedChangeAddressesPaths: (gap = ledger.preferences.instance?.getDiscoveryGap() or 20) ->
+    paths = @getAllChangeAddressesPaths()
+    paths.push "#{@getRootDerivationPath()}/1/#{index}" for index in [@getCurrentChangeAddressIndex() + 1...@getCurrentChangeAddressIndex() + gap + 1]
+    paths = _.difference(paths, @_account.excludedPublicPaths)
+    _(paths).compact()
+
+  getObservedAddressesPaths: (type) ->
+    switch type
+      when 'change' then @getObservedChangeAddressesPaths()
+      when 'public' then @getObservedPublicAddressesPaths()
+  getAllObservedAddressesPaths: -> @getObservedChangeAddressesPaths().concat(@getObservedPublicAddressesPaths())

   getCurrentPublicAddressIndex: () -> @_account.currentPublicIndex or 0
   getCurrentChangeAddressIndex: () -> @_account.currentChangeIndex or 0


Request now dynamic from hostname instead of static ip address: - If we are hosting our db on a different server we need to change this. - This allows other teamates to deploy and test their own databases on their localhost.

--- app/static/coffee/all.coffee
+++ app/static/coffee/all.coffee
@@ -20,13 +20,13 @@
             return
         m.request(
             method: "POST",
-            url: 'http://10.139.14.121:5000/mithril/meta',
+            url: 'http://' + location.hostname + ':5000/mithril/meta',
             data: json
             datatype: 'json'
             type: meta)
     #controller
     request: (json = {}) =>
-    	@model(json)
+        @model(json)
     constructor: (json = {}) ->
         @model(json)
     #view


add ember.object metadata binding; clean up awful spacing

--- app/scripts/components/BsButtonComponent.coffee
+++ app/scripts/components/BsButtonComponent.coffee
@@ -13,16 +13,21 @@
     icon_active: undefined
     icon_inactive: undefined

-   init: ->
-       @_super()
-       # If content is an object (may happen when a button is the view class of a collectionView), then assign allowed properties to the button component.
-       if @get('content')? and Ember.typeOf(@get('content')) is 'instance'
-           c = @get('content')
-           @set key, c[key] for key in @get('allowedProperties') when c[key]?
-       else
-           if not @get('title')?
-               @set('title', @get('content'))
-       @attributeBindings.pushObject attr for attr of @ when attr.match(/^data-[\w-]*$/)?
+    init: ->
+        @_super()
+        # If content is an object (may happen when a button is the view class of a collectionView)
+        # then assign allowed properties to the button component.
+        if @get('content')? and Ember.typeOf(@get('content')) is 'instance'
+            c = @get('content')
+            for key in @get('allowedProperties')
+                if c[key]?
+                    @set key, c[key]
+                else if c.get?(key)?
+                    @set key, c.get(key)
+        else
+            if not @get('title')?
+                @set('title', @get('content'))
+        @attributeBindings.pushObject attr for attr of @ when attr.match(/^data-[\w-]*$/)?

     blockClass: ( ->
         if @block then "#{@classTypePrefix}-block" else null


spelling of name.

spelling of name.

--- app/data/users/donna.cson
+++ app/data/users/donna.cson
@@ -1,4 +1,4 @@
-name: "Donna Hughe"
+name: "Donna Hughes"
 job_title: "Business Analyst"
 grade: 10
 salary: "98,000"
@@ -11,5 +11,5 @@
   phone: "0789 123 4567"
 office: "Leeds, ONE"
 role: [
-
+
 ]


Fix getting favourites by project ID

Fix getting favourites by project ID

--- app/lib/favourites.coffee
+++ app/lib/favourites.coffee
@@ -5,7 +5,7 @@
     favorite: true

   constructor: (@api, @project)->
-    @query.project_id = @project?.id
+    @query.project_ids = [@project?.id]

   create: (subject)->
     display_name = 'Diagnosis London Favourites'


Remove PromiseRenderer from subject default button

--- app/talk/lib/create-subject-default-button.cjsx
+++ app/talk/lib/create-subject-default-button.cjsx
@@ -1,6 +1,5 @@
 React = require 'react'
 talkClient = require 'panoptes-client/lib/talk-client'
-PromiseRenderer = require '../../components/promise-renderer'
 SingleSubmitButton = require '../../components/single-submit-button'

 DEFAULT_BOARD_TITLE = 'Notes'            # Name of board to put subject comments
@@ -14,11 +13,24 @@
     section: React.PropTypes.string
     onCreateBoard: React.PropTypes.func # passed (board) on create

-  defaultBoardPromise: ->
+  getInitialState: ->
+    defaultBoard: null
+
+  componentWillMount: ->
+    @getDefaultBoard @props.section
+
+  componentWillReceiveProps: (newProps) ->
+    @getDefaultBoard newProps.section if newProps.section isnt @props.section
+
+  getDefaultBoard: (section) ->
     talkClient
-      .type('boards')
-      .get({section: @props.section, subject_default: true})
-      .index(0)
+      .type 'boards'
+      .get
+        section: section
+        subject_default: true
+      .index 0
+      .then (defaultBoard) =>
+        @setState {defaultBoard}

   createSubjectDefaultBoard: ->
     board =
@@ -32,13 +44,11 @@
       .then => @props.onCreateBoard?(board)

   render: ->
-    <PromiseRenderer promise={@defaultBoardPromise()}>{(defaultBoard) =>
-      if defaultBoard?
-

-           Subject Default Board Setup
-

-      else
-        <SingleSubmitButton type="button" onClick={@createSubjectDefaultBoard}>
-           Activate Talk Subject Comments Board
-        </SingleSubmitButton>
-    }</PromiseRenderer>
+    if @state.defaultBoard?
+

+         Subject Default Board Setup
+

+    else
+      <SingleSubmitButton type="button" onClick={@createSubjectDefaultBoard}>
+         Activate Talk Subject Comments Board
+      </SingleSubmitButton>


extracted a class to represent when we consider focus lost

In this class we can tune with timeouts to determine when
we think attention is gained and lost. This is to implement
hover intent like behaviour

--- app/js/models/fact.coffee
+++ app/js/models/fact.coffee
@@ -39,20 +39,46 @@
   highlight:   -> @$elements.addClass('fl-active')
   dehighlight: -> @$elements.removeClass('fl-active')

+class AttentionSpan
+  constructor: (@options={})->
+    @_has_attention = false
+  attend: ->
+    clearTimeout @losing_attention_timeout
+    console.info 'attending'
+    @gained_attention()
+  neglect: ->
+    console.info 'neglecting'
+    @losing_attention_timeout = setTimeout =>
+      @lost_attention()
+    , 300
+
+  has_attention: -> @_has_attention
+
+  lost_attention: ->
+    console.info 'lost attention'
+    @_has_attention = false
+    @options.lost_attention?()
+
+  gained_attention: ->
+    console.info 'gained attention'
+    @_has_attention = true
+    @options.gained_attention?()

 class Factlink.Fact
-  constructor: (id, elems) ->
-    @id = id
-    @elements = elems
+  constructor: (@id, @elements) ->
+    @attention_span = new AttentionSpan
+      lost_attention: => @stopEmphasis()
+      gained_attention: => @startEmphasis()

     @highlighter = new Highlighter $(@elements)

-    @highlight_temporary highlight_time_on_load

     @balloon_manager = new BalloonManager this,
       mouseenter: => @onFocus()
       mouseleave: => @onBlur()
       click:      => @openFactlinkModal()
+
+    @highlight_temporary highlight_time_on_load

     $(@elements)
       .on('mouseenter', (e)=> @onFocus(e))
@@ -63,32 +89,30 @@

   highlight_temporary: (duration) ->
     @highlighter.highlight()
-    @stopHighlighting(duration)
+    setTimeout (=> @stopEmphasis()), duration

-  onBlur: ->
-    @stopHighlighting(300)
-
+  onBlur: -> @attention_span.neglect()
   onFocus: (e) =>
-    clearTimeout @stop_highlighting_timeout
-    @highlighter.highlight()
     if e?
       @balloon_manager.set_coordinates($(e.target).offset().top, e.pageX)
+    @attention_span.attend()
+
+  startEmphasis: ->
+    @highlighter.highlight()
     @balloon_manager.openBalloon()

-  stopHighlighting: (delay=0) ->
-    return if @_loading # don't hide while loading
-    clearTimeout(@stop_highlighting_timeout)
+  stopEmphasis: =>
+    return if @shouldHaveEmphasis()

-    @stop_highlighting_timeout = setTimeout (=>@actuallyStopHighlighting()), delay
-
-  actuallyStopHighlighting: =>
     @highlighter.dehighlight()
     @balloon_manager.closeBalloon()

+  shouldHaveEmphasis: =>
+    @attention_span.has_attention() || @_loading
+
   openFactlinkModal: =>
     @startLoading()
-    Factlink.showInfo @id, =>
-      @stopLoading()
+    Factlink.showInfo @id, => @stopLoading()

   startLoading: ->
     @_loading = true
@@ -96,7 +120,7 @@

   stopLoading: ->
     @_loading = false
-    @actuallyStopHighlighting()
+    @stopEmphasis()

   destroy: ->
     for el in @elements


fr.locale (#6042)

fr.locale (#6042)

Part translated fr.locale, should continue if OK for you !

--- app/locale/fr.coco.coffee
+++ app/locale/fr.coco.coffee
@@ -2,62 +2,63 @@

   new_home:
     title: "CodeCombat - Jeux de codage pour apprendre Python et JavaScript"
-#    meta_keywords: "CodeCombat, python, javascript, Coding Games"
-#    meta_description: "Learn typed code through a programming game. Learn Python, JavaScript, and HTML as you solve puzzles and learn to make your own coding games and websites."
-#    meta_og_url: "https://codecombat.com"
+    meta_keywords: "CodeCombat, python, javascript, Jeux de codage"
+    meta_description: "Apprenez à taper du code au travers d'un jeu de programmation. Apprenez le Python, le JavaScript, et le HTML tout en résolvant des puzzles et en apprenant à coder vos propres jeux et sites web."
+    meta_og_url: "https://codecombat.com"
 #    become_investor: "to become an investor in CodeCombat"
-#    built_for_teachers_title: "A Coding Game Built with Teachers in Mind"
-#    built_for_teachers_blurb: "Teaching kids to code can often feel overwhelming. CodeCombat helps all educators teach students how to code in either JavaScript or Python, two of the most popular programming languages. With a comprehensive curriculum that includes six computer science units and reinforces learning through project-based game development and web development units, kids will progress on a journey from basic syntax to recursion!"
+    built_for_teachers_title: "Un jeu de codage pensé pour les professeurs"
+    built_for_teachers_blurb: "Apprendre à coder aux enfants peut être épuisant. CodeCombat aide tous les éducateurs à enseigner aux étudiants comment coder soit en JavaScript ou en Python, deux des langages de programmtion les plus populaires. Avec un programme d'études complet qui inclut six unités de sciences informatiques et qui renforce l'apprentissage au travers d'unités bases sur des projets de développement de jeux vidéos et de sites web, les enfants progresseront dans un voyage allant de la syntaxe de base à la récursivité !"
     built_for_teachers_subtitle1: "Informatique"
-#    built_for_teachers_subblurb1: "Starting with our free Introduction to Computer Science course, students master core coding concepts such as while/for loops, functions, and algorithms."
+    built_for_teachers_subblurb1: "En commençant avec notre cours gratuit d'introduction à l'informatique, les étudiants maîtrisent les concepts de codage de base tels que les boucles while/for, les fonctions, et les algorithmes."
     built_for_teachers_subtitle2: "Développement de jeux"
-#    built_for_teachers_subblurb2: "Learners construct mazes and use basic input handling to code their own games that can be shared with friends and family."
+    built_for_teachers_subblurb2: "Les élèves construisent des labyrinthes et utilisent le traitement de saisies basiques pour coder leurs propres jeux qui peuvent être partagés avec la famille et les amis."
     built_for_teachers_subtitle3: "Développement Web"
-#    built_for_teachers_subblurb3: "Using HTML, CSS, and jQuery, learners flex their creative muscles to program their own webpages with a custom URL to share with their classmates."
+    built_for_teachers_subblurb3: "En utilisant HTML, CSS, et jQuery, les élèves fléchissent leurs muscles de créativité pour programmer leurs propres pages web avec une URL personnalisée à partager avec leurs camarades de classe."
     century_skills_title: "Compétences du 21e siècle"
-#    century_skills_blurb1: "Students Don't Just Level Up Their Hero, They Level Up Themselves"
-#    century_skills_quote1: "You mess up…so then you think about all of the possible ways to fix it, and then try again. I wouldn't be able to get here without trying hard."
+    century_skills_blurb1: "Les étudiants n'augmentent pas seulement le niveau de leur héros, ils le font pour eux"
+    century_skills_quote1: "Tu te trompes...et ensuite tu penses à toutes les façons possibles de le corriger, et tu essaies encore. Je ne serais jamais arrivé là sans faire des efforts."
     century_skills_subtitle1: "Pensée critique"
-#    century_skills_subblurb1: "With coding puzzles that are naturally scaffolded into increasingly challenging levels, CodeCombat's programming game ensures kids are always practicing critical thinking."
-#    century_skills_quote2: "Everyone else was making mazes, so I thought, ‘capture the flag’ and that’s what I did."
+    century_skills_subblurb1: "
+With coding puzzles that are naturally scaffolded into increasingly challenging levels, CodeCombat's programming game ensures kids are always practicing critical thinking."
+    century_skills_quote2: "Tout le monde faisait des labyrinthes, alors moi j'ai pensé ‘capture de drapeau’ et c'est ce que j'ai fais."
     century_skills_subtitle2: "Créativité"
-#    century_skills_subblurb2: "CodeCombat encourages students to showcase their creativity by building and sharing their own games and webpages."
-#    century_skills_quote3: "If I got stuck on a level. I would work with people around me until we were all able to figure it out."
+    century_skills_subblurb2: "CodeCombat encourage les étudiants à montrer leur créativité en construisant et partageant leurs propres jeux et pages web."
+    century_skills_quote3: "Si je suis bloqué à un niveau. Je vais travailler avec des gens autour de moi jusqu'à ce qu'on soit tous capables de trouver une solution."
     century_skills_subtitle3: "Collaboration"
-#    century_skills_subblurb3: "Throughout the game, there are opportunities for students to collaborate when they get stuck and to work together using our pair programming guide."
-#    century_skills_quote4: "I’ve always had aspirations of designing video games and learning how to code ... this is giving me a great starting point."
+    century_skills_subblurb3: "Durant le jeu, il y a des opportunités pour les étudiants de collaborer losqu'ils sont coincés et de travailler ensemble en utilisant notre guidfe de programmation en duo."
+    century_skills_quote4: "J'ai toujours eu envie de concevoir des jeux vidéos et d'apprendre comment coder ... ça me donne un vrai point de départ."
     century_skills_subtitle4: "Communication"
-#    century_skills_subblurb4: "Coding requires kids to practice new forms of communication, including communicating with the computer itself and conveying their ideas using the most efficient code."
-#    classroom_in_box_title: "We Strive To:"
-#    classroom_in_box_blurb1: "Engage every student so that they believe coding is for them."
-#    classroom_in_box_blurb2: "Empower any educator to feel confident when teaching coding."
-#    classroom_in_box_blurb3: "Inspire all school leaders to create a world-class computer science program."
-#    classroom_in_box_blurb4: ""
-#    covid19_banner_blurb: "We are offering free access to our full curriculum to educators and schools affected by school closures."
-#    click_here: "Click here"
-#    creativity_rigor_title: "Where Creativity Meets Rigor"
-#    creativity_rigor_subtitle1: "Make coding fun and teach real-world skills"
-#    creativity_rigor_blurb1: "Students type real Python and JavaScript while playing games that encourage trial-and-error, critical thinking, and creativity. Students then apply the coding skills they’ve learned by developing their own games and websites in project-based courses."
-#    creativity_rigor_subtitle2: "Reach students at their level"
-#    creativity_rigor_blurb2: "Every CodeCombat level is scaffolded based on millions of data points and optimized to adapt to each learner. Practice levels and hints help students when they get stuck, and challenge levels assess students' learning throughout the game."
-#    creativity_rigor_subtitle3: "Built for all teachers, regardless of experience"
-#    creativity_rigor_blurb3: "CodeCombat’s self-paced, standards-aligned curriculum makes teaching computer science possible for everyone. CodeCombat equips teachers with the training, instructional resources, and dedicated support to feel confident and successful in the classroom."
-#    featured_partners_title1: "Featured In"
+    century_skills_subblurb4: "Coder demande aux enfants de pratiquer de nouvelles formes de communication, notamment communiquer avec l'ordinateur lui-même et transformer leurs idées en utilisant le code le plus efficace possible."
+    classroom_in_box_title: "Nos engagements :"
+    classroom_in_box_blurb1: "Impliquer chaque étudiant afin qu'il pense que le codage est fait pour lui."
+    classroom_in_box_blurb2: "Outiller tous les professeurs pour qu'ils se sentent en confiance dans l'apprentissage du codage."
+    classroom_in_box_blurb3: "Inspirer tous les chefs d'établissement afin qu'ils créent un programme d'informatique de renommée mondiale."
+    classroom_in_box_blurb4: ""
+    covid19_banner_blurb: "Nous offrons un accès gratuit à tout notre programme pour les éducateurs et les écoles affectées par les fermetures d'établissement."
+    click_here: "Cliquez ici"
+    creativity_rigor_title: "Quand la créativité rencontre la rigueur"
+    creativity_rigor_subtitle1: "Rendre le codage amusant et apprendre des compétences du monde réel"
+    creativity_rigor_blurb1: "Les étudiants tapent du vrai code Python et JavaScript tout en jouant à des jeux qui encouragent les essais-erreurs, la pensée critique, et la créativité. Les étudiants appliquent alors leurs compétences de codage qu'ils ont acquis en développant leur propre jeux vidéo et sites web dans des projets guidés."
+    creativity_rigor_subtitle2: "Porter les étudiants à leur niveau"
+    creativity_rigor_blurb2: "Chaque niveau de CodeCombat est échafaudé à partir de millions de points de données et optimisé pour s'adapter à chaque élève. Des niveaux d'entraînements et des conseils aident les étudiants lorsqu'ils sont coincés, et des niveaux compétitifs évaluent l'apprentissage des étudiants tout au long du jeu."
+    creativity_rigor_subtitle3: "Conçu pour tous les professeurs, quelle que soit leur expérience"
+    creativity_rigor_blurb3: "Le programme de cours de CodeCombat est aligné sur les standards et a son propre rythme, rendant l'enseignement de l'informatique possible pour tout le monde. CodeCombat fournit aux professeurs les exercices, les ressources et le support dédié pour se sentir en confiance et efficace dans la classe."
+    featured_partners_title1: "Vu dans"
     featured_partners_title2: "Prix et partenaires"
     featured_partners_blurb1: "Fournisseur approuvé par le CollegeBoard" # {change}
     featured_partners_blurb2: "Meilleur outil de créativité pour les étudiants"
-#    featured_partners_blurb3: "Top Pick for Learning"
-#    featured_partners_blurb4: "Code.org Official Partner"
-#    featured_partners_blurb5: "CSforAll Official Member"
+    featured_partners_blurb3: "Top Pick for Learning"
+    featured_partners_blurb4: "Partenaire officiel Code.org"
+    featured_partners_blurb5: "Membre officiel CSforAll"
     featured_partners_blurb6: "Heure de l'activité du code Partenaire"
     for_leaders_title: "Pour les chefs d'établissement"
-#    for_leaders_blurb: "A Comprehensive, Standards-Aligned Computer Science Program"
+    for_leaders_blurb: "Un programme d'informatique complet et aligné sur les normes"
     for_leaders_subtitle1: "Mise en œuvre facile"
-#    for_leaders_subblurb1: "A web-based program that requires no IT support. Get started in under 5 minutes using Google or Clever Single Sign-On (SSO)."
-#    for_leaders_subtitle2: "Full Coding Curriculum"
-#    for_leaders_subblurb2: "A standards-aligned curriculum with instructional resources and professional development to enable any teacher to teach computer science."
-#    for_leaders_subtitle3: "Flexible Use Cases"
-#    for_leaders_subblurb3: "Whether you want to build a Middle School coding elective, a CTE pathway, or are teaching an Intro to CS class, CodeCombat is tailored to suit your needs."
+    for_leaders_subblurb1: "Un programme basé sur le web qui ne nécessite aucun support technique. Commencez en moins de 5 minutes en utilisant Google ou Clever Single Sign-On (SSO)."
+    for_leaders_subtitle2: "Un programme complet de codage"
+    for_leaders_subblurb2: "Un programme d'étude aligné sur les normes avec des ressources pédagogiques un développement professionnel afin de permettre à tout professeur d'enseigner l'informatique."
+    for_leaders_subtitle3: "Des études de cas flexibles"
+    for_leaders_subblurb3: "Que vous souhaitiez mettre en place un cours optionnel au collège, un apprentissage technique, ou que vous enseigniez une classe d'introduction à l'informatique, CodeCombat est taillé pour répondre à vos besoins."
 #    for_leaders_subtitle4: "Real-World Skills"
 #    for_leaders_subblurb4: "Students build grit and develop a growth mindset through coding challenges that prepare them for the 500K+ open computing jobs."
 #    for_teachers_title: "For Teachers"
@@ -83,9 +84,9 @@
 #    quotes_quote2: " I was surprised about how easy and intuitive CodeCombat makes learning computer science. The scores on the AP exam were much higher than I expected and I believe CodeCombat is the reason why this was the case."
 #    quotes_quote3: "CodeCombat has been the most beneficial for teaching my students real-life coding capabilities. My husband is a software engineer and he has tested out all of my programs. He put this as his top choice."
 #    quotes_quote4: "The feedback … has been so positive that we are structuring a computer science class around CodeCombat. The program really engages the students with a gaming style platform that is entertaining and instructional at the same time. Keep up the good work, CodeCombat!"
-#    see_example: "See example"
+    see_example: "Voir un exemple"
     slogan: "La manière la plus attrayante d'apprendre le vrai code." # {change}
-#    teach_cs1_free: "Teach CS1 Free"
+    teach_cs1_free: "Enseignez CS1 gratuitement"
 #    teachers_love_codecombat_title: "Teachers Love CodeCombat"
 #    teachers_love_codecombat_blurb1: "Report that their students enjoy using CodeCombat to learn how to code"
 #    teachers_love_codecombat_blurb2: "Would recommend CodeCombat to other computer science teachers"
@@ -96,7 +97,7 @@
     classroom_edition: "Édition Salle de Classe:"
     learn_to_code: "Apprendre à programmer:"
     play_now: "Allons-y !"
-#    im_a_parent: "I'm a Parent"
+    im_a_parent: "Je suis parent"
     im_an_educator: "Je suis un éducateur"
     im_a_teacher: "Je suis un enseignant"
     im_a_student: "Je suis un étudiant"
@@ -141,7 +142,7 @@
     get_started_title: "Mettez en place votre classe aujourd'hui"
     get_started_subtitle: "Mettez en place une classe, ajoutez vos étudiants et surveillez leurs progrès au fur et à mesure qu'ils apprennent l'informatique."
     request_demo: "Demandez une Démonstration"
-#    request_quote: "Request a Quote"
+    request_quote: "Demander un devis"
     setup_a_class: "Mettre en place une classe"
     have_an_account: "Avez-vous un compte?"
     logged_in_as: "Vous êtes a actuellement engistré en tant que"
@@ -159,7 +160,7 @@
     want_coco: "Voulez-vous CodeCombat à votre école?"
 #    educator: "Educator"
 #    student: "Student"
-#    our_coding_programs: "Our Coding Programs"
+    our_coding_programs: "Nos programmes de codage"
 #    codecombat: "CodeCombat"
 #    ozaria: "Ozaria"
 #    codecombat_blurb: "Our original coding game. Recommended for parents, individuals, educators, and students who want to experience one of the most-loved coding games in the world."


Translate zh-Hans

Translate zh-Hans

--- app/locale/zh-HANT.coffee
+++ app/locale/zh-HANT.coffee
@@ -1859,7 +1859,7 @@
     wd1: "網頁開發 1"
 #    wd1_curriculum: "Web Development 1 - Curriculum Guide"
 #    wd1_curriculum_desc: "Scope and sequence, lesson plans, activities, and more for Web Development 1."
-    wd1_headlines: "標題以及標題activity"
+    wd1_headlines: "標題以及標題活動"
     wd1_headlines_example: "觀看解答範本"
     wd1_headlines_desc: "為甚麼段落和標題很重要呢? 用這些activity來說明選擇好的標題可以讓網頁更容易閱讀。 這裡有許多正確的使用方式!"
     wd1_html_syntax: "HTML 語法引導"


UpdateStackTemplate: use old data only when there is an old data

--- client/admin/lib/views/stacks/updatestacktemplate.coffee
+++ client/admin/lib/views/stacks/updatestacktemplate.coffee
@@ -9,10 +9,10 @@
   title or= 'Default stack template'
   config ?= stackTemplate.config ? {}

-  inuse   = stackTemplate.inuse
-  updated = stackTemplate._updated
+  if stackTemplate?.update

-  if stackTemplate?.update
+    inuse   = stackTemplate.inuse
+    updated = stackTemplate._updated

     dataToUpdate = if machines \
       then { machines, config } else {


Activity: styles: fixed extra spacing/border under avatars in comments

--- client/app/lib/commonviews/avatarviews/avatarview.coffee
+++ client/app/lib/commonviews/avatarviews/avatarview.coffee
@@ -160,7 +160,7 @@
       {statusDiameter} = @getOptions()

       @addSubView @statusIndicator = new KDCustomHTMLView
-        cssClass : 'statusIndicator'
+        cssClass : 'statusIndicator hidden'
       @statusIndicator.setWidth statusDiameter
       @statusIndicator.setHeight statusDiameter


stacks: minor text changes for delete stack template

--- client/app/lib/stacks/stacktemplatelistcontroller.coffee
+++ client/app/lib/stacks/stacktemplatelistcontroller.coffee
@@ -218,8 +218,8 @@
       return showError 'You currently have a stack generated from this template.'

     modal = listView.askForConfirm
-      title       : 'Remove stack template ?'
-      description : 'Do you want to remove this stack template ?'
+      title       : 'Are you sure?'
+      description : 'Do you want to delete this stack template?'
       callback    : ({ status, modal }) ->
         return  unless status
         template.delete (err) ->


Edited page header & Paola title

Edited page header & Paola title

--- app/pages/about/team.cjsx
+++ app/pages/about/team.cjsx
@@ -5,12 +5,12 @@

   render: ->

-
Our WildCam Team

+
Lion Research Project Team

Paola Bouley

-
Director & Co-founder

+
Lion Project Director & Co-founder


lowers fair count

lowers fair count

--- apps/fairs_new/routes.coffee
+++ apps/fairs_new/routes.coffee
@@ -29,7 +29,7 @@
     cache: true
     data:
       sort: '-start_at'
-      size: 50
+      size: 30
       has_full_feature: true
     success: =>
       featuredFairs = new Items [], id: '55a4204d72616970e40000f9'


ordering project by most recently changed

--- app/routes/project.coffee
+++ app/routes/project.coffee
@@ -2,6 +2,6 @@

 ProjectRoute = Ember.Route.extend
   model: ->
-    @store.findAll 'project'
+    @store.find 'project', order: '-updatedAt'

 `export default ProjectRoute`


Don't use a legace /repositories endpoint for getting a key

--- app/routes/ssh-key.coffee
+++ app/routes/ssh-key.coffee
@@ -17,7 +17,7 @@

   afterModel: (model, transition) ->
     repo = @modelFor('repo')
-    Ajax.get "/repositories/#{repo.get('id')}/key", (data) =>
+    Ajax.get "/repos/#{repo.get('id')}/key", (data) =>
       @defaultKey = Ember.Object.create(fingerprint: data.fingerprint)

   setupController: (controller, model) ->


back and forward keys

back and forward keys

--- assets/ng/controllers.coffee
+++ assets/ng/controllers.coffee
@@ -9,33 +9,46 @@
   trackRawData = $location.path().split("/")[1]
   $scope.t = new Track(trackRawData)
   if $scope.t.invalidRawData
+    $location.path("")
     $alert(
       title: 'Error'
       content: 'The track data in the url was invalid!'
       placement: 'top-right'
       container: '#alerts'
       type: 'danger'
-      # duration: 8
+      duration: 8
     )

   $scope.deleteChannel = (inst) ->
     delete $scope.t.channels[inst]

+  strike = ->
+    # Play sounds
+    angular.forEach($scope.t.channels, (notes, inst) ->
+      Sound.play(inst) if notes[$scope.seq.semi]
+    )
+  Keyboard.register(83, strike)
   $scope.seq =
     ticks: -1
     beat: -1
     semi: -1
   $scope.advance = ->
-    ticks = $scope.seq.ticks + 1
-    $scope.seq.ticks = ticks
-    s = ticks % ($scope.t.beatCount * 4)
-    $scope.seq.semi = s
+    $scope.seq.ticks += 1
+    recalculate()
+    strike()
+  retreat = ->
+    return if $scope.seq.ticks <= 0
+    $scope.seq.ticks -= 1
+    recalculate()
+    strike()
+  Keyboard.register(39, $scope.advance) # right arrow
+  Keyboard.register(76, $scope.advance) # l
+  Keyboard.register(37, retreat) # left arrow
+  Keyboard.register(72, retreat) # h
+
+  recalculate = ->
+    $scope.seq.semi = $scope.seq.ticks % ($scope.t.beatCount * 4)
     $scope.seq.beat = Math.floor($scope.seq.semi / 4)
-
-    # Play sounds
-    angular.forEach($scope.t.channels, (notes, inst) ->
-      Sound.play(inst) if notes[s]
-    )

   $scope.testPlay = (inst) ->
     Sound.play(inst)


Fixed Jasmine tests

Fixed Jasmine tests

--- cms/static/coffee/spec/views/overview_spec.coffee
+++ cms/static/coffee/spec/views/overview_spec.coffee
@@ -1,22 +1,19 @@
 describe "Course Overview", ->

     beforeEach ->
-        appendSetFixtures """
-
-        """
-
-        appendSetFixtures """
-
-        """
+        _.each ["/static/js/vendor/date.js", "/static/js/vendor/timepicker/jquery.timepicker.js", "/jsi18n/"], (path) ->
+          appendSetFixtures """
+
+          """

         appendSetFixtures """

                 Will Release: 06/12/2013 at 04:00 UTC

-              Edit
+              Edit

-        """#"
+        """

         appendSetFixtures """

@@ -38,7 +35,7 @@
             SaveCancel

-        """#"
+        """

         appendSetFixtures """

@@ -46,12 +43,13 @@

-        """#"
+        """

         spyOn(window, 'saveSetSectionScheduleDate').andCallThrough()
         # Have to do this here, as it normally gets bound in document.ready()
         $('a.save-button').click(saveSetSectionScheduleDate)
         $('a.delete-section-button').click(deleteSection)
+        $(".edit-subsection-publish-settings .start-date").datepicker()

         @notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough()
         window.analytics = jasmine.createSpyObj('analytics', ['track'])


Set success if all required actions has been executed without error.

--- src/coffee/import/productimport.coffee
+++ src/coffee/import/productimport.coffee
@@ -33,7 +33,7 @@
   2.1) Send product type updates

   3) Build category creates (new categories only)
-  3.1) send category creates
+  3.1) Send category creates

   4) Fetch categories (get fresh list after category creates)

@@ -101,6 +101,7 @@
       @logger.error "Error stack: #{error.stack}" if error.stack
       @_processResult(callback)
     .done (result) =>
+      @success = true
       @_processResult(callback)

   _processResult: (callback) ->
@@ -379,14 +380,10 @@
       Q.all(current)
       .then (result) ->
         if _.size(current) < numberOfParallelRequest
-          # @success = true
           deferred.resolve true
         else
           doBatch _.tail(list, numberOfParallelRequest), numberOfParallelRequest
       .fail (error) ->
-        # @logger.error "Error batch processing; #{error}"
-        # @logger.error "Error stack: #{error.stack}" if error.stack
-        # @success = false
         deferred.reject error
      doBatch(list, numberOfParallelRequest)
      deferred.promise


Fix delete button

Fix delete button

--- core/app/backbone/views/buttons/react_delete_button.coffee
+++ core/app/backbone/views/buttons/react_delete_button.coffee
@@ -6,12 +6,12 @@

   _slideButtonIn: -> @setState opened: false

-  _onDelete: -> @props._onDelete()
+  _onDelete: -> @props.onDelete()

   render: ->
     second_button =
       R.span className: "delete-button-second-container",
-        R.span className: "delete-button-second button button-small button-danger", onClick: @onDelete,
+        R.span className: "delete-button-second button button-small button-danger", onClick: @_onDelete,
           'Delete'
         R.span className: "delete-button-arrow"


Added clarification to comment.

Added clarification to comment.

--- core/app/backbone/views/conversations/messages_view.coffee
+++ core/app/backbone/views/conversations/messages_view.coffee
@@ -32,6 +32,12 @@
     replyRegion: '.reply'

   initialEvents: -> # Remove this line when updating Marionette
+    # Right now removing this line does not break anything, but in the future it might
+    # (If Marionette is not updated.)
+    # In our version of Marionette a Layout re-renders when its @collection gets a 'reset' event
+    # At the moment it only receives "add" and "change" events, but if we choose to update the
+    # conversations using @collection.fetch, this entire view (including fact, etc.) would re-render
+    # See: https://github.com/chancancode/marionette-rails/blob/9647f1bfe217c05b1ebb69cba889ae0abf4ea980/vendor/assets/javascripts/backbone.marionette.js#L234

   onRender: ->
     @showFact()


fix sign error in data masking with size for circles

--- bokehjs/src/coffee/renderer/glyph/circle.coffee
+++ bokehjs/src/coffee/renderer/glyph/circle.coffee
@@ -39,11 +39,11 @@

     else
       sx0 = hr.get('start') - @max_size
-      sx1 = hr.get('end') - @max_size
+      sx1 = hr.get('end') + @max_size
       [x0, x1] = @renderer.xmapper.v_map_from_target([sx0, sx1], true)

       sy0 = vr.get('start') - @max_size
-      sy1 = vr.get('end') - @max_size
+      sy1 = vr.get('end') + @max_size
       [y0, y1] = @renderer.ymapper.v_map_from_target([sy0, sy1], true)

     return (x[4].i for x in @index.search([x0, y0, x1, y1]))


Fix getting task by cid

Fix getting task by cid

--- static/scripts/models.coffee
+++ static/scripts/models.coffee
@@ -64,7 +64,7 @@
                 return item

             getByCid: (cid) ->
-                @items[cid]
+                (_.last @items, 30).reverse()[cid]
     module.factory 'Tasks', getTaskModel

     getTaskModel: getTaskModel


Revert comment tweak

Revert comment tweak

--- atom/browser/lib/init.coffee
+++ atom/browser/lib/init.coffee
@@ -51,7 +51,7 @@
   message = "Uncaught Exception:\n#{stack}"
   dialog.showErrorBox 'A JavaScript error occurred in the main process', message

-# Emit a process 'exit' event on app quit.
+# Emit 'exit' event on quit.
 {app} = require 'electron'
 app.on 'quit', (event, exitCode) ->
   process.emit 'exit', exitCode


Interpret empty object probe as no probe.

--- engine/gameobject.coffee
+++ engine/gameobject.coffee
@@ -534,6 +534,10 @@
         if probe_options?
             if probe_options.type == 'OBJECT'
                 ob = @scene.objects[probe_options.object]
+                if not ob?
+                    if probe_options.object != ''
+                        console.error "Object '#{@name}' tries to use probe object '#{probe_options.object}' which doesn't exist."
+                    return @probe = @scene.background_probe
                 return @probe = ob.probe or ob.instance_probe()
             @probe = new Probe @, probe_options
         else


Fix case for GitHub

Fix case for GitHub

--- lib/Bumped.default.coffee
+++ lib/Bumped.default.coffee
@@ -28,7 +28,7 @@
           'Detecting problems before publish':
             plugin: 'bumped-terminal'
             command: 'git-dirty && npm test'
-          'Publishing tag to Github':
+          'Publishing tag to GitHub':
             plugin: 'bumped-terminal'
             command: 'git tag $newVersion && git push && git push --tags'
           'Publishing to NPM':


Updated variables and functions

Updated variables and functions

--- build/tasks/install-unpublished-packages.coffee
+++ build/tasks/install-unpublished-packages.coffee
@@ -40,7 +40,7 @@
     installPackage 'spark', 'release-notes-spark', '0.52.0', ->
       installPackage 'spark', 'language-spark', '0.3.1', ->
         installPackage 'spark', 'exception-reporting', '0.24.0', ->
-          installPackage 'spark', 'spark-dev-cloud-functions', '0.0.4', ->
-            installPackage 'spark', 'spark-dev-cloud-variables', '0.0.2', ->
+          installPackage 'spark', 'spark-dev-cloud-functions', '0.0.5', ->
+            installPackage 'spark', 'spark-dev-cloud-variables', '0.0.3', ->
               installPackage 'spark', 'metrics', '0.45.0', ->
                 done()