Polish translation corrections. (#4914)

Polish translation corrections. (#4914)

--- app/locale/pl.coffee
+++ app/locale/pl.coffee
@@ -740,7 +740,7 @@
     thank_you_months_prefix: ""
     thank_you_months_suffix: " - przez tyle miesięcy nas wspierałeś. Dziękujemy!"
     thank_you: "Dziękujemy za wsparcie CodeCombat."
-    sorry_to_see_you_go: "Szkoda, że juz się nie zobaczymy! Powiedz nam co moglibyśmy poprawić"
+    sorry_to_see_you_go: "Szkoda, że już się nie zobaczymy! Powiedz nam co moglibyśmy poprawić"
     unsubscribe_feedback_placeholder: "Och! Cóżeśmy ci takiego uczynili, że nas opuszczasz?"
     stripe_description: "Miesięczna subskrypcja"
 #    buy_now: "Buy Now"
@@ -984,9 +984,9 @@
     jobs_benefit_10: "Urlop macierzyński: 10 tygodni płatnych, następnych 6 w 55%"
     jobs_benefit_11: "Urlop tacierzyński: 10 tygodni płatnych"
     jobs_custom_title: "Stwórz własne stanowisko"
-    jobs_custom_description: "Zainteresował Cię CodeCombat, ale nie możesz znaleźć stanowiska odpowiadającemu Twoim kwalifikacją? Napisz do nas i pokaż co możesz wnieść do naszego zespołu. Chętnie Cię wysłuchamy!"
+    jobs_custom_description: "Zainteresował Cię CodeCombat, ale nie możesz znaleźć stanowiska odpowiadającego Twoim kwalifikacjom? Napisz do nas i pokaż co możesz wnieść do naszego zespołu. Chętnie Cię wysłuchamy!"
     jobs_custom_contact_1: "Napisz do nas"
-    jobs_custom_contact_2: "o sobie możliwe że się z Tobą skontaktujemy!"
+    jobs_custom_contact_2: "o sobie, możliwe, że się z Tobą skontaktujemy!"
     contact_title: "Prasa - Kontakt"
     contact_subtitle: "Potrzebujesz więcej informacji? Napisz do nas na"
     screenshots_title: "Zrzuty ekranu z gry"
@@ -1006,12 +1006,12 @@
   special_offer:
     special_offer: "Specjalna oferta"
     project_based_title: "Kursy bazujące na projektach"
-    project_based_description: "Kursy tworzenie gier i stron internetowych dają możliwość podzielenia się gotowym projektem."
-    great_for_clubs_title: "Idelane dla kół i klubów szkolnych"
+    project_based_description: "Kursy tworzenia gier i stron internetowych dają możliwość podzielenia się gotowym projektem."
+    great_for_clubs_title: "Idealne dla kół i klubów szkolnych"
     great_for_clubs_description: "Nauczyciele mogą kupić maksymalnie __maxQuantityStarterLicenses__ licencji startowych na rok." # {change}
     low_price_title: "Jedyne __starterLicensePrice__ za ucznia"
     low_price_description: "Licencje startowe są aktywne przez __starterLicenseLengthMonths__ miesięcy od daty ich zakupu."
-    three_great_courses: "Tzry świetne kursy dostępne w licencji startowej:"
+    three_great_courses: "Trzy świetne kursy dostępne w licencji startowej:"
     license_limit_description: "Nauczyciele mogą kupić aż do __maxQuantityStarterLicenses__ licencji startowych. Ty posiadasz już ich __quantityAlreadyPurchased__. Jeśli potrzebujesz więcej napisz na __supportEmail__. Licencje startowe są ważne przez __starterLicenseLengthMonths__ miesięcy."
     student_starter_license: "Licencje startowe uczniów"
     purchase_starter_licenses: "Kup licencje startowe"
@@ -1024,7 +1024,7 @@
     gd1_description: "używa składni, którą uczniowie już znają, aby pokazać im jak stworzyć własny, grywalny poziom w grze."
     see_an_example_project: "zobacz przykładowy projekt"
     get_started_today: "Zacznij dziś!"
-    want_all_the_courses: "Chcesz wszytskie kursy? Poproś o informacje o pełnej licencji."
+    want_all_the_courses: "Chcesz wszystkie kursy? Poproś o informacje o pełnej licencji."
     compare_license_types: "Porównaj typy licencji:"
     cs: "Informatyka"
     wd: "Tworzenie stron"
@@ -1061,8 +1061,8 @@
     purchaser_role_label: "Jaki masz wpływ na zakup"
     influence_advocate: "Wpływam/Doradzam"
     evaluate_recommend: "Oceniam/Polecam"
-    approve_funds: "Akceptuje środni Funds"
-    no_purchaser_role: "Brak żadnej roli"
+    approve_funds: "Zatwierdzam fundusze"
+    no_purchaser_role: "Nie mam wpływu na zamówienia"
     district_label: "Okręg"
     district_name: "Nazwa okręgu"
     district_na: "Wprowadź `-` jeśli nie dotyczy"
@@ -1196,7 +1196,7 @@

   community:
     main_title: "Społeczność CodeCombat"
-    introduction: "Sprawdź poniżej jak możesz nam pomóc i co sprawia ci najwięcej radości. Nie możemy się doczekać na pracę z Tobą!"
+    introduction: "Sprawdź poniżej jak możesz nam pomóc i co sprawia ci najwięcej radości. Nie możemy się doczekać pracy z Tobą!"
     level_editor_prefix: "Użyj CodeCombat"
     level_editor_suffix: "do tworzenia i edycji poziomów. Użytkownicy stworzyli poziomy dla swoich klas, przyjaciół, hackathonów, uczniów i rodzeństwa. Jeśli lubisz tworzyć nowe poziomy zacznij od przerobienia już istniejącego!"
     thang_editor_prefix: "Rzeczy dostępne w grze nazywamy 'obiektami'. Użyj"


Update ru translation

Update ru translation

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -200,13 +200,13 @@
     school_name: "Название школы and город"
     optional: "не обязательно"
     school_name_placeholder: "Школа № 2, город Электросталь, Московская область"
-#    or_sign_up_with: "or sign up with"
-#    connected_gplus_header: "You've successfully connected with Google+!"
-#    connected_gplus_p: "Finish signing up so you can log in with your Google+ account."
-#    gplus_exists: "You already have an account associated with Google+!"
-#    connected_facebook_header: "You've successfully connected with Facebook!"
-#    connected_facebook_p: "Finish signing up so you can log in with your Facebook account."
-#    facebook_exists: "You already have an account associated with Facebook!"
+    or_sign_up_with: "или авторизуйтесь через"
+    connected_gplus_header: "Вы успешно авторизовались через Google+!"
+    connected_gplus_p: "Теперь можно войти используя аккаунт Google+."
+    gplus_exists: "У вас уже имеется аккаунт связанный с Google+!"
+    connected_facebook_header: "Вы успешно авторизовались через Facebook!"
+    connected_facebook_p: "Теперь можно войти используя аккаунт Facebook."
+    facebook_exists: "У вас уже имеется аккаунт связанный сh Facebook!"
 #    hey_students: "Students, enter the class code from your teacher."

   recover:
@@ -434,10 +434,10 @@
     tip_strong_opponents: "Даже сильнейший противник имеет слабость. - Itachi Uchiha"
     tip_paper_and_pen: "Прежде чем начать программировать, вы всегда можете попробовать с листом бумаги и ручкой."
     tip_solve_then_write: "Сперва реши задачу, затем пиши код. - Джон Джонсон"
-#    tip_compiler_ignores_comments: "Sometimes I think that the compiler ignores my comments."
-#    tip_understand_recursion: "The only way to understand recursion is to understand recursion."
+    tip_compiler_ignores_comments: "Иногда мне кажется, что компилятор игнорирует мои ошибки."
+    tip_understand_recursion: "Единственный способ понять рекурсию, это понять рекурсию."
 #    tip_life_and_polymorphism: "Open Source is like a totally polymorphic heterogeneous structure: All types are welcome."
-#    tip_mistakes_proof_of_trying: "Mistakes in your code are just proof that you are trying."
+    tip_mistakes_proof_of_trying: "Ошибка в коде подтвердила твои старания."
 #    tip_adding_orgres: "Rounding up ogres."
 #    tip_sharpening_swords: "Sharpening the swords."
 #    tip_ratatouille: "You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille"
@@ -646,13 +646,13 @@
     editor_config_behaviors_description: "Автозавершать квадратные, фигурные скобки и кавычки."

   about:
-#    main_title: "If you want to learn to program, you need to write (a lot of) code."
-#    main_description: "At CodeCombat, our job is to make sure you're doing that with a smile on your face."
-#    mission_link: "Mission"
-#    team_link: "Team"
-#    community_link: "Community"
-#    story_link: "Story"
-#    careers_link: "Careers"
+    main_title: "Чтобы научиться программировать, необходимо (много) писать код."
+    main_description: "Наша задача быть уверенными, что вы делаете это с улыбкой на лице."
+    mission_link: "Миссия"
+    team_link: "Команда"
+    community_link: "Сообщество"
+    story_link: "История"
+    careers_link: "Карьера"
 #    press_link: "Press"
 #    mission_title: "Our mission: make programming accessible to every student on Earth."
 #    mission_description_1: "Programming is magic. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using typed code."
@@ -681,7 +681,7 @@
     retrostyle_blurb: "Игры в стиле ретро"
     jose_title: "Музыка"
     jose_blurb: "На взлет"
-#    community_title: "...and our open-source community"
+    community_title: "...и наше open-source сообщество"
 #    community_subtitle: "Over 450 contributors have helped build CodeCombat, with more joining every week!"
 #    community_description_1: "CodeCombat is a community project, with hundreds of players volunteering to create levels, contribute to our code to add features, fix bugs, playtest, and even translate the game into 50 languages so far. Employees, contributors and the site gain by sharing ideas and pooling effort, as does the open source community in general. The site is built on numerous open source projects, and we are open sourced to give back to the community and provide code-curious players a familiar project to explore and experiment with. Anyone can join the CodeCombat community! Check out our"
 #    community_description_link: "contribute page"
@@ -1190,7 +1190,7 @@
     ambassador_title: "Посол"
     ambassador_title_description: "(поддержка)"
     ambassador_summary: "Приручите наших пользователей форума и давайте рекомендации по их вопросам. Наши Послы представляют CodeCombat миру."
-#    teacher_title: "Teacher"
+    teacher_title: "Учитель"

   editor:
     main_title: "Редакторы CodeCombat"


Update ru.coffee

Update ru.coffee

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -99,7 +99,7 @@
     help_pref: "Нужна помощь? Напишите"
     help_suff: "и мы свяжемся!"
     resource_hub: "Ресурсы"
-#    apcsp: "AP CS Principles"
+    apcsp: "AP CS Принципы"

   modal:
     close: "Закрыть"
@@ -118,8 +118,8 @@

   play:
     play_as: "Играть за" # Ladder page
-#    get_course_for_class: "Assign Game Development and more to your classes!"
-#    request_licenses: "Contact our school specialists for details."
+    get_course_for_class: "Добавьте игровую разработку в ваши классы!"
+    request_licenses: "Связаться с нашими специалистами и узнать больше."
     compete: "Соревноваться!" # Course details page
     spectate: "Наблюдать" # Ladder page
     players: "игроки" # Hover over a level on /play
@@ -137,6 +137,7 @@
     poll: "Опрос" # Tooltip on poll button from /play
     next: "Далее" # Go from choose hero to choose inventory before playing a level
     change_hero: "Выбрать героя" # Go back from choose inventory to choose hero
+    change_hero_or_language: "Выбрать героя или язык"
     buy_gems: "Купить самоцветы"
     subscribers_only: "Только для подписчиков!"
     subscribe_unlock: "Подписаться для разблокировки!"
@@ -154,8 +155,8 @@
     brain_pop_challenge: "Испытай себя играя снова с использованием различных языков программирования!"
     replay: "Повторить"
     back_to_classroom: "Назад в класс"
-#    teacher_button: "For Teachers"
-#    get_more_codecombat: "Get More CodeCombat"
+    teacher_button: "Для учителей"
+    get_more_codecombat: "Получить больше возможностей от CodeCombat"

   code:
     if: "если" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.)
@@ -488,7 +489,7 @@
     victory: "Победа"
     victory_title_prefix: "Уровень "
     victory_title_suffix: " пройден"
-    victory_sign_up: "Зарегистрируйтесь, чтобы сохранить прогресс"
+    victory_sign_up: "Зарегистрироваться и сохранить"
     victory_sign_up_poke: "Хотите сохранить ваш код? Создайте бесплатную учётную запись!"
     victory_rate_the_level: "Насколько интересным был этот уровень?"
     victory_return_to_ladder: "Вернуться к рейтингу"


Update vi.coffee

Update vi.coffee

--- app/locale/vi.coffee
+++ app/locale/vi.coffee
@@ -540,7 +540,7 @@

   about:
     why_codecombat: "Tại sao lại sử dụng CodeCombat?"
-#    why_paragraph_1: "If you want to learn to program, you don't need lessons. You need to write a lot of code and have a great time doing it."
+    why_paragraph_1: "Nếu bạn muốn học lập trình, bạn không cần phải tham gia kháo học nào. Bạn chỉ cần phải viết thật nhiều dòng lệnh và có một khoảng thời gian tuyệt vời khi làm chuyện đó."
 #    why_paragraph_2_prefix: "That's what programming is about. It's gotta be fun. Not fun like"
 #    why_paragraph_2_italic: "yay a badge"
 #    why_paragraph_2_center: "but fun like"


fix keyboard issue

fix keyboard issue

--- public/src/classroom-page/subtitles-controls/subtitles-controls-view.coffee
+++ public/src/classroom-page/subtitles-controls/subtitles-controls-view.coffee
@@ -39,6 +39,8 @@
       )

       this.onKeyDownEvent = (event) =>
