Make it possible to copy JSONs

Make it possible to copy JSONs

When pressing escape the results shouldn't close anymore...

--- lib/modals/json-modal.coffee
+++ lib/modals/json-modal.coffee
@@ -2,30 +2,34 @@
     @Show: (json) ->
         unless typeof(json) == 'object'
             try
-                json = JSON.parse(json)
+                json = JSON.stringify JSON.parse(json), null, '  '
             catch
-                json = "\"#{json}\""
-                try
-                    json = JSON.parse(json)
-                catch
-                    console.error 'could not parse json'
-                    return
+                json = json

         return new Promise((fulfil, reject) ->
             element = document.createElement('div')
             element.classList.add('hoff-dialog')
             element.classList.add('native-key-bindings')
-            jsonElement = element.appendChild document.createElement('pre')
+            jsonElement = element.appendChild document.createElement('textarea')
             jsonElement.classList.add('native-key-bindings')
             jsonElement.style['overflow'] = 'auto'
+            jsonElement.style['width'] = '100%'
+            jsonElement.style['height'] = '600px'
+            jsonElement.style['font-family'] = 'Menlo'
+            jsonElement.style['border'] = 'none'
+            jsonElement.style['background'] = 'transparent'
+            jsonElement.style['white-space'] = 'pre'
+            jsonElement.style['overflow-wrap'] = 'normal'
+            jsonElement.style['overflow-x'] = 'scroll'
+            jsonElement.wrap = 'soft'
             jsonElement.classList.add 'force-select'
-            jsonElement.textContent = JSON.stringify json, null, '  '
+            jsonElement.value = json
             modal = atom.workspace.addModalPanel(item: element, visible: true)

-            listener = (e) =>
-                if e.keyCode == 27
-                    document.body.removeEventListener 'keydown', listener
+            atom.commands.add(jsonElement, {
+                'core:cancel': (event) =>
                     modal.destroy()
-
-            document.body.addEventListener 'keydown', listener
+                    event.stopPropagation()
+            })
+            jsonElement.focus()
         )


RC: use rc database

RC: use rc database

--- config/main.rc.coffee
+++ config/main.rc.coffee
@@ -7,7 +7,7 @@

 # PROD
 # mongo = 'PROD-koding:34W4BXx595ib3J72k5Mh@web0.beta.system.aws.koding.com:27017/beta_koding'
-mongo = 'PROD-koding:34W4BXx595ib3J72k5Mh@db-m0.prod.aws.koding.com:27017/beta_koding'
+mongo = 'PROD-koding:34W4BXx595ib3J72k5Mh@dbrc-m0.prod.aws.koding.com:27017/beta_koding'

 # RabbitMQ Host
 rabbit_host = 'rabbit-b.prod.aws.koding.com'


Pass parent element to functions in GOM children

--- lib/mixins/render.coffee
+++ lib/mixins/render.coffee
@@ -4,18 +4,18 @@

   {notAttr, emptyTags} = $

-  $.render = render = (nodes) ->
-    return _render nodes if !(nodes instanceof Array)
+  $.render = render = (nodes, parent) ->
+    return _render nodes, parent if !(nodes instanceof Array)
     result = ""
     for node in nodes
-      result += _render node
+      result += _render node, parent
     result

-  _render = (node) ->
+  _render = (node, parent) ->
     return '' if !node
     return node if typeof node is 'string'
     return render node if node instanceof Array
-    return render node() if typeof node is 'function'
+    return render node(parent) if typeof node is 'function'

     {tag,attributes,children} = node

@@ -26,16 +26,16 @@

     return "" if !tag
     return """<#{tag}#{_renderAttr(attributes)}/>""" if emptyTags.indexOf(tag) >= 0
-    return """<#{tag}#{_renderAttr(attributes)}>#{_renderChildren(children)}</#{tag}>"""
+    return """<#{tag}#{_renderAttr(attributes)}>#{_renderChildren(children, node)}</#{tag}>"""

-  _renderChildren = (children) ->
+  _renderChildren = (children, parent) ->
     return '' if children?.length <= 0
     html = ''
     for child in children
       if typeof child is 'string'
         html += child
       else
-        html += render child
+        html += render child, parent
     return html

   _renderStyles = (o) ->


Handle null editor on save

Handle null editor on save

--- lib/nsync/initializer.coffee
+++ lib/nsync/initializer.coffee
@@ -45,7 +45,7 @@

 onSave = ->
   editor = atomHelper.findActiveTextEditor()
-  path = editor.getPath()
+  path = editor?.getPath()

   if not path
     # TODO: untitled editor is saved


removing outdated arguments tests

removing outdated arguments tests

--- test/test_arguments.coffee
+++ test/test_arguments.coffee
@@ -17,17 +17,6 @@
 ) is 100, 'newline delimited arguments')

-func: ->
-  arguments: 25
-  arguments
-
-ok func(100) is 25, 'arguments as a regular identifier'
-
-
-this.arguments: 10
-ok @arguments is 10, 'arguments accessed as a property'
-
-
 sum_of_args: ->
   sum: 0
   sum += val for val in arguments


Fix linter error

Fix linter error

--- lib/snippets-provider.coffee
+++ lib/snippets-provider.coffee
@@ -34,7 +34,7 @@
   onDidInsertSuggestion: ({editor}) ->
     atom.commands.dispatch(atom.views.getView(editor), 'snippets:expand')

-ascendingPrefixComparator = (a, b) ->a.prefix  - b.prefix
+ascendingPrefixComparator = (a, b) -> a.prefix  - b.prefix

 firstCharsEqual = (str1, str2) ->
   str1[0].toLowerCase() is str2[0].toLowerCase()


Refactor constant

Refactor constant

--- source/core/constants.coffee
+++ source/core/constants.coffee
@@ -11,8 +11,7 @@
 Atoms.Core.Constants =

   ANIMATION:
-    START : "webkitAnimationEnd mozAnimationEnd AnimationEnd"
-    END   : "webkitAnimationEnd"
+    END   : "webkitAnimationEnd mozAnimationEnd AnimationEnd"

   APPEND:  "append"


BOS-716: Improvements after code review

BOS-716: Improvements after code review

--- app/assets/javascripts/models/sortable_table.js.coffee
+++ app/assets/javascripts/models/sortable_table.js.coffee
@@ -20,14 +20,14 @@
   observeRows: ->
     @dom.find('tbody tr td:not(.disable-select)').click (event) =>
       el = $(event.target)
-      wrapper = el.parents('.beacons-table')
+      wrapper = el.closest('.beacons-table')

       if wrapper.hasClass('selectable-rows')
         el.parent().find('input:checkbox').trigger('click')

       else if wrapper.hasClass('linked-rows')
         locationTarget = el.parent().find('.btn-action-edit').attr('href')
-        window.location.href = locationTarget if locationTarget
+        window.location.href = locationTarget if (locationTarget or '').length

   observeSelectAllCheckboxChange: ->
     @dom.find('#select_all').click (event)=>


fixed spacing

fixed spacing

--- app/assets/javascripts/pages/agent-show-page.js.coffee
+++ app/assets/javascripts/pages/agent-show-page.js.coffee
@@ -1,6 +1,5 @@
 class @AgentShowPage
   constructor: ->
-
     $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", @fetchLogs
     $(".agent-show #logs .clear").on "click", @clearLogs
     $(".agent-show #memory .clear").on "click", @clearMemory
@@ -43,14 +42,12 @@
           $("#logs .refresh, #logs .clear").show()

   toggleMemory: ->
-
     if $('#memorypanel').hasClass('hidden')
       $('#memorypanel').removeClass 'hidden'
     else
       $('#memorypanel').addClass 'hidden'

   clearMemory: (e) ->
-
     if confirm("Are you sure you want to clear memory of this Agent?")
       agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
       e.preventDefault()
@@ -63,7 +60,6 @@
         .fail ->
           $("#memory .spinner").fadeOut ->
             $("#memory .clear").css(display: 'inline-block')
-
-
+
 $ ->
   Utils.registerPage(AgentShowPage, forPathsMatching: /^agents\/\d+/)


Allow newlines in comment field. Meta/ctrl-enter submits

--- app/assets/javascripts/views/discussion_view.js.coffee
+++ app/assets/javascripts/views/discussion_view.js.coffee
@@ -14,7 +14,7 @@
     @commentsList.scroll => @assignAutoscroll()
     @commentsList.onInsert = => @scrollToBottom() if @autoScroll
     @textarea.keydown (e) =>
-      if e.keyCode == 13 && !e.ctrlKey # enter (not ctrl-enter though)
+      if e.keyCode == 13 && (e.metaKey || e.ctrlKey)
         e.preventDefault()
         @createComment()


feat(web): do nothing when same dialog selected;

--- apps/actor-web/app/shared/chats/chatsController.coffee
+++ apps/actor-web/app/shared/chats/chatsController.coffee
@@ -18,15 +18,15 @@
     console.log '[AW]ChatsController renderDialogs'
     console.log '[AW]ChatsController renderDialogs: dialogs:', dialogs
     @$timeout =>
-#      list.forEach (item) ->
-#        console.log item
-#        @getUnreadMessages (item.peer.id)
       @list = dialogs
       @$rootScope.$broadcast 'renderDialogs'

   selectChat: (peer) ->
     console.log '[AW]ChatsController selectChat'
     console.log '[AW]ChatsController selectChat: peer:', peer
+    if @actorService.currentPeer == peer
+      console.log '[AW]ChatsController selectChat: this peer already selected.'
+      return
     if @actorService.currentPeer
       console.log '[AW]ChatsController selectChat: conversation already opened: unbind...'
       @actorService.unbindChat @actorService.currentPeer


Stop using an extra anchor element for normalization

--- src/modules/link-tooltip.coffee
+++ src/modules/link-tooltip.coffee
@@ -2,7 +2,6 @@
 Tooltip  = require('./tooltip')
 _        = Quill.require('lodash')
 dom      = Quill.require('dom')
-anchorEl = document.createElement('a')

 class LinkTooltip extends Tooltip
   @DEFAULTS:
@@ -84,6 +83,7 @@
       )
     else
       @link.href = url
+      url = @link.href # read back the url for further normalization
       text = if url.length > @options.maxLength then url.slice(0, @options.maxLength) + '...' else url
       dom(@link).text(text)
     dom(@container).toggleClass('editing', edit)
@@ -120,8 +120,7 @@

   _normalizeURL: (url) ->
     url = 'http://' + url unless /^(https?:\/\/|mailto:)/.test(url)
-    anchorEl.href = url
-    return anchorEl.href
+    return url

   _suggestURL: (range) ->
     text = @quill.getText(range)


Remove call to keypress method causing tokens to be removed in FF

--- lib/sensu-dashboard/assets/javascripts/views/auto_complete/auto_complete_field.coffee
+++ lib/sensu-dashboard/assets/javascripts/views/auto_complete/auto_complete_field.coffee
@@ -82,8 +82,6 @@
         e.preventDefault()
         return

-      super
-
     _submit: ->
       @trigger("submit")


CHG: default request params of tree item

--- lib/assets/javascripts/tao/ui/tree/item.coffee
+++ lib/assets/javascripts/tao/ui/tree/item.coffee
@@ -79,6 +79,7 @@
       url: @remote.url
       type: 'get'
       data: _.extend {}, @remote.params,
+        tree_item_id: @taoId
         depth: @depth
         selectable: @selectable || undefined
         selected: @selected || undefined


documented tabInstance.validate()

documented tabInstance.validate()

--- client/prefs-ui.coffee
+++ client/prefs-ui.coffee
@@ -4,11 +4,12 @@
 # This module implements the Preferences dialog.
 # The actual content of tabs is in separate modules: prefs-*.coffee
 # Each of them can export the following properties:
