Update Atom settings.

Update Atom settings.

--- Atom/config.cson
+++ Atom/config.cson
@@ -16,7 +16,8 @@
     userId: "898a6da8-1036-4c89-81f7-06228adf326d"
   kite:
     showWelcomeNotificationOnStartup: false
-  "markdown-preview": {}
+  "markdown-preview":
+    useGitHubStyle: true
   "python-yapf":
     yapfPath: "/usr/local/bin/yapf"
     yapfStyle: "google"


Fix GlyphView.hit_test() after recent changes to loggers

--- bokehjs/src/coffee/renderer/glyph/glyph.coffee
+++ bokehjs/src/coffee/renderer/glyph/glyph.coffee
@@ -300,6 +300,8 @@
         ctx.stroke()

     hit_test: (geometry) ->
+      result = null
+
       if geometry.type == "point"
         if @_hit_point?
           result = @_hit_point(geometry)
@@ -307,8 +309,6 @@
           type = @mget('glyphspec').type
           logger.warn("'point' selection not available on #{type} renderer")
           @_point_hit_warned = true
-        return null
-
       else if geometry.type == "rect"
         if @_hit_rect?
           result = @_hit_rect(geometry)
@@ -316,11 +316,8 @@
           type = @mget('glyphspec').type
           logger.warn("'rect' selection not available on #{type} renderer")
           @_rect_hit_warned = true
-        return null
-
       else
         logger.error("unrecognized selection geometry type '#{ geometry.type }'")
-        return null

       return result


ace shows full paths on tab handles

ace shows full paths on tab handles

--- components/tabs/KDTabHandleView.coffee
+++ components/tabs/KDTabHandleView.coffee
@@ -48,6 +48,7 @@
     @getDomElement().removeClass "active"

   setTitle:(title)->
+    @setDomAttributes {title}
     # @getDomElement().find("span.close-tab").css "color", @getDelegate().getDomElement().css "background-color"

   isHidden: ->


Modes; simplify focusInput.

Modes; simplify focusInput.

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -359,15 +359,6 @@

     selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1)

-    # If PostFindMode is active, then the target element may already have the focus, in which case, .focus()
-    # will not generate a "focus" event to trigger insert mode.  So we handle insert mode manually, here.
-    Mode.runIn InsertModeBlocker, ->
-      visibleInputs[selectedInputIndex].element.focus()
-
-    if visibleInputs.length == 1
-      new InsertMode visibleInputs[selectedInputIndex].element
-      return
-
     hints = for tuple in visibleInputs
       hint = document.createElement("div")
       hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
@@ -400,8 +391,15 @@
               false
             else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
               @exit()
-              DomUtils.removeElement hintContainingDiv
-              new InsertMode visibleInputs[selectedInputIndex].element
+
+        visibleInputs[selectedInputIndex].element.focus()
+        @exit() if visibleInputs.length == 1
+
+      exit: ->
+        DomUtils.removeElement hintContainingDiv
+        new InsertMode visibleInputs[selectedInputIndex].element
+        super()
+        @continueBubbling

 # Decide whether this keyChar should be passed to the underlying page.
 # Keystrokes are *never* considered passKeys if the keyQueue is not empty.  So, for example, if 't' is a


Removed whitespace

Removed whitespace

--- core/app/backbone/modules/modal.coffee
+++ core/app/backbone/modules/modal.coffee
@@ -10,7 +10,6 @@

