Rename orientation to orient in certain places; remove reorientBranchDivider()

--- src/scripts/tael.coffee
+++ src/scripts/tael.coffee
@@ -74,7 +74,7 @@

         emptyInnerHtml parent.tile.value.content.dom

-        orientation_loc_substr = (parent.tile.value.content.dom.attr 'class')
+        orient_loc_substr = (parent.tile.value.content.dom.attr 'class')
             .split('-')
             .pop()

@@ -86,7 +86,7 @@
             'content':
                 'dom':
                     parent.tile.value.content.dom
-                    .attr 'class', 'tael-node-branch-' + orientation_loc_substr
+                    .attr 'class', 'tael-node-branch-' + orient_loc_substr
             'layout':
                 'orientation': orientation
                 'divider_location': 0.5
@@ -130,8 +130,6 @@
     tiles[branch.value.children.right].value.content.dom.css dimension,
         (do second_child_size.toString) + '%'

-reorientBranchDivider = (tiles, id) -> undefined
-
 module.exports = ->
     new_tiles = [
         [0, 'siblingless']
@@ -144,4 +142,4 @@
         newTile tiles, tile[0], tile[1]

     moveBranchDivider tiles, 3, 1.0 / 3
-    moveBranchDivider tiles, 5, 0.5+    moveBranchDivider tiles, 5, 0.5


add Ruby 2.1.6  to versions.coffee

add Ruby 2.1.6  to versions.coffee


redirect to /login for any unauthenticated response

--- src/scripts/loader.coffee
+++ src/scripts/loader.coffee
@@ -10,6 +10,20 @@

   # The root URI prefixed on all non-external AJAX and Backbone URIs
   root = settings.root
+
+  # Patch `Backbone.ajax` so unauthorized responses are redirected to `/login`
+  Backbone_ajax = Backbone.ajax
+  Backbone.ajax = () ->
+    promise = Backbone_ajax.apply(@, arguments)
+    promise.fail (jqXHR) ->
+      switch jqXHR.status
+        when 403 # HACK: this should be 401
+          window.location.href = '/login'
+        else
+          console.error(arguments)
+
+    return promise
+

   init = (options = {}) ->
     # Append /test to the root if the app is in test mode


Remove yomama script

Remove yomama script

The site is referring to spam now and script is not working.

--- src/scripts/yomama.coffee
+++ src/scripts/yomama.coffee
@@ -1,25 +0,0 @@
-# Description:
-#   None
-#
-# Dependencies:
-#   None
-#
-# Configuration:
-#   None
-#
-# Commands:
-#   hubot yo mama joke me - Returns a random Yo Mama joke
-#
-# Author:
-#   victorbutler
-
-module.exports = (robot) ->
-  robot.respond /(yo\s?mama( joke)?)( me)?/i, (msg) ->
-    yoMama msg, (url) ->
-      msg.send url
-
-yoMama = (msg, cb) ->
-  msg.http('http://www.yomamajokesdb.com/random-joke/')
-    .get() (err, res, body) ->
-      cb body.match(/<div id="random">([^\<]+)<\/div>/)?[1] || "Sorry, this joke is very inappropriate (or could not be found)"
-


hub: limit number of changefeeds per client to 500 to prevent bugs/users DOS'ing server (as these are expensive)

--- src/smc-hub/rethink.coffee
+++ src/smc-hub/rethink.coffee
@@ -18,6 +18,16 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ###############################################################################
+
+# The following limit massively reduces the chances of a feedback-loop/race
+# condition bug causing the servers to get killed.   Clients may currently
+# just silently not open new docs if they hit the limit, which is bad.  However,
+# it's very unlikely somebody has 100 files open at once on purpose -- the browser
+# would likely die.  This is meant to prevent situations where a *bug* causes
+# thousands of changefeeds to get opened in a second.
+# If there were a better error, this could be a parameter that depends on
+# whether the user (or project) is paying or not.
+MAX_CHANGEFEEDS_PER_CLIENT = 5*100 # about 3-5 feeds per file right now

 fs         = require('fs')
 async      = require('async')
@@ -3105,6 +3115,13 @@
         else
             opts.cb?()

+        # Also decrement count of changefeeds for given client
+        client_name = @_user_get_changefeed_id_to_user?[opts.id]
+        if client_name?
+            @_user_get_changefeed_counts[client_name] -= 1
+            delete @_user_get_changefeed_id_to_user[opts.id]
+
+
     user_query: (opts) =>
         opts = defaults opts,
             account_id : undefined
@@ -3217,6 +3234,24 @@
                 if changes and not multi
                     opts.cb("changefeeds only implemented for multi-document queries")
                     return
+                if changes
+                    # Count the number of changefeeds by a given client so we can cap it.
+                    # This code is here rather than in @user_get_query (or elsewhere) to ensure
+                    # that the counter stays consistent.
+                    client_name = "#{opts.account_id}-#{opts.project_id}"
+                    cnt = @_user_get_changefeed_counts ?= {}
+                    ids = @_user_get_changefeed_id_to_user ?= {}
+                    if not cnt[client_name]?
+                        cnt[client_name] = 1
+                    else if cnt[client_name] >= MAX_CHANGEFEEDS_PER_CLIENT
+                        opts.cb("user may create at most #{MAX_CHANGEFEEDS_PER_CLIENT} changefeeds; please close files, refresh browser, restart project")
+                        return
+                    else
+                        # increment before successfully making it to prevent huge bursts causing trouble!
+                        cnt[client_name] += 1
+                    dbg("@_user_get_changefeed_counts={#{client_name}:#{cnt[client_name]} ...}")
+                    ids[changes.id] = client_name
+
                 @user_get_query
                     account_id : opts.account_id
                     project_id : opts.project_id
@@ -3225,7 +3260,10 @@
                     options    : options
                     multi      : multi
                     changes    : changes
-                    cb         : (err, x) => opts.cb(err, {"#{table}":x})
+                    cb         : (err, x) =>
+                        if err and changes
+                            cnt[client_name] -= 1  # didn't actually make the changefeed, so don't count it.
+                        opts.cb(err, {"#{table}":x})
         else
             opts.cb("invalid user_query of '#{table}' -- query must be an object")


fix for #361

fix for #361

--- src/smc-util/client.coffee
+++ src/smc-util/client.coffee
@@ -1112,7 +1112,7 @@
             project_id : opts.project_id
             command    : "find"
             timeout    : 15
-            args       : [opts.path, '-xdev', '-type', 'd', '-iname', opts.query]
+            args       : [opts.path, '-xdev', '!', '-readable', '-prune', '-o', '-type', 'd', '-iname', opts.query, '-readable', '-print']
             bash       : false
             cb         : (err, result) =>
                 if err


test_client.coffee: reject promises with Error not string

newer versions of the promise lib seem picky about this.

--- bokehjs/test/test_common/test_client.coffee
+++ bokehjs/test/test_common/test_client.coffee
@@ -56,7 +56,7 @@
     console.log("server err: #{data}")
   handle.on 'close', (code) ->
     console.log("server exited #{code}")
-    promise.reject("Server exited before test promise was resolved")
+    promise.reject(new Error("Server exited before test promise was resolved"))

   cleanup_process = (value_or_error) ->
     console.log("Killing server process")
@@ -83,7 +83,7 @@
         client.destroy()
         client = null
     else if num_server_attempts > 10
-      promise.reject("Failed to connect to the server")
+      promise.reject(new Error("Failed to connect to the server"))
     else if client != null
       # still waiting on a client we already have...
       setTimeout checkServer, 100


removed rendundant key

removed rendundant key

--- lib/both.coffee
+++ lib/both.coffee
@@ -72,7 +72,6 @@
       return @_searchResult

 @AntiSearchSource =
-  _methodName: '__makeGlobalSearch'
   _transforms: {}
   _allowRules: {}


#1031 - KF displays 'AAUTOMATIC' after removing custom option ID

--- jsapp/xlform_model_view/view.choices.coffee
+++ jsapp/xlform_model_view/view.choices.coffee
@@ -109,7 +109,7 @@
         if val is ''
           @model.unset('name')
           @model.set('setManually', false)
-          val = 'AAUTOMATIC'
+          val = 'AUTOMATIC'
           @$el.trigger("choice-list-update", @options.cl.cid)
         else
           val = $modelUtils.sluggify(val, {


Still working on it...

Still working on it...

--- app/assets/javascripts/sprangular/services/catalog.coffee
+++ app/assets/javascripts/sprangular/services/catalog.coffee
@@ -34,7 +34,7 @@
     find: (id) ->
       $http.get("/api/products/#{id}", class: Sprangular.Product)
         .then (response) ->
-          console.log response.data
+          console.log response

     getPaged: (page=1, params={}) ->
       $http.get("/api/products", ignoreLoadingIndicator: params.ignoreLoadingIndicator, params: {per_page: @pageSize, page: page, "q[name_or_description_cont]": params.search, "q[taxons_permalink_eq]": params.taxon})


Use uic:domchange instead of document.ready

--- app/assets/javascripts/ui_components/sticky_header.coffee
+++ app/assets/javascripts/ui_components/sticky_header.coffee
@@ -1,4 +1,4 @@
-$ ->
+$(document).on 'uic:domchange', ->
   margin_top = $('.navbar-fixed-top, .navbar-static-top').height()
   $('table[data-toggle=sticky-header]').floatThead
     useAbsolutePositioning: false


8979: remove debugging line

8979: remove debugging line

--- app/assets/javascripts/views/file_uploader_view.js.coffee
+++ app/assets/javascripts/views/file_uploader_view.js.coffee
@@ -38,7 +38,6 @@
       @id_field.val('')

   file_uploaded: (response_data) ->
-    console.log(I18n.t('errors.file_upload')) #TODO: ensure this available then remove
     @id_field.val(response_data.id)

   upload_errored: (file, response_data) ->


initially, when user click the upload button, retained the current [file] to be processed later.

--- lib/assets/javascripts/attachinary.js.coffee
+++ lib/assets/javascripts/attachinary.js.coffee
@@ -96,6 +96,10 @@
       return

     bindEventHandlers: ->
+      self = this
+      @$input.on 'click', (event, data) ->
+        if self.isUpdateOperation()
+          self.oldFile = self.files[0]
         self.$input.trigger 'attachinary:fileselection'

       @$input.bind 'fileuploadsend', (event, data) =>


disabling useNewKites should disable it for all users

--- client/Main/VirtualizationController.coffee
+++ client/Main/VirtualizationController.coffee
@@ -351,8 +351,8 @@

   shouldUseNewKites: ->
     new Promise (resolve, reject) ->
-      return resolve KD.useNewKites  if KD.useNewKites?
       KD.remote.api.JKiteStack.fetchInfo (err, info) ->
+        return resolve info.isEnabled and KD.useNewKites  if KD.useNewKites?
         return reject err  if err?
         useNewKites = info.isEnabled and Math.random() <= info.ratio
         KD.useNewKites = useNewKites


Teambuilder: Level changing is now saved

--- client/app/js/views/teambuilder_view.coffee
+++ client/app/js/views/teambuilder_view.coffee
@@ -29,6 +29,7 @@
     'change .selected_ability': 'changeAbility'
     'change .selected_item': 'changeItem'
     'change .selected_gender': 'changeGender'
+    'change .selected_level': 'changeLevel'
     'change .selected_shininess': 'changeShiny'
     'change .iv-entry': 'changeIv'
     'focus .ev-entry': 'focusEv'
@@ -61,6 +62,7 @@

   attachEventsToTeam: (team) =>
     @listenTo(team, 'add', @renderPokemon)
+    @listenTo(team, 'change:level', @renderStats)
     @listenTo(team, 'change:ivs', @renderStats)
     @listenTo(team, 'change:evs', @renderStats)
     @listenTo(team, 'change:nature', @renderStats)
@@ -179,6 +181,14 @@
   changeGender: (e) =>
     $list = $(e.currentTarget)
     @getSelectedPokemon().set("gender", $list.val())
+
+  changeLevel: (e) =>
+    $input = $(e.currentTarget)
+    value = parseInt($input.val())
+    value = 100  if isNaN(value) || value > 100
+    value = 1  if value < 1
+    $input.val(value)
+    @getSelectedPokemon().set("level", value)

   changeShiny: (e) =>
     $checkbox = $(e.currentTarget)


Teambuilder: Fixed clone team

Teambuilder: Fixed clone team

--- client/app/js/views/teambuilder_view.coffee
+++ client/app/js/views/teambuilder_view.coffee
@@ -106,7 +106,7 @@
   cloneTeam: (e) =>
     $team = $(e.currentTarget).closest('.select-team')
     id = $team.data('id')
-    @getTeam(id).clone().save()
+    @getTeam(id).clone().set("id", null).save()
     @renderTeams()
     return false


Actually restore original global Function

--- src/loophole.coffee
+++ src/loophole.coffee
@@ -14,4 +14,4 @@
     global.Function = (source) -> vm.runInThisContext(source)
     fn()
   finally
-    global.Function = Function
+    global.Function = previousFunction


Update lib/assets/javascripts/turbolinks.js.coffee

Renaming function

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -224,13 +224,13 @@
 browserSupportsPushState =
   window.history and window.history.pushState and window.history.replaceState and window.history.state != undefined

-browserIsntCriOS =
+browserIsntBuggy =
   !navigator.userAgent.match /CriOS\//

 requestMethodIsSafe =
   requestMethod in ['GET','']

-initializeTurbolinks() if browserSupportsPushState and requestMethodIsSafe and browserIsntCriOS
+initializeTurbolinks() if browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe

 # Call Turbolinks.visit(url) from client code
 @Turbolinks = { visit }


fixed conflict

fixed conflict

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -53,10 +53,10 @@
     positionX: window.pageXOffset

   constrainPageCacheTo(10)
+  console.log pageCache

 constrainPageCacheTo = (limit) ->
-  delete pageCache[currentState.position - limit] if currentState.position == window.history.length - 1
-
+  delete pageCache[currentState.position - limit]

 changePage = (title, body) ->
   document.title = title
@@ -174,6 +174,7 @@
   historyState = state

+
 browserSupportsPushState =
   window.history and window.history.pushState and window.history.replaceState


Revert "Disable use_bl for /top request"

This reverts commit 0751f501525a5afa424fbdd00f7780f8ade48fdf.
Fixed in the BnW API (added use_bl for today).

--- coffee/controllers/posts_controller.coffee
+++ coffee/controllers/posts_controller.coffee
@@ -28,8 +28,6 @@
       else if params.tag?
         query.tag = params.tag
         params.title = "*#{params.tag}"
-      else
-        query = {}

       @collection = new Posts [], id: params.id, query: query
       @view = new PostsView collection: @collection, pageble: params.pageble


rm unused deep_merge

rm unused deep_merge

--- src/xml.coffee
+++ src/xml.coffee
@@ -1,5 +1,5 @@
 { EventEmitter } = require 'events'
-{ deep_merge, new_attrs, safe } = require './util'
+{ new_attrs, safe } = require './util'
 EVENTS = ['add', 'attr', 'data', 'text', 'raw', 'show', 'hide', 'remove',
           'replace', 'close']


Changed MIN_POSTS back to 5.

Changed MIN_POSTS back to 5.

--- client/coffeescript/templates/stream.coffee
+++ client/coffeescript/templates/stream.coffee
@@ -1,6 +1,6 @@
 _.extend( Template.stream,
   group_posts: ->
-    MIN_POSTS = 2
+    MIN_POSTS = 5
     count = Posts.find( 'parent_id': @_id ).count()
     return count >= MIN_POSTS
   posts: ->


welcomemodal: find correct route after destroy the Modal

--- client/home/lib/welcome/welcomemodal.coffee
+++ client/home/lib/welcome/welcomemodal.coffee
@@ -80,7 +80,18 @@

     @setClass 'out'

-    kd.singletons.router.handleRoute '/IDE'  if selfInitiated
+    { router } = kd.singletons
+    { visitedRoutes: tempVisitedRoutes } = router
+    # To remove infinite loop between these 2 routes
+    # which are `/Welcome` and `/Home` and We are going to
+    # redirect to `Any route` after destroying Welcome Modal
+    # `Any route` -> `/Welcome` -> `/Home` -> `/Welcome`
+    [..., beforeLast, last] = tempVisitedRoutes
+
+    tempVisitedRoutes.pop()  if last is '/Welcome' and beforeLast.indexOf('/Home') > -1
+
+    welcomeIndex = tempVisitedRoutes.lastIndexOf('/Welcome')
+    router.handleRoute tempVisitedRoutes[welcomeIndex - 1]  if selfInitiated

     # fat arrow is not unnecessary - sy
     kd.utils.wait 400, => super


Remove useless parenthesis

Remove useless parenthesis

--- source/javascripts/corporate/header.coffee
+++ source/javascripts/corporate/header.coffee
@@ -33,7 +33,7 @@

   updateNavigation: =>
     for section, sectionData of @sections
-      if (@sectionReachedTop(section, sectionData) || @scrollReachedBottom())
+      if @sectionReachedTop(section, sectionData) || @scrollReachedBottom()
         current = if @scrollReachedBottom() then 'footer' else section
         $('.header__navigation-item a').removeClass('active')
         $('.header__navigation-item a[href=#' + current + ']').addClass('active')


Some fixes to last merge

Some fixes to last merge

--- app/locale/bg.coffee
+++ app/locale/bg.coffee
@@ -6,11 +6,7 @@
     play: "Играй" # The big play button that opens up the campaign view.
     old_browser: "О, не! Браузърът ти е твърде стар за CodeCombat. Съжалявам!" # Warning that shows up on really old Firefox/Chrome/Safari
     old_browser_suffix: "Все пак можеш да опиваш, но най-вероятно няма да проработи."
-<<<<<<< HEAD
     ipad_browser: "Лошa новинa: CodeCombat не работи в браузъра на iPad. Добра новина: Приложението ни за iPad изчаква одобрение от Apple."
-=======
-#    ipad_browser: "Bad news: CodeCombat doesn't run on iPad in the browser. Good news: our native iPad app is awaiting Apple approval."
->>>>>>> upstream/master
     campaign: "Кампания"
     for_beginners: "За начинаещи"
 #    multiplayer: "Multiplayer" # Not currently shown on home page
@@ -64,11 +60,7 @@
 #    purchasable: "Purchasable" # For a hero you unlocked but haven't purchased
     available: "Достъпен"
 #    skills_granted: "Skills Granted" # Property documentation details
-<<<<<<< HEAD
     heroes: "Герои" # Tooltip on hero shop button from /play
-=======
-#    heroes: "Heroes" # Tooltip on hero shop button from /play
->>>>>>> upstream/master
     achievements: "Постижения" # Tooltip on achievement list button from /play
     account: "Акаунт" # Tooltip on account button from /play
     settings: "Настройки" # Tooltip on settings button from /play
@@ -77,11 +69,7 @@
 #    choose_inventory: "Equip Items"
     buy_gems: "Купи скъпоценни камъни"
     subscription_required: "Нужен е абонамент"
-<<<<<<< HEAD
     older_campaigns: "Предишни кампании"
-=======
-    older_campaigns: "Предишни капмании"
->>>>>>> upstream/master
     anonymous: "Анонимен играч"
     level_difficulty: "Трудност"
     campaign_beginner: "Кампания за начинаещи"
@@ -104,13 +92,8 @@
     campaign_classic_algorithms: "Класически алгоритми"
 #    campaign_classic_algorithms_description: "... in which you learn the most popular algorithms in Computer Science."

-<<<<<<< HEAD
-#  share_progress_modal:
+  share_progress_modal:
     blurb: "Справяш се чудесно! Сподели с някого,колко много научи чрез CodeCombat."
-=======
-  share_progress_modal:
-#    blurb: "You’re making great progress! Tell someone how much you've learned with CodeCombat."
->>>>>>> upstream/master
     email_invalid: "Имейл адресът е невалиден"
 #    form_blurb: "Enter their email below and we’ll show them!"
     form_label: "Електронна поща"
@@ -131,11 +114,7 @@
 #    finishing: "Finishing"
     sign_in_with_facebook: "Вписване чрез Facebook"
     sign_in_with_gplus: "Вписване чрез G+"
-<<<<<<< HEAD
     signup_switch: "Създаване на нов акаунт?"
-=======
-#    signup_switch: "Want to create an account?"
->>>>>>> upstream/master

   signup:
     email_announcements: "Получава анонси по имейл"
@@ -359,7 +338,7 @@
 #    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"

-#  game_menu:
+  game_menu:
     inventory_tab: "Инвентар"
     save_load_tab: "Запиши/Зареди"
     options_tab: "Настройки"
@@ -376,7 +355,7 @@
     multiplayer_caption: "Играй с приятели!"
     auth_caption: "Запиши напредъка си."

-#  leaderboard:
+  leaderboard:
 #    leaderboard: "Leaderboard"
     view_other_solutions: "Виж други решения"
     scores: "Точки"
@@ -403,7 +382,7 @@
 #    equip: "Equip"
 #    unequip: "Unequip"

-#  buy_gems:
+  buy_gems:
     few_gems: "Няколко скъпоценни камъни"
     pile_gems: "Купчина скъпоценни камъни"
     chest_gems: "Сандък със скъпоценни камъни"


Select unit copy updated (#96)

Select unit copy updated (#96)

--- app/locale/en.coffee
+++ app/locale/en.coffee
@@ -1794,7 +1794,7 @@
     copy_class_url: "Copy Class URL"
     class_join_url_blurb: "You can also post this unique class URL to a shared webpage."
     add_students_manually: "Invite Students by Email"
-    bulk_assign: "Select course"
+    bulk_assign: "Select unit" #{change}
     assigned_msg_1: "{{numberAssigned}} students were assigned {{courseName}}."
     assigned_msg_2: "{{numberEnrolled}} licenses were applied."
     assigned_msg_3: "You now have {{remainingSpots}} available licenses remaining."


Finish until line: 1272

Finish until line: 1272

--- app/locale/id.coffee
+++ app/locale/id.coffee
@@ -1191,83 +1191,84 @@
     beautify: "Percantik kodemu dengan menstandarisasi formatnya."
     maximize_editor: "Memaksimalkan/meminimalisasi editor kode."

-#  community:
-#    main_title: "CodeCombat Community"
-#    introduction: "Check out the ways you can get involved below and decide what sounds the most fun. We look forward to working with you!"
-#    level_editor_prefix: "Use the CodeCombat"
-#    level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!"
-#    thang_editor_prefix: "We call units within the game 'thangs'. Use the"
-#    thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites."
-#    article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the"
-#    article_editor_suffix: "and help CodeCombat players get the most out of their playtime."
-#    find_us: "Find us on these sites"
-#    social_github: "Check out all our code on GitHub"
-#    social_blog: "Read the CodeCombat blog on Sett"
-#    social_discource: "Join the discussion on our Discourse forum"
-#    social_facebook: "Like CodeCombat on Facebook"
-#    social_twitter: "Follow CodeCombat on Twitter"
-#    social_gplus: "Join CodeCombat on Google+"
-#    social_slack: "Chat with us in the public CodeCombat Slack channel"
-#    contribute_to_the_project: "Contribute to the project"
-
-#  clans:
-#    clan: "Clan"
-#    clans: "Clans"
-#    new_name: "New clan name"
-#    new_description: "New clan description"
-#    make_private: "Make clan private"
-#    subs_only: "subscribers only"
-#    create_clan: "Create New Clan"
-#    private_preview: "Preview"
-#    private_clans: "Private Clans"
-#    public_clans: "Public Clans"
-#    my_clans: "My Clans"
-#    clan_name: "Clan Name"
-#    name: "Name"
-#    chieftain: "Chieftain"
-#    edit_clan_name: "Edit Clan Name"
-#    edit_clan_description: "Edit Clan Description"
-#    edit_name: "edit name"
-#    edit_description: "edit description"
-#    private: "(private)"
-#    summary: "Summary"
-#    average_level: "Average Level"
-#    average_achievements: "Average Achievements"
-#    delete_clan: "Delete Clan"
-#    leave_clan: "Leave Clan"
-#    join_clan: "Join Clan"
-#    invite_1: "Invite:"
-#    invite_2: "*Invite players to this Clan by sending them this link."
-#    members: "Members"
-#    progress: "Progress"
-#    not_started_1: "not started"
-#    started_1: "started"
-#    complete_1: "complete"
-#    exp_levels: "Expand levels"
-#    rem_hero: "Remove Hero"
-#    status: "Status"
-#    complete_2: "Complete"
-#    started_2: "Started"
-#    not_started_2: "Not Started"
-#    view_solution: "Click to view solution."
-#    view_attempt: "Click to view attempt."
-#    latest_achievement: "Latest Achievement"
-#    playtime: "Playtime"
-#    last_played: "Last played"
-#    leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances."
-#    track_concepts1: "Track concepts"
-#    track_concepts2a: "learned by each student"
-#    track_concepts2b: "learned by each member"
-#    track_concepts3a: "Track levels completed for each student"
-#    track_concepts3b: "Track levels completed for each member"
-#    track_concepts4a: "See your students'"
-#    track_concepts4b: "See your members'"
-#    track_concepts5: "solutions"
-#    track_concepts6a: "Sort students by name or progress"
-#    track_concepts6b: "Sort members by name or progress"
-#    track_concepts7: "Requires invitation"
-#    track_concepts8: "to join"
-#    private_require_sub: "Private clans require a subscription to create or join."
+  community:
+    main_title: "Komunitas CodeCombat"
+    introduction: "Cek cara kamu dapat terlibat di bawah ini dan putuskan apa yang terdengar paling menyenangkan. Kami berharap dapat bekerja dengan kamu!"
+    level_editor_prefix: "Gunakan CodeCombat"
+    level_editor_suffix: "untuk membuat dan mengubah level. Pengguna dapat membuat level untuk kelas mereka, teman, hackathons, para siswa, dan saudara. Jika membuat sebuah level baru terdengar mengintimidasi, kamu bisa memulainya dengan membuat cabang salah satu dari milik kami!"
+    thang_editor_prefix: "Kami memanggil unit dalam game 'thangs'. Gunakan"
+    thang_editor_suffix: "untuk memodifikasi CodeCombat sumber karya seni. Perbolehkan unit untuk melempar proyektil, mengubah arah dari animasi, mengganti hit point unit, atau mengunggah vektor sprite milikmu."
+    article_editor_prefix: "Melihat ada kesalahan dalam dokumentasi kami? Ingin membuat beberapa instruksi untuk karakter buatanmu? Lihat"
+    article_editor_suffix: "dan bantu pemain CodeCombat untuk mendapatkan hasil maksimal dari waktu bermain mereka."
+    find_us: "Temukan kami di situs-situs berikut"
+    social_github: "Lihat semua kode kami di Github"
+    social_blog: "Baca blog CodeCombat di Sett"
+    social_discource: "Bergabung dalam diskusi di forum Discourse kami"
+    social_facebook: "Like CodeCombat di Facebook"
+    social_twitter: "Follow CodeCombat di Twitter"
+    social_gplus: "Bergabung CodeCombat di Google+"
+    social_slack: "Mengobrol bersama kami di channel publik Slack CodeCombat"
+    contribute_to_the_project: "Berkontribusi pada proyek"
+
+  clans:
+    clan: "Klan"
+    clans: "Klan"
+    new_name: "Nama baru klan"
+    new_description: "Deskripsi baru klan"
+    make_private: "Buat klan menjadi privat"
+    subs_only: "hanya pelanggan"
+    create_clan: "Buat klan baru"
+    private_preview: "Tinjau"
+    private_clans: "Klan Privat"
+    public_clans: "Klan Publik"
+    my_clans: "Klan Saya"
+    clan_name: "Nama Klan"
+    name: "Nama"
+    chieftain: "Kepala Suku"
+    edit_clan_name: "Ubah Nama Klan"
+    edit_clan_description: "Ubah Deskripsi Klan"
+    edit_name: "ubah nama"
+    edit_description: "ubah deskripsi"
+    private: "(privat)"
+    summary: "Rangkuman"
+    average_level: "Level Rata-rata"
+    average_achievements: "Pencapaian Rata-rata"
+    delete_clan: "Hapus Klan"
+    leave_clan: "Tinggalkan Klan"
+    join_clan: "Gabung Klan"
+    invite_1: "Undang:"
+    invite_2: "*Undang pemain untuk klan ini dengan mengirimkan mereka tautan."
+    members: "Anggota"
+    progress: "Perkembangan"
+    not_started_1: "belum mulai"
+    started_1: "mulai"
+    complete_1: "selesai"
+    exp_levels: "Perluas level"
+    rem_hero: "Melepaskan Hero"
+    status: "Status"
+    complete_2: "Lengkapi"
+    started_2: "Dimulai"
+    not_started_2: "Belum Dimulai"
+    view_solution: "Klik untuk melihat solusi."
+    view_attempt: "Klik untuk melihat percobaan."
+    latest_achievement: "Pencapaian Terakhir"
+    playtime: "Waktu bermain"
+    last_played: "Yang terakhir dimainkan"
+    leagues_explanation: "Bermain dalam liga melawan anggota klan lain di instansi arena multipemain."
+    track_concepts1: ""
+    track_concepts1: "Mengikuti konsep"
+    track_concepts2a: "dipelajari oleh setiap siswa"
+    track_concepts2b: "dipelajari oleh setiap anggota"
+    track_concepts3a: "Mengikuti level yang sudah selesai untuk setiap siswa"
+    track_concepts3b: "Mengikuti level yang sudah selesai untuk setiap anggota"
+    track_concepts4a: "Lihat siswa kamu'"
+    track_concepts4b: "Lihat anggota kamu'"
+    track_concepts5: "solusi"
+    track_concepts6a: "Urutkan siswa berdasarkan nama atau perkembangan"
+    track_concepts6b: "Urutkan anggota berdasarkan nama atau perkembangan"
+    track_concepts7: "Membutuhkan undangan"
+    track_concepts8: "untuk bergabung"
+    private_require_sub: "Klan privat membutuhkan langganan untuk membuat atau bergabung."

 #  courses:
 #    create_new_class: "Create New Class"


Update ru.coffee

Update ru.coffee

Oh nooooo...

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -805,7 +805,7 @@
     no_ranked_matches_pre: "Нет оценённых матчей для команды"
     no_ranked_matches_post: "! Сыграйте против нескольких противников и возвращайтесь сюда для оценки вашей игры."
     choose_opponent: "Выберите противника"
-   select_your_language: "Select your language!"
+    select_your_language: "Select your language!"
     tutorial_play: "Пройти обучение"
     tutorial_recommended: "Рекомендуется, если вы раньше никогда не играли"
     tutorial_skip: "Пропустить обучение"


Polished ru.coffee a bit

Polished ru.coffee a bit

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -315,26 +315,26 @@
     hard: "Сложно"

   about:
-    who_is_codecombat: "Кто есть CodeCombat?"
+    who_is_codecombat: "Кто стоит за CodeCombat?"
     why_codecombat: "Почему CodeCombat?"
-    who_description_prefix: "вместе начали CodeCombat in 2013. Также мы создали "
-    who_description_suffix: "в 2008, вывели его на первую строчку среди web и iOS приложений для обучения письму китайскими и японскими иероглифами."
+    who_description_prefix: "вместе начали CodeCombat в 2013 году. Также мы создали "
+    who_description_suffix: "в 2008 году, вывели его на первую строчку среди web и iOS приложений для обучения письму китайскими и японскими иероглифами."
     who_description_ending: "Теперь пришло время научить людей написанию кода."
-    why_paragraph_1: "При создании Skritter, Джордж не знал, как программировать и постоянно расстраивался из-за того, что не мог реализовать свои идеи. После этого он пытался учиться, но уроки были слишком медленными. Его сосед по дому, желая переквалифицироваться и прекратить преподавать, пробовал Codecademy, но \"наскучивало.\" Каждую неделю другой друг начинал Codecademy, затем бросал. Мы поняли, что это была та же проблема, которую мы решали со Skritter: люди получают навык через медленные, интенсивные уроки, когда то, что им нужно - быстрая, обширная практика. Мы знаем, как это исправить."
+    why_paragraph_1: "При создании Skritter, Джордж не знал, как программировать и постоянно расстраивался из-за того, что не мог реализовать свои идеи. После этого он пытался учиться, но уроки были слишком медленными. Его сосед, желая переквалифицироваться и прекратить преподавать, пробовал Codecademy, но \"потерял интерес.\" Каждую неделю очередной товарищ начинал Codecademy, затем бросал. Мы поняли, что это была та же проблема, которую мы решили со Skritter: люди получают навык через медленные, интенсивные уроки, в то время как то, что им нужно - быстрая, обширная практика. Мы знаем, как это исправить."
     why_paragraph_2: "Нужно научиться программировать? Вам не нужны уроки. Вам нужно написать много кода и прекрасно провести время, делая это."
-    why_paragraph_3_prefix: "Вот, о чём программирование. Это должно быть весело. Не вроде"
+    why_paragraph_3_prefix: "Вот где программирование. Это должно быть весело. Не забавно, вроде"
     why_paragraph_3_italic: "вау, значок,"
     why_paragraph_3_center: "а"
     why_paragraph_3_italic_caps: "НЕТ, МАМ, Я ДОЛЖЕН ПРОЙТИ УРОВЕНЬ!"
-    why_paragraph_3_suffix: "Вот, почему CodeCombat - мультиплеерная игра, а не курс уроков в игровой форме. Мы не остановимся, пока вы не сможете остановиться--в данном случае, это хорошо."
+    why_paragraph_3_suffix: "Вот, почему CodeCombat - мультиплеерная игра, а не курс уроков в игровой форме. Мы не остановимся, пока вы не потеряете голову - в данном случае, это хорошо."
     why_paragraph_4: "Если вы собираетесь увлечься какой-нибудь игрой, увлекитесь этой и станьте одним из волшебников века информационных технологий."
-    why_ending: "Эй, это бесплатно. "
+    why_ending: "И да, это бесплатно. "
     why_ending_url: "Начни волшебство сейчас!"
-    george_description: "Генеральный директор, бизнес-парень, веб-дизайнер, геймдизайнер, и чемпион начинающих программистов во всём мире."
-    scott_description: "Экстраординарный программист, архитектор программного обеспечения, кухонный волшебник, и мастер финансов. Скотт является разумным."
-    nick_description: "Маг программирования, эксцентрично мотивированный волшебник, и экспериментатор вверх ногами. Ник может делать всё и хочет построить CodeCombat."
+    george_description: "Генеральный директор, бизнес-парень, веб-дизайнер, геймдизайнер и чемпион начинающих программистов во всём мире."
+    scott_description: "Экстраординарный программист, архитектор программного обеспечения, кухонный волшебник и мастер финансов. Скотт рассудителен."
+    nick_description: "Маг программирования, мудрец эксцентричного мотивирования и чудаковатый экспериментатор. Ник может всё и хочет построить CodeCombat."
     jeremy_description: "Маг клиентской поддержки, юзабилити-тестер, и организатор сообщества; вы наверняка уже говорили с Джереми."
-    michael_description: "Программист, сисадмин и непризнанный технический гений, Михаэль является лицом, поддерживающим наши серверы онлайн."
+    michael_description: "Программист, сисадмин и непризнанный технический гений, Михаэль является лицом, поддерживающим наши серверы в доступности."

   legal:
     page_title: "Юридическая информация"
@@ -342,26 +342,26 @@
     opensource_description_prefix: "Посмотрите "
     github_url: "наш GitHub"
     opensource_description_center: "и посодействуйте, если вам понравилось! CodeCombat построен на десятках проектов с открытым кодом, и мы любим их. Загляните в "
-    archmage_wiki_url: "нашу вики Архимагов"
+    archmage_wiki_url: "наш вики-портал для Архимагов"
     opensource_description_suffix: ", чтобы увидеть список программного обеспечения, делающего игру возможной."
-    practices_title: "Лучшие уважаемые практики"
-    practices_description: "Это наши обещания тебе, игрок, менее юридическим языком."
+    practices_title: "Почтительные лучшие практики"
+    practices_description: "Это наши обещания тебе, игроку, менее юридическим языком."
     privacy_title: "Конфиденциальность"
     privacy_description: "Мы не будем продавать какой-либо личной информации. Мы намерены заработать деньги с помощью рекрутинга в конечном счёте, но будьте уверены, мы не будем распространять вашу личную информацию заинтересованным компаниям без вашего явного согласия."
     security_title: "Безопасность"
-    security_description: "Мы стремимся сохранить вашу личную информацию в безопасности. Как проект с открытым исходным кодом, наш сайт в свободном доступе для всех для пересмотра и совершенствования систем безопасности."
+    security_description: "Мы стремимся сохранить вашу личную информацию в безопасности. Как проект с открытым исходным кодом, наш сайт открыт для всех в вопросах пересмотра и совершенствования систем безопасности."
     email_title: "Email"
     email_description_prefix: "Мы не наводним вас спамом. Через"
     email_settings_url: "ваши email настройки"
     email_description_suffix: "или через ссылки в email-ах, которые мы отправляем, вы можете изменить предпочтения и легко отписаться в любой момент."
     cost_title: "Стоимость"
-    cost_description: "В настоящее время, CodeCombat 100% бесплатен! Одной из наших главных целей является сохранить его таким, так, чтобы как можно больше людей могли играть, независимо от места в жизни. Если небо потемнеет, мы, возможно, введём подписки, возможно, только на некоторый контент, но нам не хотелось бы. Если повезёт, мы сможем поддерживать компанию, используя"
+    cost_description: "В настоящее время, CodeCombat 100% бесплатен! Одной из наших главных целей является сохранить его таким, чтобы как можно больше людей могли играть, независимо от места в жизни. Если небо потемнеет, мы, возможно, введём подписки, возможно, только на некоторый контент, но нам не хотелось бы. Если повезёт, мы сможем поддерживать компанию, используя"
     recruitment_title: "Рекрутинг"
     recruitment_description_prefix: "Здесь, в CodeCombat, вы собираетесь стать могущественным волшебником не только в игре, но и в реальной жизни."
     url_hire_programmers: "Никто не может нанять программистов достаточно быстро"
     recruitment_description_suffix: "поэтому, как только вы улучшите свои навыки и будете согласны, мы начнём демонстрировать ваши лучшие программистские достижения тысячам работодателей, пускающих слюни на возможность нанять вас. Они платят нам немного, они платят вам"
     recruitment_description_italic: "много"
-    recruitment_description_ending: ", сайт остаётся бесплатным и все счастливы. Таков план."
+    recruitment_description_ending: "сайт остаётся бесплатным и все счастливы. Таков план."
     copyrights_title: "Авторские права и лицензии"
     contributor_title: "Лицензионное соглашение соавторов"
     contributor_description_prefix: "Все вклады, как на сайте, так и на нашем репозитории GitHub, подпадают под наше"
@@ -394,7 +394,7 @@
     rights_media: "Медиа (звуки, музыка) и любой другой творческий контент, созданный специально для этого уровня и не являющийся общедоступным при создании уровней."
     rights_clarification: "Чтобы уточнить, всё, что становится доступным в Редакторе уровней для целей создания уровней под CC, в то время как контент, созданный с помощью Редактора уровней или загруженный в ходе создания уровней - нет."
     nutshell_title: "В двух словах"
-    nutshell_description: "Любые ресурсы, которые мы предоставляем в Редакторе уровней можно свободно использовать как вам нравится для создания уровней. Но мы оставляем за собой право ограничивать распространение уровней самих по себе (которые создаются на codecombat.com), чтобы за них могла взиматься плата в будущем, если это то, что в конечном итоге происходит."
+    nutshell_description: "Любые ресурсы, которые мы предоставляем в Редакторе уровней можно свободно использовать как вам нравится для создания уровней. Но мы оставляем за собой право ограничивать распространение уровней самих по себе (которые создаются на codecombat.com), чтобы за них могла взиматься плата в будущем, если до этого дойдёт."
     canonical: "Английская версия этого документа является определяющей и канонической. Если есть какие-либо расхождения между переводами, документ на английском имеет приоритет."

   contribute:


Update ru.coffee

Update ru.coffee

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -83,14 +83,14 @@
   signup:
     create_account_title: "Создать аккаунт, чтобы сохранить прогресс"
     description: "Это бесплатно. Нужна лишь пара вещей, и вы сможете продолжить путешествие:"
-    email_announcements: "Получать оповещения на email"
+    email_announcements: "Получать оповещения по email"
     coppa: "Вы старше 13 лет или живёте не в США "
     coppa_why: "(почему?)"
     creating: "Создание аккаунта..."
     sign_up: "Регистрация"
     log_in: "вход с паролем"
     social_signup: "Или вы можете зарегистрироваться через Facebook или G+:"
-    required: "Войдите для того чтобы продолжить."
+    required: "Войдите для того, чтобы продолжить."

   home:
     slogan: "Научитесь программировать, играя в игру"
@@ -123,13 +123,13 @@
     campaign_multiplayer_description: "... в которых вы соревнуетесь в программировании с другими игроками."
     campaign_player_created: "Уровни игроков"
     campaign_player_created_description: "... в которых вы сражаетесь с креативностью ваших друзей Ремесленников."
-#    campaign_classic_algorithms: "Classic Algorithms"
-#    campaign_classic_algorithms_description: "... in which you learn the most popular algorithms in Computer Science."
+    campaign_classic_algorithms: "Классические принципы"
+    campaign_classic_algorithms_description: "... которые чаще всего встречаются в копьютерных науках."
     level_difficulty: "Сложность: "
     play_as: "Играть за "
     spectate: "Наблюдать"
-#    players: "players"
-#    hours_played: "hours played"
+    players: "игроки"
+    hours_played: "часов сыграно"

   contact:
     contact_us: "Связаться с CodeCombat"
@@ -142,7 +142,7 @@
     forum_suffix: "."
     send: "Отправить отзыв"
     contact_candidate: "Связаться с кандидатом"
-    recruitment_reminder: "Используйте эту форму, чтобы обратиться к кандидатам, если вы заинтересованы в интервью. Помните, что CodeCombat взимает 18% от первого года зарплаты. Плата производится по найму сотрудника и подлежит возмещению в течение 90 дней, если работник не остаётся на рабочем месте. Работники с частичной занятостью, удалённые и работающие по контракту свободны, как стажёры."
+    recruitment_reminder: "Используйте эту форму, чтобы обратиться к кандидатам, если вы заинтересованы в интервью. Помните, что CodeCombat взимает 18% от первого года зарплаты. Плата производится по найму сотрудника и подлежит возмещению в течение 90 дней, если работник не остаётся на рабочем месте. Работники с частичной занятостью, работаюие удалённо и по контракту свободны, как стажёры."

   diplomat_suggestion:
     title: "Помогите перевести CodeCombat!"


Small fixes to sk.coffee

Small fixes to sk.coffee

--- app/locale/sk.coffee
+++ app/locale/sk.coffee
@@ -128,8 +128,8 @@
   diplomat_suggestion:
     title: "Pomôžte preložiť CodeCombat!" # This shows up when a player switches to a non-English language using the language selector.
     sub_heading: "Potrebujeme tvoje jazykové zručnosti."
-    pitch_body: "CodeCombat je vyvíjaný v angličitne, ale hrajú ho hráči z celého sveta. Máme aj hráčov, ktorí potrebujú preklad do  {slovenčiny}. Ak vieš teda po anglicky aj po slovensky, prihlás sa ako diplomat a pomôž s prekladom stránky a úrovní do  {slovenčiny}."
-    missing_translations: "Pokiaľ bude všetko preložené do {slovenčiny},budú nepreložené časti do {slovenčiny} dostupné v angličtine."
+    pitch_body: "CodeCombat je vyvíjaný v angličitne, ale hrajú ho hráči z celého sveta. Máme aj hráčov, ktorí potrebujú preklad do slovenčiny. Ak vieš teda po anglicky aj po slovensky, prihlás sa ako diplomat a pomôž s prekladom stránky a úrovní do slovenčiny."
+    missing_translations: "Pokiaľ bude všetko preložené do slovenčiny, budú nepreložené časti do slovenčiny dostupné v angličtine."
     learn_more: "Zisti viac, ako byť Diplomat"
     subscribe_as_diplomat: "Prihlásiť sa ako Diplomat"


Update sk.coffee

Update sk.coffee

Translaltion of tips

--- app/locale/sk.coffee
+++ app/locale/sk.coffee
@@ -308,19 +308,19 @@
     tip_all_species: "Veríme v rovnaké možnosti učenia sa programovať pre všetky druhy."
     tip_reticulating: "Sieťovanie chrbtíc."
     tip_harry: "Hej kúzelík, "
-#    tip_great_responsibility: "With great coding skill comes great debug responsibility."
-#    tip_munchkin: "If you don't eat your vegetables, a munchkin will come after you while you're asleep."
-#    tip_binary: "There are only 10 types of people in the world: those who understand binary, and those who don't."
-#    tip_commitment_yoda: "A programmer must have the deepest commitment, the most serious mind. ~ Yoda"
-#    tip_no_try: "Do. Or do not. There is no try. - Yoda"
-#    tip_patience: "Patience you must have, young Padawan. - Yoda"
-#    tip_documented_bug: "A documented bug is not a bug; it is a feature."
-#    tip_impossible: "It always seems impossible until it's done. - Nelson Mandela"
-#    tip_talk_is_cheap: "Talk is cheap. Show me the code. - Linus Torvalds"
-#    tip_first_language: "The most disastrous thing that you can ever learn is your first programming language. - Alan Kay"
-#    tip_hardware_problem: "Q: How many programmers does it take to change a light bulb?  A: None, it's a hardware problem."
-#    tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law."
-#    tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth"
+    tip_great_responsibility: "S umením kódovať prichádza aj umenie odstraňovať chyby."
+    tip_munchkin: "Ak nebudeš jesť zeleninu, v spánku si po teba príde prežúvavec."
+    tip_binary: "Na svete je 10 druhov ľudí. Tí, ktorí poznajú binárnu sústavu, a tí, čo nie."
+    tip_commitment_yoda: "Programátor musí byť hlboko vážny a zaujatý. ~ Yoda"
+    tip_no_try: "Urob. Alebo neurob. Neskúšaj. - Yoda"
+    tip_patience: "Musíš byť trpezlivý, mladý Padawan. - Yoda"
+    tip_documented_bug: "Zdokumentovaná chyba nie je chyba, je to funkcia."
+    tip_impossible: "Vždy sa to zdá nemožným... až kým to neurobíš. - Nelson Mandela"
+    tip_talk_is_cheap: "Slová, slová. Radšej mi ukáž kód. - Linus Torvalds"
+    tip_first_language: "Najväčšia katastrofa, ktorá ťa môže postihnúť - zvládnutie prvého programovacieho jazyka. - Alan Kay"
+    tip_hardware_problem: "Koľko programátorov je treba na výmenu žiarovky ? Žiadneho. Je to problém hardwaru."
+    tip_hofstadters_law: "Hofstadterov zákon: Vždy to trvá dlhšie ako očakávame a to i vtedy, ak berieme do úvahy tento zákon."
+    tip_premature_optimization: "Predčasná optimalizácia je pôvodcom všetkého zla. - Donald Knuth"
 #    tip_brute_force: "When in doubt, use brute force. - Ken Thompson"
 #    tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..."
 #    tip_superpower: "Coding is the closest thing we have to a superpower."
@@ -458,7 +458,7 @@
 #    using_prepaid: "Using prepaid code for monthly subscription"

   choose_hero:
-    choose_hero: "Vyber svojho hrdinu"
+    choose_hero: "Vyber si svojho hrdinu"
     programming_language: "Programovací jazyk"
     programming_language_description: "Aký programovací jazyk chceš použiť ? "
     default: "Predvolený"


Update tr.coffee (#5003)

Update tr.coffee (#5003)

* Update tr.coffee

* Update tr.coffee

--- app/locale/tr.coffee
+++ app/locale/tr.coffee
@@ -2071,10 +2071,10 @@
 #    humans: "Red" # Ladder page display team name
 #    ogres: "Blue"

-#  user:
+  user:
 #    stats: "Stats"
-#    singleplayer_title: "Singleplayer Levels"
-#    multiplayer_title: "Multiplayer Levels"
+    singleplayer_title: "Tek Oyuncu Seviyesi"
+    multiplayer_title: "Çok Oyunculu Seviyesi"
 #    achievements_title: "Achievements"
 #    last_played: "Last Played"
 #    status: "Status"


sort feeds and items by time

sort feeds and items by time

--- app/js/classes/feeds.coffee
+++ app/js/classes/feeds.coffee
@@ -9,11 +9,16 @@
 			# console.log data
 			feeds = data.map( (f)->
 				o = Ember.Object.create().setProperties(f)
-				o.items = f.items.map( (i)->
+				o.items = Ember.ArrayController.create {
+					sortProperties: ['published_at']
+					sortAscending: false
+				}
+				o.items.setObjects f.items.map( (i)->
 					if i.published_at
 						i.published_at = (new Date( Date.parse(i.published_at) )).fmt('%Y-%m-%e %A')
 					Ember.Object.create().setProperties(i)
 				)
+
 				o
 			) || []
 			# console.log feeds
@@ -29,6 +34,8 @@
 	isEditMode: false
 	noFeed: false
 	pickedItems: []
+	sortProperties: ['created_at']
+	sortAscending: false
 	toggleEditMode: ->
 		@set('isEditMode', !@get('isEditMode'))
 	deleteFeed: (feed)->


fix the bug of deleting feed

fix the bug of deleting feed

--- app/js/classes/feeds.coffee
+++ app/js/classes/feeds.coffee
@@ -75,7 +75,8 @@
 			@$('input').attr('disabled', true)
 			$.post( '/v1/feeds', {url: url} ).done (json)=>
 				json = JSON.parse(json)
-				$.get( 'v1/feeds/' + json.id ).done (json)=>
+				id = json.id
+				$.get( 'v1/feeds/' + id ).done (json)=>
 					@set('isProcessing', false)
 					@$('input').attr('disabled', false)
 					json = JSON.parse(json)
@@ -84,6 +85,7 @@
 						o.set('selected', false)
 					)
 					feed = Ember.Object.create {
+						id: id
 						title: json.title
 						url: url
 						created_at: new Date()


Handle conditional requests in the response cache


More better ability service

More better ability service

--- lineman/app/services/ability_service.coffee
+++ lineman/app/services/ability_service.coffee
@@ -1,24 +1,30 @@
 angular.module('loomioApp').factory 'AbilityService', (CurrentUser) ->
   new class AbilityService
     canStartProposal: (thread) ->
+      thread and
       !thread.hasActiveProposal() and
-      (@canAdminister(thread.group()) or
+      (@canAdministerGroup(thread.group()) or
        thread.group().membersCanRaiseProposals)

     canEditThread: (thread) ->
       CurrentUser.isMemberOf(thread.group()) and
-      (@canAdminister(thread.group()) or
-      CurrentUser.isAuthorOf(discussion) or
+      (@canAdministerGroup(thread.group()) or
+      CurrentUser.isAuthorOf(thread) or
       thread.group().membersCanEditDiscussions)
+
+    canVoteOn: (proposal) ->
+      proposal.isActive() and
+      CurrentUser.isMemberOf(proposal.group()) and
+      (@canAdministerGroup(proposal.group()) or proposal.group().membersCanVote)

     canCloseOrExtendProposal: (proposal) ->
       proposal.isActive() and
-      (@canAdminister(proposal.group()) or CurrentUser.isAuthorOf(proposal))
+      (@canAdministerGroup(proposal.group()) or CurrentUser.isAuthorOf(proposal))

     canEditProposal: (proposal) ->
       proposal.isActive() and
       proposal.canBeEdited() and
-      (@canAdminister(proposal.group()) or CurrentUser.isAuthorOf(proposal))
+      (@canAdministerGroup(proposal.group()) or CurrentUser.isAuthorOf(proposal))

     canCreateOutcomeFor: (proposal) ->
       @canSetOutcomeFor(proposal) and !proposal.hasOutcome()
@@ -28,21 +34,29 @@

     canSetOutcomeFor: (proposal) ->
       proposal.isClosed() and
-      (CurrentUser.isAuthorOf(proposal) or @canAdminister(proposal.group()))
+      (CurrentUser.isAuthorOf(proposal) or @canAdministerGroup(proposal.group()))

-    canAdminister: (group) ->
+    canAdministerGroup: (group) ->
       CurrentUser.isAdminOf(group)

     canAddMembers: (group) ->
-      @canAdminister(group) or
+      @canAdministerGroup(group) or
       (CurrentUser.isMemberOf(group) and group.membersCanAddMembers)

     canCreateSubgroups: (group) ->
-      @canAdminister(group) or
+      @canAdministerGroup(group) or
       (CurrentUser.isMemberOf(group) and group.membersCanCreateSubgroups)

     canEditGroup: (group) ->
-      @canAdminister(group)
+      @canAdministerGroup(group)

     canDeactivateGroup: (group) ->
-      @canAdminister(group)
+      @canAdministerGroup(group)
+
+    canEditComment: (comment) ->
+      CurrentUser.isAuthorOf(comment) and
+      comment.isMostRecent() or comment.group().membersCanEditComments
+
+    canDeleteComment: (comment) ->
+      CurrentUser.isAuthorOf(comment) or
+      @canAdministerGroup(comment.group())


Removed `= null`s

Removed `= null`s

--- app/js/views/balloon.coffee
+++ app/js/views/balloon.coffee
@@ -1,8 +1,5 @@
 class Factlink.Balloon
   constructor: (fact) ->
-    @_mouseOutTimeoutID = null
-    @_loadingTimeoutID = null
-    @_loading = false
     @_fact = fact

     @$el = $(Factlink.templates.indicator)
@@ -45,4 +42,4 @@
     @_loading = false
     @hide => @$el.removeClass "fl-loading"

-  loading: -> @_loading
+  loading: -> !!@_loading


duplicate coordinates in data object (for raw/unprocessed values)

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -9,6 +9,11 @@
     @highlights = []

     @dataLength = Math.min @data.x.length, @data.y.length
+
+    # save raw data points
+    @data_raw = {}
+    @data_raw.x = @data.x.slice(0)
+    @data_raw.y = @data.y.slice(0)

     @processLightcurve()


Cut SoundJS-instance-based methods in continued PlaySound refactor

--- app/lib/surface/Lank.coffee
+++ app/lib/surface/Lank.coffee
@@ -683,15 +683,12 @@
       label = @addLabel 'dialogue', Label.STYLE_DIALOGUE
       label.setText e.blurb or '...'
     sound = e.sound ? AudioPlayer.soundForDialogue e.message, @thangType.get 'soundTriggers'
-    @dialogueSoundInstance?.stop()
-    if @dialogueSoundInstance = @playSound sound, false
-      @dialogueSoundInstance.addEventListener 'complete', -> Backbone.Mediator.publish 'sprite:dialogue-sound-completed', {}
+    @playSound sound, false
     @notifySpeechUpdated e

   onClearDialogue: (e) ->
     return unless @labels.dialogue?.text
     @labels.dialogue?.setText null
-    @dialogueSoundInstance?.stop()
     @notifySpeechUpdated {}

   onSetLetterbox: (e) ->
@@ -872,5 +869,4 @@
     @sprite?.off 'animationend', @playNextAction
     @sprite?.destroy?()
     clearInterval @effectInterval if @effectInterval
-    @dialogueSoundInstance?.removeAllEventListeners()
     super()


landing: teams: team selector view improved

--- client/landing/site.landing/coffee/teams/teamselectorview.coffee
+++ client/landing/site.landing/coffee/teams/teamselectorview.coffee
@@ -1,13 +1,15 @@
-kd              = require 'kd.js'
-utils           = require './../core/utils'
-MainHeaderView  = require './../core/mainheaderview'
-JView           = require './../core/jview'
-LoginInputView  = require './../login/logininputview'
+kd                = require 'kd.js'
+utils             = require './../core/utils'
+MainHeaderView    = require './../core/mainheaderview'
+JView             = require './../core/jview'
+LoginInputView    = require './../login/logininputview'
+TeamsSelectorForm = require './teamselectorform'
+

 track = (action, entry) ->

-  category = 'TeamLogin'
-  label    = 'SignupForm'
+  category = 'TeamSelector'
+  label    = 'TeamSelector'
   entry    = entry

   utils.analytics.track action, { category, label, entry }
@@ -32,69 +34,37 @@
         { title : 'Features',    href : '/Features',                name : 'features' }
       ]

-    @form = new kd.FormView
-      cssClass : 'login-form'
-      partial  : """
-

-

-        """
-      callback : (formData) ->
+    @form = new TeamsSelectorForm
+      callback : @bound 'goToTeam'

-        notify   = -> new kd.NotificationView
-          title: 'We don\'t have this team! Please try again.'
-          type: 'growl'
-        navigate = (slug) ->
-          location.assign "#{location.protocol}//#{slug}.#{location.host}"
+    @previousTeams = new kd.CustomHTMLView

-        { companyName } = formData
+    teams = utils.getPreviousTeams()

-        return notify()  if companyName is 'koding'
+  goToTeam: (formData) ->

-        track 'navigated to team login page', companyName
+    { slug } = formData

-        utils.checkIfGroupExists companyName, (err, group) ->
+    return notify()  if slug is 'koding'

-          unless err
-            return notify()  unless group
-            return navigate group.slug
+    track 'navigated to team login page', slug

-          utils.verifySlug slug = kd.utils.slugify(companyName),
-            success: -> notify()
-            error: ->
-              utils.usernameCheck slug,
-                success: ({kodingUser, forbidden}) ->
-                  return notify()  if kodingUser or forbidden
-                  navigate slug
-                error: -> notify()
+    utils.checkIfGroupExists slug, (err, group) ->

+      if err
+        return new kd.NotificationView
+          title: 'We couldn\'t find your team.'

-    @form.addSubView @form.companyName = new LoginInputView
-      inputOptions    :
-        name          : 'companyName'
-        placeholder   : 'Team name or domain'
-        attributes    : testpath : 'company-name'
-        validate      :
-          rules       :
-            required  : yes
-          messages    :
-            required  : "Please enter a team name or domain."
-    , '.team-name-or-domain'
-
-    @form.addSubView @button = new kd.ButtonView
-      title       : 'Go to your team'
-      icon        : yes
-      style       : 'TeamsModal-button TeamsModal-button--green'
-      attributes  : testpath : 'goto-team-button'
-      type        : 'submit'
-    , '.submit'
+      location.assign "#{location.protocol}//#{slug}.#{location.host}"

   pistachio: ->

     """
     {{> @header }}
-

+

Sign in to your team

+
Enter your team's Koding domain.

       {{> @form}}


remove cruft

remove cruft

--- bokehjs/src/coffee/renderer/guide/categorical_axis.coffee
+++ bokehjs/src/coffee/renderer/guide/categorical_axis.coffee
@@ -24,7 +24,6 @@
         @set_obj('formatter', CategoricalTickFormatter.Collection.create())

     _computed_bounds: () ->
-      i = @get('dimension')
       [range, cross_range] = @get('ranges')

       user_bounds = @get('bounds') ? 'auto'


JUser: update hat usage for guests to avoid collisions

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -259,22 +259,19 @@
                   isRegistration : false
                   username
                 }
-                ((require 'koding-counter') {
-                  db          : JAccount.getClient()
-                  counterName : "koding~#{toBeDeletedUsername}~"
-                  offset      : 0
-                }).reinitialize ->
+
                 user.unlinkOAuths =>
+
                   Payment = require "../payment"
-                  deletedClient = {
-                    connection: {
-                      delegate: account
-                    }
-                  }
+
+                  deletedClient = connection: delegate: account
+
                   Payment.deleteAccount deletedClient, (err)=>
+
                     @logout deletedClient, (err) =>
                       callback err
                       Sendgrid.deleteUser oldEmail, ->
+

   @isRegistrationEnabled = (callback)->

@@ -575,7 +572,11 @@

   @createGuestUsername = (callback) ->
-    callback null, "guest-#{(require 'hat')(64)}"
+
+    hat   = require 'hat'
+    rack  = hat.rack 64
+
+    callback null, "guest-#{rack()}"

   @fetchGuestUser = (callback)->


JUser:convert: emit UserRegistered so we can process rewards

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1041,6 +1041,10 @@
           queue.next()

       ->
+        JUser.emit "UserRegistered", {user, account}
+        queue.next()
+
+      ->
         # Auto confirm accounts for development environment
         # This config should be no for production! ~ GG
         if KONFIG.autoConfirmAccounts


KDlistViewController: loader options defaulted

--- components/list/KDListViewController.coffee
+++ components/list/KDListViewController.coffee
@@ -342,17 +342,7 @@
     unless @lazyLoader
       wrapper = @scrollView or @getView()
       wrapper.addSubView @lazyLoader = new KDCustomHTMLView cssClass : "lazy-loader", partial : "Loading..."
-      @lazyLoader.addSubView @lazyLoader.spinner = new KDLoaderView
-        size          :
-          width       : 16
-        loaderOptions :
-          color       : "#5f5f5f"
-          diameter    : 16
-          density     : 60
-          range       : 0.4
-          speed       : 3
-          FPS         : 24
-
+      @lazyLoader.addSubView @lazyLoader.spinner = new KDLoaderView size : width : 32
       @lazyLoader.spinner.show()
       @emit 'LazyLoadThresholdReached'  if emitWhenReached


Move property to properties section

Move property to properties section


Fix logs zip in support message

Fix logs zip in support message

--- app/src/utils/logger.coffee
+++ app/src/utils/logger.coffee
@@ -83,25 +83,6 @@

   @exportLogsToZip: (callback = undefined) ->
     now = new Date()
-    suggestedName = "ledger_wallet_logs_#{now.getFullYear()}#{_.str.lpad(now.getMonth() + 1, 2, '0')}#{now.getDate()}"
-    csv = new ledger.utils.CsvExporter(suggestedName)
-    @publicLogs (publicLogs) =>
-      @privateLogs (privateLogs) =>
-        csv.setContent _.sortBy((publicLogs || []).concat(privateLogs || []), (log) -> log.date)
-        csv.zip (zip) =>
-          callback?(name: suggestedName, zip: zip)
-
-  @exportLogsWithLink: (callback = undefined) ->
-    now = new Date()
-    suggestedName = "ledger_wallet_logs_#{now.getFullYear()}#{_.str.lpad(now.getMonth() + 1, 2, '0')}#{now.getDate()}"
-    csv = new ledger.utils.CsvExporter(suggestedName)
-    @publicLogs (publicLogs) =>
-      @privateLogs (privateLogs) =>
-        csv.setContent _.sortBy((publicLogs || []).concat(privateLogs || []), (log) -> log.date)
-        callback?(name: suggestedName, url: csv.url())
-
-  @exportLogsWithZipLink: (callback = undefined) ->
-    now = new Date()
     publicSuggestedName = "ledger_wallet_logs_#{now.getFullYear()}#{_.str.lpad(now.getMonth() + 1, 2, '0')}#{now.getDate()}"
     privateSuggestedName = "ledger_wallet_private_logs_#{now.getFullYear()}#{_.str.lpad(now.getMonth() + 1, 2, '0')}#{now.getDate()}"
     publicCsv = new ledger.utils.CsvExporter(publicSuggestedName)
@@ -113,7 +94,20 @@
           privateCsv.setContent(privateLogs)
           privateCsv.addToZip writer, (writer) ->
             publicCsv.endZip (zip) ->
-              callback?(name: publicSuggestedName, url: zip.url())
+              callback?(name: publicSuggestedName, zip: zip)
+
+  @exportLogsWithLink: (callback = undefined) ->
+    now = new Date()
+    suggestedName = "ledger_wallet_logs_#{now.getFullYear()}#{_.str.lpad(now.getMonth() + 1, 2, '0')}#{now.getDate()}"
+    csv = new ledger.utils.CsvExporter(suggestedName)
+    @publicLogs (publicLogs) =>
+      @privateLogs (privateLogs) =>
+        csv.setContent _.sortBy((publicLogs || []).concat(privateLogs || []), (log) -> log.date)
+        callback?(name: suggestedName, url: csv.url())
+
+  @exportLogsWithZipLink: (callback = undefined) ->
+    @exportsLogsToZip ({name, zip}) ->
+      callback?(name: name, url: zip.url())

   @downloadLogsWithZipLink: ->
     @exportLogsWithZipLink (data) ->


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -174,8 +174,8 @@
       #trig functions adjusted to use the angle relative
       #to the "12 o'clock" vector:
       console.log 'ts: a', a
-      d.cx = Math.sin(a) * (that.radius - 75)
-      d.cy = -Math.cos(a) * (that.radius - 75)
+      d.cx = Math.sin(a) * (radius - 75)
+      d.cy = -Math.cos(a) * (radius - 75)

       ### calculate the default position for the label,
          so that the middle of the label is centered in the arc
@@ -184,11 +184,11 @@
       bbox = @getBBox()
       #bbox.width and bbox.height will
       #describe the size of the label text
-      labelRadius = that.radius - 20
+      labelRadius = radius - 20
       d.x = Math.sin(a) * labelRadius
       d.l = d.x - bbox.width / 2 - 2
       d.r = d.x + bbox.width / 2 + 2
-      d.y = -Math.cos(a) * (that.radius - 20)
+      d.y = -Math.cos(a) * (radius - 20)
       d.b = d.oy = d.y + 5
       d.t = d.y - bbox.height - 5
       console.log 'ts: d', d


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -59,7 +59,7 @@
         .attr("id","dropshadow")
     filter.append("feGaussianBlur")
         .attr("in","SourceAlpha")
-        .attr("stdDeviation","1")
+        .attr("stdDeviation","2")
         .attr("result","blur")
     filter.append("feOffset")
         .attr("in","blur")


remove old code

remove old code

--- lib/init.coffee
+++ lib/init.coffee
@@ -70,17 +70,7 @@
 atom.workspace.addLeftPanel(item: el)

-

-

-

-        glob file pattern to analyze (e.g. /Users/john/my-project/src/**/*.js  )
-
<input id='lupa-project-root' type='text'>

-

-

-

-

-<input id='lupa-index-file' type='text'>

-<button id='lupa-load-index-file'>Load index file (not implemented)</button>
"
+
"

 decorations = []
 labelDecorations = []
@@ -222,10 +212,7 @@
     result = require(currentFile)
     alert(result)

-#'/Users/lukasz/sandbox/Mancy/src/**/*'
 document.getElementById('lupa-index-project').addEventListener('click', () ->
-    path = document.getElementById('lupa-project-root').value;
-    # plugin.indexProject({filePattern: path})
     alert "Click ok to start indexing (it can take few minutes)."
     try
         plugin.indexProject(path_.dirname(currentFile))


Fix setting order

Fix setting order

--- lib/init.coffee
+++ lib/init.coffee
@@ -138,7 +138,7 @@
         {value: 'error', description: 'Error. The same behavior as skip but not quite as silent.'}
       ]
       description: "how to treat imports"
-      order: 16
+      order: 22

   activate: ->
     require('atom-package-deps').install('linter-mypy')


MoreWorkspacesModal: Remove extra super call.

Which is a merge fuck-up.

--- client/Social/Activity/sidebar/moreworkspacesmodal.coffee
+++ client/Social/Activity/sidebar/moreworkspacesmodal.coffee
@@ -12,7 +12,7 @@
     super options, data

     @addButton = new KDButtonView
-      title    : "Add Workspace"
+      title    : 'Add Workspace'
       style    : 'add-big-btn'
       loader   : yes
       callback : =>
@@ -21,8 +21,6 @@

     @addSubView @addButton, '.kdmodal-content'

-    super
-

   populate: ->


Fix typo

Fix typo

--- storage/lib/geolog.coffee
+++ storage/lib/geolog.coffee
@@ -4,7 +4,7 @@
 Promise = require 'bluebird'

 module.exports = (db) ->
-    _refresh = refreshView db, 'logRel', 5000, debug
+    _refresh = refreshView db, 'logsRel', 5000, debug

     upsert: (data) ->
         debug "upsert #{data.Code?.toLowerCase()}"


Don't use <p> tags in content.

Don't use <p> tags in content.

--- client/ide/lib/views/tabview/ideapplicationtabview.coffee
+++ client/ide/lib/views/tabview/ideapplicationtabview.coffee
@@ -46,7 +46,7 @@
     { ace } = aceView
     file    = ace.getData()

-    content = "
Your changes will be lost if you don't save them.
"
+    content = "Your changes will be lost if you don't save them."

     { frontApp } = kd.singletons.appManager

@@ -54,7 +54,7 @@
       participants = frontApp.getWatchingMeParticipantsList()

       if participants.length
-        content += "
Also #{participants.join(', ')} maybe made changes here?
"
+        content += " Also #{participants.join(', ')} maybe made changes here?"

     modal = new KDModalView
       width         : 620


Added gravatar helper

Added gravatar helper

--- src/typhoon/view.coffee
+++ src/typhoon/view.coffee
@@ -1,6 +1,7 @@
 haml = require 'hamljs'
 utils = require './utils'
 markdown = require('node-markdown').Markdown
+crypto = require 'crypto'
 require './patches'

 ###
@@ -49,6 +50,10 @@
 class Helpers
   @markdown: (str) -> markdown str
   @summary: (body) -> body.split('<!-- more -->')[0].replace /\.$/, '…'
+  @gravatar: (email, size = 50) ->
+    'http://www.gravatar.com/avatar/' +
+    crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex') +
+    "?r=pg&s=#{size}.jpg&d=identicon"

 ###
 Module Exports


Disable supermodel tests due to incompatibility with latest Backbone.js

--- test/spec/ecosystem/supermodel.tests.coffee
+++ test/spec/ecosystem/supermodel.tests.coffee
@@ -2,6 +2,7 @@
 assert = assert or require?('chai').assert

 describe 'Knockback.js with Backbone Supermodel @supermodel', ->
+  return console.log 'DISABLING TESTS FOR SUPERMODEL - DOES NOT SUPPORT LATEST BACKBONE.JS'

   # import Underscore (or Lo-Dash with precedence), Backbone, Knockout, and Knockback
   kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback')


Add an image to haters

Add an image to haters

--- src/scripts/haters.coffee
+++ src/scripts/haters.coffee
@@ -25,6 +25,7 @@
 , "http://media.tumblr.com/tumblr_m2yv2hqw9l1rnvwt1.gif"
 , "http://loldailyfun.com/wp-content/uploads/2012/02/Haters-Gonna-hate-Funny.jpg"
 , "http://brian.io/slides/incog-intro/img/rollin.jpg"
+, "http://static.fjcdn.com/pictures/Haters+Gonna+Hate_0dd058_3113024.jpg"
 ]

 hatin = (msg) ->


Update specs for core API changes

Update specs for core API changes

--- spec/line-jumper-spec.coffee
+++ spec/line-jumper-spec.coffee
@@ -5,10 +5,12 @@
   editor = null
   beforeEach ->
     atom.workspaceView = new WorkspaceView()
-    pack = atom.packages.activatePackage("line-jumper", immediate: true)
+
+    waitsForPromise ->
+      atom.packages.activatePackage("line-jumper")

     atom.workspaceView.openSync('sample.js')
-    editor = atom.workspaceView.getActiveView()
+    editor = atom.workspaceView.getActiveView().getEditor()

   describe "moving and selecting down", ->
     it "moves down", ->


inlined function

inlined function

--- core/app/backbone/views/autocompleted_add_to_channel_view.coffee
+++ core/app/backbone/views/autocompleted_add_to_channel_view.coffee
@@ -63,9 +63,6 @@

   focusInput: -> @$("input.typeahead").focus()

-
-  setActiveAutoComplete: (key, scroll) ->
-    @_auto_completes_view.setActiveAutoComplete key, scroll

   selectDefaultItem: -> console.info 'we could do something here'

@@ -150,7 +147,7 @@

     @_auto_completes_view.search_collection.setSearch searchValue
     @_auto_completes_view.search_collection.fetch success: =>
-      @setActiveAutoComplete 0, true
+      @_auto_completes_view.setActiveAutoComplete 0
       updateWindowHeight()

   #cleaning/closing functions:


coffeescript range comparison

coffeescript range comparison

--- core/app/backbone/views/autocompleted_add_to_channel_view.coffee
+++ core/app/backbone/views/autocompleted_add_to_channel_view.coffee
@@ -92,7 +92,7 @@
     e.preventDefault()
     e.stopPropagation()
     @disable()
-    if @activeChannelKey() >= 0 and @activeChannelKey() < @_auto_completes_view.list.length
+    if 0 <= @activeChannelKey() < @_auto_completes_view.list.length
       selected = @_auto_completes_view.list[@activeChannelKey()].model
       @$("input.typeahead").val selected.get("title")
       if selected.get("user_channel")


Added follow topic button

Added follow topic button

--- core/app/backbone/views/topics/topic_popover_content_view.coffee
+++ core/app/backbone/views/topics/topic_popover_content_view.coffee
@@ -1,5 +1,12 @@
-class window.TopicPopoverContentView extends Backbone.Marionette.ItemView
+class window.TopicPopoverContentView extends Backbone.Marionette.Layout
   template:
     text: """
       {{title}}
+

     """
+
+  regions:
+    favouriteButtonRegion: '.js-favourite-button-region'
+
+  onRender: ->
+    @favouriteButtonRegion.show new FavouriteTopicButtonView(topic: @model, mini: true)


:white_check_mark: restore snapshot taken at destroyed markerLayer

--- spec/text-buffer-spec.coffee
+++ spec/text-buffer-spec.coffee
@@ -623,6 +623,50 @@
       expect(getFirstMarker(markerLayers[2])).not.toBe(marker2)
       expect(marker1.isDestroyed()).toBe(true)
       expect(marker2.isDestroyed()).toBe(true)
+
+    it "can restore snapshot taken at destroyed markerLayer to given activeMarkerLayer", ->
+      buffer.transact {activeMarkerLayer: markerLayers[1]}, ->
+        buffer.append("44444444\n")
+        marker0.setRange(rangesAfter[0])
+        marker1.setRange(rangesAfter[1])
+        marker2.setRange(rangesAfter[2])
+
+      markerLayers[1].destroy()
+      expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeTruthy()
+      expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy()
+      expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeTruthy()
+      expect(marker0.isDestroyed()).toBe(false)
+      expect(marker1.isDestroyed()).toBe(true)
+      expect(marker2.isDestroyed()).toBe(false)
+
+      buffer.undo({activeMarkerLayer: markerLayers[0]})
+      expect(buffer.getText()).toBe(textUndo)
+
+      ensureMarkerLayer(markerLayers[0], rangesBefore[1])
+      ensureMarkerLayer(markerLayers[2], rangesAfter[2])
+      expect(marker0.isDestroyed()).toBe(true)
+      expect(marker2.isDestroyed()).toBe(false)
+
+      buffer.redo({activeMarkerLayer: markerLayers[0]})
+      expect(buffer.getText()).toBe(textRedo)
+      ensureMarkerLayer(markerLayers[0], rangesAfter[1])
+      ensureMarkerLayer(markerLayers[2], rangesAfter[2])
+
+      markerLayers[3] = markerLayers[2].copy()
+      ensureMarkerLayer(markerLayers[3], rangesAfter[2])
+      markerLayers[0].destroy()
+      markerLayers[2].destroy()
+      expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeFalsy()
+      expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy()
+      expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeFalsy()
+      expect(buffer.getMarkerLayer(markerLayers[3].id)).toBeTruthy()
+
+      buffer.undo({activeMarkerLayer: markerLayers[3]})
+      expect(buffer.getText()).toBe(textUndo)
+      ensureMarkerLayer(markerLayers[3], rangesBefore[1])
+      buffer.redo({activeMarkerLayer: markerLayers[3]})
+      expect(buffer.getText()).toBe(textRedo)
+      ensureMarkerLayer(markerLayers[3], rangesAfter[1])

     it "fallback to normal behavior when snaphot includes multipe layerSnapshot of same role", ->
       # Transact without activeMarkerLayer.


Fix DST parsing in Rails encoder.

Fix DST parsing in Rails encoder.

--- tests/batman/extras/rails_extra_test.coffee
+++ tests/batman/extras/rails_extra_test.coffee
@@ -7,13 +7,21 @@
   equal decodeURIComponent(jQuery.param(data)), "foo[0][a]=b&foo[1][c]=10"
   equal decodeURIComponent(Batman.RailsStorage::_serializeToFormData(data)), "foo[][a]=b&foo[][c]=10"

+oldOffset = Batman.Encoders.railsDate.defaultTimezoneOffset
 QUnit.module "Batman.Rails: date encoding"
+  teardown: ->
+    Batman.Encoders.railsDate.defaultTimezoneOffset = oldOffset

 dateEqual = (a, b, args...) ->
   equal a.getTime(), b.getTime(), args...

 test "it should parse ISO 8601 dates", ->
-  dateEqual Batman.Encoders.railsDate.decode("2012-01-03T13:35:06"), new Date("Tues, 03 Jan 2012 13:35:06")
+  # Date not during DST
+  Batman.Encoders.railsDate.defaultTimezoneOffset = 300
+  dateEqual Batman.Encoders.railsDate.decode("2012-01-03T13:35:06"), new Date("Tues, 03 Jan 2012 13:35:06 EST")
+  # Date during DST
+  Batman.Encoders.railsDate.defaultTimezoneOffset = 240
+  dateEqual Batman.Encoders.railsDate.decode("2012-04-13T13:35:06"), new Date("Sun, 13 Apr 2012 13:35:06 EDT")

 test "it should parse ISO 8601 dates with timezones", ->
   dateEqual Batman.Encoders.railsDate.decode("2012-01-03T13:35:06-05:00"), new Date(1325615706000)


fix: duplicate emojis

fix: duplicate emojis

--- src/simditor-emoji.coffee
+++ src/simditor-emoji.coffee
@@ -9,9 +9,9 @@
     'heart_eyes'
     'smirk'
     'flushed'
-    'satisfied'
     'grin'
     'wink'
+    'kissing_closed_eyes'
     'stuck_out_tongue_winking_eye'
     'stuck_out_tongue'
     'sleeping'


tests/config: Fix PubNub key tests now after we stop using it

Resolves: #604
Change-type: patch
Signed-off-by: Thodoris Greasidis <5791bdc4541e79bd43d586e3c3eff1e39c16665f@balena.io>

--- tests/integration/models/config.spec.coffee
+++ tests/integration/models/config.spec.coffee
@@ -14,10 +14,8 @@

 		it 'should include the pubnub keys', ->
 			balena.models.config.getAll().get('pubnub').then (pubnubKeys) ->
-				m.chai.expect(_.isString(pubnubKeys.publish_key)).to.be.true
-				m.chai.expect(_.isString(pubnubKeys.subscribe_key)).to.be.true
-				m.chai.expect(pubnubKeys.publish_key).to.have.length(42)
-				m.chai.expect(pubnubKeys.subscribe_key).to.have.length(42)
+				m.chai.expect(pubnubKeys.publish_key).to.be.a('string').that.has.length(0)
+				m.chai.expect(pubnubKeys.subscribe_key).to.be.a('string').that.has.length(0)

 		it 'should include the mixpanel token', ->
 			balena.models.config.getAll().get('mixpanelToken').then (mixpanelToken) ->


fix indentation

fix indentation

--- src/opensong.coffee
+++ src/opensong.coffee
@@ -75,142 +75,142 @@
 opensong.helper = opensong.helper || {}

 opensong.helper.transposeChord = (chord, amount) ->
-    chords = [
-      "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
-      "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"
+  chords = [
+    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
+    "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"
+  ]
+
+  outputChords = []
+  for c in chord.split "/"
+    m = /^([A-G][#b]?)(.*)$/.exec c
+    return chord unless m
+
+    [_, chordRoot, chordExt] = m
+    index = chords.indexOf chordRoot
+    if index < 0 # use chord if not found
+      outputChords.push c
+      continue
+
+    # make negative amounts work, always transpose to sharps
+    newIndex = (index + amount + chords.length) % (chords.length / 2)
+    outputChords.push chords[newIndex] + chordExt
+
+  outputChords.join "/"
+
+
+opensong.helper.humanizeHeader = (abbr) ->
+  abbArr = /([a-zA-Z]+)(\d*)/.exec(abbr)[1..]
+  char = abbArr[0]
+
+  abbArr[0] = switch char
+    when "C"
+      "Chorus"
+    when "V"
+      "Verse"
+    when "B"
+      "Bridge"
+    when "T"
+      "Tag"
+    when "P"
+      "Pre-Chorus"
+    else
+      char
+
+  abbArr.pop() unless abbArr[1]? # remove num if empty
+  abbArr.join " "
+
+###
+
+json = [
+  {
+    header: "V",
+    lines: [
+      {
+        chords: ["A", "C"],
+        lyrics: [
+          ["Yeah", "Yeah, God is grea!"]
+        ]
+      },
+      {
+        comments: "This is a comment"
+      }
     ]
-
-    outputChords = []
-    for c in chord.split "/"
-      m = /^([A-G][#b]?)(.*)$/.exec c
-      return chord unless m
-
-      [_, chordRoot, chordExt] = m
-      index = chords.indexOf chordRoot
-      if index < 0 # use chord if not found
-        outputChords.push c
-        continue
-
-      # make negative amounts work, always transpose to sharps
-      newIndex = (index + amount + chords.length) % (chords.length / 2)
-      outputChords.push chords[newIndex] + chordExt
-
-    outputChords.join "/"
-
-
-opensong.helper.humanizeHeader = (abbr) ->
-    abbArr = /([a-zA-Z]+)(\d*)/.exec(abbr)[1..]
-    char = abbArr[0]
-
-    abbArr[0] = switch char
-      when "C"
-        "Chorus"
-      when "V"
-        "Verse"
-      when "B"
-        "Bridge"
-      when "T"
-        "Tag"
-      when "P"
-        "Pre-Chorus"
+  }
+]
+
+###
+opensong.helper.parseLyrics = (lyrics) ->
+  lyricsLines = lyrics.split("\n")
+
+  dataModel = []
+
+  while lyricsLines.length > 0
+    line = lyricsLines.shift()
+
+    continue unless line?
+
+    switch line[0]
+      when "["
+        header = line.match(/\[(.*)\]/)[1]
+
+        dataObject =
+          header: header
+          lines: []
+        dataModel.push dataObject
+      when "."
+        chordsLine = line.substr(1)
+
+        # split cords
+        chordArr = []
+        while chordsLine.length > 0
+          m = /^(\S*\s*)(.*)$/.exec(chordsLine)
+          chordArr.push m[1]
+          chordsLine = m[2]
+        # add an item if it is an empty line
+        chordArr.push chordsLine if chordArr.length is 0
+
+        # clean Chord line from trailing white spaces
+        chordArrCleaned = []
+        $.each chordArr, (index, value) ->
+          m = /(\S*\s?)\s*/.exec(value)
+          chordArrCleaned.push m[1]
+
+        textLine = ""
+        m = null
+        cleanRegExp = /_|\||---|-!!/g
+
+        textLineArr = []
+
+        # while we have lines that match a textLine create an html table row
+        while (textLine = lyricsLines.shift()) and (m = textLine.match(/^([ 1-9])(.*)/))
+          textArr = []
+          textLineNr = m[1]
+          textLine = m[2]
+          # split lyrics line based on chord length
+          for i of chordArr
+            if i < chordArr.length - 1
+              chordLength = chordArr[i].length
+              # split String with RegExp (is there a better way?)
+              m = textLine.match(new RegExp("(.{0," + chordLength + "})(.*)"))
+              textArr.push m[1].replace(cleanRegExp, "")
+              textLine = m[2]
+            else
+              # add the whole string if at the end of the chord arr
+              textArr.push textLine.replace(cleanRegExp, "")
+
+          textLineArr.push textArr
+
+        dataObject.lines.push
+          chords: chordArrCleaned
+          lyrics: textLineArr
+
+        # attach the line again in front (we cut it off in the while loop)
+        lyricsLines.unshift textLine if textLine isnt 'undefined'
+      when " "
+        dataObject.lines.push {lyrics: line.substr(1)}
+      when ";"
+        dataObject.lines.push {comments: line.substr(1)}
       else
-        char
-
-    abbArr.pop() unless abbArr[1]? # remove num if empty
-    abbArr.join " "
-
-  ###
-
-  json = [
-    {
-      header: "V",
-      lines: [
-        {
-          chords: ["A", "C"],
-          lyrics: [
-            ["Yeah", "Yeah, God is grea!"]
-          ]
-        },
-        {
-          comments: "This is a comment"
-        }
-      ]
-    }
-  ]
-
-  ###
-opensong.helper.parseLyrics = (lyrics) ->
-    lyricsLines = lyrics.split("\n")
-
-    dataModel = []
-
-    while lyricsLines.length > 0
-      line = lyricsLines.shift()
-
-      continue unless line?
-
-      switch line[0]
-        when "["
-          header = line.match(/\[(.*)\]/)[1]
-
-          dataObject =
-            header: header
-            lines: []
-          dataModel.push dataObject
-        when "."
-          chordsLine = line.substr(1)
-
-          # split cords
-          chordArr = []
-          while chordsLine.length > 0
-            m = /^(\S*\s*)(.*)$/.exec(chordsLine)
-            chordArr.push m[1]
-            chordsLine = m[2]
-          # add an item if it is an empty line
-          chordArr.push chordsLine if chordArr.length is 0
-
-          # clean Chord line from trailing white spaces
-          chordArrCleaned = []
-          $.each chordArr, (index, value) ->
-            m = /(\S*\s?)\s*/.exec(value)
-            chordArrCleaned.push m[1]
-
-          textLine = ""
-          m = null
-          cleanRegExp = /_|\||---|-!!/g
-
-          textLineArr = []
-
-          # while we have lines that match a textLine create an html table row
-          while (textLine = lyricsLines.shift()) and (m = textLine.match(/^([ 1-9])(.*)/))
-            textArr = []
-            textLineNr = m[1]
-            textLine = m[2]
-            # split lyrics line based on chord length
-            for i of chordArr
-              if i < chordArr.length - 1
-                chordLength = chordArr[i].length
-                # split String with RegExp (is there a better way?)
-                m = textLine.match(new RegExp("(.{0," + chordLength + "})(.*)"))
-                textArr.push m[1].replace(cleanRegExp, "")
-                textLine = m[2]
-              else
-                # add the whole string if at the end of the chord arr
-                textArr.push textLine.replace(cleanRegExp, "")
-
-            textLineArr.push textArr
-
-          dataObject.lines.push
-            chords: chordArrCleaned
-            lyrics: textLineArr
-
-          # attach the line again in front (we cut it off in the while loop)
-          lyricsLines.unshift textLine if textLine isnt 'undefined'
-        when " "
-          dataObject.lines.push {lyrics: line.substr(1)}
-        when ";"
-          dataObject.lines.push {comments: line.substr(1)}
-        else
-          console?.log "no support for: #{line}"
-    dataModel
-
+        console?.log "no support for: #{line}"
+  dataModel
+


revert check for python traceback in compute server response

--- src/smc-hub/client.coffee
+++ src/smc-hub/client.coffee
@@ -1431,10 +1431,7 @@
                         listing = x; cb(err)
         ], (err) =>
             if err
-                if misc.startswith(err, 'Traceback')
-                    @push_to_client(message.public_directory_listing(id:mesg.id, result:[]))
-                else
-                    @error_to_client(id:mesg.id, error:err)
+                @error_to_client(id:mesg.id, error:err)
             else
                 @push_to_client(message.public_directory_listing(id:mesg.id, result:listing))
         )


Fix broken content duplicate

Fix broken content duplicate

--- BackofficeBundle/Resources/public/coffee/table/TableviewAction.coffee
+++ BackofficeBundle/Resources/public/coffee/table/TableviewAction.coffee
@@ -38,8 +38,6 @@
   clickEdit: (event) ->
     event.preventDefault()
     parent = $('#nav-'+@options.entityType).parent('[data-type]')
-    if (parent.length)
-      @options.entityType = parent[0].getAttribute('data-type')
     parameter =
       'entityType': @options.entityType
       'entityId': @options.element.get('id')


Make sure we don't step outside of the file

--- lib/main.coffee
+++ lib/main.coffee
@@ -7,6 +7,8 @@
       while true
         offset = 0
         tokenizedLine = textEditor.tokenizedLineForScreenRow(lineNumber)
+        if tokenizedLine is undefined
+          break
         foundDecorator = false
         for token in tokenizedLine.tokens
           if 'meta.function.python' in token.scopes
@@ -41,6 +43,8 @@
       while true
         offset = 0
         tokenizedLine = textEditor.tokenizedLineForScreenRow(lineNumber)
+        if tokenizedLine is undefined
+          break
         for token in tokenizedLine.tokens
           if token.value is symbol
             return [[lineNumber, offset], [lineNumber, offset + token.bufferDelta]]


remove javascript and objc formatting by default

--- lib/main.coffee
+++ lib/main.coffee
@@ -10,10 +10,10 @@
       default: true
     formatObjectiveCOnSave:
       type: 'boolean'
-      default: true
+      default: false
     formatJavascriptOnSave:
       type: 'boolean'
-      default: true
+      default: false
     executable:
       type: 'string'
       default: 'clang-format'


better variable name

better variable name

--- lib/main.coffee
+++ lib/main.coffee
@@ -13,7 +13,7 @@
     @mruListViews = []

     keyBindSource = 'tabs package'
-    configKey = 'tabs.enableMruTabSwitching'
+    enableMruConfigKey = 'tabs.enableMruTabSwitching'

     @updateTraversalKeybinds = ->
       # We don't modify keybindings based on our setting if the user has already tweaked them.
@@ -26,7 +26,7 @@
         keystrokes: 'ctrl-shift-tab')
       return if bindings.length > 1 and bindings[0].source isnt keyBindSource

-      if atom.config.get(configKey)
+      if atom.config.get(enableMruConfigKey)
         atom.keymaps.removeBindingsFromSource(keyBindSource)
       else
         disabledBindings =
@@ -37,7 +37,7 @@
             'ctrl-shift-tab ^ctrl': 'unset!'
         atom.keymaps.add(keyBindSource, disabledBindings, 0)

-    @subscriptions.add atom.config.observe configKey, => @updateTraversalKeybinds()
+    @subscriptions.add atom.config.observe enableMruConfigKey, => @updateTraversalKeybinds()
     @subscriptions.add atom.keymaps.onDidLoadUserKeymap? => @updateTraversalKeybinds()

     # If the command bubbles up without being handled by a particular pane,


cleanup unnecessary check

cleanup unnecessary check

--- lib/main.coffee
+++ lib/main.coffee
@@ -80,18 +80,19 @@
   # callback() need to return Range to be replaced.
   setTextForCursors: (text, callback) ->
     editor = @getEditor()
-    pasted = null

     @lock()
     editor.transact =>
-      for cursor in @getEditor().getCursors()
-        break unless range = callback(cursor)
-        pasted = true
+      for cursor in editor.getCursors()
+        range = callback(cursor)
+        unless range
+          trhow "hoge"
         @setText cursor, range, text
-      if settings.get('flashOnPaste')
-        @getFlasher().flash settings.get('flashDurationMilliSeconds')
     @unLock()
-    @lastPastedText = text if pasted
+
+    @lastPastedText = text
+    if settings.get('flashOnPaste')
+      @getFlasher().flash settings.get('flashDurationMilliSeconds')

   registerCleanUp: ->
     @pasteSubscription = @getEditor().onDidChangeCursorPosition (event) =>


Add addListItems setting to config (#45); defaults to true

--- lib/main.coffee
+++ lib/main.coffee
@@ -4,6 +4,14 @@
 fs = require 'fs'

 module.exports =
+
+  config:
+    addListItems:
+      title: 'Add new list-items'
+      description: 'Automatically add a new list-item after the current (non-empty) one when pressing `ENTER`'
+      type: 'boolean'
+      default: true
+
   subscriptions: null

   activate: (state) ->
@@ -23,68 +31,70 @@
     # Create a new list-item after pressing [enter]
     @subscriptions.add atom.workspace.observeTextEditors (editor) ->
       editor.onDidInsertText (event) ->
-        grammar = editor.getGrammar()
-        if grammar.name is 'Markdown'
-          if event.text is "\n"
+        if atom.config.get('language-markdown.addListItems')
+          grammar = editor.getGrammar()
+          if grammar.name is 'Markdown'
+            if event.text is "\n"

-            previousRowNumber = event.range.start.row
-            previousRowRange = editor.buffer.rangeForRow(previousRowNumber)
-            previousLine = editor.getTextInRange(previousRowRange)
-            # FIXME
-            # Because the line is tokenized outside of its context, a line that
-            # looks like it contains a list-item, but is part of a fenced-code-
-            # block, will still be considered a valid list-item. A solution
-            # would be to get the {tokens} from the {previousLine} via the
-            # {editor}, as it was implemented in an earlier, rather verbose
-            # version.
-            {tokens} = grammar.tokenizeLine(previousLine)
+              previousRowNumber = event.range.start.row
+              previousRowRange = editor.buffer.rangeForRow(previousRowNumber)
+              previousLine = editor.getTextInRange(previousRowRange)

-            tokens.reverse()
-            for token in tokens
-              isPunctuation = false
-              isListItem = false
+              # FIXME
+              # Because the line is tokenized outside of its context, a line that
+              # looks like it contains a list-item, but is part of a fenced-code-
+              # block, will still be considered a valid list-item. A solution
+              # would be to get the {tokens} from the {previousLine} via the
+              # {editor}, as it was implemented in an earlier, rather verbose
+              # version.
+              {tokens} = grammar.tokenizeLine(previousLine)

-              scopes = token.scopes.reverse()
-              for scope in scopes
-                classes = scope.split('.')
+              tokens.reverse()
+              for token in tokens
+                isPunctuation = false
+                isListItem = false

-                # a list-item is valid when a punctuation class is immediately
-                # followed by a non-empty list-item class
-                if classes.indexOf('punctuation') isnt -1
-                  isPunctuation = true
-                else if isPunctuation and classes.indexOf('list') isnt -1
-                  if classes.indexOf('empty') is -1
-                    isListItem = true
-                    typeOfList = 'unordered'
-                    if classes.indexOf('ordered') isnt -1 then typeOfList = 'ordered'
-                    # Skip definition-lists
-                    if classes.indexOf('definition') isnt -1 then isListItem = false
-                    break
+                scopes = token.scopes.reverse()
+                for scope in scopes
+                  classes = scope.split('.')
+
+                  # a list-item is valid when a punctuation class is immediately
+                  # followed by a non-empty list-item class
+                  if classes.indexOf('punctuation') isnt -1
+                    isPunctuation = true
+                  else if isPunctuation and classes.indexOf('list') isnt -1
+                    if classes.indexOf('empty') is -1
+                      isListItem = true
+                      typeOfList = 'unordered'
+                      if classes.indexOf('ordered') isnt -1 then typeOfList = 'ordered'
+                      # Skip definition-lists
+                      if classes.indexOf('definition') isnt -1 then isListItem = false
+                      break
+                    else
+                      isListItem = false
+                      isPunctuation = false
                   else
-                    isListItem = false
                     isPunctuation = false
-                else
-                  isPunctuation = false

-              if isListItem
-                text = token.value
+                if isListItem
+                  text = token.value

-                # increment ordered list-item
-                if typeOfList is 'ordered'
-                  length = text.length
-                  punctuation = text.match(/[^\d]+/)
-                  value = parseInt(text) + 1
-                  text = value + punctuation
-                  # add left padding to ordered list-items (003.)
-                  if text.length < length
-                    for i in [0 .. (text.length - length + 1)]
-                      text = '0' + text
-                else
-                  # Convert task-list-items into incompleted ones
-                  text = text.replace('x', ' ')
+                  # increment ordered list-item
+                  if typeOfList is 'ordered'
+                    length = text.length
+                    punctuation = text.match(/[^\d]+/)
+                    value = parseInt(text) + 1
+                    text = value + punctuation
+                    # add left padding to ordered list-items (003.)
+                    if text.length < length
+                      for i in [0 .. (text.length - length + 1)]
+                        text = '0' + text
+                  else
+                    # Convert task-list-items into incompleted ones
+                    text = text.replace('x', ' ')

-                editor.insertText(text + '')
-                break
+                  editor.insertText(text + '')
+                  break

     # Only when in dev-mode,
     # create the {language-markdown:compile-grammar} command,


empty string fix

empty string fix

--- src/string-to-json.coffee
+++ src/string-to-json.coffee
@@ -7,7 +7,11 @@

   generate: ->
     counter = 0
-    for line in "#{@text}".split "\n"
+    splitted = "#{@text}".split "\n"
+
+    if splitted.length == 1 and splitted[0] == '' then return {}
+
+    for line in splitted
       @json["#{counter}_#{crypto.createHash('md5').update(line).digest('hex')}"] = line
       counter += 1
     @json


coordinator: Pass port schema in component message

If specified in discovery message

--- src/protocol.coffee
+++ src/protocol.coffee
@@ -11,6 +11,7 @@
     id: port.id
     type: port.type or "any"
     description: port.description or ""
+    schema: port.schema # optional
     addressable: false
     required: false # TODO: implement
   return m


Fix uploader type for agent

Fix uploader type for agent

--- src/provider.coffee
+++ src/provider.coffee
@@ -99,6 +99,7 @@
             searchScope: formObject.searchScope ? component.searchScope
             searchPeriod: formObject.searchPeriod ? component.searchPeriod
             transformations: formObject.transformations ? component.transformations
+            uploaderType: formObject.uploaderType ? component.uploaderType
         result

     @reindexFormObject = (name) =>


Update button state quickly.

Update button state quickly.

--- app/assets/javascripts/audio.js.coffee
+++ app/assets/javascripts/audio.js.coffee
@@ -60,9 +60,14 @@
         form.submit()

   togglePlay: ->
+    play = $("[data-behavior~=audio_play_#{feedbin.nowPlayingData.entryId}]")
     if window.player.paused
+      play.addClass('playing')
+      play.removeClass('paused')
       window.player.play()
     else
+      play.addClass('paused')
+      play.removeClass('playing')
       window.player.pause()

   setDuration: (entryId, duration) ->


Make the refresh timer a sane value

Make the refresh timer a sane value

--- app/assets/javascripts/meals.js.coffee
+++ app/assets/javascripts/meals.js.coffee
@@ -45,6 +45,6 @@
     )
   )

-  window.setInterval(meal.update_orders, 3000)
+  window.setInterval(meal.update_orders, 30000)
 )


Make spec assertions insensitive to scrollbars on Windows

--- spec/editor-component-spec.coffee
+++ spec/editor-component-spec.coffee
@@ -1697,32 +1697,38 @@
       component.measureHeightAndWidth()
       runSetImmediateCallbacks()

+      # try to scroll past the top, which is impossible
       componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50))
       expect(editor.getScrollTop()).toBe 0
       expect(WheelEvent::preventDefault).not.toHaveBeenCalled()

+      # scroll to the bottom in one huge event
       componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000))
       runSetImmediateCallbacks()
-      expect(editor.getScrollTop()).toBe editor.getScrollHeight() - editor.getHeight() + 15
+      maxScrollTop = editor.getScrollTop()
       expect(WheelEvent::preventDefault).toHaveBeenCalled()
       WheelEvent::preventDefault.reset()

+      # try to scroll past the bottom, which is impossible
       componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30))
-      expect(editor.getScrollTop()).toBe editor.getScrollHeight() - editor.getHeight() + 15
+      expect(editor.getScrollTop()).toBe maxScrollTop
       expect(WheelEvent::preventDefault).not.toHaveBeenCalled()

+      # try to scroll past the left side, which is impossible
       componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0))
       expect(editor.getScrollLeft()).toBe 0
       expect(WheelEvent::preventDefault).not.toHaveBeenCalled()

+      # scroll all the way right
       componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0))
       runSetImmediateCallbacks()
-      expect(editor.getScrollLeft()).toBe editor.getScrollWidth() - editor.getWidth() + 15
+      maxScrollLeft = editor.getScrollLeft()
       expect(WheelEvent::preventDefault).toHaveBeenCalled()
       WheelEvent::preventDefault.reset()

+      # try to scroll past the right side, which is impossible
       componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0))
-      expect(editor.getScrollLeft()).toBe editor.getScrollWidth() - editor.getWidth() + 15
+      expect(editor.getScrollLeft()).toBe maxScrollLeft
       expect(WheelEvent::preventDefault).not.toHaveBeenCalled()

   describe "input events", ->


Scroll textarea to bottom when adding content

--- app/assets/javascripts/sugar.js.coffee
+++ app/assets/javascripts/sugar.js.coffee
@@ -56,4 +56,5 @@
         else
           $(this).val(content + newContent)

+      this.scrollTop = this.scrollHeight
       $(this).focus()