Login: resize handler and transtion handler removed

--- client/Login/AppView.coffee
+++ client/Login/AppView.coffee
@@ -126,26 +126,20 @@

   viewAppended:->

-    @setY -KD.getSingleton('windowController').winHeight
-    @listenWindowResize()
     @setClass "login-screen login"

     @setTemplate @pistachio()
     @template.update()

-  _windowDidResize:->
-    if @hidden
-      @setY -KD.getSingleton('windowController').winHeight
-
   pistachio:->
+      # {{> @loginOptions}}
+      # {{> @registerOptions}}
     """

         beta
         {{> @logo}}

-      {{> @loginOptions}}
-      {{> @registerOptions}}

         {{> @loginForm}}


Refactor javascript from feedback

Refactor javascript from feedback

--- app/assets/javascripts/spree/backend/product_properties.js.coffee
+++ app/assets/javascripts/spree/backend/product_properties.js.coffee
@@ -49,7 +49,7 @@
     @properties = []

     categoryEditorTemplate = _.template($("#category-editor-template").html())
-    @node = $("

").html(categoryEditorTemplate(category_editor: this))
+    @node = $("
").html(categoryEditorTemplate(category_editor: this))
     @parent.append @node

     @delete_button = @node.find '.js-delete-category'
@@ -92,7 +92,7 @@
 class ProductPropertyEditor
   constructor: (@category_editor, @parent, @property) ->
     propertyEditorTemplate = _.template($("#property-editor-template").html())
-    @node = $("

").html(propertyEditorTemplate(property_editor: this))
+    @node = $("
").html(propertyEditorTemplate(property_editor: this))
     @parent.append @node

     @delete_button = @node.find '.js-delete-property'
@@ -116,12 +116,13 @@
     helper: (event, ui) ->
       ui.children().each ->
         $(this).width($(this).width())
-      return ui
+      ui
     update: ->
       rows = $(this).find("tr")
-      $.each rows, (index, row) =>
-        if $(row).find(".js-position").length > 0
-          $(row).find(".js-position").attr("value", index)
+      $.each rows, (index, row) ->
+        position = $(row).find(".js-position")
+        if position.length > 0
+          position.attr("value", index)
         else
           input = $("<input />").attr
             type: "hidden",
@@ -133,4 +134,4 @@
     placeholder: 'ui-sortable-placeholder',
     start: (event, ui) ->
       ui.placeholder.height(ui.item.height())
-      ui.placeholder.html "		"
+      ui.placeholder.html "		"


client: make sure groups controller has the revived group

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/index.coffee
+++ client/app/lib/index.coffee
@@ -48,6 +48,7 @@
     globals.currentGroup           = remote.revive globals.currentGroup
     globals.userAccount            = remote.revive globals.userAccount
     globals.config.entryPoint.slug = globals.currentGroup.slug
+    kd.singletons.groupsController.currentGroupData.setGroup globals.currentGroup

     setupAnalytics()  unless globals.config.environment is 'default'


Keybindings for (more|less)-context commands

--- keymaps/git.cson
+++ keymaps/git.cson
@@ -132,3 +132,13 @@
   'end': 'github:co-author:end'
   'delete': 'github:co-author:delete'
   'shift-backspace': 'github:co-author-exclude'
+
+'.platform-darwin .github-Reviews':
+  'cmd-=': 'github:more-context'
+  'cmd--': 'github:less-context'
+'.platform-win32 .github-Reviews':
+  'ctrl-=': 'github:more-context'
+  'ctrl--': 'github:less-context'
+'.platform-linux .github-Reviews':
+  'ctrl-=': 'github:more-context'
+  'ctrl--': 'github:less-context'


IDE: use klient.storage instead localStorage [completes #92331726]

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -110,8 +110,6 @@

       @resizeActiveTerminalPane()

-    @localStorageController = kd.getSingleton('localStorageController').storage 'IDE'
-

   prepareIDE: (withFakeViews = no) ->

@@ -357,11 +355,10 @@
           @fakeEditor.once 'EditorIsReady', => kd.utils.defer => @fakeEditor.setFocus no

         else
-          snapshot = @localStorageController.getValue @getWorkspaceSnapshotName()
-
-          if snapshot
-            @resurrectLocalSnapshot snapshot
-          else
+
+          @fetchSnapshot (snapshot)=>
+            return @resurrectLocalSnapshot snapshot  if snapshot
+
             @ideViews.first.createEditor()
             @ideViews.last.createTerminal { machine }
             @setActiveTabView @ideViews.first.tabView
@@ -696,17 +693,17 @@
     name  = @getWorkspaceSnapshotName()
     value = @getWorkspaceSnapshot()

-    @localStorageController.setValue name, value
+    @mountedMachine.getBaseKite().storageSetSynced name, value

   removeWorkspaceSnapshot: ->

-    @localStorageController.unsetKey @getWorkspaceSnapshotName()
+    @mountedMachine.getBaseKite().storageDelete @getWorkspaceSnapshotName()

   getWorkspaceSnapshotName: ->

-    return "wss.#{@mountedMachine.uid}.#{@workspaceData.slug}"
+    return "wss.#{@workspaceData.slug}"

   registerPane: (pane) ->
@@ -933,22 +930,22 @@
     else
       finderController.reset()

-    snapshot = @localStorageController.getValue @getWorkspaceSnapshotName()
-
     machine.getBaseKite().fetchTerminalSessions()

-    unless @fakeViewsDestroyed
-      @removeFakeViews()
-      @fakeViewsDestroyed = yes
-
-    if snapshot
-      @resurrectLocalSnapshot snapshot  unless @isLocalSnapshotRestored
-    else
-      @addInitialViews()
-
-    data = { machine, workspace: @workspaceData }
-    kd.singletons.mainView.activitySidebar.selectWorkspace data
-    @emit 'IDEReady'
+    @fetchSnapshot (snapshot) =>
+
+      unless @fakeViewsDestroyed
+        @removeFakeViews()
+        @fakeViewsDestroyed = yes
+
+      if snapshot
+        @resurrectLocalSnapshot snapshot  unless @isLocalSnapshotRestored
+      else
+        @addInitialViews()
+
+      data = { machine, workspace: @workspaceData }
+      kd.singletons.mainView.activitySidebar.selectWorkspace data
+      @emit 'IDEReady'

   removeFakeViews: ->
@@ -969,12 +966,16 @@

   resurrectLocalSnapshot: (snapshot) ->

-    snapshot or= @localStorageController.getValue @getWorkspaceSnapshotName()
-
-    for key, value of snapshot when value
-      @createPaneFromChange value, yes
-
-    @isLocalSnapshotRestored = yes
+    resurrect = (snapshot)=>
+      for key, value of snapshot when value
+        @createPaneFromChange value, yes
+
+      @isLocalSnapshotRestored = yes
+
+    return  if snapshot then resurrect snapshot
+
+    @fetchSnapshot (snapshot)->
+      resurrect snapshot  if snapshot

   toggleFullscreenIDEView: ->
@@ -1355,3 +1356,17 @@
     @machineStateModal?.once 'MachineTurnOnStarted', ->
       kookies.expire 'newRegister', path: '/'
       kd.getSingleton('mainView').activitySidebar.initiateFakeCounter()
+
+
+  fetchSnapshot: (callback)->
+
+    if not @mountedMachine or not @mountedMachine.isRunning()
+      callback null
+      return
+
+    @mountedMachine.getBaseKite().storageGet @getWorkspaceSnapshotName()
+    .then (snapshot)->
+      callback snapshot
+    .catch (err)->
+      console.warn 'Failed to fetch snapshot', err
+      callback null


Added golang

Added golang

--- client/lib/languages.coffee
+++ client/lib/languages.coffee
@@ -34,6 +34,12 @@
         informatics: 18,
         ejudge: 18
         codeforces: 36
+    },
+    "GNU Go": {
+        extensions: ["go"],
+        informatics: 53
+        ejudge: 53
+        codeforces: 32
     },
     "PHP": {
         extensions: ["php"],


Add scroll

Add scroll

--- assets/javascripts/app.coffee
+++ assets/javascripts/app.coffee
@@ -4,6 +4,7 @@
 win = document.body
 hero = d3.select("#hero")[0][0]
 nav = d3.select("#nav-sticky")
+staticNav = d3.select("#nav")
 navList = d3.selectAll(".nav-list")
 sticky = false

@@ -97,3 +98,14 @@
   .attr("d", arc)

 d3.select(".days").text(Math.floor((end - now) / (24 * 60 * 60 * 1000)))
+
+d3.select("#scroll-button").on "click", ->
+  navPos = getOffsetTop(d3.select("#nav")[0][0])
+  scrollTo(navPos)
+
+scrollTo = (pos) ->
+  if win.scrollTop < pos
+    document.body.scrollTop += 20
+    setTimeout ->
+      scrollTo(pos)
+    , 5


Remove bogus Tutorial link

Remove bogus Tutorial link


Add options for nested forms to do not show new/remove buttons.

--- app/assets/javascripts/formagic/inputs/documents.coffee
+++ app/assets/javascripts/formagic/inputs/documents.coffee
@@ -29,7 +29,7 @@
     @forms = []

     @config.namePrefix         ||= name
-    @config.removeButton         = true
+    @config.removeButton         = if @config.disableRemoveDocuments then false else true
     @config.ignoreOnSubmission ||= false

     @config.formSchema._id = { type: 'hidden', name: 'id', ignoreOnSubmission: @config.ignoreOnSubmission }
@@ -99,10 +99,13 @@

   _add_new_button: ->
-    label = @config.newButtonLabel || "Add"
-    @$newButton =$ """#{ label }"""
-    @$el.append @$newButton
-    @$newButton.on 'click', (e) => e.preventDefault() ; @addNewForm()
+    if ! @config.disableNewDocuments
+      label = @config.newButtonLabel || "Add"
+
+      @$newButton =$ """#{ label }"""
+      @$el.append @$newButton
+
+      @$newButton.on 'click', (e) => e.preventDefault() ; @addNewForm()

   # PUBLIC ================================================


Use text() over html() when rendering select options

--- src/coffee/cilantro/ui/controls/select.coffee
+++ src/coffee/cilantro/ui/controls/select.coffee
@@ -20,7 +20,7 @@
             'change:selected': 'render'

         onRender: ->
-            @$el.html(@model.get('label'))
+            @$el.text(@model.get('label'))
             @$el.attr('value', @model.get('value'))
             @$el.attr('selected', @model.get('selected'))


member: will only can resend or revoke his/her invitation

--- client/home/lib/myteam/components/hometeamteammates/member.coffee
+++ client/home/lib/myteam/components/hometeamteammates/member.coffee
@@ -3,7 +3,7 @@
 ProfilePicture = require 'app/components/profile/profilepicture'
 capitalizeFirstLetter = require 'app/util/capitalizefirstletter'
 ButtonWithMenu = require 'app/components/buttonwithmenu'
-
+whoami = require 'app/util/whoami'

 module.exports = class Member extends React.Component

@@ -92,17 +92,19 @@
         admins={@props.admins}
         canEdit={canEdit}
         onClick={@onClickMemberRole.bind(this, role)}
-        isMenuOpen={@state.isMenuOpen} />
+        isMenuOpen={@state.isMenuOpen}
+        member={@props.member} />

-MemberRoleWithDropDownMenu = ({ canEdit, role, onClick, items, isMenuOpen, admins }) ->
+MemberRoleWithDropDownMenu = ({ canEdit, role, onClick, items, isMenuOpen, admins, member }) ->

   showButtonWithMenu = role is 'owner' and admins.size is 0

   unless canEdit and not showButtonWithMenu

       <MemberRole role={role} canEdit={canEdit}  />
+      {<ButtonWithMenu menuClassName='menu-class' items={items} isMenuOpen={isMenuOpen} />  if member.get('inviter_id') is whoami()._id}

   else


reconcile container id snapshot. Change state of a service to 'removed' when the container doesn't exist anymore. this leads to eventual consistency of the dashboard.

--- imports/server/stateReconciler.coffee
+++ imports/server/stateReconciler.coffee
@@ -27,7 +27,6 @@

 determineState = (doc, stateF) ->
-  console.log 'determineState', doc
   services = _.values doc.services
   overallState = stateF doc, services
   overallState = 'failing' unless overallState
@@ -137,3 +136,14 @@

   imagePulling: (image, instance) ->
     Instances.upsert {name: instance}, $set: status: "Pulling #{image}"
+
+  #
+  # Checks containers we know about against the snapshot list. If we know of
+  # a container that isn't in the list, the container is removed
+  #
+  reconcileContainerIdsSnapshot: (containerIds) ->
+    Instances.find().forEach (i) ->
+      for name, service of i.services
+        if service.container?.id not in containerIds
+          console.log "Found a container that is not in the snapshotted list: changed state to 'removed' for #{i.name}.#{name}"
+          Instances.update i._id, $set: "services.#{name}.state": 'removed'