-

       """


Add preventScrollPropagation to .modal-body

Prevents scrolling of the background page when modal content is larger than the modal-body. The content has grown with suggested channels and channel search.

--- core/app/backbone/modules/modal.coffee
+++ core/app/backbone/modules/modal.coffee
@@ -28,6 +28,7 @@
       @modalRegion.show @options.content_view
       @$('.modal').show()
       @$('.transparent-layer').show()
+      @$('.modal-body').preventScrollPropagation()

     stopPropagation: (e) ->
       e.stopPropagation()


Minor reordering

Minor reordering

--- library/membrane/preflight.coffee
+++ library/membrane/preflight.coffee
@@ -38,8 +38,8 @@
 http = require "http"
 util = require "util"

+{Marshal} = require "./marshal"
 {BowerSupport} = require "./bower"
-{Marshal} = require "./marshal"

 # This abstract base class service is an extension of the Screenplay
 # family that does some further environment initialization and set


handle rejected promises in pdf viewer

handle rejected promises in pdf viewer

--- services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
+++ services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
@@ -59,7 +59,8 @@
 					# console.log 'resolved q.all, page size is', result
 					$scope.numPages = result.numPages
 				.catch (error) ->
-					$scope.$emit 'pdf:error', 'loading initial document parameters'
+					$scope.$emit 'pdf:error', error
+					return $q.reject(error)

 		@setScale = (scale, containerHeight, containerWidth) ->
 			$scope.loaded.then () ->
@@ -87,6 +88,9 @@
 					numScale * $scope.pdfPageSize[1]
 				]
 				# console.log 'in setScale result', $scope.scale.scale, $scope.defaultPageSize
+			.catch (error) ->
+				$scope.$emit 'pdf:error', error
+				return $q.reject(error)

 		@redraw = (position) ->
 			# console.log 'in redraw'
@@ -302,6 +306,10 @@
 									spinner.remove(element)
 								ctrl.redraw(origposition)
 								$timeout renderVisiblePages
+								scope.loadSuccess = true
+						.catch (error) ->
+							scope.$emit 'pdf:error', error
+							return $q.reject(error)

 				elementTimer = null
 				spinnerTimer = null
@@ -364,9 +372,13 @@
 					if scope.loadCount > 3 || error.match(/^Missing PDF/i) || error.match(/^loading/i)
 						scope.$emit 'pdf:error:display'
 						return
-					ctrl.load()
-					# trigger a redraw
-					scope.scale = angular.copy (scope.scale)
+					if scope.loadSuccess
+						ctrl.load()
+						# trigger a redraw
+						scope.scale = angular.copy (scope.scale)
+					else
+						scope.$emit 'pdf:error:display'
+						return

 				scope.$on 'pdf:page:size-change', (event, pageNum, delta) ->
 					#console.log 'page size change event', pageNum, delta
@@ -400,6 +412,7 @@
 					# console.log 'loading pdf', newVal, oldVal
 					return unless newVal?
 					scope.loadCount = 0; # new pdf, so reset load count
+					scope.loadSuccess = false
 					ctrl.load()
 					# trigger a redraw
 					scope.scale = angular.copy (scope.scale)


Added contextualizeBefore and contextualizeAfter events access for plugins

--- src/lib/docpad.coffee
+++ src/lib/docpad.coffee
@@ -117,6 +117,8 @@
 		'generateAfter'
 		'parseBefore'
 		'parseAfter'
+		'contextualizeBefore'
+		'contextualizeAfter'
 		'renderBefore'
 		'render'
 		'renderDocument'


protocol: Make edge data work for project mode

--- src/protocol.coffee
+++ src/protocol.coffee
@@ -44,6 +44,7 @@
 handleMessage = (proto, sub, cmd, payload, ctx) ->
   debug 'RECV:', sub, cmd, payload

+  # FIXME: should be 'main' or whatever came from `graph:clear`. However, Flowhub doesn't work with that??
   defaultGraph = 'default/main'

   if sub == 'runtime' and cmd == 'getruntime'
@@ -224,7 +225,7 @@
       id = "#{from}() #{fromPort.toUpperCase()} -> #{toPort.toUpperCase()} #{to}()"
       msg =
         id: id # FIXME: https://github.com/noflo/noflo-ui/issues/293
-        graph: 'default/main' # FIXME: unhardcode
+        graph: 'main' # FIXME: unhardcode
         src:
           node: from
           port: fromPort


fejl 40

fejl 40

--- src/scripts/directives/artist-view/artist-view.coffee
+++ src/scripts/directives/artist-view/artist-view.coffee
@@ -6,7 +6,7 @@
       render = (data) ->
         svg = d3.select ".song-chart svg"

-        svg.select(".dislike").remove()
+        svg.select(".empty").remove()

         votes = [
           {


Chain hypermedia calls.

Chain hypermedia calls.

--- src/replacer.coffee
+++ src/replacer.coffee
@@ -61,7 +61,7 @@
   # Convert things that end in `_url` to methods which return a Promise
   _replaceKeyValue: (acc, key, value) ->
     if /_url$/.test(key)
-      fn = (cb, args...) =>
+      fn = (args...) =>
         # url can contain {name} or {/name} in the URL.
         # for every arg passed in, replace {...} with that arg
         # and remove the rest (they may or may not be optional)
@@ -90,15 +90,12 @@
           url = url.replace(match, param)
           i++

-        if /upload_url$/.test(key)
-          # POST https://<upload_url>/repos/:owner/:repo/releases/:id/assets?name=foo.zip
-          # Pull off the last 2 args to .upload()
-          [contentType, data]     = args[-2..]
-          @_request('POST', url, data, {contentType, raw:true}, cb)
-        else
-          @_request('GET', url, null, null, cb) # TODO: Heuristically set the isBoolean flag
+        key = key.replace(/_url$/, '')
+        context = TREE_OPTIONS
+        for k in key.split('.')
+          context = context[k]
+        Chainer(@_request, url, k, context, acc)

-      fn = toPromise(fn)
       fn.url = value
       newKey = key.substring(0, key.length-'_url'.length)
       acc[plus.camelize(newKey)] = fn


Reorder topic and reply functions

Reorder topic and reply functions

--- src/response.coffee
+++ src/response.coffee
@@ -17,6 +17,15 @@
   send: (strings...) ->
     @robot.adapter.send @message.user, strings...

+  # Public: Posts a message mentioning the current user.
+  #
+  # strings - One or more strings to be posted. The order of these strings
+  #           should be kept intact.
+  #
+  # Returns nothing.
+  reply: (strings...) ->
+    @robot.adapter.reply @message.user, strings...
+
   # Public: Posts a topic changing message
   #
   # strings - One or more strings to set as the topic of the
@@ -25,15 +34,6 @@
   # Returns nothing.
   topic: (strings...) ->
     @robot.adapter.topic @message.user, strings...
-
-  # Public: Posts a message mentioning the current user.
-  #
-  # strings - One or more strings to be posted. The order of these strings
-  #           should be kept intact.
-  #
-  # Returns nothing.
-  reply: (strings...) ->
-    @robot.adapter.reply @message.user, strings...

   # Public: Picks a random item from the given items.
   #


Gruntfile: Change gh-pages directory.

Gruntfile: Change gh-pages directory.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -167,9 +167,9 @@
           configure      : 'jsdoc.conf.json'

     'gh-pages'           :
-      src                : 'docs/*'
-      options            :
-        base             : 'build'
+      src                : '*'
+      options            :
+        base             : 'build/docs'
         message          : 'Update docs.'
         repo             : 'https://' + process.env.GH_TOKEN + '@github.com/fatihacet/spark.git'
         silent           : yes


Remove old runtimes from exposed modules

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -11,8 +11,7 @@
         options:
           exposed_modules:
             'noflo': 'noflo'
-            'noflo-runtime-iframe': 'noflo-runtime-iframe'
-            'noflo-runtime-webrtc': 'noflo-runtime-webrtc'
+            'noflo-runtime-postmessage': 'noflo-runtime-postmessage'
             'fbp-protocol-client': 'fbp-protocol-client'
             'noflo-runtime': 'noflo-runtime'
           manifest:


Make Grunt less verbose

Make Grunt less verbose

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -117,25 +117,6 @@
       dev: {}
     }

-
-
-  grunt.event.on "watch", (action, filepath, target) ->
-    grunt.log.writeln target + ": " + filepath + " has " + action
-
-  delayTime = 2000
-
-  grunt.registerTask "delayed-open", "wait for game to load.", ->
-    done = @async()
-    setTimeout ->
-      do grunt.task.run('open')
-    , delayTime
-
-  # grunt.registerTask "delayed-server-open", "wait for game to load.", ->
-  #   done = @async()
-  #   setTimeout ->
-  #     do grunt.task.run(['open:server'])
-  #   , delayTime
-
   grunt.loadNpmTasks "grunt-contrib-clean"
   grunt.loadNpmTasks "grunt-contrib-concat"
   grunt.loadNpmTasks "grunt-contrib-copy"
@@ -148,9 +129,6 @@
   grunt.loadNpmTasks "grunt-nodemon"
   grunt.loadNpmTasks "grunt-concurrent"

-  # grunt.registerTask "server", ["jshint:server", "copy:server", "delayed-server-open", "notify:server"]
-  # grunt.registerTask "client", ["jshint:client", "copy:client", "concat", "stylus", "delayed-client-open", "notify:client"]
-
   grunt.registerTask "server", ["jshint:server", "copy:server"]
   grunt.registerTask "client", ["jshint:client", "copy:client", "concat", "stylus"]
   grunt.registerTask "client-prod", ["client", "uglify"]


Increase allowable cyclomatic complexity to 13

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -88,7 +88,7 @@
           value: 120
         cyclomatic_complexity:
           level: 'error'
-          value: 10
+          value: 13

       source: ['src/**/*.coffee']
       grunt: 'Gruntfile.coffee'


Lint specs

Lint specs

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -17,7 +17,8 @@
         max_line_length:
           level: 'ignore'

-      src: ['src/*.coffee']
+      src: ['src/**/*.coffee']
+      spec: ['spec/**/*.coffee']

     shell:
       test:


Run build before executing tests

Run build before executing tests

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -22,5 +22,5 @@
 	grunt.loadNpmTasks 'grunt-mocha-cli'

 	grunt.registerTask 'build', ['clean:package', 'coffee']
-	grunt.registerTask 'test', ['mochacli']
+	grunt.registerTask 'test', ['build', 'mochacli']
 	grunt.registerTask 'default', []


feat(grunt): auto-tag, -commit, and -push on bump

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -31,13 +31,13 @@
     bump:
       options:
         files: ['package.json', 'bower.json']
-        commit: false
+        commit: true
         commitMessage: 'chore(release): v%VERSION%'
-        commitFiles: ['package.json', 'client/bower.json']
-        createTag: false
+        commitFiles: ['package.json']
+        createTag: true
         tagName: 'v%VERSION%'
         tagMessage: 'Version %VERSION%'
-        push: false
+        push: true
         pushTo: 'origin'

     # Documentation


Let new saves interrupt scripts watch

Let new saves interrupt scripts watch

It's slow

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -91,6 +91,8 @@
       scripts:
         files: ['js/**/*.coffee', 'js/vendor/*']
         tasks: ['coffee', 'concat', 'uglify', 'replace']
+        options:
+          interrupt: true

       templates:
         files: 'js/**/*.handlebars'


Creates gen-version task

Creates gen-version task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -6,7 +6,7 @@
 module.exports = (grunt) ->
 	# Project configuration.
 	grunt.initConfig
-		resourceToken: '$RESOURCE_URL$'
+		resourceToken: process.env['RESOURCE_TOKEN'] or 'http://vtex.io'
 		relativePath: ''
 		pkg: grunt.file.readJSON('package.json')
 		clean: ['build', 'deploy']
@@ -29,7 +29,7 @@

 			env:
 				expand: true
-				cwd: 'deploy/<%= meta.commit %>/'
+				cwd: 'build/<%= relativePath %>'
 				src: ['index.html', 'index.debug.html']
 				dest: 'deploy/<%= meta.env %>/'

@@ -101,19 +101,8 @@
 				tasks: ['test']
 				spawn: true

-	grunt.registerTask 'set-env', ->
-		env = this.args[0]
-		console.log 'Deploying to environment:', env
-		grunt.config 'meta.env', env
-
-	grunt.registerTask 'token', ->
-		if process.env.RESOURCE_TOKEN
-			console.log 'Resource Token set by environment variable.'
-			grunt.config 'resourceToken', process.env.RESOURCE_TOKEN
-		console.log 'Resource Token:', grunt.config('resourceToken')
-
 	# Looks for the commit hash in a GIT_COMMIT env var, or tries calling git.
-	grunt.registerTask 'deploy-version', ->
+	grunt.registerTask 'set-version', ->
 		if process.env.GIT_COMMIT
 			grunt.config 'meta.commit', new String(process.env.GIT_COMMIT)
 			grunt.log.writeln 'Version set by environment variable GIT_COMMIT to: ' + grunt.config('meta.commit')
@@ -156,11 +145,18 @@
 	grunt.registerTask 'test', ['dev', 'jasmine']
 	grunt.registerTask 'test-watch', ['test', 'regarde:test']

+	# Generates version folder
+	grunt.registerTask 'gen-version', ->
+		env = this.args[0] or 'dev'
+		console.log 'Deploying to environment:', env
+		grunt.config 'meta.env', env
+		grunt.task.run ['set-version', 'copy:env', 'string-replace:deploy']
+
 	# Deploy - creates deploy folder structure
 	grunt.registerTask 'deploy', ->
 		env = this.args[0] or 'dev'
-		grunt.task.run ['prod', 'jasmine', 'set-env:' + env, 'deploy-version', 'copy:deploy', 'copy:env', 'token', 'string-replace:deploy']
-
+		grunt.task.run ['prod', 'jasmine', 'gen-version:' + env, 'copy:deploy']
+
 	# Example usage of deploy task
 	grunt.registerTask 'deploy-example', ['deploy:master']


feat(grunt): it's safe to remove empty elements

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -180,7 +180,7 @@
           useShortDoctype: false
           removeEmptyAttributes: true
           removeOptionalTags: true
-          removeEmptyElements: false
+          removeEmptyElements: true
           lint: false
           keepClosingSlash: false
           caseSensitive: true


- remove powerbuild grunt task.

- remove powerbuild grunt task.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -39,15 +39,6 @@
           check: ['src/**/*.coffee', 'test/**/*.js', 'test/**/*.coffee']
         src: ['test/**/*.coffee']

-    powerbuild:
-      options:
-        sourceMap: true
-        node: false
-        handlers:
-          '.coffee': (src, canonicalName) ->
-            {js, v3SourceMap} = coffee.compile src, sourceMap: true, bare: true
-            return {code: js, map: v3SourceMap}
-
     watch:
       options:
         nospawn: true
@@ -72,7 +63,6 @@
   grunt.loadNpmTasks('grunt-contrib-coffee')
   grunt.loadNpmTasks('grunt-contrib-clean')
   grunt.loadNpmTasks('grunt-newer')
-  grunt.loadNpmTasks('powerbuild')

   grunt.registerTask('build', ['newer:coffee', 'newer:copy'])
   grunt.registerTask('rebuild', ['clean', 'build'])


Compile all LESS files

Compile all LESS files

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -70,8 +70,13 @@

     less:
       app:
-        files:
-          "public/css/bootstrap.css": "client/style/bootstrap.less"
+        files: [{
+          expand: true,
+          cwd: 'client/style'
+          src: ['*.less']
+          dest: 'public/css/'
+          ext: '.css'
+        }]

     modernizr:
       app:


fixed grunt watch task

fixed grunt watch task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -70,10 +70,10 @@
       watch:
          app_scripts:
             files: ['app/**/*.es6']
-            tasks: ['clean:app_build', 'babel:app', 'copy:app_build']
+            tasks: ['build_app']
          client_scripts:
             files: ['client/assets/javascript/**/*.es6']
-            tasks: ['clean:client_build', 'babel:client', 'webpack:dev', "copy:client_public"]
+            tasks: ['build_client']

       copy:
          app_build:


Add OPORJSON to docs.

Add OPORJSON to docs.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -182,6 +182,11 @@
 		component = docsProcessJs component
 		html.push (docsAppendTitle component, 'Base JavaScript Component Class')

+		# OPORJSON
+		opor = grunt.file.read 'tamia/opor.js'
+		opor = docsProcessJs opor
+		html.push (docsAppendTitle opor, 'OPORJSON API')
+
 		saveHtml 'docs', marked html.join '\n\n'

 		# Modules


Clarify buildnpm note.

Clarify buildnpm note.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -191,8 +191,9 @@
       # Install the npm packages.
       buildnpm:
         command:
-          # Note: the first install fails to completely install secondary
-          #   lodash dependencies. The second install remedies this npm bug.
+          # Note: the first install below fails to completely install
+          #   secondary lodash dependencies. The second install remedies
+          #   this npm bug.
           # TODO - revisit this with a new npm release in 2017. Try deleting
           #   the second install below, running npm run reinstall and then
           #   grunt start. If grunt fails with a missing babel lodash error,


remove test, and add build command

remove test, and add build command

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -63,5 +63,6 @@
   grunt.loadNpmTasks 'grunt-contrib-stylus'

   # tasks
-  grunt.registerTask "run", ["connect", "watch:coffee"]
-  grunt.registerTask "run_with_test", ["coffee", "connect", "watch:coffee_with_test"]
+  grunt.registerTask "run", ["connect", "watch"]
+  grunt.registerTask "build", ["coffee", "jade", "stylus"]
+


Fix on Gruntfile

Fix on Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -14,27 +14,18 @@
         'src/quo.coffee'
         'src/quo.*.coffee'
       ]
-      widgets: [
-        'src/gestures/quo.gestures.coffee'
-        'src/gestures/quo.gestures.*.coffee'
-      ]

     coffee:
-      app:      files: 'package/<%= meta.file %>.debug.js'        : ['<%= resources.app %>']
-      # widgets:  files: 'package/<%= meta.file %>.widget.debug.js' : ['<%= resources.widgets %>']
+      app: files: 'package/<%= meta.file %>.debug.js': ['<%= resources.app %>']

     uglify:
       options: compress: false, banner: "<%= meta.banner %>"
-      app:      files: 'package/<%= meta.file %>.js'        : 'package/<%= meta.file %>.debug.js'
-      # widgets:  files: 'package/<%= meta.file %>.widget.js' : 'package/<%= meta.file %>.widget.debug.js'
+      app: files: 'package/<%= meta.file %>.js': 'package/<%= meta.file %>.debug.js'

     watch:
       app:
         files: ['<%= resources.app %>']
         tasks: ["coffee:app"]
-      # widgets:
-      #   files: ['<%= resources.widgets %>']
-      #   tasks: ["coffee:widgets"]

   grunt.loadNpmTasks "grunt-contrib-coffee"
   grunt.loadNpmTasks "grunt-contrib-uglify"


workaround grunt sass bug

workaround grunt sass bug

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -57,6 +57,7 @@
       build:
         options:
           bundleExec: true
+          noCache: true #workaround for https://github.com/gruntjs/grunt-contrib-sass/issues/63
         files:
           'build/css/basic_without_embeds.css': 'app/css/basic.scss'
           'build/css/framed_controls_without_embeds.css': 'app/css/framed_controls.scss'


Separate library tests from CLI tests in Gruntfile.coffee

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -15,7 +15,8 @@
 			options:
 				compilers: ['coffee:coffee-script/register']
 				require: ['test/support/common', 'test/support/fixtures']
-			files: ['test/brave-mouse', 'test/cli']
+			lib: ['test/brave-mouse']
+			cli: ['test/cli']

 	grunt.loadNpmTasks 'grunt-contrib-clean'
 	grunt.loadNpmTasks 'grunt-contrib-coffee'
@@ -23,4 +24,6 @@

 	grunt.registerTask 'build', ['clean:package', 'coffee']
 	grunt.registerTask 'test', ['build', 'mochacli']
+	grunt.registerTask 'test:lib', ['build', 'mochacli:lib']
+	grunt.registerTask 'test:cli', ['build', 'mochacli:cli']
 	grunt.registerTask 'default', []


Grunt: Respect TESTS envvar, passed as grep to Mocha

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -9,6 +9,7 @@
         src: ['spec/*.coffee']
         options:
           reporter: 'spec'
+          grep: process.env['TESTS']

     # Coding standards
     coffeelint:


Use git repos

Use git repos

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -2,8 +2,13 @@
 exec = require("child_process").exec
 spawn = require("child_process").spawn

-WEB_REPO = "git@bitbucket.org:sharelatex/web-sharelatex.git"
-DOC_UPDATER_REPO = "git@bitbucket.org:sharelatex/documentupdater-sharelatex.git"
+SERVICES = [{
+	name: "web"
+	repo: "git@github.com:sharelatex/web-sharelatex.git"
+}, {
+	name: "document-updater"
+	repo: "git@github.com:sharelatex/document-updater-sharelatex.git"
+}]

 module.exports = (grunt) ->
@@ -46,21 +51,21 @@
 						"Misc": [
 							"help"
 						]
+						"Install tasks": ("install:#{service.name}" for service in SERVICES).concat("install:all")
+						"Update tasks": ("update:#{service.name}" for service in SERVICES).concat("update:all")

-	grunt.registerTask 'install:web', "Download and set up the web-sharelatex service", () ->
-		done = @async()
-		Helpers.installService(WEB_REPO, "web", done)
-	grunt.registerTask 'install:document-updater', "Download and set up the document-updater-sharelatex service", () ->
-		done = @async()
-		Helpers.installService(DOC_UPDATER_REPO, "document-updater", done)
+	for service in SERVICES
+		do (service) ->
+			grunt.registerTask "install:#{service.name}", "Download and set up the #{service.name} service", () ->
+				done = @async()
+				Helpers.installService(service.repo, service.name, done)
+			grunt.registerTask "update:#{service.name}", "Checkout and update the #{service.name} service", () ->
+				done = @async()
+				Helpers.updateService(service.name, done)

-	grunt.registerTask 'update:web', "Checkout and update the web-sharelatex service", () ->
-		done = @async()
-		Helpers.updateService("web", done)
-	grunt.registerTask 'update:document-updater', "Checkout and update the document-updater-sharelatex service", () ->
-		done = @async()
-		Helpers.updateService("document-updater", done)
-
+	grunt.registerTask 'install:all', "Download and set up all ShareLaTeX services", ("install:#{service.name}" for service in SERVICES)
+	grunt.registerTask 'update:all', "Checkout and update all ShareLaTeX services", ("update:#{service.name}" for service in SERVICES)
+
 	grunt.registerTask 'help', 'Display this help list', 'availabletasks'

 	grunt.registerTask 'run:web', "Run web-sharelatex, the ShareLaTeX web server", ["bunyan", "execute:web"]


Remove some tests

Remove some tests

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -12,8 +12,8 @@
   browsers.push browser for browser in ({browserName: "android", version: "#{vers}"} for vers in ["4.0", "4.4", "5.0", "5.1"])
   browsers.push browser for browser in ({browserName: "safari", version: "#{vers}"} for vers in [5..9])
   browsers.push browser for browser in ({platform: "XP", browserName: "opera", version: "#{vers}"} for vers in [11..12])
-  browsers.push browser for browser in ({platform: "XP", browserName: "googlechrome", version: "#{vers}"} for vers in [26..46])
-  browsers.push browser for browser in ({platform: "linux", browserName: "firefox", version: "#{vers}"} for vers in [4..42] by 3)
+  browsers.push browser for browser in ({platform: "XP", browserName: "googlechrome", version: "#{vers}"} for vers in [26..46] by 2)
+  browsers.push browser for browser in ({platform: "linux", browserName: "firefox", version: "#{vers}"} for vers in [4..42] by 4)
   browsers.push browser for browser in ({browserName: "internet explorer", version: "#{vers}"} for vers in [6..11])

   grunt.initConfig


Adjust grunt tasks list

Adjust grunt tasks list

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -100,10 +100,10 @@
     watch:
       default:
         files: ["src/coffee/*.coffee"]
-        tasks: ["clean:default", "coffeelint:default", "coffee:default", "concat", "uglify"]
+        tasks: ["build"]
       test:
-        files: ["test/spec/*.coffee"]
-        tasks: ["clean:test", "coffeelint:test", "coffee:test", "jasmine"]
+        files: ["src/spec/*.coffee"]
+        tasks: ["test"]
       doc:
         files: ["docs/*.coffee"]
         tasks: ["coffeelint:doc", "coffee:doc"]
@@ -175,7 +175,7 @@

   # register tasks
   grunt.registerTask "default", ["run"]
-  grunt.registerTask "run", ["connect", "open", "watch:doc"]
+  grunt.registerTask "run", ["build", "connect", "open", "watch:doc"]
   grunt.registerTask "build", ["clean", "coffeelint", "coffee", "concat", "less", "uglify", "copy"]
   grunt.registerTask "test", ["build", "jasmine"]
   grunt.registerTask "release", "Release a new version, push it and publish it", (target)->


Just always run mocha

Just always run mocha

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -208,12 +208,12 @@
     'clean', 'copy:build',  'copy:extension_events', 'coffee','copy:postFactlinkObject',
     'sass', 'cssUrlEmbed', 'cssmin',
     'concat', 'uglify', 'code_inliner',
-    'shell:gzip_js_files', 'copy:dist'
+    'shell:gzip_js_files', 'copy:dist', 'mocha'
   ]

   grunt.registerTask 'default', ['compile']
   grunt.registerTask 'server',  ['compile']
-  grunt.registerTask 'test',    ['compile', 'mocha']
+  grunt.registerTask 'test',    ['compile']

   grunt.loadNpmTasks 'grunt-contrib-sass'
   grunt.loadNpmTasks 'grunt-contrib-uglify'


Watch Gruntfile.coffee

Watch Gruntfile.coffee

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -34,7 +34,7 @@

     watch:
       src:
-        files: ["src/*.coffee", "test/src/*.coffee"]
+        files: ["src/*.coffee", "test/src/*.coffee", "Gruntfile.coffee"]
         tasks: ["build"]

     shell:


uglify task

uglify task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -35,18 +35,25 @@
           'coffee:src'
         ]

+    uglify:
+      js:
+        files:
+          './dist/cmsviz.min.js': ['./dist/cmsviz.js']
+
   # Register grunt tasks supplied by grunt-contrib-*.
   # Referenced in package.json.
   # https://github.com/gruntjs/grunt-contrib
   grunt.loadNpmTasks 'grunt-contrib-clean'
   grunt.loadNpmTasks 'grunt-contrib-coffee'
   grunt.loadNpmTasks 'grunt-contrib-watch'
+  grunt.loadNpmTasks 'grunt-contrib-uglify'

   # Compiles the app with non-optimized build settings.
   # Enter the following command at the command line to execute this build task:
   # grunt
   grunt.registerTask 'default', [
     'coffee:src'
+    'uglify'
   ]


fix trailing zeros removal code

fix trailing zeros removal code

--- vendor/assets/javascripts/fustrate/_prototypes.coffee
+++ vendor/assets/javascripts/fustrate/_prototypes.coffee
@@ -47,12 +47,12 @@
 Number::bytesToString = ->
   return "#{@} B" if @ < 1000

-  return "#{(@ / 1000).toFixed(2).replace(/[0.]+$/, '')} kB" if @ < 1000000
+  return "#{(@ / 1000).toFixed(2).replace(/\.?0+$/, '')} kB" if @ < 1000000

   if @ < 1000000000
-    return "#{(@ / 1000000).toFixed(2).replace(/[0.]+$/, '')} MB"
+    return "#{(@ / 1000000).toFixed(2).replace(/\.?0+$/, '')} MB"

-  "#{(@ / 1000000000).toFixed(2).replace(/[0.]+$/, '')} GB"
+  "#{(@ / 1000000000).toFixed(2).replace(/\.?0+$/, '')} GB"

 Object.defineProperty Object::, 'tap',
   enumerable: false
@@ -60,7 +60,7 @@
     if typeof func is 'function'
       func.apply(@)
     else
-      @[func].apply(@, Array::slice.call(arguments).slice(1))
+      @[func].apply @, Array::slice.call(arguments).slice(1)

     @


Use correct datapath

Use correct datapath

--- client/landing/app/Applications/Apps.kdapplication/Views/AppsListItemView.coffee
+++ client/landing/app/Applications/Apps.kdapplication/Views/AppsListItemView.coffee
@@ -80,7 +80,7 @@

-          {{#(counts.tagged) or 0}} Followers
+          {{#(counts.followers) or 0}} Followers


fix image token

fix image token

--- app/assets/javascripts/ab_admin/components/select2_bridge.js.coffee
+++ app/assets/javascripts/ab_admin/components/select2_bridge.js.coffee
@@ -27,9 +27,9 @@
     if @el.data('image')
       @buildimageOptions()
     if @el.data('result')
-      @options.formatResult = (item) -> fetchTemplate(@el.data('result'))(item)
+      @options.formatResult = (item) => fetchTemplate(@el.data('result'))(item)
     if @el.data('selection')
-      @options.formatSelection = (item) -> fetchTemplate(@el.data('selection'))(item)
+      @options.formatSelection = (item) => fetchTemplate(@el.data('selection'))(item)

     @options.initSelection = (el, callback) =>
       data = @el.data('pre')


[Fixes #70338086]

[Fixes #70338086]

Manually update phase->tasks relationship when tasks are loaded
via the event stream.

--- app/assets/javascripts/controllers/application_controller.js.coffee
+++ app/assets/javascripts/controllers/application_controller.js.coffee
@@ -23,13 +23,21 @@
     Ember.run =>
       # add code for when esData is a message_task
       if esData.task
-        if task = @store.findTask(esData.task.id)
+        phaseId = esData.task.phase_id
+        taskId = esData.task.id
+        if task = @store.findTask(taskId)
           # This is an ember bug.  A task's phase needs to be notified that the other side of
           # the hasMany relationship has changed via set.  Simply loading the updated task into the store
-          # won't trigger the relationship update.
-          task.set('phase', @store.getById('phase', esData.task.phase_id))
-          task.triggerLater('didLoad')
-      @store.pushPayload('task', esData)
+          # won't trigger the relationship update.
+          task.set('phase', @store.getById('phase', phaseId))
+          @store.pushPayload('task', esData)
+        else
+          @store.pushPayload('task', esData)
+          task = @store.findTask(taskId)
+          phase = @store.getById('phase', phaseId)
+          phase.get('tasks').addObject(task)
+
+        task.triggerLater('didLoad')

   overlayBackground: Ember.computed.defaultTo('defaultBackground')


debug works with object

debug works with object

--- src/steroids.coffee
+++ src/steroids.coffee
@@ -27,19 +27,16 @@
   debugMessages: []
   debugEnabled: false

-  debug: (options={}) ->
+  debug: (msg) ->
     return unless steroids.debugEnabled

-    msg = if options.constructor.name == "String"
-      options
-    else
-      options.msg
+    msgJSON = JSON.stringify(msg)

     red   = '\u001b[31m'
     blue  = '\u001b[34m'
     reset = '\u001b[0m'

-    debugMessage = "[#{red}DEBUG#{reset}] - #{msg} - #{blue} #{window.location.href}#{reset}"
+    debugMessage = "[#{red}DEBUG#{reset}] - #{msgJSON} - #{blue} #{window.location.href}#{reset}"

     window.steroids.debugMessages.push debugMessage
     console.log debugMessage


Convert strayed `this`.

Convert strayed `this`.

--- src/nodes/node.coffee
+++ src/nodes/node.coffee
@@ -121,7 +121,7 @@
     {
       type    : 'text'
       cw      : @codeBlockLevel
-      hw      : if @uglify then 0 else @blockLevel - this.codeBlockLevel
+      hw      : if @uglify then 0 else @blockLevel - @codeBlockLevel
       text    : if escape then e(text) else text
     }

@@ -151,7 +151,7 @@
     {
       type            : 'insert'
       cw              : @codeBlockLevel
-      hw              : if @uglify then 0 else @blockLevel - this.codeBlockLevel
+      hw              : if @uglify then 0 else @blockLevel - @codeBlockLevel
       escape          : escape
       preserve        : preserve
       findAndPreserve : findAndPreserve


Don't need these parentheses

Don't need these parentheses

--- app/assets/javascripts/incidents/filter.coffee
+++ app/assets/javascripts/incidents/filter.coffee
@@ -10,6 +10,6 @@

 $(document).ready ->
   $('.filters.divisions').on 'click', 'button', () ->
-    $(this).filterTableFromButton('incidents', 'division')
+    $(this).filterTableFromButton 'incidents', 'division'
   $('.filters.groups').on 'click', 'button', () ->
-    $(this).filterTableFromButton('index', 'group')
+    $(this).filterTableFromButton 'index', 'group'


Fix for bad dragging issue

Fix for bad dragging issue

--- app/assets/javascripts/layout_editor.js.coffee
+++ app/assets/javascripts/layout_editor.js.coffee
@@ -11,19 +11,11 @@
 $(document).ready ->
   $(".sortable").sortable
     revert: true
-  $("#draggable").draggable
-    connectToSortable: "#sortable"
-    helper: "original"
-    revert: "invalid"
   make_editable()

 make_sortable = ->
   $(".sortable_cats").sortable
     revert: true
-  $(".draggable_cats").draggable
-    connectToSortable: ".sortable_cats"
-    helper: "original"
-    revert: "invalid"

 load_product_type_tree = ->
   $("#product_type_tree").jstree


Bug: don't show chart button if showing a chart. ref #578

--- app/assets/javascripts/lib/models/chart.coffee
+++ app/assets/javascripts/lib/models/chart.coffee
@@ -143,7 +143,7 @@
       # show/hide format toggle button
       if @current().view.can_be_shown_as_table()
         $("a.table_format").show()
-        $("a.chart_format").show()
+        $("a.chart_format").hide()
       else
         $("a.table_format").hide()
         $("a.chart_format").hide()


add tace to js search

add tace to js search

--- app/assets/javascripts/search/presenter.coffee
+++ app/assets/javascripts/search/presenter.coffee
@@ -237,4 +237,5 @@
   # Error view, rendered in case of any sendMainSearch/onMainResults exceptions.
   failure: (error) =>
     console.log error
+    console.trace()
     @render '#search-wrapper', 'error_ajax', I18n.t('js.ajax_error')


Changed the way the word frequencies are sorted for winning facet viz.

For some reason, Safari wasn't reversing the list, based on the
comparison function. This fixes that.

Rather than using a user-defined comparison function, I'm using a
Schwartzian transformation and two calls to native methods (`sort` and
`reverse`), which I expect is still faster than calling into a
user-defined function for each comparison that the sort makes. Of
course, for a list of this size (3), it's probably a toss-up.

Also, I built the word frequencies list using a comprehension.

Fixes #280.

--- app/assets/javascripts/visualization.js.coffee
+++ app/assets/javascripts/visualization.js.coffee
@@ -87,19 +87,18 @@
     words = $("span.word")
     words.each( (i,word) ->

-      word_freqs = []
-      for facet_num in all_facet_nums
-        word_freqs.push([window.all_colors[facet_num],frequencies[facet_num][i]])
+      word_freqs = for facet_num in all_facet_nums
+        [frequencies[facet_num][i], window.all_colors[facet_num]]

-      word_freqs.sort (a,b) ->
-        return ((a[1] < b[1]) ? -1 : ((a[1] > b[1]) ? 1 : 0));
+      word_freqs.sort()
+      word_freqs.reverse()

       #no highlighting gets grey, ties get black

-      if word_freqs[0][1] > word_freqs[1][1]
-        winning_color = word_freqs[0][0]
+      if word_freqs[0][0] > word_freqs[1][0]
+        winning_color = word_freqs[0][1]
         $(word).toggleClass(winning_color + "-vis", true)
-      else if word_freqs[0][1] != 0
+      else if word_freqs[0][0] != 0
         $(word).css('color', 'black')
       else
         $(word).css('color', 'lightgrey')


removing .orig file created by mergetools

--- app/controllers/interactive/interactive.coffee
+++ app/controllers/interactive/interactive.coffee
@@ -1,56 +0,0 @@
-Spine = require('spine')
-
-class Interactive extends Spine.Controller
-
-  events:
-    'click .navigator_home': 'linkToNavigatorHome'
-    'click .classify_galaxies': 'linkToGalaxyZoo'
-    'click .my_galaxies': 'linkToMyGalaxies'
-    'click .graph': 'linkToGraph'
-    'click input#dx-change': 'addDX'
-
-  constructor: ->
-    super
-    @render()
-
-  render: ->
-    home = require('views/interactive/home')
-    @html home
-
-  linkToNavigatorHome: (ev) ->
-    home = require('views/interactive/home')
-    @html home
-
-  linkToGalaxyZoo: (ev) ->
-    @navigate '/classify'
-
-  linkToMyGalaxies: (ev) ->
-    my_gals = require('views/interactive/my_galaxies')
-    $('#main_body').replaceWith(my_gals)
-    ev.preventDefault()
-
-  linkToGraph: (ev) ->
-    graph = require('views/interactive/graph')
-    $('#main_body').replaceWith(graph)
-    ev.preventDefault()
-
-  addDX: ->
-    if $("#dx-change").hasClass('unclicked')
-      $("#dx-change").removeClass('unclicked').addClass('clicked')
-      $('div#home').addClass('dx')
-      $('#box').addClass('dx')
-      $('h1').addClass('dx')
-      $('h2').addClass('dx')
-      $('p').addClass('dx')
-      $('#link_buttons a').addClass('dx')
-    else
-      $("#dx-change").removeClass('clicked').addClass('unclicked')
-      $('div#home').removeClass('dx')
-      $('#box').removeClass('dx')
-      $('h1').removeClass('dx')
-      $('h2').removeClass('dx')
-      $('p').removeClass('dx')
-      $('#link_buttons a').removeClass('dx')
-
-
-module.exports = Interactive


total user count view added to filtered user view.

--- client/Dashboard/views/badge/badgerules.coffee
+++ client/Dashboard/views/badge/badgerules.coffee
@@ -28,6 +28,9 @@
         @createUserSelector()
         @giveBadgeButton.show()

+    @totalUserCount   = new KDCustomHTMLView
+      partial         : ""
+
     @filteredUsersController = new KDListViewController
       startWithLazyLoader    : no
       view                   : new KDListView
@@ -72,7 +75,7 @@
     userListView.on "RemoveBadgeUser", (ac) =>
       tmpArr = @usersInput.getValue().split ','
       index = tmpArr.indexOf ac._id
-      tmpArr.splice index,1
+      tmpArr.splice index, 1
       @usersInput.setValue tmpArr.toString()
       @usersInput.getValue()

@@ -96,13 +99,14 @@
       operArr[action]    = propVal
       selector[property] = operArr
       rules += countProp + tmpAct + propVal
-      rules += "+" if key < ruleItems.length-1
+      rules += "+" if key < ruleItems.length - 1

     @rule.setValue rules
     KD.remote.api.JAccount.someWithRelationship selector, {}, (err, users) =>
       return err if err
       @usersInput.setValue (user._id for user in users)
       @filteredUsersController.removeAllItems()
+      @totalUserCount.setPartial "Total users : #{users.length}"
       @filteredUsersController.instantiateListItems users

   updateRulesList:->
@@ -119,12 +123,14 @@
     """
     {{> @addRuleButton}}
     {{> @badgeListView}}