-#   name     a string used as the tab's title
-#   init()   called only once, when Preferences is opened for the first time
-#   load()   called every time Preferences is opened
-#   save()   called when Save is pressed
-#   resize() called when the Preferences dialog is resized or the tab is selected
+#   name       a string used as the tab's title
+#   init()     called only once, when Preferences is opened for the first time
+#   load()     called every time Preferences is opened
+#   validate() should return a falsey value on failure, checked before attempting to save() any of the tabs
+#   save()     called when Save is pressed
+#   resize()   called when the Preferences dialog is resized or the tab is selected

 tabs = [
   require './prefs-glyphs'


cistrans: Remove unneeded console.log call

--- cistrans/cistrans.coffee
+++ cistrans/cistrans.coffee
@@ -124,8 +124,6 @@
   # maximum lod score
   maxlod = d3.max(data.peaks, (d) -> d.lod)

-  console.log("before sorting: #{data.peaks[0].lod}")
-
   # sort peaks to have increasing LOD score
   data.peaks = data.peaks.sort (a,b) ->
     return if a.lod < b.lod then -1 else +1


CLI/docs making recoursive docs processing work

--- cli/commands/docs.coffee
+++ cli/commands/docs.coffee
@@ -20,11 +20,15 @@
 #
 generate_docs_for = (filename)->
   doc_file = "#{cwd}/#{filename.replace(/^src\//, docs)}.md"
+  doc_dir  = doc_file.replace(/\/[^\/]+$/, '')
+
   index.push(doc_file)

   sout ("   ■ ".yellow + filename + ' ').ljust(60, '.') + ' '

   return sout("Already exists\n".yellow) if path.existsSync(doc_file)
+
+  fs.mkdirSync(doc_dir, 0o0755) unless path.existsSync(doc_dir)

   content = generate.from_file("#{cwd}/#{filename}")
   return sout("Not supported yet".yellow) if content is false
@@ -41,7 +45,7 @@
 loop_directory_recursively = (dirname)->
   for filename in fs.readdirSync("#{cwd}/#{dirname}")
     if fs.statSync("#{cwd}/#{dirname}/#{filename}").isDirectory()
-      loop_directory_recursively("#{cwd}/#{dirname}/#{filename}")
+      loop_directory_recursively("#{dirname}/#{filename}")
     else
       generate_docs_for("#{dirname}/#{filename}")


live update auto trigger button scroll is 50px now.

--- client/landing/app/Applications/Activity.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppView.coffee
@@ -34,7 +34,7 @@
     @feedWrapper.ready =>
       @activityHeader = @feedWrapper.controller.activityHeader
       @on 'scroll', (event) =>
-        if event.delegateTarget.scrollTop > 0
+        if event.delegateTarget.scrollTop > 50
           @activityHeader.setClass "scrolling-up-outset"
           @activityHeader.liveUpdateButton.setValue off
         else


Removed duplicated doc line

Removed duplicated doc line

--- src/scripts/meme_captain.coffee
+++ src/scripts/meme_captain.coffee
@@ -10,7 +10,6 @@
 #   hubot <text> (SUCCESS|NAILED IT) - Generates success kid with the top caption of <text>
 #   hubot <text> ALL the <things> - Generates ALL THE THINGS
 #   hubot <text> TOO DAMN <high> - Generates THE RENT IS TOO DAMN HIGH guy
-#   hubot Not sure if <text> or <text> - Generates Futurama Fry
 #   hubot Yo dawg <text> so <text> - Generates Yo Dawg
 #   hubot All your <text> are belong to <text> - All your <text> are belong to <text>
 #   hubot If <text>, <word that can start a question> <text>? - Generates Philosoraptor
@@ -22,7 +21,7 @@
 #   hubot Aliens guy <text> - Aliens guy weighs in on something
 #   hubot Brace yourself <text> - Ned Stark braces for <text>
 #   hubot Iron Price <text> - To get <text>? Pay the iron price!
-#   hubot not sure if <something> or <something else> - Generates a Futurama Fry meme
+#   hubot Not sure if <something> or <something else> - Generates a Futurama Fry meme
 #
 # Author:
 #   bobanj


Fixed not sure if meme.  Added ability bring in memes from imgur.  Added Pay the Iron Price, Brace Yourself, Aliens guy, and One does not simply memes

--- src/scripts/meme_captain.coffee
+++ src/scripts/meme_captain.coffee
@@ -8,77 +8,97 @@
 #   hubot Y U NO <text> - Generates the Y U NO GUY with the bottom caption of <text>
 #   hubot I don't always <something> but when i do <text> - Generates The Most Interesting man in the World
 #   hubot <text> (SUCCESS|NAILED IT) - Generates success kid with the top caption of <text>
-#   hubot <text> ALL the <things>    - Generates ALL THE THINGS
+#   hubot <text> ALL the <things> - Generates ALL THE THINGS
 #   hubot <text> TOO DAMN <high> - Generates THE RENT IS TOO DAMN HIGH guy
 #   hubot Not sure if <text> or <text> - Generates Futurama Fry
 #   hubot Yo dawg <text> so <text> - Generates Yo Dawg
-#   hubot ALL YOUR <text> ARE BELONG TO US - Generates Zero Wing with the caption of <text>
+#   hubot All your <text> are belong to <text> - All your <text> are belong to <text>
 #   hubot If <text>, <word that can start a question> <text>? - Generates Philosoraptor
 #   hubot <text>, BITCH PLEASE <text> - Generates Yao Ming
 #   hubot <text>, COURAGE <text> - Generates Courage Wolf
 #   hubot ONE DOES NOT SIMPLY <text> - Generates Boromir
 #   hubot IF YOU <text> GONNA HAVE A BAD TIME - Ski Instructor
 #   hubot IF YOU <text> TROLLFACE <text> - Troll Face
+#   hubot Aliens guy <text> - Aliens guy weighs in on something
+#   hubot Brace yourself <text> - Ned Stark braces for <text>
+#   hubot Iron Price <text> - To get <text>? Pay the iron price!
+#   hubot not sure if <something> or <something else> - Generates a Futurama Fry meme
 #
 # Author:
 #   bobanj

 module.exports = (robot) ->
   robot.respond /Y U NO (.+)/i, (msg) ->
-    memeGenerator msg, 'y_u_no.jpg', 'Y U NO', msg.match[1], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/y_u_no.jpg', 'Y U NO', msg.match[1], (url) ->
+      msg.send url
+
+  robot.respond /iron price (.+)/i, (msg) ->
+    memeGenerator msg, 'http://imgur.com/nqVZQel.jpg', msg.match[1], 'Pay the iron price', (url) ->
+      msg.send url
+
+  robot.respond /aliens guy (.+)/i, (msg) ->
+    memeGenerator msg, 'http://memecaptain.com/aliens.jpg', msg.match[1], '', (url) ->
+      msg.send url
+
+  robot.respond /brace yourself (.+)/i, (msg) ->
+    memeGenerator msg, 'http://i.imgur.com/cOnPlV7.jpg', 'Brace Yourself', msg.match[1], (url) ->
       msg.send url

   robot.respond /(.*) (ALL the .*)/i, (msg) ->
-    memeGenerator msg, 'all_the_things.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/all_the_things.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(I DON'?T ALWAYS .*) (BUT WHEN I DO,? .*)/i, (msg) ->
-    memeGenerator msg, 'most_interesting.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/most_interesting.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(.*)(SUCCESS|NAILED IT.*)/i, (msg) ->
-    memeGenerator msg, 'success_kid.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/success_kid.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(.*) (\w+\sTOO DAMN .*)/i, (msg) ->
-    memeGenerator msg, 'too_damn_high.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/too_damn_high.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(NOT SURE IF .*) (OR .*)/i, (msg) ->
-    memeGenerator msg, 'fry.png', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/fry.png', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(YO DAWG .*) (SO .*)/i, (msg) ->
-    memeGenerator msg, 'xzibit.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/xzibit.jpg', msg.match[1], msg.match[2], (url) ->
+      msg.send url
+
+  robot.respond /(All your .*) (are belong to .*)/i, (msg) ->
+    memeGenerator msg, 'http://i.imgur.com/gzPiQ8R.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(.*)\s*BITCH PLEASE\s*(.*)/i, (msg) ->
-    memeGenerator msg, 'yao_ming.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/yao_ming.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(.*)\s*COURAGE\s*(.*)/i, (msg) ->
-    memeGenerator msg, 'courage_wolf.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/courage_wolf.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /ONE DOES NOT SIMPLY (.*)/i, (msg) ->
-    memeGenerator msg, 'boromir.jpg', 'ONE DOES NOT SIMPLY', msg.match[1], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/boromir.jpg', 'ONE DOES NOT SIMPLY', msg.match[1], (url) ->
       msg.send url

   robot.respond /(IF YOU .*\s)(.* GONNA HAVE A BAD TIME)/i, (msg) ->