+        if $(event.target).is('input')
+          return
         this.onKeyDown(event)
       $(window).on('keydown', this.onKeyDownEvent)
       window.subtitlesControlsTeardown = this.teardown


Stringify the JSON test

Stringify the JSON test

--- spec/Graph.coffee
+++ spec/Graph.coffee
@@ -147,7 +147,7 @@
     it 'should contain one group', ->
       chai.expect(g.edges.length).to.equal 1
     it 'should produce same JSON when serialized', ->
-      chai.expect(g.toJSON()).to.deep.equal json
+      chai.expect(JSON.stringify(g.toJSON())).to.equal JSON.stringify(json)
     describe 'renaming a node', ->
       it 'should emit an event', (done) ->
         g.once 'renameNode', (oldId, newId) ->


Quick fix for folder conflicts

Quick fix for folder conflicts

--- src/remote/watcher.coffee
+++ src/remote/watcher.coffee
@@ -182,7 +182,7 @@
             @prep.updateDoc @side, doc, callback
         else if doc.checksum? and was.checksum is doc.checksum
             @prep.moveDoc @side, doc, was, callback
-        else if was.remote._rev is doc._rev
+        else if doc.docType is 'folder' or was.remote._rev is doc._rev
             # Example: doc is modified + renamed on cozy with desktop stopped
             @prep.deleteDoc @side, was, (err) =>
                 log.error err if err


address comments

address comments

--- src/rules/eol_last.coffee
+++ src/rules/eol_last.coffee
@@ -15,6 +15,6 @@
         previousIsNewline = if lineApi.lineCount > 1
             lineApi.lines[lineApi.lineNumber - 1].length is 0
         else
-            no
+            false

-        return true unless isNewline && !previousIsNewline
+        return true unless isNewline and not previousIsNewline


BUG-FIX Change runner to take inner template, clear code

--- src/runners/repeat.coffee
+++ src/runners/repeat.coffee
@@ -1,4 +1,6 @@
 Neck.Controller.runners['repeat'] = class RepeatRunner extends Neck.Controller
+
+  innerTemplate: true

   scope:
     filter: '='
@@ -7,18 +9,12 @@
   constructor: ->
     super

-    unless @scope.view
-      @template = @el.html()
-
-    # Clear runner body
-    @el.empty()
-
     # run attribute should pass regexp "'item_name in items_name"
     unless @runAttr.match /^[a-zA-Z$_][^\ \(\)\{\}]*\ in\ [a-zA-Z$_][^\ \(\)\{\}]*$/
       throw 'Wrong list definition'

     # Get item and items
-    console.log @runAttr = @runAttr.split(' in ')
+    @runAttr = @runAttr.split(' in ')

     # Set items property
     @scope.addProperty 'items', @runAttr[1]
@@ -42,15 +38,25 @@
           itemController
         undefined

-    timeout = null
+    if @scope.hasOwnProperty 'filter'
+      timeout = null
+      @scope.watch 'filter', =>
+        clearTimeout timeout
+        timeout = setTimeout =>
+          if @controllers
+            @checkFilter(controller) for controller in @controllers
+          undefined
+        , 10

-    @scope.watch 'filter', =>
-      clearTimeout timeout
-      timeout = setTimeout =>
-        if @controllers
-          controller.checkFilter() for controller in @controllers
-        undefined
-      , 10
+  checkFilter: (c)->
+    if @scope.filter isnt undefined
+      if typeof @scope.filter is 'function'
+        match = @scope.filter(c.item)
+      else
+        match = c.item.toString()?.match new RegExp(@scope.filter, 'gi')
+      if match then c.el.show() else c.el.hide()
+    else
+      c.el.show()

 class RepeatItem extends Neck.Controller

@@ -65,21 +71,4 @@
     # Listen to update event on spine models
     if @item instanceof Spine.Model
       @listenTo @item, 'update', @render
-      @listenTo @item, 'destroy', => @release()
-
-    @checkFilter()
-
-  checkFilter: ->
-    if !@scope.filter and !@visible
-      @el.show()
-      @visible = true
-    else
-      if @item.toString()?.match new RegExp(@scope.filter, 'gi')
-        unless @visible
-          @el.show()
-          @visible = true
-      else
-        if @visible
-          @el.hide()
-          @visible = false
-
+      @listenTo @item, 'destroy', => @release()


Remove the dependency to gitio

Remove the dependency to gitio

I just had a hard time debugging this, with removing that dependency it is way more stable then before.

--- src/scripts/travis.coffee
+++ src/scripts/travis.coffee
@@ -3,7 +3,6 @@
 #   Can also notify about builds, just enable the webhook notification on travis http://about.travis-ci.org/docs/user/build-configuration/ -> 'Webhook notification'
 #
 # Dependencies:
-#   "gitio": "1.0.1"
 #
 # Configuration:
 #   None
@@ -21,7 +20,6 @@

 url = require('url')
 querystring = require('querystring')
-gitio = require('gitio')

 module.exports = (robot) ->

@@ -39,9 +37,6 @@

   robot.router.post "/hubot/travis", (req, res) ->
     query = querystring.parse url.parse(req.url).query
-    res.end JSON.stringify {
-       received: true #some client have problems with and empty response
-    }

     user = {}
     user.room = query.room if query.room
@@ -50,9 +45,11 @@
     try
       payload = JSON.parse req.body.payload

-      gitio payload.compare_url, (err, data) ->
-        robot.send user, "#{payload.status_message.toUpperCase()} build (#{payload.build_url}) on #{payload.repository.name}:#{payload.branch} by #{payload.author_name} with commit (#{if err then payload.compare_url else data})"
+      robot.send user, "#{payload.status_message.toUpperCase()} build (#{payload.build_url}) on #{payload.repository.name}:#{payload.branch} by #{payload.author_name} with commit (#{payload.compare_url})"

     catch error
       console.log "travis hook error: #{error}. Payload: #{req.body.payload}"
-
+
+    res.end JSON.stringify {
+      send: true #some client have problems with and empty response, sending that response ion sync makes debugging easier
+    }


fixes

fixes

--- config/featured_judges.coffee
+++ config/featured_judges.coffee
@@ -94,13 +94,13 @@
   },
   {
     "name": "Jason Hoffman",
-    "label": "Joyent founder",
+    "label": "Founder of Joyent",
     "twitter": "http://twitter.com/jasonh",
     "linkedin": "http://www.linkedin.com/in/jasonhoffman",
     "avatar": "https://si0.twimg.com/profile_images/3021341335/e19f61309193fbc176421481be47e1f7.png"
   },
   {
-    "name": "Tim Lee",
+    "name": "Timothy Lee",
     "label": "Sequoia Capital",
     "twitter": "",
     "linkedin": "http://www.linkedin.com/in/timothytclee",


Fix typo

Fix typo

--- src/token-iterator.coffee
+++ src/token-iterator.coffee
@@ -33,7 +33,7 @@
       if tag < 0
         if tag % 2 is 0
           scopeName = atom.grammars.scopeForId(tag + 1)
-          if @scopeEnds.length is 0
+          if @scopeStarts.length is 0
             @scopeEnds.push(scopeName)
           else
             startsTop = this.scopeStarts.pop()


hotfix grunt config for github-releaser

hotfix grunt config for github-releaser

--- config/grunt/ghrelease.coffee
+++ config/grunt/ghrelease.coffee
@@ -13,6 +13,7 @@
       repository: 'casetext/quill'
       files:
         '.build': ['quill.tar.gz']
-      auth: process.env.GITHUB_TOKEN
+      auth:
+        user: process.env.GITHUB_TOKEN

   )


Send unique users to perfmon

Send unique users to perfmon

--- server/commons/perfmon.coffee
+++ server/commons/perfmon.coffee
@@ -25,7 +25,8 @@
       path = req.route?.path or '/*'
       stat = req.method + "." + path.replace /[^A-Za-z0-9]+/g, '_'
       realClient.timing stat, ms
-
+      name = req.user?._id
+      realClient.unique 'users', name if name

     res.once 'finish', recordMetrics
     res.once 'error', cleanup
@@ -39,5 +40,5 @@
   return callback unless realClient
   time = process.hrtime()
   (args...) ->
-    realClient.timing name, ms
+    realClient.timing name.replace(/[^A-Za-z0-9]+/g, '_'), ms
     return callback.apply(this, args)


Typo and fix wrong logger in konnector init module

--- server/init/konnectors.coffee
+++ server/init/konnectors.coffee
@@ -34,7 +34,7 @@
                     createKonnectors konnectorsToCreate, callback

-# Reset konnector importing flags: isImporting flage is set to false if value
+# Reset konnector importing flags: isImporting flag is set to false if value
 # is true. This happens when the app is crashing while importing.
 konnectorResetValue = (konnector, callback) ->

@@ -58,7 +58,7 @@
     for name, konnectorModule of konnectorModules
         unless konnectorHash[konnectorModule.slug]?
             konnectorsToCreate.push konnectorModule
-            console.info "New konnector to init: #{name}"
+            log.info "New konnector to init: #{name}"

     return konnectorsToCreate


[bugfix] Set settings on new install

[bugfix] Set settings on new install

--- server/models/settings.coffee
+++ server/models/settings.coffee
@@ -24,7 +24,7 @@
         if existing
             callback null, existing
         else
-            callback null, new Settings()
+            Settings.create {}, callback

 Settings.get = (callback) ->
     Settings.getInstance (err, instance) ->


handling layers a little better

handling layers a little better

and allowing user to reset to nigeria_base_map

--- static/coffee/nmis.coffee
+++ static/coffee/nmis.coffee
@@ -949,6 +949,7 @@
           minZoom: 6
         ).addTo lmap
         mapLayers = {}
+        currentLeafletLayer = false
         for mdgL in mdgLayers
           do ->
             curMdgL = mdgL
@@ -959,11 +960,22 @@
               tileset: mdgL.slug
               maxZoom: 9
               minZoom: 6
-            curMdgL.onSelect = ()->
-              lmap.removeLayer baseLayer
-              lmap.addLayer(ml)
-              @show_description()
-              @show_legend()
+            if "mdg" of mdgL
+              curMdgL.onSelect = ()->
+                lmap.addLayer(ml)
+                lmap.removeLayer baseLayer
+                lmap.removeLayer(currentLeafletLayer)  if currentLeafletLayer
+                currentLeafletLayer = ml
+                @show_description()
+                @show_legend()
+            else
+              curMdgL.onSelect = ->
+                lmap.addLayer baseLayer
+                lmap.removeLayer(currentLeafletLayer)  if currentLeafletLayer
+                currentLeafletLayer = false
+                @hide_legend()
+                @hide_description()
+
       launcher.fail ()->
         log "LAUNCHER FAIL! Scripts not loaded"

@@ -988,11 +1000,18 @@
           else
             layersWitoutMdg.push @

+        hide_description: ->
+          descWrap = $(".mn-iiwrap")
+          descWrap.find(".mdg-display").text "Nigeria Base Map"
+          descWrap.find("div.layer-description").empty()
+
         show_description: ->
           descWrap = $(".mn-iiwrap")
           goalText = NMIS.mdgGoalText(@mdg)
           descWrap.find(".mdg-display").html goalText
           descWrap.find("div.layer-description").html $("<p>", text: @description)
+        hide_legend: ->
+          $(".mn-legend").removeClass("open")
         show_legend: ->
           legendData = for lrow in @legend_data.split(";")
             [value, opacity, color] = lrow.split(",")
@@ -1017,12 +1036,14 @@
           $ "<option>", value: @slug, text: @name

       onSelect: ()->