+    {{> @totalUserCount}}
     {{> @doneButton}}
     {{> @userList}}
     {{> @giveBadgeButton}}
     {{> @usersInput}}
     {{> @rule}}
     """
+

 class BadgeUsersItem extends KDListItemView
   constructor: (options ={}, data)->


log an error if core file is found in output

--- services/clsi/app/coffee/CompileController.coffee
+++ services/clsi/app/coffee/CompileController.coffee
@@ -39,6 +39,10 @@
 						for file in outputFiles
 							if file.path?.match(/output\.pdf$/)
 								status = "success"
+						# log an error if any core files are found
+						for file in outputFiles
+							if file.path is "core"
+								logger.error project_id:request.project_id, req:req, outputFiles:outputFiles, "core file found in output"

 					timer.done()
 					res.status(code or 200).send {


Use moment to get/format Dates values

Use moment to get/format Dates values

--- client/app/components/date_range_picker.coffee
+++ client/app/components/date_range_picker.coffee
@@ -4,7 +4,6 @@

 momentFormat     = 'DD/MM/YYYY'
 datePickerFormat = '%d/%m/%Y'
-

 module.exports = DateRangePicker = React.createClass
     displayName: 'DateRangePicker'
@@ -53,15 +52,11 @@

     filterize: ->
-        start = if @state.startDate
-            [d, m, y] = @state.startDate.split '/'
-            "#{y}-#{m}-#{d}T00:00:00.000Z"
+        getValue = (key) =>
+            return unless (value = @state[key + 'Date'])
+            moment(value, momentFormat)[key + 'Of']('day').toISOString()

-        end = if @state.endDate
-            [d, m, y] = @state.endDate.split '/'
-            "#{y}-#{m}-#{d}T23:59:59.999Z"
-
-        @props.onDateFilter start, end
+        @props.onDateFilter getValue('start'), getValue('end')

     onReset: ->
@@ -77,11 +72,11 @@
         @changeDates 'month'

     changeDates: (type) ->
-        value = moment().subtract(1, type).startOf(type) if type
+        value = moment().subtract(1, type) if type
         state =
             isActive    : !!type
-            startDate   : value?.format momentFormat
-            endDate     : value?.endOf(type).format momentFormat
+            startDate   : start = value?.startOf(type).format momentFormat
+            endDate     : end = value?.endOf(type).format momentFormat

         @setState state, @filterize


Environment: getters: filter first then make the assignment

--- client/app/lib/flux/environment/getters.coffee
+++ client/app/lib/flux/environment/getters.coffee
@@ -30,11 +30,11 @@
     own
       .map (id) ->
         machine = machines.get(id)
+        return  if 'own' isnt machine.get 'type'
         machine
           .set 'isApproved', yes
           .set 'isManaged', machineRuleChecker machine, ['managed']
-      .filter (machine) ->
-        'own' is machine.get 'type'
+      .filter Boolean

 ]

@@ -45,12 +45,12 @@
     shared
       .map (id) ->
         machine = machines.get(id)
+        return  if 'shared' isnt machine.get 'type'
         machine
           .set 'owner', getMachineOwner machine
           .set 'isApproved', machineRuleChecker machine, ['approved']
           .set 'isPermanent', machineRuleChecker machine, ['permanent']
-      .filter (machine) ->
-        'shared' is machine.get 'type'
+      .filter Boolean

 ]

@@ -61,12 +61,12 @@
     collaboration
       .map (id) ->
         machine = machines.get(id)
+        return  if 'collaboration' isnt machine.get 'type'
         machine
           .set 'owner', getMachineOwner machine
           .set 'isApproved', machineRuleChecker machine, ['approved']
           .set 'isPermanent', machineRuleChecker machine, ['permanent']
-      .filter (machine) ->
-        'collaboration' is machine.get 'type'
+      .filter Boolean

 ]


spec: Increase timeout for window.open specs

--- spec/chromium-spec.coffee
+++ spec/chromium-spec.coffee
@@ -56,6 +56,8 @@
       assert.notEqual navigator.language, ''

   describe 'window.open', ->
+    @timeout 10000
+
     it 'returns a BrowserWindowProxy object', ->
       b = window.open 'about:blank', '', 'show=no'
       assert.equal b.closed, false
@@ -80,6 +82,8 @@
       b = window.open "file://#{fixtures}/pages/window-open-size.html", '', 'show=no'

   describe 'window.opener', ->
+    @timeout 10000
+
     url = "file://#{fixtures}/pages/window-opener.html"
     w = null


Added a test case for a hash trie indexing error.

--- spec/hash_set_spec.coffee
+++ spec/hash_set_spec.coffee
@@ -1,8 +1,9 @@
 if typeof(require) != 'undefined'
   require.paths.unshift('#{__dirname}/../lib')
-  HashSet = require('indexed').HashSet
+  { Sequence } = require 'sequence'
+  { HashSet }  = require 'indexed'
 else
-  HashSet = pazy.HashSet
+  { Sequence, HashSet } = pazy

 class FunnyKey
@@ -340,3 +341,34 @@

       it "should not be empty", ->
         expect(h.size()).toBeGreaterThan 0
+
+  describe "with a peculiar sequence of additions and deletions", ->
+    class Triangle
+      constructor: (@a, @b, @c) ->
+      toSeq: -> new Sequence [@a, @b, @c]
+      toString: -> "T(#{@a}, #{@b}, #{@c})"
+
+    moves = [
+      ['plus',0,1,13]
+      ['plus',0,7,2]
+      ['plus',0,13,14]
+      ['plus',0,14,7]
+      ['plus',1,6,13]
+      ['plus',2,4,15]
+      ['plus',2,7,15]
+      ['plus',2,10,4]
+      ['plus',2,15,12]
+      ['minus',0,7,2]
+      ['minus',2,4,15]
+      ['minus',2,7,15]
+    ]
+
+    T = Sequence.reduce moves, new HashSet(), (s, [mode, a, b, c]) ->
+      t = new Triangle a, b, c
+      if mode == 'minus' then s.minus t else s.plus t
+
+    it "should contain the correct number of items", ->
+      expect(Sequence.size T).toBe 6
+
+    it "should contain the correct number after one item is removed", ->
+      expect(Sequence.size T.minus new Triangle 2, 10, 4).toBe 5


fix logic

fix logic

--- atmospheric/002-accumulated-heat/viewer.coffee
+++ atmospheric/002-accumulated-heat/viewer.coffee
@@ -79,15 +79,15 @@
         points
     contour = (positioning, heatdata) ->
         for col in [0...256]
-            column = heatdata[col]
             for idx in [1...256]
                 value = sc / 256 * idx
-                points = find(column, value)
-                for row in points
-                    [x, y] = positioning(lng(col), lat(row))
-                    if x != -1 && y != -1
-                        context.fillStyle = colorHeat(idx)
-                        context.fillRect(Math.floor(256 + 250 * x), Math.floor(256 + 250 * y), 1, 1)
+                for col in [0...256]
+                    for row in [0...255]
+                        [x, y] = positioning(lng(col), lat(row))
+                        points = find([heatdata[col][row], heatdata[col][row+1]], value)
+                        if x != -1 && y != -1
+                            context.fillStyle = colorHeat(idx)
+                            context.fillRect(Math.floor(256 + 250 * x), Math.floor(256 + 250 * y), 1, 1)

     viewer.paint = (positioning, geodata, heatdata) ->
         context.clearRect(0, 0, 512, 512)


Flesh out comment about the + register needing a Linux update in the future

- Specifically, + and * work the same everywhere except X11 based
  environments, so just comment it until we have a linux build and can
  deal with it correctly

--- spec/prefixes-spec.coffee
+++ spec/prefixes-spec.coffee
@@ -75,7 +75,9 @@
           expect(atom.clipboard.read()).toEqual 'new content'

     # FIXME: once linux support comes out, this needs to read from
-    # the correct system clipboard. For now it behaves just like the * register
+    # the correct clipboard. For now it behaves just like the * register
+    # See :help x11-cut-buffer and :help registers for more details on how these
+    # registers work on an X11 based system.
     describe "the + register", ->
       describe "reading", ->
         it "is the same the system clipboard", ->


Add device.screenSize and better inspect

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -107,6 +107,7 @@

 		@_context = new Framer.Context(parent:@content, name:"DeviceScreen")
 		@_context.perspective = 1200
+		@_context.device = @

 	_update: =>

@@ -207,7 +208,15 @@
 		@_update()
 		@emit("change:fullScreen")

-
+	@define "screenSize",
+		get: ->
+
+			if @_shouldRenderFullScreen()
+				return Canvas.size
+
+			return size =
+				width: @_device.screenWidth
+				height: @_device.screenHeight

 	###########################################################################
 	# DEVICE TYPE
@@ -550,6 +559,9 @@
 		# 	return "#{resourceUrl}/#{hand}.jp2"

 		return "#{resourceUrl}/#{hand}.png"
+
+	toInspect: ->
+		return "<Device '#{@deviceType}' #{@screenSize.width}x#{@screenSize.height}>"

 ###########################################################################


:lipstick:

:lipstick:

--- spec/snippets-spec.coffee
+++ spec/snippets-spec.coffee
@@ -8,7 +8,7 @@
 SnippetsAvailable = require '../lib/snippets-available'

 describe "Snippets extension", ->
-  [buffer, editorElement, editor, snippets] = []
+  [editorElement, editor, snippets] = []

   simulateTabKeyEvent = ({shiftKey}={}) ->
     event = keydownEvent('tab', {shiftKey, target: editorElement})


fixing: tests need to run also when there is no canvas defined

--- fizzylogo_coda.coffee
+++ fizzylogo_coda.coffee
@@ -98,7 +98,7 @@
   # in Retina Displays 2x we'd get drawings that
   # are much smaller.

-  canvasOutputElement.getContext('2d').scale devicePixelRatio, devicePixelRatio
+  canvasOutputElement?.getContext('2d').scale devicePixelRatio, devicePixelRatio

   reset()


Dropbox: Improved "Upload to Dropbox" feature. Added notifications for success and error cases. Removed zipped file.

--- client/app/MainApp/filetree/controllers/findertreecontroller.coffee
+++ client/app/MainApp/filetree/controllers/findertreecontroller.coffee
@@ -792,36 +792,45 @@
     notification   = null
     kiteController = KD.getSingleton "kiteController"
     plainPath      = FSHelper.plainPath nodeView.getData().path
-    tmpFileName    = "tmp#{Date.now()}"
     isFolder       = nodeView.getData().type is "folder"
+    timestamp      = Date.now()
+    tmpFileName    = if isFolder then "tmp#{timestamp}.zip" else "tmp#{timestamp}"
+    relativePath   = "/home/#{KD.nick()}/Web/#{tmpFileName}"
     kallback       = ->
       fileName     = FSHelper.getFileNameFromPath plainPath
-      if isFolder
-        fileName   = "#{fileName}.zip"
+      fileName     = "#{fileName}.zip"  if isFolder
       options      =
-        files : [
-          {
-            filename: fileName
-            url     : "http://#{KD.getSingleton('vmController').defaultVmName}/#{tmpFileName}"
-          }
+        files      : [
+          filename : fileName
+          url      : "http://#{KD.getSingleton('vmController').defaultVmName}/#{tmpFileName}"
         ]
         success: ->
-          notification.notificationSetTitle "Your file has been uploaded"
+          notification.notificationSetTitle "Your file has been uploaded."
           notification.notificationSetTimer 4000
           notification.setClass "success"
+          kiteController.run "rm #{relativePath}"
+        error: ->
+          notification.notificationSetTitle "An error occured while uploading your file."
+          notification.notificationSetTimer 4000
+          notification.setClass "error"

       Dropbox.save options

     if isFolder
       notification = new KDNotificationView
-        title      : "Zipping your folder"
+        title      : "Zipping your folder..."
         type       : "mini"
         duration   : 120000

-      kiteController.run "mkdir -p Web ; zip -r /home/#{KD.nick()}/Web/#{tmpFileName}.zip #{plainPath}", (err, res) =>
+      kiteController.run "mkdir -p Web ; zip -r #{relativePath} #{plainPath}", (err, res) =>
+        return  warn err if err
+        notification.notificationSetTitle "Uploading zipped file..."
+        kallback()
+    else
+      notification = new KDNotificationView
+        title      : "Uploading your file..."
+        type       : "mini"
+        duration   : 120000
+      kiteController.run "mkdir -p Web ; cp #{plainPath} #{relativePath}", (err, res) =>
         return  warn err if err
         kallback()
-    else
-      kiteController.run "cp #{plainPath} /home/#{KD.nick()}/Web/#{tmpFileName}", (err, res) =>
-        return  warn err if err
-        kallback()


Workspace: Minor fixes for broadcast message.

--- client/landing/CollaborativeWorkspace/collaborativeworkspace.coffee
+++ client/landing/CollaborativeWorkspace/collaborativeworkspace.coffee
@@ -306,7 +306,9 @@
         cssClass : details.cssClass  ? "success"
         duration : details.duration or 4200
         origin   : details.origin   or "users"
-        sender   : details.sender   or @nickname
+        sender   : details.sender    ? @nickname
+
+    @broadcastRef.set {}

   displayBroadcastMessage: (options) ->
     # simple broadcast message is


Setting the width and height to the calculated width and height

--- framer/SVGPath.coffee
+++ framer/SVGPath.coffee
@@ -25,6 +25,14 @@
 	@define "html",
 		get: ->
 			@_element.outerHTML or ""
+
+	@define "width",
+		get: ->
+			@_width
+
+	@define "height",
+		get: ->
+			@_height

 	# Disabled properties
 	@undefine ["label", "blending", "image"]
@@ -87,7 +95,9 @@
 		_.defaults options, @constructor.attributesFromElement(pathProperties, @_element)
 		super(options)
 		@_length = @_element.getTotalLength()
-
+		rect = @_element.getBoundingClientRect()
+		@_width = rect.width
+		@_height = rect.height
 		for parent in @ancestors()
 			if parent instanceof SVGLayer
 				@_svg = parent.svg


added "only show question if…"

added "only show question if…"

--- dkobo/koboform/static/js/xlform/XLF.view.rowDetail.SkipLogic.coffee
+++ dkobo/koboform/static/js/xlform/XLF.view.rowDetail.SkipLogic.coffee
@@ -29,6 +29,9 @@
           <input type="radio" class="skiplogic__delimselectcb" name="#{tempId}" value="or" />
           Any

+

+

+        Only show this question if...

     """)