module指定方法の不備らしい

module指定方法の不備らしい

--- gulp/tasks/compile.coffee
+++ gulp/tasks/compile.coffee
@@ -3,7 +3,7 @@
 less = require('gulp-less')
 coffee = require('gulp-coffee')
 jade = require('gulp-jade')
-reactJade = require('gulp-react-jade')
+reactJade = require('@mizchi/gulp-react-jade')

 config = require('../config')


Don't ever let the end date be before the start

In webconf invitations.

refs #1179

--- app/assets/javascripts/app/custom_bigbluebutton_rooms/_invitation_form.js.coffee
+++ app/assets/javascripts/app/custom_bigbluebutton_rooms/_invitation_form.js.coffee
@@ -60,9 +60,10 @@
     storeDuration($start, $end)
     updateDuration()

-  $(endsOnSelector).on 'changeDate', ->
+  $(endsOnSelector).on 'changeDate', (val1, val2) ->
     # updates the date to ignore the seconds
     $end.setDate(ignoreSeconds($end.getDate()))
+    adjustEndOnEndChange()
     storeDuration($start, $end)
     updateDuration()

@@ -101,9 +102,9 @@

   $(durationSelector).text(text)

-# Adjusts the end date according to the start date. The end can never be lower
-# than the start date, and will also sometimes be automatically set to maintain
-# the duration previously specified.
+# Adjusts the end date according to the start date when the start date is changed
+# by the user. The end can never be lower than the start date, and will also sometimes
+# be automatically set to maintain the duration previously specified.
 adjustEndOnStartChange = ->
   $end = $(endsOnSelector).data("datetimepicker")
   $start = $(startsOnSelector).data("datetimepicker")
@@ -124,6 +125,15 @@
   else
     $end.setDate(addDuration($start.getDate(), defaultDuration))

+# Adjusts the end date according when the end date input is changed by the user.
+adjustEndOnEndChange = (oldValue) ->
+  $end = $(endsOnSelector).data("datetimepicker")
+  $start = $(startsOnSelector).data("datetimepicker")
+
+  # don't ever let the end be before the start
+  if $end.getDate() < $start.getDate()
+    $end.setDate($start.getDate())
+
 # Adds 'duration' seconds to a Date object 'date'.
 addDuration = (date, duration) ->
   moment(date).add(duration, 'seconds').toDate()


Remove jQuery innerShiv workaround in View#render. This shouldn’t be necessary any longer.

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -336,12 +336,7 @@
       html = templateFunc @getTemplateData()

       # Replace HTML
-      # ------------
-
-      # This is a workaround for an apparent issue with jQuery 1.7’s
-      # innerShiv feature. Using @$el.html(html) caused issues with
-      # HTML5-only tags in IE7 and IE8.
-      @$el.empty().append html
+      @$el.html html

     # Call `attach` if `render` was not wrapped.
     @attach() unless @renderIsWrapped