-    memeGenerator msg, 'bad_time.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/bad_time.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(.*)TROLLFACE(.*)/i, (msg) ->
-    memeGenerator msg, 'troll_face.jpg', msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, 'http://memecaptain.com/troll_face.jpg', msg.match[1], msg.match[2], (url) ->
       msg.send url

   robot.respond /(IF .*), ((ARE|CAN|DO|DOES|HOW|IS|MAY|MIGHT|SHOULD|THEN|WHAT|WHEN|WHERE|WHICH|WHO|WHY|WILL|WON\'T|WOULD)[ \'N].*)/i, (msg) ->
-    memeGenerator msg, 'philosoraptor.jpg', msg.match[1], msg.match[2] + (if msg.match[2].search(/\?$/)==(-1) then '?' else ''), (url) ->
+    memeGenerator msg, 'http://memecaptain.com/philosoraptor.jpg', msg.match[1], msg.match[2] + (if msg.match[2].search(/\?$/)==(-1) then '?' else ''), (url) ->
       msg.send url

 memeGenerator = (msg, imageName, text1, text2, callback) ->
-  imageUrl = "http://memecaptain.com/" + imageName
+  imageUrl = imageName

   processResult = (err, res, body) ->
     return msg.send err if err


don't cache bad responses

don't cache bad responses

--- src/scripts/wunderground.coffee
+++ src/scripts/wunderground.coffee
@@ -32,7 +32,7 @@
   robot.brain.data.wunderground or= {}

   data = robot.brain.data.wunderground[cache_key]
-  if data? and ttl(lifetime, data) <= 0
+  if data? and ttl(data) <= 0
     #console.log 'needs refresh'
     robot.brain.data.wunderground[cache_key] = data = null
   if data?
@@ -46,6 +46,7 @@
     msg
       .http("http://api.wunderground.com/api/#{process.env.HUBOT_WUNDERGROUND_API_KEY}/#{service}/q/#{query}.json")
       .get() (err, res, body) ->
+        # check for a non-200 response. cache it for some short amount of time && msg.send 'unavailable'
         data = JSON.parse(body)

         # probably an unknown place
@@ -101,8 +102,11 @@
 # how long till our cached data expires?
 ttl = (data) ->
   now = new Date
-  retrieved = Date.parse(data.retrieved)
-  data.lifetime * 1000 - (now.getTime() - retrieved)
+  if not data.lifetime? or not data.retrieved?
+    -1
+  else
+    retrieved = Date.parse(data.retrieved)
+    data.lifetime * 1000 - (now.getTime() - retrieved)

 alternative_place = (item) ->
   return '' if item.country != 'US' || item.state == "" || item.city == ""


Fixed: 'show previous event' in list view was triggering an error if there was no event

--- client/app/views/list_view.coffee
+++ client/app/views/list_view.coffee
@@ -38,6 +38,9 @@
         first = @$('.after').first()
         body = $ 'html, body'
         @$('.before').slideDown
-            progress: -> body.scrollTop first.offset().top
+            progress: ->
+                # only scroll to top if it's necessary
+                if first.length > 0
+                    body.scrollTop first.offset().top

         @$('.showbefore').fadeOut()


utils: trim full name of user

utils: trim full name of user

New registered user's last name field is empty which causes a space at
the end of full name.

--- client/Main/utils.coffee
+++ client/Main/utils.coffee
@@ -285,7 +285,7 @@
       name = account.profile.firstName
     else
       name = "#{account.profile.firstName} #{account.profile.lastName}"
-    return Encoder.htmlEncode name or 'a Koding user'
+    return Encoder.htmlEncode name.trim() or 'a Koding user'

   getNameFromFullname :(fullname)->
     fullname.split(' ')[0]


[webpack] disable sourcemaps by default

[webpack] disable sourcemaps by default

- they bloat up the bundle, unnecessary in production
- in development mode sourcemap are available by default

--- webpack.config.coffee
+++ webpack.config.coffee
@@ -11,7 +11,6 @@
 config =
   context: path.resolve __dirname, 'src'
   entry: './index.coffee'
-  devtool: 'inline-source-map'
   devServer: {
     contentBase: './build'
   }


feat(webpack): wrap the module into a umd

--- webpack.config.coffee
+++ webpack.config.coffee
@@ -10,6 +10,7 @@
     path: "./"
     filename: "[name]"
     library: "vk"
+    libraryTarget: "umd"

   module:
     loaders: [


Imptove language.

Imptove language.

--- examples/dragdrop.coffee
+++ examples/dragdrop.coffee
@@ -44,8 +44,8 @@
     dragstart: (d) ->
       @removeDrop d

-  # You should never mutate the DOM manually, just update the model and let
-  # render/bindings do the work!
+  # You should never mutate the DOM outside of render/bindings, prefer to
+  # update the model instead.
   removeDrop: (idx) ->
     @model.drops.pop idx
   addDrop: (d) ->


Keep track of attributes behind the cursor

--- src/trix/text_controller.coffee
+++ src/trix/text_controller.coffee
@@ -11,6 +11,7 @@
     @dom = new Trix.DOM @element
     @selectionObserver = new Trix.SelectionObserver
     @selectionObserver.delegate = this
+    @currentAttributes = {}

   # Text delegate

@@ -20,7 +21,7 @@
   # Input responder

   insertString: (string) ->
-    text = new Trix.Text(string)
+    text = new Trix.Text(string, @currentAttributes)

     if selectedRange = @getSelectedRange()
       position = selectedRange[0]
@@ -48,7 +49,9 @@
   # Selection observer delegate

   selectionDidChange: ->
-    console.log "selection changed: position =", @getPosition(), "selected range =", @getSelectedRange()
+    if selectedRange = @dom.getSelectedRange()
+      position = selectedRange[0] - 1
+      @currentAttributes = @text.getAttributesAtPosition(position)

   # Selection


show roof colours

show roof colours

--- web/app/source/nvwrapper.coffee
+++ web/app/source/nvwrapper.coffee
@@ -24,16 +24,29 @@

 exports.getChartOfType = (target, name) ->
   switch name
+    when "egg"
+      chartj
     when "roof"
       chart = nv.models['discreteBarChart']()
+      chart.showValues true
       chart.xAxis.axisLabel('Roof Color').tickFormat (d) -> d
       chart.yAxis.axisLabel('Temp (C)').tickFormat d3.format('.1f')
+      chart.color (d) ->
+        switch d.x
+          when 'Charcoal' then '#36454f'
+          when 'Ultra White' then '#ddd'
+          when 'Steel Blue' then '#4682b4'
+          when 'White' then '#ccc'
+          when 'Creme' then '#FFFFCC'
+          when 'Desert Red' then '#B6571D'
+          when 'Bone White' then '#E1D4C0'
+          when 'Forest Green' then '#228b22'
       nv.utils.windowResize(chart.update)
       new BarChartDrawer target, chart
     else
       chart = nv.models[name]()
       if chart.useInteractiveGuideline?
-        chart = chart.useInteractiveGuideline(true)
+        chart.useInteractiveGuideline(true)
       chart.xAxis.axisLabel('Time').tickFormat (d) -> d3.time.format('%H:%M') new Date(d)
       chart.yAxis.axisLabel('Temp (C)').tickFormat d3.format('.1f')
       nv.utils.windowResize(chart.update)


Clarify conditions under which modal:close is triggered

--- vendor/assets/javascripts/modal.js.coffee
+++ vendor/assets/javascripts/modal.js.coffee
@@ -279,7 +279,7 @@
             return false # to block disappearance

           beforeCancel: ->
-            if !@submitting
+            if !@submitting? or @submitting is false
               $(document).trigger('modal:close')
               return true
         }
@@ -323,7 +323,7 @@
               $(this.el).find(".close").addClass('alone')

           beforeCancel: ->
-            if !@submitting
+            if !@submitting? or @submitting is false
               $(document).trigger('modal:close')
               return true
         }


[nj] set `ext` statically for renaming in `grunt-nunjucks-2-html`

See comment inside commit for details

--- tasks/grunt-nunjucks-2-html.coffee
+++ tasks/grunt-nunjucks-2-html.coffee
@@ -174,7 +174,10 @@
         rename: (dest, src) =>
           exclude  = /^(index|\d\d\d)$/

-          ext      = grunt.task.current.data.files[0].ext
+          # @todo Find way to get extension from current config dynamically
+          #       `grunt.task.current.data.files[0].ext` will work during initialization
+          #       but will fail when `grunt-newer` will run, since it will override `task.current`
+          ext      = '.html'
           basename = path.basename(src, ext)

           if !exclude.test(basename)


Refactor a bit; add automatic increases of maxIterations, with recalibration based on an acceptable error level.

--- static/js/app/Mandelbrot.coffee
+++ static/js/app/Mandelbrot.coffee
@@ -2,13 +2,50 @@
 Mandelbrot.coffee

 Render the Mandelbrot set using a CanvasTileMap.
+
+Optionally use a simple method to compute smoothly varying escape times
+to produce smooth gradients instead of discrete bands of color.
+http://linas.org/art-gallery/escape/escape.html
+
+Question: How should we increase the maxIterations limit as we zoom in?
+- At a given zoom level, let's keep track of the distribution of (rounded)
+  escape times.
+- If we choose maxIterations too large, then we'll spend a lot of computations
+  for a very small % of pixels.
+- If we choose it too small, a significant % of pixels will be colored black
+  that shouldn't be.
+- So, let's consider the error
+  Err = % of pixels on the canvas that are black but shouldn't be.
+- We'd like the smallest maxIterations such that e.g. Err < 0.1%.
+- So for each zoom, we'll recalibrate maxIterations to this error level,
+  then multiply it by a factor determined heuristically.
 ###

-# Optionally use a simple method to compute smoothly varying escape times
-# to produce smooth gradients instead of discrete bands of color.
-# http://linas.org/art-gallery/escape/escape.html
+# Start with this value of maxIterations at zoom 0.
+INITIAL_MAX_ITERATIONS = 50
+maxIterations = INITIAL_MAX_ITERATIONS
+
+# Use a heuristic to increase maxIterations as we zoom in: for each zoom
+# first scale maxIterations by this factor...
+MAX_ITERATIONS_SCALE_FACTOR = 1.5
+
+# ...then re-calibrate it by lowering it to the lowest value we can without
+# losing colors with a % of canvas pixels greater than this % threshold.
+MAX_ITERATIONS_ERROR = 0.1
+
+
+BLACK = "#000000"
+
+# Use (nearly-)continuous escape times instead of discrete integer values?
 USE_SMOOTH_ESCAPE_TIMES = true
+
+
+# Keep a count of each rounded escape time value (excluding 0) at a given zoom level.
+escapeTimeDistribution = {}
+incrementEscapeTime = (time) ->
+    time = Math.round time
+    escapeTimeDistribution[time] = (escapeTimeDistribution[time] || 0) + 1

 # Is a complex point c inside the main cardioid?
@@ -22,10 +59,10 @@
     return (c_x + 1) * (c_x + 1) + c_y * c_y < 1 / 16.0

-# Iterate until we leave the ball of radius 2, up to a given max # of iterations.
+# Iterate until we leave the ball of radius 2, up to the current # of max iterations.
 # If we escape after k iterations, return an escape time value (approx. k).
 # If we never escape, return 0.
-getEscapeValue = (c_x, c_y, maxIterations) ->
+getEscapeValue = (c_x, c_y) ->
     NO_ESCAPE = 0

     # Bypass the escape time computation if we can determine algebraically that
@@ -52,37 +89,61 @@
     # Give up, assume no escape.
     if i == maxIterations then return NO_ESCAPE

-    # Using the iteration count as escape time is simple but produces bands of color.
-    if not USE_SMOOTH_ESCAPE_TIMES then return i

-    # We can compute a fractional escape time using the modulus in addition to i.
+    if USE_SMOOTH_ESCAPE_TIMES
+        # Compute a fractional escape time using the modulus in addition to i.

-    # A few more iterations reduces the normalization error.
-    extraIterations = 4
-    for j in [0...extraIterations]
-        x2 = z_x * z_x
-        y2 = z_y * z_y
-        z_y = 2 * z_x * z_y + c_y
-        z_x = x2 - y2 + c_x
+        # A few more iterations reduces the normalization error.
+        extraIterations = 4
+        for j in [0...extraIterations]
+            x2 = z_x * z_x
+            y2 = z_y * z_y
+            z_y = 2 * z_x * z_y + c_y
+            z_x = x2 - y2 + c_x

-    norm = z_x * z_x + z_y * z_y
-    modulus = Math.sqrt norm
-    normalized = i + extraIterations - Math.log(Math.log(modulus)) / Math.log(2)
+        norm = z_x * z_x + z_y * z_y
+        modulus = Math.sqrt norm
+        escapeTime = i + extraIterations - Math.log(Math.log(modulus)) / Math.log(2)

-    # Ensure the value is >= 1.
-    normalized = Math.max(1, normalized);
-
-    return normalized
+        # Ensure the value is >= 1.
+        escapeTime = Math.max(1, escapeTime)
+    else
+        # Using the iteration count as escape time is simple but produces bands of color.
+        escapeTime = i

+    incrementEscapeTime escapeTime
+    return escapeTime

-MAX_ITERATIONS = 100
-BLACK = "#000000"
+
+# Given the current distribution of escape times, return the smallest escape time
+# such that larger ones account for only MAX_ITERATIONS_ERROR % of the distribution.
+# I.e., we could have used this smaller value of maxIterations without
+# any significant visual difference.
+recalibrateMaxIterations = () ->
+    # Convert raw pixel counts into a percent distribution.
+    percents = {}
+    sum = 0
+    sum += count for time, count of escapeTimeDistribution
+
+    # Sum escape time %s in reverse order until they exceed err.
+    times = _.map(_.keys(escapeTimeDistribution), Math.round)
+
+    index = times.length - 1
+    totalError = 0
+    while index >= 0 and totalError / sum * 100 < MAX_ITERATIONS_ERROR
+        time = times[index]
+        if time
+            totalError += escapeTimeDistribution[time]
+        index--
+
+    console.log "Was: " + times[times.length - 1] + " ; Now: " + time
+    return time

 # Return a color to indicate the escape time at a point.
 getColor = (x, y) ->
-    value = getEscapeValue(x, y, MAX_ITERATIONS)
+    value = getEscapeValue(x, y)
     if value == 0 then return BLACK

     value = Math.sqrt(value - 1)
@@ -97,7 +158,13 @@
         xMax:  2.0
         yMin: -2.0
         yMax:  2.0
-    # Reset the iteration counts on zoom.
+
+    # Recalibrate maxIterations and reset the distribute on each each.
     beforeZoom: () ->
+        maxIterations = Math.round(MAX_ITERATIONS_SCALE_FACTOR * recalibrateMaxIterations 0.1)
+        maxIterations = Math.max(maxIterations, INITIAL_MAX_ITERATIONS)
+        console.log "new maxIterations: " + maxIterations
+        escapeTimeDistribution = {}
+
 map.zoomIn()
 map.zoomIn()


:bug: Fix incorrect assertions

:bug: Fix incorrect assertions

--- spec/editor-view-spec.coffee
+++ spec/editor-view-spec.coffee
@@ -3050,7 +3050,7 @@
     it "adds and updates the grammar data attribute based on the current grammar", ->
       editorView.attachToDom()
       editor.setGrammar(atom.syntax.grammarForScopeName('text.plain'))
-      expect(editorView.attr('data-grammar')).toEqual 'text.plain'
-
-      editor.setGrammar(atom.syntax.grammarForScopeName('source.javascript'))
-      expect(editorView.attr('data-grammar')).toEqual 'source.javascript'
+      expect(editorView.attr('data-grammar')).toBe 'text plain'
+
+      editor.setGrammar(atom.syntax.grammarForScopeName('source.js'))
+      expect(editorView.attr('data-grammar')).toBe 'source js'


Remove comment from dropdown when it is reopened

--- services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee
+++ services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee
@@ -29,6 +29,7 @@

 			scope.handleUnresolve = (threadId) ->
 				scope.onUnresolve({ threadId })
+				scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId

 			scope.handleDelete = (entryId, threadId) ->
 				scope.onDelete({ entryId, threadId })


:lipstick: folded not folder

:lipstick: folded not folder

--- spec/editor-view-spec.coffee
+++ spec/editor-view-spec.coffee
@@ -2631,7 +2631,7 @@
         editorView.trigger 'editor:move-line-down'
         expect(editor.getCursorBufferPosition()).toEqual [1, 2]

-      describe "when the line below is folder", ->
+      describe "when the line below is folded", ->
         it "moves the line around the fold", ->
           editor.setCursorBufferPosition([0, 0])
           editor.foldBufferRow(1)


Use consistent quotes in spec

Use consistent quotes in spec

--- spec/pathwatcher-spec.coffee
+++ spec/pathwatcher-spec.coffee
@@ -1,15 +1,15 @@
 pathWatcher = require '../lib/main'
 fs = require 'fs'

-describe "PathWatcher", ->
+describe 'PathWatcher', ->
   beforeEach ->
     fs.writeFileSync('/tmp/watch.txt', '')

   afterEach ->
     pathWatcher.closeAllWatchers()

-  describe ".getWatchedPaths()", ->
-    it "returns an array of all watched paths", ->
+  describe '.getWatchedPaths()', ->
+    it 'returns an array of all watched paths', ->
       expect(pathWatcher.getWatchedPaths()).toEqual []
       watcher1 = pathWatcher.watch '/tmp/watch.txt', ->
       expect(pathWatcher.getWatchedPaths()).toEqual ['/tmp/watch.txt']
@@ -20,16 +20,16 @@
       watcher2.close()
       expect(pathWatcher.getWatchedPaths()).toEqual []

-  describe ".closeAllWatchers()", ->
-    it "closes all watched paths", ->
+  describe '.closeAllWatchers()', ->
+    it 'closes all watched paths', ->
       expect(pathWatcher.getWatchedPaths()).toEqual []
       watcher = pathWatcher.watch '/tmp/watch.txt', ->
       expect(pathWatcher.getWatchedPaths()).toEqual ['/tmp/watch.txt']
       pathWatcher.closeAllWatchers()
       expect(pathWatcher.getWatchedPaths()).toEqual []

-  describe "when a watched path is changed", ->
-    it "fires the callback with the event type and empty path", ->
+  describe 'when a watched path is changed', ->
+    it 'fires the callback with the event type and empty path', ->
       eventType = null
       eventPath = null
       watcher = pathWatcher.watch '/tmp/watch.txt', (type, path) ->
@@ -42,8 +42,8 @@
         expect(eventType).toBe 'change'
         expect(eventPath).toBe ''

-  describe "when a watched path is renamed", ->
-    it "fires the callback with the event type and new path and watches the new path", ->
+  describe 'when a watched path is renamed', ->
+    it 'fires the callback with the event type and new path and watches the new path', ->
       eventType = null
       eventPath = null
       watcher = pathWatcher.watch '/tmp/watch.txt', (type, path) ->
@@ -57,8 +57,8 @@
         expect(eventPath).toBe fs.realpathSync('/tmp/watch-renamed.txt')
         expect(pathWatcher.getWatchedPaths()).toEqual [fs.realpathSync('/tmp/watch-renamed.txt')]

-  describe "when a watched path is deleted", ->
-    it "fires the callback with the event type and null path", ->
+  describe 'when a watched path is deleted', ->
+    it 'fires the callback with the event type and null path', ->
       eventType = null
       eventPath = null
       watcher = pathWatcher.watch '/tmp/watch.txt', (type, path) ->


Remove dumb cache key

Remove dumb cache key

--- lineman/app/models/membership_records_interface.coffee
+++ lineman/app/models/membership_records_interface.coffee
@@ -23,7 +23,6 @@
         params:
           group_key: groupKey
           per: options['per']
-        cacheKey: "membershipsFor#{groupKey}"

     makeAdmin: (membership) ->
       @restfulClient.postMember membership.id, "make_admin"


Actually use beatmap cache

Actually use beatmap cache

--- resources/assets/coffee/react/beatmap-discussions-history/main.coffee
+++ resources/assets/coffee/react/beatmap-discussions-history/main.coffee
@@ -93,6 +93,8 @@

   beatmaps: =>
+    @cache.beatmaps if @cache.beatmaps?
+
     beatmaps = _.map(@discussions(), (d) => d.beatmap)
                 .filter((b) => b != undefined)


now running all tests

now running all tests

--- tests/src/Remote-Chrome-API/test-Accessing-Node-WebKit.coffee
+++ tests/src/Remote-Chrome-API/test-Accessing-Node-WebKit.coffee
@@ -105,7 +105,7 @@
           properties.assert_Contains('outerText')
           done()

-  it.only 'dynamically invoke javascript via innerHTML script injection',(done)->
+  it 'dynamically invoke javascript via innerHTML script injection',(done)->
     chrome.open 'nw:about', ->
       chrome.eval_Script 'document.body.innerHTML="<img src=a onerror=document.write(42) />"', ()->
         chrome.html (value,$)->


Do not allow users to delete koding-apps like Ace or Webterm

--- client/landing/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
+++ client/landing/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
@@ -279,6 +279,7 @@
     @img.$().attr "src", "/images/#{data.icon}"

     @compile = new KDView
+    @delete  = new KDView  if data.type is 'koding-app'

   appDeleteCall:({name})->
     @showLoader()


Statistics uses graphWidth as a function

--- app/scripts/alchemy/core/stats.coffee
+++ app/scripts/alchemy/core/stats.coffee
@@ -120,7 +120,7 @@
         return nodeData

     insertSVG: (element, data) ->
-        width = alchemy.conf.graphWidth * .25
+        width = alchemy.conf.graphWidth() * .25
         height = 250
         radius = width / 4
         color = d3.scale.category20()


Destroy geometry appropriately

Destroy geometry appropriately

--- app/scripts/client/webgl/chunk.coffee
+++ app/scripts/client/webgl/chunk.coffee
@@ -14,10 +14,11 @@

 		@_isDirty = false

-		@showBounding()
+		# @showBounding()

 	destroy: ->
 		# Remove geometry from scene
+		@_destroyGeometry()

 	showBounding: ->
 		geometry = new THREE.BoxGeometry 1, 1, 1
@@ -65,10 +66,7 @@
 		position.x + ',' + position.y

 	_destroyGeometry: ->
-		unless @_chunkMesh
-			return
-
-		@world.scene.remove @_chunkMesh
+		@world.scene.remove @_chunkMesh if @_chunkMesh
 		@_chunkMesh = undefined

 	_generateGeometry: ->


Label

Label

- Fixed "manage_overflow".

--- src/elements/Label/Label.coffee
+++ src/elements/Label/Label.coffee
@@ -223,7 +223,6 @@
 			if @__overflow_button.isActive()
 				@__label.addClass("cui-label--show-overflow")
 		else
-			@__overflow_button.hide()
 			@__label.addClass("cui-label--show-overflow")

 		@__label.removeClass("cui-label--measure-overflow")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/floating_text_effect.coffee
+++ src/floating_text_effect.coffee
@@ -4,7 +4,7 @@

     # adds a FloatingTextEffect to the engine
     # at (50, 50). This effect will float upward
-    # at 90 pixels/sec and
+    # at 90 pixels/sec and will fadeOut over 0
     engine.add 'FloatingTextEffect'
       x: 50
       y: 50


bad parsing of compilers response

bad parsing of compilers response

--- src/fs_utils/source_file.coffee
+++ src/fs_utils/source_file.coffee
@@ -45,8 +45,8 @@
         return callbackError 'Compiling', error if error?
         # compiler is able to produce sourceMap
         if typeof compiledData is 'object'
-          sourceMap = compiled.map
-          compiled = compiled.compiled
+          sourceMap = compiledData.map
+          compiled = compiledData.compiled
         else
           compiled = compiledData
         getDependencies source, path, compiler, (error, dependencies) =>


Remove .only

Remove .only

--- spec/feature/example_bounded_context.spec.coffee
+++ spec/feature/example_bounded_context.spec.coffee
@@ -26,7 +26,7 @@
           done()

-      it.only 'then it should haved triggered the correct DomainEvent', (done) ->
+      it 'then it should haved triggered the correct DomainEvent', (done) ->
         exampleContext.onDomainEvent 'Example:create', (domainEvent) ->
           expect(domainEvent.getName()).to.equal 'create'
           expect(domainEvent.getAggregateChanges().props).to.deep.equal props


change nav generator from double to single quotes

--- packages/tower-generator/server/helpers.coffee
+++ packages/tower-generator/server/helpers.coffee
@@ -35,7 +35,7 @@
       @injectIntoFile "config/assets.coffee", "      \'#{path}\'\n", after: new RegExp("\\s*#{bundle}: *\\[[^\\]]+\\n", "i")

   navigation: (key, path) ->
-    pattern = /div *class: *"nav-collapse" *, *->\s+ul *class: *"nav", *-> */
+    pattern = /div *class: *'nav-collapse' *, *->\s+ul *class: *'nav', *-> */
     content = """\n    navItem t('links.#{key}'), #{path}
 """


numbering the intersections

numbering the intersections

--- ui/javascripts/main.coffee
+++ ui/javascripts/main.coffee
@@ -20,8 +20,16 @@
         cell = duplicate
         duplicate = cell.clone()

+  numberIntersections = ->
+    $('table.go-board tr').each (y, row)->
+      $(row).children().each (x, cell)->
+        $cell = $(cell)
+        $cell.attr('data-y', y)
+        $cell.attr('data-x', x)
+
   stretchRowsTo(number)
   stretchColumnsTo(number)
+  numberIntersections()

 $ ->
   current_color = BLACK


Remove stray console log

Remove stray console log

--- services/web/public/coffee/ide/LinkSharingOnboardingController.coffee
+++ services/web/public/coffee/ide/LinkSharingOnboardingController.coffee
@@ -11,7 +11,6 @@
 			event_tracking.sendMB "shown-linksharing-onboarding"

 		$scope.$on 'ide:loaded', () ->
-			console.log ">> woot"
 			shareBtn = angular.element('#shareButton')
 			offset = shareBtn.offset()
 			popover.show()


fix bug async js

fix bug async js

--- _assets/javascripts/google_map_fix.js.coffee
+++ _assets/javascripts/google_map_fix.js.coffee
@@ -1,6 +1,3 @@
-$ ->
-  enable_map_when_clicked() if screen.width >= 768
-
 # On devices with large screens, override the link and activate map when clicked
 # instead of opening map in new window
 enable_map_when_clicked = ->
@@ -13,3 +10,6 @@

 deactivate_map = ->
   $('.map_container .overlay').show()
+
+$ ->
+  enable_map_when_clicked() if screen.width >= 768


model_postgres: do not use rowCount

model_postgres: do not use rowCount

API breakage all over the packagescape :'(

--- src/local/model_postgres.coffee
+++ src/local/model_postgres.coffee
@@ -110,10 +110,10 @@
             async.waterfall [(cb2) ->
                 db.query "SELECT node FROM nodes WHERE node=$1", [ node ], cb2
             , (res, cb2) ->
-                if res.rowCount < 1
+                if res?.rows?[0]
+                    cb2 null
+                else
                     cb2 new errors.NotFound("Node does not exist")
-                else
-                    cb2 null
             ], cb

     createNode: (node, cb) ->
@@ -121,7 +121,7 @@
         async.waterfall [(cb2) ->
             db.query "SELECT node FROM nodes WHERE node=$1", [ node ], cb2
         , (res, cb2) ->
-            if res.rowCount > 0
+            if res?.rows?[0]
                 # Node already exists: ignore
                 cb2(null)
             else
@@ -326,10 +326,10 @@
         async.waterfall [(cb2) ->
             db.query "DELETE FROM items WHERE node=$1 AND id=$2", [ node, itemId ], cb2
         , (res, cb2) ->
-            if res.rowCount < 1
+            if res?.rows?[0]
+                cb2 null
+            else
                 cb2 new errors.NotFound("No such item")
-            else
-                cb2 null
         ], cb

     ##


ext/table: Fix #mouse to return [null, null] instead of null when mouse not over table.

--- oghma/ext/table.coffee
+++ oghma/ext/table.coffee
@@ -59,6 +59,8 @@
     pos = @stage.getMousePosition()
     if pos?
       [ @tX( pos.x ), @tY( pos.y ) ]
+    else
+      [ null, null ]

   # Convert client X to table X:
   tX: ( x ) ->


slides to lastFrame of end

slides to lastFrame of end

--- modules/levelcreator/app/assets/javascripts/level_creator/plugins/get_meta_values.coffee
+++ modules/levelcreator/app/assets/javascripts/level_creator/plugins/get_meta_values.coffee
@@ -105,14 +105,19 @@

     # get endSlide
-    lastStartFrame = DataUtils.nmToSlide(mission.start.lastFrame, slidesBeforeProblem)
+
+    endS = _.detect(mission.possibleEnds, id: mission.end.id)
+    if endS?
+      maxEndSlide = Math.min(DataUtils.nmToSlide(endS.lastFrame, slidesBeforeProblem), desiredEndSlide)
+    else
+      maxEndSlide = desiredEndSlide

     if noStartSegmentAtLastSlide
       searchEnd = Math.max(slidesBeforeProblem, lastStartFrame)
     else
       searchEnd = slidesBeforeProblem

-    for i in [Math.floor(desiredEndSlide)...searchEnd] by -1
+    for i in [Math.floor(maxEndSlide)...searchEnd] by -1
       result = _.filter(mission.possibleEnds, (e) =>
         #console.log DataUtils.nmToSlide(e.firstFrame) + "  - " + i + " -  " + DataUtils.nmToSlide(e.lastFrame)
         DataUtils.nmToSlide(e.firstFrame, slidesBeforeProblem) < i < DataUtils.nmToSlide(e.lastFrame, slidesBeforeProblem) ).length


Make BufferMarker return copies of its points so they can't be mutated

--- src/app/buffer-marker.coffee
+++ src/app/buffer-marker.coffee
@@ -74,19 +74,19 @@
   # Returns a {Range}.
   getRange: ->
     if @tailPosition
-      new Range(@tailPosition, @headPosition)
+      new Range(@getTailPosition(), @getHeadPosition())
     else
-      new Range(@headPosition, @headPosition)
+      new Range(@getHeadPosition(), @getHeadPosition())

   # Public: Retrieves the position of the marker's head.
   #
   # Returns a {Point}.
-  getHeadPosition: -> @headPosition
+  getHeadPosition: -> @headPosition?.copy()

   # Public: Retrieves the position of the marker's tail.
   #
   # Returns a {Point}.
-  getTailPosition: -> @tailPosition ? @getHeadPosition()
+  getTailPosition: -> @tailPosition?.copy() ? @getHeadPosition()

   # Public: Sets the position of the marker's head.
   #


fix slight issue with popover not regenerating on reload

--- src/main/coffee/NR.Views.coffee
+++ src/main/coffee/NR.Views.coffee
@@ -272,12 +272,11 @@
                 shareBtn = this.domObject.find(".shareButton")
                 shareBtn.popover({
                     html: true
-                    container: 'body'
                     placement: 'top'
                     content: this.shareTemplate.call this, this.model
                 })
                 shareBtn.on("shown.bs.popover", () =>
-                    ele = shareBtn[0]
+                    ele = $("#share-items-" + this.model.article.id)
                     FB.XFBML.parse(ele[0])
                     gapi.plusone.go(ele[0])
                     twttr.widgets.load(ele[0])


Removing parameter check for optional variable

--- packages/rocketchat-lib/server/methods/joinRoom.coffee
+++ packages/rocketchat-lib/server/methods/joinRoom.coffee
@@ -2,7 +2,7 @@
 	joinRoom: (rid, code) ->

 		check rid, String
-		check code, Match.Maybe(String)
+		# check code, Match.Maybe(String)

 		if not Meteor.userId()
 			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'joinRoom' }


Fix for scrolling to top on paste.

Fix for scrolling to top on paste.

--- packages/pixelartacademy-studyguide/pages/admin/activities/article-client/article.coffee
+++ packages/pixelartacademy-studyguide/pages/admin/activities/article-client/article.coffee
@@ -39,6 +39,7 @@

     # Initialize quill.
     quill = new Quill @$('.writing-area')[0],
+      scrollingContainer: document.documentElement
       theme: 'snow'
       formats: PAA.StudyGuide.Article.quillFormats
       modules:


copy new package.json

copy new package.json

--- app/generator.coffee
+++ app/generator.coffee
@@ -91,7 +91,7 @@
       @copy "secrets-sample.json",  "secrets.json"
       @copy "gulpfile.coffee",      "gulpfile.coffee"
       @copy "gulpconfig.json",      "gulpconfig.json"
-      @template "_package.json",    "package.json"
+      @copy "package.json",         "package.json"
       @template "_robots.txt",      "src/robots.txt"


Commit client-side code for acdfe7c

Commit client-side code for acdfe7c

--- app/views/ladder/LadderTabView.coffee
+++ app/views/ladder/LadderTabView.coffee
@@ -168,7 +168,8 @@
       team = _.find @teams, name: histogramWrapper.data('team-name')
       histogramData = null
       $.when(
-        url = "/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}"
+        level = "#{@level.get('original')}.#{@level.get('version').major}"
+        url = "/db/level/#{level}/histogram_data?team=#{team.name.toLowerCase()}"
         url += '&leagues.leagueID=' + @options.league.id if @options.league
         $.get url, {cache: false}, (data) -> histogramData = data
       ).then =>


Fix poll keys when key is a number

Fix poll keys when key is a number

jQuery converts data strings into a possible JavaScript value, so keys
like '0' and '1' get converted into numbers, which fails the validation
(it expects a string for a key).

--- app/views/play/modal/PollModal.coffee
+++ app/views/play/modal/PollModal.coffee
@@ -73,7 +73,7 @@
   onClickAnswer: (e) ->
     $selectedAnswer = $(e.target).closest('.answer')
     pollVotes = @userPollsRecord.get('polls') ? {}
-    pollVotes[@poll.id] = $selectedAnswer.data('answer')
+    pollVotes[@poll.id] = $selectedAnswer.data('answer').toString()
     @userPollsRecord.set 'polls', pollVotes
     @updateAnswers true
     @userPollsRecord.save {polls: pollVotes}, {success: => @awardRandomGems?()}


popovers stay within the viewport

popovers stay within the viewport

--- @client/popover.coffee
+++ @client/popover.coffee
@@ -114,6 +114,8 @@

 calc_coords = (el) ->
   coords = $(el).offset()
+  coords.width = el.offsetWidth
+  coords.height = el.offsetHeight
   coords.left += el.offsetWidth / 2
   coords

@@ -149,23 +151,69 @@
       boxShadow: '0 1px 9px rgba(0,0,0,.5)'
       # maxWidth: 350

-
     arrow_size =
       height: 10
       width: 26

-    if popover.top || !popover.top?
+    window_height = window.innerHeight
+    viewport_top = window.scrollY
+    viewport_bottom = viewport_top + window_height
+
+    arrow_up = popover.top || !popover.top?
+    try_top = (force) ->
+
+      top = coords.top + (popover.offsetY or 0) - (popover.rendered_size?.height or 0) - arrow_size.height - 6
+      if top < viewport_top && !force
+        arrow_up = false
+        return null
+      else
+        arrow_up = true
+        top
+
+    try_bottom = (force) ->
+      top = coords.top + (popover.offsetY or 0) + arrow_size.height + coords.height + 12
+      if top + (popover.rendered_size?.height or 0) + arrow_size.height > viewport_bottom && !force
+        arrow_up = true
+        return null
+      else
+        arrow_up = false
+        top
+
+    if popover.top || !popover.top?
       # place the popover above the element
-      _.extend style,
-        top: coords.top + (popover.offsetY or 0) - (popover.rendered_size?.height or 0) - arrow_size.height
-        left: if !popover.rendered_size then -99999 else coords.left + (popover.offsetX or 0) - popover.rendered_size?.width / 2
+      top = try_top()
+      if top == null
+        top = try_bottom()
+        if top == null
+          top = try_top(true)
     else
       # place the popover below the element
-      _.extend style,
-        top: coords.top + (popover.offsetY or 0)
-        left: if !popover.rendered_size then -99999 else coords.left + (popover.offsetX or 0) - (popover.rendered_size.width or 0)
-
-    arrow_up = popover.top || !popover.top?
+      top = try_bottom()
+      if top == null
+        top = try_top()
+        if top == null
+          top = try_bottom(true)
+
+
+    arrow_adjustment = 0
+    if popover.rendered_size?.width
+      left = coords.left + (popover.offsetX or 0) - popover.rendered_size.width / 2
+
+      if left < 0
+        arrow_adjustment = -1 * left
+        left = 0
+      else if left + popover.rendered_size.width > window.innerWidth
+        arrow_adjustment = (window.innerWidth - popover.rendered_size.width) - left
+        left = window.innerWidth - popover.rendered_size.width
+
+    else
+      left = -999999 # render it offscreen first to get sizing
+
+
+    _.extend style,
+      top: top
+      left: left
+

     get_focus = ->
       popover.has_focus = true
@@ -199,7 +247,7 @@
           position: 'absolute'
           bottom: if arrow_up then -arrow_size.height
           top: if !arrow_up then -arrow_size.height
-          left: if popover.positioned != 'right' then "calc(50% - #{arrow_size.width / 2}px)"
+          left: if popover.positioned != 'right' then "calc(50% - #{arrow_size.width / 2 + arrow_adjustment}px)"
           right: if popover.positioned == 'right' then 7
           transform: if !arrow_up then 'scale(1,-1)'
           display: if popover.hide_triangle then 'none'


catch errors

catch errors

--- API/src/scraper.coffee
+++ API/src/scraper.coffee
@@ -16,9 +16,7 @@
     new Promise (f, r) ->
         request url, (err, response, body) ->
             if err
-                console.error err
                 r err
-                return
             $ = cheerio.load body
             content = $('#content')
             name = content.find('h1').text()
@@ -45,16 +43,13 @@
     new Promise (f, r) ->
         request url, (err, response, body) ->
             if err
-                console.error err
                 r err
-                return
             urls = []
             $ = cheerio.load body
             $('.teaser-subline').each ->
                 elem = $(this)
                 detail_url = base_url + elem.find('.teaser-image').find('a').attr('href')
                 urls.push detail_url
-                return
             f urls

 # Get the data from cache or save data in cache if data is none
@@ -63,18 +58,20 @@
     new Promise (f, r) ->
         values = cache.get('tiere')
         if not values
-            #console.log "no cache"
             get_detailUrls tierheim_url
                 .then (urls) ->
                     p = []
                     for url in urls
                         p.push get_details url
                     Promise.all p
+                .catch (err)
+                    r err
                 .then (values) ->
                     cache.set('tiere', values, 60*60*24)
                     f values
+                .catch err
+                    r err
         else
-            #console.log "cache"
             f values

 # get a pet that was not posted yet
@@ -88,7 +85,6 @@
                 fs.writeFileSync filename, JSON.stringify []
                 postedPets = []
         catch err
-            console.error err
             r err
         get_data().then (pets) ->
             notPostedPets = []
@@ -113,16 +109,22 @@
 ###

 # Return all pets
-app.get '/', (req, rep) ->
-    get_data().then (pets) ->
-        #console.log values
-        rep.json pets
+app.get '/', (req, res) ->
+    get_data()
+        .then (pets) ->
+            res.json pets
+        .catch (err) ->
+            console.error err
+            res.status(500).json(err);

 # Return a random pet
-app.get '/random', (req, rep) ->
-    get_notPostedPet().then (pet) ->
-        #console.log pet
-        rep.json pet
+app.get '/random', (req, res) ->
+    get_notPostedPet()
+        .then (pet) ->
+            res.json pet
+        .catch (err) ->
+            console.error err
+            res.status(500).json(err);

 server = app.listen 3000, 'localhost', ->
     host = server.address().address


Add link hint support for jsaction event listeners

This was adapted from PR #1316, commit
846a19efe51bfc639ae1ee84e18a5f2d3e12aaff

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -156,6 +156,13 @@
           element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or
           element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"])
         isClickable = true
+
+      # Check for jsaction event listeners on the element.
+      if element.hasAttribute "jsaction"
+        jsactionRules = element.getAttribute("jsaction").split(";")
+        for jsactionRule in jsactionRules
+          ruleSplit = jsactionRule.split ":"
+          isClickable = true if ruleSplit[0] == "click" or (ruleSplit.length == 1 and ruleSplit[0] != "none")

       # Check for tagNames which are natively clickable.
       switch tagName


DRYed a bit the angularjs service that lazy-loads images.

--- app/assets/javascripts/ng-services/ng-LazyLoadingSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-LazyLoadingSvc.js.coffee
@@ -12,13 +12,12 @@
   # - entry to which the image belongs
   #--------------------------------------------
   load_image = (img, entry=null)->
-    # Create a clone of the image, hide it and load the actual image there. If it loads successfully,
-    # hide the placeholder image and show the loaded image, otherwise hide both images.
+    # The actual URL of the image is in the data-src attribute
     data_src = img.attr 'data-src'

     if !data_src || data_src?.trim()?.length == 0
-      # If data-src is blank, just hide the spinner
-      img.addClass 'hidden'
+      # If data-src is blank, just hide the spinner placeholder
+      hide_placeholder_image img
     else
       # If data-src is not blank, lazy-load the image
       uri = new URI(data_src).normalize()
@@ -34,10 +33,12 @@
       if !scheme
         uri.scheme 'https'

+      # Create a hidden clone of the image and load the actual image there. If it loads successfully,
+      # hide the placeholder image and show the loaded image, otherwise hide both images.
       img.removeAttr('data-src')
       loaded_img = img.clone().addClass('loading').removeAttr('src').insertAfter(img)
       loaded_img.on 'error', ->
-        # If https failed try with http, and the other way around
+        # If https failed try again with http, and vice versa
         if uri.scheme() == 'https'
           uri.scheme 'http'
         else
@@ -45,7 +46,7 @@

         loaded_img.off 'error load'
         loaded_img.on 'error', ->
-          img.addClass 'hidden'
+          hide_placeholder_image img
         .on 'load', ->
           show_loaded_image img, loaded_img
         loaded_img.attr 'src', uri.toString()
@@ -60,8 +61,16 @@
   # - jQuery object wrapping the loaded img
   #--------------------------------------------
   show_loaded_image = (placeholder_img, loaded_img) ->
-    placeholder_img.addClass 'hidden'
+    hide_placeholder_image placeholder_img
     animationsSvc.show_image loaded_img
+
+  #--------------------------------------------
+  # PRIVATE FUNCTION: Hide the spinning placeholder of a loading image.
+  # Receives as argument:
+  # - jQuery object wrapping the placeholder img
+  #--------------------------------------------
+  hide_placeholder_image = (img) ->
+    img.addClass 'hidden'

   service =


Update cs.coffee (#4907)

Update cs.coffee (#4907)

--- app/locale/cs.coffee
+++ app/locale/cs.coffee
@@ -138,7 +138,7 @@
     poll: "Hlasování" # Tooltip on poll button from /play
     next: "Další" # Go from choose hero to choose inventory before playing a level
     change_hero: "Změnit hrdinu" # Go back from choose inventory to choose hero
-#    change_hero_or_language: "Change Hero or Language"
+    change_hero_or_language: "Změnit hrdinu nebo jazyk"
     buy_gems: "Zakoupit drahokamy"
     subscribers_only: "Pouze předplatitelé!"
     subscribe_unlock: "Předplaťte si pro odemknutí!"


Added maka_blurb + maka_title to en.coffee

--- app/locale/en.coffee
+++ app/locale/en.coffee
@@ -616,6 +616,8 @@
     josh_c_blurb: "Designs games"
     carlos_title: "Region Manager, Brazil"
     carlos_blurb: "Celery Man"
+    maka_title: "Customer Advocate"
+    maka_blurb: "Storyteller"

   teachers:
     more_info: "More Info for Teachers"


Update fi.coffee

Update fi.coffee

more translations

--- app/locale/fi.coffee
+++ app/locale/fi.coffee
@@ -7,12 +7,12 @@
     im_a_teacher: "Olen Opettaja"
     im_a_student: "Olen Oppilas"
     learn_more: "Lue lisää"
-#    classroom_in_a_box: "A classroom in-a-box for teaching computer science."
-#    codecombat_is: "CodeCombat is a platform for students to learn computer science while playing through a real game."
-#    our_courses: "Our courses have been specifically playtested to excel in the classroom, even for teachers with little to no prior programming experience."
-#    watch_how: "Watch how CodeCombat is transforming the way people learn computer science."
-#    top_screenshots_hint: "Students write code and see their changes update in real-time"
-#    designed_with: "Designed with teachers in mind"
+    classroom_in_a_box: "Luokkahuone taskussa tietojenkäsittelyn opetukseen."
+    codecombat_is: "CodeCombat on alusta oppilaille oppia ohjelmointia pelaamisen kautta."
+    our_courses: "Kurssimme on suunniteltu juuri luokkahuone käyttöön, jopa opettajille joilla on vain vähän ohjelmoinnin osaamista."
+    watch_how: "Katso kuinka CodeCombat muuttaa miten tietojenkäsittelyä voi oppia."
+    top_screenshots_hint: "Oppilaat kirjoittavat koodia ja näkevät muutokset reaaliajassa"
+    designed_with: "Suunniteltu opettajat mielessä"
 #    real_code: "Real, typed code"
 #    from_the_first_level: "from the first level"
 #    getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure."
@@ -22,8 +22,8 @@
 #    accessible_to: "Accessible to"
 #    everyone: "everyone"
 #    democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code."
-#    forgot_learning: "I think they actually forgot that they were actually learning something."
-#    wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
+    forgot_learning: "Luulenpa, että he oikeastaan unohtivat, että olivat oppimassa jotain."
+    wanted_to_do: "´Koodaus on sitä mitä olen aina halunnut tehdä, enkä uskonut että voisin oppia sitä koulussa."
 #    why_games: "Why is learning through games important?"
 #    games_reward: "Games reward the productive struggle."
 #    encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn."
@@ -41,11 +41,11 @@
 #    real_game: "A real game, played with real coding."
 #    great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
 #    agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code."
-#    request_demo_title: "Get your students started today!"
+    request_demo_title: "Aloita oppilaitesi kanssa tänään!"
 #    request_demo_subtitle: "Request a demo and get your students started in less than an hour."
 #    get_started_title: "Set up your class today"
 #    get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science."
-#    request_demo: "Request a Demo"
+    request_demo: "Pyydä demo"
 #    setup_a_class: "Set Up a Class"
     have_an_account: "Onko sinulla jo käyttäjätunnus?"
     logged_in_as: "Olet kirjautunut sisään käyttäjänä"
@@ -54,11 +54,11 @@
 #    curriculum: "Total curriculum hours:"
     ffa: "Ilmainen kaikille oppilaille"
     lesson_time: "Kesto:"
-#    coming_soon: "More coming soon!"
-#    courses_available_in: "Courses are available in JavaScript and Python. Web Development courses utilize HTML, CSS, and jQuery."
+    coming_soon: "Lisää tulossa pian!"
+    courses_available_in: "Kurssimme on saatavilla JavaScript ja Python ohjelmointikielillä. Nettikehitys kurssit käyttävät HTML, CSS, ja jQuery kieliä."
 #    boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike."
 #    winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable."
-#    run_class: "Everything you need to run a computer science class in your school today, no CS background required."
+    run_class: "Kaikki mitä tarvitset tietotekniikan opetukseen koulussasi, eikä tietoteknistä taustaa edes vaadita."
 #    goto_classes: "Go to My Classes"
     view_profile: "Tarkastele profiiliani"
     view_progress: "Tarkastele edistymistäni"
@@ -78,7 +78,7 @@
     home: "Koti"
     contribute: "Avusta"
     legal: "Laillisuus"
-#    privacy: "Privacy"
+    privacy: "Yksityisyys"
     about: "Tietoja"
     contact: "Ota yhteyttä"
     twitter_follow: "Seuraa"
@@ -92,13 +92,13 @@
     learn_to_code: "Opi koodaamaan!"
 #    toggle_nav: "Toggle navigation"
     schools: "Koulut"
-#    get_involved: "Get Involved"
+    get_involved: "Osallistu"
     open_source: "Avoin lähdekoodi (GitHub)"
     support: "Tuki"
     faqs: "UKK"
     help_pref: "Tarviksetko apua? lähetä viesti"
     help_suff: "ja me autamme!"
-#    resource_hub: "Resource Hub"
+    resource_hub: "Resurssit"

   modal:
     close: "Sulje"
@@ -157,8 +157,8 @@
     brain_pop_challenge: "Haasta itsesi ja pelaa uudelleen eri ohjelmointikielellä!"
     replay: "Uudelleentoisto"
     back_to_classroom: "Takaisin luokkahuoneeseen"
-#    teacher_button: "For Teachers"
-#    get_more_codecombat: "Get More CodeCombat"
+    teacher_button: "Opettajille"
+    get_more_codecombat: "Hanki Jatkoa CodeCombat:lle"

   code:
     if: "jos" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.)
@@ -188,9 +188,9 @@
     "!": "ei"
     "=": "aseta"
     "==": "ovat samat (yhtäsuuret)"
-#    "===": "strictly equals"
+    "===": "täysin yhtäsuuri"
     "!=": "ovat eri (erisuuruuset)"
-#    "!==": "does not strictly equal"
+    "!==": "ei täysin yhtäsuuri"
     ">": "suurempi kuin"
     ">=": "suurempi tai yhtäsuuri kuin"
     "<": "pienempi kuin"
@@ -206,8 +206,8 @@
     False: "Epätotta"
     false: "epätotta"
     undefined: "määrittelemätön"
-#    null: "null"
-#    nil: "nil"
+    null: "tyhjä"
+    nil: "nolla"
     None: "Olematon"

   share_progress_modal:
@@ -332,9 +332,9 @@
     coming_soon: "Tulossa pian!"
     continue: "Jatka" # When used as an action verb, like "Continue forward"
     next: "Seuraava"
-#    default_code: "Default Code"
+    default_code: "Oletuskoodi"
     loading: "Ladataan..."
-#    overview: "Overview"
+    overview: "Yleissilmäys"
     processing: "Prosessoidaan..."
     solution: "Ratkaisu"
     table_of_contents: "Sisällysluettelo"
@@ -413,14 +413,14 @@
     wizard: "Velho"
     first_name: "Etunimi"
     last_name: "Sukunimi"
-#    last_initial: "Last Initial"
+    last_initial: "Nimikirjain"
     username: "Käyttäjätunnus"
     contact_us: "Ota yhteyttä"
     close_window: "Sulje ikkuna"
     learn_more: "Lue lisää"
-#    more: "More"
-#    fewer: "Fewer"
-#    with: "with"
+    more: "Lisää"
+    fewer: "Vähemmän"
+    with: "lisäksi"

   units:
     second: "sekunti"
@@ -440,12 +440,12 @@

   play_level:
     back_to_map: "Takaisin karttaan"
-#    directions: "Directions"
+    directions: "Suunnat"
     edit_level: "Muokkaa tasoa"
-#    keep_learning: "Keep Learning"
-#    explore_codecombat: "Explore CodeCombat"
-#    finished_hoc: "I'm finished with my Hour of Code"
-#    get_certificate: "Get your certificate!"
+    keep_learning: "Jatka oppimista"
+    explore_codecombat: "Tutki CodeCombat:a"
+    finished_hoc: "Kooditunti on valmis"
+    get_certificate: "Hanki todistus!"
     level_complete: "Taso suoritettu"
     completed_level: "Suoritit tason:"
     course: "Kurssi:"
@@ -471,7 +471,7 @@
     reload_title: "Ladataanko kaikki koodi uudestaan?"
     reload_really: "Oletko varma että haluat ladata tämän tason uudestaan ja aloittaa alusta?"
     reload_confirm: "Lataa Kaikki"
-#    test_level: "Test Level"
+    test_level: "Testaa taso"
     victory: "Voitto"
     victory_title_prefix: ""
     victory_title_suffix: " Valmis"
@@ -1428,7 +1428,7 @@
     select_your_hero_description: "Voit vaihtaa sankariasi menemällä kurssit sivulle ja valitsemalla \"Valitse sankarisi\""
     select_this_hero: "Valitse tämä sankari"
     current_hero: "Nykyinen sankari:"
-#    current_hero_female: "Current Hero:"
+    current_hero_female: "Nykyinen sankari:"
     change_hero: "Vaihda sankaria"
 #    web_dev_language_transition: "All classes program in HTML / JavaScript for this course.  Classes that have been using Python will start with extra JavaScript intro levels to ease the transition.  Classes that are already using JavaScript will skip the intro levels."
 #    course_membership_required_to_play: "You'll need to join a course to play this level."
@@ -1759,7 +1759,7 @@
     victory_course_share_web: "katsoa nettisivua,"
     victory_course_share_suffix: "jonka sinä juuri loit."
     copy_url: "Kopio URL-osoite"
-#    share_with_teacher_email: "Send to your teacher"
+    share_with_teacher_email: "Lähetä opettajallesi"

   game_dev:
     creator: "Tekijä"


renamed grey to shade

renamed grey to shade

Former-commit-id: 4d8670d5835d6cd14c982205e1db189bf5b11e37 [formerly e04efeb66cbfb701dd95ecaafacdcfb4656ec102]
Former-commit-id: 822be90c16783273d6bd74399c07c42d1626150c

--- src/menu/views/ColorMenu.coffee
+++ src/menu/views/ColorMenu.coffee
@@ -67,7 +67,7 @@

   grey: (menuColor) ->
     # greys all lowercase letters
-    @addNode "Grey", =>
+    @addNode "Shade", =>
       @g.colorscheme.set "showLowerCase", false
       @model.each (seq) ->
         residues = seq.get "seq"
@@ -77,7 +77,7 @@
             grey.push index
         seq.set "grey", grey

-    @addNode "Grey by threshold", =>
+    @addNode "Shade by threshold", =>
       threshold = prompt "Enter threshold (in percent)", 20
       threshold = threshold / 100
       maxLen = @model.getMaxLength()
@@ -90,13 +90,13 @@
       @model.each (seq) ->
         seq.set "grey", grey

-    @addNode "Grey selection", =>
+    @addNode "Shade selection", =>
       maxLen = @model.getMaxLength()
       @model.each (seq) =>
         blocks = @g.selcol.getBlocksForRow(seq.get("id"),maxLen)
         seq.set "grey", blocks

-    @addNode "Reset grey", =>
+    @addNode "Reset shade", =>
       @g.colorscheme.set "showLowerCase", true
       @model.each (seq) ->
         seq.set "grey", []


Uncommented some headers

Uncommented some headers

--- app/locale/ko.coffee
+++ app/locale/ko.coffee
@@ -399,7 +399,7 @@
     recovered: "gem 구매후 브라우져를 새로고침 하세요."
     price: "x{{gems}} / 한달"

-#  subscribe:
+  subscribe:
 #    comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"
 #    feature1: "110+ basic levels across 4 worlds"
 #    feature2: "10 powerful new heroes with unique skills!"
@@ -496,7 +496,7 @@
 #    level_to_unlock: "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."

-#  skill_docs:
+  skill_docs:
 #    writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this
 #    read_only: "read-only"
 #    action: "Action"
@@ -516,7 +516,7 @@
     returns: "뒤로가기"
 #    granted_by: "Granted by"

-#  save_load:
+  save_load:
     granularity_saved_games: "저장됨"
     granularity_change_history: "기록"

@@ -586,7 +586,7 @@
 #    more_info_2: "teachers forum"
 #    more_info_3: "is a good place to connect with fellow educators who are using CodeCombat."

-#  teachers_survey:
+  teachers_survey:
 #    title: "Teacher Survey"
 #    must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
 #    retrieving: "Retrieving information..."


Update ru.coffee

Update ru.coffee

Translated new blocks and small fixes with You/you

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -94,15 +94,15 @@
     campaign_classic_algorithms: "Классические принципы"
     campaign_classic_algorithms_description: "... которые чаще всего встречаются в копьютерных науках."

-#  share_progress_modal:
-#    blurb: "You’re making great progress! Tell someone how much you've learned with CodeCombat."
-#    email_invalid: "Email address invalid."
-#    form_blurb: "Enter their email below and we’ll show them!"
-#    form_label: "Email Address"
-#    placeholder: "email address"
-#    title: "Excellent Work, Apprentice"
-#    tell_friend: "Tell your Friend"
-#    tell_parent: "Tell your Parent"
+  share_progress_modal:
+    blurb: "Вы отлично продвигаетесь! Расскажите кому-нибудь как много вы уже выучили с CodeCombat."
+    email_invalid: "Email-адрес некорректен."
+    form_blurb: "Введите их email-адреса ниже, и мы покажем им!"
+    form_label: "Email-адрес"
+    placeholder: "email-адрес"
+    title: "Прекрасная работа, Ученик"
+    tell_friend: "Расскажите своему другу"
+    tell_parent: "Расскажите своим родителям"

   login:
     sign_up: "Создать аккаунт"
@@ -141,8 +141,8 @@
     books: "Книги"

   common:
-#    back: "Back" # When used as an action verb, like "Navigate backward"
-#    continue: "Continue" # When used as an action verb, like "Continue forward"
+    back: "Вернуться" # When used as an action verb, like "Navigate backward"
+    continue: "Продолжить" # When used as an action verb, like "Continue forward"
     loading: "Загрузка..."
     saving: "Сохранение..."
     sending: "Отправка..."
@@ -335,10 +335,10 @@
     tip_source_code: "Я хочу изменить мир, но они вряд ли дадут мне исходники."
     tip_javascript_java: "Java к JavaScript относится так же, как кол относится к колготкам. - Chris Heilmann (перефраз.)"
     tip_move_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_miss_lunch: "Anyone who has lost track of time when using a computer knows the propensity to dream, the urge to make dreams come true and the tendency to miss lunch. - Tim Berners-Lee"
-#    tip_hate_computers: "That's the thing about people who think they hate computers. What they really hate is lousy programmers. - Larry Niven"
+    tip_google: "У вас проблема, которую вы не можете решить? Гуглите!"
+    tip_adding_evil: "Добавим щепотку зла."
+    tip_miss_lunch: "Каждый, кто потерял счет времени, используя компьютер, знает, что такое склонность к мечтанию, желание превратить мечты в реальность и тенденция к пропуску обеда. - Tim Berners-Lee"
+    tip_hate_computers: "Есть одна вещь в людях, которые думают, что они ненавидят компьютеры. Что они на самом деле ненавидят, так это плохих программистов. - Larry Niven"

   game_menu:
     inventory_tab: "Инвентарь"
@@ -361,7 +361,7 @@
     leaderboard: "Таблица лидеров"
     view_other_solutions: "Посмотреть другие решения"
     scores: "Рейтинг"
-#    top_players: "Top Players by"
+    top_players: "Сортировать игроков по"
     day: "Сегодня"
     week: "На этой неделе"
     all: "За все время"
@@ -395,7 +395,7 @@
     prompt_body: "Хотите еще?"
     prompt_button: "Войти в магазин"
     recovered: "Предыдущие покупки самоцветов восстановлены. Пожалуйста, обновите страницу."
-#    or_subscribe: "Or 3500 gems/mo if you..."
+    or_subscribe: "Или 3500 самоцветов в месяц, если вы..."

   subscribe:
     comparison_blurb: "Отточите свое мастерство багодаря подписке на CodeCombat!"
@@ -411,7 +411,7 @@
     unsubscribe: "Отписаться"
     confirm_unsubscribe: "Подтвердить отмену подписки"
     never_mind: "Неважно, Я Все Равно Тебя Люблю"
-    thank_you_months_prefix: "Спасибо Вам за поддерживание нас в течение последних"
+    thank_you_months_prefix: "Спасибо вам за поддерживание нас в течение последних"
     thank_you_months_suffix: "месяцев."
     thank_you: "Спасибо за поддержку CodeCombat."
     sorry_to_see_you_go: "Жаль, что вы уходите! Пожалуйста, расскажите нам, что мы могли бы сделать лучше."
@@ -539,11 +539,11 @@
     preparation_title: "Подготовка"
     preparation_1: "CodeCombat бесплатен для игры на базовых уровнях и не требует регистрации от студентов. Мы призываем учителей"
     preparation_play_campaign: "пройти кампанию"
-    preparation_2: ", чтобы попробовать CodeCombat в деле, но единственная вещь, которая Вам действительно необходима - уверенность, что студенты имеют доступ к компьютеру."
+    preparation_2: ", чтобы попробовать CodeCombat в деле, но единственная вещь, которая вам действительно необходима - уверенность, что студенты имеют доступ к компьютеру."
     preparation_3: "Для учителей нет необходимости в обладании обширными знаниями в понятиях компьютерной науки для того, чтобы студенты получали удудовольствие от обучения с CodeCombat."
     violent_title: "В игре есть насилие?"
     violent_1: "Нас часто спрашивают об этом из-за нашего названия. Хотя CodeCombat содержит элементы мультипликационного насилия, ни в визуальных эффектах ни в языке нет чего-либо наглядного."
-    violent_2: "Если Вы спокойно позволяете своим студентам играть в Angry Birds, Вы будете спокойны и с CodeCombat."
+    violent_2: "Если вы спокойно позволяете своим студентам играть в Angry Birds, вы будете спокойны и с CodeCombat."
     for_girls_title: "А девочкам понравится?"
     for_girls_1: "У нас есть три игровых режима: строительство, загадки и бои. Мы намеренно разработали каждый из режимов так, чтобы быть интересными и для мальчиков и для девочек, и думаем, что уровни со строительством и загадками будут особенно отличать игру от насильственных ААА игр, которые отталкивают игроков-женщин."
     what_cover_title: "Что мы охватываем?"
@@ -566,7 +566,7 @@
     what_cover_input_1: "Обработка пользовательского ввода"
     what_cover_input_2: "- реагирование на события ввода для создания пользовательского интерфейса."
     sys_requirements_title: "Системные требования"
-    sys_requirements_1: "Так как CodeCombat - это игра, для плавной работы она более требовательна к компьютерам, чем видео или электронные учебники. Мы оптимизировали CodeCombat для быстрой работы во всех современных браузерах и старых машинах, так что все могут играть. Тем не менее, вот наши предложения, чтобы Вы могли получить максимум от Вашего Часа Кода:"
+    sys_requirements_1: "Так как CodeCombat - это игра, для плавной работы она более требовательна к компьютерам, чем видео или электронные учебники. Мы оптимизировали CodeCombat для быстрой работы во всех современных браузерах и старых машинах, так что все могут играть. Тем не менее, вот наши предложения, чтобы вы могли получить максимум от вашего Часа Кода:"
     sys_requirements_2: "Используйте последние версии Chrome или Firefox."
     sys_requirements_3: "Хотя CodeCombat будет работать в таких старых браузерах как IE9, производительность не будет хорошей. Chrome подходит лучше всего."
     sys_requirements_4: "Используйте новые компьютеры."
@@ -1015,7 +1015,7 @@
     play_counts: "Счетчики игр"
     feedback: "Отзыв"
     payment_info: "Информация о платеже"
-#    campaigns: "Campaigns"
+    campaigns: "Кампании"

   delta:
     added: "Добавлено"
@@ -1154,7 +1154,7 @@
     basics_job_title: "Предпочитаемая роль в работе"
     basics_job_title_help: "Какую роль вы хотите?"
     basics_city: "Город"
-    basics_city_help: "Город, в котором Вы живете или хотели ли бы работать."
+    basics_city_help: "Город, в котором вы живете или хотели ли бы работать."
     basics_country: "Страна"
     basics_country_help: "Страна, в которой вы хотите работать (или живете сейчас)."
     basics_visa: "Статус работы в США"


Fixed translation issue.

Fixed translation issue.

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -1201,8 +1201,8 @@
     avg_student_exp_select: "Выберите лучший вариант"
     avg_student_exp_none: "Нет или почти нет опыта"
     avg_student_exp_beginner: "Новички - некоторый или фрагментарный опыт"
-    avg_student_exp_intermediate: "Средний уровень - некоторый опыт программирования типизированного кода"
-    avg_student_exp_advanced: "Продвинутый - обширный опыт программирования типизированного кода"
+    avg_student_exp_intermediate: "Средний уровень - некоторый опыт написания кода"
+    avg_student_exp_advanced: "Продвинутый - обширный опыт написания кода"
     avg_student_exp_varied: "Разные уровни опыта"
     student_age_range_label: "Диапазон возрастов учеников"
     student_age_range_younger: "Младше 6"


update ru

update ru

employers, admin blocks, misc

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -621,9 +621,9 @@
     achievement_search_title: "Искать достижения"
     read_only_warning2: "Примечание: вы не можете сохранять любые правки здесь, потому что вы не авторизованы."
     no_achievements: "Для этого уровня еще не были добавлены достижения."
-#    achievement_query_misc: "Key achievement off of miscellanea"
-#    achievement_query_goals: "Key achievement off of level goals"
-#    level_completion: "Level Completion"
+    achievement_query_misc: "Ключевое достижение за прочие задачи."
+    achievement_query_goals: "Ключевое достижение за цели уровня"
+    level_completion: "Выполнение уровня"
     pop_i18n: "Популяризируйте I18N"

   article:
@@ -860,11 +860,11 @@
     user_names: "Никнеймы"
     thang_names: "Имена предмета"
     files: "Файлы"
-#    top_simulators: "Top Simulators"
+    top_simulators: "Лучшие симуляторы"
     source_document: "Исходный документ"
     document: "Документ"
-#    sprite_sheet: "Sprite Sheet"
-    employers: "Работники"
+    sprite_sheet: "Лист спрайтов"
+    employers: "Работодатели"
     candidates: "Кандидаты"
     candidate_sessions: "Сессии кандидатов"
     user_remark: "Пользовательские поправки"
@@ -874,16 +874,16 @@
     heroes: "Герои"
     achievement: "Достижение"
     clas: "ЛСС"
-#    play_counts: "Play Counts"
+    play_counts: "Счетчики игр"
     feedback: "Отзыв"

   delta:
     added: "Добавлено"
     modified: "Изменено"
     deleted: "Удалено"
-#    moved_index: "Moved Index"
+    moved_index: "Перемещен индекс"
     text_diff: "Разница"
-#    merge_conflict_with: "MERGE CONFLICT WITH"
+    merge_conflict_with: "КОНФЛИКТ СЛИЯНИЯ С"
     no_changes: "Нет изменений"

   guide:
@@ -975,7 +975,7 @@
     oreilly: "электронная книга на ваш выбор"

   account_profile:
-    settings: "Настойки" # We are not actively recruiting right now, so there's no need to add new translations for this section.
+    settings: "Настройки" # We are not actively recruiting right now, so there's no need to add new translations for this section.
     edit_profile: "Редактировать профиль"
     done_editing: "Завершить редактирование"
     profile_for_prefix: "Профиль для "
@@ -1080,39 +1080,39 @@
     player_code: "Код игрока"

   employers:
-#    deprecation_warning_title: "Sorry, CodeCombat is not recruiting right now."
-#    deprecation_warning: "We are focusing on beginner levels instead of finding expert developers for the time being."
-#    hire_developers_not_credentials: "Hire developers, not credentials." # We are not actively recruiting right now, so there's no need to add new translations for the rest of this section.
+    deprecation_warning_title: "Извините, в данный момент CodeCombat не набирает новых сотрудников."
+    deprecation_warning: "В настоящее время мы фокусируемся на разработчиков начального уровня нежели опытных."
+    hire_developers_not_credentials: "Нанимаем разработчиков, не данные о них." # We are not actively recruiting right now, so there's no need to add new translations for the rest of this section.
     get_started: "Начнем"
-#    already_screened: "We've already technically screened all our candidates"
-#    filter_further: ", but you can also filter further:"
+    already_screened: "Мы уже технически просеили всех наших кандидатов"
+    filter_further: ", но вы можете профильтровать далее:"
     filter_visa: "Visa"
-#    filter_visa_yes: "US Authorized"
-#    filter_visa_no: "Not Authorized"
-#    filter_education_top: "Top School"
-#    filter_education_other: "Other"
+    filter_visa_yes: "Разрешение на работу в США"
+    filter_visa_no: "Нет разрешения на работу"
+    filter_education_top: "Высшее образование"
+    filter_education_other: "Другое"
     filter_role_web_developer: "Веб-разработчик"
-#    filter_role_software_developer: "Software Developer"
-#    filter_role_mobile_developer: "Mobile Developer"
+    filter_role_software_developer: "Разработчик ПО"
+    filter_role_mobile_developer: "Разработчик мобильных приложений"
     filter_experience: "Опыт"
-#    filter_experience_senior: "Senior"
-#    filter_experience_junior: "Junior"
-#    filter_experience_recent_grad: "Recent Grad"
-#    filter_experience_student: "College Student"
-#    filter_results: "results"
-#    start_hiring: "Start hiring."
-#    reasons: "Three reasons you should hire through us:"
-#    everyone_looking: "Everyone here is looking for their next opportunity."
-#    everyone_looking_blurb: "Forget about 20% LinkedIn InMail response rates. Everyone that we list on this site wants to find their next position and will respond to your request for an introduction."
-#    weeding: "Sit back; we've done the weeding for you."
-#    weeding_blurb: "Every player that we list has been screened for technical ability. We also perform phone screens for select candidates and make notes on their profiles to save you time."
-#    pass_screen: "They will pass your technical screen."
-#    pass_screen_blurb: "Review each candidate's code before reaching out. One employer found that 5x as many of our devs passed their technical screen than hiring from Hacker News."
-#    make_hiring_easier: "Make my hiring easier, please."
-#    what: "What is CodeCombat?"
-#    what_blurb: "CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. Our players have experience with all major tech stacks."
-#    cost: "How much do we charge?"
-#    cost_blurb: "We charge 15% of first year's salary and offer a 100% money back guarantee for 90 days. We don't charge for candidates who are already actively being interviewed at your company."
+    filter_experience_senior: "Опытный (Senior)"
+    filter_experience_junior: "Начинающий (Junior)"
+    filter_experience_recent_grad: "Выпускник"
+    filter_experience_student: "Студент"
+    filter_results: "результаты"
+    start_hiring: "Начать наем."
+    reasons: "Три причины, почему вы должны искать работников через нас:"
+    everyone_looking: "Все здесь ищут их следующую возможность."
+    everyone_looking_blurb: "Забудьте о 20% рейтинге откликов через LinkedIn InMail. Каждый в нашем списке хочет найти свое следующее место работы и ответит на ваше приглашение на интервью."
+    weeding: "Расслабьтесь; мы отобрали их для вас.."
+    weeding_blurb: "Каждый игрок в списке был просмотрен на предмет технических способностей. Мы так же ведем телефонные переговоры по выбранным кандидатам и делаем пометки в их профилях, чтобы сэкономить ваше время."
+    pass_screen: "Они пройдут ваш технический отбор."
+    pass_screen_blurb: "Просмотрите код каждого кандидата перед принятием решения. Один работодатель заметил, что количество наших разрабочиков, прошедших их технический отбор, в 5 раз превышало число набранных через Hacker News."
+    make_hiring_easier: "Сделайте мой наем сотрудников проще, пожалуйста."
+    what: "Что такое CodeCombat?"
+    what_blurb: "CodeCombat - это многопользовательская браузерная игра про программирование. Игроки пишут код, чтобы контроллировать свои войска в битвах против других разработчиков. Наши игроки имеют опыт во всех основных технических областях."
+    cost: "Как много мы просим взамен?"
+    cost_blurb: "Мы просим 15% оплаты за первый год работы и предлагаем 100% гарантию возврата денег в течении 90 дней. Мы не взимаем платы за кандидатов, котоые уже были активно проинтервьюированы в вашей компании."
     candidate_name: "Имя"
     candidate_location: "Местонахождение"
     candidate_looking_for: "Ищет"
@@ -1120,27 +1120,27 @@
     candidate_top_skills: "Лучшие навыки"
     candidate_years_experience: "Лет опыта"
     candidate_last_updated: "Последнее обновление"
-#    candidate_who: "Who"
-#    featured_developers: "Featured Developers"
-#    other_developers: "Other Developers"
-#    inactive_developers: "Inactive Developers"
+    candidate_who: "Кто"
+    featured_developers: "Рекомендуемые разработчики"
+    other_developers: "Прочие разработчики"
+    inactive_developers: "Неактивные разработчики"

   admin:
-#    av_espionage: "Espionage" # Really not important to translate /admin controls.
-#    av_espionage_placeholder: "Email or username"
-#    av_usersearch: "User Search"
-#    av_usersearch_placeholder: "Email, username, name, whatever"
-#    av_usersearch_search: "Search"
+    av_espionage: "Разведка" # Really not important to translate /admin controls.
+    av_espionage_placeholder: "Почта или юзернейм"
+    av_usersearch: "Поиск пользователей"
+    av_usersearch_placeholder: "Почта, юзернейм, имя, что угодно."
+    av_usersearch_search: "Поиск"
     av_title: "Панель администрирования"
     av_entities_sub_title: "Сущности"
     av_entities_users_url: "Пользователи"
     av_entities_active_instances_url: "Активные экземпляры"
     av_entities_employer_list_url: "Список работодателей"
-#    av_entities_candidates_list_url: "Candidate List"
-#    av_entities_user_code_problems_list_url: "User Code Problems List"
+    av_entities_candidates_list_url: "Список кандидатов"
+    av_entities_user_code_problems_list_url: "Список проблем с пользовательским кодом"
     av_other_sub_title: "Другое"
     av_other_debug_base_url: "База (для отладки base.jade)"
     u_title: "Список пользователей"
-#    ucp_title: "User Code Problems"
+    ucp_title: "Проблемы с пользовательским кодом"
     lg_title: "Последние игры"
     clas: "ЛСС"


Update ru.coffee

Update ru.coffee

Fixed some errors in formatting, seems like I broke that...

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -27,13 +27,13 @@
     hour: "час"
     hours: "часа(ов)"
     day: "день"
-   days: "дня"
+    days: "дня"
     week: "неделя"
-   weeks: "недел(ь)и"
+    weeks: "недел(ь)и"
     month: "месяц"
-   months: "месяца"
+    months: "месяца"
     year: "год"
-   years: "лет"
+    years: "лет"
    # Также может быть 2 года, но я не не уверен, могу ли я правильно скормить это вашему парсеру...
    # It also can be "2 года - 2 goda", but I do not know, can I feed it to your parser correctly...


Change "This attempt has been logged" to something more familiar

Test Plan: None

Reviewers: ben

Reviewed By: ben

Differential Revision: http://phabricator.khanacademy.org/D3700

--- scripts/moo.coffee
+++ scripts/moo.coffee
@@ -10,5 +10,5 @@
       robot.messageHipchat "MOOOOOOOOOOOOOOOOO"
     else
       if !msg.envelope.room
-        msg.send "This attempt has been logged"
+        msg.send "This incident will be reported"


New cluster text

New cluster text

Closes #9

--- app/lib/en-us.coffee
+++ app/lib/en-us.coffee
@@ -269,7 +269,7 @@
       description: '''Bubbles are usually circular or elliptical, but often only partially seen. They may be viewed as arcs of red in these data, sometimes brightening into pinks and whites. They are seen because ultraviolet radiation from young stars causes molecules in the surrounding gas to glow in the infrared. Bubble will sometimes have obvious star clusters nearby, but not always.'''
     clusters:
       title: 'Star Cluster Examples'
-      description: '''Star clusters are small groups of stars huddled together in these images. They are bright, often white or blue. Bright stars often create a cross-hair shape because of the light overloading the Spitzer detectors, this is normal and you don't need to mark them.'''
+      description: '''Star clusters are small groups of stars huddled together in these images. They are bright, often white or blue. You might see them inside a bubble, or on their own. Bright stars often create a cross-hair shape because of the light overloading the Spitzer detectors, this is normal and you don't need to mark those shapes as anything unusual.'''
     egos:
       title: 'EGO Examples'
       description: '''Extended green objects (EGOs) are usually seen near to bright stars as very small, irregular blobs of green. They are quite rare and they are a special find. There is much work being done by researchers to find and understand these objects.'''


Update content

Update content


minor fix

minor fix

--- app/locale/bg.coffee
+++ app/locale/bg.coffee
@@ -322,7 +322,7 @@
     tip_brute_force: "Когато се съмняваш, използвай груба сила. - Ken Thompson"
     tip_extrapolation: "Има само два типа хора: тези, които могат да екстраполират от непълни данни..."
     tip_superpower: "Програмирането е най-голямото ни доближаване до суперсилата."
-    tip_control_destiny: "При истинският отворен код всеки има правото да контролира собствената си съдба. - Линус Торвалдс"
+    tip_control_destiny: "При истинския отворен код всеки има правото да контролира собствената си съдба. - Линус Торвалдс"
     tip_no_code: "Няма по-бърз код от отсъстващия."
     tip_code_never_lies: "Кодът никога не лъже, коментарите - понякога. — Ron Jeffries"
     tip_reusable_software: "Преди кодът да стане преизползваем, той трябва да е използваем."