request address when customer loads

request address when customer loads

--- client/skr/screens/sales-order/SalesOrder.cjsx
+++ client/skr/screens/sales-order/SalesOrder.cjsx
@@ -26,8 +26,9 @@
                     syncOptions={@syncOptions} model={@sales_order}
                     commands={@state.commands} />

-                <SC.CustomerFinder selectField sm=3 xs=4
-                    customer={@sales_order.customer} model={@sales_order} />
+                <SC.CustomerFinder
+                    syncOptions={ include: ['billing_address', 'shipping_address' ] }
+                    selectField sm=3 xs=4 model={@sales_order} />

                 <LC.Input sm=3 xs=4 name='po_num' model={@sales_order} />


better naming, fixes bug in editing current taste

--- static/index.coffee
+++ static/index.coffee
@@ -1,5 +1,5 @@
 $ () ->
-  data = ''
+  currentContent = ''
   $linenos = $ '#linenos'
   $linenosCode = $ '> code', $linenos
   $editor = $ '#editor'
@@ -10,16 +10,16 @@
   metaKeyName = 'Ctrl'
   metaKeyName = 'Cmd'  if /^Mac/.test navigator.platform

-  getContent = () ->
-    if $editorCode[0].innerText?
+  getDomContent = () ->
+    if 'innerText' of $editorCode[0]
       $editorCode[0].innerText
     else
       content = $editorCode[0].innerHTML
       content = content.replace /
