Added caching of tickets.

Added caching of tickets.

--- app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
@@ -23,9 +23,13 @@
     @navupdate '#'

     @edit_form = undefined
-#    @render()
     @ticket_id = params.ticket_id
     @article_id = params.article_id
+
+    @key = 'ticket::' + @ticket_id
+    cache = App.Store.get( @key )
+    if cache
+      @load(cache)
     @fetch(@ticket_id)

   fetch: (ticket_id) ->
@@ -40,25 +44,29 @@
       }
       processData: true,
       success: (data, status, xhr) =>
-        # reset old indexes
-        @ticket = undefined
-        @articles = undefined
-
-        # get edit form attributes
-        @edit_form = data.edit_form
-
-        # load user collection
-        @loadCollection( type: 'User', data: data.users )
-
-        # load ticket collection
-        @loadCollection( type: 'Ticket', data: [data.ticket] )
-
-        # load article collections
-        @loadCollection( type: 'TicketArticle', data: data.articles || [] )
-
-        # render page
-        @render()
-    )
+        @load(data)
+        App.Store.write( @key, data )
+    )
+
+  load: (data) =>
+    # reset old indexes
+    @ticket = undefined
+    @articles = undefined
+
+    # get edit form attributes
+    @edit_form = data.edit_form
+
+    # load user collection
+    @loadCollection( type: 'User', data: data.users )
+
+    # load ticket collection
+    @loadCollection( type: 'Ticket', data: [data.ticket] )
+
+    # load article collections
+    @loadCollection( type: 'TicketArticle', data: data.articles || [] )
+
+    # render page
+    @render()

   render: =>


fix issue #444

fix issue #444

--- app/assets/javascripts/locomotive/views/shared/form_view.js.coffee
+++ app/assets/javascripts/locomotive/views/shared/form_view.js.coffee
@@ -82,7 +82,14 @@
         content.slideUp 100, -> parent.addClass('folded')

   enable_save_with_keys_combination: ->
-    $.cmd 'S', (() => @$('form input[type=submit]').trigger('click')), [], ignoreCase: true
+    $.cmd 'S', (() =>
+      # make sure that the current text field gets saved too
+      input = @$('form input[type=text]:focus, form input[type=password]:focus')
+      input.trigger('change') if input.size() > 0
+
+      @$('form input[type=submit]').trigger('click')
+    ), [], ignoreCase: true
+

   enable_form_notifications: ->
     @$('form').formSubmitNotification()


markdown input: made setDomElement less retarded

--- components/inputs/KDInputViewWithPreview.coffee
+++ components/inputs/KDInputViewWithPreview.coffee
@@ -242,11 +242,11 @@
      scrollTop : ((s[0].scrollHeight - s.height())*percentage/100)
     , 50, "linear"

-  setDomElement:(CssClass="")->
-    super CssClass
-
-    @$().after """
-

+  setDomElement:(cssClass="")->
+    @inputName = @getOptions().name
+    name = "name='#{@inputName}'"
+    @domElement = $ """<textarea #{name} class='kdinput text #{cssClass}'></textarea>
+