bugfix/type-not-found-rejection (#97)

bugfix/type-not-found-rejection (#97)

--- src/coffee/stockimport.coffee
+++ src/coffee/stockimport.coffee
@@ -211,6 +211,8 @@
             @_getCustomTypeDefinition(customTypeKey).then (response) =>
               customTypeDefinition = response.body
               @_mapCustomField(_data, cell, headerName, customTypeDefinition, rowIndex)
+            .catch (error) =>
+              return reject(error)

           else
             Promise.resolve(@_mapCellData(cell, headerName)).then (cellData) ->


discussion markdown-css now closer to the actual element that uses it

--- client/app/Applications/Activity.kdapplication/ContentDisplays/Discussion.coffee
+++ client/app/Applications/Activity.kdapplication/ContentDisplays/Discussion.coffee
@@ -268,6 +268,8 @@

     @hightlightCode()

+    @$(".discussion-body .data").addClass "has-markdown"
+
   pistachio:->
     """
     {{> @header}}
@@ -291,7 +293,7 @@

             {{> @editDiscussionLink}}
             {{> @deleteDiscussionLink}}
-
{{@utils.expandUsernames(@utils.applyMarkdown(#(body)),"pre")}}

+
{{@utils.expandUsernames(@utils.applyMarkdown(#(body)),"pre")}}


rtsp: fix some msg only have LF in respondDescribe


DRY up rendering of different page types

--- src/cli.coffee
+++ src/cli.coffee
@@ -82,13 +82,6 @@
     @renderClass(klass) for _, klass of @digestedMetadata.classes
     @renderFile(file) for file in @docFiles()

-  renderFile: (file) ->
-    doc = Template.render 'layout',
-                          content: FilePage.render(file)
-                          title: 'Endokken'
-                          navigation: @navigation
-    fs.writeFileSync("./docs/#{path.basename(file, path.extname(file))}", doc)
-
   docFiles: ->
     (file for file in fs.readdirSync('.') when file.match(/\.md$/))

@@ -107,12 +100,18 @@
     files = @getNavFiles('.')
     @navigation = "#{classes}\n#{files}"

-  renderClass: (klass) ->
+  render: (content, filePath) ->
     doc = Template.render 'layout',
-                          content: ClassPage.render(klass)
+                          content: content
                           title: 'Endokken'
                           navigation: @navigation
                           version: @version
-    fs.writeFileSync("./docs/#{klass.name}", doc)
+    fs.writeFileSync(filePath, doc)
+
+  renderFile: (file) ->
+    @render(FilePage.render(file), "./docs/#{path.basename(file, path.extname(file))}")
+
+  renderClass: (klass) ->
+    @render(ClassPage.render(klass), "./docs/#{klass.name}")

 module.exports = Cli


Support output path when reading from stdin

--- src/cli.coffee
+++ src/cli.coffee
@@ -29,6 +29,10 @@
     return

   [filePath] = cli.argv._
+
+  outputPath = cli.argv.output
+  outputPath = path.resolve(outputPath) if outputPath
+
   if filePath
     filePath = path.resolve(filePath)
     unless fs.isFileSync(filePath)
@@ -37,8 +41,7 @@
       return

     html = new Highlights().highlightSync({filePath, scopeName: cli.argv.scope})
-    if cli.argv.output
-      outputPath = path.resolve(cli.argv.output)
+    if outputPath
       fs.writeFileSync(outputPath, html)
     else
       console.log(html)
@@ -49,4 +52,7 @@
     process.stdin.on 'data', (chunk) -> fileContents += chunk.toString()
     process.stdin.on 'end', ->
       html = new Highlights().highlightSync({fileContents, scopeName: cli.argv.scope})
-      console.log(html)
+      if outputPath
+        fs.writeFileSync(outputPath, html)
+      else
+        console.log(html)


Print usage on 'migrate help' too.

Print usage on 'migrate help' too.

--- src/cli.coffee
+++ src/cli.coffee
@@ -28,6 +28,8 @@
     console.error 'Usage: migrate <generate|one|down|pending|all|test [--name <migration_name>]>'
     process.exit 0

+  usage() if argv.help
+
   command = argv._[0]

   steps = []
@@ -36,9 +38,6 @@
   steps.push config.before.bind(config) if config.before?

   switch command
-    when '--help'
-      usage()
-
     when 'generate'
       die('must provide migration name with --name') unless argv.name
       steps.push (cb) -> migrate.generate argv.name, (err, filename) ->


Added resume command for cli

Added resume command for cli

--- src/cli.coffee
+++ src/cli.coffee
@@ -71,10 +71,16 @@

 # Pause/stop time tracking
 cmd.command('pause')
-    .description('Stops the last running timer. Shortcut: p')
+    .description('Stops the currently running timer. Shortcut: p')
     .action ->
         commands.pause()

+
+# Resume time tracking
+cmd.command('resume')
+    .description('Restarts the last running timer. Shortcut: r')
+    .action ->
+        commands.resume()

 # The function is executed every time user runs `bin/sow`
 exports.run = ->
@@ -88,6 +94,7 @@
         when 'w' then 'week'
         when 's' then 'start'
         when 'p' then 'pause'
+        when 'r' then 'resume'
         when 'stop' then 'pause'
         else command


parse user domain for mail

parse user domain for mail

--- server/controllers/mails.coffee
+++ server/controllers/mails.coffee
@@ -116,6 +116,8 @@
             db.view 'user/all', (err, users) ->
                 if instance?[0]?.value.domain?
                     domain = instance[0].value.domain
+                    if domain.indexOf('https://') isnt -1
+                        domain = domain.substring(8, domain.length)
                 else
                     domain = 'your.cozy.io'


Increase test write timeout

Increase test write timeout

--- spec/csv-editor-spec.coffee
+++ spec/csv-editor-spec.coffee
@@ -9,7 +9,7 @@

 {click} = require './helpers/events'

-WRITE_TIMEOUT = 200
+WRITE_TIMEOUT = 400

 describe "CSVEditor", ->
   [csvEditor, csvEditorElement] = []


Fix numeric tests

Fix numeric tests

--- spec/powershell-spec.coffee
+++ spec/powershell-spec.coffee
@@ -313,31 +313,30 @@
       it "tokenizes constant value in bytes", ->
         for constant in constants
           {tokens} = grammar.tokenizeLine constant
-          for token in tokens
-            expect(token).toHaveScopes ["source.powershell", "constant.numeric.scientific.powershell"]
+          expect(tokens[0]).toHaveScopes ["source.powershell", "constant.numeric.integer.powershell"]
+          expect(tokens[1]).toHaveScopes [ "source.powershell", "keyword.other.powershell" ]

     describe "Constant float values", ->
       constants = [
         "1.0", "0.89324", "123124235.2385923234"
       ]

-      scientificConstants = [
-        "3.23e24", "2.33e-12", "9.11e+21", "21e6", "7e-12", "12e+24"
-      ]
-
       it "tokenizes constant float values", ->
         for constant in constants
           {tokens} = grammar.tokenizeLine constant
           expect(tokens[0].value).toEqual constant
-          expect(tokens[0]).toHaveScopes ["source.powershell", "support.constant.powershell", "constant.numeric.scientific.powershell"]
+          expect(tokens[0]).toHaveScopes ["source.powershell", "constant.numeric.integer.powershell"]
+
+    describe "Scientific float values", ->
+
+      scientificConstants = [
+        "3.23e24", "2.33e-12", "9.11e+21", "21e6", "7e-12", "12e+24"
+      ]

       it "tokenizes scientific numbers", ->
         for scientificConstant in scientificConstants
           {tokens} = grammar.tokenizeLine scientificConstant
-          expect(tokens[0]).toHaveScopes ["source.powershell", "support.constant.powershell", "constant.numeric.scientific.powershell"]
-          expect(tokens[1].value.substr(0, 1)).toBe "e"
-          expect(tokens[1]).toHaveScopes ["source.powershell", "constant.numeric.scientific.powershell", "keyword.operator.math.powershell"]
-          expect(tokens[2]).toHaveScopes ["source.powershell", "support.constant.powershell", "constant.numeric.scientific.powershell"]
+          expect(tokens[0]).toHaveScopes ["source.powershell", "constant.numeric.integer.powershell"]

     describe "Constant hexadecimal values", ->
       constants = [ "0x1234", "0x1FF2", "0xff2e" ]
@@ -345,10 +344,8 @@
       it "tokenizes constant hexadecimal integer values", ->
         for constant in constants
           {tokens} = grammar.tokenizeLine constant
-          expect(tokens[0].value).toEqual "0x"
-          expect(tokens[0]).toHaveScopes ["source.powershell", "constant.numeric.hexadecimal.powershell", "keyword.operator.math.powershell"]
-          expect(tokens[1].value).toEqual constant.substr(2)
-          expect(tokens[1]).toHaveScopes ["source.powershell", "constant.numeric.hexadecimal.powershell", "support.constant.powershell"]
+          expect(tokens[0].value).toEqual constant
+          expect(tokens[0]).toHaveScopes ["source.powershell", "constant.numeric.hex.powershell" ]

   # TODO: Fix these later. They seem to highlight correctly but the tests are weird
   xdescribe "Types", ->


Output body text when timeout occurs. (Fixes #12)

--- lib/guard/jasmine/phantomjs/run-jasmine.coffee
+++ lib/guard/jasmine/phantomjs/run-jasmine.coffee
@@ -16,7 +16,18 @@
         condition = test()
       else
         if not condition
-          console.log JSON.stringify({ error: 'Timeout requesting Jasmine test runner!' })
+          text = page.evaluate -> document.getElementsByTagName('body')[0]?.innerText
+
+          if text
+            error = """
+                    Timeout waiting for the Jasmine test results!
+
+                    #{ text }
+                    """
+            console.log JSON.stringify({ error: error })
+          else
+            console.log JSON.stringify({ error: 'Timeout waiting for the Jasmine test results!' })
+
           phantom.exit(1)
         else
           ready()


enable new pdf viewer by default for newly created users


Fix #6 in the right place.

Fix #6 in the right place.

--- src/extract.coffee
+++ src/extract.coffee
@@ -71,7 +71,7 @@
                         addString(filename, str) if str

             file.src.forEach (input) ->
-                extractHtml(input) if input.match /\.(htm(|l)|php)$/
+                extractHtml(input) if input.match /\.(htm(|l)|php|phtml)$/
                 extractJs(input) if input.match /\.js$/


Fix typo. Make Japanese translation more natural.

--- t9n/ja.coffee
+++ t9n/ja.coffee
@@ -1,111 +1,121 @@
 #Language: Japanese
-#Translators: y-ich
+#Translators: y-ich and exKAZUu

 ja =

-  add: "足す"
+  t9Name: '日本語'
+
+  add: "アカウント連携："
   and: "と"
   back: "戻る"
   changePassword: "パスワードを変更する"
   choosePassword: "パスワードを選ぶ"
-  clickAgree: "「登録」をクリックすると同意したことになります"
+  clickAgree: "アカウント登録をクリックすると、次の内容に同意したことになります。"
   configure: "設定する"
-  createAccount: "アカウントを作る"
+  createAccount: "新しいアカウントの登録"
   currentPassword: "現在のパスワード"
-  dontHaveAnAccount: "アカウントをお持ちでは無いですか？"
-  email: "Eメール"
-  emailAddress: "Eメールアドレス"
-  emailResetLink: "Eメールリセットリンク"
+  dontHaveAnAccount: "まだアカウントをお持ちでない場合は"
+  Email: "メールアドレス"
+  email: "メールアドレス"
+  emailAddress: "メールアドレス"
+  emailResetLink: "パスワードリセットのメールを送る"
   forgotPassword: "パスワードをお忘れですか？"
-  ifYouAlreadyHaveAnAccount: "もしも既にアカウントをお持ちなら"
-  newPassword: "新パスワード"
-  newPasswordAgain: "新パスワード(確認)"
+  ifYouAlreadyHaveAnAccount: "既にアカウントをお持ちの場合は"
+  newPassword: "新しいパスワード"
+  newPasswordAgain: "新しいパスワード（確認）"
   optional: "オプション"
   OR: "または"
   password: "パスワード"
-  passwordAgain: "パスワード(確認)"
+  passwordAgain: "パスワード（確認）"
   privacyPolicy: "プライバシーポリシー"
-  remove: "削除する"
-  resetYourPassword: "パスワードをリセットする"
+  remove: "連携の解除："
+  resetYourPassword: "パスワードのリセット"
   setPassword: "パスワードを設定する"
-  sign: "サイン"
-  signIn: "サインインする"
-  signin: "サインイン"
-  signOut: "サインアウトする"
-  signUp: "登録する"
-  signupCode: "レジストレーションコード"
-  signUpWithYourEmailAddress: "Eメールアドレスで登録する"
-  terms: "利用条件"
-  updateYourPassword: "パスワードを更新する"
+  sign: "署名"
+  signIn: "ログイン"
+  signin: "ログイン"
+  signOut: "ログアウト"
+  signUp: "アカウント登録"
+  signupCode: "登録用コード"
+  signUpWithYourEmailAddress: "メールアドレスで登録する"
+  terms: "利用規約"
+  updateYourPassword: "パスワードを変更する"
   username: "ユーザー名"
-  usernameOrEmail: "ユーザー名またはEメール"
-  with: "with"
-
+  usernameOrEmail: "ユーザー名またはメールアドレス"
+  with: "："
+  maxAllowedLength: "最大文字数"
+  minRequiredLength: "最低文字数"
+  resendVerificationEmail: "認証メールの再送"
+  resendVerificationEmailLink_pre: "認証メールが届いていない場合は"
+  resendVerificationEmailLink_link: "再送"

   info:
-    emailSent: "Eメールを送りました"
-    emailVerified: "Eメールが確認されました"
-    passwordChanged: "パスワードが変更されました"
-    passwordReset: "パスワードがリセットされました"
+    emailSent: "メールを送りました"
+    emailVerified: "メールアドレスを確認しました"
+    passwordChanged: "パスワードを変更しました"
+    passwordReset: "パスワードをリセットしました"

   error:
-    emailRequired: "Eメールが必要です"
-    minChar: "パスワードには最低7文字必要です"
-    pwdsDontMatch: "パスワードが違います"
-    pwOneDigit: "パスワードは少なくとも1つ数字を含む必要があります"
-    pwOneLetter: "パスワードは少なく遠m1つアルファベットを含む必要があります"
-    signInRequired: "その操作にはサインインが必要です"
-    signupCodeIncorrect: "レジストレーションコードが間違っています"
-    signupCodeRequired: "レジストレーションコードが必要です"
-    usernameIsEmail: "ユーザー名にEメールアドレスは使えません"
-    usernameRequired: "ユーザー名が必要です"
+    emailRequired: "メールアドレスを入力してください。"
+    minChar: "パスワードの文字数が足りません。"
+    pwdsDontMatch: "パスワードが一致しません。"
+    pwOneDigit: "パスワードに1文字以上の数字を含めてください。"
+    pwOneLetter: "パスワードに1文字以上のアルファベットを含めてください。"
+    signInRequired: "その操作にはログインが必要です。"
+    signupCodeIncorrect: "登録用コードが間違っています。"
+    signupCodeRequired: "登録用コードが必要です。"
+    usernameIsEmail: "ユーザー名にメールアドレスは使えません。"
+    usernameRequired: "ユーザー名が必要です。"

     accounts:
-
       #---- accounts-base
       #"@" + domain + " email required"
       #"A login handler should return a result or undefined"
-      "Email already exists.": "そのEメールは既に登録されています"
-      "Email doesn't match the criteria.": "Eメールが基準を満たしていません"
-      "Invalid login token": "無効なログイントークンです"
-      "Login forbidden": "ログインが許可されません"
+      "Email already exists.": "そのメールアドレスは既に登録されています。"
+      "Email doesn't match the criteria.": "正しいメールアドレスを入力してください。"
+      "Invalid login token": "無効なログイントークンです。"
+      "Login forbidden": "ログインできません。"
       #"Service " + options.service + " already configured"
-      "Service unknown": "サービスが不明です"
-      "Unrecognized options for login request": "ログインリクエストのオプションが認識できません"
-      "User validation failed": "ユーザー確認できません"
-      "Username already exists.": "そのユーザー名は既に使われています"
-      "You are not logged in.": "ログインしていません"
-      "You've been logged out by the server. Please log in again.": "ログアウトされました。再度ログインしてください"
-      "Your session has expired. Please log in again.": "セッションが切れました。再度ログインしてください"
+      "Service unknown": "不明なサービスです"
+      "Unrecognized options for login request": "不明なログインオプションです"
+      "User validation failed": "ユーザ認証に失敗しました"
+      "Username already exists.": "そのユーザー名は既に使われています。"
+      "You are not logged in.": "ログインしていません。"
+      "You've been logged out by the server. Please log in again.": "既にログアウトしています。再度ログインしてください。"
+      "Your session has expired. Please log in again.": "セッションが切れました。再度ログインしてください。"
+      "Already verified": "認証済です"

       #---- accounts-oauth
-      "No matching login attempt found": "対応のログイン試行が見つかりません"
+      "No matching login attempt found": "対応するログイン試行が見つかりません"

       #---- accounts-password-client
-      "Password is old. Please reset your password.": "パスワードが古くなりました。パスワードをリセットしてください"
+      "Password is old. Please reset your password.": "古いパスワードです。パスワードをリセットしてください。"

       #---- accounts-password
       "Incorrect password": "パスワードが正しくありません"
-      "Invalid email": "Eメールが無効です"
+      "Invalid email": "無効なメールアドレスです"
       "Must be logged in": "ログインが必要です"
-      "Need to set a username or email": "ユーザー名かEメールを設定する必要があります"
-      "old password format": "パスワード形式が古いものです"
-      "Password may not be empty": "パスワードが入力されていないようです"
-      "Signups forbidden": "サインアップが禁止されています"
-      "Token expired": "トークンが切れました"
-      "Token has invalid email address": "トークンが無効なEメールアドレスを含んでいます"
+      "Need to set a username or email": "ユーザー名かメールアドレスを入力してください"
+      "old password format": "古いパスワード形式です"
+      "Password may not be empty": "パスワードを入力してください"
+      "Signups forbidden": "アカウントを登録できません"
+      "Token expired": "無効なトークンです"
+      "Token has invalid email address": "トークンに無効なメールアドレスが含まれています"
       "User has no password set": "パスワードが設定されていません"
-      "User not found": "ユーザーが見つかりません"
-      "Verify email link expired": "Eメールリンクが切れたか確認する"
-      "Verify email link is for unknown address": "Eメールリンクが不明なアドレス用か確認する"
+      "User not found": "ユーザー名が見つかりません"
+      "Verify email link expired": "期限の切れた認証メールのリンクです"
+      "Verify email link is for unknown address": "不明なメールアドレスに対する認証メールのリンクです"
+      "At least 1 digit, 1 lowercase and 1 uppercase": "数字、小文字、大文字をそれぞれ1文字以上入力してください"
+      "Please verify your email first. Check the email and follow the link!": "まず認証メールが届いているか確認して、リンクを押してください！"
+      "A new email has been sent to you. If the email doesn't show up in your inbox, be sure to check your spam folder.": "新しいメールを送信しました。もしメールが届いていなければ、迷惑メールに分類されていないか確認してください。"

       #---- match
-      "Match failed": "一致しませんでした"
+      "Match failed": "一致しません"

       #---- Misc...
       "Unknown error": "不明なエラー"


Throw error when setting html id attribute that already exists

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -235,6 +235,8 @@
 		@updateShadowStyle()

 		@onChange("size", @updateForSizeChange)
+
+	@ExistingIdMessage: (type, id) -> "Can not set #{type}: There's already an element with id '#{id}' in this document'"

 	##############################################################
 	# Properties
@@ -924,6 +926,12 @@
 			# a child node to insert it in, so it won't mess with Framers
 			# layer hierarchy.
 			@_createHTMLElementIfNeeded()
+			ids = Utils.getIdAttributesFromString(value)
+			for id in ids
+				existingElement = document.querySelector("[id='#{id}']")
+				if existingElement?
+					Utils.throwInStudioOrWarnInProduction(Layer.ExistingIdMessage("html", id))
+					return

 			@_elementHTML.innerHTML = value
 			@_updateHTMLScale()


Account for absent package dependencies.

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -391,7 +391,7 @@
   catch error
     throw new Error "You probably need to execute `npm install` to install brunch plugins. #{error}"

-  items = Object.keys(json.dependencies)
+  items = Object.keys(json.dependencies or {})
     .filter (dep) ->
       # Ignore Brunch plugins.
       dep isnt 'brunch' and


Require without .coffee

Require without .coffee

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -66,7 +66,7 @@

 slovakCmdKeymap = null
 slovakCmdCharactersForKeyCode = (code) ->
-  slovakCmdKeymap ?= require('./slovak-cmd-keymap.coffee')
+  slovakCmdKeymap ?= require('./slovak-cmd-keymap')
   slovakCmdKeymap[code]

 exports.normalizeKeystrokes = (keystrokes) ->


add ad-hoc method to compute document-relative path


Fix logic bug with fetchDone state

Fix logic bug with fetchDone state

--- SingularityUI/app/views/request.coffee
+++ SingularityUI/app/views/request.coffee
@@ -10,16 +10,18 @@
         @request = app.allRequests[@options.requestId]

         @requestTasks = new RequestTasks [], requestId: @options.requestId
-        @requestTasks.fetch().done => @render fetchDone = true
+        @requestTasks.fetch().done =>
+            @fetchDone = true
+            @render()

-    render: (fetchDone = false) =>
+    render: =>
         if not @request
             vex.dialog.alert("
Could not open a request by that ID.

#{ @options.requestId }
")
             return

         context =
             request: @request
-            fetchDone: fetchDone
+            fetchDone: @fetchDone
             requestTasksActive: _.filter(@requestTasks.toJSON(), (t) -> t.isActive)
             requestTasksHistorical: _.first(_.filter(@requestTasks.toJSON(), (t) -> not t.isActive), 100)
             requestTasksScheduled: _.filter(app.collections.tasksScheduled.toJSON(), (t) => t.requestId is @options.requestId)


sorted lines

sorted lines

--- lib/fonts.coffee
+++ lib/fonts.coffee
@@ -7,58 +7,58 @@
       type: 'string'
       default: 'Inconsolata'
       enum: [
-        'Anka/Coder',
-        'Anonymous Pro',
-        'Aurulent Sans Mono',
-        'Average Mono',
-        'Bitstream Vera Sans Mono',
-        'BPmono',
-        'Consolamono',
-        'Cousine',
-        'Cutive Mono',
-        'DejaVu Mono',
-        'Droid Sans Mono',
-        'Effects Eighty',
-        'Fantasque Sans Mono',
+        'Anka/Coder'
+        'Anonymous Pro'
+        'Aurulent Sans Mono'
+        'Average Mono'
+        'BPmono'
+        'Bitstream Vera Sans Mono'
+        'Consolamono'
+        'Cousine'
+        'Cutive Mono'
+        'DejaVu Mono'
+        'Droid Sans Mono'
+        'Effects Eighty'
+        'Fantasque Sans Mono'
         'Fifteen'
-        'Fira Mono',
-        'Fixedsys',
-        'Generic Mono',
-        'GNU Freefont',
-        'GNU Unifont',
-        'Hermit',
-        'Hermit Light',
-        'Inconsolata',
-        'Inconsolata-g',
-        'Latin Modern Mono',
-        'Latin Modern Mono Light',
-        'Lekton',
-        'Liberation Mono',
-        'Luxi Mono',
-        'Meslo',
-        'M+',
-        'M+ Light',
-        'M+ Medium',
-        'M+ Thin',
-        'Monofur',
-        'NotCourierSans',
-        'Nova Mono',
-        'Oxygen Mono',
-        'Profont',
-        'Proggy Clean',
-        'PT Mono',
-        'Quinze',
-        'saxMono',
-        'Share Tech Mono',
-        'Source Code Pro',
-        'Source Code Pro Extra Light',
-        'Source Code Pro Light',
-        'Source Code Pro Medium',
-        'Sudo',
-        'TeX Gyre Cursor',
-        'Ubuntu Mono',
-        'VT323',
+        'Fira Mono'
+        'Fixedsys'
+        'GNU Freefont'
+        'GNU Unifont'
+        'Generic Mono'
+        'Hermit Light'
+        'Hermit'
+        'Inconsolata'
+        'Inconsolata-g'
+        'Latin Modern Mono Light'
+        'Latin Modern Mono'
+        'Lekton'
+        'Liberation Mono'
+        'Luxi Mono'
+        'M+ Light'
+        'M+ Medium'
+        'M+ Thin'
+        'M+'
+        'Meslo'
+        'Monofur'
+        'NotCourierSans'
+        'Nova Mono'
+        'Oxygen Mono'
+        'PT Mono'
+        'Profont'
+        'Proggy Clean'
+        'Quinze'
+        'Share Tech Mono'
+        'Source Code Pro Extra Light'
+        'Source Code Pro Light'
+        'Source Code Pro Medium'
+        'Source Code Pro'
+        'Sudo'
+        'TeX Gyre Cursor'
+        'Ubuntu Mono'
+        'VT323'
         'Verily Serif Mono'
+        'saxMono'
       ]

   activate: (state) ->


Add regexr support

Add regexr support

--- grammars/stata.cson
+++ grammars/stata.cson
@@ -10,7 +10,7 @@
 'patterns': [
   {
     'comment': 'Add Regular Expression support'
-    'match': '(regexm)(\\()(.+),\\s*(\"|`\")(.+)(\"|\"\')(\\))'
+    'match': '(regexm)(\\()(.+)(,)\\s*(\"|`\")(.+)(\"|\"\')\\s*(\\))'
     'captures':
       '1':
         'name': 'support.function.builtin.stata'
@@ -37,18 +37,176 @@
             'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
             'name': 'variable.parameter.function.stata'
           }
+          {
+            'include': '#comments'
+          }
         ]
       '4':
+        'name': 'punctuation.definition.variable.begin.stata' # the comma
+      '5':
         'name': 'punctuation.definition.string.begin.stata'
+      '6':
+        'patterns': [
+          {'include': 'source.regexp.stata'}
+        ]
+      '7':
+        'name': 'punctuation.definition.string.end.stata'
+      '8':
+        'name': 'punctuation.definition.parameters.end.stata'
+  }
+  {
+    'comment': 'Add Regex support for regexr() too'
+    'match': '(regexr)(\\()(.+)(,)\\s*(\"|`\")(.+)(\"|\"\')\\s*(,)([^\\)]*)(\\))'
+    'captures':
+      '1':
+        'name': 'support.function.builtin.stata'
+      '2':
+        'name': 'punctuation.definition.parameters.begin.stata'
+      '3':
+        'patterns': [
+          {
+            'include': '#string-compound'
+          }
+          {
+            'include': '#string-regular'
+          }
+          {
+            'include': '#macro-local'
+          }
+          {
+            'include': '#macro-global'
+          }
+          {
+            'include': '#builtin_functions'
+          }
+          {
+            'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
+            'name': 'variable.parameter.function.stata'
+          }
+          {
+            'include': '#comments'
+          }
+        ]
+      '4':
+        'name': 'punctuation.definition.variable.begin.stata' # the comma
       '5':
+        'name': 'punctuation.definition.string.begin.stata'
+      '6':
         'patterns': [
           {'include': 'source.regexp.stata'}
         ]
-      '6':
+      '7':
         'name': 'punctuation.definition.string.end.stata'
-      '7':
-        'name': 'punctuation.definition.parameters.end.stata'
-  }
+      '8':
+        'name': 'punctuation.definition.variable.begin.stata' # the comma
+      '9':
+        'patterns': [
+          {
+            'include': '#string-compound'
+          }
+          {
+            'include': '#string-regular'
+          }
+          {
+            'include': '#macro-local'
+          }
+          {
+            'include': '#macro-global'
+          }
+          {
+            'include': '#builtin_functions'
+          }
+          {
+            'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
+            'name': 'variable.parameter.function.stata'
+          }
+          {
+            'include': '#comments'
+          }
+        ]
+      '10':
+        'name': 'punctuation.definition.parameters.end.stata'
+  }
+  # {
+  #   'comment': 'Add Regex support for regexr() too'
+  #   'begin': '(regexr)(\\()(.*)(,)\\s*(\"|`\")(.+)(\"|\"\')\\s*'
+  #   'beginCaptures':
+  #     '1':
+  #       'name': 'support.function.builtin.stata'
+  #     '2':
+  #       'name': 'punctuation.definition.parameters.begin.stata'
+  #     '3':
+  #       'patterns': [
+  #         {
+  #           'include': '#string-compound'
+  #         }
+  #         {
+  #           'include': '#string-regular'
+  #         }
+  #         {
+  #           'include': '#macro-local'
+  #         }
+  #         {
+  #           'include': '#macro-global'
+  #         }
+  #         {
+  #           'include': '#builtin_functions'
+  #         }
+  #         {
+  #           'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
+  #           'name': 'variable.parameter.function.stata'
+  #         }
+  #         {
+  #           'include': '#comments'
+  #         }
+  #       ]
+  #     '4':
+  #       'name': 'punctuation.definition.variable.begin.stata' # the comma
+  #   'contentName':
+  #     'patterns': [
+  #
+  #     ]
+  #     '5':
+  #       'name': 'punctuation.definition.string.begin.stata'
+  #     '6':
+  #       'patterns': [
+  #         {'include': 'source.regexp.stata'}
+  #       ]
+  #     '7':
+  #       'name': 'punctuation.definition.string.end.stata'
+  #     '8':
+  #       'name': 'punctuation.definition.variable.begin.stata' # the comma
+  #     '9':
+  #       'patterns': [
+  #         {
+  #           'include': '#string-compound'
+  #         }
+  #         {
+  #           'include': '#string-regular'
+  #         }
+  #         {
+  #           'include': '#macro-local'
+  #         }
+  #         {
+  #           'include': '#macro-global'
+  #         }
+  #         {
+  #           'include': '#builtin_functions'
+  #         }
+  #         {
+  #           'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
+  #           'name': 'variable.parameter.function.stata'
+  #         }
+  #         {
+  #           'include': '#comments'
+  #         }
+  #       ]
+  #   'end': '(,)(.*)(\\))'
+  #   'endCaptures':
+  #     '1':
+  #       'name': 'punctuation.definition.parameters.end.stata'
+  # }
+
   {
     'include': '#builtin_functions'
   }
@@ -147,7 +305,16 @@
         'name': 'entity.name.type.class.stata'
       '3':
         'name': 'keyword.operator.assignment.stata'
-    'match': '\\b(forvalues)\\s+([a-zA-Z0-9_]{1,31})\\s*(=)\\s*[0-9\\(\\)/:\. ]+\\s*\\{'
+      '4':
+        'patterns': [
+          {
+            'include': '#numbers'
+          }
+          {
+            'include': '#operators'
+          }
+        ]
+    'match': '\\b(forvalues)\\s+([a-zA-Z0-9_]{1,31})\\s*(=)\\s*([0-9\\(\\)/:\. ]+)\\s*'
   }
   {
     'comment': 'keywords that delimit loops'
@@ -193,7 +360,21 @@
     "comment": "model commands."
     "match": "\\b(anova|xtmixed|regress|ivregress|xtreg|xtreg_be|xtreg_fe|xtreg_ml|xtreg_pa_p|xtreg_re|xtregar|xtrere_p|xtset|xtsf_ll|xtsf_llti|xtsum|xttab|xttest0|xttobit|xttobit_8|xttobit_p|xttrans)\\b"
   }
-
+  {
+    'begin': '(\\{)'
+    'beginCaptures':
+      '0':
+        'name': 'keyword.control.block.begin.stata'
+    'end': '(\\})'
+    'endCaptures':
+      '0':
+        'name': 'keyword.control.block.begin.stata'
+    'patterns': [
+      {
+        'include': '$self'
+      }
+    ]
+  }
   # Start manual functions:
   {
     'captures':
@@ -274,6 +455,9 @@
       }
     ]
   }
+  {
+    'include': '#commands-other'
+  }
 ]
 'repository':
   'builtin_functions':
@@ -309,6 +493,13 @@
       {
         'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
         'name': 'variable.parameter.function.stata'
+      }
+      {
+        'include': '#comments'
+      }
+      {
+        'match': ','
+        'name': 'punctuation.definition.variable.begin.stata'
       }
     ]
   'builtin_types':
@@ -428,6 +619,10 @@
       {
         'match': '\\b([0-9]+|0)'
         'name': 'constant.numeric.integer.decimal.stata'
+      }
+      {
+        'match': '(?<![a-zA-Z0-9])(\\.)(?![a-zA-Z0-9])'
+        'name': 'constant.language.stata'
       }
     ]
   'operators':


Fix reference to backbone history

Fix reference to backbone history

--- app/js/app.coffee
+++ app/js/app.coffee
@@ -26,7 +26,7 @@
       notifications: @notifications
     )

-    Backbone.history.start() unless History.started
+    Backbone.history.start() unless Backbone.History.started

     Backbone.history.navigate 'all', trigger: true


detail: fix fb parse rendering

detail: fix fb parse rendering

--- js/views/detail.coffee
+++ js/views/detail.coffee
@@ -18,6 +18,7 @@
       "tap .category-filter": "categories"
       "tap .star-rate": "starRate"
     mapElement: "detail-map"
+    rendered: false

     initialize: ->
       FB.init(
@@ -56,12 +57,16 @@
         posts: 2
         href: "http://taukkis.fi/#detail?id="+@poi.id
       )
-      FB.XFBML.parse();
+      if(@rendered)
+        FB.XFBML.parse();

       @clearRoute()
       @clearPOILayer()
       @renderRoute()
       @renderPoi()
+
+      if(!@rendered)
+        @rendered = true

     categories: (e) ->


Updated tests.

Updated tests.

--- test.coffee
+++ test.coffee
@@ -44,7 +44,7 @@
       """

   test 'CoffeeScript helper (function)', ->
-    expected = "<script>var __slice = Array.prototype.slice;var __hasProp = Object.prototype.hasOwnProperty;var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };var __extends = function(child, parent) {  for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }  function ctor() { this.constructor = child; }  ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype;  return child; };var __indexOf = Array.prototype.indexOf || function(item) {  for (var i = 0, l = this.length; i < l; i++) {    if (this[i] === item) return i;  } return -1; };(function () {\n          return alert('hi');\n        })();</script>"
+    expected = "<script>var __slice = Array.prototype.slice;var __hasProp = Object.prototype.hasOwnProperty;var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };var __extends = function(child, parent) {  for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }  function ctor() { this.constructor = child; }  ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype;  return child; };var __indexOf = Array.prototype.indexOf || function(item) {  for (var i = 0, l = this.length; i < l; i++) {    if (this[i] === item) return i;  } return -1; };(function () {\n          return alert('hi');\n        }).call(this);</script>"
     expected is render ->
       coffeescript -> alert 'hi'


Add create plugin and open plugins directory commands

--- lib/index.coffee
+++ lib/index.coffee
@@ -10,13 +10,17 @@
   config:
     evaluateOnSave:
       type        : 'boolean'
-      description : 'Re-evaluate on save'
+      description : 'Watch for changes and re-evaluate a source file on save'
       default     : true

   activate: (state) ->
     @subscriptions = new CompositeDisposable
     @subscriptions.add atom.commands.add 'atom-workspace', 'magic-console:toggle':
       => @toggle()
+    @subscriptions.add atom.commands.add 'atom-workspace', 'magic-console:open-plugins-directory':
+      => @openPluginsDirectory()
+    @subscriptions.add atom.commands.add 'atom-workspace', 'magic-console:create-new-plugin':
+      => @createNewPlugin()
     @views = []
     @blankRuntime = null
     atom.workspace.addOpener (uriToOpen) =>
@@ -101,3 +105,12 @@
       if consoleRuntimeView instanceof ConsoleRuntimeView
         previousActivePane.activate()
         @runBlank(consoleRuntimeView)
+
+  openPluginsDirectory: ->
+    atom.open(pathsToOpen: path.join(__dirname, '..', 'plugins'))
+
+  createNewPlugin: ->
+    template = fs.readFileSync(path.join(__dirname, '..', 'utils', 'plugin-template.js'), encoding: 'utf-8')
+    atom.workspace.open(path.join(__dirname, '..', 'plugins', 'New.js')).then (textEditor) =>
+      textEditor.setText(template)
+


update code style for hosts variable, add opts.host

--- lib/index.coffee
+++ lib/index.coffee
@@ -15,10 +15,9 @@
   reserved for storing Contentful system metadata, please rename this field to
   a different value.'

-hosts = {
-  develop: 'preview.contentful.com',
+hosts =
+  develop: 'preview.contentful.com'
   production: 'cdn.contentful.com'
-}

 module.exports = (opts) ->
   # throw error if missing required config
@@ -27,7 +26,9 @@

   # setup contentful api client
   client = contentful.createClient
-    host: hosts[process.env.CONTENTFUL_ENV] || hosts.production
+    host: hosts[process.env.CONTENTFUL_ENV] ||
+          hosts.develop if opts.preview ||
+          hosts.production
     accessToken: opts.access_token
     space:       opts.space_id


kills link directly to player

kills link directly to player

--- kurea.base/client/scripts/idle.coffee
+++ kurea.base/client/scripts/idle.coffee
@@ -78,13 +78,17 @@

     statisticsObj: (key) ->
       val = Session.get('player')?.statistics[key] or 0
-      buildHtmlFromObject = (obj) ->
+
+      buildHtmlFromObject = (obj, playerLink = no) ->
         _.sortBy (_.keys obj), (key) ->
           -obj[key]
         .map (key) ->
-          "
#{key} (#{obj[key]})
"
+          embedKey =  if playerLink then "#{key}" else key
+          "
#{embedKey} (#{obj[key]})
"
         .join ""
-      if _.isObject val then "
#{buildHtmlFromObject val}
" else val
+
+      return "
#{buildHtmlFromObject val, true}
" if (_.isObject val) and key is 'calculated kills'
+      val

     knob: (stat) ->
       renderKnob()


fix profile bug

fix profile bug

--- both/methods/user/user.coffee
+++ both/methods/user/user.coffee
@@ -99,7 +99,7 @@
 						setTeam['teams.$.declined'] = newDeclined

 					if setTeam != {}
-						Shifts.update shift._id, 'teams._id': team._id,
+						Shifts.update _id: shift._id, 'teams._id': team._id,
 							$set: setTeam

 	toggleAvailability: (day, hour) ->


Bump up ISY timeout

Bump up ISY timeout

--- lib/adapters/isy/IsyAPI.coffee
+++ lib/adapters/isy/IsyAPI.coffee
@@ -6,9 +6,11 @@
 winston = require('winston')
 WebSocketClient = require('websocket').client

+# XXX Commands "time out" if they do not result in a change
+
 module.exports = class IsyAPI extends Backbone.Model
   defaults:
-    commandTimeout: 2000
+    commandTimeout: 5000

   initialize: ->
     @_pendingCommands = {}


trying to add scrollto

trying to add scrollto

--- lineman/app/modules/thread_page/thread_page_controller.coffee
+++ lineman/app/modules/thread_page/thread_page_controller.coffee
@@ -1,6 +1,11 @@
-angular.module('loomioApp').controller 'ThreadPageController', ($scope, $modal, discussion, Records, MessageChannelService, UserAuthService, DiscussionFormService) ->
+angular.module('loomioApp').controller 'ThreadPageController', ($scope, $document, $modal, discussion, Records, MessageChannelService, UserAuthService, DiscussionFormService) ->
   $scope.discussion = discussion
   $scope.group = discussion.group()
+
+  # if first time visiting the page
+  $document.scrollTop(0, 500)
+  console.log 'scrolling'
+  # if returning

   # maybe not needed
   onMessageReceived = ->


Document ApplicationMenu

Document ApplicationMenu

--- src/application-menu.coffee
+++ src/application-menu.coffee
@@ -2,6 +2,10 @@
 Menu = require 'menu'
 _ = require 'underscore'

+# Private: Used to manage the global application menu.
+#
+# It's created by {AtomApplication} upon instantiation and used to add, remove
+# and maintain the state of all menu items.
 module.exports =
 class ApplicationMenu
   version: null
@@ -12,11 +16,22 @@
     @menu = Menu.buildFromTemplate @getDefaultTemplate()
     Menu.setApplicationMenu @menu

+  # Public: Updates the entire menu with the given keybindings.
+  #
+  # keystrokesByCommand - An Object where the keys are commands and the values
+  #                       are Arrays containing the keystrokes.
+  #
+  # Returns nothing.
   update: (keystrokesByCommand) ->
     template = @getTemplate(keystrokesByCommand)
     @menu = Menu.buildFromTemplate(template)
     Menu.setApplicationMenu(@menu)

+  # Private: Flattens the given menu and submenu items into an single Array.
+  #
+  # menu - A complete menu configuration object for atom-shell's menu API.
+  #
+  # Returns an Array of native menu items.
   allItems: (menu=@menu) ->
     items = []
     for index, item of menu.items or {}
@@ -24,16 +39,31 @@
       items = items.concat(@allItems(item.submenu)) if item.submenu
     items

+  # Public: Used to make all window related menu items are active.
+  #
+  # enable - If true enables all window specific items, if false disables all
+  #          window specific items.
+  #
+  # Returns nothing.
   enableWindowSpecificItems: (enable) ->
     for item in @allItems()
       item.enabled = enable if item.metadata?['windowSpecific']

+  # Public: Makes the download menu item visible if available.
+  #
+  # Note: The update menu item's must match 'Install update' exactly otherwise
+  # this function will fail to work.
+  #
+  # Returns nothing.
   showDownloadUpdateItem: (newVersion, quitAndUpdateCallback) ->
     downloadUpdateItem = _.find @allItems(), (item) -> item.label == 'Install update'
     if downloadUpdateItem
       downloadUpdateItem.visible = true
       downloadUpdateItem.click = quitAndUpdateCallback

+  # Private: Default list of menu items.
+  #
+  # Returns an Array of menu item Objects.
   getDefaultTemplate: ->
     [
       label: "Atom"
@@ -45,6 +75,13 @@
       ]
     ]

+  # Private: The complete list of menu items.
+  #
+  # keystrokesByCommand - An Object where the keys are commands and the values
+  #                       are Arrays containing the keystrokes.
+  #
+  # Returns a complete menu configuration Object for use with atom-shell's
+  #   native menu API.
   getTemplate: (keystrokesByCommand) ->
     atomMenu =
       label: 'Atom'
@@ -113,6 +150,14 @@

     @translateTemplate template, keystrokesByCommand

+  # Private: Combines a menu template with the appropriate keystrokes.
+  #
+  # template            - An Object conforming to atom-shell's menu api but
+  #                       lacking accelerator and click properties.
+  # keystrokesByCommand - An Object where the keys are commands and the values
+  #                       are Arrays containing the keystrokes.
+  #
+  # Returns a complete menu configuration object for atom-shell's menu API.
   translateTemplate: (template, keystrokesByCommand) ->
     template.forEach (item) =>
       item.metadata = {}
@@ -123,6 +168,14 @@
       @translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
     template

+  # Private: Determine the accelerator for a given command.
+  #
+  # command - The name of the command.
+  # keystrokesByCommand - An Object where the keys are commands and the values
+  #                       are Arrays containing the keystrokes.
+  #
+  # Returns a String containing the keystroke in a format that can be interpreted
+  #   by atom shell to provide nice icons where available.
   acceleratorForCommand: (command, keystrokesByCommand) ->
     keystroke = keystrokesByCommand[command]?[0]
     return null unless keystroke


Fix All The Bugs

Fix All The Bugs

--- build/tasks/mkdeb-task.coffee
+++ build/tasks/mkdeb-task.coffee
@@ -3,7 +3,7 @@
 _ = require 'underscore-plus'

 module.exports = (grunt) ->
-  {spawn} = require('./task-helpers')(grunt)
+  {spawn,cp,rm} = require('./task-helpers')(grunt)

   fillTemplate = (filePath, data) ->
     pkgName = grunt.config.get('name')
@@ -58,13 +58,15 @@
       controlFilePath = fillTemplate(path.join('resources', 'linux', 'debian', 'control'), data)
       desktopFilePath = fillTemplate(path.join('resources', 'linux', 'app.desktop'), data)

-      grunt.file.mv(desktopFilePath, path.join(path.dirname(desktopFilePath), "#{pkgName}.desktop"))
+      cp(desktopFilePath, path.join(path.dirname(desktopFilePath), "#{pkgName}.desktop"))
+      rm(desktopFilePath)

       icon = path.join('resources', 'app.png')

       cmd = path.join('script', 'mkdeb')
       args = [version, arch, controlFilePath, desktopFilePath, icon, buildDir, data.name]

+      grunt.verbose.ok "About to invoke #{cmd} #{args.join(' ')}"
       spawn {cmd, args}, (error) ->
         if error?
           done(error)


Adding in remaining dialogue.

Adding in remaining dialogue.

--- src/game/states/script.coffee
+++ src/game/states/script.coffee
@@ -4,7 +4,7 @@
         {speech: "Who put out all the stars? I need to get back to base\n with all this loot.", speaker: 0},
         {speech: "Woof (I'm getting hungy)", speaker:1},
         {speech: "Can we use any of the other stars to find the way?\n I'm sure I've heard you can find lots of stars\n once you've found the Big Dipper.", speaker:2},
-        {speech: "Lets follow Mizar and Alkaid. \nThat takes us to a really bright star that's nearby.", speaker:0}
+        {speech: "There one star still alight. I recognise that one. It's Polaris, the North star. We can use that to find the other stars nearby.", speaker:0}
     ]
   },
   {
@@ -14,7 +14,56 @@
       {speech: "I'm sure there are some stars that point to the North Star. Let's try and find them.", speaker: 2},
       {speech: "Full speed ahead!", speaker: 0}
     ]