/gi, '\n'
       he.decode content

-  setContent = (content) ->
-    if $editorCode[0].innerText?
+  setDomContent = (content) ->
+    if 'innerText' of $editorCode[0]
       $editorCode[0].innerText = content
     else
       content = content.replace
@@ -27,7 +27,7 @@

   startEditing = (evt) ->
     evt.preventDefault()
-    edit data
+    edit currentContent
     $editor.off 'dblclick', startEditing
     false

@@ -61,16 +61,15 @@
       method = 'POST'
       url = 'tastes/'

-    data = getContent().trim().replace(/\s+\n/g, '\n')
-    data += '\n'  if data.length
-    unless data.length
-      setContent(data)  if data isnt getContent()
-      return true
+    currentContent = getDomContent().trim().replace(/\s+\n/g, '\n')
+    currentContent += '\n'  if currentContent.length
+    setDomContent currentContent  if currentContent isnt getDomContent()
+    return true  unless currentContent.length

     always = () ->
       edit()

-    done = (responseData, status, xhr) ->
+    done = (body, status, xhr) ->
       if method is 'POST'
         window.location.hash = xhr.getResponseHeader 'Location'
       else
@@ -84,12 +83,12 @@
       method
       url
       contentType: 'application/octet-stream'
-      data
+      data: currentContent
     }).always(always).done(done).fail(fail)
     false

   edit = (content) ->
-    content ?= getContent()
+    content ?= getDomContent()
     $linenosCode.html "#{metaKeyName}+s to Save - Shift+#{metaKeyName}+s to Save As...".replace /(.)/g, '$1
'
     $editorCode.html(content).attr('contentEditable', 'true').focus()
     $editor.addClass('editing')
@@ -97,7 +96,7 @@
     $(window).on 'keydown', maybeSave

   lock = (content, lines = []) ->
-    content ?= getContent()
+    content ?= getDomContent()
     if Array.isArray lines
       if lines.length is 0
         linenosCount = content.split('\n').length
@@ -123,14 +122,15 @@

     [filename, language] = filename.split '.'

-    done = (data) ->
+    done = (body, status, xhr) ->
+      currentContent = body
       if language?
         try
-          high = hljs.highlight language, data
+          high = hljs.highlight language, currentContent
         catch e
-          high = {value: data}
+          high = {value: currentContent}
       else
-        high = hljs.highlightAuto data
+        high = hljs.highlightAuto currentContent
         if high.language?
           window.location.hash = "#{filename}.#{high.language}"
           return


made default textwrap size range [4, 80]

--- src/textwrap/helpers/parseSize.coffee
+++ src/textwrap/helpers/parseSize.coffee
@@ -77,6 +77,6 @@
   # Sets .size( ) method, if not specififed.
   unless vars.size.value
     if vars.resize.value
-      vars.self.size [size, size * 2]
+      vars.self.size [4, 80]
     else
       vars.self.size [size / 2, size]


make URL independent of feth.com

make URL independent of feth.com

--- extension/chrome/js/src/content.coffee
+++ extension/chrome/js/src/content.coffee
@@ -18,7 +18,7 @@
 		@ID_PREFIX: 'mailbutler-id-'

 		# production URL
-		url: "https://script.google.com/a/macros/feth.com/s/AKfycbxf5DLvznehMYEK5u3p9d-f1F_iwIIqs11SCw_loUDogp3iDg/exec"
+		url: "https://script.google.com/macros/s/AKfycbxf5DLvznehMYEK5u3p9d-f1F_iwIIqs11SCw_loUDogp3iDg/exec"

 		# dev URL
 		# url: "https://script.google.com/a/macros/feth.com/s/AKfycbztqUX2xb2_w4NnlsaUP_f5sdLl8h9Fsc5AORb9Pg/dev"


use deferreds

use deferreds

--- extension/chrome/js/src/content.coffee
+++ extension/chrome/js/src/content.coffee
@@ -285,17 +285,11 @@
         pickerMenu.close()
         presetMenu.close()
         button.close()
-
-        loading = ->
-          button.addClass M.CLS_LOADER
-          return
-        reset = ->
+        button.addClass M.CLS_LOADER
+        props.when = wen
+        (@onSchedule props).always ->
           button.removeClass M.CLS_LOADER
           return
-
-        props.when = wen
-        @onSchedule props, loading, reset
-
         return

       isValid = =>
@@ -455,69 +449,51 @@
         throw __msg 'errorNotWithinAConversation'
       id

-    onSchedule: (props, loadingIcon, resetIcon) =>
+    onSchedule: (props) =>
       try
         messageId = @getMessageId()
       catch e
-        @onScheduleError null, null, e.toString()
-        return
-
-      archive = !!props.archive
-
-      loadingIcon?() unless archive
+        @onScheduleError null, null, e.toString(), ''
+        return

       data =
-        msgId:  messageId
-        when:    props.when
-        markUnread:    !!props.unread
-        starIt:    !!props.star
-        onlyIfNoAnswer:  !!props.noanswer
-        moveToInbox:    !!props.inbox
-        archiveAfterScheduling:  archive
-
-      # remove false values to transmit less data over the wire
-      _.each data, (val, key) ->
-        delete data[key] if val is false
-        return
+        msgId: messageId
+        when:  props.when
+
+      data.markUnread = true              if !!props.unread
+      data.starIt = true                  if !!props.star
+      data.onlyIfNoAnswer = true          if !!props.noanswer
+      data.moveToInbox = true             if !!props.inbox
+      data.archiveAfterScheduling = true  if !!props.archive

       log 'scheduling mail...', data

-
-      $.ajax
-        url:       @getServiceURL() + M.SCHEDULE_SUFFIX
-        #type:     'POST'
-        dataType:  'json'
-        data:      data
-        success:   (resp, textStatus, jqXHR) =>
-          success = @onScheduleSuccess resp, data
-          if success
-            resetIcon?() unless archive
-          return
-        error:     (jqXHR, textStatus, errorThrown) =>
-          @onScheduleError textStatus, data, errorThrown, jqXHR.responseText
-          return
-
-      if archive
-        @activateArchiveButton()
-      else
-        _.delay (->
-          resetIcon?()
-          return
-          ), 600
-      return
-
-
-    onScheduleSuccess: (data, params) =>
-      log 'Scheduling success', data
-      if data.success
+      url = @getServiceURL() + M.SCHEDULE_SUFFIX
+
+      success = =>
         chrome.extension.sendMessage
           action:   'notification'
-          icon:     "images/tie.svg"
+          icon:     'images/tie.svg'
           title:     __msg 'notificationScheduleSuccessTitle'
           message:   __msg 'notificationScheduleSuccess'
-      else
-        @onScheduleError data.error, params, data.error
-      data.success
+        @activateArchiveButton() if data.archiveAfterScheduling
+        return
+
+      error = (status, error, responseText) =>
+        @onScheduleError status, data, error, responseText
+        return
+
+      ($.post url, data, null, 'json')
+      .done (resp, textStatus, jqXHR) ->
+        if resp.success
+          success()
+        else
+          error textStatus, resp.error, jqXHR.responseText
+        return
+      .fail (jqXHR, textStatus, reason) ->
+        error textStatus, reason, jqXHR.responseText
+        return
+      .promise()

     createDialog: (title, okButton, cancelButton) ->
       dialog = new GMailUI.ModalDialog title
@@ -616,7 +592,7 @@
       if (errorCodeAvailable and error.code is'authMissing') or
       (status is 'parsererror' and M.isAuthorisationErrorPage responseText) or
       (M.isAuthorisationErrorResponse status)
-        @gettingStarted params ? {}
+        @gettingStarted params
       else
         getMessage = ->
           if errorCodeAvailable


Fix webinar day error

Fix webinar day error


translate some new items in zh-HANS.coffee

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -10,7 +10,7 @@
     classroom_in_a_box: "教授计算机科学的一站式课堂。"
     codecombat_is: "CodeCombat是一个让学生通过玩游戏学习计算机科学的平台。"
     our_courses: "我们的游戏编程课程尤其适用于教室教学的需求。无编程经验的教师也可轻松授课。"
-#    watch_how: "Watch how CodeCombat is transforming the way people learn computer science."
+    watch_how:"看一看CodeCombat是怎样转变人们学习计算机的方式。"
     top_screenshots_hint: "学生编写代码后可即时查看改动。"
     designed_with: "从教育者角度设计"
     real_code: "真实可行的代码"