"""


kdinputvwithprev generates preview when edit mode is used in the widget, status update snapshot/CD text preview is identical, tray dot gets rendered correctly for tutorial snapshot action links,  added tooltip to fullscreen button in widget view, changed tutorial modal text

--- components/inputs/KDInputViewWithPreview.coffee
+++ components/inputs/KDInputViewWithPreview.coffee
@@ -96,6 +96,10 @@
     if @options.preview.mirrorScroll then @$().scroll (event)=>
       @setPreviewScrollPercentage @getEditScrollPercentage()

+  setValue:(value)->
+    super value
+    @generatePreview()
+
   generatePreview:=>
     if @showPreview
       if @options.preview.language is "markdown"


[#88724] Ignore rails hidden form fields when deciding when to disable the submit button

Proper behavior: if any visible/non rails input (utf8, _method) is active, then
the submit button should be enabled. If they are all disabled, then the submit
button should be disabled as well.

After the recent change where we don't disable the hidden utf8, etc fields, the
submit button was always visible.

--- app/assets/javascripts/app/manage_orders.coffee
+++ app/assets/javascripts/app/manage_orders.coffee
@@ -112,8 +112,11 @@
       leaveEnabled = $(this).hasClass('js-always-enabled') || $(this).is('[type=submit]') || obj.isRailsFormInput(this)
       !leaveEnabled

-    # remove the submit button if all form elements are disabled
-    any_enabled = form_elements.filter(':not([type=submit])').is(':not(:disabled)')
+    # remove the submit button if all form elements are disabled (and ignore
+    # Rails hidden inputs)
+    any_enabled = form_elements.filter(':not([type=submit])')
+      .filter(":not(#{@railsFormInputSelector})")
+      .is(':not(:disabled)')
     form_elements.filter('[type=submit]').remove() unless any_enabled

   initReconcileNote: ->
@@ -140,7 +143,9 @@
       $(this).closest('.control-group').find('.account-owner').text(owner_name)

   isRailsFormInput: (input) ->
-    $(input).is("[name=_method],[name=utf8],[name=authenticity_token]")
+    $(input).is(@railsFormInputSelector)
+
+  railsFormInputSelector: "[name=_method],[name=utf8],[name=authenticity_token]"

 $ ->
   prepareForm = ->


opinion comment box reverted to original, logging removed, reply to opinions fixed, removed comments from post

--- client/landing/app/CommonViews/opinions/opinionlistitemview.coffee
+++ client/landing/app/CommonViews/opinions/opinionlistitemview.coffee
@@ -53,7 +53,8 @@
         href      : '#'
       cssClass    : 'edit-link hidden'

-    @commentBox = new OpinionCommentView null, data
+    @commentBox = new CommentView null, data
+    # @commentBox = new OpinionCommentView null, data

     @commentBox.on "DiscussionTeaserShouldRefresh",=>
       @parent.emit "DiscussionTeaserShouldRefresh"


targets: safer number formatting

targets: safer number formatting

--- app/assets/javascripts/lib/models/target.coffee
+++ app/assets/javascripts/lib/models/target.coffee
@@ -46,6 +46,7 @@
   dom_element: => $("#goal_" + @get("goal_id"))

   format_value: (n) =>
+    return null unless _.isNumber(n)
     switch @get('display_fmt')
       when 'percentage'
         return Metric.ratio_as_percentage(n, false, 2)


order submeasures in UI

order submeasures in UI

--- app/assets/javascripts/models/measure.js.coffee
+++ app/assets/javascripts/models/measure.js.coffee
@@ -42,6 +42,7 @@
 class SubCollection extends Thorax.Collection
   model: Thorax.Models.Submeasure
   initialize: (models, options) -> @parent = options.parent
+  comparator: 'sub_id'

 class Thorax.Models.Query extends Thorax.Model


回老家了没网啊，预留图片上传接口

回老家了没网啊，预留图片上传接口

--- routes/api.coffee
+++ routes/api.coffee
@@ -199,4 +199,9 @@
           return
     return

+
+#上传图片接口
+
+
+
 module.exports = router


Closes #24. Fixed Desktop extension compatibility problem

--- extension/packages/commands.coffee
+++ extension/packages/commands.coffee
@@ -26,9 +26,11 @@
 command_t = (vim) ->
   if chromeWindow = utils.getRootWindow vim.window
     if gBrowser = chromeWindow.gBrowser
-      gBrowser.selectedTab = chromeWindow.gBrowser.addTab()
-      if urlbar = chromeWindow.document.getElementById('urlbar')
-        urlbar.focus()
+      # Get the default url for the new tab
+      newtab_url = Services.prefs.getCharPref 'browser.newtab.url'
+      gBrowser.addTab newtab_url
+      # Focus the address bar
+      chromeWindow.focusAndSelectUrlBar()

 # Copy current URL to the clipboard
 command_yf = (vim) ->
@@ -214,9 +216,11 @@
     'k|c-y':    [ command_k_cy,   _('help_command_k_cy') ]
     'h':        [ command_h,      _('help_command_h') ]
     'l':        [ command_l ,     _('help_command_l') ]
+
+    # Can't use c-u/c-d because it's generally used for viewing sources
     'd':        [ command_d,      _('help_command_d') ]
-    # Can't use c-u because it's generally used for viewing sources
     'u':        [ command_u,      _('help_command_u') ]
+
     # Can't use c-f because it's generally used for viewing sources
     #'c-f':      [ command_cf,     _('help_command_cf') ]
     #'c-b':      [ command_cb,     _('help_command_cb') ]


Added General purpose role middleware

Added General purpose role middleware

--- server/authentication/index.coffee
+++ server/authentication/index.coffee
@@ -178,6 +178,19 @@
         else
           res.json 401, {}

+  roleAction = (role) ->
+    (req, res, next) ->
+      userAction req, res, () ->
+        isInRole role, req.user, (ok) ->
+          if ok
+            next()
+          else
+            isAdmin req.user, (admin) ->
+              if admin
+                next()
+              else
+                res.json 401, {}
+
   isInRole = (role, user, callback) ->
     result = false
     settingName = role + 'RoleName'
@@ -188,7 +201,7 @@
         roleName = result.value
       app.models.roles.findOneBy 'name', roleName, user.systemId
       , (err, obj) ->
-        if obj and not err
+        if obj?._id? and not err
           _.each(user.roles, (role) ->
             if role.toString() is obj._id.toString()
               result = true
@@ -239,6 +252,7 @@
   #Export the authentiaction action middleware
     publicAction: publicAction
     publicReadAction: publicReadAction
+    roleAction: roleAction
     userAction: userAction
     adminAction: adminAction
     sysAdminAction: sysAdminAction


:fire: No longer use decoration to hl narrow keyword

--- lib/narrow.coffee
+++ lib/narrow.coffee
@@ -12,7 +12,6 @@
   buildEditor: (params={}) ->
     @editor = atom.workspace.buildTextEditor(lineNumberGutterVisible: false)
     @editor.onDidDestroy =>
-      # @markerLayer?.destroy()
       @provider?.destroy?()

     @editorElement = atom.views.getView(@editor)
@@ -47,11 +46,6 @@
     filterKey = @provider.getFilterKey()

     @getItems().then (items) =>
-      # @markerLayer?.destroy()
-      # @decorationLayer?.destroy()
-      # @markerLayer = @editor.addMarkerLayer()
-      # @decorationLayer = @editor.decorateMarkerLayer(@markerLayer, {type: 'highlight', class: 'narrow-match'})
-
       patterns = []
       for word in words
         pattern = _.escapeRegExp(word)
@@ -62,10 +56,6 @@

       @updateGrammar(@editor, patterns.join('|'))
       @setItems(items)
-      # pattern = ///#{patterns.join('|')}///gi
-      # scanRange = getVisibleBufferRange(@editor)
-      # @editor.scanInBufferRange pattern, scanRange, ({range}) =>
-      #   @markerLayer.markBufferRange(range)

   observeInputChange: (editor) ->
     buffer = editor.getBuffer()


Refactoring horizontal directive

Refactoring horizontal directive

Thanks @heynemann; +1 for you

--- app/scripts/directives/horizontal.coffee
+++ app/scripts/directives/horizontal.coffee
@@ -1,4 +1,82 @@
 'use strict'
+
+class HorizontalCtrl
+  constructor: (@scope, @element, @attrs, @filter) ->
+    @showTotal = false
+    @showTotal = @scope.showtotal if @scope.showtotal?
+    @elements = {}
+
+    @buildElements()
+
+    @setElementValue(@scope.value)
+    @setElementTotalValue(@scope.total)
+
+    @watchForChanges()
+
+  setElementValue: (value) ->
+    valueLabel = ''
+    if @scope.valuelabel?
+      valueLabel = '
' + @scope.valuelabel + '
'
+
+    @elements.value.html(@filter('number')(value) + valueLabel)
+    @setElementTotalValue(@scope.total)
+
+  setElementTotalValue: (totalValue) ->
+    return unless @elements.totalValue?
+
+    total = @filter('number')(totalValue - @scope.value)
+    @elements.totalValue.html(total)
+
+  buildElements: ->
+    @element.html('

')
+    @elements.value = @element.find('.value')
+    @maxWidth = @element.width()
+    @width = @scope.percentage * @maxWidth
+    @elements.value.css('width', @width)
+
+    @elements.totalLabel = null
+    if @scope.totallabel?
+      @elements.totalLabel = angular.element('
' + @scope.totallabel + '
')
+      @element.append(@elements.totalLabel)
+      @totalLabelWidth = @elements.totalLabel.width()
+
+    @elements.totalValue = null
+    if @showTotal
+      @elements.totalValue = angular.element('

')
+      @element.append(@elements.totalValue)
+
+  watchForChanges: ->
+    @scope.$watch('value', (newValue, oldValue) =>
+      @setElementValue(newValue)
+    )
+
+    @scope.$watch('total', (newValue, oldValue) =>
+      @setElementTotalValue(newValue)
+    )
+
+    @scope.$watch('percentage', @updatePercentage)
+
+  updatePercentage: =>
+    idealdx = (Math.floor(Math.log(@scope.value) / Math.log(1000))) * 0.012
+    idealRatio = idealdx + (Math.floor(Math.log(@scope.value) / Math.log(10)) + 1) * 0.024
+    @width = if @scope.percentage > idealRatio then @scope.percentage * @maxWidth else idealRatio * @maxWidth
+    @elements.value.css('width', @width)
+
+    if totalLabel?
+      if @width > @maxWidth - @totalLabelWidth
+        @elements.totalLabel.fadeOut()
+        @elements.totalValue.fadeOut() if @elements.totalValue?
+      else
+        @elements.totalLabel.fadeIn()
+        @elements.totalValue.fadeIn() if @elements.totalValue?
+
+    if @scope.valuelabel?
+      label = @elements.value.find('.value-label')
+      if @width
@@ -13,73 +91,5 @@
       valuelabel: '@'
       totallabel: '@'
     link: (scope, element, attrs) ->
-      showTotal = false
-      showTotal = scope.showtotal if scope.showtotal?
-
-      element.html('

')
-      valueElement = element.find('.value')
-      maxWidth = element.width()
-      width = scope.percentage * maxWidth
-      valueElement.css('width', width)
-      valueLabelWidth = null
-      totalLabelWidth = null
-
-      setElementValue = (value) ->
-        valueLabel = ''
-        if scope.valuelabel?
-          valueLabel = '
' + scope.valuelabel + '
'
-
-        valueElement.html($filter('number')(value) + valueLabel)
-        valueLabelWidth = valueElement.find('.value-label').width()
-
-      setElementValue(scope.value)
-
-      totalLabel = null
-      if scope.totallabel?
-        totalLabel = angular.element('
' + scope.totallabel + '
')
-        element.append(totalLabel)
-        totalLabelWidth = totalLabel.width()
-
-      totalValueElement = null
-      if showTotal
-        total = $filter('number')(scope.total - scope.value)
-        totalValueElement = angular.element('
' + total + '
')
-        element.append(totalValueElement)
-
-      setElementTotalValue = (totalValue) ->
-        totalValueElement.html(totalValue)
-
-      if totalValueElement
-        setElementTotalValue(scope.total)
-
-      scope.$watch('value', (newValue, oldValue) ->
-        setElementValue(newValue)
-      )
-
-      scope.$watch('total', (newValue, oldValue) ->
-        if totalValueElement
-          setElementTotalValue(newValue)
-      )
-
-      scope.$watch('percentage', (newValue, oldValue) ->
-        idealdx = (Math.floor(Math.log(scope.value) / Math.log(1000))) * 0.012
-        idealRatio = idealdx + (Math.floor(Math.log(scope.value) / Math.log(10)) + 1) * 0.024
-        width = if scope.percentage > idealRatio then scope.percentage * maxWidth else idealRatio * maxWidth
-        valueElement.css('width', width)
-
-        if totalLabel?
-          if width > maxWidth - totalLabelWidth
-            totalLabel.fadeOut()
-            totalValue.fadeOut() if totalValue?
-          else
-            totalLabel.fadeIn()
-            totalValue.fadeIn() if totalValue?
-
-        if scope.valuelabel?
-          label = valueElement.find('.value-label')
-          if width < label.width()
-            label.fadeOut()
-          else
-            label.fadeIn()
-      )
+      ctrl = new HorizontalCtrl(scope, element, attrs, $filter)
   )


Run copy-i18n-tags (#5693)

Run copy-i18n-tags (#5693)

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -195,6 +195,7 @@
     resource_hub: "资源中心"
     apcsp: "AP计算机科学原理"
     parent: "家长参与"
+#    browser_recommendation: "For the best experience we recommend using the latest version of Chrome. Download the browser here!"

   modal:
     close: "关闭"
@@ -3008,3 +3009,7 @@
 #    try_again: "Whoops, try again!"
 #    select_statement_left: "Whoops, select a statement from the left before hitting \"Submit.\""
 #    fill_boxes: "Whoops, make sure to fill all boxes before hitting \"Submit.\""
+
+#  browser_recommendation:
+#    title: "CodeCombat works best on Chrome!"
+#    pitch_body: "For the best CodeCombat experience we recommend using the latest version of Chrome. Download the latest version of chrome by clicking the button below!"


hard code modification

hard code modification

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -1,34 +1,34 @@
 module.exports = nativeDescription: "简体中文", englishDescription: "Chinese (Simplified)", translation:
-  ozaria_home:
-    subtitle: "引人入胜的计算机科学教程"
-    summary: "Ozaria（“奥佳睿”）既是计算机科学程序，扣人心弦的冒险游戏，也是一个让学生们掌握计算机代码魔法来拯救世界的科幻故事。"
-    value_prop1_h1: "史诗故事，沉浸式体验"
-    value_prop1_h2: "Inspire a deeper connection to learning"
-    value_prop1_p: "在奥佳睿，学生们身处史诗故事的中心，他们必须通过掌握计算思维来晋级。逐渐展开的情节打造出以学生为中心的学习体验，基于每个学生对奥佳睿的投入程度。"
-    value_prop2_h1: "Discover the Hero within Every Student"
-    value_prop2_h2: "Build real skills through play"
-    value_prop2_p: "计算机科学概念和技能通过富有吸引力的内容切入，包括游戏关卡、教学角色对话、过渡动画等。学生学习键入 Python 或 JavaScript 真实代码，运用计算思维解决问题，通过结构性教学评估证验证知识理解，并制作自己的项目来应用所学知识。"
-    value_prop3_h1: "免费开启你的编程之旅"
-    value_prop3_p: "通过冒险故事的进行开展授课，提供涵盖计算机科学入门概念的免费内容，以及获得课程指南和课程计划的渠道。"
-    projects_inspire_heading: "Projects that Inspire. Resources that Empower."
-    student_creativity: "Unleash Student Creativity"
-    student_creativity_paragraph: "在每个单元最后，学生将运用所学知识创建自己的项目，包括选择你自己的冒险故事到游戏设计中来。建立在明确目标下，但可以有无限构想的内容形式，创建共享和可玩的作品集，展示了学生们无限的创造力。"
-    teach_with_support: "Teach with Support"
-    teach_with_support_paragraph: "Ozaria（“奥佳睿”）专为教育工作者而建。 整个课程中的每个单元都有一个课程计划，概述学习目标，常见学生误区，关键术语和CSTA标准。 此外，完善的教学管理后台可以帮助教师跟踪学生的进度并检查学生的代码。"
-    request_demo: "Request a Demo"
-    im_an_educator: "我是老师"
-    im_a_student: "我是学生"
-    global_title: "Join Our Global Community of Learners and Educators"
-    global_subtitle1: "学习者"
-    global_subtitle2: "行代码"
-    global_subtitle3: "老师"
-    global_subtitle4: "国家"
-    sign_up: "注册"
-
+
+#  ozaria_home:
+#    subtitle: "Computer Science that Captivates"
+#    summary: "Ozaria is a computer science program, an adventure game, and a fantasy story where students master the lost magic of coding to save their world."
+#    value_prop1_h1: "Epic Story. Immersive Gameplay."
+#    value_prop1_h2: "Inspire a deeper connection to learning"
+#    value_prop1_p: "In Ozaria, students find themselves embedded in a new world where they master core computer science concepts to progress within the game. The unfolding narrative creates a powerful student-centered learning experience, with each student’s investment in Ozaria’s story paralleling the investment they are making in their own learning journey."
+#    value_prop2_h1: "Discover the Hero within Every Student"
+#    value_prop2_h2: "Build real skills through play"
+#    value_prop2_p: "Concepts and skills are introduced via a thoughtfully-designed content progression that includes game levels, instructional character dialogue, cutscenes, and more. Students learn to type Python or JavaScript, problem-solve using computational thinking, demonstrate understanding through formative assessments, and apply what they learn by making their own projects."
+#    value_prop3_h1: "Start Your Journey For Free"
+#    value_prop3_p: "Teach through adventure with free content covering introductory computer science concepts, plus access to implementation guides and lesson plans."
+#    projects_inspire_heading: "Projects that Inspire. Resources that Empower."
+#    student_creativity: "Unleash Student Creativity"
+#    student_creativity_paragraph: "At the end of each unit, students apply what they've learned by creating projects, from games to choose-your-own adventure stories and more. Built with scaffolded goals, yet allowing for limitless ideation, students build a shareable and playable portfolio of work that showcases their creative identity."
+#    teach_with_support: "Teach with Support"
+#    teach_with_support_paragraph: "Ozaria is built for educators. Each unit throughout the curriculum has a lesson plan that outlines learning objectives, common student misconceptions, key terms, and CSTA standards. In addition, a comprehensive teacher dashboard helps teachers to track student progress and check student code."
+#    request_demo: "Request a Demo"
+#    im_an_educator: "I'm an Educator"
+#    im_a_student: "I'm a Student"
+#    global_title: "Join Our Global Community of Learners and Educators"
+#    global_subtitle1: "Learners"
+#    global_subtitle2: "Lines of Code"
+#    global_subtitle3: "Teachers"
+#    global_subtitle4: "Countries"
+#    sign_up: "Sign Up"

   new_home:
-    title: "Ozaria - 引人入胜的计算机科学教程"
-    meta_description: "Ozaria（“奥佳睿”）既是计算机科学程序，扣人心弦的冒险游戏，也是一个让学生们掌握计算机代码魔法来拯救世界的科幻故事。"
+#    title: "Ozaria - Computer science that captivates"
+#    meta_description: "Ozaria is a computer science program, an adventure game, and a fantasy story where students master the lost magic of coding to save their world."
     built_for_teachers_title: "一个以教师为中心的课程体系"
     built_for_teachers_blurb: "教孩子们写代码往往会觉得无从下手。 CodeCombat 旨在帮助所有的教育工作者传授学生学习和掌握当下最流行的两种计算机语言，JavaScript 和 Python。整个体系包含六门计算机科学课程，以及通过基于项目的游戏开发和网页开发单元巩固学习，教会孩子们一步步从基本语法到高级递归前进！"
     built_for_teachers_subtitle1: "计算机科学"
@@ -175,9 +175,8 @@
     view_progress: "查看进度"
     go_to_courses: "进入我的课程"
     want_coco: "把 CodeCombat 带到您的学校？"
-    educator: "教师"
-    student: "学生"
-    go_to_my_dashboard: "进入我的课程"
+#    educator: "Educator"
+#    student: "Student"

   nav:
     educators: "教育者"
@@ -222,14 +221,14 @@
     resource_hub: "资源中心"
     apcsp: "AP计算机科学原理"
     parent: "家长参与"
-    faq: "常见问题"
-    return_coco: "返回CodeCombat"
-    classes: "我的课程"
+#    faq: "FAQ"
+#    return_coco: "Return to CodeCombat"
+#    classes: "Classes"

   modal:
     close: "关闭"
     okay: "是"
-    cancel: "取消"
+#    cancel: "Cancel"

   not_found:
     page_not_found: "找不到网页"
@@ -274,7 +273,7 @@
     achievements: "成就" # Tooltip on achievement list button from /play
     settings: "设置" # Tooltip on settings button from /play
     poll: "投票" # Tooltip on poll button from /play
-    next: "下一步" # Go from choose hero to choose inventory before playing a level
+    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: "购买宝石"
@@ -296,9 +295,9 @@
     back_to_classroom: "返回教室"
     teacher_button: "带到学校"
     get_more_codecombat: "获取跟过CodeCombat游戏的信息"
-    back_to_dashboard: "返回我的课程"
-    back_to_ozaria: "返回奥佳睿首页"
-    customize_hero: "英雄设置"
+#    back_to_dashboard: "Back to Dashboard"
+#    back_to_ozaria: "Back to ozaria.com"
+#    customize_hero: "Customize Hero"

   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.)
@@ -362,7 +361,7 @@
 #    auth_sign_up: "New to Ozaria? Create an Account"
     sign_up: "注册"
     email_or_username: "邮箱或用户名" # {change}
-    or: "或"
+#    or: "or"
     log_in: "登录"
 #    sign_in: "Sign In"
     logging_in: "正在登录"
@@ -450,7 +449,7 @@
     teacher_list_resource_hub_3: "关卡解决方案，和"
     teacher_list_resource_hub_4: "资源汇总"
     teacher_list_resource_hub_5: "课程指导，课程活动和更多信息！"
-    teacher_additional_questions: "如果需要额外帮助或遇到问题，请联系 china@codecombat.com。"
+    teacher_additional_questions: "如果需要额外帮助或遇到问题，请联系 bill@codecombat.com。"
     dont_use_our_email_silly: "请填写家长的邮箱地址。"
     want_codecombat_in_school: "想接着玩CodeCombat?"
     eu_confirmation: "我同意 CodeCombat 在美国的服务器上存储我的数据"
@@ -481,10 +480,10 @@
     back: "返回" # When used as an action verb, like "Navigate backward"
     coming_soon: "敬请期待！"
     continue: "继续" # When used as an action verb, like "Continue forward"
-    next: "下一步"
+    next: "下一个"
     default_code: "默认代码"
     loading: "读取中……"
-    LOADING: "加载中"
+#    LOADING: "LOADING"
     overview: "概览"
     processing: "进行中……"
     solution: "解法"
@@ -510,11 +509,11 @@
     submit_changes: "提交更新"
     save_changes: "保存更新"
     required_field: "必填" # {change}
-    submit: "提交"
-    replay: "重新开始"
-    complete: "完成"
-    continue_editing: "继续编辑"
-    logout: "退出登录"
+#    submit: "Submit"
+#    replay: "Replay"
+#    complete: "Complete"
+#    continue_editing: "Continue Editing"
+#    logout: "Logout"

   general:
     and: "与"
@@ -597,7 +596,7 @@

   play_level:
     back_to_map: "返回地图"
-    directions: "游戏指引"
+    directions: "方向"
     edit_level: "编辑关卡"
     keep_learning: "不断学习"
     explore_codecombat: "探索CodeCombat"
@@ -674,7 +673,7 @@
     tome_cast_button_run: "运行"
     tome_cast_button_running: "正在运行"
     tome_cast_button_ran: "运行过"
-    tome_cast_button_update: "更新"
+#    tome_cast_button_update: "Update"
     tome_submit_button: "提交"
     tome_reload_method: "恢复关卡初始代码"
     tome_available_spells: "可用的法术"
@@ -779,29 +778,20 @@
     concept_unlocked: "已解锁的概念"
     use_at_least_one_concept: "请使用以下至少一个概念： "
 #    command_bank: "Code Bank"
-    learning_goals: "学习目标"
-    start: "开始"
+#    learning_goals: "Learning Goals"
+#    start: "Start"
 #    vega_character: "Vega Character"
-    click_to_continue: "点击继续"
-    share_your_project: "分享你的项目"
-    keep_editing_your_project: "即使在分享出去之后，你也一直可以编辑你的项目"
-    copy_url: "复制链接"
-    level_status: "状态"
-    level_status_locked: "已锁定"
-    level_status_in_progress: "进行中"
-    level_status_complete: "已完成"
-    level_type_practice: "练习关卡"
-    level_type_challenge: "挑战关卡"
-    level_type_cutscene: "过渡关卡"
-    level_type_capstone: "顶点关卡"
-    level_type_intro: "热身关卡"
+#    click_to_continue: "Click to Continue"
+#    share_your_project: "Share Your Project"
+#    keep_editing_your_project: "You can always keep editing your project, even after sharing it."
+#    copy_url: "Copy URL"

   apis:
     methods: "函数"
     events: "事件"
 #    handlers: "Handlers"
-    properties: "属性"
-    snippets: "字段"
+#    properties: "Properties"
+#    snippets: "Snippets"
     spawnable: "可生成物"
     html: "HTML"
     math: "数学"
@@ -832,7 +822,7 @@
     created_during_hoc: "在“编程一小时”中创建。"
     restart: "重新开始关卡"
     play: "开始关卡"
-    play_more_codecombat: "玩更多的 CodeCombat 关卡"
+    learn_about_ozaria: "Learn About Ozaria"
     default_student_instructions: "单击控制英雄，并取得胜利！"
     goal_survive: "生存。"
     goal_survive_time: "生存 __seconds__ 秒。"
@@ -959,7 +949,7 @@
     year_subscription: "年度订阅"
     year_price: "$__price__/年"
     support_part1: "付款时需要帮助? Email" # {change}
-    support_part2: "china@codecombat.com"
+    support_part2: "bill@codecombat.com"

   announcement:
     now_available: "现在订阅者可以获得更多内容"
@@ -1128,7 +1118,7 @@
     learn_more: "了解更多"
     main_title: "学习编程需要动手写大量代码"
     main_description: "CodeCombat让你带着微笑学习"
-    mission_link: "愿景"
+    mission_link: "任务"
     team_link: "团队"
     story_link: "历程"
     press_link: "联系方式"
@@ -1138,7 +1128,7 @@
 #    mission_desc_2: "We believe that learning computer science can be captivating. It should feel like magic. After all, programming empowers anyone to explore new ideas, to build something from pure imagination, and to interact with each other in new ways. Our goal is to inspire a connection to the creativity and imagination inherent in computer science by building educational games that are creative and imaginative."
 #    mission_desc_3: "Over the last five years, over twelve million people have learned to write Python and JavaScript using CodeCombat, making it one of the most popular coding games in history. Now, we are excited to launch Ozaria and expand on our commitment to inspire a new generation of students to learn computer science. Ozaria is an adventure game that places each learner at the center of a legendary story where they must save the world of Ozaria from impending darkness by mastering the lost magic of coding. As the narrative unfolds, a powerful student-centered learning experience is created - each student’s investment in Ozaria’s story paralleling the investment they are making in their own learning journey."
     team_title: "与CodeCombat团队见面"
-    team_values: "我们期待开放的、互相尊敬的对话，好点子会得到采纳。我们决定深入市场调研，我们集中于提供实际性的结果。从我们的 CEO 到 Github 上的贡献者，每个人都亲力亲为，因为在团队中我们收获了成长与经验。"
+    team_values: "我们期待开放的、互相尊敬的对话，好点子会得到采纳。我们决定深入市场调研，我们集中于提供实际性的结果。从我们的 CEO 到 Github 上的贡献者，每个人都亲历亲为，因为在团队中我们收获了成长与经验。" # {change}
 #    nick_title: "Cofounder, CEO"
 #    matt_title: "Cofounder, CTO"
 #    cat_title: "Game Designer"
@@ -1178,7 +1168,7 @@
     community_subtitle: "超过 450 个志愿者帮助我们建设 CodeCombat，每周都有新成员加入！" # {change}
     community_description_3: "CodeCombat 是一个"
     community_description_link_2: "开源的社区项目"
-    community_description_1: "上百名玩家创建了各种关卡，给我们的源代码添砖加瓦，添加新功能、修复Bug、测试、甚至是将其翻译为50种语言。职员、贡献者以及网站本身都在分享创意并奋斗中成长，这就是开源社区的力量。此网站在众多开源项目之上建立，我们为表感恩，也将网站开源奉献给社区，给好奇源码的玩家提供探索和实验的项目。每个人都能加入CodeCombat社区！看一下我们的" # {change}
+    community_description_1: "，上百名玩家创建了各种关卡，给我们的源代码添砖加瓦，添加新功能、修复Bug、测试、甚至是将其翻译为50种语言。职员、贡献者以及网站本身都在分享创意并奋斗中成长，这就是开源社区的力量。此网站在众多开源项目之上建立，我们为表感恩，也将网站开源奉献给社区，给好奇源码的玩家提供探索和实验的项目。每个人都能加入CodeCombat社区！看一下我们的" # {change}
     community_description_link: "贡献者页面"
     community_description_2: "了解更多信息。"
     number_contributors: "超过 450 个志愿者将他们的知识和时间投入到这个项目中。"
@@ -1188,7 +1178,7 @@
     story_statistic_1b: "总玩家"
     story_statistic_1c: "已经通过CodeCombat开始了他们的编程之旅" # {change}
     story_statistic_2a: "我们将这个游戏翻译为超过 50 种语言 —— 得益于来自"
-    story_statistic_2b: "190+ 个国家" # {change}
+    story_statistic_2b: "200+ 个国家" # {change}
     story_statistic_3a: "的玩家，他们总共写下了"
     story_statistic_3b: "10亿行代码和说明"
     story_statistic_3c: "，涵盖了多种编程语言。"
@@ -1235,7 +1225,7 @@
     low_price_title: "每个学生仅仅只要 __starterLicensePrice__ 元 "
     low_price_description: "学生入门许可证在购买后的 __starterLicenseLengthMonths__ 个月内有效."
     three_great_courses: "学生入门许可证中包含三个伟大的课程:"
-    license_limit_description: "老师最多可以购买 __maxQuantityStarterLicenses__ 个许可证. 你已经购买了 __quantityAlreadyPurchased__ 个. 如果你需要更多, 请联系 china@codecombat.com. 学生入门许可证有效期为 __starterLicenseLengthMonths__ 个月."
+    license_limit_description: "老师最多可以购买 __maxQuantityStarterLicenses__ 个许可证. 你已经购买了 __quantityAlreadyPurchased__ 个. 如果你需要更多, 请联系 bill@codecombat.com. 学生入门许可证有效期为 __starterLicenseLengthMonths__ 个月."
     student_starter_license: "学生入门许可"
     purchase_starter_licenses: "购买入门许可证"
     purchase_starter_licenses_to_grant: "购买入门许可证可被授权访问 __starterLicenseCourseList__"
@@ -1270,8 +1260,8 @@
   teachers_quote:
     subtitle: "将 CodeCombat 带到您的班级、俱乐部、学校甚至地区！" # {change}
     email_exists: "使用该账号的电子邮箱"
-    phone_number: "手机号码"
-    phone_number_help: "工作日我们如何找到您？"
+    phone_number: "电话号码"
+    phone_number_help: "工作日时我们在哪可以找到您？"
     primary_role_label: "您的主要职业"
     role_default: "选择职业"
     primary_role_default: "（请选择）"
@@ -1287,12 +1277,12 @@
     approve_funds: "批准资金"
     no_purchaser_role: "不参与购买决策"
     district_label: "地区"
-    district_name: "区/县"
+    district_name: "区"
     district_na: "不可用时请输入 N/A"
     organization_label: "学校："
-    school_name: "学校或机构名称"
-    city: "市（直辖市可填无）"
-    state: "省/直辖市" # {change}
+    school_name: "学校名称"
+    city: "城市"
+    state: "省" # {change}
     country: "国家"
     num_students_help: "您估计有多少学生加入CodeCombat？" # {change}
     num_students_default: "选择数量范围"
@@ -1329,7 +1319,7 @@
     create_account_subtitle: "在 CodeCombat 班级中使用老师专用工具。 创建班级添加学生、观察学生进度！"
     convert_account_title: "升级为老师账号"
     not: "不是"
-    full_name_required: "姓和名是必填项"
+#    full_name_required: "First and last name required"

   versions:
     save_version_title: "保存新版本"
@@ -1419,8 +1409,8 @@
     beautify: "利用标准编码格式美化您的代码。"
     maximize_editor: "最大化/最小化代码编辑器"

-  cinematic:
-    click_anywhere_continue: "点击任意区域继续"
+#  cinematic:
+#    click_anywhere_continue: "click anywhere to continue"

   community:
     main_title: "CodeCombat社区"
@@ -1558,7 +1548,7 @@
     play_now_learn_2: "解决麻烦谜题的while循环"
     play_now_learn_3: "自定义动作的字符串和变量"
     play_now_learn_4: "如何打败食人魔（重要生存技巧！）"
-    welcome_to_page: "欢迎来到我的课程！"
+    welcome_to_page: "欢迎来到你的课程页面！" # {change}
     my_classes: "我的课程" # {change}
     class_added: "成功添加课程！"
     view_map: "查看地图"
@@ -1570,7 +1560,7 @@
     enter_c_code: "<输入班级代码>" # {change}
     join: "加入"
     joining: "加入课程中"
-    unit_complete: "已完成"
+#    unit_complete: "Unit Complete"
     play_arena: "玩竞技场"
     view_project: "查看项目"
     start: "开始"
@@ -1582,7 +1572,7 @@
     are_you_sure: "你确定要从该课程中删除该学生吗？"
     remove_description1: "学生将失去访问该课程和被分派课程的权利。游戏进度不会丢失，该学生可以在任何时间被添加回该班级。"
     remove_description2: "被激活的付费许可证将无法返还。"
-    license_will_revoke: "该学生付费许可将被撤销，稍后可以分配给其他学生"
+#    license_will_revoke: "This student's paid license will be revoked and made available to assign to another student."
     keep_student: "保留学生"
     removing_user: "删除用户"
     subtitle: "复习课程指导、解法和关卡" # {change}
@@ -1778,7 +1768,7 @@
     revoke_license: "终止许可"
     revoke_licenses: "终止所有许可"
     course_progress: "课程进度"
-    unit_progress: "课程进度"
+#    unit_progress: "Unit Progress"
     not_applicable: "N/A"
     edit: "修改"
     edit_2: "修改"
@@ -1818,7 +1808,7 @@
 #    not_assigned_modal_starter_body_2: "Purchase Starter Licenses to grant access to this course."
     not_assigned_modal_full_body_1: "本课程需要许可证才能使用。您没有足够的许可证, 因此无法将此课程分配给已选定的 __selected__ 名学生。" # {change}
     not_assigned_modal_full_body_2: "您当前仅有__numFullLicensesAvailable__个许可证可供使用 (__numStudentsWithoutFullLicenses__名学生目前没有许可证) 。"
-    not_assigned_modal_full_body_3: "请适量减少选定的学生数量, 或联系china@codecombat.com寻求帮助。"
+    not_assigned_modal_full_body_3: "请适量减少选定的学生数量, 或联系bill@codecombat.com寻求帮助。"
     assigned: "已添加"
     enroll_selected_students: "注册选中的学生"
     no_students_selected: "没有学生被选中。"
@@ -2047,7 +2037,7 @@
     sync_google_classroom: "同步谷歌教室"
 #    implementation_guide: "Implementation Options - Ozaria and CodeCombat"
 #    implementation_guide_desc: "Options for bridging Ozaria and CodeCombat content based on the structure and focus of your classroom."
-    prologue: "序章: 天山"
+#    prologue: "Prologue: The Sky Mountain"
 #    unit_overview: "Unit Overview"
 #    module_overview: "Module Overview"
 #    prologue_unit_overview_desc: "Summary of student experience, including Cutscenes, Practice Levels, Intros, and the Capstone project in the Prologue Unit."
@@ -2415,12 +2405,12 @@
     left_xp_postfix: ""

   account:
-    title: "账户"
-    settings_title: "账户设置"
-    unsubscribe_title: "取消订阅"
-    payments_title: "支付"
-    subscription_title: "订阅"
-    invoices_title: "发票"
+#    title: "Account"
+#    settings_title: "Account Settings"
+#    unsubscribe_title: "Unsubscribe"
+#    payments_title: "Payments"
+#    subscription_title: "Subscription"
+#    invoices_title: "Invoices"
 #    prepaids_title: "Prepaids"
     payments: "支付方式"
     prepaid_codes: "预付费码"
@@ -2488,13 +2478,13 @@
     you_can2: "购买一个预购码"
     you_can3: "使用在您的账户或者给其他人。"

-  ozaria_chrome:
-    sound_off: "关闭音效"
-    sound_on: "开启音效"
-    back_to_map: "回到地图"
-    level_options: "关卡选项"
-    restart_level: "重新开始"
-    max_browser: "全屏模式"
+#  ozaria_chrome:
+#    sound_off: "Sound Off"
+#    sound_on: "Sound On"
+#    back_to_map: "Back to Map"
+#    level_options: "Level Options"
+#    restart_level: "Restart Level"
+#    max_browser: "Maximize Browser"

   loading_error:
     could_not_load: "载入失败" # {change}
@@ -2542,8 +2532,8 @@
     arithmetic: "四则运算"
     array_2d: "二维数组"
     array_index: "数组下标"
-    array_iterating: "数组迭代"
-    array_literals: "数组字面量"
+#    array_iterating: "Iterating Over Arrays"
+#    array_literals: "Array Literals"
     array_searching: "数组遍历"
     array_sorting: "数组排序"
     arrays: "数组"
@@ -2585,7 +2575,7 @@
     game_goals: "游戏目标"
 #    game_spawn: "Game Spawn"
     graphics: "图形"
-    graphs: "图"
+#    graphs: "Graphs"
     heaps: "堆"
     if_condition: "If条件语句"
     if_else_if: "If/Else If语句"
@@ -2606,7 +2596,7 @@
     jquery_selectors: "jQuery选择器"
     length: "数组长度"
 #    math_coordinates: "Coordinate Math"
-    math_geometry: "几何"
+#    math_geometry: "Geometry"
     math_operations: "数学程序库操作"
 #    math_proportions: "Proportion Math"
 #    math_trigonometry: "Trigonometry"
@@ -2633,7 +2623,7 @@
     while_condition_loops: "使用条件语句的While循环"
     while_loops_simple: "While循环"
     while_loops_nested: "While嵌套循环"
-    xy_coordinates: "坐标对"
+#    xy_coordinates: "Coordinate Pairs"
     advanced_strings: "高级字符串" # Rest of concepts are deprecated
     algorithms: "算法"
     boolean_logic: "布尔逻辑"
@@ -3019,7 +3009,7 @@
     projects_created: "学生作品数"
     other: "其他"
     notice: "以下学校管理员对您的课堂数据有查看权限："
-    add_additional_teacher: "需要添加其他的教师吗？请联系您的CodeCombat客户经理或发送电子邮件至china@codecombat.com。 "
+    add_additional_teacher: "需要添加其他的教师吗？请联系您的CodeCombat客户经理或发送电子邮件至bill@codecombat.com。 "
     license_stat_description: "可用许可证是教师可用的许可证总数，包括共享的许可证。"
     students_stat_description: "学生总数是所有教室的所有学生数，不管他们是否使用了许可证，都会被计算在内。"
     active_students_stat_description: "活跃学生数是在过去60天登录CodeCombat的学生人数。"


Add case to display spinning wheel on all rows

If the compute all metrics button is pressed the row for displaying the
spinning wheel could not be found. Hence, I have added a case which
leads to selecting all metric rows.

Signed-off-by: Konrad Reiche <4ff0213f56e2082fcd8c882d04c546881c579ce4@gmail.com>

--- app/assets/javascripts/admin.js.coffee
+++ app/assets/javascripts/admin.js.coffee
@@ -32,7 +32,10 @@
     optitons =
       lines: 10
       length: 1
-    target = $(".admin.control.#{metric} > .admin.control.status")
+    if metric == '*'
+      target = $(".admin.control > .admin.control.status")
+    else
+      target = $(".admin.control.#{metric} > .admin.control.status")
     target.spin(optitons)

   # Removes the spinning wheel from the status table cell


Translate items under multiplayer_launch

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -632,14 +632,14 @@
     introducing_dungeon_arena: "介绍地下城竞技场"
     new_way: "用代码竞技的新方式."
     to_battle: "去战斗, 开发者们!"
-#    modern_day_sorcerer: "You know how to code? That's badass. You're a modern-day sorcerer! Isn't about time that you used your magic coding powers to command your minions in epic combat? And we're not talking robots here."
-#    arenas_are_here: "CodeCombat head-to-head multiplayer arenas are here."
+    modern_day_sorcerer: "你能编程? 这太牛了。 在这个时代你就是巫师! 还不快用你编码的魔力指挥你的手下投入壮烈的战斗中么? 我们说的可不是和机器战斗哦。"
+    arenas_are_here: "CodeCombat一个多人对抗的头脑竞技场."
 #    ladder_explanation: "Choose your heroes, enchant your human or ogre armies, and climb your way over defeated fellow Wizards to reach the top of the ladders–then challenge your friends in our glorious, asynchronous multiplayer coding arenas. If you're feeling creative, you can even"
     fork_our_arenas: "派生我的竞技场"
     create_worlds: "以及创造我自己的世界."
-#    javascript_rusty: "JavaScript a bit rusty? Don't worry; there's a"
+    javascript_rusty: "对JavaScript有点生疏? 没关系，这里有一个"
     tutorial: "教程"
-#    new_to_programming: ". New to programming? Hit our beginner campaign to skill up."
+    new_to_programming: ". 编程新手? 参加我们的新手训练营来提高技能吧."
     so_ready: "我准备好了!"

   loading_error:


Reset models instead of simply setting in ViewModel.jsonToFacets

The addition of `idAttribute` in 34604027 to ViewModel caused the
`set` to break since changes were merged in without the new order
of the attributes.

--- src/coffee/cilantro/models/view.coffee
+++ src/coffee/cilantro/models/view.coffee
@@ -62,9 +62,10 @@
             return attrs

         jsonToFacets: (json) ->
-            # Implies this is an array of object, set directly
+            # Implies this is an array of object, set directly. This is for
+            # forwards compatibility.
             if _.isArray(json)
-                @facets.set(json)
+                @facets.reset(json)
                 return

             models = []
@@ -78,9 +79,9 @@
                     if id is _id
                         attrs.sort = sort
                         attrs.sort_index = i
-                models.push attrs
+                models.push(attrs)

-            @facets.set models
+            @facets.reset(models)

         facetsToJSON: ->
             json =


Add $modal to dependency

Add $modal to dependency

--- src/coffee/controllers/mainCtrl.coffee
+++ src/coffee/controllers/mainCtrl.coffee
@@ -1,4 +1,4 @@
-timeTracker.controller 'MainCtrl', ($rootScope, $scope, $timeout, $location, $anchorScroll, $window, $q, Ticket, Project, Redmine, Account, State, DataAdapter, Message, Platform, Resource, Option, Log, Analytics) ->
+timeTracker.controller 'MainCtrl', ($rootScope, $scope, $timeout, $location, $anchorScroll, $window, $q, $modal, Ticket, Project, Redmine, Account, State, DataAdapter, Message, Platform, Resource, Option, Log, Analytics) ->

   # represents 5 minuts [msec]
   MINUTE_5 = 5 * 60 * 1000


Fix "expand post" not restoring enough space in some cases.

In same cases, the saved oldHeight could have become inaccurate due
to, for example, loaded images. This commit fixes this issue by
reverting the property to its default value of "auto" instead.

--- app/assets/javascripts/posts.js.coffee
+++ app/assets/javascripts/posts.js.coffee
@@ -23,12 +23,11 @@
 #        console.log(div.height())
 #        console.log(div.html())
         if div.height() > (14 * 10)
-            div.data("oldHeight", div.height())
             div.height(14 * 10)
             div.css("overflow", "hidden")
             link_span.html("
Expand this post →
")
             link_span.children("a").click (event) ->
-                div.height(div.data("oldHeight"))
+                div.height("auto")
                 div.css("overflow", "visible")
                 link_span.html("")
                 event.preventDefault()


Remove TaskSerializer.

Remove TaskSerializer.

--- app/assets/javascripts/store.js.coffee
+++ app/assets/javascripts/store.js.coffee
@@ -4,56 +4,6 @@
   # Override the default adapter with the `DS.ActiveModelAdapter` which
   # is built to work nicely with the ActiveModel::Serializers gem.
   adapter: '-active-model'
-
-
-ETahi.TaskSerializer = DS.ActiveModelSerializer.extend
-  extractSingle: (store, primaryType, payload, recordId, requestType) ->
-    payload = @normalizePayload(primaryType, payload)
-    primaryTypeName = primaryType.typeKey
-    primaryRecord = undefined
-    debugger
-    for prop of payload
-      typeName = @typeForRoot(prop)
-      type = store.modelFor(typeName)
-
-      if payload[prop].parent_type == 'task'
-        isPrimary = true
-        primaryType = type
-      else
-        isPrimary = type.typeKey is primaryTypeName
-
-      # legacy support for singular resources
-      if isPrimary and Ember.typeOf(payload[prop]) isnt "array"
-        primaryRecord = @normalize(primaryType, payload[prop], prop)
-        continue
-
-      #jshint loopfunc:true
-      for hash in payload[prop]
-        hash.foobar = 'hello!'
-        # custom code starts here
-        typeName = if hash.type
-          @typeForRoot hash.type
-        else
-          @typeForRoot prop
-        # custom code ends here
-        type = store.modelFor(typeName)
-        typeSerializer = store.serializerFor(type)
-        hash = typeSerializer.normalize(type, hash, prop)
-        isFirstCreatedRecord = isPrimary and not recordId and not primaryRecord
-        isUpdatedRecord = isPrimary and coerceId(hash.id) is recordId
-
-        # find the primary record.
-        #
-        # It's either:
-        # * the record with the same ID as the original request
-        # * in the case of a newly created record that didn't have an ID, the first
-        #   record in the Array
-        if isFirstCreatedRecord or isUpdatedRecord
-          primaryRecord = hash
-        else
-          store.push typeName, hash
-
-    primaryRecord

 ETahi.PaperSerializer = DS.ActiveModelSerializer.extend
   normalizePayload: (primaryType, payload) ->


Add debug op

Add debug op

--- app/src/tasks/transaction_observer_task.coffee
+++ app/src/tasks/transaction_observer_task.coffee
@@ -20,8 +20,6 @@
       switch data.payload.type
         when 'new-transaction'
           ledger.tasks.TransactionConsumerTask.instance.pushTransaction(data.payload.transaction)
-          data.payload.transaction.hash =  data.payload.transaction.hash.replace('a', 'b').replace('1', '2').replace('c', 'e')
-          ledger.tasks.TransactionConsumerTask.instance.pushTransaction(data.payload.transaction)
         when 'new-block'
           @_handleNewBlock data.payload.block
     @newTransactionStream.onclose = => @_listenNewTransactions() if @isRunning()


Tweak logging (#6017)

Tweak logging (#6017)

* Tweak logging

* Fix pathname

--- app/views/teachers/TeachersContactModal.coffee
+++ app/views/teachers/TeachersContactModal.coffee
@@ -13,9 +13,6 @@
     'submit form': 'onSubmitForm'

   initialize: (options={}) ->
-    @slackLog({
-      event: 'Initialized'
-    })
     @state = new State({
       formValues: {
         name: ''
@@ -45,12 +42,12 @@
         Phone Number: #{props.phoneNumber or ''}
       """
       @state.set('formValues', { name, email, message })