-  }
+  },
+  {
+     dialogArray: [
+        {speech: "Merak and Dubhe. What cool names for stars.", speaker: 0},
+        {speech: "Woof (How do you even say Dubhe)", speaker:1},
+        {speech: "Can we use any of the other stars to find the way?\n I'm sure I've heard you can find lots of stars\n once you've found the Big Dipper.", speaker:2},
+        {speech: "Lets follow Mizar and Alkaid. \nThat takes us to a really bright star that's nearby.", speaker:0}
+    ]
+  },
+  {
+     dialogArray: [
+        {speech: "Arcturus in Boötes, the Herdsman. It looks more like an ice cream cone to me.", speaker: 0},
+        {speech: "Woof (Pictures made from stars are called asterisms))", speaker:1},
+        {speech: "It looks like a huge spike to me.", speaker:2},
+        {speech: "The Spike to Spica. Let's go and sort out Virgo next.", speaker:0}
+    ]
+  },
+  {
+     dialogArray: [
+        {speech: "I don't have any more leads from here. Lets go back to the Big Dipper.", speaker: 0},
+        {speech: "Woof (also known as the Plough or the Great Bear)", speaker:1},
+        {speech: "Aye-aye Captain.", speaker:2}
+    ]
+  },
+  {
+     dialogArray: [
+        {speech: "This time lets follow Megrez and Phecda.", speaker: 0},
+        {speech: "Woof (who came up with these names)", speaker:1},
+        {speech: "Where does that take us?", speaker:2}
+        {speech: "Another zodiac constellation. Let's go and relight Leo", speaker: 0},
+    ]
+  },
+  {
+     dialogArray: [
+        {speech: "Deneb in Cygnus is one of the stars in the Summer Triangle", speaker: 0},
+        {speech: "Woof (why would a Greek god turn himself into a swan?)", speaker:1},
+        {speech: "Can we get to the others?", speaker:2}
+        {speech: "We can get to Vega if we fly the same route as before but with a gentle curve.", speaker: 0},
+        {speech: "Woof (gentle curve? what sort of direction is that?)", speaker:1},
+    ]
+  },
+  {
+     dialogArray: [
+        {speech: "That's 2 stars out of 3. We should be able to find Altair in Aquilla now.", speaker: 0},
+        {speech: "Woof... (the summer triangle. That's a beautiful story about star crossed lovers)", speaker:1},
+        {speech: "That should be enough stars to get us back to base this time", speaker:2}
+        {speech: "Yes, a job well done.", speaker: 0},
+        {speech: "Woof (food!)", speaker:1},
+    ]
+  },
 ]