@@ -118,7 +118,7 @@

   play:
     play_as: "扮演" # Ladder page
-    compete: "开战!" # Course details page
+    compete: "开战！" # Course details page
     spectate: "旁观他人的游戏" # Ladder page
     players: "玩家" # Hover over a level on /play
     hours_played: "游戏时长" # Hover over a level on /play
@@ -138,10 +138,10 @@
     change_hero: "重新选择英雄" # Go back from choose inventory to choose hero
     buy_gems: "购买宝石"
     subscription_required: "需订阅"
-#    subscribers_only: "Subscribers Only!"
-#    subscribe_unlock: "Subscribe to Unlock!"
-#    subscriber_heroes: "Subscribe today to immediately unlock Amara, Hushbaum, and Hattori!"
-#    subscriber_gems: "Subscribe today to purchase this hero with gems!"
+    subscribers_only: "仅订阅者可用！"
+    subscribe_unlock: "订阅解锁！"
+    subscriber_heroes: "现在订阅，马上解锁Amara、Hushbaum和Hattori吧！"
+    subscriber_gems: "现在订阅，用宝石购买这些英雄吧！"
     anonymous: "匿名玩家"
     level_difficulty: "难度："
     play_classroom_version: "玩课堂模式" # Choose a level in campaign version that you also can play in one of your courses
@@ -152,10 +152,10 @@
     adjust_volume: "音量调节"
     campaign_multiplayer: "多人竞技场"
     campaign_multiplayer_description: "……在这里您可以与其他玩家进行代码肉搏战。"
-#    brain_pop_done: "You’ve defeated the Ogres with code! You win!"
-#    brain_pop_challenge: "Challenge yourself to play again using a different programming language!"
-#    replay: "Replay"
-#    back_to_classroom: "Back to Classroom"
+    brain_pop_done: "你用代码击败了巨魔！你赢了！"
+    brain_pop_challenge: "挑战自己，用不同的编程语言再玩一次！"
+    replay: "重玩"
+    back_to_classroom: "返回教室"

   code:
     if: "如果" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.)
@@ -293,18 +293,18 @@
     sso_connected: "成功与以下账号连接："
     select_your_starting_hero: "选择你的起始英雄："
     you_can_always_change_your_hero_later: "之后你可以随时更换英雄。"
-#    finish: "Finish"
+    finish: "完成"
 #    teacher_ready_to_create_class: "You're ready to create your first class!"
 #    teacher_students_can_start_now: "Your students will be able to start playing the first course, Introduction to Computer Science, immediately."
 #    teacher_list_create_class: "On the next screen you will be able to create a new class."
 #    teacher_list_add_students: "Add students to the class by clicking the View Class link, then sending your students the Class Code or URL. You can also invite them via email if they have email addresses."
 #    teacher_list_resource_hub_1: "Check out the"
-#    teacher_list_resource_hub_2: "Course Guides"
+    teacher_list_resource_hub_2: "课程指南"
 #    teacher_list_resource_hub_3: "for solutions to every level, and the"
 #    teacher_list_resource_hub_4: "Resource Hub"
 #    teacher_list_resource_hub_5: "for curriculum guides, activities, and more!"
-#    teacher_additional_questions: "That’s it! If you need additional help or have questions, reach out to __supportEmail__."
-#    dont_use_our_email_silly: "Don't put our email here! Put your parent's email."
+    teacher_additional_questions: "就是这样！如果您还需要额外的帮助或者遇到了什么问题，请联系__supportEmail__。"
+    dont_use_our_email_silly: "请不要在这里填写我们的邮箱地址！要填写你家长的邮箱地址。"

   recover:
     recover_account_title: "找回账户"
@@ -324,13 +324,13 @@
     go_back: "返回"
     coming_soon: "敬请期待！"
     continue: "继续" # When used as an action verb, like "Continue forward"
-#    next: "Next"
+    next: "下一个"
     default_code: "默认代码"
     loading: "读取中……"
     overview: "概览"
-#    processing: "Processing..."
+    processing: "进行中……"
     solution: "解法"
-#    table_of_contents: "Table of Contents"
+    table_of_contents: "目录"
     intro: "简介"
     saving: "保存中……"
     sending: "发送中……"
@@ -352,7 +352,7 @@
     submit_changes: "提交更新"
     save_changes: "保存更新"
     required_field: "必填" # {change}
-#    valid_phone: "Enter a valid phone number."
+    valid_phone: "输入有效的电话号码。"

   general:
     and: "与"
@@ -408,9 +408,9 @@
     last_name: "名"
     last_initial: "（姓）首大写字母"
     username: "用户名"
-#    contact_us: "Contact Us"
-#    close_window: "Close Window"
-#    learn_more: "Learn More"
+    contact_us: "联系我们"
+    close_window: "关闭窗口"
+    learn_more: "学习更多"

   units:
     second: "秒"
@@ -429,12 +429,12 @@
     years: "年"

   play_level:
-#    back_to_map: "Back to Map"
-#    directions: "Directions"
-#    edit_level: "Edit Level"
-#    explore_codecombat: "Explore CodeCombat"
+    back_to_map: "返回地图"
+    directions: "方向"
+    edit_level: "编辑关卡"
+    explore_codecombat: "探索CodeCombat"
 #    finished_hoc: "I'm finished with my Hour of Code"
-#    get_certificate: "Get your certificate!"
+    get_certificate: "获取你的证书！"
     level_complete: "关卡已完成"
     completed_level: "完成关卡:"
     course: "课程:"
@@ -452,7 +452,7 @@
     goals: "目标"
     goal: "目标"
     running: "运行中..."
-    success: "成功!"
+    success: "成功！"
     incomplete: "未完成"
     timed_out: "运行超时"
     failing: "失败"
@@ -460,7 +460,7 @@
     reload_title: "重载所有代码？"
     reload_really: "确定重载这一关，返回开始处吗？"
     reload_confirm: "重载所有"
-#    test_level: "Test Level"
+    test_level: "测试关卡"
     victory: "胜利"
     victory_title_prefix: ""
     victory_title_suffix: " 完成"
@@ -473,12 +473,12 @@
     victory_review: "给我们反馈！"
     victory_review_placeholder: "关卡如何?"
     victory_hour_of_code_done: "您完成了吗?"
-    victory_hour_of_code_done_yes: "是的, 完成了!"
+    victory_hour_of_code_done_yes: "是的, 完成了！"
     victory_experience_gained: "获得经验"
     victory_gems_gained: "获得宝石"
     victory_new_item: "新的物品"
     victory_new_hero: "新的英雄"
-    victory_viking_code_school: "这关真的超难! 如果您想成为一个软件开发人员，您就应该去试一下Viking Code School。在这里您可以把您的知识增长到另一个台阶。只需要14周您就能成为一个专业的网页开发人员。"
+    victory_viking_code_school: "这关真的超难！ 如果您想成为一个软件开发人员，您就应该去试一下Viking Code School。在这里您可以把您的知识增长到另一个台阶。只需要14周您就能成为一个专业的网页开发人员。"
     victory_become_a_viking: "成为一个维京人吧"
     victory_no_progress_for_teachers: "教师不能保存进度。但您可在您的班级为自己添加一个学生账号。"
     tome_cast_button_run: "运行"
@@ -579,13 +579,13 @@
 #    already_published_suffix: "Keep experimenting and making this project even better, or see what the rest of your class has built! Your changes will automatically be saved and shared with your classmates."
 #    view_gallery: "View Gallery"
 #    project_published_noty: "Your level has been published!"
-#    keep_editing: "Keep Editing"
+    keep_editing: "继续编辑"

   play_game_dev_level:
     created_by: "由 {{name}} 创建"
     restart: "重新开始关卡"
     play: "开始关卡"
-    play_more_codecombat: "玩更多 CodeCombat 关卡"
+    play_more_codecombat: "玩更多的 CodeCombat 关卡"
     default_student_instructions: "单击控制英雄，并取得胜利！"

   game_menu:
@@ -601,7 +601,7 @@
     choose_hero_caption: "选择英雄和语言"
     options_caption: "确认设置"
     guide_caption: "文档和提示"
-    multiplayer_caption: "与您的朋友一起玩!"
+    multiplayer_caption: "与您的朋友一起玩！"
     auth_caption: "保存进度"

   leaderboard:
@@ -611,7 +611,7 @@
     day: "今天"
     week: "这个星期"
     all: "实时"
-#    latest: "Latest"
+    latest: "最新的"
     time: "时间"
     damage_taken: "接收的伤害"
     damage_dealt: "造成的伤害"
@@ -669,7 +669,7 @@
     thank_you_months_prefix: "感谢您这"
     thank_you_months_suffix: "个月以来对我们的支持。"
     thank_you: "感谢您支持CodeCombat。"
-    sorry_to_see_you_go: "真舍不得亲离开啊! 请一定要让我们知道哪里做的还不够好。"
+    sorry_to_see_you_go: "真舍不得亲离开啊！ 请一定要让我们知道哪里做的还不够好。"
     unsubscribe_feedback_placeholder: "噢, 我们做了神马?"
     parent_button: "请求您的父母"
     parent_email_description: "我们会发送邮件给您的父母让他们帮您订阅CodeCombat。"
@@ -677,7 +677,7 @@
     parent_email_input_label: "父母邮件地址"
     parent_email_input_placeholder: "输入父母邮件地址"
     parent_email_send: "发送邮件"
-    parent_email_sent: "邮件已发送!"
+    parent_email_sent: "邮件已发送！"
     parent_email_title: "什么是您父母的邮件地址？"
     parents: "致家长"
     parents_title: "亲爱的家长: 您的孩子将要学习编程。您会支持孩子发展吗?"
@@ -715,24 +715,24 @@
     feature_games: "为你的朋友制作游戏"
     feature_websites: "构建网站和应用程序"
     feature_items: "装备更强大的物品"
-#    feature_learn: "Learn to make games and websites" # {change}
+    feature_learn: "学习制作游戏和网页" # {change}
     month_price: "$__price__/月" # {change}
     lifetime: "终身订阅"
-#    lifetime_price: "$__price__"
+    lifetime_price: "$__price__"
     year_subscription: "年度订阅"
     year_price: "$__price__/年"
-    kids_message_1: "孩子们！我们会发送一封电子邮件给您的父母，以便他们可以为您购买订阅."
+    kids_message_1: "孩子们！我们会发送一封电子邮件给您的父母，以便他们可以为您购买订阅。"
     kids_message_2: "联系你的父母"
     support_part1: "付款时需要帮助? Email"
     support_part2: "support@codecombat.com"
-    support_part3: "如果您有任何问题."
-#    you_are_purchasing_monthly_sub: "You're purchasing a Monthly Premium Subscription!"
-    you_are_purchasing_year_sub: "您正在购买年度高级订阅!"
-    you_are_purchasing_lifetime_sub: "您正在购买终身优惠订阅!"
-    you_will_be_charged: "您将被一次性收取 $__priceString__ ." # {change}
+    support_part3: "如果您有任何问题。"
+    you_are_purchasing_monthly_sub: "您正在购买月度高级订阅！"
+    you_are_purchasing_year_sub: "您正在购买年度高级订阅！"
+    you_are_purchasing_lifetime_sub: "您正在购买终身高级订阅！"
+    you_will_be_charged: "您将被一次性收取 $__priceString__ 。" # {change}
     choose_payment_method: "选择付款方式"
     pay_with_credit_card_or_bitcoin: "用信用卡/比特币支付"
-    paypal_payment_error: "PayPal收费时遇到错误."
+    paypal_payment_error: "PayPal收费时遇到错误。"

 #  announcement:
 #    now_available: "Now available for subscribers!"
@@ -778,17 +778,17 @@

   premium_features:
     get_premium: "获取
CodeCombat
高级订阅" # Fit into the banner on the /features page
-    master_coder: "通过订阅成为编程高手!"
+    master_coder: "通过订阅成为编程高手！"
 #    paypal_redirect: "You will be redirected to PayPal to complete the subscription process."
     subscribe_now: "现在订阅"
-    hero_blurb_1: "订阅后可使用 __premiumHeroesCount__ 个超级英雄,利用Okar Stompfoot不可阻挡的力量,叶之Naria的致命精度,或Nalfar Cryptor的\"可爱\"骷髅!"
+    hero_blurb_1: "订阅后可使用 __premiumHeroesCount__ 个超级英雄,利用Okar Stompfoot不可阻挡的力量,叶之Naria的致命精度,或Nalfar Cryptor的\"可爱\"骷髅！"
     hero_blurb_2: "高级战士可解锁惊人的武术技能，如Warcry、Stomp和Hurl Enemy。 或者玩游侠,使用隐形和弓箭、飞刀、陷阱！尝试用你的技能作为一个真正的编码教学，释放一个强大的原力，死灵或元素魔法阵列！"
-    hero_caption: "令人兴奋的新英雄!"
+    hero_caption: "令人兴奋的新英雄！"
 #    pet_blurb_1: "Pets aren't just adorable companions, they also provide unique functionality and methods. The Baby Griffon can carry units through the air, the Wolf Pup plays catch with enemy arrows, the Cougar is fond of chasing ogres around, and the Mimic attracts coins like a magnet!"