+
       selectBoxChange = ()->
         layersBySlug[$(@).val()].onSelect()

       createSelectBox = ->
         sb = $ "<select>", title: plsSelectMsg, style: "width:100%", change: selectBoxChange
         sb.append $ '<option>', value:""
+        sb.append $ '<option>', value:"nigeria_base", text: "Nigeria Base Map"
         for mdg in mdgs.sort() when mdg?
           sb.append og = $ "<optgroup>", label: "MDG #{mdg}"
           og.append layer.$option()  for layer in layersByMdg[mdg]


:hand: implementing zoom level based map movement

--- src/Termap.coffee
+++ src/Termap.coffee
@@ -156,10 +156,10 @@
       when "k" then @rotation += 15
       when "l" then @rotation -= 15

-      when "left" then @center.lon -= 1
-      when "right" then @center.lon += 1
-      when "up" then @center.lat += 1
-      when "down" then @center.lat -= 1
+      when "left" then @center.lon -= 8/Math.pow(2, @zoom)
+      when "right" then @center.lon += 8/Math.pow(2, @zoom)
+      when "up" then @center.lat += 6/Math.pow(2, @zoom)
+      when "down" then @center.lat -= 6/Math.pow(2, @zoom)

       else
         null


raspberrypi3-64.coffee: Updating links to getting started guide

Signed-off-by: Gareth Davies <2f9181a725196cd0a5e07c6a6e161d83fcc71157@balena.io>

--- raspberrypi3-64.coffee
+++ raspberrypi3-64.coffee
@@ -18,9 +18,9 @@

 	instructions: commonImg.instructions
 	gettingStartedLink:
-		windows: 'https://docs.resin.io/raspberrypi3/nodejs/getting-started/#adding-your-first-device'
-		osx: 'https://docs.resin.io/raspberrypi3/nodejs/getting-started/#adding-your-first-device'
-		linux: 'https://docs.resin.io/raspberrypi3/nodejs/getting-started/#adding-your-first-device'
+		windows: 'https://www.balena.io/docs/learn/getting-started/raspberrypi3/nodejs/'
+		osx: 'https://www.balena.io/docs/learn/getting-started/raspberrypi3/nodejs/'
+		linux: 'https://www.balena.io/docs/learn/getting-started/raspberrypi3/nodejs/'
 	supportsBlink: true

 	options: [ networkOptions.group ]


JMachine: update comments, put a max 9 limit per operation for sharing methods

--- workers/social/lib/social/models/computeproviders/machine.coffee
+++ workers/social/lib/social/models/computeproviders/machine.coffee
@@ -432,14 +432,16 @@
         @update $set: { label }, (err)-> kallback err, slug

-  # .share can be used like this:
+  # .shareWith can be used like this:
   #
-  # JMachineInstance.shareWith { user: yes, owner: no, target: "gokmen"}, cb
-  #                                                user slug -> ^^^^^^
+  # JMachineInstance.shareWith {
+  #   user: yes, owner: no, target: ["gokmen", "dicle"]}, cb
+  # }
+  #                    user slugs ->  ^^^^^^ ,  ^^^^^

   shareWith$: permit 'populate users',

-    success: revive
+    success : revive

       shouldReviveClient   : yes
       shouldReviveProvider : no
@@ -453,10 +455,14 @@

       { target } = options

+      if target.length > 9
+        return callback new KodingError \
+          "It is not allowed to change more than 9 state at once."
+
       # Owners cannot unassign them from a machine
       # Only another owner can unassign any other owner
-      if user.username is target
+      if user.username in target
         return callback \
-          new KodingError "It's not allowed to change owner's state!"
+          new KodingError "It is not allowed to change owner state!"

       JMachine::shareWith.call this, options, callback


JMachine: also include username of the user in the users list where needed

--- workers/social/lib/social/models/computeproviders/machine.coffee
+++ workers/social/lib/social/models/computeproviders/machine.coffee
@@ -1,10 +1,10 @@
-
 { Module }  = require 'jraphical'
 { revive }  = require './computeutils'
 KodingError = require '../../error'

 { argv }    = require 'optimist'
 KONFIG      = require('koding-config-manager').load("main.#{argv.c}")
+

 module.exports = class JMachine extends Module