修正NumberDataType的逻辑错误

修正NumberDataType的逻辑错误


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/array_extensions.coffee
+++ src/array_extensions.coffee
@@ -1,5 +1,5 @@
 ###*
-Calculate the aerage
+Calculate the a

 [null, undefined, 3, 3, undefined, 5].compact()


One last place using window.updateProfileUrl

--- resources/assets/coffee/react/profile-page/cover-selection.coffee
+++ resources/assets/coffee/react/profile-page/cover-selection.coffee
@@ -21,7 +21,7 @@
   onClick: =>
     return if @props.url == null

-    $.ajax window.updateProfileUrl,
+    $.ajax Url.updateProfileAccount,
       method: 'put'
       data:
         cover_id: @props.name


Replace Atom Shell with Atom in menu text

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -118,7 +118,7 @@
         { type: 'separator' }
         { label: 'Preferences...', accelerator: 'Command+,', click: => @openConfig() }
         { type: 'separator' }
-        { label: 'Hide Atom Shell', accelerator: 'Command+H', selector: 'hide:' }
+        { label: 'Hide Atom', accelerator: 'Command+H', selector: 'hide:' }
         { label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' }
         { label: 'Show All', selector: 'unhideAllApplications:' }
         { type: 'separator' }


Add Haskell example + Reinstate auto focus

--- emulation/atom-emulator.coffee
+++ emulation/atom-emulator.coffee
@@ -393,14 +393,20 @@
     atom.workspace.editor.buffer.setText """
     [ Inky, Dinky, [some, inner, nesting], Pinky, (1,2,3) ]

-    o = { p2: 'B'
-        , p3: 'C'
-        , p1: { x: 10
-              , y: 20
-              }
+    o = { radius: 32
+        , z: 2
+        , pos: { x: 4
+               , y: 3
+               }
+        }
+
+    main =
+     do { putStrLn "Hello world"
+        ; let x = [10, 20, 30, 40]
+        ; return ()
         }
     """
-
+    $editableDiv.focus()
     setTimeout ( ->
       atom.notifications.addSuccess 'Atom emulator initialized'
     ), 300 # Small delay to see it appear after page has loaded


Fixed PPU read buffer post-fetch.

Fixed PPU read buffer post-fetch.

--- emulator/core/units/ppu.coffee
+++ emulator/core/units/ppu.coffee
@@ -302,24 +302,27 @@
         address

     readData: ->
-        address = @$incrementAddress()
-        oldBufferContent = @vramReadBuffer
-        @vramReadBuffer = @ppuMemory.read address                                # Always increments the address
-        if @$isPaletteAddress address then @vramReadBuffer else oldBufferContent # Delayed read outside the palette memory area
+        if @$isPaletteAddress @vramAddress
+            value = @ppuMemory.read @vramAddress
+            @vramReadBuffer = @ppuMemory.read @vramAddress & 0x2FFF # Buffer musn't be reloaded from palette address, but from underlying nametable address
+            @incrementAddress()
+            value
+        else
+            value = @vramReadBuffer # Delayed read outside the palette memory area (values are passed through read buffer first)
+            @vramReadBuffer = @ppuMemory.read @vramAddress
+            @incrementAddress()
+            value

     writeData: (value) ->
-        address = @$incrementAddress()                      # Always increments the address
-        @ppuMemory.write address, value unless @$isRenderingActive() # Only during VBlank or disabled rendering
+        @ppuMemory.write @vramAddress, value unless @$isRenderingActive()
+        @incrementAddress()
         value

     incrementAddress: ->
-        previousAddress = @vramAddress
         @vramAddress = (@vramAddress + @$getAddressIncrement()) & 0xFFFF
-        previousAddress

     getAddressIncrement: ->
         if @bigAddressIncrement then 0x20 else 0x01 # Vertical/horizontal move in pattern table.
-

     isPaletteAddress: (address) ->
         (address & 0x3F00) is 0x3F00


refactor($tests): add concrete usage of whenPass in promises specs

--- specs/units/promise.spec.coffee
+++ specs/units/promise.spec.coffee
@@ -1,30 +1,34 @@
 describe spectacular.Promise, ->

   it -> should exist
-  itsInstance -> should exist

-  describe '.unit', ->
-    it -> should exist
-    itsReturn -> should exist
+  whenPass ->
+    itsInstance -> should exist

-    describe 'the returned promise', ->
-      subject 'promise', -> spectacular.Promise.unit()
+    describe '.unit', ->
+      it -> should exist
+      whenPass ->
+        itsReturn -> should exist

-      it -> should be 'fulfilled'
-      its 'value', -> should equal 0
+        describe 'the returned promise', ->
+          subject 'promise', -> spectacular.Promise.unit()

-  describe '.all', ->
-    it -> should exist
+          it -> should be 'fulfilled'
+          its 'value', -> should equal 0

-    context 'when called with an array of promise', ->
-      withParameters [
-        spectacular.Promise.unit()
-        spectacular.Promise.unit()
-        spectacular.Promise.unit()
-      ]
+    describe '.all', ->
+      it -> should exist

-      itsReturn -> should exist
-      itsReturn -> should be 'fulfilled'
+      whenPass ->
+        context 'when called with an array of promise', ->
+          withParameters [
+            spectacular.Promise.unit()
+            spectacular.Promise.unit()
+            spectacular.Promise.unit()
+          ]

-  describe '::isPending', ->
-    itsReturn -> should equal true
+          itsReturn -> should exist
+          itsReturn -> should be 'fulfilled'
+
+    describe '::isPending', ->
+      itsReturn -> should equal true


Allow user to force themselves to be part of autocompile rollout

--- services/web/app/coffee/Features/Project/ProjectController.coffee
+++ services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -270,6 +270,10 @@
 				CollaboratorsHandler.userIsTokenMember user_id, project_id, cb
 			showAutoCompileOnboarding: (cb) ->
 				cb = underscore.once(cb)
+				# Force autocompile rollout if query param set
+				if req.query.ac == 't'
+					return cb(null, { enabled: true, showOnboarding: true })
+
 				if !user_id?
 					return cb()


Quieten noisy Ace warnings

Quieten noisy Ace warnings

--- services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -48,6 +48,7 @@
 						@$originalApply(fn);

 				editor = ace.edit(element.find(".ace-editor-body")[0])
+				editor.$blockScrolling = Infinity
 				window.editors ||= []
 				window.editors.push editor


reapply type-detection logic during 'finish'

Close #361

--- files.coffee
+++ files.coffee
@@ -1090,6 +1090,7 @@
     self          = @
     result.type   = @_getMimeType opts.file
     result.public = @public
+    @_updateFileTypes result

     @collection.insert _.clone(result), (error, _id) ->
       if error
@@ -1219,6 +1220,22 @@
   ###
   @locus Anywhere
   @memberOf FilesCollection
+  @name _updateFileTypes
+  @param {Object} data - File data
+  @summary Internal method. Classify file based on 'type' field
+  ###
+  _updateFileTypes: (data) ->
+    data.isVideo  = /^video\//i.test data.type
+    data.isAudio  = /^audio\//i.test data.type
+    data.isImage  = /^image\//i.test data.type
+    data.isText   = /^text\//i.test data.type
+    data.isJSON   = /application\/json/i.test data.type
+    data.isPDF    = /application\/pdf|application\/x-pdf/i.test data.type
+    return
+
+  ###
+  @locus Anywhere
+  @memberOf FilesCollection
   @name _dataToSchema
   @param {Object} data - File data
   @summary Internal method. Build object in accordance with default schema from File data
@@ -1239,14 +1256,9 @@
           size: data.size
           type: data.type
           extension: data.extension
-      isVideo: /^video\//i.test data.type
-      isAudio: /^audio\//i.test data.type
-      isImage: /^image\//i.test data.type
-      isText:  /^text\//i.test data.type
-      isJSON:  /application\/json/i.test data.type
-      isPDF:   /application\/pdf|application\/x-pdf/i.test data.type
       _downloadRoute:  data._downloadRoute or @downloadRoute
       _collectionName: data._collectionName or @collectionName
+    @_updateFileTypes ds
     ds._storagePath = data._storagePath or @storagePath(_.extend(data, ds))
     return ds


Firing events while loading

Firing events while loading

--- src/loading.coffee
+++ src/loading.coffee
@@ -109,6 +109,21 @@
 		loaderComponent: -> switch typeof @loader
 			when 'string' then Vue.component @loader
 			when 'object' then @loader
+
+	##############################################################################
+	watch:
+
+		# Fire events when assets finish loading
+		loading:         (bool) -> @$emit 'loading' if bool
+		loaded:          (bool) -> @$emit 'loaded' if bool
+		posterLoading:   (bool) -> @$emit 'poster-loading' if bool
+		posterLoaded:    (bool) -> @$emit 'poster-loaded' if bool
+		imageLoading:    (bool) -> @$emit 'image-loading' if bool
+		imageLoaded:     (bool) -> @$emit 'image-loaded' if bool
+		videoLoading:    (bool) -> @$emit 'video-loading' if bool
+		videoLoaded:     (bool) -> @$emit 'video-loaded' if bool
+		fallbackLoading: (bool) -> @$emit 'fallback-loading' if bool
+		fallbackLoaded:  (bool) -> @$emit 'fallback-loaded' if bool

 	##############################################################################
 	methods:


[Minor fix] Router now stops when quiz not found

--- app/lib/router.coffee
+++ app/lib/router.coffee
@@ -54,19 +54,21 @@
         # Find the quiz
         quizId = @params._id
         quiz = Quizzes.findOne quizId
-        if not quiz?
+        unless quiz?
           @render 'notFound'
           pause()
+          return

         # Check that the quiz has started and hasn't run out
         now = new Date()
         if not (quiz.startDate < now and now < quiz.endDate)
           @render 'notAvailable'
           pause()
+          return

         # Check if player is already playing a quiz
         currentQuizId = Session.get('currentQuizId')
-        if currentQuizId
+        if currentQuizId?
           # player is already playing a quiz
           if currentQuizId != @params._id
             # TODO: This should render an error message that the user is already


Create project_preferences to track tutorial completion if none exists

--- app/lib/tutorial.cjsx
+++ app/lib/tutorial.cjsx
@@ -54,6 +54,12 @@
                     now = new Date().toISOString()
                     if user?
                       user.get('project_preferences', project_id: project.id).then ([projectPreferences]) =>
+                        projectPreferences ?= apiClient.type('project_preferences').create({
+                          links: {
+                            project: '231'
+                          },
+                          preferences: {}
+                        })
                         projectPreferences.update 'preferences.tutorial_completed_at': now
                         projectPreferences.save()
                     else


Fix typos

Fix typos

--- app/lib/en-us.coffee
+++ app/lib/en-us.coffee
@@ -72,7 +72,7 @@
       '''

       g_03: '''
-        As well as bubbles and stars clusters there are other objects hidden throughout these images
+        As well as bubbles and star clusters there are other objects hidden throughout these images
         such as strange green objects seen near young stars (called EGOs)
         and never-before-seen galaxies that lie far beyond our own galaxy, hidden behind the stars and dust.
         If you see anything unusual in the images, make sure to mark it and give your best guess as to what it is.
@@ -534,7 +534,7 @@

     selectClusterTool:
       content: '''
-        Great! We also want to find stars clusters.
+        Great! We also want to find star clusters.
         Let's mark the cluster of bright stars off to the right.
       '''


Translation

Translation

--- app/locale/bg.coffee
+++ app/locale/bg.coffee
@@ -5,7 +5,7 @@
     no_mobile: "CodeCombat не е направен за мобилни устройства и може да не работи с тях!" # Warning that shows up on mobile devices
     play: "Играй" # The big play button that opens up the campaign view.
     old_browser: "О, не! Браузърът ти е твърде стар за CodeCombat. Съжалявам!" # Warning that shows up on really old Firefox/Chrome/Safari
-    old_browser_suffix: "Все пак можеш да опиваш, но най-вероятно няма да проработи."
+    old_browser_suffix: "Все пак можеш да опиташ, но най-вероятно няма да проработи."
     ipad_browser: "Лошa новинa: CodeCombat не работи в браузъра на iPad. Добра новина: Приложението ни за iPad изчаква одобрение от Apple."
     campaign: "Кампания"
     for_beginners: "За начинаещи"
@@ -42,8 +42,8 @@
   diplomat_suggestion:
     title: "Помогни да преведем CodeCombat!" # This shows up when a player switches to a non-English language using the language selector.
     sub_heading: "Имаме нужда от твоите езикови познания!"
-    pitch_body: "We develop CodeCombat in English, but we already have players all over the world. Many of them want to play in Bulgarian but don't speak English, so if you can speak both, please consider signing up to be a Diplomat and help translate both the CodeCombat website and all the levels into Bulgarian."
-    missing_translations: "Until we can translate everything into Bulgarian, you'll see English when Bulgarian isn't available."
+    pitch_body: "Ние разработихме CodeCombat на английски, но имаме играчи от целият свят. Много от тях искат да играят на български, защото не знаят английски, така че ако знаете и двата езика можете да се регистрирате като Дипломат и да помогнете в българският превод както на сайта на CodeCombat, така и на всички нива."
+    missing_translations: "До момента, в който всичко бъде преведено на български, някои неща ще са на английски."
     learn_more: "Научи повече за това как да станеш Дипломат"
     subscribe_as_diplomat: "Стани дипломат"

@@ -317,28 +317,28 @@
     tip_talk_is_cheap: "Приказките са вятър и мъгла. Покажи ми кода. - Линус Торвалдс"
     tip_first_language: "Най-пагубното нещо, което можеш да научиш е първият ти език за програмиране. - Alan Kay"
     tip_hardware_problem: "Въпр.: Колко програмиста са нужни, за да сменят електрическа крушка?  Отг.: Николко, това е хардуерен проблем."
-#    tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law."
-#    tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth"
-#    tip_brute_force: "When in doubt, use brute force. - Ken Thompson"
-#    tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..."
-#    tip_superpower: "Coding is the closest thing we have to a superpower."
-#    tip_control_destiny: "In real open source, you have the right to control your own destiny. - Linus Torvalds"
-#    tip_no_code: "No code is faster than no code."
-#    tip_code_never_lies: "Code never lies, comments sometimes do. — Ron Jeffries"
-#    tip_reusable_software: "Before software can be reusable it first has to be usable."
-#    tip_optimization_operator: "Every language has an optimization operator. In most languages that operator is ‘//’"
-#    tip_lines_of_code: "Measuring programming progress by lines of code is like measuring aircraft building progress by weight. — Bill Gates"
-#    tip_source_code: "I want to change the world but they would not give me the source code."
-#    tip_javascript_java: "Java is to JavaScript what Car is to Carpet. - Chris Heilmann"
-#    tip_move_forward: "Whatever you do, keep moving forward. - Martin Luther King Jr."
-#    tip_google: "Have a problem you can't solve? Google it!"
-#    tip_adding_evil: "Adding a pinch of evil."
-#    tip_hate_computers: "That's the thing about people who think they hate computers. What they really hate is lousy programmers. - Larry Niven"
-#    tip_open_source_contribute: "You can help CodeCombat improve!"
-#    tip_recurse: "To iterate is human, to recurse divine. - L. Peter Deutsch"
-#    tip_free_your_mind: "You have to let it all go, Neo. Fear, doubt, and disbelief. Free your mind. - Morpheus"
-#    tip_strong_opponents: "Even the strongest of opponents always has a weakness. - Itachi Uchiha"
-#    tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen."
+    tip_hofstadters_law: "Закон на Хофщадтер: Всяка работа продължава повече от колкото се очаква, дори дори когато законът на Хофщадтер е взет в предвид."
+    tip_premature_optimization: "Прибързаната оптимизиция е коренът на всяко зло. - Donald Knuth"
+    tip_brute_force: "Когато се съмняваш използвай груба сила. - Ken Thompson"
+    tip_extrapolation: "Има само два типа хора: тези, които могат да екстраполират от непълни данни..."
+    tip_superpower: "Програмирането е най-голямото ни доближаване до суперсилата."
+    tip_control_destiny: "При истинският отворен код всеки има правото да контролира собствената си съдба. - Линус Торвалдс"
+    tip_no_code: "Няма по-бърз код от отсъстващия."
+    tip_code_never_lies: "Кодът никога не лъже, коментарите - понякога. — Ron Jeffries"
+    tip_reusable_software: "Преди кодът да стане преизползваем, той трябва да е използваем."
+    tip_optimization_operator: "Всеки език има оптимизационен оператор. В повечето езици операторът е ‘//’"
+    tip_lines_of_code: "Да мериш работата на програмиста по брой редове е все едно да мериш построяването на самолет по теглото му. — Бил Гейтс"
+    tip_source_code: "Искам да променя света, но едва ли ще ми дадат сорсовете."
+    tip_javascript_java: "Java е за JavaScript това, което са компютрите за компотите. - Chris Heilmann (перифразирано)"
+    tip_move_forward: "каквото и да правиш, продължавай напред. - Мартин Лутър Кинг Мл."
+    tip_google: "Имаш проблем, който не можеш да решиш? Гугълни го!"
+    tip_adding_evil: "Добавяме щипка зло."
+    tip_hate_computers: "Какво да ви кажа за хората, които си мислят че мразят компютрите. Всъщност те мразят скапаните програмисти. - Лари Нивън"
+    tip_open_source_contribute: "И Вие можете да помогнете за подобряването на CodeCombat!"
+    tip_recurse: "Итерациите са човещина, рекурсиите са божествени. - L. Peter Deutsch"
+    tip_free_your_mind: "Трябва да им позволиш да си отидат Нео. Страхът, съмнението и липсата на вяра. Освободи съзнанието си. - Морфеус"
+    tip_strong_opponents: "Дори най-силният противник си има слабости. Винаги. - Itachi Uchiha"
+    tip_paper_and_pen: "Преди да почнеш с програмирането, винаги започни с планиране на хартия."

   game_menu:
     inventory_tab: "Инвентар"
@@ -347,19 +347,19 @@
     guide_tab: "Упътване"
     guide_video_tutorial: "Видео Упътване"
     guide_tips: "Съвети"
-#    multiplayer_tab: "Multiplayer"
+    multiplayer_tab: "Мултиплейър"
     auth_tab: "Записване"
-#    inventory_caption: "Equip your hero"
+    inventory_caption: "Екипирай героя си"
     choose_hero_caption: "Избери герой, език"
-#    save_load_caption: "... and view history"
+    save_load_caption: "... и вижте историята"
     options_caption: "Промени настройките"
-#    guide_caption: "Docs and tips"
+    guide_caption: "Документи и съвети"
     multiplayer_caption: "Играй с приятели!"
     auth_caption: "Запиши напредъка си."

   leaderboard:
-#    leaderboard: "Leaderboard"
-    view_other_solutions: "Виж други решения" # {change}
+    leaderboard: "Таблица на лидерите"
+    view_other_solutions: "Виж решенията на лидерите"
     scores: "Точки"
     top_players: "ТОП играчи според"
     day: "Днес"
@@ -371,18 +371,18 @@
     difficulty: "Трудност"
     gold_collected: "Събрано Злато"

-#  inventory:
-#    choose_inventory: "Equip Items"
-#    equipped_item: "Equipped"
-#    required_purchase_title: "Required"
-#    available_item: "Available"
-#    restricted_title: "Restricted"
-#    should_equip: "(double-click to equip)"
-#    equipped: "(equipped)"
-#    locked: "(locked)"
-#    restricted: "(restricted in this level)"
-#    equip: "Equip"
-#    unequip: "Unequip"
+  inventory:
+    choose_inventory: "Избери предмети"
+    equipped_item: "Избрано"
+    required_purchase_title: "Задължително"
+    available_item: "Достъпно"
+    restricted_title: "Недостъпно"
+    should_equip: "(двоен клик за обличане)"
+    equipped: "(облечено)"
+    locked: "(заключено)"
+    restricted: "(забранено за това ниво)"
+    equip: "Облечи"
+    unequip: "Съблечи"

   buy_gems:
     few_gems: "Няколко скъпоценни камъни"
@@ -393,9 +393,9 @@
     retrying: "Грешка в сървъра, пробвам отново."
     prompt_title: "Недостатъчно скъпоценни камъни"
     prompt_body: "Искате ли още?"
-#    prompt_button: "Enter Shop"
-#    recovered: "Previous gems purchase recovered. Please refresh the page."
-#    price: "x3500 / mo"
+    prompt_button: "Влез в Магазина"
+    recovered: "Предишните покупки на скъпоценни камъни са възстановени. Моля опреснете страницата."
+    price: "x3500 / месец"

   subscribe:
 #    comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"


no message

no message

--- app/locale/el.coffee
+++ app/locale/el.coffee
@@ -74,10 +74,10 @@
     anonymous: "Ανώνυμος Παίκτης"
     level_difficulty: "Δυσκολία: "
     campaign_beginner: "Εκστρατεία για Αρχάριους"
-#    awaiting_levels_adventurer_prefix: "We release new levels every week."
-#    awaiting_levels_adventurer: "Sign up as an Adventurer"
-#    awaiting_levels_adventurer_suffix: "to be the first to play new levels."
-#    adjust_volume: "Adjust volume"
+    awaiting_levels_adventurer_prefix: "Βγάζουμε νέα επίπεδα κάθε εβδομάδα."
+    awaiting_levels_adventurer: "Εγγράψου ως Εξερευνητής"
+    awaiting_levels_adventurer_suffix: "για να παίξεις πρώτος τα νέα επίπεδα."
+#    adjust_volume: "Ρύθμιση ήχου"
     choose_your_level: "Διάλεξε το επίπεδό σου" # The rest of this section is the old play view at /play-old and isn't very important.
     adventurer_prefix: "Μπορείτε να μεταβείτε σε οποιοδήποτε επίπεδο κάτω, ή να συζητήσετε για τις πίστες στο "
     adventurer_forum: "Φόρουμ του Adventurer"
@@ -94,9 +94,9 @@
     campaign_classic_algorithms_description: "... όπου μαθαίνεις του πιο δημοφιλείς αλγορίθμους της Επιστήμης της Πληροφορικής."

   share_progress_modal:
-#    blurb: "You’re making great progress! Tell your parent how much you've learned with CodeCombat."
-#    email_invalid: "Email address invalid."
-#    form_blurb: "Enter your parent's email below and we’ll show them!"
+    blurb: "Επιδεικνύεις εξαιρετική πρόοδο! Πες στους γονείς σου πόσα πολλά έχεις μάθει με το CodeCombat."
+    email_invalid: "Λανθασμένη διεύθυνση ηλεκτρονικού ταχυδρομείου."
+    form_blurb: "Εισάγαγε τη διεύθυνση ηλ. ταχυδρομείου των γονέων σου, και θα τους ενημερώσουμε εμείς!"
     form_label: "Διεύθυνση ηλεκτρονικού ταχυδρομείου"
     placeholder: "Διεύθυνση ηλεκτρονικού ταχυδρομείου"
     title: "Εξαιρετική Δουλειά, Μαθητευόμενε"
@@ -115,7 +115,7 @@
     signup_switch: "Θέλεις να ανοίξεις λογαριασμό;"

   signup:
-    email_announcements: "Λαμβάνετε ανακοινώσεις μέσω email"
+    email_announcements: "Θέλω να λαμβάνω ανακοινώσεις μέσω email"
     creating: "Δημιουργία Λογαριασμού..."
     sign_up: "Εγγραφή"
     log_in: "Σύνδεση με κωδικό"
@@ -151,14 +151,14 @@
 #    fork: "Fork"
     play: "Παίξε" # When used as an action verb, like "Play next level"
     retry: "Προσπάθησε εκ νέου"
-#    actions: "Actions"
+    actions: "Ενέργειες"
     info: "Πληροφορίες"
     help: "Βοήθεια"
     watch: "Παρακολούθηση"
     unwatch: "Παύση παρακολούθησης"
     submit_patch: "Αποστολή Επιδιόρθωσης"
     submit_changes: "Αποστολή Αλλαγών"
-#    save_changes: "Save Changes"
+    save_changes: "Αποθήκευση Αλλαγών"

   general:
     and: "και"
@@ -203,7 +203,7 @@
     medium: "Μέτριο"
     hard: "Δύσκολο"
     player: "Παίκτης"
-#    player_level: "Level" # Like player level 5, not like level: Dungeons of Kithgard
+    player_level: "Επίπεδο" # Like player level 5, not like level: Dungeons of Kithgard
     warrior: "Πολεμιστής"
 #    ranger: "Ranger"
     wizard: "Μάγος"
@@ -234,10 +234,10 @@
     restart: "Επανεκκίνηση"
     goals: "Στόχοι"
     goal: "Στόχος"
-#    running: "Running..."
+    running: "Εκτέλεση..."
     success: "Επιτυχία!"
 #    incomplete: "Incomplete"
-#    timed_out: "Ran out of time"
+    timed_out: "Ο χρόνος τελείωσε"
 #    failing: "Failing"
     action_timeline: "Χρονοδιάγραμμα δράσης"
     click_to_select: "Κάντε κλικ σε μια μονάδα για να το επιλέξετε."
@@ -370,9 +370,9 @@
 #    view_other_solutions: "View Leaderboards"
 #    scores: "Scores"
 #    top_players: "Top Players by"
-#    day: "Today"
-#    week: "This Week"
-#    all: "All-Time"
+    day: "Σήμερα"
+    week: "Τούτη την εβδομάδα"
+    all: "Όλων των εποχών"
 #    time: "Time"
 #    damage_taken: "Damage Taken"
 #    damage_dealt: "Damage Dealt"
@@ -406,11 +406,11 @@
 #    price: "x3500 / mo"

 #  subscribe:
-#    comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"
-#    feature1: "80+ basic levels across 4 worlds"
-#    feature2: "7 powerful new heroes with unique skills!"
-#    feature3: "60+ bonus levels"
-#    feature4: "3500 bonus gems every month!"
+    comparison_blurb: "Βελτίωση τις δεξιότητές σου με μια συνδρομή στο CodeCombat!"
+    feature1: "80+ βασικά επίπεδα εκτεινόμενα σε 4 κόσμους"
+    feature2: "7 ισχυροί νέοι ήρωες με μοναδικές δεξιότητες!"
+    feature3: "60+ επί πλέον επίπεδα"
+    feature4: "3500 bonus gems every month!"
 #    feature5: "Video tutorials"
 #    feature6: "Premium email support"
 #    feature7: "Private Clans"
@@ -420,20 +420,20 @@
 #    unsubscribe: "Unsubscribe"
 #    confirm_unsubscribe: "Confirm Unsubscribe"
 #    never_mind: "Never Mind, I Still Love You"
-#    thank_you_months_prefix: "Thank you for supporting us these last"
-#    thank_you_months_suffix: "months."
-#    thank_you: "Thank you for supporting CodeCombat."
-#    sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
-#    unsubscribe_feedback_placeholder: "O, what have we done?"
-#    parent_button: "Ask your parent"
-#    parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
-#    parent_email_input_invalid: "Email address invalid."
-#    parent_email_input_label: "Parent email address"
-#    parent_email_input_placeholder: "Enter parent email"
-#    parent_email_send: "Send Email"
-#    parent_email_sent: "Email sent!"
-#    parent_email_title: "What's your parent's email?"
-#    parents: "For Parents"
+    thank_you_months_prefix: "Ευχαριστούμε για την υποστήριξή σου τους τελευταίους"
+    thank_you_months_suffix: "μήνες."
+    thank_you: "Ευχαριστούμε για την υποστήριξή σου στο CodeCombat."
+    sorry_to_see_you_go: "Λυπούμαστε που φεύγεις! Παρακαλούμε να μας πεις σε τί θα μπορούσαμε να βελτιωθούμε."
+    unsubscribe_feedback_placeholder: "Ω, μα σε τί φταίξαμε;"
+    parent_button: "Ρώτησε τους γονείς σου"
+    parent_email_description: "Θα τους στείλουμε μήνυμα, ώστε να σου αγοράσουν μια συνδρομή στο CodeCombat."
+    parent_email_input_invalid: "Η διεύθυνση ηλεκτρονικού ταχυδρομείου δεν είναι σε έγκυρη μορφή."
+    parent_email_input_label: "Διεύθυνση ηλεκτρονικού ταχυδρομείου γονέων"
+    parent_email_input_placeholder: "Εισάγαγε τη διεύθυνση ηλεκτρονικού ταχυδρομείου των γονιών σου"
+    parent_email_send: "Αποστολή μηνύματος"
+    parent_email_sent: "Το μήνυμα εστάλη!"
+    parent_email_title: "Ποιά είναι η διεύθυνση ηλεκτρονικού ταχυδρομείου των γονιών σου;"
+    parents: "Προς τους γονείς"
 #    parents_title: "Dear Parent: Your child is learning to code. Will you help them continue?"
 #    parents_blurb1: "Your child has played __nLevels__ levels and learned programming basics. Help cultivate their interest and buy them a subscription so they can keep playing."
 #    parents_blurb1a: "Computer programming is an essential skill that your child will undoubtedly use as an adult. By 2020, basic software skills will be needed by 77% of jobs, and software engineers are in high demand across the world. Did you know that Computer Science is the highest-paid university degree?"
@@ -495,8 +495,8 @@
     attack: "Επίθεση" # Can also translate as "Attack"
     health: "Υγεία"
     speed: "Ταχύτητα"
-#    regeneration: "Regeneration"
-#    range: "Range" # As in "attack or visual range"
+    regeneration: "Αναζωογόνηση"
+    range: "Βεληνεκές" # As in "attack or visual range"
 #    blocks: "Blocks" # As in "this shield blocks this much damage"
 #    backstab: "Backstab" # As in "this dagger does this much backstab damage"
     skills: "Ικανότητες"
@@ -506,11 +506,11 @@
 #    health_1: "Gains"
 #    health_2: "of listed"
 #    health_3: "armor health."
-#    speed_1: "Moves at"
-#    speed_2: "meters per second."
-#    available_for_purchase: "Available for Purchase" # Shows up when you have unlocked, but not purchased, a hero in the hero store
+    speed_1: "Κινείται"
+    speed_2: "μέτρα το δευτερόλεπτο."
+    available_for_purchase: "Διαθέσιμος για αγορά" # Shows up when you have unlocked, but not purchased, a hero in the hero store
     level_to_unlock: "Ξεκλειδώνει στο επίπεδο:" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
-#    restricted_to_certain_heroes: "Only certain heroes can play this level."
+    restricted_to_certain_heroes: "Μόνο συγκεκριμένοι ήρωες μπορούν να παίξουν το επίπεδο αυτό."

 #  skill_docs:
 #    writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this
@@ -716,8 +716,8 @@
 #    enter: "Enter"
 #    escape: "Escape"
 #    shift: "Shift"
-#    run_code: "Run current code."
-#    run_real_time: "Run in real time."
+    run_code: "Εκτέλεση τρέχοντος κώδικα."
+    run_real_time: "Εκτέλεση σε πραγματικό χρόνο."
 #    continue_script: "Continue past current script."
 #    skip_scripts: "Skip past all skippable scripts."
 #    toggle_playback: "Toggle play/pause."
@@ -1178,43 +1178,43 @@
     multiplayer_coming_soon: "Περισσότερα multiplayer χαρακτιριστηκα προσεχως!"
 #    multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard."

-#  legal:
-#    page_title: "Legal"
-#    opensource_intro: "CodeCombat is completely open source."
-#    opensource_description_prefix: "Check out "
-#    github_url: "our GitHub"
-#    opensource_description_center: "and help out if you like! CodeCombat is built on dozens of open source projects, and we love them. See "
-#    archmage_wiki_url: "our Archmage wiki"
-#    opensource_description_suffix: "for a list of the software that makes this game possible."
-#    practices_title: "Respectful Best Practices"
-#    practices_description: "These are our promises to you, the player, in slightly less legalese."
-#    privacy_title: "Privacy"
-#    privacy_description: "We will not sell any of your personal information."
-#    security_title: "Security"
-#    security_description: "We strive to keep your personal information safe. As an open source project, our site is freely open to anyone to review and improve our security systems."
-#    email_title: "Email"
-#    email_description_prefix: "We will not inundate you with spam. Through"
-#    email_settings_url: "your email settings"
-#    email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time."
-#    cost_title: "Cost"
-#    cost_description: "CodeCombat is free to play for all of its core levels, with a $9.99 USD/mo subscription for access to extra level branches and 3500 bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
-#    copyrights_title: "Copyrights and Licenses"
-#    contributor_title: "Contributor License Agreement"
-#    contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our"
-#    cla_url: "CLA"
-#    contributor_description_suffix: "to which you should agree before contributing."
-#    code_title: "Code - MIT"
-#    code_description_prefix: "All code owned by CodeCombat or hosted on codecombat.com, both in the GitHub repository or in the codecombat.com database, is licensed under the"
-#    mit_license_url: "MIT license"
-#    code_description_suffix: "This includes all code in Systems and Components that are made available by CodeCombat for the purpose of creating levels."
-#    art_title: "Art/Music - Creative Commons "
-#    art_description_prefix: "All common content is available under the"
-#    cc_license_url: "Creative Commons Attribution 4.0 International License"
-#    art_description_suffix: "Common content is anything made generally available by CodeCombat for the purpose of creating Levels. This includes:"
-#    art_music: "Music"
-#    art_sound: "Sound"
-#    art_artwork: "Artwork"
-#    art_sprites: "Sprites"
+  legal:
+    page_title: "Νομικά"
+    opensource_intro: "Το CodeCombat είναι ένα έργο ανοιχτού κώδικα."
+    opensource_description_prefix: "Επισκέψου μας "
+    github_url: "στο GitHub"
+    opensource_description_center: "και, αν θες, βοήθησέ μας! Το CodeCombat βασίζεται σε δεκάδες έργα ανοιχτού κώδικα, τα οποία αγαπάμε. Δες "
+    archmage_wiki_url: "τη συνεργατική σελίδα Archmage"
+    opensource_description_suffix: "για τη λίστα των λογισμικών που κάνουν εφικτό το παρόν παιχνίδι."
+    practices_title: "Καλές Πρακτικές"
+    practices_description: "Αυτές είναι οι υποσχέσεις μας προς εσένα, τον παίκτη, σε απλή γλώσσα."
+    privacy_title: "Ιδιωτικότητα"
+    privacy_description: "Δεν πρόκειται να εμπορευτούμε τα προσωπικά σου στοιχεία."
+    security_title: "Ασφάλεια"
+    security_description: "Καταβάλλουμε ιδιαίτερη προσπάθεια να κρατάμε τα προσωπικά σου στοιχεία ασφαλή. Ως έργο ανοιχτού κώδικα, ο ιστότοπός μας είναι ανοιχτός σε οποιονδήποτε θέλει να ελέγξει και να βελτιώσει τους μηχανισμούς ασφαλείας μας."
+    email_title: "Ηλεκτρονικό ταχυδρομείο"
+    email_description_prefix: "Δεν θα σε κατακλύσουμε με ενοχλητικά μηνύματα. Μέσω"
+    email_settings_url: "των ρυθμίσεων ηλεκτρονικού ταχυδρομείου"
+    email_description_suffix: "ή μέσω των συνδέσμων που σου στέλνουμε στα μηνύματα, μπορείς να αλλάξεις εύκολα τις προτιμήσεις σου οποιαδήποτε στιγμή και να πάψεις να λαμβάνεις ειδοποιήσεις από εμάς."
+    cost_title: "Κόστος"
+    cost_description: "Το CodeCombat είναι δωρεάν για να παίξεις όλα τα βασικά του επίπεδα, ενώ με μια συνδρομή $9.99 δολαρίων/μήνα αποκτάς πρόσβαση σε επί πλέον επίπεδα και λαμβάνεις δώρο 3500 gems ανά μήνα. Μπορείς να ακυρώσεις τη συνδρομή με μια απλή κίνηση, και βέβαια προσφέρουμε εγγύηση επιστροφής χρημάτων στο 100%."
+    copyrights_title: "Δικαιώματα Πνευματικής Ιδιοκτησίας και Άδειες Χρήσης"
+    contributor_title: "Σύμβαση Άδειας Χρήσης Συντελεστή"
+    contributor_description_prefix: "Όλες οι συνεισφορές, στον ιστότοπο και στο αποθετήριό μας στο GitHub, υπόκεινται στη σύμβαση"
+    cla_url: "CLA"
+    contributor_description_suffix: "με την οποία θα πρέπει να συμφωνήσεις προτού συνεισφέρεις."
+    code_title: "Κώδικας - MIT"
+    code_description_prefix: "Όλος ο πηγαίος κώδικας που ανήκει στο CodeCombat ή φιλοξενείται στο codecombat.com, τόσο στο αποθετήριο στο GitHub όσο και στη βάση δεδομένων που τρέχει στο codecombat.com, υπόκειται στην"
+    mit_license_url: "Άδεια Χρήσης MIT"
+#    code_description_suffix: "Περιλαμβάνεται ο κώδικας και τα εργαλεία που διατίθενται από το CodeCombat με σκοπό τη δημιουργία επιπέδων."
+#    art_title: "Τέχνη/Μουσική - Creative Commons "
+#    art_description_prefix: "Όλο το κοινό περιεχόμενο υπόκειται στην άδεια"
+#    cc_license_url: "Αναφορά Δημιουργού 4.0 Διεθνές"
+#    art_description_suffix: "Ως κοινό εννοείται το περιεχόμενο που γίνεται δημόσια διαθέσιμο από το CodeCombat με σκοπό τη δημιουργία επιπέδων, και περιλαμβάνει τα εξής:"
+#    art_music: "Μουσική"
+#    art_sound: "Ήχοι"
+#    art_artwork: "Γραφικά"
+#    art_sprites: "Φιγούρες"
 #    art_other: "Any and all other non-code creative works that are made available when creating Levels."
 #    art_access: "Currently there is no universal, easy system for fetching these assets. In general, fetch them from the URLs as used by the site, contact us for assistance, or help us in extending the site to make these assets more easily accessible."
 #    art_paragraph_1: "For attribution, please name and link to codecombat.com near where the source is used or where appropriate for the medium. For example:"