-    pet_blurb_2: "收集所有的宠物,发现它们独特的能力!"
-    pet_caption: "领养宠物陪伴你的英雄!"
-    game_dev_blurb: "学习游戏脚本并建立新的关卡,与您的朋友分享! 放置你想要的项目,编写单位逻辑和行为代码,看看你的朋友是否可以击败关卡!"
-    game_dev_caption: "设计自己的游戏来挑战你的朋友!"
+    pet_blurb_2: "收集所有的宠物,发现它们独特的能力！"
+    pet_caption: "领养宠物陪伴你的英雄！"
+    game_dev_blurb: "学习游戏脚本并建立新的关卡,与您的朋友分享！ 放置你想要的项目,编写单位逻辑和行为代码,看看你的朋友是否可以击败关卡！"
+    game_dev_caption: "设计自己的游戏来挑战你的朋友！"
     everything_in_premium: "您在CodeCombat高级订阅中获得的内容:"
     list_gems: "获得奖励的宝石购买装备、宠物和英雄"
     list_levels: "获得超过 __premiumLevelsCount__ 个关卡"
@@ -919,12 +919,12 @@
     lisa_title: "市场开发代表" # {change}
     lisa_blurb: "坚韧不拔的人"
     sean_title: "领土经理"
-#    liz_title: "Territory Manager"
+    liz_title: "领土经理"
     retrostyle_title: "插画师"
     retrostyle_blurb: "复古风格的游戏"
     jose_title: "音乐"
     jose_blurb: "放轻松"
-#    bryukh_title: "Game Designer"
+    bryukh_title: "游戏设计师"
 #    bryukh_blurb: "Constructs puzzles"
     community_title: "...以及我们的开源社区"
     community_subtitle: "超过 450 个志愿者帮助我们建设 CodeCombat，每周都有新成员加入！" # {change}
@@ -1039,10 +1039,10 @@
     approve_funds: "批准资金"
     no_purchaser_role: "不参与购买决策"
     district_label: "地区"
-#    district_name: "District Name"
+    district_name: "地区名称"
     district_na: "不可用时请输入 N/A"
     organization_label: "学校："
-#    school_name: "School Name"
+    school_name: "学校名称"
     city: "城市"
     state: "州"
     country: "国家"
@@ -1055,7 +1055,7 @@
     please_explain: "(请注明)"
     middle_school: "初中"
     college_plus: "大学以上"
-#    referrer: "How did you hear about us?"
+    referrer: "您是怎么知道我们的？"
 #    referrer_help: "For example: from another teacher, a conference, your students, Code.org, etc."
     anything_else: "您还有什么建议？"
     thanks_header: "申请已发送！"
@@ -1265,9 +1265,9 @@
     change_language: "修改课程语言"
     keep_using: "继续使用"
     switch_to: "切换到"
-    greetings: "欢迎!"
+    greetings: "欢迎！"
     back_classrooms: "回到我的教室"
-#    back_classroom: "Back to classroom"
+    back_classroom: "返回教室"
     back_courses: "回到我的课程"
     edit_details: "编辑课程详情"
     purchase_enrollments: "购买学生许可"
@@ -1278,10 +1278,10 @@
     teacher: "老师"
     arena: "竞技场"
     available_levels: "可用关卡"
-#    started: "started"
-#    complete: "complete"
-#    practice: "practice"
-#    required: "required"
+    started: "开始"
+    complete: "完成"
+    practice: "练习"
+    required: "必需的"
     welcome_to_courses: "探险者，欢迎来到课程中！"
     ready_to_play: "准备玩了吗？"
     start_new_game: "开始新游戏"
@@ -1294,7 +1294,7 @@
     my_classes: "我的课程" # {change}
     class_added: "成功添加课程！"
     view_levels: "浏览关卡" # {change}
-#    view_project_gallery: "view my classmates' projects"
+    view_project_gallery: "浏览同学的项目"
     join_class: "加入课程"
     join_class_2: "加入班级"
     ask_teacher_for_code: "从你的老师领取CodeCombat课程码！领取后请在下方输入："
@@ -1366,7 +1366,7 @@
     update_account_students_can3: "与同学一决高下"
     update_account_students_can4: "推出学生专用功能时获得使用权"
     update_account_students_warning: "警告：你将不能再创建新班级或管理已创建的班级"
-#    unsubscribe_warning: "Warning: You will be unsubscribed from your monthly subscription."
+    unsubscribe_warning: "警告：您的月度订阅将被取消。"
     update_account_remain_student: "保持学生身份"
     update_account_update_student: "升级为学生账号"
     need_a_class_code: "您需要拥有以下班级的班级码："
@@ -1397,11 +1397,11 @@
 #    edit_project: "Edit Project"

   teacher:
-#    assigning_course: "Assigning course"
+    assigning_course: "分配课程"
     course_solution: "关卡解法"
     level_overview_solutions: "关卡概览和解法"
-#    no_student_assigned: "No students have been assigned this course."
-#    paren_new: "(new)"
+    no_student_assigned: "没有学生被分配到这个课程。"
+    paren_new: "(新)"
     teacher_dashboard: "教师面板" # Navbar
     my_classes: "我的教室"
     courses: "课程指导"
@@ -1423,7 +1423,7 @@
     unarchive_class: "解档课堂"
     unarchive_this_class: "解档本课堂"
     no_students_yet: "该课堂尚未有学生加入。"
-    no_students_yet_view_class: "查看课程并添加学生."
+    no_students_yet_view_class: "查看课程并添加学生。"
     try_refreshing: "(您可能需要刷新页面)"
     create_new_class: "创建新课堂"
     class_overview: "课堂概览" # View Class page
@@ -1447,11 +1447,11 @@
     sort_by: "排列依据"
     progress: "进度"
     completed: "已完成"
-#    practice: "Practice"
+    practice: "练习"
     started: "已开始"
     click_to_view_progress: "点击查看进度"
     no_progress: "无进度"
-#    not_required: "Not required"
+    not_required: "不需要"
     select_course: "选择查看"
     progress_color_key: "进度颜色指示："
     level_in_progress: "正在通关"
@@ -1465,9 +1465,9 @@
     class_join_url_blurb: "新学生可以在登录之后，访问该网址加入教室。" # {change}
     add_students_manually: "手工添加学生" # {change}
     bulk_assign: "批量添加"
-    assigned_msg_1: "{{numberAssigned}} 学生被分配 {{courseName}}."
-    assigned_msg_2: "{{numberEnrolled}} 被应用许可证."
-    assigned_msg_3: "你现在还有 {{remainingSpots}} 个可用的许可."
+    assigned_msg_1: "{{numberAssigned}} 学生被分配 {{courseName}}。"
+    assigned_msg_2: "{{numberEnrolled}} 被应用许可证。"
+    assigned_msg_3: "你现在还有 {{remainingSpots}} 个可用的许可。"
     assign_course: "分配课程"
     not_assigned_modal_title: "课程未分配"
 #    not_assigned_modal_starter_body_1: "This course requires a Starter License. You do not have enough Starter Licenses available to assign this course to all __selected__ selected students."
@@ -1485,7 +1485,7 @@
     all_students: "所有学生"
     apply_licenses: "应用许可"
     not_enough_enrollments: "没有足够许可注册。"
-    enrollments_blurb: "学生需要一个许可，以便在第一课程后访问内容."
+    enrollments_blurb: "学生需要一个许可，以便在第一课程后访问内容。"
     how_to_apply_licenses: "如何应用许可"
     export_student_progress: "导出学生进度 (CSV)"
     send_email_to: "将重置密码邮件发送到以下邮箱："
@@ -1507,7 +1507,7 @@
     get_enrollments_blurb: " 我们会帮您解决您的班级、学校或地区的需求。"
     how_to_apply_licenses_blurb_1: "当教师第一次将课程分配给学生时，我们将自动应用许可证。 使用教室中的批量分配下拉菜单为所选学生分配课程：:"
     how_to_apply_licenses_blurb_2: "我可以只申请许可而不分配课程吗?"
-    how_to_apply_licenses_blurb_3: "是的 — 转到课堂中的\"许可状态\"选项卡，然后单击 \"应用许可\"到没有被分配许可的学生."
+    how_to_apply_licenses_blurb_3: "是的 — 转到课堂中的\"许可状态\"选项卡，然后单击 \"应用许可\"到没有被分配许可的学生。"
     request_sent: "申请已发送！"
     license_status: "许可状态"
     status_expired: "在 {{date}} 过期"
@@ -1516,48 +1516,48 @@
     select_all: "全选"
     project: "项目"
 #    project_gallery: "Project Gallery"
-#    view_project: "View Project"
-#    unpublished: "(unpublished)"
+    view_project: "查看项目"
+    unpublished: "(未发布的)"
     view_arena_ladder: "查看竞技场天梯"
     resource_hub: "资源中心"
     getting_started: "入门"
     educator_faq: "常见问题"
-    educator_faq_desc: "在您的学校或教室使用 CodeCombat 的常见问题."
+    educator_faq_desc: "在您的学校或教室使用 CodeCombat 的常见问题。"
     teacher_getting_started: "教师入门指南"
     teacher_getting_started_desc: "第一次使用 CodeCombat ？下载此教师入门指南开始设置您的账号，创建第一个课堂并邀请学生参加第一门课程。"
     student_getting_started: "学生快速入门指南"
-    student_getting_started_desc: "您可以在讲解 CodeCombat 之前先将此入门指南发布给学生，以便他们熟悉代码编辑器。本指南可用于 Python 和 JavaScript 语言教室."
+    student_getting_started_desc: "您可以在讲解 CodeCombat 之前先将此入门指南发布给学生，以便他们熟悉代码编辑器。本指南可用于 Python 和 JavaScript 语言教室。"
     cs1: "计算机科学的介绍"
     cs2: "计算机科学 2"
     cs3: "计算机科学 3"
     cs4: "计算机科学 4"
     cs5: "计算机科学 5"
     cs1_syntax_python: "课程 1 Python 语法指南"
-    cs1_syntax_python_desc: "参考常见的 Python 语法，学生将学习计算科学的介绍."
+    cs1_syntax_python_desc: "参考常见的 Python 语法，学生将学习计算科学的介绍。"
     cs1_syntax_javascript: "课程 1 JavaScript 语法指南"
-    cs1_syntax_javascript_desc: "参考常见的 JavaScript 语法，学生将学习计算科学的介绍."
-    coming_soon: "更多指南即将推出!"
+    cs1_syntax_javascript_desc: "参考常见的 JavaScript 语法，学生将学习计算科学的介绍。"
+    coming_soon: "更多指南即将推出！"
     engineering_cycle_worksheet: "培训周期表"
-    engineering_cycle_worksheet_desc: "基于此表来管理学生的培训周期、评估、计划、进度和调试。请参考完整的示例表作为指南."
+    engineering_cycle_worksheet_desc: "基于此表来管理学生的培训周期、评估、计划、进度和调试。请参考完整的示例表作为指南。"
     engineering_cycle_worksheet_link: "查看示例"
     progress_journal: "学习进度"
-    progress_journal_desc: "鼓励学生通过进度日志跟踪他们的进步."
+    progress_journal_desc: "鼓励学生通过进度日志跟踪他们的进步。"
     cs1_curriculum: "计算机科学的介绍 - 课程指南"
-    cs1_curriculum_desc: "课程 1 的范围、顺序、课程计划和活动等."
+    cs1_curriculum_desc: "课程 1 的范围、顺序、课程计划和活动等。"
     arenas_curriculum: "竞技场 － 教师指导"
 #    arenas_curriculum_desc: "Instructions on how to run Wakka Maul, Cross Bones and Power Peak multiplayer arenas with your class."
     cs2_curriculum: "计算机科学 2 - 课程指南"
-    cs2_curriculum_desc: "课程 2 的范围、顺序、课程计划和活动等."
+    cs2_curriculum_desc: "课程 2 的范围、顺序、课程计划和活动等。"
     cs3_curriculum: "计算机科学 3 - 课程指南"
-    cs3_curriculum_desc: "课程 3 的范围、顺序、课程计划和活动等."
+    cs3_curriculum_desc: "课程 3 的范围、顺序、课程计划和活动等。"
     cs4_curriculum: "计算机科学 4 - 课程指南"
-    cs4_curriculum_desc: "课程 4 的范围、顺序、课程计划和活动等."
+    cs4_curriculum_desc: "课程 4 的范围、顺序、课程计划和活动等。"
 #    cs5_curriculum_js: "Computer Science 5 - Curriculum Guide (JavaScript)"
 #    cs5_curriculum_desc_js: "Scope and sequence, lesson plans, activities and more for Course 5 classes using JavaScript."
 #    cs5_curriculum_py: "Computer Science 5 - Curriculum Guide (Python)"
 #    cs5_curriculum_desc_py: "Scope and sequence, lesson plans, activities and more for Course 5 classes using Python."
     cs1_pairprogramming: "组队编程活动"
-    cs1_pairprogramming_desc: "让两个学生一起组队编程练习，这将有助于他们互相更好的学习与沟通."
+    cs1_pairprogramming_desc: "让两个学生一起组队编程练习，这将有助于他们互相更好的学习与沟通。"
 #    gd1: "Game Development 1"
 #    gd1_guide: "Game Development 1 - Project Guide"
 #    gd1_guide_desc: "Use this to guide your students as they create their first shareable game project in 5 days."
@@ -1576,11 +1576,11 @@
 #    wd2_quizlet_worksheet_instructions: "View instructions & examples"
 #    wd2_quizlet_worksheet_desc: "Before your students build their personality quiz project at the end of Web Development 2, they should plan out their quiz questions, outcomes and responses using this worksheet. Teachers can distribute the instructions and examples for students to refer to."
     student_overview: "概述"