@@ -210,6 +210,7 @@
     unless inList
       newUsers.push {
         id       : user.getId()
+        username : user.username
         owner    : no
         approved : no
         permanent
@@ -273,10 +274,16 @@
     # Users list can be provided before machine create
     # We also need to make sure that the real owner of the
     # machine is in list. ~ GG
+    userObj    =
+      id       : user.getId()
+      sudo     : yes
+      owner    : yes
+      username : user.username
+
     if Array.isArray(data.users) and data.users.length > 0
-      data.users.push { id: user.getId(), sudo: yes, owner: yes }
+      data.users.push userObj
     else
-      data.users   = [{ id: user.getId(), sudo: yes, owner: yes }]
+      data.users   = [ userObj ]

     data.groups    = [{ id: group.getId() }]

@@ -734,5 +741,3 @@
       machine.shareWith$ client, options, (err) ->

         callback err, machine
-
-


Fix find-mode hangs.

Fix find-mode hangs.

"/" followed immediately by "i" can hang Vimium.

The problem is that launching find mode is asynchronous (we wait until
the HUD is available). Because normal mode is still active, we can enter
insert mode *before* the find-mode HUD receives the focus.

The result is that we end up in both find mode and insert mode, the HUD
has the focus, but the HUD is in insert mode, so it ignores keyboard
events (including `Escape`).

The only way out is to click the page's body and then type `Escape`.

This commit demonstrates the problem: 7d2b00411eae3293fa4c7b1f61b384c0c495b5a2.

This happens in practice, for example while a busy page is loading.

This commit fixes this by ensuring that find-mode blocks keyboard events
immediately (and synchronously) on launch.

--- content_scripts/mode_find.coffee
+++ content_scripts/mode_find.coffee
@@ -71,6 +71,10 @@
       name: "find"
       indicator: false
       exitOnClick: true
+      exitOnEscape: true
+      # This prevents further Vimium commands launching before the find-mode HUD receives the focus.
+      # E.g. "/" followed quickly by "i" should not leave us in insert mode.
+      suppressAllKeyboardEvents: true

     HUD.showFindMode this


Prepend media to containers when dragging and dropping Fixes #59

--- scripts/models/content/inherits/container.coffee
+++ scripts/models/content/inherits/container.coffee
@@ -59,6 +59,7 @@
       @loading = true
       @fetch
         silent: true
+        loading: true
         success: (model, response, options) =>
           @loading = false

@@ -66,7 +67,12 @@
       if (!_.isArray(models)) then (models = if models then [models] else [])

       _.each models, (model, index, arr) =>
-        @get('contents').add(model)
+        contents = @get('contents')
+
+        if contents.length and not options?.loading
+          @get('contents').unshift(model)
+        else
+          @get('contents').add(model)

       if not options?.silent then @trigger('change')

@@ -81,6 +87,7 @@
       else
         (attrs = {})[key] = val

+      options = options || {}
       contents = attrs.contents
       attrs.contents = @get('contents') or new Container()

@@ -90,7 +97,7 @@
         require ['cs!collections/content'], (content) =>
           content.loading().done () =>
             _.each contents, (item) =>
-              @add(content.get({id: item.id}), {silent: true})
+              @add(content.get({id: item.id}), options)
             @trigger('change:contents')

       return Backbone.Model::set.call(@, attrs, options)


Added code to find most recent item in queue for current user

--- server/methods/box/buildFromTemplate.coffee
+++ server/methods/box/buildFromTemplate.coffee
@@ -13,6 +13,10 @@
       targetFolder = "0" #Root level for Dev
     else if targetFolder
       console.log(targetFolder)
+      console.log(targetFolder.folderId)
+      console.log(targetFolder.folderName)
+      console.log(targetFolder.boxUserId)
+      targetFolder = targetFolder.folderId
     else
       throw new Meteor.Error(400, "Target folder is unknown. Try launching this window directly by right-clicking on a Box folder.");


Clarified use of basename in filter

Clarified use of basename in filter

--- src/bundle.coffee
+++ src/bundle.coffee
@@ -66,7 +66,9 @@
       if err then return callback err
       ignores = content.split('\n').filter (m) -> m.length
       filter = ->
-        basename = this.basename
+        # @basename because when the filter is applied the
+        # context is the path of the file being considered
+        basename = @basename
         ignore = ignores.some (i) -> i && minimatch basename, i
         return !ignore


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -36,7 +36,7 @@
       height: 2 * deadzone.y

     if objectCenter.x > centerRect.right
-      I.scroll.x = I.scroll.x.approach(I.maxVelocity)
+      I.scroll.x = I.scroll.x.approach(centerI.maxVelocity.x)
     else if objectCenter.x < centerRect.left


Improve editor behavior

Improve editor behavior

--- vendor/assets/javascripts/control.js.coffee
+++ vendor/assets/javascripts/control.js.coffee
@@ -2,6 +2,14 @@
   TOKEN_REGEX: /[^<]+<\/span>/i
   ID_REGEX: /data-id="[0-9]+"/i
   WORD_REGEX: /[a-z]+$/i
+
+  SUCCESS_COLOR: '#64b80b;'
+  ERROR_COLOR: '#b94a48;'
+  NEUTRAL_COLOR: '#E0E0E0;'
+
+  SUCCESS_MSG: 'Valid equation'
+  ERROR_MSG: 'Syntax error in equation'
+  CHECKING_MSG: 'Checking equation...'

   constructor: (@element, @options = {}) ->
     # Remove outline and return if readonly (display) mode.
@@ -9,23 +17,30 @@
       @element.addClass "noborder"
       return

+    # Find input, set editable, don't allow tokens to be edited.
     @input = @element.find(".token-text-area-input")
-
     @input.attr "contenteditable", "true"
     @input.find(".token").attr("contenteditable", "false")

-    @element.after('
Syntax error in formula.
')
-    @errorMsg = @element.next()
-    @errorMsg.hide()
-
+    # Ensure whitespace is correct.
+    @fixWhitespace()
+
+    # Create and store error message box, initialized to valid.
+    @element.after('

')
+    @msg = @element.next()
+    @showSuccess()
+
+    # Create instance variables.
     @typingTimer = null
     @resultMenu = null
     @resultList = null
     @word = null
     @range = null

+    # Create the result menu.
     @createResultMenu()

+    # Bind all handlers.
     @registerEvents()

   registerEvents: ->
@@ -41,10 +56,7 @@

       # Re-check validity of equation unless event is an arrow key.
       unless @isArrow(event.which)
-        @element.removeClass "valid"
-        @element.removeClass "invalid"
-        @element.addClass "maybevalid"
-        @errorMsg.hide()
+        @showChecking()

         clearTimeout(@typingTimer) unless @typingTimer is null
         @typingTimer = setTimeout( =>
@@ -188,7 +200,6 @@
     node.contentEditable = false
     node.dataset.id = id
     node.innerHTML = name
-    @range.insertNode(document.createTextNode('\u00A0')) # Add a nbsp; at the end.
     @range.insertNode(node)

     # Set selection range (i.e. caret position) to new token.
@@ -219,21 +230,12 @@
     if @options.onChange
       @options.onChange equation, (results) =>
         if results.valid
-          @element.removeClass "maybevalid"
-          @element.removeClass "invalid"
-          @element.addClass "valid"
-          @errorMsg.hide()
+          @showSuccess()
         else
-          @element.removeClass "maybevalid"
-          @element.removeClass "valid"
-          @element.addClass "invalid"
-          @errorMsg.show()
+          @showError()

   isArrow: (code) ->
     $.inArray(code, [37, 38, 39, 40]) != -1
-
-  stripNbsp: (string) ->
-    string.replace(/ /g, ' ')

   getRange: ->
     # Return currently selected range.
@@ -262,6 +264,36 @@
       html.empty().append(newContents)

     html.text().substr(0, caretPos)
+
+  stripNbsp: (string) ->
+    string.replace(/ /g, '')
+
+  fixWhitespace: ->
+    html = @stripNbsp(@input.html())
+    html = html.replace(/ /g, ' ')
+    html = html.replace(/[\s]+/g, ' ')
+    @input.html(html)
+
+  showSuccess: ->
+    @element.removeClass "invalid"
+    @element.removeClass "maybevalid"
+    @element.addClass "valid"
+    @msg.html(@SUCCESS_MSG)
+    @msg.css('color', @SUCCESS_COLOR)
+
+  showError: ->
+    @element.removeClass "valid"
+    @element.removeClass "maybevalid"
+    @element.addClass "invalid"
+    @msg.html(@ERROR_MSG)
+    @msg.css('color', @ERROR_COLOR)
+
+  showChecking: ->
+    @element.removeClass "valid"
+    @element.removeClass "invalid"
+    @element.addClass "maybevalid"
+    @msg.html(@CHECKING_MSG)
+    @msg.css('color', @NEUTRAL_COLOR)

 $.fn.tokenTextArea = (options, args...) ->
   @each ->


added alpha attribute to canvas graphics

--- platform/src/services.coffee
+++ platform/src/services.coffee
@@ -112,6 +112,7 @@
     ctx.save()
     handleTransformations(shape, ctx)
     if shape.composition then ctx.globalCompositeOperation = shape.composition
+    if shape.alpha then ctx.globalAlpha = shape.alpha

     switch shape.type
       when 'rectangle'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/array_extensions.coffee
+++ test/array_extensions.coffee
@@ -1,6 +1,6 @@
 module "Array"

-test "#aver"
+test "#average"

 test "#compact", ->
   a = [0, 1, undefined, 2, null, 3, '', 4]


Adding back channel lookup for non-slack plugins

--- src/client.coffee
+++ src/client.coffee
@@ -86,6 +86,12 @@
     @robot.logger.debug "~~~~~~~"
     @robot.logger.debug message
     {room} = envelope
+
+    if !(room.match /[A-Z]/) # slack rooms are always lowercase
+      # try to translate room name to room id
+      channelForName = @rtm.dataStore.getChannelByName(room)
+      room = channelForName.id if channelForName
+
     options = { as_user: true, link_names: 1 }

     if typeof message isnt 'string'


Less options in refresh combobox, because sometimes less is more.

--- muleview/app/view/ChartsView.coffee
+++ muleview/app/view/ChartsView.coffee
@@ -137,7 +137,6 @@
               text: Muleview.model.Retention.toLongFormat(secs),
               value: secs
             } for secs in [
-              1,3,5
               10
               30
               60


Updated callback_ordered to new expect syntax

--- test/callback_ordered.coffee
+++ test/callback_ordered.coffee
@@ -3,14 +3,17 @@
 expect      = chai.expect
 after       = require('fluent-time').after
 lies        = require '../src/beautiful-lies'
-createLiar = lies.createLiar
+
+lies.init()

 # TODO: Add support for simultaneous calls of another (of) callback
 # and the handler callback

 describe 'Runs callback', ->

-  liar = createLiar [
+  liar = {}
+
+  liar.expect [
     function_name: 'connect'
     run_callback: [
       argument_2:
@@ -26,7 +29,9 @@

 describe 'Runs callback (result object instead of array of result object)', ->

-  liar = createLiar [
+  liar = {}
+
+  liar.expect [
     function_name: 'connect'
     run_callback:
       argument_2:
@@ -40,8 +45,8 @@
       done()

 describe 'Runs callback with error arguments', ->
-
-  liar = createLiar [
+  liar = {}
+  liar.expect [
     function_name: 'query'
     run_callback: [
       argument_1:
@@ -57,8 +62,8 @@
       done()

 describe 'Runs callback with dual arguments', ->
-
-  liar = createLiar [
+  liar = {}
+  liar.expect [
     function_name: 'query'
     run_callback: [
       argument_2:
@@ -77,11 +82,11 @@
       done()

 describe 'run_callback defined with no_arguments', ->
-
+  liar = {}
   passedToCallback = null

   beforeEach (done) ->
-    liar = createLiar [
+    liar.expect [
       function_name: 'query',
       run_callback: [
         no_arguments: true
@@ -99,9 +104,10 @@

 describe 'Runs callback order', ->

+
   it 'should call callbacks in turn', ->
-
-    liar = createLiar [
+    liar = {}
+    liar.expect [
       function_name: 'query'
       run_callback: [
         {
@@ -133,8 +139,8 @@

   it 'should work with multiple expectations', (done) ->
-
-    liar = createLiar [
+    liar = {}
+    liar.expect [
       {
         function_name: 'count'
         run_callback: [
@@ -176,12 +182,15 @@

   describe 'yeild delay', ->

+
     describe 'when running callback without any delay specified', ->
-
+      liar = null
       result = null

       beforeEach (done) ->
-        liar = createLiar [
+        liar = {}
+
+        liar.expect [
           function_name: 'query'
           run_callback: [
             {
@@ -206,9 +215,11 @@
           done()

     describe 'when calling back with a delay of 237 ms', () ->
+      liar = null
       result = null
       beforeEach (done) ->
-        liar = createLiar [
+        liar = {}
+        liar.expect [
           function_name: 'query'
           run_callback: [
             {
@@ -236,15 +247,21 @@

   onLoadResult = null
   onErrorResult = null
+  yielded = null
   liar = null

+  beforeEach ->
+    onLoadResult = null
+    onErrorResult = null
+    yielded = null
+    liar = {}
+
+
   describe 'and has one event listener', ->
-    liar = null
-    yielded = null

     beforeEach (done) ->

-      liar = createLiar [
+      liar.expect [
         {
           function_name: 'addEventListener'
         }, {
@@ -258,8 +275,6 @@
         }
       ]

-
-
       liar.addEventListener 'onLoad', (error, result) ->
         yielded = result
       liar.loadStuff()
@@ -271,10 +286,8 @@

   describe 'and has multiple event listeners', ->

-
-
     beforeEach ->
-      liar = createLiar [
+      liar.expect [
         {
           function_name: 'addEventListener'
           arguments: [ 'onLoad' ]
@@ -325,7 +338,7 @@

   describe 'and defines a single argument (as opposed to array)', ->
     beforeEach ->
-      liar = createLiar [
+      liar.expect [
         {
           function_name: 'addEventListener'
           arguments: [ 'onResult' ]
@@ -356,8 +369,8 @@
   # TODO certain arguments

 it 'should support on_value for callback arguments', (done) ->
-
-  liar = createLiar [
+  liar = {}
+  liar.expect [
     function_name: 'connect'
     run_callback: [
       argument_2:
@@ -378,8 +391,8 @@
     done()

 it 'should treat simple objects to on_value the same way as an array with 1 item', (done) ->
-
-  liar = createLiar [
+  liar = {}
+  liar.expect [
     function_name: 'connect'
     run_callback: [
       argument_2:
@@ -400,10 +413,11 @@
     done()

 describe 'Syntax checking', ->
-
+  liar = null
+  beforeEach -> liar = {}
   it 'should have a nice warning when too few callbacks', ->
     (->
-      liar = createLiar [
+      liar.expect [
         function_name: 'kaboom'
         run_callback: [
           {
@@ -422,7 +436,7 @@
     ).should.throw 'kaboom was called 3 times, but only defined 2 run_callback.'

   it 'should not display the nice warning when there is only a single callback result', (done) ->
-    liar = createLiar [
+    liar.expect [
       function_name: 'shoot',
       run_callback: [{
         argument_1:
@@ -438,7 +452,7 @@

   it 'should validate that arguments is an array (on_value)', ->
     (->
-      liar = createLiar [
+      liar.expect [
         function_name: 'birth'
         returns:
           value: {}
@@ -454,7 +468,7 @@

   it 'should validate function_name of on_value', ->
     (->
-      liar = createLiar [
+      liar.expect [
         function_name: 'do_stuff'
         returns:
           value: {}
@@ -467,7 +481,7 @@

   it 'should verify that of is an object (string)', ->
     (->
-      liar = createLiar [
+      liar.expect [
         function_name: 'do_things'
         run_callback:
           of: 'otherFunction'
@@ -477,7 +491,7 @@

   it 'should verify that of is an object (number)', ->
     (->
-      liar = createLiar [
+      liar.expect [
         function_name: 'hello'
         run_callback:
           of: 871
@@ -487,7 +501,7 @@

   it 'should throw pretty error message if an of command does\'nt match any callback', ->
     (->
-      liar = createLiar [{
+      liar.expect [{
           function_name: 'addEventListener'
           arguments: [ 'onLoad' ]
         },{
@@ -504,7 +518,7 @@

   it 'should throw a nice error message if too broad a match', ->
     (->
-      liar = createLiar [
+      liar.expect [
         {
           function_name: 'addEventListener'
           arguments: [ 'onLoad' ]


Add useful debug information to JSON.parse errors

--- src/common.coffee
+++ src/common.coffee
@@ -92,5 +92,7 @@
         rv = JSON.parse buffer.toString()
         callback null, rv
       catch error
+        error.filename = filename
+        error.message = "parsing #{ path.basename(filename) }: #{ error.message }"
         callback error
   ], callback


Remove old environment config options

Remove old environment config options

--- src/config.coffee
+++ src/config.coffee
@@ -24,8 +24,8 @@
 nconf.env()

 nconf.defaults
-  port: process.env.BREAKPAD_PORT || 1127
-  baseUrl: process.env.BASEURL || '/'
+  port: 1127
+  baseUrl: '/'
   database:
     host: 'localhost'
     dialect: 'sqlite'


coplete coverage (kinda)

coplete coverage (kinda)

--- test/phabs_admin_test.coffee
+++ test/phabs_admin_test.coffee
@@ -446,7 +446,7 @@

       context 'phad bug feed to #dev', ->
         hubot 'phad bug feed to #dev'
-        it 'should say that the alias was forgotten', ->
+        it 'should say that the feed is already there', ->
           expect(hubotResponse())
             .to.eql "The feed from 'bug' to '#dev' already exist."

@@ -471,8 +471,65 @@

       context 'phad bug feed to #dev', ->
         hubot 'phad bug feed to #dev'
-        it 'should say that the alias already exists', ->
+        it 'should say that the feed was created', ->
           expect(hubotResponse())
             .to.eql "Ok, 'bug' is now feeding '#dev'."
           expect(room.robot.brain.data.phabricator.projects['Bug Report'].feeds)
             .to.include '#dev'
+
+  # ---------------------------------------------------------------------------------
+  context 'user wants to remove a feed from a project', ->
+
+    context 'and the feed exists', ->
+      beforeEach ->
+        room.robot.brain.data.phabricator.projects = {
+          'Bug Report': {
+            phid: 'PHID-PROJ-qhmexneudkt62wc7o3z4',
+            feeds: [
+              '#dev'
+            ]
+          },
+          'project with phid': { phid: 'PHID-PROJ-1234567' },
+        }
+        room.robot.brain.data.phabricator.aliases = {
+          bugs: 'Bug Report',
+          bug: 'Bug Report'
+        }
+        do nock.disableNetConnect
+
+      afterEach ->
+        room.robot.brain.data.phabricator = { }
+        nock.cleanAll()
+
+      context 'phad bug remove from #dev', ->
+        hubot 'phad bug remove from #dev'
+        it 'should say that the feed was removed', ->
+          expect(hubotResponse())
+            .to.eql "Ok, The feed from 'bug' to '#dev' was removed."
+          expect(room.robot.brain.data.phabricator.projects['Bug Report'].feeds)
+            .to.include '#dev'
+
+    context 'but the feed do not already exists', ->
+      beforeEach ->
+        room.robot.brain.data.phabricator.projects = {
+          'Bug Report': {
+            phid: 'PHID-PROJ-qhmexneudkt62wc7o3z4',
+            feeds: [ ]
+          },
+          'project with phid': { phid: 'PHID-PROJ-1234567' },
+        }
+        room.robot.brain.data.phabricator.aliases = {
+          bugs: 'Bug Report',
+          bug: 'Bug Report'
+        }
+        do nock.disableNetConnect
+
+      afterEach ->
+        room.robot.brain.data.phabricator = { }
+        nock.cleanAll()
+
+      context 'phad bug remove from #dev', ->
+        hubot 'phad bug remove from #dev'
+        it 'should say that the feed could not be removed', ->
+          expect(hubotResponse())
+            .to.eql "Sorry, 'bug' is not feeding '#dev'."


Apps: Updated createdAt date when a new app created.

--- workers/social/lib/social/models/app.coffee
+++ workers/social/lib/social/models/app.coffee
@@ -309,6 +309,7 @@
                         title     : @getAt 'title'
                         body      : @getAt 'body'
                         manifest  : @getAt 'manifest'
+                        meta      : createdAt: new Date()
                         approved  : yes
                       $addToSet   :
                         versions  : @getAt 'manifest.version'


expect an object instead of an array for stacktemplate.one$

--- workers/social/lib/social/models/computeproviders/stacktemplate.test.coffee
+++ workers/social/lib/social/models/computeproviders/stacktemplate.test.coffee
@@ -92,10 +92,10 @@
           { client, stackTemplate, stackTemplateData } = data
           selector = { _id : stackTemplate._id }

-          StackTemplate.one$ client, selector, null, (err, templates) ->
+          StackTemplate.one$ client, selector, null, (err, template) ->
             expect(err?.message).to.not.exist
-            expect(templates[0].title).to.be.equal stackTemplateData.title
-            expect(templates[0].template.content).to.be.deep.equal stackTemplateData.template
+            expect(template.title).to.be.equal stackTemplateData.title
+            expect(template.template.content).to.be.deep.equal stackTemplateData.template
             done()


Fix angular loadingbar spec

Fix angular loadingbar spec

--- wrapper/src/app/project/scripts/configs/specs/angularLoadingBar.spec.coffee
+++ wrapper/src/app/project/scripts/configs/specs/angularLoadingBar.spec.coffee
@@ -11,7 +11,6 @@
   # =============================================
   beforeEach module('ui.router')
   beforeEach module('ngToast')
-  beforeEach module('pascalprecht.translate')
   beforeEach module('angular-loading-bar')


Fix layout issue in Chrome

Fix layout issue in Chrome

--- index/src/main/frontend/src/js/right-panel.coffee
+++ index/src/main/frontend/src/js/right-panel.coffee
@@ -13,6 +13,7 @@
   # reset heights before doing any calculations
   padding.css('height', '0px')
   tab.css('height', 'auto');
+  tab.css('overflow-y', 'visible')
   tabPosition = tab.position()
   tabHeight = tab.height() + footer.outerHeight()


remove unused function

remove unused function

--- src/editor.coffee
+++ src/editor.coffee
@@ -268,11 +268,6 @@
         leaf.setText(leaf.node.textContent.substr(0, position.offset) + text + leaf.node.textContent.substr(position.offset))
     @doc.updateLine(leaf.line)

-  normalize: ->
-    this.preserveSelection(null, 0, =>
-      Tandem.Document.normalizeHtml(@doc.root)
-    )
-
   preserveSelection: (modificationStart, charAdditions, fn) ->
     @currentSelection = this.getSelection()
     if @currentSelection?


important the old and new range both use start not end, otherwise we get inserts where we can use retains

--- src/editor.coffee
+++ src/editor.coffee
@@ -110,7 +110,7 @@
   try
     newRange = this.getSelection()
     if @selection.range? and newRange?
-      newIndex = newRange.end.index
+      newIndex = newRange.start.index
       [oldLeftDelta, oldRightDelta] = oldDelta.split(oldIndex)
       [newLeftDelta, newRightDelta] = newDelta.split(newIndex)
       decomposeLeft = newLeftDelta.decompose(oldLeftDelta)


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -21,7 +21,7 @@

 # now the enemy's velocity will point toward the player
 enemy.I.velocity
-# => Point(
+# => Point(-1, )

 player.update()
 </pre></code>


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -26,7 +26,7 @@
 enemy.update()

 enemy.I.x
-# =>
+# => 99


Added comments. Got events sending data properly!

--- tests/bluetooth/index.coffee
+++ tests/bluetooth/index.coffee
@@ -21,6 +21,8 @@
   console.log 'Services Set'
   console.log err if err

+value = false
+
 bleno.on 'advertisingStart', (err)->
   if err
     console.log err
@@ -32,8 +34,13 @@
       characteristics: [
         new bleno.Characteristic
           uuid: 'FF10'
-          properties: [ 'read' ]
-          value: false
+          properties: [ 'read', 'notify' ]
+          value: value
+          onSubscribe: (maxValueSize, updateValueCallback)->
+            # TODO - Get this moved out into separate files, and hooked up into the button/LED info. Perhaps even start figuring out users
+            console.log 'maxValueSize', maxValueSize
+            value = 'foo+' + Math.random()
+            updateValueCallback(new Buffer(value))
       ]
     bleno.setServices [primaryService], (err)->
       console.log 'Error setting services', err if err


Only bring in segfault-handler in Debug mode. Temporarily force Release mode until some install script stuff is sorted out.

--- src/gitteh.coffee
+++ src/gitteh.coffee
@@ -4,10 +4,11 @@
 _path = require "path"
 args = require "./args"

-env = if process.env.npm_lifecycle_event is "test" then "Debug" else "Release"
+# env = if process.env.npm_lifecycle_event is "test" then "Debug" else "Release"
+env = "Release"
 bindings = require "../build/#{env}/gitteh"

-(require "segfault-handler").registerHandler()
+(require "segfault-handler").registerHandler() if env is "Debug"

 {minOidLength, types, NativeRepository, NativeRemote} = bindings


guv-heroku-workerstats: Also output stats for scale up requests

--- src/heroku.coffee
+++ src/heroku.coffee
@@ -129,7 +129,7 @@
   state.dynostate = {} if not state.dynostate # 'dyno.N' -> DynoState
   state.startups = [] if not state.startups
   state.shutdowns = [] if not state.shutdowns
-  state.startrequests = [] if not state.startrequests
+  state.scaleups = [] if not state.scaleups
   state.requestedWorkers = {} if not state.requestedWorkers # 'dyno" -> Number

   # Note, they can happen initially because we don't generally know initial state
@@ -166,6 +166,13 @@

       when 'process-starting'
         if state.lasttransition[event.dyno] and state.dynostate[event.dyno] == 'requested'
+          s =
+            dyno: event.dyno
+            start: state.lasttransition[event.dyno]
+            end: event
+          s.duration = s.end.time.getTime() - s.start.time.getTime()
+          state.scaleups.push s
+
           state.dynostate[event.dyno] = 'starting'
           state.lasttransition[event.dyno] = event
         else
@@ -225,7 +232,11 @@

     starts = state.startups.map (s) -> s.duration/1000
     stops = state.shutdowns.map (s) -> s.duration/1000
+    scaleups = state.scaleups.map (s) -> s.duration/1000
     results =
+      scaleup: statistics.mean scaleups
+      scaleup_stddev: statistics.standard_deviation scaleups
+      scaleup_length: scaleups.length
       startup: statistics.mean starts
       startup_stddev: statistics.standard_deviation starts
       startup_length: starts.length


make a pointer when dropping to folder

make a pointer when dropping to folder

--- scripts/models/content/folder.coffee
+++ scripts/models/content/folder.coffee
@@ -54,7 +54,10 @@
       children = json.contents.map (id) =>
         model = allContent.get(id)
         throw new Error 'BUG: id not found when loading folder' if not model
-        return @newNode {model:model}
+        # Do not make a pointer to the content because it may be a book
+        # and the book needs to be able to expand its children (model.getChildren() needs to work)
+        #return @newNode {model:model}
+        return model
       @getChildren().reset(children, {parse:true})

       delete json.contents
@@ -105,3 +108,11 @@
       model = options.model
       node = new TocPointerNode {root:@, model:model, passThroughChanges:true}
       return node
+
+    addChild: (model, at) ->
+      # Dereference the pointer, create a new pointer, and **then** add it
+      model = model.dereferencePointer?() or model
+      # Do not make a pointer to the content because it may be a book
+      # and the book needs to be able to expand its children (model.getChildren() needs to work)
+      # model = @newNode {model:model}
+      super(model, at)


Add more logging for CF

Add more logging for CF

--- server/testSystems/Codeforces.coffee
+++ server/testSystems/Codeforces.coffee
@@ -173,10 +173,11 @@
             href = "#{BASE_URL}/#{contest}/submit"
             csrfHref = "#{BASE_URL}/#{contest}"
         else
-            href = "#{BASE_URL}/problemset/problem/#{contest}/#{problem}?csrf_token=#{csrf}"
+            href = "#{BASE_URL}/problemset/problem/#{contest}/#{problem}"
             csrfHref = href
         page = await @download(csrfHref)
         csrf = @_getCsrf(page)
+        logger.info "Found csrf=#{csrf}"
         data = {
                 csrf_token: csrf
                 ftaa: @ftaa
@@ -200,8 +201,10 @@
             timeout: 30 * 1000
         })
         if page.includes("You have submitted exactly the same code before")
+            logger.info "Submit is a duplicate"
             throw {duplicate: true}
         if not page.includes("Contest status")
+            logger.error "Can't submit"
             throw "Can't submit"
         logger.info "Apparently submitted!"


Fix: multiple ajax select

Fix: multiple ajax select

process only enabled inputs

--- vendor/assets/javascripts/auto_select2/multi_ajax_select2_value_parser.js.coffee
+++ vendor/assets/javascripts/auto_select2/multi_ajax_select2_value_parser.js.coffee
@@ -2,7 +2,7 @@
 # Tune input fields before submitting form, change csv values into input elements with array names
   $(document).on 'submit', 'form', ->
     $form = $(@)
-    $('input.auto-ajax-select2.multiple').each ->
+    $('input.auto-ajax-select2.multiple:enabled').each ->
       $multi = $(@)
       data = $multi.select2('data')
       name = $multi.attr('name')


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/active_bounds.coffee
+++ test/active_bounds.coffee
@@ -1,73 +1,5 @@
 module "ActiveBounds"

-test "#bounds returns correct x, y, width, height", ->
-  x = 5
-  y = 10
-  width = 50
-  height = 75
-
-  obj = GameObject
-    x: x
-    y: y
-    width: width
-    height: height
-
-  obj.include(Bounded)
-
-  equals obj.bounds().x, x - width/2
-  equals obj.bounds().y, y - height/2
-  equals obj.bounds().width, width
-  equals obj.bounds().height, height
-
-test "#centeredBounds returns correct x, y, xw, yx", ->
-  x = -5
-  y = 20
-
-  obj = GameObject
-    x: x
-    y: y
-    width: 100,
-    height: 200
-
-  obj.include(Bounded)
-  bounds = obj.centeredBounds()
-
-  equals bounds.x, x
-  equals bounds.y, y
-  equals bounds.xw, 100 / 2
-  equals bounds.yw, 200 / 2
-
-test "#bounds(width, height) returns correct x, y", ->
-  x = 20
-  y = 10
-  width = 15
-  height = 25
-
-  offsetX = 7.5
-  offsetY = 12
-
-  obj = GameObject
-    x: x
-    y: y
-    width: width
-    height: height
-
-  obj.include(Bounded)
-  bounds = obj.bounds(offsetX, offsetY)
-
-  equals bounds.x, obj.center().x + offsetX - width/2
-  equals bounds.y, obj.center().y + offsetY - height/2
-
-test "#center returns correct center point", ->
-  obj = GameObject
-    x: -5
-    y: 20
-    width: 10
-    height: 60
-
-  obj.include(Bounded)
-  center = obj.center()
-
-  ok center.equal(Point(-5, 20))
+test

 module()


Support both API versions

Support both API versions

Temp fix to handle both API versions,
while deploying the updated versions of all
necessary components.

--- src/actions_manager.coffee
+++ src/actions_manager.coffee
@@ -13,6 +13,13 @@

       @session = null
       @pageview_timeout = null
+
+      # Temp stuff
+      @temp_redirect_url = null
+      @temp_session_data =
+        account: null
+        session: null
+        user: null

       @_autoPageView() if Settings.send_auto_pageview

@@ -32,7 +39,29 @@
         @run('site', 'sendPageView')
       ), Settings.auto_pageview_timeout

+    _tempHandleSession: (type, data)=>
+      s = @temp_session_data
+      return unless s.account
+
+      if s.session
+        @_sessionAction 'create',
+          shop_code: s.account
+          yogurt_session: s.session
+          yogurt_user_id: s.user_id
+      else
+        @_sessionAction 'connect', {shop_code: s.account}
+
+
     _actions:
+      settings:
+        redirectTo: (url)-> @temp_redirect_url = url
+        setYogurtSession: (data)->
+          @temp_session_data.session = data
+        setYogurtUser: (data)->
+          @temp_session_data.user = data
+        setAccount: (data)->
+          @temp_session_data.account = data
+          setTimeout @_tempHandleSession, 500
       session:
         create: (shop_code, yogurt_session, yogurt_user_id)-> @_sessionAction 'create',
           shop_code: shop_code
@@ -43,7 +72,8 @@
         productClick: (data, redirect_url, redirect = true)->
           clearTimeout @pageview_timeout
           @_reportAction 'yogurt', 'productClick', data, (analytics_session)=>
-            @_redirect(redirect_url, analytics_session) if redirect_url and redirect
+            url = redirect_url or @temp_redirect_url
+            @_redirect(url, analytics_session) if url and redirect
       ecommerce:
         addOrder: (data, callback)->
           clearTimeout @pageview_timeout


improve configuration file

improve configuration file

--- test/config.public.coffee
+++ test/config.public.coffee
@@ -1,5 +1,5 @@
 module.exports =
-  user: 'user'
+  user: process.env.USER
   host: 'localhost'
   prefixPath: '/tmp/sshfs'
-  folderName: '/'+  folderName: ''


add ability to ignore specific flows in headless-test.js

--- test/headless-test.coffee
+++ test/headless-test.coffee
@@ -3,6 +3,7 @@
 timeout = null
 packs = null
 hostname = null
+excludeFlows = null
 perf = false
 date = null
 buildId = null
@@ -23,11 +24,13 @@
 printUsageAndExit = (message) ->
   console.log "*** #{message} ***"
   console.log 'Usage: phantomjs headless-test.js [--host ip:port] [--timeout seconds] [--packs foo:bar:baz] \
-                                                 [--perf date buildId gitHash gitBranch ncpu os jobName outputDir]'
-  console.log '    ip:port  defaults to localhost:54321'
-  console.log '    timeout  defaults to 3600'
-  console.log '    packs    defaults to examples'
-  console.log '    perf     performance of individual tests will be recorded in perf.csv in the output directory'
+                                                 [--perf date buildId gitHash gitBranch ncpu os jobName outputDir] \
+                                                 [--excludeFlows flow1;flow2]'
+  console.log '    ip:port      defaults to localhost:54321'
+  console.log '    timeout      defaults to 3600'
+  console.log '    packs        defaults to examples'
+  console.log '    perf         performance of individual tests will be recorded in perf.csv in the output directory'
+  console.log '    excludeFlows do not run these flows'
   phantom.exit 1

 parseOpts = (args) ->
@@ -72,6 +75,10 @@
       i = i + 1
       if i > args.length then printUsageAndExit "Unknown argument: #{args[i]}"
       outputDir = args[i]
+    else if args[i] == "--excludeFlows"
+      i = i + 1
+      if i > args.length then printUsageAndExit "Unknown argument: #{args[i]}"
+      excludeFlows = args[i]
     else
       printUsageAndExit "Unknown argument: #{args[i]}"
     i = i + 1
@@ -88,6 +95,11 @@
 console.log "PHANTOM: Using packs:"
 for packName in packNames
   console.log "PHANTOM: #{packName}"
+
+excludeFlowNames = if excludeFlows == null then [] else excludeFlows.split ';'
+console.log "PHANTOM: Excluding flows:"
+for excludeFlowName in excludeFlowNames
+  console.log "PHANTOM: #{excludeFlowName}"

 page = webpage.create()

@@ -126,7 +138,7 @@
   if status is 'success'
     test = ->
       page.evaluate(
-        (packNames, date, buildId, gitHash, gitBranch, hostname, ncpu, os, jobName, perf) ->
+        (packNames, date, buildId, gitHash, gitBranch, hostname, ncpu, os, jobName, perf, excludeFlowNames) ->
           window._date = date
           window._buildId = buildId
           window._gitHash = gitHash
@@ -136,6 +148,7 @@
           window._os = os
           window._jobName = jobName
           window._perf = perf
+          window._excludeFlowNames = excludeFlowNames
           context = window.flow.context
           if window._phantom_started_
             if window._phantom_exit_ then yes else no
@@ -159,51 +172,60 @@
                   (Flow.Async.iterate tasks) go

             runFlow = (packName, flowName, go) ->
-              flowTitle = "#{packName} - #{flowName}"
-              window._phantom_test_summary_[flowTitle] = 'FAILED'
-              console.log "Fetching flow document: #{packName} - #{flowName}..."
-              context.requestFlow packName, flowName, (error, flow) ->
-                if error
-                  console.log "*** ERROR *** Failed fetching flow #{flowTitle}"
-                  go new Error "Failed fetching flow #{flowTitle}", error
-                else
-                  console.log "Opening flow #{flowTitle}..."
-
-                  window._phantom_running_ = yes
-
-                  # open flow
-                  context.open flowTitle, flow
-
-                  waitForFlow = ->
-                    if window._phantom_running_
-                      console.log 'ACK'
-                      setTimeout waitForFlow, 2000
-                    else
-                      console.log 'Flow completed!'
-                      errors = window._phantom_errors_
-                      # delete all keys from the k/v store
-                      context.requestRemoveAll ->
-                        go if errors then errors else null
-
-                  console.log 'Running flow...'
-                  window._startTime = new Date().getTime() / 1000
-                  context.executeAllCells yes, (status, errors) ->
-                    window._endTime = new Date().getTime() / 1000
-                    console.log "Flow finished with status: #{status}"
-                    if status is 'failed'
-                      window._pass = 0
-                      window._phantom_errors_ = errors
-                    else
-                      window._pass = 1
-                      window._phantom_test_summary_[flowTitle] = 'PASSED'
-                    if window._perf
-                      window.callPhantom "#{window._date}, #{window._buildId}, #{window._gitHash}, \
-                                          #{window._gitBranch}, #{window._hostname}, #{flowName}, \
-                                          #{window._startTime}, #{window._endTime}, #{window._pass}, \
-                                          #{window._ncpu}, #{window._os}, #{window._jobName}\n"
-                    window._phantom_running_ = no
+              doFlow = (flowName, excludeFlowNames) ->
+                for f in excludeFlowNames
+                  return false if flowName is f
+                true
+
+              if doFlow flowName, window._excludeFlowNames
+                flowTitle = "#{packName} - #{flowName}"
+                window._phantom_test_summary_[flowTitle] = 'FAILED'
+                console.log "Fetching flow document: #{packName} - #{flowName}..."
+                context.requestFlow packName, flowName, (error, flow) ->
+                  if error
+                    console.log "*** ERROR *** Failed fetching flow #{flowTitle}"
+                    go new Error "Failed fetching flow #{flowTitle}", error
+                  else
+                    console.log "Opening flow #{flowTitle}..."
+
+                    window._phantom_running_ = yes
+
+                    # open flow
+                    context.open flowTitle, flow
+
+                    waitForFlow = ->
+                      if window._phantom_running_
+                        console.log 'ACK'
+                        setTimeout waitForFlow, 2000
+                      else
+                        console.log 'Flow completed!'
+                        errors = window._phantom_errors_
+                        # delete all keys from the k/v store
+                        context.requestRemoveAll ->
+                          go if errors then errors else null
+
+                    console.log 'Running flow...'
+                    window._startTime = new Date().getTime() / 1000
+                    context.executeAllCells yes, (status, errors) ->
+                      window._endTime = new Date().getTime() / 1000
+                      console.log "Flow finished with status: #{status}"
+                      if status is 'failed'
+                        window._pass = 0
+                        window._phantom_errors_ = errors
+                      else
+                        window._pass = 1
+                        window._phantom_test_summary_[flowTitle] = 'PASSED'
+                      if window._perf
+                        window.callPhantom "#{window._date}, #{window._buildId}, #{window._gitHash}, \
+                                            #{window._gitBranch}, #{window._hostname}, #{flowName}, \
+                                            #{window._startTime}, #{window._endTime}, #{window._pass}, \
+                                            #{window._ncpu}, #{window._os}, #{window._jobName}\n"
+                      window._phantom_running_ = no

                   setTimeout waitForFlow, 2000
+              else
+                console.log "Ignoring flow: #{flowName}"
+                go null

             console.log 'Starting tests...'
             window._phantom_errors_ = null
@@ -216,7 +238,7 @@
                 console.log 'Finished running all packs!'
               window._phantom_exit_ = yes
             no
-        packNames, date, buildId, gitHash, gitBranch, hostname, ncpu, os, jobName, perf
+        packNames, date, buildId, gitHash, gitBranch, hostname, ncpu, os, jobName, perf, excludeFlowNames
       )

     printErrors = (errors, prefix='') ->


don't force a render on change

don't force a render on change

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -789,7 +789,7 @@
                 @controls.autoRotateSpeed = @opts.spin
             @controls.autoRotate = true

-        @controls.addEventListener 'change', => @renderScene true
+        @controls.addEventListener 'change', => @renderScene()

     # on mouseover, we switch the renderer out to use the dynamic renderer
     setOnMouseOver: ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/matrix.coffee
+++ src/matrix.coffee
@@ -178,9 +178,12 @@
     ###
     scale: (sx, sy, aboutPoint) ->
       @concat(Matrix.scale(sx, sy, aboutPoint))
-
+
+    ###*
+    ###
     skew: (skewX, skewY) ->
       @concat(Matrix.skew(skewX, skewY))
+
     ###*
     Returns a string representation of this matrix.


matrix

matrix

--- src/matrix.coffee
+++ src/matrix.coffee
@@ -149,11 +149,9 @@
     @name rotate
     @methodOf Matrix#
     @see Matrix.rotation
-
     @param {Number} theta Amount to rotate in radians.
     @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0).
-    @returns A new matrix, rotated by the specified amount.
-    @type Matrix
+    @returns {Matrix} A new matrix, rotated by the specified amount.
     ###
     rotate: (theta, aboutPoint) ->
       @concat(Matrix.rotation(theta, aboutPoint))


Require jQuery sortable for latest ActiveAdmin

ActiveAdmin's master branch no longer include the entire jQuery library.
The nested sortable JS library depends on jQuery's sortable library.

--- app/assets/javascripts/active_admin/sortable.js.coffee
+++ app/assets/javascripts/active_admin/sortable.js.coffee
@@ -1,3 +1,4 @@
+#= require jquery.ui.sortable
 #= require jquery.mjs.nestedSortable

 window.ActiveAdminSortableEvent = do ->


Clear filterSubcategory flag on cards/received

--- app/assets/javascripts/lib/components/filter.js.coffee
+++ app/assets/javascripts/lib/components/filter.js.coffee
@@ -20,6 +20,9 @@

     # Subscribe
     listen: ->
+
+      $(LISTENER).on ':cards/received', (e, data) =>
+        @_clearFilterSubcategory()

       $(LISTENER).on ':page/received', (e, data) =>
         @_clearFilterSubcategory()


Tests: Update target length in rewind buffer test

Now that chunk durations won't always be over-target, allow for the data
returned to float a little more around our intended size.

--- test/rewind_buffer.coffee
+++ test/rewind_buffer.coffee
@@ -132,7 +132,8 @@

                 t_len = 0
                 t_len += b.data.length for b in mock.buffer
-                expect(t_len).to.be.within 64000, 65000
+                # 64Kb/sec audio, so 8 sec should be ~64KB. Unit for length is bytes
+                expect(t_len).to.be.within 63000, 65000

                 done()

@@ -194,5 +195,3 @@
                 pos += chunk

             pt.end()
-
-


Remove console.log

Remove console.log

--- test/selector.spec.coffee
+++ test/selector.spec.coffee
@@ -103,7 +103,6 @@

     test 'not', ->
         list = list_items()
-        console.log(list.not('.a'))
         assert.equal list.not('.a').length , 2, "Removes .a element"

         doc = $(document)


payment: WIP: don't show controls on the summary view

this is a bit of a hack, but I don't want to spend a lot of time on this until a front-end engineer has the chance to take a look at it, since I might be wasting my time for now

--- client/app/Applications/Environments.kdapplication/views/vms/vmproductview.coffee
+++ client/app/Applications/Environments.kdapplication/views/vms/vmproductview.coffee
@@ -1,9 +1,16 @@
 class VmProductView extends KDListItemView

+  shouldShowControls: ->
+    @getOptions().showControls ? yes
+
   viewAppended: ->
+    options = @getOptions()
+
     @chooseButton = new KDButtonView
       title     : 'Create VM'
       callback  : => @emit 'PackSelected'
+
+    @chooseButton.hide()  unless @shouldShowControls()

     JView::viewAppended.call this


Teamwork: Added invite modal to includes. An error check for CW user list.

--- client/landing/CollaborativeWorkspace/views/collaborativeworkspaceuserlist.coffee
+++ client/landing/CollaborativeWorkspace/views/collaborativeworkspaceuserlist.coffee
@@ -187,6 +187,7 @@

   close: ->
     container = @container
+    return  unless container
     container.unsetClass "active"
     container.once "transitionend", =>
       container.destroySubViews()


Provide errorCallback to aggregate.create

--- src/command_service.coffee
+++ src/command_service.coffee
@@ -42,7 +42,15 @@
     aggregate.initialize()

     if typeof aggregate.create == 'function'
-      aggregate.create props
+      # TODO: Should be ok as long as aggregates arent async
+      errorCallbackCalled = false
+      errorCallback = (err) =>
+        errorCallbackCalled = true
+        callback err
+
+      aggregate.create props, errorCallback
+
+      return if errorCallbackCalled
     else
       aggregate.applyProps props

@@ -77,7 +85,7 @@
       if not (params instanceof Array)
         params = [params]

-      # Should be ok because aggregates shouldn't be async!
+      # TODO: Should be ok as long as aggregates arent async
       errorCallbackCalled = false
       errorCallback = (err) =>
         errorCallbackCalled = true


More natural promise invocation + combination with Q

--- api.coffee
+++ api.coffee
@@ -17,8 +17,7 @@
     throw "must provide db connection" unless @db

   createUser: (name) ->
-    create = Q.nbind(User.create, User)
-    create(name: name)
+    Q(User.create(name: name).exec())
     .then (val) ->
       bus.emit('user:created', val)
       val
@@ -26,8 +25,7 @@
   listUsers: (userId) ->
     if userId?
       query = _id: ObjectID(userId)
-    find = Q.nbind(User.find, User)
-    find(query)
+    Q(User.find(query).exec())

   listCompetitionsByOwner: (ownerId) ->
     if not userId?
@@ -38,43 +36,43 @@
       return errPromise

     query = owner_id: ObjectID(userId)
-    find = Q.nbind(Competition.find, Competition)
-    find(query)
+    Q(Competition.find(query).exec())

   listCompetitions: ->
-    Q.nbind(Competition.find, Competition)()
+    Q(Competition.find().exec())

   listCompetitionsByMembership: (userId) ->

   startCompetition: (userId, name) ->
-    create = Q.nbind(Competition.create, Competition)
-    create(
-      name: name
-      owner_id: userId
-      open: true
-      type: 'multi'
-      participants: [ObjectID(userId)]
+    Q(
+      Competition.create(
+        name: name
+        owner_id: userId
+        open: true
+        type: 'multi'
+        participants: [ObjectID(userId)]
+      ).exec()
     ).then (competition) ->
       bus.emit('competition:started', competition)
       competition

   endCompetition: (competitionId) ->
-    update = Q.nbind(Competition.findOneAndUpdate, Competition)
-    update(
-      { _id: competitionId },
-      { open: false }
+    Q(
+      Competition.findOneAndUpdate(
+        { _id: competitionId },
+        { open: false }
+      )
     ).then (competition) ->
       bus.emit('competition:ended', competition)
       competition

   voteFor: (userId, competitionId, choiceId) ->
-    update = Q.nbind(@Vote.findByIdAndUpdate, @Vote)
     query =
       user_id: userId
     toUpdate =
       choice_id: choiceId

-    update(query, toUpdate)
+    Q(Vote.findByIdAndUpdate(query, toUpdate).exec())
     .then (vote) ->
       bus.emit('vote:cast', vote)
       vote


More work on the Anchor Tab loader.

More work on the Anchor Tab loader.

--- src/main/webapp/coffeescripts-hidden/load.coffee
+++ src/main/webapp/coffeescripts-hidden/load.coffee
@@ -5,32 +5,44 @@
 All Rights Reserved
 ###

-includeAnalyticsIfNeeded = ->
+loadScript = (url, callback) ->
+  script = document.createElement("script")
+  script.async = true
+  script.type = "text/javascript"
+
+  if script.readyState # IE
+    script.onreadystatechange = ->
+      if script.readyState == "loaded" || script.readyState == "complete"
+        script.onreadystatechange = null
+        callback()
+  else # Others
+    script.onload = ->
+      callback();
+
+  script.src = url
+  document.getElementsByTagName("head")[0].appendChild(script)
+
+withGoogleAnalytics = (callback) ->
   if ! _gaq
-    # Ported from Google's embed code.
-    ga = document.createElement 'script'
-    ga.type = 'text/javascript'
-    ga.async = true
-    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
-    s = document.getElementsByTagName 'head'
-    s[0].appendChild ga
+    analyticsSrc = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'
+    loadScript(analyticsSrc, callback)
+  else
+    callback()

-includeJQueryIfNeeded = ->
+withJQueryLoaded = (callback) ->
   if ! jQuery
-    # Pull down jQuery from GooleAPIs
-    jqueryUri = "http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"
+    loadScript("http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js", callback)
+  else if ! /^1.[678]/.test(jQuery.fn.jquery)
+    console?.error("AnchorTab is disabled because you are using an unsupported version of jQuery.")

-    jq = document.createElement 'script'
-    jq.type = 'text/javascript'
-    jq.src = jqueryUri
-    s = document.getElementsByTagName 'head'
-    s[0].appendChild jq
-    true
-  else if ! /^1.[678]/.test(jQuery.fn.jquery)
-    console?.error("AnchorTab is disabled. Please update your version of jQuery.")
-    false
+    # Track this on GA so we can see if we need to adjust this value over time.
+    withGoogleAnalytics ->
+      _gaq.push ["anchortab._trackEvent", "Bootstrap", "Incompatible jQuery Failure", jQuery.fn.jquery]

-anchorTabLoader = ->
+  else
+    callback()
+
+loadAnchorTab = ->
   # Load the anchor tab. At this point we can assume that jQuery exists and that we're able to use it
   # safely without things getting hairy. If we find out in the future that things are wonky about that
   # then we'll have to refactor some of this code, I suppose.
@@ -60,12 +72,11 @@
 # are in place.
 oldOnload = window.onload
 window.onload = ->
-  includeAnalyticsIfNeeded()
+  withGoogleAnalytics ->
+    _gaq.push ["anchortab._setAccount", "UA-35269224-2"]
+    _gaq.push ["anchortab._setDomainName", "embed.anchortab.com"]
+    _gaq.push ["anchortab._trackPageview"]

-  # We only proceed to load the tab if the jQuery load was successful.
-  # If we found an ancient version of jQuery on the page, we bunk and don't
-  # load the tab.
-  if includeJQueryIfNeeded()
-    anchorTabLoader()
+  withJQueryLoaded(loadAnchorTab)

   oldOnload?()


Update common.coffee

Update common.coffee

--- src/fs_utils/common.coffee
+++ src/fs_utils/common.coffee
@@ -37,7 +37,7 @@
       write (error) ->
         callback error, path, data

-# RegExp that would filter invalid files (dotfiles, emacs caches etc).
+# RegExp that filters out invalid files (dotfiles, emacs caches etc).
 exports.ignored = ignored = do ->
   re1 = /\.(?!htaccess|rewrite)/
   re2 = /(^[.#]|(?:__|~)$)/
@@ -45,7 +45,7 @@
     base = sysPath.basename path
     re1.test(base) and re2.test(base)

-# Files that should be always ignored (git / mercurial metadata etc).
+# Files that should always be ignored (git / mercurial metadata etc).
 exports.ignoredAlways = ignoredAlways = (path) ->
   /^\.(git|hg)$/.test sysPath.basename path


Process entire document if mathml is found

If the mathml element's parent is shared with other mathml elments then
it may be processed repeatedly, leading to the elements having zero
height or width set on their inline styles.

This switches to simply processing the entire document when any `mathml`
elments are found.

--- src/helpers/mathjax.coffee
+++ src/helpers/mathjax.coffee
@@ -8,13 +8,10 @@
 MATH_ML_SELECTOR   = "math:not(.#{MATH_RENDERED_CLASS})"
 COMBINED_MATH_SELECTOR = "#{MATH_DATA_SELECTOR}, #{MATH_ML_SELECTOR}"

-setAsRendered = (node, type = 'mathjax') ->
-  node.classList.add("#{type}-rendered")
-  node.classList.add(MATH_RENDERED_CLASS)
-
 # Search document for math and [data-math] elements and then typeset them
 typesetDocument = ->
   allNodes = []
+
   for node in document.querySelectorAll(MATH_DATA_SELECTOR)
     formula = node.getAttribute('data-math')
     # divs should be rendered as a block, others inline
@@ -23,19 +20,23 @@
     else
       node.textContent = "#{MATH_MARKER_INLINE}#{formula}#{MATH_MARKER_INLINE}"
     allNodes.push(node)
-  # Mathjax doesn't typeset a element when it's passed one directly
-  # It will only render child elements
-  allNodes = allNodes.concat(
-    _.pluck(document.querySelectorAll(MATH_ML_SELECTOR), 'parentNode')
-  )
-  window.MathJax.Hub.Typeset( allNodes )
+
+  mathMLNodes = _.toArray(document.querySelectorAll(MATH_ML_SELECTOR))
+  hasMathML = not _.isEmpty(mathMLNodes)
+  allNodes = allNodes.concat(mathMLNodes)
+
+  # MathJax sometimes fails to set the height/width properly when processing individual mathml elements
+  # If we have any of those nodes then mark the entire document for processing
+  window.MathJax.Hub.Typeset( if hasMathML then document.body else allNodes )
   window.MathJax.Hub.Queue ->
+    # Queue a call to mark the found nodes as rendered so
+    # they can be ignored if typesetting is called repeatedly
     for node in allNodes
-      setAsRendered(node)
+      node.classList.add(MATH_RENDERED_CLASS)

 # Install a debounce around typesetting function so that it will only run once
-# every 10ms even if called multiple calls times in that period
-typesetDocument = _.debounce( typesetDocument, 10)
+# every 10ms even if called multiple times in that period
+typesetDocument = _.debounce( typesetDocument, 100)

 # typesetMath is the main exported function.


Add position, activeBefore and activeAfter attributes

--- src/angular-moment-timepicker.coffee
+++ src/angular-moment-timepicker.coffee
@@ -4,11 +4,10 @@
     A highly extensible date time picker directive.
 ###
 module = angular.module 'ui.bootstrap.moment.datetimepicker', []
-
 #
 # Contain default parameters for the directive
 #
-module.constant('dateTimePickerConfig', {
+module.constant 'dateTimePickerConfig', {
   year:
     format: 'YYYY'
     first: (date) ->
@@ -64,27 +63,49 @@
     step: moment.duration(5, 'second')
     previous: moment.duration(-1, 'hour')
     next: moment.duration(1, 'hour')
-})
+}

 #
 # moment datetimepicker directive.
 #
-module.directive 'momentDatetimepicker', ->
+module.directive 'momentDatetimepicker', ['dateTimePickerConfig', (defaultConfig)->
   return {
   scope: {
-    mdFormat: '&'     # moment format string use to format the current date.
     minDate: '=?'     # minimum selectionable date.
     maxDate: '=?'     # maximum selectionable date.
     currentView: '=?' #
+    mdFormat: '&'     # moment format string use to format the current date.
     commitView: '&'   #
     afterSelect: '&'
+    tokens: '&'
+    position: '=?'    # Default to ng-model.
+    selected: '=?'    # Hold the selection until commit.
+    activeBefore: '=?'
+    activeAfter: '=?'
   }
   require: 'ngModel'
   replace: true
   link: (scope, elm, attr, ngModel)->
-    scope.currentView = if scope.currentView? then 'year'
-    console.log scope
-
+    scope.currentView = 'year' if not scope.currentView?
+
+    # TODO: It might be possible to dynamicaly link pos and model using a watch on pos only if undefined.
+    # TODO: Maybe a better aproach is to have a setting attribute like "follow".
+    # TODO: Same thing with the commit thing.
+    followDate = false
+
+    #
+    # Used to calculate the next values
+    #
+    steps = defaultConfig
+
+    # This should be calculated D. M YYYY
+    scope.tokens = if not scope.tokens()? then [
+      {format: 'd', view: 'day'}
+      {format: '. '}
+      {format: 'MMMM', view: 'month'}
+      {format: ' '}
+      {format: 'yyyy', view: 'year'}
+    ] else scope.tokens()

     #
     # Helper functions returning the durations
@@ -112,12 +133,7 @@

     #ngModel.$setViewValue(newValue);
     ngModel.$render = ->
-      scope.date = moment(ngModel.$modelValue) || moment()
-
-    views = [
-      'year', 'month', 'day'
-      'hour', 'minute', 'second'
-    ]
+      scope.date = moment(ngModel.$modelValue)

     afterSelect = angular.noop

@@ -146,12 +162,7 @@
       'hour', 'minute', 'second'
     ]

-    #
-    # Used to calculate the next values
-    #
-    steps =
-
-      scope.dow = moment.weekdaysShort()
+    scope.dow = moment.weekdaysShort()

     scope.selectDate = (date, event) ->
       if event?
@@ -188,16 +199,6 @@
       date.add(offset)
       scope.setDate(date)

-    scope.currentView = 'month'
-
-    # This should be calculated D. M YYYY
-    scope.tokens = [
-      {format: 'd', view: 'day'}
-      {format: '. '}
-      {format: 'MMMM', view: 'month'}
-      {format: ' '}
-      {format: 'yyyy', view: 'year'}
-    ]

     ###
       Helper method that split the steps by week
@@ -213,7 +214,23 @@
         stepsByWeek[stepsByWeek.length - 1].push step
       return stepsByWeek

-    scope.$watch('currentView + date + minDate + maxDate + currentView', ->
+    scope.$watch('date', ->
+      scope.position = scope.date if followDate
+      console.log "new date!"  if followDate
+    )
+
+    scope.$watch('activeBefore + activeAfter + currentView + position', ->
+      # need to update the steps.
+      return if not scope.steps? or not (scope.activeBefore? or scope.activeAfter?)
+
+      for step in scope.steps
+        date = moment(step.value)
+        isBefore = if scope.activeBefore? then date.isBefore(scope.activeBefore) else true
+        isAfter = if scope.activeAfter? then date.isAfter(scope.activeAfter) else true
+        step.active = isBefore and isAfter
+    )
+
+    scope.$watch('currentView + position + minDate + maxDate', ->
       # Get the starting date.
       step = steps[scope.currentView]
       stepDate = step.first(moment(scope.date))
@@ -225,6 +242,7 @@

       scope.steps = for i in [1..amount]

+        # TODO: Use the step to make the comparison!
         before = if scope.minDate? then stepDate.isBefore(scope.minDate, scope.currentView) else true
         after = if scope.maxDate? then stepDate.isAfter(scope.maxDate, scope.currentView) else true
         same = stepDate.isSame(scope.date, scope.currentView)
@@ -238,7 +256,7 @@
           value: stepDate.toDate()
         }
         stepDate = moment(stepDate).add(period)
-        newDate
+        newDate # Important!

       scope.stepsByWeek = splitByWeeks(scope.steps)

@@ -336,14 +354,14 @@

-
+
                  {{ date.toDate() | date:token.format }}
                  {{ token.value }}

-

+

            {{ day }}

@@ -369,4 +387,4 @@

   }
-
+]


empty the stylesheet before replacing it

--- src/client/style/style-scoper.coffee
+++ src/client/style/style-scoper.coffee
@@ -8,6 +8,7 @@

 CSS_URL_REGEXP = /(url\()([^)]*)(\))/g;
 CSS_IMPORT_REGEXP = /(@import[\s]+(?!url\())([^;]*)(;)/g;
+
 resolveUrlsInStyle= (style, linkUrl)->
   doc = style.ownerDocument
   resolver = doc.createElement("a")
@@ -42,6 +43,7 @@
     tag.styleSheet.cssText = content;
   else
     textnode = document.createTextNode(content);
+    tag.removeChild(tag.firstChild) while tag.firstChild
     tag.appendChild(textnode);

 createStyleSheet = (content, disabled=false)->


Added code comments and moved the URL so it would be explicit

--- scripts/helpers/logger.coffee
+++ scripts/helpers/logger.coffee
@@ -1,8 +1,11 @@
+# This file takes every `Backbone.Event` and POSTs it to the URL `/logging`
 define [
   'jquery'
   'underscore'
   'backbone'
 ], ($, _, Backbone) ->
+
+  LOGGING_URL = '/logging'

   isEnabled = true

@@ -11,7 +14,7 @@

     if isEnabled
       log = "trigger: #{ args }"
-      $.post('/logging', log)
+      $.post(LOGGING_URL, log)

     originalTrigger.apply(@, args)


display known asteroid message in summary (only if asteroids exist)

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -492,10 +492,12 @@

   showSummary: ->
     @resetMarkingSurfaces() # remove previous marks
-    @knownAsteroidMessage.hide()
     @removeKnownAsteroids()
-    return if @Subject.current.metadata.known_asteroids is undefined
-    @knownAsteroidMessage.show()
+    console.log "known asteroids: ", @Subject.current.metadata.known_asteroids
+    if @Subject.current.metadata.known_asteroids.length isnt 0
+      @knownAsteroidMessage.show()
+    else
+      @knownAsteroidMessage.hide()
     for knownAsteroid in [@Subject.current.metadata.known_asteroids...]
       xs = []
       ys = []
@@ -510,6 +512,8 @@
         x_sum += xs[frame]
         y_sum += ys[frame]
       console.log "frame image: ", @summaryImageContainer.find("frame-image")
+      # TODO: remove hardwired width
+      # should be changed to @surface.el.offsetWidth, as in marking-tool
       x_avg = Math.round(x_sum/@numFrames)/512 * 190
       y_avg = Math.round(y_sum/@numFrames)/512 * 190
       P = {x: x_sum, y: y_sum}


Account for missing error arguments.

Account for missing error arguments.

--- src/prompt.coffee
+++ src/prompt.coffee
@@ -55,7 +55,7 @@
     async.waterfall [
       (done) =>
         async.each @_orderedSections, (section, complete) =>
-          @_content section, (content) ->
+          @_content section, (err, content) ->
             section._formattedContent = content
             complete()
         , done
@@ -109,7 +109,7 @@
         else
           results = results.join ''

-        done results.toString()
+        done null, results.toString()
     ], (err, results) ->
       # If `section.when` fails, just pass along blank content.
       if err instanceof WhenError


Remove logging

Remove logging

--- app/routes/bookmarks_route.coffee
+++ app/routes/bookmarks_route.coffee
@@ -1,5 +1,2 @@
 module.exports = App.BookmarksRoute = Ember.Route.extend
-  model: ->
-    console.log 'before'
-    console.log @store
-    @store.find('bookmark')
+  model: -> @store.findAll('bookmark')


add todo to cosmo.coffee

add todo to cosmo.coffee

--- cosmo.coffee
+++ cosmo.coffee
@@ -2,6 +2,7 @@
   version: '0.1.0'

 # All apps should be an extended instance of this class
+# TODO: rewrite this as an object
 class Cosmo.Router
   # app regions, assumes main content goes in a div#container
   regions:


fix practice route

fix practice route

--- src/router.coffee
+++ src/router.coffee
@@ -16,7 +16,7 @@
     <Route path='dashboard/?' name='dashboard' handler={Dashboard} />
     <Route path='courses/:courseId/tasks/?' name='listTasks' handler={Tasks} />
     <Route path='courses/:courseId/tasks/:id/?' name='viewTask' handler={SingleTask} />
-    <Route path='courses/:courseId/practice/' name='viewPractice' handler={SinglePractice} />
+    <Route path='courses/:courseId/practice/?' name='viewPractice' handler={SinglePractice} />
     <Route path='courses/:courseId/readings/?' name='taskplans' handler={TeacherTaskPlans} />
     <Route path='courses/:courseId/readings/new/?' name='createReading' handler={ReadingShell} />
     <Route path='courses/:courseId/readings/:id/?' name='editReading' handler={ReadingShell} />


Add router

Add router

--- src/router.coffee
+++ src/router.coffee
@@ -1,4 +1,4 @@
-define 'milksugar/router', ['root'], ->
+define ['root'], ->

   class Router
     constructor: (routes) ->


Refactor plugin code; parametrize container selection and refresh

--- src/sample.coffee
+++ src/sample.coffee
@@ -44,17 +44,19 @@
 maxDepth = (node) ->
   return 0 if not node
   return 1 if not node.children
+  return node.maxDepth if node.maxDepth

   max = 0
   node.children.forEach (child) ->
     depth = maxDepth(child)
     max = depth if depth > max

-  return max + 1
+  node.maxDepth = max + 1
+  return node.maxDepth

-getLastTwoTokens = (str) ->
-  tokens = str.split(".")
+getClassAndMethodName = (fqdn) ->
+  tokens = fqdn.split(".")
   tokens.slice(tokens.length - 2).join(".")

 d3.flameGraph = ->
@@ -65,9 +67,16 @@
         'width',
         'height',
         'cellHeight',
-        'containerId',
+        'containerSelector',
         'colorScheme'])
       @_allData = []
+
+    container: (selector) ->
+      return @_container if not selector
+      outerContainer = d3.select(selector)
+      outerContainer.select('svg').remove()
+      @_container = outerContainer.append('svg')
+      @_container

     data: (data) ->
       return @_data if not data
@@ -78,57 +87,33 @@
         .nodes(data)
       @

-    breadcrumbs: () ->
-      breadcrumbData = @_allData.map((prevData, idx) -> { name: prevData.name, value: idx })
-      breadcrumbs = d3.select('.breadcrumb')
-        .selectAll('li')
-        .data(breadcrumbData)
-
-      breadcrumbs.enter()
-        .append('li')
-          .append('a')
-          .text((d) -> getLastTwoTokens(d.name))
-          .on 'click', (breadcrumb) =>
-            idx = breadcrumb.value
-            displayed = @_allData[idx]
-            @_allData = @_allData.slice(0, idx)
-            @data(displayed).render()
-
-      breadcrumbs.exit().remove()
-      @
-
-    getLabelText: (label, dx) ->
-      return "" if not label
-      shortLabel = getLastTwoTokens(label)
-      maxLength = Math.round(@x(dx) / 4)
-      return shortLabel.substr(0, maxLength)
+    label: (d) ->
+      return "" if not d.name
+      label = getClassAndMethodName(d.name)
+      label.substr(0, Math.round(@x(d.dx) / 4))

     color: (d) -> @colorScheme()[Math.floor(Math.random() * @colorScheme().length)]

     render: () ->
-      # compute height dynamically so the fixed unit is the height of a cell
-      console.time('rendering')
+      console.time('render')

-      d3.select(@containerId())
-        .select('svg').remove()
+      # refresh container
+      @container(@containerSelector())
+        .attr('width', @width())
+        .attr('height', @height())

-      @container = d3.select(@containerId())
-        .append('svg')
-          .attr('width', @width())
-          .attr('height', @height())
+      @maxCells = Math.floor(@height() / @cellHeight())
+      @maxDepth = maxDepth(@data()[0])

-      @maxCells    = Math.floor(@height() / @cellHeight())
-
-      depth = maxDepth(@data()[0])
       @x = d3.scale.linear()
         .domain([0, d3.max(@data(), (d) -> d.x + d.dx)])
         .range([0, @width()])
       @y = d3.scale.quantize()
         .domain([d3.max(@data(), (d) -> d.y), 0])
-        .range(d3.range(depth)
-          .map((cell) =>  (cell  - depth + @maxCells) * @cellHeight()))
+        .range(d3.range(@maxDepth)
+          .map((cell) =>  (cell - @maxDepth + @maxCells) * @cellHeight()))

-      @container
+      @container()
         .selectAll('.node')
         .data(@data().filter((d) =>
           @x(d.dx) > 0.1 and @y(d.y) >= 0))
@@ -139,11 +124,11 @@
             .attr('height', (d) => @cellHeight() - 2)
             .attr('x', (d) => @x(d.x))
             .attr('y', (d) => @y(d.y))
-            .attr('stroke', "#bd0026")
-            .attr('fill', (d) => console.log(@color(d)); @color(d))
+            .attr('stroke', @colorScheme()[@colorScheme().length - 1])
+            .attr('fill', (d) => @color(d))
             .attr('fill-opacity', '0.8')

-      @container
+      @container()
         .selectAll('.label')
         .data(@data().filter((d) => d.name and @x(d.dx) > 40))
         .enter()
@@ -153,24 +138,39 @@
             .attr('dy', '.25em')
             .attr('x', (d) => @x(d.x) + @x(d.dx) / 2)
             .attr('y', (d) => @y(d.y) + @cellHeight() / 2)
-            .text((d) => @getLabelText(d.name, d.dx))
+            .text((d) => @label(d))

-      console.timeEnd('rendering')
-      console.log("Rendered #{@container.selectAll('.node')[0].length} elements")
-      return @breadcrumbs().interactivity()
+      console.timeEnd('render')
+      console.log("Rendered #{@container().selectAll('.node')[0].length} elements")
+      return @_breadcrumbs()._interactivity()

-    interactivity: () ->
-      console.time('interactivity')
-      clickHandler = (d) => @data(d).render()
+    _breadcrumbs: () ->
+      breadcrumbData = @_allData.map((prevData, idx) -> { name: prevData.name, value: idx })
+      breadcrumbs = d3.select('.breadcrumb')
+        .selectAll('li')
+        .data(breadcrumbData)

-      @container
+      breadcrumbs.enter()
+        .append('li')
+          .append('a')
+            .attr('title', (d) -> d.name)
+            .text((d) -> getClassAndMethodName(d.name))
+            .on 'click', (breadcrumb) =>
+              idx = breadcrumb.value
+              displayed = @_allData[idx]
+              @_allData = @_allData.slice(0, idx)
+              @data(displayed).render()
+
+      breadcrumbs.exit().remove()
+      @
+
+    _interactivity: () ->
+      @container()
         .selectAll('.node')
-        .on 'click', clickHandler
-
-      @container
+        .on 'click', (d) => @data(d).render()
+      @container()
         .selectAll('.label')
-        .on 'click', clickHandler
-      console.timeEnd('interactivity')
+        .on 'click', (d) => @data(d).render()
       @

     _generateAccessors: (accessors) ->
@@ -186,7 +186,7 @@
 d3.json "data/profile-large.json", (err, data) ->
   window.flameGraph = d3.flameGraph()
   flameGraph
-    .containerId('#d3-flame-graph')
+    .containerSelector('#d3-flame-graph')
     .width(1200).height(600).cellHeight(10)
     .data(convert(data.profile))
     .colorScheme(["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026"])