-      @slackLog({
+      @logContactFlowToSlack({
         event: 'Done loading',
         message: "name: #{name}, email: #{email}, trialRequest: #{trialRequest._id}"
       })
     catch e
-      @slackLog({
+      @logContactFlowToSlack({
         event: 'Done loading',
         error: e
       })
@@ -60,7 +57,7 @@

   onSubmitForm: (e) ->
     try
-      @slackLog({
+      @logContactFlowToSlack({
         event: 'Submitting',
         message: "Beginning. sendingState: #{@state.get('sendingState')}"
       })
@@ -81,7 +78,7 @@
         formErrors.message = 'Message required.'
       @state.set({ formErrors, formValues, sendingState: 'standby' })

-      @slackLog({
+      @logContactFlowToSlack({
         event: 'Submitting',
         message: "Validating. name: #{formErrors.name or formValues.name}, email: #{formErrors.email or formValues.email}, licensesNeeded: #{formErrors.licensesNeeded or formValues.licensesNeeded}, message: #{formErrors.message or formValues.message}"
       })
@@ -90,7 +87,7 @@

       @state.set('sendingState', 'sending')
       data = _.extend({ country: me.get('country') }, formValues)
-      @slackLog({
+      @logContactFlowToSlack({
         event: 'Submitting',
         message: "Sending. email: #{formValues.email}"
       })
@@ -98,7 +95,7 @@
         data
         context: @
         success: ->
-          @slackLog({
+          @logContactFlowToSlack({
             event: 'Submitting',
             message: "Successfully sent. email: #{formValues.email}"
           })
@@ -110,7 +107,7 @@
             @hide?()
           , 3000)
         error: ->
-          @slackLog({
+          @logContactFlowToSlack({
             event: 'Submitting',
             message: "Error sending! email: #{formValues.email}"
           })
@@ -119,12 +116,17 @@

       @trigger('submit')
     catch e
-      @slackLog({
+      @logContactFlowToSlack({
         event: 'Submitting',
         message: "General error! error: #{e}"
       })

-  slackLog: (data) ->
+  logContactFlowToSlack: (data) ->
+    logUrl = '/contact/slacklog'
+    # /teachers/licenses and /teachers/starter-licenses
+    if window?.location?.pathname?.endsWith('licenses')
+      logUrl = '/db/trial.request.slacklog'
+
     try
       data.name = me.broadName()
       data.email = me.get('email')
@@ -133,6 +135,6 @@

     $.ajax({
       type: 'POST',
-      url: '/db/trial.request.slacklog',
+      url: logUrl,
       data
     })


:bug:Fix cs6 course id in core/utils

:bug:Fix cs6 course id in core/utils

--- app/core/utils.coffee
+++ app/core/utils.coffee
@@ -28,7 +28,7 @@
   WEB_DEVELOPMENT_2: '5789587aad86a6efb5737020'
   COMPUTER_SCIENCE_4: '56462f935afde0c6fd30fc8d'
   COMPUTER_SCIENCE_5: '569ed916efa72b0ced971447'
-  COMPUTER_SCIENCE_6: '56a683b9506a6936008ba424'
+  COMPUTER_SCIENCE_6: '5817d673e85d1220db624ca4'

 orderedCourseIDs = [
   courseIDs.INTRODUCTION_TO_COMPUTER_SCIENCE


VNC is avilable for both platforms

VNC is avilable for both platforms

--- app/models/project.coffee
+++ app/models/project.coffee
@@ -36,7 +36,8 @@
   ).property "files.@each"

   canDoDynamicScan:(->
-    ENUMS.PLATFORM.ANDROID is @get "platform"
+    return true
+    # ENUMS.PLATFORM.ANDROID is @get "platform"
   ).property 'platform'

   hasMultipleFiles:( ->


Icon: Fixed an icon name.

Icon: Fixed an icon name.

--- src/base/Icon/Icon.coffee
+++ src/base/Icon/Icon.coffee
@@ -142,7 +142,7 @@
 		user: "fa-user"
 		warning: "fa-warning"
 		west: "fa-angle-left"
-		zip: "fa fa-file-archive-o"
+		zip: "fa-file-archive-o"
 		zoom_in: "fa-search-plus"
 		zoom_out: "fa-search-minus"


phone validator

phone validator

--- src/bbf-validators.coffee
+++ src/bbf-validators.coffee
@@ -40,4 +40,13 @@
         type: options.type
         message: options.message

+  Form.validators.phone = (options) ->
+    options = _.extend
+      type: 'phone'
+      message: 'Incorrect phone number'
+      regexp: /^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$/
+    , options
+
+    Form.validators.regexp options
+
   return


setting size to none result in a flush like disable does

--- src/benchmark/pipe.coffee
+++ src/benchmark/pipe.coffee
@@ -33,7 +33,6 @@

             file.stdout.pipe buffer
             buffer.pipe res
-            do buffer.disable

             setTimeout( () =>
                 @info "set buffer size to none"


Removed some obsolete code from number.coffee

--- lib/number.coffee
+++ lib/number.coffee
@@ -194,17 +194,6 @@
 # Javascript number, a `LongInt` is produced instead.

 class CheckedInt extends NumberBase
-  getval = (x) ->
-    if x instanceof CheckedInt
-      x.val
-    else if typeof x == 'number'
-      if Math.floor(x) == x
-        x
-      else
-        throw new Error "expected an integer, got #{n}"
-    else
-      throw new Error "expected a number, got #{n}"
-
   constructor: (@val = 0) ->

   neg__: -> new CheckedInt -@val
@@ -258,15 +247,13 @@
   dump = (s) -> rdump s?.reverse()

   ZERO = seq [0]
-  ONE  = seq [1]
-  TWO  = seq [2]

   cleanup = (s) -> s?.reverse()?.dropWhile((x) -> x == 0)?.reverse() or null

   sqrt = (s) ->
     n = s.size()
     step = (r) ->
-      rn = seq div(add(r, div(s, r)), TWO)
+      rn = seq div(add(r, div(s, r)), seq([2]))
       if cmp(r, rn) then -> step(rn) else rn
     trampoline step s.take n >> 1


Reverting back headers method

Reverting back headers method

--- lib/oauth1.coffee
+++ lib/oauth1.coffee
@@ -49,13 +49,6 @@
 				return keyset[val].join params[val].separator || ","
 			return keyset[val]

-	_createHeaders: (token, parameters) ->
-		headers = {}
-		headers["Accept"] = short_formats[token.format] || token.format if token.format
-		for name, value of token.headers
-			param = @_replaceParam value, @_params, {}, parameters
-			headers[name] = param if param
-
 	authorize: (provider, parameters, opts, callback) ->
 		@_setParams(provider.parameters)
 		@_setParams(provider.oauth1.parameters)
@@ -79,7 +72,11 @@
 		for name, value of request_token.query
 			param = @_replaceParam value, @_params, state:state.id, callback:config.host_url+config.relbase, parameters
 			query[name] = param if param
-		headers = @_createHeaders(opts.options?.request_token, parameters)
+		headers = {}
+		headers["Accept"] = short_formats[request_token.format] || request_token.format if request_token.format
+		for name, value of request_token.headers
+			param = @_replaceParam value, @_params, {}, parameters
+			headers[name] = param if param
 		options =
 			url: request_token.url
 			method: request_token.method?.toUpperCase() || "POST"


Materials: Workaround for missing lamp names exported by Blender


Account credentials: hide internal meta field and unescape credential values

--- client/account/lib/accountcredentiallist.coffee
+++ client/account/lib/accountcredentiallist.coffee
@@ -1,5 +1,6 @@
 kd                          = require 'kd'
 hljs                        = require 'highlight.js'
+_                           = require 'lodash'

 KDListView                  = kd.ListView
 KDModalView                 = kd.ModalView
@@ -120,9 +121,12 @@
     credential.fetchData (err, data) ->
       return if showError err

-      data.meta.identifier = credential.identifier
-
-      cred = JSON.stringify data.meta, null, 2
+      { meta } = data
+
+      meta            = helper.prepareCredentialMeta meta
+      meta.identifier = credential.identifier
+
+      cred = JSON.stringify meta, null, 2
       cred = hljs.highlight('json', cred).value

       new KDModalView
@@ -146,6 +150,7 @@
     credential.fetchData (err, data) ->
       return if showError err

+      data.meta  = helper.prepareCredentialMeta data.meta
       data.title = credential.title

       new AccountCredentialEditModal { provider, credential }, data
@@ -196,3 +201,11 @@

         console.warn "Verify failed:", err
         err
+
+
+  helper =
+
+    prepareCredentialMeta: (meta) ->
+
+      delete meta.__rawContent
+      return _.mapValues meta, (val) -> _.unescape val


Adds something

Adds something

--- client/apps/edit/components/layout/index.coffee
+++ client/apps/edit/components/layout/index.coffee
@@ -141,7 +141,7 @@
     fullText = @getBodyText()
     return fullText.match(/==(\S+.*?)==/ig)

-  autolinkText: ->
+  autolinkText: =>
     linkableText = @getLinkableText()
     console.log linkableText
     async.mapSeries linkableText, (findText, cb) =>


:bird: Fix lack of cut/copy/paste/selectall roles in browser window

--- app/metal/menu.coffee
+++ app/metal/menu.coffee
@@ -24,6 +24,33 @@
           label: "Quit"
           accelerator: "CmdOrCtrl+Q"
           click: -> AppActions.quit()
+        }
+      ]
+    }
+
+    edit: {
+      label: "Edit"
+      visible: false
+      submenu: [
+        {
+          label: "Cut"
+          accelerator: "CmdOrCtrl+X"
+          role: "cut"
+        }
+        {
+          label: "Copy"
+          accelerator: "CmdOrCtrl+C"
+          role: "copy"
+        }
+        {
+          label: "Paste"
+          accelerator: "CmdOrCtrl+V"
+          role: "paste"
+        }
+        {
+          label: "Select all"
+          accelerator: "CmdOrCtrl+A"
+          role: "selectall"
         }
       ]
     }
@@ -79,7 +106,7 @@
     }
   }

-  template = [menus.file]
+  template = [menus.file, menus.edit]

   if AppStore.get_current_user()
     template.push menus.account


Fix scoping when adding a stream

Fix scoping when adding a stream

--- src/streammachine/master/admin/assets/js/admin/admin.js.coffee
+++ src/streammachine/master/admin/assets/js/admin/admin.js.coffee
@@ -241,7 +241,7 @@
             modal.on "save", =>
                 console.log "save called on ", stream
                 # try saving the new stream to the server
-                stream.save {}, success:(model,res) ->
+                stream.save {}, success:(model,res) =>
                     console.log "got success", res
                     @collection.add model
                     $(modal.render().el).modal "hide"


hide toolpad after press enter

hide toolpad after press enter

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -202,6 +202,8 @@
 		.delegate 'input', 'keydown', (e)->
 			if self.lastLinkElement
 				self.lastLinkElement.setAttribute 'href', $(@).val()
+			if e.keyCode == 13
+				self.hideToolpad()

 		$('#toolpad').delegate '.cancel', 'click', (e)=>
 			@cancelLinkInput()


fix :bug: #95

fix :bug: #95

--- lib/text-object.coffee
+++ lib/text-object.coffee
@@ -501,7 +501,7 @@
       {cursor} = selection
       cursor.moveToBeginningOfLine()
       cursor.moveToFirstCharacterOfLine() if @isInner()
-      selection.selectToEndOfLine()
+      selection.selectToEndOfBufferLine()

 class ACurrentLine extends CurrentLine
   @extend()


Add paragraph to the theme section

Add paragraph to the theme section

--- lib/theme-panel.coffee
+++ lib/theme-panel.coffee
@@ -19,6 +19,7 @@
   @content: ->
     @div class: 'section', id: 'themes-config', =>
       @h1 class: 'section-heading', "Themes"
+      @p 'Drag themes between the Available Themes and the Enabled Themes sections'
       @div id: 'theme-picker', =>
         @div class: 'panel', =>
           @div class: 'panel-heading', "Enabled Themes"


Don't require login for verifyEmail and recovery.

--- lib/router.coffee
+++ lib/router.coffee
@@ -84,6 +84,8 @@
     except: [
       "join"
       "signin"
+      "verifyEmail"
+      "recovery"
     ]

 if Meteor.isClient


No need to store options array after parse

--- lib/schema.coffee
+++ lib/schema.coffee
@@ -3,9 +3,9 @@
 inflection = require 'inflection'

 class Relationship
-  constructor: (@model_type, @key, @options_array) ->
-    @to_model_type = @options_array[0]
-    @[key] = value for key, value of @options_array[1]
+  constructor: (@model_type, @key, options_array) ->
+    @to_model_type = options_array[0]
+    @[key] = value for key, value of options_array[1]
     @foreign_key = inflection.foreign_key(@key) unless @foreign_key
     # foreign_key: options.foreign_key or @_keyFromTypeAndModel(relation_type, from_model, to_model, options.reverse)


Pass test with two turtles

Pass test with two turtles


fix email validation condition (mail input)

--- client/app/components/mails_input.coffee
+++ client/app/components/mails_input.coffee
@@ -246,9 +246,8 @@
                     # Trick to make sure that the alert error is not pop up
                     # twiced due to multiple blur and key down.
                     # Do not display anything when the field is blurred.
-                    if not @isShowingAlert
-                    and @state.contacts.length is 0
-                    and not isBlur
+                    isContacts = @state.contacts?.length is 0
+                    if not @isShowingAlert and not isBlur and isContacts
                         @isShowingAlert = true
                         alert t('compose wrong email format',
                             address: address.address)


Make create-model button use assist() instead of direct call

--- src/ext/components/frame-output.coffee
+++ src/ext/components/frame-output.coffee
@@ -146,7 +146,7 @@
     dataRows: createDataRows offset, rowCount, columns

   createModel = ->
-    _.insertAndExecuteCell 'cs', "buildModel null, training_frame: #{stringify _frame.key.name}"
+    _.insertAndExecuteCell 'cs', "assist buildModel, null, training_frame: #{stringify _frame.key.name}"

   data: _frame
   key: _frame.key.name


Redirect if successful

Redirect if successful

--- web/coffee/account.coffee
+++ web/coffee/account.coffee
@@ -2,7 +2,7 @@
   e.preventDefault()
   apiCall "POST", "/api/user/update_password", $("#password-update-form").serializeObject()
   .done (data) ->
-    apiNotify data
+    apiNotify data, "/account"

 resetPassword = (e) ->
   e.preventDefault()
@@ -10,8 +10,7 @@
   form.token = window.location.hash.substring(1)
   apiCall "POST", "/api/user/confirm_password_reset", form
   .done (data) ->
-    apiNotify data
-
+    apiNotify data, "/login"
 $ ->
   $("#password-update-form").on "submit", updatePassword
   $("#password-reset-form").on "submit", resetPassword


Changed condition to pass only for production

--- server/app.coffee
+++ server/app.coffee
@@ -30,16 +30,15 @@
 app.set('port', parseInt(process.env["PORT"], 100) or config.port or 3000)

 # Live bundle reloading in development
-if env is "development"
+if env is not "production"
   compiler = webpack(webpackConfig)
-  webpackDevMiddleware = webpackDevMiddleware(compiler,
+  compiler.plugin("done", io.onInvalided)
+  webpackDevMiddleware = webpackDevMiddleware compiler,
     stats:
       assets: false
       colors: true
       chunks: false
       version: false
-  )
-  compiler.plugin("done", io.onInvalided)
   app.use(webpackDevMiddleware)

 # Serve all static files from static folder


Fixing possible bug with option-less deprecations

--- app/app.coffee
+++ app/app.coffee
@@ -14,7 +14,7 @@
       return false if test
       if already_shown.indexOf(msg) == -1
         warning = "DEPRECATION: #{msg}"
-        warning += " See: #{opt.url}" if opt.url
+        warning += " See: #{opt.url}" if opt && opt.hasOwnProperty 'url'
         console.debug warning
       already_shown.push msg
   )()


Exo.Spine.List fixed a bug where the node.nodeId() was not properly compared

--- src/spine/list.coffee
+++ src/spine/list.coffee
@@ -55,7 +55,7 @@

 	# Find children that have been deleted from the collection. Deactivate them, remove them from the DOM and make them available for GC.
 	deactivateAndKillOrphans: (children, collection) ->
-		orphans = children.filter (child) -> child.id not in collection.map (item) -> item.constructor.className + item.id
+		orphans = children.filter (child) -> child.nodeId() not in collection.map (item) -> item.constructor.className + item.id
 		for orphan in orphans
 			if orphan.isActivated() and not orphan.isBusy()
 				orphan.bind 'onDeactivated', (controller) =>


Add decision and decision letter to ember paper model

--- app/assets/javascripts/models/paper.js.coffee
+++ app/assets/javascripts/models/paper.js.coffee
@@ -2,6 +2,8 @@
 ETahi.Paper = DS.Model.extend
   shortTitle: a('string')
   title: a('string')
+  decision: a('string')
+  decisionLetter: a('string')
   assignees: DS.hasMany('assignee')
   phases: DS.hasMany('phase')
   declarations: DS.hasMany('declaration')


絆システムの対応を修正

絆システムの対応を修正

--- app/assets/javascripts/models/party.js.coffee
+++ app/assets/javascripts/models/party.js.coffee
@@ -84,12 +84,12 @@
       m = mems[k]
       return unless m

-      if Arcana.sameCharacter(m.arcana, ta) || Arcana.sameCharacter(m.arcana, tc)
+      if Arcana.sameArcana(m.arcana, ta) || Arcana.sameArcana(m.arcana, tc)
         mems[k] = null
         return

       return unless m.chainArcana
-      if Arcana.sameCharacter(m.chainArcana, ta) || Arcana.sameCharacter(m.chainArcana, tc)
+      if Arcana.sameArcana(m.chainArcana, ta) || Arcana.sameArcana(m.chainArcana, tc)
         mems[k] = new Member(m.arcana)
     mems


added comment with links to a known problem

--- src/draft4/logical.coffee
+++ src/draft4/logical.coffee
@@ -26,6 +26,9 @@
         runtime.error context, data

+  # Note from author of draft4 on how allOf works w/r/t additionalProperties
+  # http://stackoverflow.com/questions/22689900/json-schema-allof-with-additionalproperties/23001194#23001194
+  # also https://github.com/fge/json-schema-validator/issues/88
   allOf: (definition, context) ->
     unless @test_type "array", definition
       throw new Error "The 'allOf' attribute must be an array"


Fix goto function definition shortcut

Fix goto function definition shortcut

--- lib/existdb.coffee
+++ lib/existdb.coffee
@@ -63,7 +63,7 @@
         @subscriptions.add atom.commands.add 'atom-workspace', 'existdb:toggle-tree-view': => @treeView.toggle()
         @subscriptions.add atom.commands.add 'atom-workspace', 'existdb:goto-definition': =>
             editor = atom.workspace.getActiveTextEditor()
-            def = @getFunctionDefinition(editor, editor.getCursorBufferPosition())
+            def = XQUtils.getFunctionDefinition(editor, editor.getCursorBufferPosition())
             @gotoDefinition(def.signature, editor) if def?

     deactivate: ->


Record db slice with error message.

Record db slice with error message.

--- lib/worker.coffee
+++ lib/worker.coffee
@@ -18,8 +18,9 @@
     @_pubsub = @queue._worker_pubsub
     @_kv = @queue._worker_kv
     @_counters = {}
-    @req_channel = _('requests').namespace @queue.options.db_index
-    @resp_channel = _('responses').namespace @queue.options.db_index
+    @slice = @queue.options.db_index
+    @req_channel = _('requests').namespace @slice
+    @resp_channel = _('responses').namespace @slice
     @cache = {}
     unless @runner = @queue.runners[@prefix]
       @emit @key, null
@@ -255,7 +256,7 @@
   error: (err) ->
     trace = err.stack ? err
     tail = @_error_tail ? []
-    tail.unshift { trace, @key }
+    tail.unshift { trace, @key, @slice }
     @_emit @key, error: tail

   # Call the runner. We optionally


exit with 1 on api error

exit with 1 on api error

--- evilbot.coffee
+++ evilbot.coffee
@@ -71,6 +71,7 @@
       response.setEncoding('utf8')
       response.on 'data', (chunk) ->
         console.log chunk
+      process.exit(1)

   req.write(body) if method is 'POST' and body
   req.end()


Show all assignment groups in the gradebook

fixes CNVS-23943

Quick patch to handle pagination in the gradebook page of the
assignment groups endpoint, introduced as a performance optimization.

Future commits should extract pagination logic since it's shared with
students (duplicated) in this file.

also make sure we clear the rows array before building
it always, and that we make data loading students
order dependent on already having assignments loaded

TEST PLAN:
1) have more than 10 assignment groups
2) go to the grade book
3) see all your assignment groups

Change-Id: I9b9491b7ae81d01bda2b1f33d9eb98fdb762d655
Reviewed-on: https://gerrit.instructure.com/64963
Reviewed-by: Keith T. Garner <a1a80143a840c140f31c3495ebbd9ec906e09fed@instructure.com>
Reviewed-by: Matthew Wheeler <ce71fbafda91d6c8f12582e1837687d5fe592dd9@instructure.com>
Product-Review: Ethan Vizitei <73eeb0b2f65d05a906adf3b21ee1f9f5a2aa3c1c@instructure.com>
QA-Review: Pedro Fajardo <121b528f0c2f2e923ce3103793aca31b9a662ac4@instructure.com>
Tested-by: Ethan Vizitei <73eeb0b2f65d05a906adf3b21ee1f9f5a2aa3c1c@instructure.com>

--- app/coffeescripts/gradebook2/Gradebook.coffee
+++ app/coffeescripts/gradebook2/Gradebook.coffee
@@ -102,18 +102,21 @@
       else
         'students_url'

-      gotAllStudents = $.Deferred().done => @gotAllStudents()
+      @gotAllAssignmentGroupsPromise = $.Deferred().done => @gotAllAssignmentGroups()
+      gotAllStudents = $.Deferred().done(()=>
+        @gotAllAssignmentGroupsPromise.then(()=> @gotAllStudents())
+      )

       # this method should be removed after a month in production
       @alignCoursePreferencesWithLocalStorage()

-      assignmentGroupsParams = {exclude_descriptions: true}
+      @assignmentGroupsParams = {exclude_descriptions: true}
       if @mgpEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
-        $.extend(assignmentGroupsParams, {grading_period_id: @gradingPeriodToShow})
+        $.extend(@assignmentGroupsParams, {grading_period_id: @gradingPeriodToShow})

       ajax_calls = [
         $.ajaxJSON(@options[enrollmentsUrl], "GET")
-      , $.ajaxJSON(@options.assignment_groups_url, "GET", assignmentGroupsParams, @gotAssignmentGroups)
+      , $.ajaxJSON(@options.assignment_groups_url, "GET", @assignmentGroupsParams, @gotAssignmentGroups)
       , $.ajaxJSON( @options.sections_url, "GET", {}, @gotSections)
       ]

@@ -155,7 +158,7 @@
           gotAllStudents.resolve()

       gotCustomColumns = @getCustomColumns()
-      @gotAllData = $.when(gotCustomColumns, gotAllStudents)
+      @gotAllData = $.when(gotCustomColumns, gotAllStudents, @gotAllAssignmentGroupsPromise)

       @allSubmissionsLoaded.done =>
         for c in @customColumns
@@ -271,7 +274,8 @@
       @getSubmissionsChunks()
       @initHeader()

-    gotAssignmentGroups: (assignmentGroups) =>
+    gotAllAssignmentGroups: () =>
+      assignmentGroups = @allAssignmentGroups;
       @assignmentGroups = {}
       @assignments      = {}

@@ -290,6 +294,39 @@

       @postGradesStore.setGradeBookAssignments @assignments

+
+    gotAssignmentGroups: (assignmentGroups, xhr) =>
+      @allAssignmentGroups = [];
+
+      gotAssignmentGroupsChunk = (groups) =>
+        for group in groups
+          @allAssignmentGroups.push(group)
+
+      gotAssignmentGroupsChunk(assignmentGroups)
+
+
+      paginationLinks = xhr.getResponseHeader('Link')
+      lastLink = paginationLinks.match(/<[^>]+>; *rel="last"/)
+      unless lastLink?
+        @gotAllAssignmentGroupsPromise.resolve()
+        return
+      lastPage = lastLink[0].match(/page=(\d+)/)[1]
+      lastPage = parseInt lastPage, 10
+      if lastPage == 1
+        @gotAllAssignmentGroupsPromise.resolve()
+        return
+      agParams = @assignmentGroupsParams
+      fetchAssignmentGroupsChunk = (page) =>
+        $.ajaxJSON @options.assignment_groups_url, "GET", $.extend(agParams, {page})
+      dfds = (fetchAssignmentGroupsChunk(page) for page in [2..lastPage])
+      $.when(dfds...).then (responses...) =>
+        if dfds.length == 1
+          gotAssignmentGroupsChunk(responses[0])
+        else
+          gotAssignmentGroupsChunk(groups) for [groups, x, y] in responses
+        @gotAllAssignmentGroupsPromise.resolve()
+
+
     gotCourse: (course) =>
       @course = course

@@ -318,6 +355,8 @@

     gotAllStudents: ->
       @withAllStudents (students) =>
+        #empty the array so this is idempotent, you can end up with too many rows
+        @rows.length = 0
         for student_id, student of students
           student.computed_current_score ||= 0
           student.computed_final_score ||= 0


We started to using manifest.json instead .manifest make sure you have a link for your apps. :/

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -71,7 +71,7 @@

         apps.forEach (app)=>
           stack.push (cb)=>
-            manifest = FSHelper.createFileFromPath "#{app.path}/.manifest"
+            manifest = FSHelper.createFileFromPath "#{app.path}/manifest.json"
             manifest.fetchContents (err, response)->
               # shadowing the error is intentional here
               # to not to break the result of the stack


minor control

minor control

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -226,7 +226,7 @@

   renderLogo: ->

-    src = @state.team.getIn(['customize', 'logo']) or DEFAULT_LOGOPATH
+    src = @state.team?.getIn(['customize', 'logo']) or DEFAULT_LOGOPATH

     <img src="#{src}" className='Sidebar-footer-logo' />


video: fix start video behavior

video: fix start video behavior

--- client/app/lib/videocollaboration/model.coffee
+++ client/app/lib/videocollaboration/model.coffee
@@ -266,13 +266,12 @@
         error   : (err) -> console.error err

     session.on 'signal:start', =>
-
       @setActive()
-      @changeActiveParticipant getNick()
-
-      return  unless @isMySession()
-
-      @requestVideoPublishForHost()
+
+      if @isMySession()
+        @requestVideoPublishForHost => @changeActiveParticipant getNick()
+      else
+        @changeActiveParticipant getNick()

     # this event only comes to the user who has been muted, so need to make a


Be more specific about publish date input

--- client/apps/edit/components/admin/index.coffee
+++ client/apps/edit/components/admin/index.coffee
@@ -317,7 +317,7 @@
   setupEmailMetadata: ->
     if @article.get('email_metadata')?.image_url
       @renderInputs @article.get('email_metadata').image_url
-    else if @article.get('thumbnail_image').length
+    else if @article.get('thumbnail_image')?.length
       @renderInputs @article.get('thumbnail_image')

     new ImageUploadForm
@@ -457,8 +457,8 @@
     clientFormat = moment(date).format('L')
     publishTime = moment(date).format('HH:mm')
     @saveFormat = moment(date).toDate()
-    $('.edit-admin-input-date').val(clientFormat)
-    $('.edit-admin-input-time').val(publishTime)
+    $('#edit-admin-publish-date input').val(clientFormat)
+    $('#edit-admin-schedule-publish-time input').val(publishTime)

   toggleScheduled: (e) ->
     e.preventDefault()


focus text field when editor mounts

focus text field when editor mounts

--- client/lanes/components/grid/EditingMixin.cjsx
+++ client/lanes/components/grid/EditingMixin.cjsx
@@ -34,7 +34,8 @@
     listenNetworkEvents: true
     getDefaultProps: -> editors: {}
     componentDidMount: ->
-        _.dom(@).qs('input').focusAndSelect()
+        qs = if @props.initiallyFocusedField then ".field[data-id=\"#{@props.initiallyFocusedField}\"] input" else 'input'
+        _.defer => _.dom(@).qs(qs).focusAndSelect()

     renderControls: ->
         if @props.allowDelete
@@ -95,6 +96,7 @@
             @renderDisplayValue(props)

         <div key={field.id}
+            data-id={field.id}
             style = {@props.cellStyles.styles[index]}
             className = {_.classnames('field', @props.cellStyles.classes[index])}
         >


Use regex to clean code strings

Use regex to clean code strings

--- client/lanes/models/mixins/HasCodeField.coffee
+++ client/lanes/models/mixins/HasCodeField.coffee
@@ -1,13 +1,18 @@
+DEFAULT_INVALID = /[^A-Z0-9a-z]/
+
 Lanes.Models.Mixins.HasCodeField = {

-    INVALID: /[^A-Z0-9a-z]/

-    included: (klass)->
-        klass::INVALID_CODE_CHARS ||= Lanes.Models.mixins.Lanes.sCodeField.INVALID
+    included: (klass) ->
+        klass::INVALID_CODE_CHARS ||= DEFAULT_INVALID

     initialize: ->
-        this.on('change:code', this.upcaseCode)
+        this.on('change:code', this._cleanCodeAttr)

-    upcaseCode: ->
-        this.set('code', this.get('code').toUpperCase())
+    _cleanCodeAttr: ->
+        this.set(
+            'code', this.get('code').toUpperCase().replace(@INVALID_CODE_CHARS, '')
+        )
+
+    visibleIdentifier: -> @code
 }


Move grid to SkuLines component

Move grid to SkuLines component

--- client/skr/screens/sales-order/SalesOrder.cjsx
+++ client/skr/screens/sales-order/SalesOrder.cjsx
@@ -9,60 +9,24 @@
                 initialFieldIndex: 1
                 title: 'Sales Order'
                 syncOptions:
-                    include: ['billing_address', 'shipping_address', 'lines']
-                    with: ['with_details', 'customer_code', 'customer_name']
+                    include: [ 'billing_address', 'shipping_address', 'lines'   ]
+                    with:    [ 'with_details', 'customer_code', 'customer_name' ]
                 src: Skr.Models.SalesOrder, fields: [
-                    { id: 'id', visible: false}
+                    { id: 'id', visible: false }
                     { id: 'visible_id' }
                     { id: 'customer_code' }
                     { id: 'notes', flex: 2}
                     { id: 'order_total', title: 'Total' }
                 ]
             })
-        linesQuery: ->
-            new Lanes.Models.Query({
-                src: Skr.Models.SoLine, fields: [
-                    { id:'id', visible: false}
-                    { id: 'sku_code' }
-                    { id: 'description', flex: 2}
-                    { id: 'uom', title: 'UOM', query: false, format: (v, r, q) -> v.combined }
-                    { id: 'qty',   textAlign: 'center' }
-                    { id: 'price', textAlign: 'right'  }
-                ]
-            })

     getInitialState: ->
         isEditing: true
-        commands: new Lanes.Screens.Commands(this,
-            modelName: 'sales_order'
-            modelDidRebind: (model) => @linesQuery.src = @sales_order.lines
-        )
+        commands: new Lanes.Screens.Commands(this, modelName: 'sales_order')

     componentDidMount: ->
         @sales_order.visible_id = '1'
         @refs.finder.loadCurrentSelection()
-
-    editors: (props) ->
-        sku_code: ({model}) ->
-            <LC.SelectField
-                key="sku-code"
-                editOnly writable unstyled
-                model={model}
-                syncOptions={include: 'sku', fields: 'sku_code'}
-                labelField="sku_code"
-                name="sku_loc"
-            />
-        uom: ({model}) ->
-            return '' unless model.sku_loc_id
-            <LC.SelectField
-                key="uom"
-                editOnly writable unstyled
-                model={model}
-                queryOrder={size: 'desc'}
-                labelField="combined"
-                name="uom"
-                collection={model.uom_choices}
-            />

     render: ->
         <div className="sales-order flex-vertically" >
@@ -107,23 +71,7 @@
                 </LC.FieldSet>
             </BS.Row>

-            <LC.Grid query={@linesQuery}
-                commands={@state.commands}
-                autoLoadQuery={false}
-                expandY={true}
-                columEditors={@editors()}
-                editorProps: {syncImmediatly: false}
-                height=200
-                allowDelete={true}
-                allowCreate={true}
-                editor={true}
-            />
-            <BS.Row>
-                <BS.Col md=10 className="text-right">
-                    Total:
-                </BS.Col>
-                <BS.Col md=2>
-                    <Skr.Components.Currency amount={@sales_order.total} />
-                </BS.Col>
-            </BS.Row>
+            <Skr.Components.SkuLines commands={@state.commands} lines={@sales_order.lines} />
+
+            <Skr.Components.OrderTotals model={@sales_order} />
         </div>


[test] tag Shippable tests as such on Saucelabs.

--- smoke-test.coffee
+++ smoke-test.coffee
@@ -28,6 +28,7 @@
 tags = []
 tags.push('travis') if env.TRAVIS
 tags.push('drone') if env.DRONE
+tags.push('shippable') if env.USER is 'shippable'
 tags.push(env.CI_NAME) if env.CI_NAME

 browser = wd.remote('ondemand.saucelabs.com', 80, sauceUser, sauceKey)


Add back removed check

Add back removed check

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -159,7 +159,7 @@
     unless names instanceof Array
       names = [names]
     startDir = startDir.split(path.sep)
-    while startDir.length
+    while startDir.length and startDir.join(path.sep)
       currentDir = startDir.join(path.sep)
       for name in names
         filePath = path.join(currentDir, name)


:fire: Remove an unnecessary import

:fire: Remove an unnecessary import

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -1,5 +1,4 @@
 {Range} = require('atom')
-XRegExp = require('xregexp').XRegExp
 path = require 'path'
 child_process = require('child_process')


add tests for cookie header

add tests for cookie header

--- components/main_layout/header/test/view.coffee
+++ components/main_layout/header/test/view.coffee
@@ -7,6 +7,8 @@
 HeaderView = rewire '../view'
 HeaderView.__set__ 'SearchBarView', sinon.stub()
 HeaderView.__set__ 'AuthModalView', sinon.stub()
+HeaderView.__set__ 'readCookie', sinon.stub()
+HeaderView.__set__ 'createCookie', sinon.stub()

 { resolve } = require 'path'

@@ -21,19 +23,30 @@
           $window: @$window =
             on: sinon.stub()
             off: sinon.stub()
+            scrollTop: -> 55
           $body: $('body')
         done()

   it 'hides the welcome header on scroll', ->
     @$window.on.args[0][0].should.equal 'scroll.welcome-header'
-    @$window.on.args[0][1].should.equal @view.hideWelcomeHeader
+    @$window.on.args[0][1].should.equal @view.checkRemoveWelcomeHeader

   describe '#hideWelcomeHeader', ->
+    beforeEach ->
+      @view.$welcomeHeader = height: (-> 50), remove: sinon.stub()
+
     it 'hides the welcome header when scrolling past the search bar', ->
-      @view.$welcomeHeader = height: (-> 50), hide: sinon.stub()
+      @view.$welcomeHeader = height: (-> 50), remove: sinon.stub()
       @view.$window.scrollTop = -> 55
-      @view.hideWelcomeHeader()
-      @view.$welcomeHeader.hide.called.should.be.ok
+      @view.checkRemoveWelcomeHeader()
+      @view.$welcomeHeader.remove.called.should.be.ok
+      @view.$window.off.called.should.be.ok
+
+    it 'hides the welcome header if there is a cookie', ->
+      HeaderView.__set__ 'readCookie', -> true
+      @view.$window.scrollTop = -> 0
+      @view.checkRemoveWelcomeHeader()
+      @view.$welcomeHeader.remove.called.should.be.ok
       @view.$window.off.called.should.be.ok

   # describe '#login', ->


indexed: made the class name a property of the class, not the instance.

--- lib/indexed.coffee
+++ lib/indexed.coffee
@@ -242,7 +242,7 @@
   toArray: -> @entries?.toArray() or []

   # Returns a string representation of this collection.
-  toString: -> "#{@className}(#{@root})"
+  toString: -> "#{@class.name}(#{@root})"

 # --------------------------------------------------------------------
@@ -269,7 +269,7 @@
 # The IntSet class.
 class IntSet extends Collection
   this::class = this
-  className: "IntSet"
+  @name = "IntSet"

   # Returns the elements as a stream
   elements: -> @entries
@@ -320,7 +320,7 @@
 # The IntMap class is essentially a huge sparse array.
 class IntMap extends Collection
   this::class = this
-  className: "IntMap"
+  @name = "IntMap"

   # Returns the (key,value)-pairs as a stream
   items: -> @entries
@@ -445,7 +445,7 @@
 # for the various node classes that hold the actual information.
 class HashSet extends Collection
   this::class = this
-  className: "HashSet"
+  @name = "HashSet"

   # Returns the elements as a stream
   elements: -> @entries
@@ -508,7 +508,7 @@
 # for the various node classes that hold the actual information.
 class HashMap extends Collection
   this::class = this
-  className: "HashMap"
+  @name = "HashMap"

   # Returns the (key,value)-pairs as a stream
   items: -> @entries


add osx_image to keys-map

add osx_image to keys-map

--- app/utils/keys-map.coffee
+++ app/utils/keys-map.coffee
@@ -26,6 +26,7 @@
   lein:        'Lein'
   compiler:    'Compiler'
   crystal:     'Crystal'
+  osx_image:   'Xcode'
 }

 configKeys = {


Add Perl6 to matrix

Add Perl6 to matrix

--- app/utils/keys-map.coffee
+++ app/utils/keys-map.coffee
@@ -3,6 +3,7 @@
   php:         'PHP'
   node_js:     'Node.js'
   perl:        'Perl'
+  perl6:       'Perl6'
   python:      'Python'
   scala:       'Scala'
   ruby:        'Ruby'


commit

commit

--- app/modules/casting/casting.controller.coffee
+++ app/modules/casting/casting.controller.coffee
@@ -30,11 +30,11 @@
         user = @auth.getUser()
         if user

-            promise = @castingService.getCastingMembers(false)
-            promise.then (data) =>
-                @scope.castingMembers = data.toJS()
+            #promise = @castingService.getCastingMembers(false)
+            #promise.then (data) =>
+            #    @scope.castingMembers = data.toJS()

-            #@scope.castingMembers = @currentUserService.inventory.get("all").toJS()
+            @scope.castingMembers = @currentUserService.inventory.get("all").toJS()
             #@scope.castingMembers = @castingService.inventory.get("all").toJS()

             promise = @.loadInitialData()


fix clear cart button

fix clear cart button

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -262,7 +262,7 @@
           specTxtHtml = "

#{ db[v.product]['title_product'] }

-                            <btn class='Cart-removeItem Grid-cell u-textRight' style='align-self:start;'>
+                            <btn class='Cart-removeItem'>
                               close
                             </btn>


Removed dead code

Removed dead code

--- core/app/backbone/models/evidence.coffee
+++ core/app/backbone/models/evidence.coffee
@@ -1,5 +1,4 @@
 class window.Evidence extends Backbone.Model
-  positiveImpact: -> @get('impact') >= 0

   toJSON: ->
     _.extend super(),


Avoid extra declare cycle when refreshing vparams

--- lib/session.coffee
+++ lib/session.coffee
@@ -679,20 +679,30 @@
     allVirtualParameters = virtualParametersCache.get(sessionContext)

     vpd = vpd.filter((declaration) ->
-      if declaration[0].length == 1
-        if Object.keys(allVirtualParameters).length
-          toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, {object: [timestamp, 1], writable: [timestamp, 0]}, toClear)
-        else
-          toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
-      else if declaration[0].length == 2
-        if declaration[0][1] == '*'
-          for k, v of allVirtualParameters
-            toClear = device.set(sessionContext.deviceData, ['VirtualParameters', k], timestamp, {object: [timestamp, 0]}, toClear)
-          toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
-        else if not (declaration[0][1] of allVirtualParameters)
-          toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
-        else
-          return true
+      if Object.keys(allVirtualParameters).length
+        if declaration[0].length == 1
+          # Avoid setting on every inform as "exist" timestamp is not saved in DB
+          if not sessionContext.deviceData.attributes.has(declaration[0])
+            toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, {object: [timestamp, 1], writable: [timestamp, 0]}, toClear)
+          return false
+        else if declaration[0].length == 2
+          if declaration[0][1] == '*'
+            for k, v of allVirtualParameters
+              toClear = device.set(sessionContext.deviceData, ['VirtualParameters', k], timestamp, {object: [timestamp, 0]}, toClear)
+            toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, null, toClear)
+            return false
+          else if declaration[0][1] of allVirtualParameters
+            # Avoid setting on every inform as "exist" timestamp is not saved in DB
+            if not sessionContext.deviceData.attributes.has(declaration[0])
+              toClear = device.set(sessionContext.deviceData, declaration[0], timestamp, {object: [timestamp, 0]}, toClear)
+            return true
+
+      for p in sessionContext.deviceData.paths.find(declaration[0], false, true)
+        if sessionContext.deviceData.attributes.has(p)
+          toClear ?= []
+          toClear.push([declaration[0], timestamp])
+          break
+
       return false
     )


handle request_error events

handle request_error events

--- src/client.coffee
+++ src/client.coffee
@@ -20,6 +20,8 @@
             callback(null, client)
           error: (response) ->
             callback(response)
+          request_error: (error) ->
+            throw error
     else
       throw "Expected to receive a String, but got something else"

@@ -270,10 +272,16 @@
       # FIXME: I can't figure out why, but if I don't do the hokey pokey with
       # the default "response" handler here, Shred selects it over specific
       # status code handlers. Might be a Shred bug.
-      for status in [202, 204, "error", "response"]
+      for status in [202, 204, "error", "request_error", "response"]
         if handler = options.on[status]
           request.on[status] = handler
           delete options.on[status]
+
+
+      # never silently die on request errors.
+      # TODO: allow a default request_error handler on Client construction
+      request.on.request_error ||= (err) ->
+        throw err

       # TODO: figure out how Shred handles 30x and assess whether Patchboard
       # needs to care.


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -55,7 +55,7 @@

   hslToRgb = (hsl) ->
     [h, s, l, a] = (parseFloat(channel) for channel in hsl)
-    h = (h / 360.0).round()
+    h /= 360.0
     a ||= 1.0

     r = g = b = null
@@ -74,9 +74,9 @@
     else
       q = (if l < 0.5 then l * (1 + s) else l + s - l * s)
       p = 2 * l - q
-      r = hueToRgb(p, q, h + 1/3).round()
-      g = hueToRgb(p, q, h).round()
-      b = hueToRgb(p, q, h - 1/3).round()
+      r = hueToRgb(p, q, h + 1/3)
+      g = hueToRgb(p, q, h)
+      b = hueToRgb(p, q, h - 1/3)

       rgbMap = (channel * 0xFF for channel in [r, g, b])


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -6,9 +6,15 @@
       when 0
         parsedColor = [0, 0, 0, 1]
       when 1
-        if Object.isArray(colorArray = args.first())
-          parsedColor = (parseFloat(channel) for channel in colorArray)
-
+        if Object.isArray(channels = args.first())
+          parsedColor = (parseFloat(channel) for channel in channels)
+      when 2
+        if Object.isArray(channels = args.first())
+          parsedColor = (parseFloat(channel) for channel in channels)
+          parsedColor[3] ||= parseFloat(args[1])
+        #else
+        #  parsedColor = lookup[normalizeKey(color)] || parseHex(color) || parseRGB(color) || parseHSL(color)
+        #  parsedColor[3] = alpha

     r: parsedColor[0]
     g: parsedColor[1]


The twatting client now takes params to tweet

Work toward passing media in a bit.

--- lib/twitter.coffee
+++ lib/twitter.coffee
@@ -15,9 +15,20 @@
   # as a tweet, and calls you back with it, as well
   # as a pre-built URL.
   #
+  # body - String to tweet
+  # opts - (optional) Any other parameters to add to the outgooing update
+  # cb   - Finish callback invoked in the form cb(err, tweet, url)
+  #
   # Callback invoked as `cb(err, tweet, url)`
-  tweet: (body, cb=(->)) =>
-    params = status: body
+  tweet: (body, opts, cb) =>
+    # "Optional" for you "Annoying" for me
+    if typeof(opts) is typeof(->)
+      cb = opts
+      opts = {}
+
+    (param[key] = opts[key] for own key of opts)
+    params.status = body
+
     client.post 'statuses/update', params, (error, tweet, response) ->
       return cb(error) if error


Prevent Base component from choking when @props.meta is missing

--- base/Base.cjsx
+++ base/Base.cjsx
@@ -41,6 +41,7 @@
     getDefaultProps: -> {
         className: ''
         client_modules: {}
+        meta: {}
     }

     render: ->


Add contributor

Add contributor

--- assets/data/constant.cson
+++ assets/data/constant.cson
@@ -71,4 +71,8 @@
     name: 'Gizeta',
     avatar: 'https://avatars2.githubusercontent.com/u/1572561?v=3&s=400'
     link: 'https://github.com/Gizeta'
+  ,
+    name: 'KagamiChan',
+    avatar: 'https://avatars2.githubusercontent.com/u/3816900?v=3&s=400'
+    link: 'https://github.com/KagamiChan'
 ]


Minor changes with Atom

Minor changes with Atom

--- atom/atom/config.cson
+++ atom/atom/config.cson
@@ -60,7 +60,8 @@
       "autocomplete-bibtex"
       "wrap-guide"
       "whitespace"
-      "tabs"
+      "vim-mode-plus"
+      "vim-mode-clipboard-plus"
     ]
     ignoredNames: [
       ".git"
@@ -77,7 +78,6 @@
   editor:
     autoIndentOnPaste: false
     fontFamily: "Hack"
-    fontSize: 12
     invisibles:
       space: "•"
       tab: "›"
@@ -146,7 +146,6 @@
     silent: true
   "one-dark-ui":
     fontSize: 11
-    layoutMode: "Compact"
   "one-light-ui":
     fontSize: 10
     layoutMode: "Compact"


auto pin mode

auto pin mode

--- grovepi.coffee
+++ grovepi.coffee
@@ -75,25 +75,25 @@

 	receive: ( port, args... ) ->
-		@mode port, MODES.input
-		readArgs = []
-		if args[1]
-			length = args[0]
-			callback = args[1]
-			readArgs.push port
-			readArgs.push length
-			readCmd = 'readBytes'
-		else
-			callback = args[0]
-			readCmd = 'readByte'
+		@mode port, MODES.input, ->
+			readArgs = []
+			if args[1]
+				length = args[0]
+				callback = args[1]
+				readArgs.push port
+				readArgs.push length
+				readCmd = 'readBytes'
+			else
+				callback = args[0]
+				readCmd = 'readByte'

-		debug.log 'Receive from port', port
-		readArgs.push (error, data) ->
-			callback data
-			debug.log 'Received', (Array.prototype.slice.call data, 0)
-			debug.log 'ERROR:', error  if error
+			debug.log 'Receive from port', port
+			readArgs.push (error, data) ->
+				callback data
+				debug.log 'Received', (Array.prototype.slice.call data, 0)
+				debug.log 'ERROR:', error  if error

-		wire[readCmd] readArgs...
+			wire[readCmd] readArgs...


Get grid size split from MY

Get grid size split from MY

--- src/config.coffee
+++ src/config.coffee
@@ -7,10 +7,9 @@

 exports.config = {
   device: my.device
-  size: 480
-  split: 6
-  scale: (world, args) ->
-    world.get('size') / world.get('split')
+  size: my.grid.size
+  split: my.grid.split
+  scale: (world, args) -> world.get('size') / world.get('split')
   i: 0
   j: 0
   angle: 0


adiciona método de substituição de classe

--- lib/assets/javascripts/do/helpers/jquery.coffee
+++ lib/assets/javascripts/do/helpers/jquery.coffee
@@ -1,6 +1,22 @@
 $.fn.findByHref = (href)->
   $(this).find "a[href='#{href}']"

+
+$.fn.replaceToggleClass = (cls1, cls2)->
+    obj = $(this)
+
+    obj.each (i)->
+        current = obj.item(i)
+        if current.classList.contains(cls1)
+            $(current).removeClass cls1
+            $(current).addClass cls2
+        else if current.classList.contains(cls2)
+                $(current).removeClass cls2
+                $(current).addClass cls1
+
+    # setTimeout ->, interval || 0
+
+    obj

 $.fn.replaceClass = (sought, substitute, interval)->
     obj = $(this)
@@ -8,6 +24,7 @@
         obj.each (i)->
             current = obj.item(i)
             if current.classList.contains(sought)
+                # de repentemente é só remover a classe antiga e adicionar a nova através do jquery, ao invés de fazer via proprio método de manipulação de string
                 className = current.className.allReplaced "(#{sought}|\\s#{sought}|#{sought}\\s)", " #{substitute} "
                 current.className = className.trim().allReplaced("  ", " ")
     , interval || 0


createSubClass now accepts rule and ruleVariant options

--- src/babel_bridge/nodes/node.coffee
+++ src/babel_bridge/nodes/node.coffee
@@ -15,6 +15,8 @@
   @createSubClass: (options) ->
     class NodeSubClass extends @
       @_name = @prototype._name = options.name if options.name
+      @ruleVarient = options.ruleVarient if options.ruleVarient
+      @rule = options.rule if options.rule
       mergeInto @prototype, options

   toString: -> @text


SearchController: handle missing Algolia config

--- client/app/lib/searchcontroller.coffee
+++ client/app/lib/searchcontroller.coffee
@@ -24,7 +24,11 @@

   initAlgolia: ->

-    { appId } = globals.config.algolia
+    { appId } = globals.config.algolia ? {}
+
+    unless appId
+      kd.warn 'Algolia search is disabled because of missing configuration'
+      return @ready = yes

     @ready = no

@@ -97,7 +101,7 @@
     new Promise (resolve, reject) =>

       return reject new Error 'Search not ready yet'  unless @ready
-
+      return reject new Error 'Algolia search is disabled'  unless @algolia
       return reject new Error 'Illegal input'  if seed is ''

       index = @getIndex "#{ indexName }#{ globals.config.algolia.indexSuffix }"
@@ -160,6 +164,8 @@

     { showCurrentUser } = opts
     delete opts.showCurrentUser
+
+    return @searchAccountsMongo seed, { showCurrentUser }  if @ready and not @algolia

     @search 'accounts', seed, opts
       .then (data) ->


Fix message order inside conversation

Fix message order inside conversation

--- client/app/stores/message_store.coffee
+++ client/app/stores/message_store.coffee
@@ -18,7 +18,7 @@
     _sortOrder   = 1
     _filter      = '-'
     _quickFilter = ''
-    __getSortFunction = (criteria) ->
+    __getSortFunction = (criteria, order) ->
         sortFunction = (message1, message2) ->
             if typeof message1.get is 'function'
                 val1 = message1.get criteria
@@ -26,11 +26,11 @@
             else
                 val1 = message1[criteria]
                 val2 = message2[criteria]
-            if val1 > val2 then return -1 * _sortOrder
-            else if val1 < val2 then return 1 * _sortOrder
+            if val1 > val2 then return -1 * order
+            else if val1 < val2 then return 1 * order
             else return 0

-    __sortFunction = __getSortFunction 'date'
+    __sortFunction = __getSortFunction 'date', 1

     # Creates an OrderedMap of messages
     _messages = Immutable.Sequence()
@@ -213,7 +213,7 @@
     getMessagesByMailbox: (mailboxID) ->
         sequence = _messages.filter (message) ->
             return mailboxID in Object.keys message.get 'mailboxIDs'
-        .sort(__getSortFunction _sortField)
+        .sort(__getSortFunction _sortField, _sortOrder)

         ###
         if _filter isnt MessageFilter.ALL
@@ -271,7 +271,7 @@
             newIdsToLook = temp.map((item) -> item.get('id')).toArray()
             idsToLook = idsToLook.concat newIdsToLook

-        return conversation
+        return conversation.sort(__getSortFunction 'date', -1)

     getConversation: (conversationID) ->
         conversation = []
@@ -279,7 +279,7 @@
             return message.get('conversationID') is conversationID
         .map (message) -> conversation.push message
         .toJS()
-        return conversation
+        return conversation.sort(__getSortFunction 'date', -1)

     getParams: -> return _params


Implement syncFileSystem saving.

Implement syncFileSystem saving.


add diag()

add diag()

--- matrixSpec.coffee
+++ matrixSpec.coffee
@@ -266,3 +266,19 @@
                                  0, 0, 1 ]
       expect(M.I(3, 2)).toEqual M [ 1, 0, 0
                                     0, 1, 0 ], 3
+
+
+  describe 'diag()', ->
+
+    it 'creates a new diagonal matrix', ->
+      expect(M.diag(M [2, 3, 4], 1)).toEqual M [ 2, 0, 0
+                                                 0, 3, 0
+                                                 0, 0, 4 ]
+
+    it 'can use existing buffer', ->
+      T = (M.I 3).fill 5
+      D = M.diag((M [2, 3, 4], 1), T)
+      expect(D).toEqual M [ 2, 0, 0
+                            0, 3, 0
+                            0, 0, 4 ]
+      expect(D).toBe T


Fix narrative editing

Fix narrative editing

--- client/src/views/narrative_view.coffee
+++ client/src/views/narrative_view.coffee
@@ -7,6 +7,7 @@

   events:
     "click .save-narrative": "saveNarrative"
+    "click .content-text": "startEdit"
     # textarea resize-related events
     "change textarea.content-text-field": "resize"
     "cut textarea.content-text-field": "delayedResize"
@@ -33,9 +34,10 @@
     return @

   saveNarrative: (event) =>
-    @narrative.set('title', "title")
-    @narrative.set('content',
-      @$el.find('.content-text-field').val().replace(/^\s+|\s+$/g, ''))
+    @narrative.set(
+      content: @$el.find('.content-text-field').val().replace(/^\s+|\s+$/g, '')
+      editing: false
+    )
     @narrative.save()

   # Following 2 methods are used for dynamically resize the textarea.
@@ -50,6 +52,10 @@
   delayedResize: ->
     setTimeout @resize, 0

+  startEdit: =>
+    @narrative.set('editing', true)
+    @render()
+
   onClose: ->


Tests: Fixed selector in waitForVMRunning helper.

--- client/test/lib/helpers/helpers.coffee
+++ client/test/lib/helpers/helpers.coffee
@@ -52,7 +52,7 @@
     unless machineName
       machineName = 'koding-vm-0'

-    vmSelector     = '[href="/IDE/' + machineName + '"].running.vm'
+    vmSelector     = ".#{machineName} .running.vm"
     modalSelector  = '.env-modal.env-machine-state'
     loaderSelector = modalSelector + ' .kdloader'
     buildingLabel  = modalSelector + ' .state-label.building'