-#    student_details: "Student Details"
+    student_details: "学生详情"
     student_name: "学生姓名"
     no_name: "未提供姓名。"
     no_username: "未提供用户名。"
-    no_email: "学生没有设置电子邮件地址."
+    no_email: "学生没有设置电子邮件地址。"
     student_profile: "学生档案"
     playtime_detail: "游戏时间详情"
     student_completed: "学生已完成"
@@ -1592,11 +1592,11 @@
     student_state: "看看"
     student_state_2: "的情况?"
     student_good: "表现得很好，课程"
-    student_good_detail: "这个学生的进度达到了班级的平均进度."
+    student_good_detail: "这个学生的进度达到了班级的平均进度。"
     student_warn: "可能需要一些帮助"
-    student_warn_detail: "这个学生可能需要一些帮助，在本课程中介绍的新概念."
+    student_warn_detail: "这个学生可能需要一些帮助，在本课程中介绍的新概念。"
     student_great: "做得太好了，关卡"
-    student_great_detail: "这个学生可能是一个很好的候选人，帮助其他学生通过这门课程."
+    student_great_detail: "这个学生可能是一个很好的候选人，帮助其他学生通过这门课程。"
     full_license: "全课许可"
 #    starter_license: "Starter License"
 #    trial: "Trial"
@@ -1633,13 +1633,13 @@
     quests_complete: "任务完成"
     teacher_quest_create_classroom: "添加课堂"
     teacher_quest_add_students: "添加学生"
-    teacher_quest_teach_methods: "帮助你的学生学习如何 `调用方法`."
+    teacher_quest_teach_methods: "帮助你的学生学习如何 `调用方法`。"
     teacher_quest_teach_methods_step1: "班级中至少有 75% 的学生通过该关卡, __Dungeons of Kithgard__"
-    teacher_quest_teach_methods_step2: "打印资源中心的 [学生快速入门指南](http://files.codecombat.com/docs/resources/StudentQuickStartGuide.pdf) ."
-    teacher_quest_teach_strings: "不要让你的学生跟着走, 教他们 `strings`."
+    teacher_quest_teach_methods_step2: "打印资源中心的 [学生快速入门指南](http://files.codecombat.com/docs/resources/StudentQuickStartGuide.pdf) 。"
+    teacher_quest_teach_strings: "不要让你的学生跟着走, 教他们 `strings`。"
     teacher_quest_teach_strings_step1: "班级中至少有 75% 的学生通过该关卡 __True Names__"
-    teacher_quest_teach_strings_step2: "使用 [课程指导](/teachers/courses) 页面上的教师关卡选择器预览 __True Names__."
-    teacher_quest_teach_loops: "让你的学生在循环中学习 `loops`."
+    teacher_quest_teach_strings_step2: "使用 [课程指导](/teachers/courses) 页面上的教师关卡选择器预览 __True Names__。"
+    teacher_quest_teach_loops: "让你的学生在循环中学习 `loops`。"
 #    teacher_quest_teach_loops_step1: "Get 75% of at least one class through __Fire Dancing__."
 #    teacher_quest_teach_loops_step2: "Use the __Loops Activity__ in the [CS1 Curriculum guide](/teachers/resources/cs1) to reinforce this concept."
 #    teacher_quest_teach_variables: "Vary it up with `variables`."
@@ -1654,7 +1654,7 @@
 #    teacher_quest_reach_gamedev: "Explore new worlds!"
 #    teacher_quest_reach_gamedev_step1: "[Get licenses](/teachers/licenses) so that your students can explore new worlds, like Game Development and Web Development!"
 #    teacher_quest_done: "Want your students to learn even more code? Get in touch with our [school specialists](mailto:schools@codecombat.com) today!"
-    teacher_quest_keep_going: "继续! 这是你下一步可以做的:"
+    teacher_quest_keep_going: "继续！ 这是你下一步可以做的:"
     teacher_quest_more: "查看所有任务"
     teacher_quest_less: "查看基础任务"
     refresh_to_update: "(刷新页面查看更新)"
@@ -1679,8 +1679,8 @@
   sharing:
     game: "游戏"
     webpage: "网页"
-    your_students_preview: "您的学生可以点击这里查看他们的完成项目，在老师预览中不可用."
-    unavailable: "教师预览中不提供链接共享."
+    your_students_preview: "您的学生可以点击这里查看他们的完成项目，在老师预览中不可用。"
+    unavailable: "教师预览中不提供链接共享。"
     share_game: "分享此游戏"
     share_web: "分享此网页"
     victory_share_prefix: "将这个链接分享给亲朋好友，让他们"
@@ -1701,9 +1701,9 @@
     select_an_image: "选择您要使用的图片"
     scroll_down_for_more_images: "(向下滚动查看更多图片)"
     copy_the_url: "复制以下网址"
-    copy_the_url_description: "如果您想要替换现有的图片，这很有用."
+    copy_the_url_description: "如果您想要替换现有的图片，这很有用。"
     copy_the_img_tag: "复制  标签"
-    copy_the_img_tag_description: "如果您想要插入新图片，这很有用."
+    copy_the_img_tag_description: "如果您想要插入新图片，这很有用。"
     copy_url: "复制网址"
     copy_img: "复制 "
     how_to_copy_paste: "如何复制/粘贴"
@@ -1843,7 +1843,7 @@
     join_url_email: "给我们发邮件"
     join_url_slack: "公共 Slack 频道"
     archmage_subscribe_desc: "通过电子邮件获得新的编码机会和公告。"
-    artisan_introduction_pref: "我们必须设计更多的关卡! 大家为了更多的游戏内容在高声呐喊，但是我们靠自己只能创建这些。现在您的电脑就是第一关！我们的关卡编辑器刚刚完成了基本功能，所以创造关卡的时候请小心使用。只要您有制作关卡的灵感，不管是简单的for循环还是"
+    artisan_introduction_pref: "我们必须设计更多的关卡！ 大家为了更多的游戏内容在高声呐喊，但是我们靠自己只能创建这些。现在您的电脑就是第一关！我们的关卡编辑器刚刚完成了基本功能，所以创造关卡的时候请小心使用。只要您有制作关卡的灵感，不管是简单的for循环还是"
     artisan_introduction_suf: "这种东西，这个职业都很适合您。"
     artisan_attribute_1: "最好有创建地图的经历，比如使用暴雪的关卡编辑器，但这不是必须的条件。"
     artisan_attribute_2: "渴望完成全部的测试和迭代。要做一个优秀的关卡，您需要把它给别的玩家观察他们怎么玩，然后时刻准备着找到很多细节去打磨。"
@@ -1859,14 +1859,14 @@
     adventurer_attribute_2: "魅力！温柔但懂得表达事态；了解并知道如何改进。"
     adventurer_join_pref: "无论是与工匠一起工作，或者只是在下面的格子打勾以获得新关卡测试的通知，我们都会发布关于对新关卡的看法。"
     adventurer_forum_url: "我们的论坛"
-    adventurer_join_suf: "如果您更喜欢以这些方式被通知, 那就在这里注册吧!"
+    adventurer_join_suf: "如果您更喜欢以这些方式被通知, 那就在这里注册吧！"
     adventurer_subscribe_desc: "通过电子邮件获得新关卡通知。"
     scribe_introduction_pref: "CodeCombat不只是拥有一堆等级或关卡。它也是个知识的源泉，一个编程的宝典，一个人人都可以参与的，有趣的游戏。比起每个工匠都要详细解释其中的奥秘，他们可以以更简单的方式将自己的东西连接到一篇已经准备好的文章给玩家阅读。 有些东西就像 "
     scribe_introduction_url_mozilla: "Mozilla开发者社区"
     scribe_introduction_suf: " 被建成。如果您那有趣的想法能把编程的理论简易的带出来，那么这里就是您的所属了。"
     scribe_attribute_1: "您需要具备在文字语言上的技能。不仅仅是语法和拼音，重要的是能传达各种各样想法给别人。"
     contact_us_url: "联系我们"
-    scribe_join_description: "介绍下您自己, 比如您的编程经历和您喜欢的东西, 我们将会努力的去了解您！!"
+    scribe_join_description: "介绍下您自己, 比如您的编程经历和您喜欢的东西, 我们将会努力的去了解您！！"
     scribe_subscribe_desc: "通过电子邮件获得写作新文档的通知。"
     diplomat_introduction_pref: "如果说我们从"
     diplomat_launch_url: "十月的发布"
@@ -1924,7 +1924,7 @@
     no_ranked_matches_pre: "没有排名对决给 "
     no_ranked_matches_post: " 啊哈哟！去和其他玩家打过再回来这里查看您游戏排名吧！"
     choose_opponent: "选择一个对手"
-    select_your_language: "选择您使用的语言!"
+    select_your_language: "选择您使用的语言！"
     tutorial_play: "玩教程"
     tutorial_recommended: "如果您从未玩过的话,推荐先玩下教程"
     tutorial_skip: "跳过教材"
@@ -1945,8 +1945,8 @@
     tournament_ended: "锦标赛已结束"
     tournament_rules: "锦标赛规则"
     tournament_blurb: "写代码，收金币，建军队，碎敌军，赢奖品，以及在我们奖励多达$40,000的Greed tournament里升级您的事业！ 快去查阅详情！"
-    tournament_blurb_criss_cross: "赢得竞拍，建造道路，智胜对手，夺取宝石，在纵横交错锦标赛中完成生涯晋级! 现在就查看详情！"
-    tournament_blurb_zero_sum: "在红方法师和蓝方法师的山峰对决中, 同时在收集金币和战术上发挥你的代码创造力。竞赛在3月27日开始, 在4月6日(星期一)的下午5点(PDT 太平洋时区)结束。为乐趣和荣耀竞赛吧! 浏览了解更多"
+    tournament_blurb_criss_cross: "赢得竞拍，建造道路，智胜对手，夺取宝石，在纵横交错锦标赛中完成生涯晋级！ 现在就查看详情！"
+    tournament_blurb_zero_sum: "在红方法师和蓝方法师的山峰对决中, 同时在收集金币和战术上发挥你的代码创造力。竞赛在3月27日开始, 在4月6日(星期一)的下午5点(PDT 太平洋时区)结束。为乐趣和荣耀竞赛吧！ 浏览了解更多"
     tournament_blurb_ace_of_coders: "我的主场我主宰，在天寒地冻的冰原上和自己的镜像大战一场吧！本次比赛将由9月16日星期三开办到10月14日下午5点PDT。详情请查看"
     tournament_blurb_blog: "关注我们的博客"
     rules: "规则"
@@ -2029,7 +2029,7 @@
     purchase_code1: "订阅码可以为一个或多个CodeCombat账号兑换额外的订阅时间。" # {change}
     purchase_code2: "每个CodeCombat账号每次只能兑换一个订阅码。"
     purchase_code3: "订阅码时间会在账号现有的订阅时间基础上延长。"
-    purchase_code4: "订阅码适用于玩独立版CodeCombat的帐户，不能代替“课堂”版本的学生许可."
+    purchase_code4: "订阅码适用于玩独立版CodeCombat的帐户，不能代替“课堂”版本的学生许可。"
     purchase_code5: "有关学生许可的更多信息, 请联系"
     users: "玩家"
     months: "月份"
@@ -2125,7 +2125,7 @@
 #    for_loops_nested: "Nested For Loops"
 #    for_loops_range: "For Loops Range"
     functions: "函数"
-#    game_ai: "Game AI"
+    game_ai: "游戏AI"
 #    game_goals: "Game Goals"
 #    game_spawn: "Game Spawn"
     graphics: "图形"
@@ -2265,7 +2265,7 @@
     blurb_1: "这些奖项将会奖励，依据"
     blurb_2: "锦标赛规则"
     blurb_3: ", 给顶尖的人类和怪物玩家。"
-    blurb_4: "两队表示获得的奖励将会是两倍!"
+    blurb_4: "两队表示获得的奖励将会是两倍！"
     blurb_5: "(将有两个第一名, 两个第二名, 以此类推。)"
     rank: "排名"
     prizes: "奖项"


Add favorites scan

Add favorites scan

--- app/assets/javascripts/home.coffee
+++ app/assets/javascripts/home.coffee
@@ -15,6 +15,12 @@
     else
       oops()

+updateFavorites = (ids) ->
+  $favorited = $('.ps-photo').filter () ->
+    ids.indexOf($(this).data('id')) > -1
+
+  $favorited.find('.favorited').show()
+

 # Taken from https://github.com/500px/500px-js-sdk/blob/master/examples/example1.html
 setup500px = () ->
@@ -29,6 +35,15 @@
     _500px.api '/users', (response) ->
       me = response.data.user;
       $('#username').html(me.username)
+
+      _500px.api '/photos', { feature: 'user_favorites', user_id: me.id }, (response) ->
+        photos = response.data.photos
+        if (photos.length != 0)
+          ids = $.map photos, (photo) ->
+            photo.id
+
+          updateFavorites(ids)
+

   _500px.on 'logout', () ->
     $('.not_logged_in').show()


Add tofix (REBASE!)

Add tofix (REBASE!)

--- app/assets/javascripts/live.coffee
+++ app/assets/javascripts/live.coffee
@@ -107,6 +107,8 @@
   set_play off
   actual = (new Date().getTime()) - host.ticker_start
   host.ticker_over = actual - host.ticker_elapsed
+  # TODO: Fix: If quickly paused and then removed again, the old ticker "thread"
+  # stays alive. Kill it when pausing!
 # ------------

 # --- Load. Executed when the page is loaded (via Tubolinks or otherwise) ---