Update zh-HANT.ozar.coffee

Update zh-HANT.ozar.coffee

--- app/locale/zh-HANT.ozar.coffee
+++ app/locale/zh-HANT.ozar.coffee
@@ -325,21 +325,21 @@

   play:
 #    title: "Play CodeCombat Levels - Learn Python, JavaScript, and HTML"
-#    meta_description: "Learn programming with a coding game for beginners. Learn Python or JavaScript as you solve mazes, make your own games, and level up. Challenge your friends in multiplayer arena levels!"
-#    level_title: "__level__ - Learn to Code in Python, JavaScript, HTML"
+    meta_description: "通過適合初學者的編碼遊戲學習程式設計。在解決迷宮、製作自己的遊戲與升級的過程中學習 Python 或 JavaScript。在多人競技場關卡中挑戰你的朋友！"
+    level_title: "__level__ - 學習 Python, JavaScript, HTML 的程式開發"
 #    video_title: "__video__ | Video Level"
-#    game_development_title: "__level__ | Game Development"
-#    web_development_title: "__level__ | Web Development"
-#    anon_signup_title_1: "CodeCombat has a"
-#    anon_signup_title_2: "Classroom Version!"
-#    anon_signup_enter_code: "Enter Class Code:"
-#    anon_signup_ask_teacher: "Don't have one? Ask your teacher!"
-#    anon_signup_create_class: "Want to create a class?"
-#    anon_signup_setup_class: "Set up a class, add your students, and monitor progress!"
+    game_development_title: "__level__ | 遊戲開發"
+    web_development_title: "__level__ | 網頁開發"
+    anon_signup_title_1: "CodeCombat 也有"
+    anon_signup_title_2: "教室版本!"
+    anon_signup_enter_code: "輸入教室代號:"
+    anon_signup_ask_teacher: "找不到嗎? 問問你的指導老師喔!"
+    anon_signup_create_class: "想建立一間教室嗎?"
+    anon_signup_setup_class: "建立教室, 添加您的學生並且關注他們的學習進度!"
 #    anon_signup_create_teacher: "Create free teacher account"
     play_as: "扮演" # Ladder page
-#    get_course_for_class: "Assign Game Development and more to your classes!"
-#    request_licenses: "Contact our school specialists for details."
+    get_course_for_class: "將游戲開發等分配給您的課程!"
+    request_licenses: "聯繫我們的學校專家了解詳情."
     compete: "競賽!" # Course details page
     spectate: "觀戰" # Ladder page
     players: "玩家" # Hover over a level on /play
@@ -374,15 +374,15 @@
     brain_pop_done: "你剛剛用編程戰勝了巨魔! 你贏了!"
     brain_pop_challenge: "自我挑戰！用另一種程式語言再玩一次。"
     replay: "重播"
-    back_to_classroom: "回到教室"
+    back_to_classroom: "返回教室"
     teacher_button: "老師專用功能"
-#    get_more_codecombat: "Get More CodeCombat"
-#    back_to_dashboard: "Back to Dashboard"
-#    back_to_ozaria: "Back to ozaria.com"
-#    customize_hero: "Customize Hero"
-#    account_settings: "Account Settings"
-#    back_to_tutorial: "Back to tutorial"
-#    teacher_vega_message: "Students are guided by these tutorial messages. In order to view these messages and to save your progress as you play through the game, please create a student account and play Ozaria as a student."
+    get_more_codecombat: "取得更多 CodeCombat" # "Get More CodeCombat"
+    back_to_dashboard: "返回管理介面"
+    back_to_ozaria: "返回 ozaria.com"
+    customize_hero: "自訂英雄角色"
+    account_settings: "帳號設定"
+    back_to_tutorial: "返回入門教學"
+    teacher_vega_message: "這些入門教學資訊將指導學生, 為了不影響您原本的遊戲進度與查看這些教學資訊, 請創建一個學生帳戶並利用它來玩 Ozaria" # "Students are guided by these tutorial messages. In order to view these messages and to save your progress as you play through the game, please create a student account and play Ozaria as a student."

   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.)
@@ -443,10 +443,10 @@
     title: "做得不錯，同學"

   login:
-#    auth_sign_up: "New to Ozaria? Create an Account"
+    auth_sign_up: "剛到 Ozaria? 建立一個帳號吧" # "New to Ozaria? Create an Account"
     sign_up: "註冊"
     email_or_username: "輸入Email帳號 或是 使用者名稱" # {change}
-#    or: "or"
+    or: "或"
     log_in: "登入"
 #    sign_in: "Sign In"
     logging_in: "登入中"
@@ -456,11 +456,11 @@
     sign_in_with_facebook: "使用 Facebook 登入" # {change}
     sign_in_with_gplus: "使用 Google+ 登入" # {change}
     signup_switch: "建立一個帳號"
-#    already_have_account1: "Already have a CodeCombat Teacher or Student account?"
-#    already_have_account2: "You can sign into Ozaria.com using that account."
-#    sign_into_ozaria: "Sign Into Ozaria.com"
-#    individual_users_not_supported: "Ozaria is for classrooms only!  Please visit CodeCombat to continue playing!"
-#    accounts_merge_confirmation: "There is an account associated with the email from this Google account. Would you like to merge these accounts?"
+    already_have_account1: "已經有 CodeCombat 教師或學生帳號了嗎?" # "Already have a CodeCombat Teacher or Student account?"
+    already_have_account2: "您能使用這些帳號登入 Ozaria.com" # "You can sign into Ozaria.com using that account."
+    sign_into_ozaria: "登入 Ozaria.com"
+    individual_users_not_supported: "Ozaria 是教室模式! 請到 CodeCombat 繼續遊戲!" # "Ozaria is for classrooms only!  Please visit CodeCombat to continue playing!"
+    accounts_merge_confirmation: "已經有帳號與這個 Google Email關聯. 您想合併這些帳號嗎?" # "There is an account associated with the email from this Google account. Would you like to merge these accounts?"

   signup:
     complete_subscription: "完成訂閱"
@@ -469,7 +469,7 @@
     create_individual_header: "建立私人帳號"
     email_announcements: "接收通知以了解CodeCombat中的新關卡和新功能!"
     sign_in_to_continue: "登入或建立一個新帳號以繼續"
-#    teacher_email_announcements: "Keep me updated on new teacher resources, curriculum, and courses!"
+    teacher_email_announcements: "讓我持續獲得新的教材與教學資源等資訊!" # "Keep me updated on new teacher resources, curriculum, and courses!"
     creating: "帳號建立中…"
     sign_up: "註冊"
     log_in: "登入"
@@ -593,7 +593,7 @@
     loading: "載入中…"
 #    LOADING: "LOADING"
     overview: "總覽"
-#    processing: "Processing..."
+    processing: "處理中..."
     solution: "解決方法"
 #    table_of_contents: "Table of Contents"
     intro: "簡介"
@@ -617,14 +617,14 @@
     submit_changes: "送出修改"
     save_changes: "保存更改"
     required_field: "需求"
-#    submit: "Submit"
-#    replay: "Replay"
-#    complete: "Complete"
+    submit: "送出"
+    replay: "重播"
+    complete: "完成"
 #    continue_editing: "Continue Editing"
 #    logout: "Logout"
 #    empty: "Empty"
 #    done: "Done"
-#    remove: "Remove"
+    remove: "移除"
 #    locked: "Locked"

   general:
@@ -638,7 +638,7 @@
     rejected: "未接受"
     withdrawn: "已撤回"
     accept: "接受"
-#    accept_and_save: "Accept&Save"
+    accept_and_save: "接受並儲存"
     reject: "拒絕"
     withdraw: "撤回"
     submitter: "提交者"
@@ -963,7 +963,7 @@
     play: "開始玩關卡"
     learn_about_ozaria: "Learn About Ozaria"
     default_student_instructions: "利用點擊來控制英雄，贏得遊戲！"
-#    goal_survive: "Survive."
+    goal_survive: "生存."
 #    goal_survive_time: "Survive for __seconds__ seconds."
 #    goal_defeat: "Defeat all enemies."
 #    goal_defeat_amount: "Defeat __amount__ enemies."


 add in a description of ! in header of manifest file (#2884)

* add in help line for results page

add in help line for results page

* fixed the accidental R deletion

fixed the accidental R deletion

* added in help description for how to reorder workflows

added in a line of text to the workflow page help to tell project
builders how to reorder the listing of their workflows on the project
landing page.

* add in a description of ! in header of manifest file

! in the field header then the values will be shown in Talk only and
not in the classification interface. Adding in to the subject set help
info about this for project owners.

--- app/pages/lab/subject-set.cjsx
+++ app/pages/lab/subject-set.cjsx
@@ -164,6 +164,7 @@
           Drag-and-drop or click to upload manifests and subject images here.

           Manifests must be .csv or .tsv. The first row should define metadata headers. All other rows should include at least one reference to an image filename in the same directory as the manifest.

           Headers that begin with "#" or "//" denote private fields that will not be visible to classifiers in the main classification interface or in the Talk discussion tool.

+          Headers that begin with "!" denote fields that will not be visible to classifiers in the main classification interface but will be  visible after classification in the Talk discussion tool.

           Subject images can be up to {MAX_FILE_SIZE / 1024}KB and any of: {{ext}{', ' if VALID_SUBJECT_EXTENSIONS[i + 1]?} for ext, i in VALID_SUBJECT_EXTENSIONS}{' '}
           and may not contain {{char}{', ' if INVALID_FILENAME_CHARS[i + 1]?} for char, i in INVALID_FILENAME_CHARS}

         </UploadDropTarget>


add a form disabled when click

add a form disabled when click

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -253,13 +253,14 @@
       # create a new user
       $scope.create = (form, params) ->
         # console.log(params)
+        form.disabled = true

         Registration.post(params).then((response) ->
           user =
-              username:response.username
-              first_name:response.first_name
-              last_name:response.last_name
-              email:response.email
+              username:$scope.username
+              first_name:$scope.first_name
+              last_name:$scope.last_name
+              email:$scope.email

           localStorage.setItem("user", JSON.stringify(user))

@@ -271,6 +272,7 @@
           # form.error = "Error Inscription " + JSON.stringify(response.error, null, '\t')
           form.error = "Error Inscription " + response.error

+          form.disabled = false
         )

       # identification confirmation update email


Tests: Clean up PUSH IN MIDDLE in master_stream test

We weren't silencing the normal FileSource emit, so it was pushing unexpected
data during our test.

--- test/master_stream.coffee
+++ test/master_stream.coffee
@@ -161,7 +161,7 @@
         source = null
         before (done) ->
             stream  = new MasterStream null, "test1", logger, _.extend {}, STREAM1, hls:{segment_duration:10}
-            source  = new FileSource format:"mp3", filePath:mp3, chunkDuration:0.5
+            source  = new FileSource format:"mp3", filePath:mp3, chunkDuration:0.5, do_not_emit:true
             stream.addSource source

             source.once "_loaded", ->
@@ -183,6 +183,8 @@
         it "should have created HLS segments", (done) ->
             this.timeout 5000
             stream.rewind.hls_segmenter.once "snapshot", (snapshot) =>
-                expect(snapshot.segments).to.have.length 4
+                # depending on how our segment boundary falls, this could be
+                # three or four
+                expect(snapshot.segments).to.have.length.within 3,4
                 expect(snapshot.segment_duration).to.eq 10
                 done()


fix tests

fix tests

--- app/selectors/libs.spec.coffee
+++ app/selectors/libs.spec.coffee
@@ -11,7 +11,7 @@
   {name: 'cool'}
 ]

-withQuery = (query) -> {query, libs: {items: LIBS}}
+withQuery = (query) -> {query, data: {libs: LIBS}}

 describe 'libs', ->
   describe '#filteredByQuery', ->


improve readibility of the dumpers

improve readibility of the dumpers

--- app/templates/_Gulpfile.coffee
+++ app/templates/_Gulpfile.coffee
@@ -142,11 +142,19 @@
 # 	http://dev.mysql.com/doc/refman/5.1/en/mysqldump.html
 #*------------------------------------*/
 gulp.task "db_dump:local", shell.task [
-  'mysqldump --host=' + pvt.db_local.host + ' --user=' + pvt.db_local.user + ' --password=' + pvt.db_local.pass + ' ' + pvt.db_local.name + ' > ' + './database/dev/dev-db-' + Date.now() + '.sql'
+  'mysqldump --host=' + pvt.db_local.host +
+  ' --user=' + pvt.db_local.user +
+  ' --password=' + pvt.db_local.pass +
+  ' ' + pvt.db_local.name +
+  ' > ' + './database/dev/dev-db-' + Date.now() + '.sql'
 ]

 gulp.task "db_dump:prod", shell.task [
-  'mysqldump --host=' + pvt.db_prod.host + ' --user=' + pvt.db_prod.user + ' --password=' + pvt.db_prod.pass + ' ' + pvt.db_prod.name + ' > ' + './database/prod/prod-db-' + Date.now() + '.sql'
+  'mysqldump --host=' + pvt.db_prod.host +
+  ' --user=' + pvt.db_prod.user +
+  ' --password=' + pvt.db_prod.pass +
+  ' ' + pvt.db_prod.name + ' > ' +
+  './database/prod/prod-db-' + Date.now() + '.sql'
 ]


Add CocoView.scrollToTop

Add CocoView.scrollToTop

--- app/views/core/CocoView.coffee
+++ app/views/core/CocoView.coffee
@@ -444,6 +444,9 @@
   scrollToLink: (link, speed=300) ->
     scrollTo = $(link).offset().top
     $('html, body').animate({ scrollTop: scrollTo }, speed)
+
+  scrollToTop: (speed=300) ->
+    $('html, body').animate({ scrollTop: 0 }, speed)

   toggleFullscreen: (e) ->
     # https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode?redirectlocale=en-US&redirectslug=Web/Guide/DOM/Using_full_screen_mode


Test: change open file text selector

Test: change open file text selector

speculation: since `.ace_content` is deeply nested i think nightwatch is having hard times accessing it.

--- tests/src/ide/file.coffee
+++ tests/src/ide/file.coffee
@@ -25,9 +25,9 @@
       .click                   indexSelector + ' + .chevron'
       .waitForElementVisible   'li.open-file', 20000
       .click                   'li.open-file'
-      .waitForElementVisible   '.indexhtml',   20000 # Assertion
-      .waitForElementVisible   '.ace_content', 20000 # Assertion
-      .assert.containsText     '.ace_content', 'Hello World from HTML by Koding' # Assertion
+      .waitForElementVisible   '.indexhtml', 20000 # Assertion
+      .waitForElementVisible   '.kdtabpaneview.indexhtml', 20000 # Assertion
+      .assert.containsText     '.kdtabpaneview.indexhtml', 'Hello World from HTML by Koding' # Assertion
       .end()


Cosmetic function change

Cosmetic function change

--- dpr.coffee
+++ dpr.coffee
@@ -7,7 +7,7 @@
   return format arg if typeof arg is 'string'

   # Configure if arg is an object
-  return configure arg if typeof arg is 'object'
+  return config arg if typeof arg is 'object'

   # Return the current DPR
   get()
@@ -66,7 +66,7 @@
       ($t = $ @).attr(src: dpr $t.data 'dprSrc').removeAttr 'data-dpr-src'

 # Define a configure method for easy option setting
-(configure = (options) ->
+(config = (options) ->
   dpr[name] = option for name, option of options
   if dpr.scanOnLoad and $
     $ -> dpr.scan()


conflict fix

conflict fix

--- client/app/MainApp/maincontroller/mainview.coffee
+++ client/app/MainApp/maincontroller/mainview.coffee
@@ -110,7 +110,6 @@
       appController = KD.getSingleton "kodingAppsController"
       frontApp = appManager.getFrontApp()
       frontAppName = name for name, instances of appManager.appControllers when frontApp in instances
-      console.log frontAppName
       appController.constructor.manifests?[frontAppName]

     @mainSettingsMenuButton = new KDButtonView


cancel button and upload to memory options added to dnd uploader

--- client/landing/app/CommonViews/DNDUploader.coffee
+++ client/landing/app/CommonViews/DNDUploader.coffee
@@ -4,6 +4,7 @@

     options.cssClass      = "file-droparea"
     options.hoverDetect  ?= yes
+    options.uploadToVM   ?= yes
     options.defaultPath or= "/home/#{KD.nick()}/Uploads"

     super options, data
@@ -42,11 +43,12 @@
       event.stopPropagation()

   reset: ->
+    {uploadToVM, defaultPath} = @getOptions()
     @setPath()
     @updatePartial """

         Drop files here!
-        #{@getOptions().defaultPath}
+        #{if uploadToVM then defaultPath else ''}

     """

@@ -63,7 +65,7 @@
     lastFile = files[files.length-1]
     for file, index in files
       sizeInMb = file.size/1000/1000
-      if sizeInMb > 5
+      if sizeInMb > 5 && @getOptions().uploadToVM
         new KDNotificationView
           type    : "tray"
           title   : "File is too big to upload!"
@@ -72,11 +74,12 @@
         continue
       reader = new FileReader
       reader.onloadend = do (file=files[index])=> (event)=>
-        @upload file.name, event.target.result
+        if @getOptions().uploadToVM
+          @upload file.name, event.target.result

         if file is lastFile
           @reset()
-          @emit "drop"
+          @emit "drop", file.name, event.target.result

       reader.readAsBinaryString file

@@ -84,12 +87,18 @@
     @updatePartial """

         Drop files here!
-        #{FSHelper.getVMNameFromPath(@path) or ''}
-        #{FSHelper.plainPath @path}
+        #{if @getOptions().uploadToVM then (FSHelper.getVMNameFromPath(@path) or '') else ''}
+        #{if @getOptions().uploadToVM then FSHelper.plainPath @path else ''}

     """
+    @addSubView new KDCustomHTMLView
+      tagName   : "a"
+      partial   : "cancel"
+      cssClass  : "cancel"
+      attributes: href: "#"
+      click     : => @emit "cancel"

-  upload: (file, data, callback=noop)->
+  upload: (file, data)->
     modalStack  = KDModalView.createStack lastToFirst: yes
     uploadDir   = FSHelper.createFileFromPath @path, 'folder'
     uploadFile  = FSHelper.createFileFromPath "#{@path}/#{file}"


remove unsaved event when cancel is clicked

--- client/skr/screens/time-tracking/PopoverEdit.cjsx
+++ client/skr/screens/time-tracking/PopoverEdit.cjsx
@@ -19,12 +19,14 @@
             change.event.set({content: @entry.content}, {silent: true})
         @forceUpdate()

-
     getTarget: ->
         _.dom(this.props.parent).el

     onCancel: ->
-        this.props.event.set(editing: false)
+        if @entry.isNew()
+            @props.event.remove()
+        else
+            this.props.event.set(editing: false)

     onSave: ->
         @entry.save().then (te) =>


no path.existSync deprecation warning in node .8

--- code/config.coffee
+++ code/config.coffee
@@ -3,6 +3,7 @@
 _    = require 'underscore'

 settings = (require 'settings').settings
+exists = fs.existsSync or path.existsSync

 exports.Config = class Config
   hooks: []
@@ -15,7 +16,7 @@

   _load_config: (name) ->
     config_path = "#{settings.config_path}/#{name}/config.json"
-    return unless path.existsSync config_path
+    return unless exists config_path
     text = fs.readFileSync config_path
     config = JSON.parse text
     if config.include?


Try to set this in a way that actually works.

--- tests/youtube-info.coffee
+++ tests/youtube-info.coffee
@@ -13,7 +13,7 @@

     it 'should be able to find title', (done) ->

-      @timeout 10000
+      @timeout(10000)

       room.user.say 'alice', 'https://www.youtube.com/watch?v=ePoi0_zSnYk'
       setTimeout =>
@@ -26,7 +26,7 @@

     it 'should be able to find title', (done) ->

-      @timeout 10000
+      @timeout(10000)

       room.user.say 'alice', 'https://www.youtube.com/watch?v=iq9DLJfpHd0&feature=youtu.be&ab_channel=CatRe-Tailer'
       setTimeout =>
@@ -39,7 +39,7 @@

     it 'should be able to find title', (done) ->

-      @timeout 10000
+      @timeout(10000)

       room.user.say 'alice', 'https://youtu.be/HP66dH1yEZo'
       setTimeout =>
@@ -52,7 +52,7 @@

     it 'should be able to find title', (done) ->

-      @timeout 10000
+      @timeout(10000)

       room.user.say 'alice', 'https://youtu.be/oVnAVcbMoSM?t=39s'
       setTimeout =>


Update areFunctionsEqual comment

Update areFunctionsEqual comment

--- lib/yargs-command/yargs-command.spec.coffee
+++ lib/yargs-command/yargs-command.spec.coffee
@@ -25,9 +25,9 @@
 	noMatch:
 		_: [ 'foo', 'bar', 'baz' ]

-# Hacky way to check that both functions are equal based
-# on what they returns, as mocha crashes when trying equal
-# or deep equal directly on the functions for some reason
+# Hacky way to check that both functions are equal
+# as mocha crashes when trying equal or deep equal
+# directly on the functions for some reason
 areFunctionsEqual = (fn1, fn2) ->
 	return fn1.toString() is fn2.toString()


Pre-populate the location search field with what we know

--- apps/personalize/client/views/location.coffee
+++ apps/personalize/client/views/location.coffee
@@ -1,3 +1,4 @@
+_                   = require 'underscore'
 StepView            = require './step.coffee'
 LocationSearchView  = require '../../../../components/location_search/index.coffee'
 GeoFormatter        = require 'geoformatter'
@@ -6,6 +7,15 @@
 module.exports = class LocationView extends StepView
   events:
     'click a': 'advance'
+
+  # @return {String} used as the suggested search text that
+  # pre-populates the location search field
+  locationDisplay: ->
+    _.compact([
+      @user.get('location')?.city
+      @user.get('location')?.state_code
+      @user.get('location')?.country
+    ]).join ', '

   update: (location) ->
     @geo = new GeoFormatter(location)
@@ -30,5 +40,5 @@

   postRender: ->
     @locationSearchView = new LocationSearchView el: @$('#personalize-location-search')
-    @locationSearchView.render()
+    @locationSearchView.render @locationDisplay()
     @listenTo @locationSearchView, 'location:update', @update


Stripe checkout should pass zeroes for month + year if expiry field is blank


Fixing tests.

Fixing tests.

--- tests.coffee
+++ tests.coffee
@@ -20990,6 +20990,9 @@
   (test, expect) ->
     insertedItem = SubfieldItem.documents.findOne @item._id
     test.equal insertedItem, @item
+
+    # To make sure there are no residual observes going into @changes.
+    waitForDatabase test, expect
 ,
   (test, expect) ->
     itemId = Random.id()


Fix specs

Fix specs

--- lib/status-bar-manager.coffee
+++ lib/status-bar-manager.coffee
@@ -29,7 +29,7 @@
   # Private

   attach: ->
-    {statusBar} = atom.workspaceView
+    statusBar = atom.workspaceView?.statusBar
     if statusBar?
       statusBar.prependRight(@element)
       @disposables.add =>


add #nextImageSrc to subject loader, fixes no more subjects behavior

--- classify/subject-loader.coffee
+++ classify/subject-loader.coffee
@@ -11,11 +11,14 @@
       @firstSubject = false
       ClassifyMetadata.load()

+  nextImageSrc: (queueNum) ->
+    classifyPage.Subject.instances[queueNum]?.location.standard
+
   nextImage: (queue = 2) ->
     # queue is 2 by default to load the image 2 ahead of subject into the offscreen-right position
     $ "

-
+

"

   init: ->


'local.json' file now is supplemented by the missed properties

--- lib/tasks/build/bundle.coffee
+++ lib/tasks/build/bundle.coffee
@@ -34,8 +34,10 @@
 				return callback err

 			if fs.existsSync('./local.json')
-				local = JSON.parse fs.readFileSync './local.json', 'utf-8'
-				local = utils.mergeAll {}, DEFAULT_LOCAL_FILE, local
+				localFile = JSON.parse fs.readFileSync './local.json', 'utf-8'
+				local = {}
+				utils.mergeDeep local, DEFAULT_LOCAL_FILE
+				utils.mergeDeep local, localFile
 			else
 				local = DEFAULT_LOCAL_FILE
 			fs.writeFileSync './local.json', JSON.stringify(local, null, 4)


DevTools: add ToggleFullscreenMenuView for context menu, needs some improvements too

--- client/DevTools/AppView.coffee
+++ client/DevTools/AppView.coffee
@@ -26,6 +26,26 @@

     toggleLiveReload.on "viewAppended", ->
       toggleLiveReload.parent.setClass "default"
+
+  getToggleFullscreenMenuView: (item, menu)->
+    labels = [
+      "Enter Fullscreen"
+      "Exit Fullscreen"
+    ]
+    mainView = KD.getSingleton "mainView"
+    state    = mainView.isFullscreen() or 0
+    toggleFullscreen = new KDView
+      partial : "<span>#{labels[Number state]}</span>"
+      click   : =>
+        mainView.toggleFullscreen()
+        # @getActiveAceView().toggleFullscreen()
+        menu.contextMenu.destroy()
+        # menu.click()
+    # behave like a menu item
+    toggleFullscreen.on "viewAppended", ->
+      toggleFullscreen.parent.setClass "default"
+
+
   viewAppended:->

     @addSubView @workspace      = new CollaborativeWorkspace


implement redis-store attachment for the socket servers

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -50,6 +50,7 @@
 plumbs = require "./plumbs"
 watch = require "./watch"

+{RedisStore} = require "socket.io"
 {Archetype} = require "./archetype"

 # This is a primary gateway interface for the framework. This class
@@ -307,6 +308,17 @@
         @domain.add @secureSocket; @domain.add @serverSocket
         @secureSocket.on "connection", -> newSocket "HTTPS"
         @serverSocket.on "connection", -> newSocket "HTTP"
+        return @on "redis-ready", (redis) => do (redis) =>
+            message = "Attaching Redis storage to sockets"
+            logger.debug message.toString().cyan.underline
+            assert disposition = Object redisClient: redis
+            disposition.redisPub = disposition.redisClient
+            disposition.redisSub = disposition.redisClient
+            assert compiled = new RedisStore disposition
+            assert _.isFunction pub = compiled.publish
+            compiled.publish = (n) -> pub.call compiled, n
+            @secureSocket.set "store", compiled or {}
+            @serverSocket.set "store", compiled or {}

     # Setup the Connect middleware framework along with the default
     # pipeline of middlewares necessary for the Granite framework to


Semantically reorder method defs

Semantically reorder method defs

--- library/nucleus/scaled.coffee
+++ library/nucleus/scaled.coffee
@@ -98,24 +98,21 @@
             logger.error message.red, error.stack
             return process.exit -1

-    # Setup and launch either HTTP or HTTPS servers to listen at
-    # the configured addresses and ports. This method reads up the
-    # scoping configuration in order to obtain the data necessary
-    # for instantiating, configuring and launching up the servers.
-    # This version goes to the Seaport hub to obtain the options!
-    startupHttpServer: ->
-        assert _.isObject config = nconf.get()
-        assert _.isObject(@seaport), "no seaport"
-        msg = "Got HTTP port from the Seaport: %s"
-        assert identica = @constructor.identica()
-        cfg = config: config, identica: identica
-        _.extend cfg, uuid: uuid.v4(), type: "http"
-        _.extend cfg, token: @token or undefined
-        record = @seaport.register identica, cfg
-        assert _.isNumber(record), "got mistaken"
-        logger.info msg.green, "#{record}".bold
-        assert config?.server?.http = record
-        nconf.set config; super; return this
+    # A configuration routine that ensures the scope config has the
+    # Seaport hub related configuration data. If so, it proceeds to
+    # retrieving that info and using it to locate and connect to a
+    # Seaport hub, which is then installed as the kernel instance
+    # variable, so that it can be accessed by the other routines.
+    @configure "access service Seaport hub", (next) ->
+        assert _.isString host = nconf.get "hub:host"
+        assert _.isNumber port = nconf.get "hub:port"
+        assert _.isObject opts = nconf.get "hub:opts"
+        @seaport = seaport.connect host, port, opts
+        assert _.isObject(@seaport), "seaport failed"
+        assert @seaport.register?, "a broken seaport"
+        shl = "#{host}:#{port}".toString().underline
+        msg = "Locate a Seaport hub at #{shl}".blue
+        logger.info msg; return next undefined

     # Setup and launch either HTTP or HTTPS servers to listen at
     # the configured addresses and ports. This method reads up the
@@ -136,18 +133,21 @@
         assert config?.server?.https = record
         nconf.set config; super; return this

-    # A configuration routine that ensures the scope config has the
-    # Seaport hub related configuration data. If so, it proceeds to
-    # retrieving that info and using it to locate and connect to a
-    # Seaport hub, which is then installed as the kernel instance
-    # variable, so that it can be accessed by the other routines.
-    @configure "access service Seaport hub", (next) ->
-        assert _.isString host = nconf.get "hub:host"
-        assert _.isNumber port = nconf.get "hub:port"
-        assert _.isObject opts = nconf.get "hub:opts"
-        @seaport = seaport.connect host, port, opts
-        assert _.isObject(@seaport), "seaport failed"
-        assert @seaport.register?, "a broken seaport"
-        shl = "#{host}:#{port}".toString().underline
-        msg = "Locate a Seaport hub at #{shl}".blue
-        logger.info msg; return next undefined
+    # Setup and launch either HTTP or HTTPS servers to listen at
+    # the configured addresses and ports. This method reads up the
+    # scoping configuration in order to obtain the data necessary
+    # for instantiating, configuring and launching up the servers.
+    # This version goes to the Seaport hub to obtain the options!
+    startupHttpServer: ->
+        assert _.isObject config = nconf.get()
+        assert _.isObject(@seaport), "no seaport"
+        msg = "Got HTTP port from the Seaport: %s"
+        assert identica = @constructor.identica()
+        cfg = config: config, identica: identica
+        _.extend cfg, uuid: uuid.v4(), type: "http"
+        _.extend cfg, token: @token or undefined
+        record = @seaport.register identica, cfg
+        assert _.isNumber(record), "got mistaken"
+        logger.info msg.green, "#{record}".bold
+        assert config?.server?.http = record
+        nconf.set config; super; return this


Fixes #11

Fixes #11

--- src/detectr.coffee
+++ src/detectr.coffee
@@ -157,7 +157,8 @@
         language.split('-')[0] if language?

     # Sets language attribute to browser language by default
-    document.documentElement.setAttribute 'lang', detectr.Browser.language()
+    unless document.documentElement.getAttribute 'lang'
+      document.documentElement.setAttribute 'lang', detectr.Browser.language()

     detectr.Display or=
       width: -> root.screen.width


Fix EE and the logic in the tree impl

Fix EE and the logic in the tree impl

--- library/visualize/tree.coffee
+++ library/visualize/tree.coffee
@@ -89,6 +89,7 @@
         return @$name unless name?
         invalid = "Name is not a string"
         throw new Error invalid unless correct
+        @emit("name", this, @$name, name)
         @$name = name.toString()

     # Either get or set the identification prefix string for this
@@ -100,6 +101,7 @@
         return @$prefix unless prefix?
         invalid = "Prefix is not a string"
         throw new Error invalid unless correct
+        @emit("prefix", this, @$prefix, prefix)
         @$prefix = prefix.toString()

     # Resolve the specific node by its tag. The method is recursive
@@ -113,8 +115,8 @@
         throw new Error invalid unless correct
         for own node in (@children or [])
             return node if node.tag is tag
-            element = node instanceof Element
-            return n if n = node.reslove tag
+            continue unless node instanceof Element
+            return child if child = node.reslove tag

     # Remove the supplied node from array of children nodes of
     # this element. The node undergoes some substantial testing


Don’t export the keyboardEvent

Don’t export the keyboardEvent

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -153,7 +153,7 @@
 exports.keyupEvent = (key, options) ->
   return exports.keyboardEvent(key, 'keyup', options)

-exports.keyboardEvent = (key, eventType, {ctrl, shift, alt, cmd, keyCode, target, location}={}) ->
+keyboardEvent = (key, eventType, {ctrl, shift, alt, cmd, keyCode, target, location}={}) ->
   event = document.createEvent('KeyboardEvent')
   bubbles = true
   cancelable = true


Only highlight if elements have been found

--- js-library/app/js/jail_iframe/util/highlight.coffee
+++ js-library/app/js/jail_iframe/util/highlight.coffee
@@ -77,7 +77,11 @@
   for range in ranges
     normalizedRange = new FactlinkJailRoot.Range.BrowserRange(range).normalize()
     elements = highlightRange(normalizedRange, id)
-    FactlinkJailRoot.highlightsByFactIds[id].push new FactlinkJailRoot.Highlight(id, elements)
+
+    if elements.length > 0
+      FactlinkJailRoot.highlightsByFactIds[id].push new FactlinkJailRoot.Highlight(id, elements)
+    else
+      console.error "Could not highlight, empty factlink or complete overlap? Text: <#{text}>"

 highlightFacts = (facts_data) ->
   # If there are multiple matches on the page, loop through them all


Teamwork: Destroy import modal when import started.

--- client/Teamwork/Views/teamworkworkspace.coffee
+++ client/Teamwork/Views/teamworkworkspace.coffee
@@ -189,7 +189,9 @@
           cssClass : "modal-clean-green"
           icon     : yes
           iconClass: "tw-import-icon"
-          callback : => @getDelegate().emit "ImportRequested", importUrlInput.getValue()
+          callback : =>
+            modal.destroy()
+            @getDelegate().emit "ImportRequested", importUrlInput.getValue()
         Close      :
           title    : "Cancel"
           cssClass : "modal-cancel"
@@ -198,7 +200,9 @@
     modal.addSubView importUrlInput = new KDHitEnterInputView
       type         : "text tw-import-url"
       placeholder  : "Enter the URL of a git repository or zip archive."
-      callback     : => @getDelegate().emit "ImportRequested", importUrlInput.getValue()
+      callback     : =>
+        modal.destroy()
+        @getDelegate().emit "ImportRequested", importUrlInput.getValue()

     modal.addSubView new KDCustomHTMLView
       tagName      : "span"


Use vmController for kodingAppsController oskite related calls

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

     @appManager     = KD.getSingleton "appManager"
-    @kiteController = KD.getSingleton "kiteController"
+    @vmController   = KD.getSingleton "vmController"
     mainController  = KD.getSingleton "mainController"
     @manifests      = KodingAppsController.manifests
     @publishedApps  = {}
@@ -305,7 +305,7 @@
         method   : "app.publish"
         withArgs : {appPath}

-      @kiteController.run options, (err, res)=>
+      @vmController.run options, (err, res)=>
         if err
           warn err
           notification.destroy()
@@ -343,11 +343,11 @@
         title    : "Compiling #{name}..."
         type     : "mini"

-      @kiteController.run "kdc #{appPath}", (err, response)=>
+      @vmController.run "kdc #{appPath}", (err, response)=>
         if not err
           nickname    = KD.nick()
           publishPath = "/home/#{nickname}/Web/.applications"
-          @kiteController.run
+          @vmController.run
             kiteName    : "os"
             method      : "fs.createDirectory"
             withArgs    :
@@ -356,7 +356,7 @@
           , (err, response)=>
             loader.notificationSetTitle "Publishing app static files..."
             linkFile = "#{publishPath}/#{KD.utils.slugify name}"
-            @kiteController.run "rm #{linkFile}; ln -s #{appPath} #{linkFile}", (err, response)=>
+            @vmController.run "rm #{linkFile}; ln -s #{appPath} #{linkFile}", (err, response)=>
               loader.notificationSetTitle "Fetching compiled app..."
               @fetchCompiledAppSource app, (err, res)=>
                 if not err
@@ -452,7 +452,7 @@
                       appPath     : @getAppPath app.manifest
                       version     : version

-                  @kiteController.run options, (err, res)=>
+                  @vmController.run options, (err, res)=>
                     if err then warn err
                     else
                       app.install (err)=>
@@ -567,7 +567,7 @@

     # Copy default app files (app Skeleton)
     stack.push (cb)=>
-      @kiteController.run
+      @vmController.run
         method    : "app.skeleton"
         withArgs  :
           type    : if isBlank then "blank" else "sample"
@@ -595,7 +595,7 @@
         callback new KDNotificationView type : "mini", title : "Please refresh your apps and try again!"
         return

-      @kiteController.run
+      @vmController.run
         kiteName    : "applications"
         method      : "downloadApp"
         withArgs    :


Emit a new App created signal

Emit a new App created signal

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

         async.parallel stack, (error, result) =>
           if err then warn err
+          @emit "aNewAppCreated" if not err
           callback? err, result

   # #


Activity Router : fixed the issue that group name is overridden at social worker side

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -123,49 +123,50 @@
     if passOptions
       method += 'WithOptions'
       options = {model:models, route, query}
+    KD.getSingleton("appManager").tell section, method, options ? models,
+      (contentDisplay)=>
+        routeWithoutParams = route.split('?')[0]
+        @openRoutes[routeWithoutParams] = contentDisplay
+        @openRoutesById[contentDisplay.id] = routeWithoutParams
+        contentDisplay.emit 'handleQuery', query
+
+  loadContent:(name, section, slug, route, query, passOptions)->
+    routeWithoutParams = route.split('?')[0]
+
     groupName = if section is "Groups" then name else "koding"
     KD.getSingleton('groupsController').changeGroup groupName, (err) =>
-      KD.getSingleton("appManager").tell section, method, options ? models,
-        (contentDisplay)=>
-          routeWithoutParams = route.split('?')[0]
-          @openRoutes[routeWithoutParams] = contentDisplay
-          @openRoutesById[contentDisplay.id] = routeWithoutParams
-          contentDisplay.emit 'handleQuery', query
-
-  loadContent:(name, section, slug, route, query, passOptions)->
-    routeWithoutParams = route.split('?')[0]
-    # return log name, ">>>>>"
-
-    onSuccess = (models)=> @openContent name, section, models, route, query, passOptions
-    onError   = (err)=>
-      KD.showError err
-      @handleNotFound route
-
-    if name and not slug
-      KD.remote.cacheable name, (err, models)=>
-        if models?
-        then onSuccess models
-        else onError err
-    else
-      # TEMP FIX: getting rid of the leading slash for the post slugs
-      slashlessSlug = routeWithoutParams.slice(1)
-      KD.remote.api.JName.one { name: slashlessSlug }, (err, jName)=>
-        if err then onError err
-        else if jName?
-          models = []
-          jName.slugs.forEach (aSlug, i)=>
-            {constructorName, usedAsPath} = aSlug
-            selector = {}
-            konstructor = KD.remote.api[constructorName]
-            selector[usedAsPath] = aSlug.slug
-            selector.group = aSlug.group if aSlug.group
-            konstructor?.one selector, (err, model)=>
-              return onError err if err?
-              if model
-                models[i] = model
-                if models.length is jName.slugs.length
-                  onSuccess models
-        else onError()
+      KD.showError err if err
+      onSuccess = (models)=>
+        @openContent name, section, models, route, query, passOptions
+      onError   = (err)=>
+        KD.showError err
+        @handleNotFound route
+
+      if name and not slug
+        KD.remote.cacheable name, (err, models)=>
+          if models?
+          then onSuccess models
+          else onError err
+      else
+        # TEMP FIX: getting rid of the leading slash for the post slugs
+        slashlessSlug = routeWithoutParams.slice(1)
+        KD.remote.api.JName.one { name: slashlessSlug }, (err, jName)=>
+          if err then onError err
+          else if jName?
+            models = []
+            jName.slugs.forEach (aSlug, i)=>
+              {constructorName, usedAsPath} = aSlug
+              selector = {}
+              konstructor = KD.remote.api[constructorName]
+              selector[usedAsPath] = aSlug.slug
+              selector.group = aSlug.group if aSlug.group
+              konstructor?.one selector, (err, model)=>
+                return onError err if err?
+                if model
+                  models[i] = model
+                  if models.length is jName.slugs.length
+                    onSuccess models
+          else onError()

   createContentDisplayHandler:(section, passOptions=no)->
     ({params:{name, slug}, query}, models, route)=>


code cleanup: remove a commented out line

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -165,7 +165,6 @@

     requireLogout =(fn)->
       mainController.accountReady ->
-        # console.log 'sfsfsfsfsfsf', KD.whoami(), KD.isLoggedIn()
         unless KD.isLoggedIn() then __utils.defer fn
         else clear()


Remove GitHubFactory (replaced with GitHubFetcher)

--- src/library.coffee
+++ src/library.coffee
@@ -7,12 +7,3 @@

   # global.Libs.Canarium = require("canarium")

-  global.Libs.GitHubFactory = {
-    GitHub: require("github-api")
-    apiBase: "http://#{window.location.host}/api"
-    create: (args...) ->
-      instance = new @GitHub(args...)
-      instance.__apiBase = @apiBase if @apiBase?
-      return instance
-  }
-


Show avatars inline

Show avatars inline

--- client.coffee
+++ client.coffee
@@ -300,12 +300,12 @@
         Plugin.users.observeEach (user) !->
             Dom.div !->
                 Dom.style
-                    Box: 'center middle'
                     position: 'relative'
                     padding: '6px 4px'
                     margin: '1px'
                     borderRadius: '2px'
                     flexDirection: 'column'
+                    display: 'inline-flex'

                 Ui.avatar Plugin.userAvatar(user.key()),
                     size: size
@@ -318,12 +318,10 @@
                     Dom.div !->
                         Dom.style
                             position: 'absolute'
-                            left: 'auto'
-                            top: '6px'
-                            width: (size+2)+'px'
-                            height: (size+2)+'px'
-                            borderRadius: (size+2)+'px'
-                            lineHeight: (size+2)+'px'
+                            width: size+'px'
+                            height: size+'px'
+                            borderRadius: size+'px'
+                            lineHeight: size+'px'
                             backgroundColor: 'rgba(0, 0, 0, 0.5)'
                             fontWeight: 'bold'
                             fontSize: '250%'


Fix the Bellefit hack

Fix the Bellefit hack

--- lib/Task/ActivityTask/Save/_3DCartSaveOrders.coffee
+++ lib/Task/ActivityTask/Save/_3DCartSaveOrders.coffee
@@ -6,11 +6,4 @@
   createModel: -> create_3DCartOrders(@bookshelf)
   createSerializer: -> new Serializer({model: @model})

-  insert: (trx, externalObject) ->
-    # hack for Bellefit
-    if externalObject.InvoiceNumber and externalObject.OrderItemList.length
-      super
-    else
-      Promise.resolve(false)
-
 module.exports = _3DCartSaveOrders


Add client version to acl list

Add client version to acl list

--- client.coffee
+++ client.coffee
@@ -147,7 +147,7 @@
       page = factory.createPage('min')
       page.counts = 0
     page.counts++
-    _acl = []
+    _acl = if $args.version then ["+#{$args.version}"] else []
     for k, v of acl
       if (new Date() - v) > 0
         delete acl[k]


Use QueryHandlers

Use QueryHandlers

#6 Classify and objectify query handlers

--- client.coffee
+++ client.coffee
@@ -14,9 +14,12 @@
 parallel = require('run-parallel')
 string2compact = require('string2compact')
 utils = require './utils'
+FindNodeQueryHandler = require "./src/queryhandlers/FindNodeQueryHandler"
+PingQueryHandler = require "./src/queryhandlers/PingQueryHandler"

 #
 # Constants
+# TODO move them to a `constants.coffee` or make them configurable
 #

 BOOTSTRAP_NODES = [
@@ -86,15 +89,6 @@
     @_port = null

     ###*
-    # Query Handlers table
-    # @type {Object} string -> function
-    ###
-
-    @queryHandler =
-      ping: @_onPing
-      find_node: @_onFindNode
-
-    ###*
     # Routing table
     # @type {KBucket}
     ###
@@ -103,6 +97,15 @@
       localNodeId: @nodeId
       numberOfNodesPerKBucket: K
       numberOfNodesToPing: MAX_CONCURRENCY
+
+    ###*
+    # Query Handlers table
+    # @type {Object} string -> function
+    ###
+
+    @queryHandler =
+      ping: new PingQueryHandler(@nodeId)
+      find_node: new FindNodeQueryHandler(@nodeId, @nodes)

     ###*
     # Cache of routing tables used during a lookup. Saved in this object so we can access
@@ -474,7 +477,18 @@
     # @_debug('adding (potentially) new node %s %s', utils.idToHexString(nodeId), addr)
     @addNode addr, nodeId, addr
   if type == MESSAGE_TYPE.QUERY
-    @_onQuery addr, message
+    try
+      result =
+        t: message.t
+        y: MESSAGE_TYPE.RESPONSE
+        r: @_onQuery(addr, message)
+    catch e
+      console.error(e)
+      message = e.message
+      @_debug message
+    if result
+      @_send addr, result
+
   else if type == MESSAGE_TYPE.RESPONSE or type == MESSAGE_TYPE.ERROR
     @_onResponseOrError addr, type, message

@@ -486,12 +500,12 @@

 DHT::_onQuery = (addr, message) ->
   query = message.q.toString()
-  if typeof @queryHandler[query] == 'function'
-    @queryHandler[query].call @, addr, message
+  handler = @queryHandler[query]
+  @_debug "handler:", handler.name
+  if typeof handler == "object"
+    return handler.handle message
   else
-    errMessage = 'unexpected query type'
-    @_debug errMessage
-    @_sendError addr, message.t, ERROR_TYPE.METHOD_UNKNOWN, errMessage
+    throw new TypeError "unexpected query type"

 ###*
 # Called when another node sends a response or error.
@@ -565,20 +579,6 @@
   @_query { q: 'ping' }, addr, cb

 ###*
-# Called when another node sends a "ping" query.
-# @param  {string} addr
-# @param  {Object} message
-###
-
-DHT::_onPing = (addr, message) ->
-  res =
-    t: message.t
-    y: MESSAGE_TYPE.RESPONSE
-    r: id: @nodeId
-  @_debug 'got ping from %s', addr
-  @_send addr, res
-
-###*
 # Send "find_node" query to given addr.
 # @param {string} addr
 # @param {Buffer} nodeId
@@ -602,29 +602,6 @@
     cb null, res

   @_query data, addr, onResponse
-
-###*
-# Called when another node sends a "find_node" query.
-# @param  {string} addr
-# @param  {Object} message
-###
-
-DHT::_onFindNode = (addr, message) ->
-  nodeId = message.a and message.a.target
-  if !nodeId
-    errMessage = '`find_node` missing required `a.target` field'
-    @_debug errMessage
-    @_sendError addr, message.t, ERROR_TYPE.PROTOCOL, errMessage
-  @_debug 'got find_node %s from %s', utils.idToHexString(nodeId), addr
-  # Convert nodes to "compact node info" representation
-  nodes = utils.convertToNodeInfo(@nodes.closest({ id: nodeId }, K))
-  res =
-    t: message.t
-    y: MESSAGE_TYPE.RESPONSE
-    r:
-      id: @nodeId
-      nodes: nodes
-  @_send addr, res

 ###*
 # Send an error to given host and port.


Propagate the callback for cursor.close

Propagate the callback for cursor.close

--- drivers/javascript/cursor.coffee
+++ drivers/javascript/cursor.coffee
@@ -67,7 +67,7 @@
         if @_closeAsap is false
             @_promptNext()
         else
-            @close()
+            @close @_closeCb
         @

     _getCallback: ->


Home page text

Home page text

--- project.coffee
+++ project.coffee
@@ -7,9 +7,9 @@
   subjectGroup: false

   title: 'Orchid Observers'
-  summary: 'hello world'
+  summary: 'Is climate change affecting the UK’s orchids?'
   description: '''
-    short project description goes here.
+    Photograph wild orchids this summer and annotate museum specimens to contribute to climate change research at the Natural History Museum. Gathering new photographs of UK orchids and extracting data on flowering times from over 10,000 Museum specimens is a huge task, so we need your help. Get involved here!
                  '''


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -149,7 +149,7 @@
       if not I[I.name]
         I[I.name] = 100

-      if not I["max#{name.capitalize()}"]
+      if not I["max#{Iname.capitalize()}"]
         if I[name]
           I["max#{name.capitalize()}"] = I[name]
         else


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -127,7 +127,7 @@
   @name meter
   @methodOf Metered#
   @param {Object} options The meter configuration options
-    borderColor - The color of the meter's border
+    borderColor - Color of the meter's border
     borderWidth - The width of the meter's border
     color - The col
   ###


Optimize take_params.

Optimize take_params.

--- src/methods.coffee
+++ src/methods.coffee
@@ -71,8 +71,13 @@
     }

   take_params: (caller_stack) ->
-    params = []
-    caller_stack.splice(caller_stack.length-@param_bytes, @param_bytes)
+    params = new Array @param_bytes
+    start = caller_stack.length - @param_bytes
+    for i in [0...@param_bytes] by 1
+      params[i] = caller_stack[start + i]
+    # this is faster than splice()
+    caller_stack.length -= @param_bytes
+    params

   # used by run and run_manually to print arrays for debugging. we need this to
   # distinguish [null] from [].


typeclass fields are finally working

typeclass fields are finally working

--- test/typeclasses.coffee
+++ test/typeclasses.coffee
@@ -67,7 +67,7 @@
         name: "MemberType"
         typeclasses: ["TypeclassWithField"]
         fields:
-          ownField: "Int"
+          ownField: "Integer"

     typeclass =
       newtypeclass:
@@ -86,7 +86,7 @@
     matched.data.should.eql {ownField: 0, classField: 'foo'}
     matched.typedata.type.should.eql 'MemberType'
     matched.typedata.iscontainer.should.eql true
-    matched.typedata.fields.ownField.type.should.eql 'Int'
+    matched.typedata.fields.ownField.type.should.eql 'Integer'
     matched.typedata.fields.ownField.iscontainer.should.eql false
     matched.typedata.fields.classField.type.should.eql 'String'
     matched.typedata.fields.classField.iscontainer.should.eql false


:racehorse: Skip require from settings view if configSchema

--- src/package.coffee
+++ src/package.coffee
@@ -159,6 +159,7 @@

   # TODO: Remove. Settings view calls this method currently.
   activateConfig: ->
+    return if @configSchemaRegisteredOnLoad
     @requireMainModule()
     @registerConfigSchemaFromMainModule()


circle map - redo search on enter

circle map - redo search on enter

--- app/assets/javascripts/_open_street_map.coffee
+++ app/assets/javascripts/_open_street_map.coffee
@@ -5,10 +5,11 @@
     @joinButton  = @form.find('.sumbit')

     @target.addClass('hidden')
-    @input.on 'input', @render
+    @input.on 'input change', @render

-    $(window).keydown (event)->
+    $(window).keydown (event) =>
       if(event.keyCode == 13)
+        @render()
         event.preventDefault()
         return false


Only initialize data tables that aren't yet data tables

--- app/assets/javascripts/data-tables-init.coffee
+++ app/assets/javascripts/data-tables-init.coffee
@@ -1,5 +1,7 @@
 $(document).on "page:change", ->
-  $(".data-table").DataTable
-    "responsive": true
-    "order": [[ 0, "desc" ]]
-
+  $(".data-table").each ->
+    table = $(@)
+    return if $.fn.dataTable.isDataTable(table)
+    table.DataTable
+      responsive: true
+      order: [[ 0, "desc" ]]


[Feature] Implement break time plaing movie.

--- src/renderer/scripts/services/Player/Player.coffee
+++ src/renderer/scripts/services/Player/Player.coffee
@@ -1,4 +1,6 @@
 Request = global.require "request-promise"
+
+BREAK_TIME_PLAYING_MOVIE = "sm13848574"

 CONFIG_PLAYER_ENABLED = "nco.services.player.enabled"
 CONFIG_PLAYER_VOLUME = "nco.services.player.volume"
@@ -38,13 +40,22 @@

         channel.onDidChangeMovie (movie) =>
             return if app.config.get(CONFIG_PLAYER_ENABLED, no) is no
-            @_loadMovie()
+
+            if movie?
+                @_loadMovie(false, movie)
+                return
+
+            # Play break time movie
+            app.getSession().video.getVideoInfo(BREAK_TIME_PLAYING_MOVIE)
+            .then (movie) => @_loadMovie(false, movie)
+
+        return

-    _loadMovie : (fadeIn = false)->
+    _loadMovie : (fadeIn = false, movie = null) ->
         nicoSession = app.getSession()
         channel = app.nsenStream.getStream()
-        movie = channel?.getCurrentVideo()
+        movie ?= channel.getCurrentVideo()
         live = channel?.getLiveInfo()

         return if false in [nicoSession?, channel?, movie?]
@@ -53,9 +64,10 @@
         # 動画を取得（再生する）するための`nicohistory`クッキーを取得するために
         # 動画ページへアクセスする
         Request.get
-            resolveWithFullResponse : true
-            url : "http://www.nicovideo.jp/watch/#{movie.id}"
-            jar : nicoSession.cookie
+                resolveWithFullResponse : true
+                url : "http://www.nicovideo.jp/watch/#{movie.id}"
+                jar : nicoSession.cookie
+
         .then (res) =>
             browserSession = app.currentWindow.browserWindow.webContents.session
             cookies = @_translateToughCookieToElectronSettable(nicoSession.cookie.getCookies("http://www.nicovideo.jp/"))
@@ -72,6 +84,13 @@
             playContent = live.get("stream.contents.0")
             elapsedFromStart = (Date.now() - playContent.startTime) / 1000 | 0

+            # Exception process for break time.
+            if movie.id is BREAK_TIME_PLAYING_MOVIE
+                elapsedFromStart = 0
+                @_mp4player.loop = true
+            else
+                @_mp4player.loop = false
+
             if @_mp4player.src is "" or fadeIn
                 volume = app.config.get(CONFIG_PLAYER_VOLUME)
                 @_mp4player.volume = 0
@@ -79,7 +98,6 @@

             @_mp4player.src = result.url
             @_mp4player.currentTime = elapsedFromStart
-            @_mp4player.play()

         .catch =>
             console.log arguments


Do not remove inputs when dropdown menu hides

--- app/assets/javascripts/labels_select.js.coffee
+++ app/assets/javascripts/labels_select.js.coffee
@@ -291,7 +291,6 @@
                 saveLabelData()

           if $dropdown.hasClass('js-filter-bulk-update')
-            @removeInputs()
             $dropdown.parent().find('.is-active, .is-indeterminate').removeClass()

         multiSelect: $dropdown.hasClass 'js-multiselect'


- defer writing to compression stream to reduce writes

--- lib/reference.coffee
+++ lib/reference.coffee
@@ -12,6 +12,7 @@
     @compress = @document.compress and not @data.Filter
     @uncompressedLength = 0
     @chunks = []
+    @chunksToCompress = [];

   initDeflate: ->
     @data.Filter = 'FlateDecode'
@@ -32,7 +33,7 @@

     if @compress
       @initDeflate() if not @deflate
-      @deflate.write chunk
+      @chunksToCompress.push(chunk);
     else
       @chunks.push chunk
       @data.Length += chunk.length
@@ -42,6 +43,7 @@
       @write chunk

     if @deflate
+      @deflate.write(Buffer.concat(@chunksToCompress));
       @deflate.end()
     else
       @finalize()


Whaaargarbl

Whaaargarbl

Fix typo.

--- lib/routes/v1.coffee
+++ lib/routes/v1.coffee
@@ -1,6 +1,6 @@
 router = require('express').Router()

-ImageFiles  = require('../ImageFiles.coffee')
+ImageFiles  = require('../imageFiles.coffee')
 SlotMachine = require('../slotMachine.coffee')
 common      = require('./common.coffee')
 imager      = require('../imager.coffee')


Inlined/moved start/stopHovering

Inlined/moved start/stopHovering

--- app/js/jail_iframe/classes/icon_buttons.coffee
+++ app/js/jail_iframe/classes/icon_buttons.coffee
@@ -15,9 +15,6 @@
   _bindCallbacks: (callbacks) ->
     @_robustHover = new FactlinkJailRoot.RobustHover @$el, callbacks
     @$el.on 'click', -> callbacks.click?()
-
-  startHovering: => @frame.addClass 'hovered'
-  stopHovering: => @frame.removeClass 'hovered'

   # TODO: do all updates of different buttons in one pass
   _updatePositionRegularly: ->
@@ -41,6 +38,9 @@
     # remove when removing client_buttons feature toggle
     unless FactlinkJailRoot.can_haz.client_buttons
       @$el.addClass 'fl-button-hide-default'
+
+  startHovering: => @frame.addClass 'hovered'
+  stopHovering: => @frame.removeClass 'hovered'

   _textContainer: (el) ->
     for el in $(el).parents()
@@ -70,8 +70,8 @@
     super

     @_bindCallbacks
-      mouseenter: => @startHovering()
-      mouseleave: => @stopHovering()
+      mouseenter: => @frame.addClass 'hovered'
+      mouseleave: => @frame.removeClass 'hovered'
       click: => @_onClick()

     @$paragraph = $(options.el)


Revert "Only destroy attentionSpan when it exists"

This reverts commit 1af0446c7003c6f9a069b93313f0f210f3bb7974.

--- app/js/jail_iframe/classes/icon_buttons.coffee
+++ app/js/jail_iframe/classes/icon_buttons.coffee
@@ -151,7 +151,7 @@

   destroy: ->
     @_iconButton.destroy()
-    @_attentionSpan?.destroy()
+    @_attentionSpan.destroy()
     @_robustParagraphHover?.destroy()
     FactlinkJailRoot.off 'factlink.factsLoaded factlinkAdded', @_destroyUnlessValid
     FactlinkJailRoot.off 'hideAllParagraphButtons', @_onHideAllParagraphButtons


Revert CoffeeScript breakage

Revert CoffeeScript breakage

--- lib/ui/dialog.coffee
+++ lib/ui/dialog.coffee
@@ -16,7 +16,7 @@
       'core:confirm': => @onConfirm(@miniEditor.getText())
       'core:cancel': => @cancel()
     @miniEditor.on 'blur', => @close()
-    @miniEditor.getModel().(onDidChange) => @showError()
+    @miniEditor.getModel().onDidChange => @showError()
     @miniEditor.getModel().setText(input)

     if select


Insert backward-compatibility check for Atom versions < 0.200

--- lib/vim-state.coffee
+++ lib/vim-state.coffee
@@ -547,4 +547,9 @@
 # Package authors - copy and paste this code at your own risk.
 getChangesSinceCheckpoint = (buffer, checkpoint) ->
   {history} = buffer
-  history.undoStack.slice(history.getCheckpointIndex(checkpoint))
+
+  # TODO: remove this conditional once Atom 0.200 has been out for a while.
+  if index = history.getCheckpointIndex?(checkpoint)
+    history.undoStack.slice(index)
+  else
+    []


Removed nap.

Removed nap.

--- kelvin.coffee
+++ kelvin.coffee
@@ -3,7 +3,6 @@
 path = require 'path'
 _ = require 'underscore'
 crypto = require 'crypto'
-nap = require 'nap'

 linkAttrs = {
   'href': '',


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -1,5 +1,5 @@
 ###*
-The TimedEvents module allows arbi
+The TimedEvents module allows arbitrary code to be exe

 player = Core


tiny change fof debug output for reki

tiny change fof debug output for reki

--- src/tracker/reki.coffee
+++ src/tracker/reki.coffee
@@ -145,7 +145,7 @@
             if data != 0
               callback()
             else
-              console.log 'mongo'
+              console.log 'mongo ' + info_hash
               @torrents.findOne {'infohash' : info_hash}, (err, exists) =>
                 if exists
                   callback()


fix: resolve i18n issues

fix: resolve i18n issues

--- app/locale/en.coco.coffee
+++ app/locale/en.coco.coffee
@@ -2970,6 +2970,7 @@
     live_class_details_4: "Individualized and instant feedback"
     live_class_details_5: "Curriculum trusted by 80,000+ educators"
     try_free_class: "Try a free 60 minute class"
+    pricing_plans: "Pricing Plans"
     choose_plan: "Choose Plan"
     per_student: "per student"
     sibling_discount: "15% Sibling Discount!"
@@ -2985,6 +2986,8 @@
     eight_sessions_per_month: "8 Private Sessions / Mo."
     four_private_classes_price: "$219/mo"
     eight_private_classes_price: "$399/mo"
+    private_classes_detail: "4 or 8 Private Sessions / Mo."
+    private_classes_price: "$219/mo or $399/mo"
     private_classes_detail_1: "1:1 student to instructor ratio"
     private_classes_detail_2: "60 minute classes"
     private_classes_detail_3: "Flexible schedule tailored to your needs"


translate latest parts (#4908)

translate latest parts (#4908)

translate latest English into Chinese (teacher and achievement).

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -2588,43 +2588,43 @@
 #    value_copy3: "We know how important fun and engagement is for the developing brain, so we’ve packed in as much learning as we can while wrapping it up in a game they'll love."
 #    dive_head1: "Not just for software engineers"
 #    dive_intro: "Computer science skills have a wide range of applications. Take a look at a few examples below!"
-#    medical_flag: "Medical Applications"
-#    medical_flag_copy: "From mapping of the human genome to MRI machines, coding allows us to understand the body in ways we’ve never been able to before."
-#    explore_flag: "Space Exploration"
-#    explore_flag_copy: "Apollo got to the Moon thanks to hardworking human computers, and scientists use computer programs to analyze the gravity of planets and search for new stars."
-#    filmaking_flag: "Filmmaking and Animation"
-#    filmaking_flag_copy: "From the robotics of Jurassic Park to the incredible animation of Dreamworks and Pixar, films wouldn’t be the same without the digital creatives behind the scenes."
-#    dive_head2: "Games are important for learning"
-#    dive_par1: "Multiple studies have found that game-based learning promotes"
-#    dive_link1: "cognitive development"
-#    dive_par2: "in kids while also proving to be"
-#    dive_link2: "more effective"
-#    dive_par3: "in helping students"
-#    dive_link3: "learn and retain knowledge"
-#    dive_par4: ","
-#    dive_link4: "concentrate"
-#    dive_par5: ", and perform at a higher level of achievement."
-#    dive_par6: "Game based learning is also good for developing"
-#    dive_link5: "resilience"
-#    dive_par7: ", cognitive reasoning, and"
-#    dive_par8: ". Science is just telling us what learners already know. Children learn best by playing."
-#    dive_link6: "executive functions"
-#    dive_head3: "Team up with teachers"
-#    dive_3_par1: "In the future, "
-#    dive_3_link1: "coding is going to be as fundamental as learning to read and write"
-#    dive_3_par2: ". We’ve worked closely with teachers to design and develop our content, and we can't wait to get your kids learning. Educational technology programs like CodeCombat work best when the teachers implement them consistently. Help us make that connection by introducing us to your child’s teachers!"
-#    mission: "Our mission: to teach and engage"
-#    mission1_heading: "Coding for today's generation"
-#    mission2_heading: "Preparing for the future"
-#    mission3_heading: "Supported by parents like you"
-#    mission1_copy: "Our education specialists work closely with teachers to meet children where they are in the educational landscape. Kids learn skills that can be applied outside of the game because they learn how to solve problems, no matter what their learning style is."
-#    mission2_copy: "A 2016 survey showed that 64% of girls in 3-5th grade want to learn how to code. There were 7 million job openings in 2015 required coding skills. We built CodeCombat because every child should be given a chance to create their best future."
-#    mission3_copy: "At CodeCombat, we’re parents. We’re coders. We’re educators. But most of all, we’re people who believe in giving our kids the best opportunity for success in whatever it is they decide to do."
- #  parent_modal:
-#    refer_teacher: "Refer Teacher"
-#    name: "Your Name"
-#    parent_email: "Your Email"
-#    teacher_email: "Teacher's Email"
-#    message: "Message"
-#    custom_message: "I just found CodeCombat and thought it'd be a great program for your classroom! It's a computer science learning platform with standards-aligned curriculum.\n\nComputer literacy is so important and I think this would be a great way to get students engaged in learning to code."
-#    send: "Send Email"
+    medical_flag: "医学应用"
+    medical_flag_copy: "从人类基因组的映射到核磁共振，代码让我们以前所未有的方式，了解身体。"
+    explore_flag: "太空探索"
+    explore_flag_copy: "由于人类计算机的辛勤工作，阿波罗登上了月球。科学家们利用计算机程序分析行星的引力并寻找新恒星。"
+    filmaking_flag: "制作电影和动画"
+    filmaking_flag_copy: "从《侏罗纪公园》的机器人到梦工厂和皮克斯令人难以置信的动画，没有幕后的数字创意，电影就没有那样精彩了。"
+    dive_head2: "游戏对于学习很重要"
+    dive_par1: "多项研究发现，基于游戏的学习能够促进学习"
+    dive_link1: "认识的不断发展"
+    dive_par2: "在孩子身上也证明了这一点"
+    dive_link2: "更高效地"
+    dive_par3: "在帮助学生的过程中"
+    dive_link3: "不断学习记忆知识"
+    dive_par4: "，"
+    dive_link4: "集中精力"
+    dive_par5: "，表现出来一种高水平的成就。"
+    dive_par6: "基于学习的游戏对于游戏开发更有好处。"
+    dive_link5: "恢复"
+    dive_par7: "，认识的原因，和"
+    dive_par8: "。科学就是告诉我们已经知道了什么知识，孩子们在玩游戏中学习效率更高。"
+    dive_link6: "行政职能"
+    dive_head3: "和老师一起组队"
+    dive_3_par1: "将来，"
+    dive_3_link1: "就像阅读和写作一样，编程将来会成为一种基本的能力"
+    dive_3_par2: "。我们与老师密切合作，设计和开发我们的内容，我们等不及让你的孩子学习。像CodeCombat这样的教育技术项目最好是由老师持续执行。帮助我们通过介绍您孩子的老师来建立这种联系!"
+    mission: "我们的任务是教学和提高"
+    mission1_heading: "为了今天的一代人而编程"
+    mission2_heading: "为了未来准备吧"
+    mission3_heading: "我们受到很多家长支持，就像你一样"
+    mission1_copy: "我们的教育专家与教师紧密合作，以满足孩子们在教育领域的需求。孩子们学习的技能可以在游戏之外应用，因为他们学会了如何解决问题，不管他们的学习方式是什么。"
+    mission2_copy: "2016年的一项调查显示，64%的3-5年级女生想学习编程。2015年需要的编码技能岗位有700万个。我们创建CodeCombat游戏是因为每个孩子都应该有机会去创造他们最好的未来。"
+    mission3_copy: "在CodeCombat中，我们是父母，我们程序员，我们教育工作者。但最重要的是，我们相信无论孩子们决定做什么，都应该给他们最好的成功机会。"
+#  parent_modal:
+    refer_teacher: "指导老师"
+    name: "你的名字"
+    parent_email: "你的邮箱"
+    teacher_email: "你的老师的邮箱"
+    message: "消息"
+    custom_message: "我刚刚找到了CodeCombat，我想它对你的教室来说是个很棒的程序!它是一个符合标准课程的计算机科学学习平台。计算机知识是非常重要的，我认为这将是一个让学生学习编码的好方法。"
+    send: "发送邮件"


Add ferengi_2 to subject.coffee

Add ferengi_2 to subject.coffee

--- app/models/subject.coffee
+++ app/models/subject.coffee
@@ -62,7 +62,11 @@
       id: Config.surveys.sdss_lost_set.id
       workflowId: Config.surveys.sdss_lost_set.workflowId
       tree: SloanTree
-
+    ferengi_2:
+      id: Config.surveys.ferengi_2.id
+      workflowId: Config.surveys.ferengi_2.workflowId
+      tree: FerengiTree
+
   @url: (params) -> @withParams "/projects/galaxy_zoo/groups/#{ params.surveyId }/subjects", limit: params.limit

   # NOTE: Before changing the active surveys on the live website,
@@ -91,7 +95,7 @@
       @fetch() if @count() is 1
     else
       @fetch()
-
+
   @fetch: ->
     count = Config.subjectCache - @count()
     idCounts = {}
@@ -105,20 +109,20 @@
       Api.get @url(surveyId: id, limit: limit), (results) =>
         @create result for result in results
         @current or= @first()
-
+
         if @current and not hasTriggered
           hasTriggered = true
           @trigger 'fetched'
-
+
   @show: (id) ->
     Analytics.logEvent { "type": "view", "subject_id": id }
     Api.get "/projects/galaxy_zoo/subjects/#{ id }"
-
+
   constructor: ->
     super
     img = new Image
     img.src = @image()
-
+
   survey: -> @surveys[@metadata.survey]
   surveyId: -> @metadata.provided_image_id
   tree: -> @survey().tree


Minor change

Minor change

--- app/scripts/navbar.coffee
+++ app/scripts/navbar.coffee
@@ -10,7 +10,6 @@
     main.addEventListener 'click', @closeMenu

     @appbarElement = document.querySelector '.app-bar'
-    console.log @appbarElement

     menuBtn = document.querySelector '.menu'
     menuBtn.addEventListener 'click', @toggleMenu


Remove atom-shell during partial-clean task instead of clean task

--- tasks/clean-task.coffee
+++ tasks/clean-task.coffee
@@ -8,9 +8,9 @@
     rm '/tmp/atom-coffee-cache'
     rm '/tmp/atom-cached-atom-shells'
     rm 'node'
+    rm 'atom-shell'

   grunt.registerTask 'clean', 'Delete all the build files', ->
     rm 'node_modules'
-    rm 'atom-shell'
     rm path.join(process.env.HOME, '.atom', '.node-gyp')
     grunt.task.run('partial-clean')


Don’t change key name in serialized application state

We changed it internally for clarity, but this is breaking compatibility
with restoring state when upgrading from previous versions.

Fixes #9690

--- src/browser/atom-application.coffee
+++ src/browser/atom-application.coffee
@@ -441,7 +441,7 @@
     states = []
     for window in @windows
       if not window.isSpec and window.projectDirectoryPaths?
-        states.push(projectDirectoryPaths: window.projectDirectoryPaths)
+        states.push(initialPaths: window.projectDirectoryPaths)
     if states.length > 0 or allowEmpty
       @storageFolder.store('application.json', states)

@@ -449,7 +449,7 @@
     if (states = @storageFolder.load('application.json'))?.length > 0
       for state in states
         @openWithOptions(_.extend(options, {
-          pathsToOpen: state.projectDirectoryPaths
+          pathsToOpen: state.initialPaths
           urlsToOpen: []
           devMode: @devMode
           safeMode: @safeMode


Pass through apiPreviewMode flag

Pass through apiPreviewMode flag

--- src/browser/atom-application.coffee
+++ src/browser/atom-application.coffee
@@ -503,9 +503,9 @@
   #   :safeMode - A Boolean which controls whether any newly opened windows
   #               should be in safe mode or not.
   #   :window - An {AtomWindow} to use for opening a selected file path.
-  promptForPathToOpen: (type, {devMode, safeMode, window}) ->
+  promptForPathToOpen: (type, {devMode, safeMode, apiPreviewMode, window}) ->
     @promptForPath type, (pathsToOpen) =>
-      @openPaths({pathsToOpen, devMode, safeMode, window})
+      @openPaths({pathsToOpen, devMode, safeMode, apiPreviewMode, window})

   promptForPath: (type, callback) ->
     properties =


Array::join works

Array::join works

--- src/built-ins/ArrayPrototype.coffee
+++ src/built-ins/ArrayPrototype.coffee
@@ -53,13 +53,27 @@
 ARRAY_PROTOTYPE.put 'pop', pop

+join = new NATIVE_FUNCTION (separator) ->
+	value = @data.map (element) ->
+		if element == UNDEFINED || element == NULL
+			''
+		else
+			element.asString()
+	.join separator.asString()
+
+	return: true
+	value: new STRING value
+
+ARRAY_PROTOTYPE.put 'join', join
+
+
 toString = new NATIVE_FUNCTION ->
 	value = @data.map (element) ->
 		if element == UNDEFINED || element == NULL
 			''
 		else
 			element.asString()
-	.join ','
+	.join ',' # spec mentions using join

 	return: true
 	value: new STRING value


Fix constraint application for onxxx event handler attributes

--- layout.coffee
+++ layout.coffee
@@ -200,8 +200,8 @@
         args = [scope[name]]
       else
         args = arguments
-        # console.log 'event callback', name, args, scope, js
-        js.apply(scope, args)
+      # console.log 'event callback', name, args, scope, js
+      js.apply(scope, args)

   class Node extends Module


remove old code

remove old code

--- layout.coffee
+++ layout.coffee
@@ -1,78 +1,3 @@
-drawState = (cell, state) ->
-    node = {
-        name: state.name
-        cell: cell
-        size: 1000
-    }
-
-    if state.children?
-        g = cell.append('g').attr('class', 'children')
-        node.children = drawChildren(g, state.children)
-        node.size += d3.sum(node.children, (d) -> d.size) * Math.pow(node.children.length, 0.8) * 2
-
-    r = Math.sqrt(node.size)
-
-    cell.insert('rect', ':first-child')
-        .attr('class', 'border')
-        .attr('x', -r / 2)
-        .attr('y', -r / 2)
-        .attr('width', r)
-        .attr('height', r)
-        .attr('rx', 15)
-        .attr('ry', 15)
-      .append('title')
-        .text(state.name)
-
-    return node
-
-
-drawChildren = (g, childStates) ->
-    children = []
-    childMap = {}
-
-    linkGroup = g.append('g').attr('class', 'linkGroup')
-
-    links = []
-
-    for state in childStates
-        cell = g.append('g').attr('class', 'cell')
-        child = drawState(cell, state)
-        children.push(child)
-        childMap[child.name] = child
-
-    for state in childStates
-        child = childMap[state.name]
-        for tr in state.transitions or []
-            target = childMap[tr.target]
-            links.push({source: child, target: target})
-
-    link = linkGroup.selectAll('.link')
-        .data(links)
-      .enter().append('line')
-        .attr('class', 'link')
-        .style('stroke-width', (d) -> Math.sqrt(d.value))
-
-    force = d3.layout.force()
-        .charge((d) -> -Math.sqrt(d.size) * 5)
-        .linkDistance((d) -> Math.sqrt(d.source.size) + Math.sqrt(d.target.size))
-
-    force
-        .nodes(children)
-        .links(links)
-        .start()
-
-    force.on 'tick', ->
-      link.attr('x1', (d) -> d.source.x)
-          .attr('y1', (d) -> d.source.y)
-          .attr('x2', (d) -> d.target.x)
-          .attr('y2', (d) -> d.target.y)
-
-      for node in children
-          node.cell.attr('transform', "translate(#{node.x},#{node.y})")
-
-    return children
-
-
 walk = (state, callback, parent=null) ->
     callback(state, parent)
     for child in state.children or []


example with cross-state transitions

example with cross-state transitions

--- layout.coffee
+++ layout.coffee
@@ -237,3 +237,19 @@
         ]}
     ]}
 ])
+
+
+demo([
+    {name: "A", children: [
+        {name: "A1", transitions: [{target: "B1"}]},
+        {name: "A2", transitions: [{target: "B2"}]},
+    ], transitions: [{target: "B"}]},
+    {name: "B", children: [
+        {name: "B1", transitions: [{target: "C1"}]},
+        {name: "B2", transitions: [{target: "C2"}]},
+    ], transitions: [{target: "C"}]},
+    {name: "C", children: [
+        {name: "C1", transitions: [{target: "A1"}]},
+        {name: "C2", transitions: [{target: "A2"}]},
+    ], transitions: [{target: "A"}]},
+])


Do not introduce 'Glass' here

Do not introduce 'Glass' here

--- app/scripts/levelblocks/GameplayDemo.js.coffee
+++ app/scripts/levelblocks/GameplayDemo.js.coffee
@@ -198,7 +198,7 @@
     @add(650, 15, Crafty.e('2D, Canvas, Edge, Color').color('#404040').attr({ w: 10, h: h }))
     @add(600, 15 + h, Crafty.e('2D, Canvas, Edge, Color').color('#404040').attr({ w: 60, h: 10 }))
     @add(600, 25 + h, Crafty.e('2D, Canvas, Edge, Color').color('#404040').attr({ w: 10, h: h2 }))
-    @add(600, 25 + h + h2, Crafty.e('2D, Canvas, Edge, Color, Glass').color('#404040').attr({ w: 60, h: 10, alpha: 0.5, z: 1 }))
+    @add(600, 25 + h + h2, Crafty.e('2D, Canvas, Edge, Color').color('#404040').attr({ w: 60, h: 10 }))
     @add(650, 25 + h2, Crafty.e('2D, Canvas, Edge, Color').color('#404040').attr({ w: 60, h: 10 }))

     @add(900, 100, Crafty.e('2D, Canvas, Edge, Color').color('#404040').attr({ w: 10, h: @level.visibleHeight - 100 }))


Fix binary bug.

Fix binary bug.

--- src/request.coffee
+++ src/request.coffee
@@ -194,7 +194,7 @@
             # Convert raw data to binary chopping off the higher-order bytes in each char.
             # Useful for Base64 encoding.
             converted = ''
-            for i in [0..data.length]
+            for i in [0...data.length]
               converted += String.fromCharCode(data.charCodeAt(i) & 0xff)

             data = converted


:bug: Fix prevent ui router from encoding path names in the URL

--- frontend/app/app.coffee
+++ frontend/app/app.coffee
@@ -3,9 +3,17 @@
   'ui.bootstrap'
 ]

-mod.config ($stateProvider, $urlRouterProvider) ->
+mod.config (
+  $stateProvider, $urlRouterProvider, $urlMatcherFactoryProvider
+) ->

   $urlRouterProvider.otherwise('/playing')
+
+  valToString = (val) -> if val? then val.toString() else val
+  $urlMatcherFactoryProvider.type 'nonURIEncoded',
+    encode: valToString
+    decode: valToString
+    is: -> true

   $stateProvider.state
     name: 'layout'
@@ -35,7 +43,7 @@
   # This needs to be the last state, because of the wildcard url match.
   $stateProvider.state
     name: 'browse.uri'
-    url: '/{uri:.*}?page&sort&direction'
+    url: '/{uri:nonURIEncoded}?page&sort&direction'
     parent: 'layout'
     template: '<m-browse></m-browse>'


mongoimport: location coordinates and geometries are now imported too

--- data/imports/aiddata.coffee
+++ data/imports/aiddata.coffee
@@ -5,6 +5,7 @@
 dbconf = require '../../.dbconf'
 mongo = require '../../app/mongo'
 linereader = require './linereader'
+d3 = require 'd3'

 queue = require 'queue-async'

@@ -12,7 +13,7 @@
 pgurl = dbconf.postgres

 DEBUG_updateRowsLimit = null  # set for debugging
-OMIT_NULL_VALUE_FIELDS_IN_COMMITMENTS_JSON = false
+OMIT_NULL_VALUE_FIELDS_IN_COMMITMENTS_JSON = true

 PATH = "data/imports/"
 TEMP_DIR = "temp"
@@ -24,6 +25,7 @@
 mongodb = pgclient = null

 locationNameToCode = {}
+

@@ -208,54 +210,85 @@

           # Ensure each location has a meaningful code
-          for r in result.rows
-            oldname = r.name
-
-            if /, regional/.test r.name
-              r.type = "region"
-              match = /(.*), regional/.exec r.name
-              r.name = match[1]
-              r.code = "R-"+acronym(r.name)
-            else if (r.name in ["GLOBAL", "Bilateral, unspecified", "MADCT Unspecified",
-            "Sts Ex-Yugo. Unspec."])
-              r.code = "C-"+acronym(r.name)
-              r.type = "congl"
-            else if r.code?
-              r.type = "country"
-              unless r.code.trim().length > 0
-                r.code = acronym(r.name)
-            else
-              r.type = "org"
-              match = /(.*)\(([A-Z]+)\)(.*)/.exec r.name
-              if match?
-                # if there is an acronym in parethesis in the name, use it as the code
-                r.code = "O-"+match[2]
-                r.name = (match[1] + match[3]).trim()
+          do ->
+            for r in result.rows
+              oldname = r.name
+
+              if /, regional/.test r.name
+                r.type = "region"
+                match = /(.*), regional/.exec r.name
+                r.name = match[1]
+                r.code = "R-"+acronym(r.name)
+              else if (r.name in ["GLOBAL", "Bilateral, unspecified", "MADCT Unspecified",
+              "Sts Ex-Yugo. Unspec."])
+                r.code = "C-"+acronym(r.name)
+                r.type = "congl"
+              else if r.code?
+                r.type = "country"
+                unless r.code.trim().length > 0
+                  r.code = acronym(r.name)
+
+                # fix the problem with the wrong country code of Italy
+                if (r.code in ["IRA", "ITA"] and r.name is "Italy")
+                  r.code = "ITA"
+                  delete codes["ITA"]
+
               else
-                r.code = "O-"+acronym(r.name)
-
-            # if code was already used, add a number at the end
-            do ->
-              c = r.code
-              i = 1
-              while codes[c]?
-                c = r.code + i++
-              codes[c] = true
-              r.code = c
-
-            locationNameToCode[oldname] = r.code
-
-
-          #console.log locationNameToCode
-          #console.log result
-
-
-
-          os.mkdir TEMP_DIR
-          fd = fs.openSync(LOCATIONS_TEMP_FILE, 'w')
-          for r in result.rows
-            fs.writeSync fd, JSON.stringify(r) + "\n"
-          fs.closeSync fd
+                r.type = "org"
+                match = /(.*)\(([A-Z]+)\)(.*)/.exec r.name
+                if match?
+                  # if there is an acronym in parethesis in the name, use it as the code
+                  r.code = "O-"+match[2]
+                  r.name = (match[1] + match[3]).trim()
+                else
+                  r.code = "O-"+acronym(r.name)
+
+              # if code was already used, add a number at the end
+              do ->
+                c = r.code
+                i = 1
+                while codes[c]?
+                  c = r.code + i++
+                codes[c] = true
+                r.code = c
+
+              locationNameToCode[oldname] = r.code
+
+
+          # add coordinates and polygons
+          do ->
+            countries = d3.csv.parse(fs.readFileSync("data/static/data/aiddata-countries.csv", "utf8"))
+            countriesByCode = d3.nest().key((c) -> c.Code).rollup((a) -> a[0]).map(countries)
+
+            geometry = JSON.parse(fs.readFileSync("data/static/data/world-countries.json", "utf8"))
+            geometryByCode = d3.nest().key((c) -> c.id).rollup((a) -> a[0]).map(geometry.features)
+
+            for r in result.rows when r.type is "country"
+              c = countriesByCode[r.code]
+              if c?
+                r.lat = c.Lat
+                r.lon = c.Lon
+              g = geometryByCode[r.code]
+              if g?
+                r.geometry = g.geometry
+                delete geometryByCode[r.code]
+
+
+            console.log "! Countries with no centroid coords:\n" +
+              ("(#{r.name},#{r.code})" for r in result.rows when r.type is "country" and not r.lat?) + "\n"
+            console.log "! Countries with no geometry:\n" +
+              ("(#{r.name},#{r.code})" for r in result.rows when r.type is "country" and not r.geometry?) + "\n"
+            console.log "! Countries found in the map, but not in the list of locations:\n" +
+              ("(#{code},#{obj.properties.name})" for code,obj of geometryByCode) + "\n"
+
+          # writing to the temp file for importing into Mongo
+          do ->
+            os.mkdir TEMP_DIR
+            fd = fs.openSync(LOCATIONS_TEMP_FILE, 'w')
+            for r in result.rows
+              fs.writeSync fd, JSON.stringify(r) + "\n"
+            fs.closeSync fd
+

           callWhenEnded()
     )


fixing coverage pseudo-regression, wherein all but the first test would fail

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -67,11 +67,15 @@
   # Init the first class, and put the command-line args on the stack for use by
   # its main method.
   initialize: (class_name, initial_args) ->
-    # initialize the system class
-    @class_lookup(c2t 'java/lang/System').methods['initializeSystemClass()V'].run(this)
+    unless @system_initialized?
+      # initialize the system class
+      @class_lookup(c2t 'java/lang/System').methods['initializeSystemClass()V'].run(this)
+      @system_initialized = true
+
+    # load the main class (which calls <clinit>, if needed)
     @class_lookup c2t class_name

-    # prepare meta_stack for main(String[] args)
+    # prepare the call stack for main(String[] args)
     args = @set_obj(c2t('[Ljava/lang/String;'),(@init_string(a) for a in initial_args))
     @curr_thread.fields.$meta_stack = new root.CallStack [args]
     debug "### finished runtime state initialization ###"


Updating test to not include class property

--- test/oj.CheckBox.coffee
+++ test/oj.CheckBox.coffee
@@ -27,6 +27,18 @@
     control = oj.CheckBox id:id
     expect(control.id).to.equal id
     expect(control.attributes.id).to.equal id
+
+  # it 'construct with class', ->
+  #   cls = 'my-class'
+  #   control = oj.CheckBox class:cls
+  #   expect(control.class).to.equal cls
+  #   expect(control.attributes.class).to.equal cls
+
+  #   cls2 = 'my-class2'
+  #   cls3 = 'my-class3'
+  #   control.class = [cls2,cls3]
+  #   expect(control.classes).to.deep.equal cls
+

   it 'construct with name', ->
     name = 'my-name'


update plugin-info

update plugin-info

--- index.cjsx
+++ index.cjsx
@@ -31,10 +31,10 @@
   name: 'Akashic'
   priority: 10
   displayName: <FontAwesome key={0} name='book' /> 航海日志
-  description: '日志'
+  description: '日志. 战果部分 by rui'
   author: 'W.G.'
   link: 'https://github.com/JenningsWu'
-  version: '1.1.0'
+  version: '1.2.0'
   handleClick: ->
     # checkAkashicRecordsCrashed()
     window.akashicRecordsWindow.show()


Fix: Cannot set current secretary.

Fix: Cannot set current secretary.

--- index.cjsx
+++ index.cjsx
@@ -36,8 +36,8 @@
   version: '0.1.2'
   reactClass: React.createClass
     getInitialState: ->
-      secretary: config.get('plugin.secretary.ship')
-      ships: []   # Contains ship id < 500 only.
+      secretary: config.get('plugin.secretary.ship', 0)
+      ships: []
       shipgraph: []
     componentDidMount: ->
       window.addEventListener 'game.response', @handleResponse
@@ -60,7 +60,7 @@
             ships: ships
             shipgraph: shipgraph
         when '/kcsapi/api_port/port', '/kcsapi/api_get_member/deck', '/kcsapi/api_req_hensei/change'
-          if @state.secretary == null
+          if @state.secretary == 0
             @updateNotifyConfig(@state.secretary)

     ###*
@@ -68,11 +68,11 @@
      * @param {number} ship id in window.$ships
      ###
     updateNotifyConfig: (ship_id) ->
-      server = SERVERS[Math.floor(Math.random() * SERVERS.length)];
+      server = SERVERS[Math.floor(Math.random() * SERVERS.length)]
       return unless server

-      # ship_id null represents using flagship from fleet 1
-      if not ship_id
+      # ship_id 0 represents using flagship from fleet 1
+      if ship_id == 0
         {_decks, _ships} = window
         ship = _decks[0]?.api_ship[0]
         if ship and ship > 0
@@ -90,12 +90,12 @@
       # config.set('poi.notify.morale.audio', audio_morale)

     handleShipChange: (e) ->
-      if e && e.target.value != @state.secretary
-        ship = e.target.value
-        @setState
-          secretary: ship
-        config.set('plugin.secretary.ship', ship)
-        @updateNotifyConfig(ship)
+      ship = parseInt(e.target.value)
+      return unless ship != NaN
+      @setState
+        secretary: ship
+      config.set('plugin.secretary.ship', ship)
+      @updateNotifyConfig(ship)

     handleAudition: (type) ->
       audio = null
@@ -120,7 +120,7 @@
         <Grid>
           <Col xs=12>
             <Input type="select" value={@state.secretary} onChange={@handleShipChange}>
-              <option key={0} value={null}>当前秘书舰</option>
+              <option key={0} value={0}>当前秘书舰</option>
               {
                 for ship, i in @state.ships
                   continue unless ship?


fix 1400 to 1428

fix 1400 to 1428

--- index.cjsx
+++ index.cjsx
@@ -126,8 +126,8 @@
     componentWillUpdate: (nextProps, nextState) ->
       {baseDetail, senka} = @state
       if baseDetail.exp? and baseDetail.exp isnt null
-        if (nextState.exp - baseDetail.exp) > 1400 or senka is 0
-          senkaDelta = Math.floor((nextState.exp - baseDetail.exp) / 1400)
+        if (nextState.exp - baseDetail.exp) > 1428 or senka is 0
+          senkaDelta = Math.floor((nextState.exp - baseDetail.exp) / 1428)
           if senka isnt (senkaDelta + baseDetail.rate)
             @setState
               senka: senkaDelta + baseDetail.rate


add check repair

add check repair

--- index.cjsx
+++ index.cjsx
@@ -190,7 +190,43 @@
   if api_kouku.api_stage2?
     planeCount.sortie[0] -= api_kouku.api_stage2.api_f_lostcount
     planeCount.enemy[0] -= api_kouku.api_stage2.api_e_lostcount
-analogBattle = (sortieHp, enemyHp, combinedHp, isCombined, isWater, body, leastHp, planeCount) ->
+
+checkRepair = (sortieHp, combinedHp, sortieInfo, combinedInfo) ->
+  {_ships} = window
+  for i in [0..5]
+    continue if sortieInfo[i] == -1
+    continue if sortieHp[i] > 0
+    slot = Object.clone _ships[sortieInfo[i]].api_slot
+    slot.push _ships[sortieInfo[i]].api_slot_ex
+    for x in slot
+      # Repair Team
+      if x == 42
+        sortieHp.now[i] = Math.floor(sortieHp.max[i] / 4)
+        sortieHp.dmg[i] = 0
+        break
+      # Repair Goddess
+      if x == 43
+        sortieHp.now[i] = sortieHp.max[i]
+        sortieHp.dmg[i] = 0
+        break
+  for i in [0..5]
+    continue if combinedInfo[i] == -1
+    continue if combinedHp[i] > 0
+    slot = Object.clone _ships[combinedInfo[i]].api_slot
+    slot.push _ships[combinedInfo[i]].api_slot_ex
+    for x in slot
+      # Repair Team
+      if x == 42
+        combinedHp.now[i] = Math.floor(combinedHp.max[i] / 4)
+        combinedHp.dmg[i] = 0
+        break
+      # Repair Goddess
+      if x == 43
+        combinedHp.now[i] = combinedHp.max[i]
+        combinedHp.dmg[i] = 0
+        break
+
+analogBattle = (sortieHp, enemyHp, combinedHp, isCombined, isWater, body, leastHp, planeCount, sortieInfo, combinedInfo) ->
   # First air battle

   if body.api_kouku?
@@ -202,11 +238,13 @@
       planeCount.enemy[0] = tmp.api_e_count - tmp.api_e_lostcount
       planeCount.enemy[1] = tmp.api_e_count
     analogKouku body.api_kouku, planeCount
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo

     if body.api_kouku.api_stage3?
       koukuAttack sortieHp, enemyHp, body.api_kouku.api_stage3
     if body.api_kouku.api_stage3_combined?
       koukuAttack combinedHp, enemyHp, body.api_kouku.api_stage3_combined
+      checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Second air battle

   if body.api_kouku2?
@@ -216,6 +254,7 @@
       koukuAttack sortieHp, enemyHp, body.api_kouku2.api_stage3
     if body.api_kouku2.api_stage3_combined?
       koukuAttack combinedHp, enemyHp, body.api_kouku2.api_stage3_combined
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Support battle

   if body.api_support_info?
@@ -225,6 +264,7 @@
       supportAttack enemyHp, body.api_support_info.api_support_hourai.api_damage
     else
       supportAttack enemyHp, body.api_support_info.api_damage
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Opening battle

   if body.api_opening_atack?
@@ -232,6 +272,7 @@
       raigekiAttack combinedHp, enemyHp, body.api_opening_atack
     else
       raigekiAttack sortieHp, enemyHp, body.api_opening_atack
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Night battle

   if body.api_hougeki?
@@ -239,6 +280,7 @@
       hougekiAttack combinedHp, enemyHp, body.api_hougeki
     else
       hougekiAttack sortieHp, enemyHp, body.api_hougeki
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # First hougeki battle

   if body.api_hougeki1?
@@ -246,10 +288,12 @@
       hougekiAttack combinedHp, enemyHp, body.api_hougeki1
     else
       hougekiAttack sortieHp, enemyHp, body.api_hougeki1
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Second hougeki battle

   if body.api_hougeki2?
     hougekiAttack sortieHp, enemyHp, body.api_hougeki2
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Combined hougeki battle

   if body.api_hougeki3?
@@ -257,6 +301,7 @@
       hougekiAttack combinedHp, enemyHp, body.api_hougeki3
     else
       hougekiAttack sortieHp, enemyHp, body.api_hougeki3
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
   # Raigeki battle

   if body.api_raigeki?
@@ -264,7 +309,8 @@
       raigekiAttack combinedHp, enemyHp, body.api_raigeki
     else
       raigekiAttack sortieHp, enemyHp, body.api_raigeki
-
+    checkRepair sortieHp, combinedHp, sortieInfo, combinedInfo
+
   getResult sortieHp, enemyHp, combinedHp, leastHp

 escapeId = -1
@@ -351,7 +397,7 @@
           if path != '/kcsapi/api_req_battle_midnight/battle'
             getEnemyInfo enemyHp, enemyInfo, body, false

-          result = analogBattle sortieHp, enemyHp, combinedHp, false, false, body, 0, planeCount
+          result = analogBattle sortieHp, enemyHp, combinedHp, false, false, body, 0, planeCount, sortieInfo, combinedInfo

           for i in [0..5]
             sortieHp.dmg[i] -= daySortieDmg[i]
@@ -371,7 +417,7 @@
           daySortieDmg = Object.clone sortieHp.dmg
           dayEnemyDmg = Object.clone enemyHp.dmg
           dayCombinedDmg = Object.clone combinedHp.dmg
-          result = analogBattle sortieHp, enemyHp, combinedHp, false, false, body, 1, planeCount
+          result = analogBattle sortieHp, enemyHp, combinedHp, false, false, body, 1, planeCount, sortieInfo, combinedInfo
           for i in [0..5]
             sortieHp.dmg[i] -= daySortieDmg[i]
             enemyHp.dmg[i] -= dayEnemyDmg[i]
@@ -389,7 +435,7 @@
           dayCombinedDmg = Object.clone combinedHp.dmg
           if path != '/kcsapi/api_req_combined_battle/midnight_battle'
             getEnemyInfo enemyHp, enemyInfo, body, false
-          result = analogBattle sortieHp, enemyHp, combinedHp, true, isWater, body, 1, planeCount
+          result = analogBattle sortieHp, enemyHp, combinedHp, true, isWater, body, 1, planeCount, sortieInfo, combinedInfo
           for i in [0..5]
             sortieHp.dmg[i] -= daySortieDmg[i]
             enemyHp.dmg[i] -= dayEnemyDmg[i]


Make disable button esay understanding

Make disable button esay understanding

--- index.cjsx
+++ index.cjsx
@@ -217,13 +217,9 @@
       </div>
       <Grid>
         <Col xs={6}>
-          <OverlayTrigger placement='top' overlay={
-              <Tooltip id="secretary-reset-note">{__ "Reset to default audio poi"}</Tooltip>
-            }>
-            <Button bsStyle='warning' style={width: '100%'} onClick={@handleDisable}>
-              {__ 'Disable'}
-            </Button>
-          </OverlayTrigger>
+          <Button bsStyle='warning' style={width: '100%'} onClick={@handleDisable}>
+            {__ 'Reset to default audio poi'}
+          </Button>
         </Col>
       </Grid>


Course listing now includes periods

Course listing now includes periods

No need to load id specific endpoint

--- src/components/task-plan/index.cjsx
+++ src/components/task-plan/index.cjsx
@@ -62,8 +62,10 @@
     store: TaskPlanStore
     actions: TaskPlanActions

-  loadTasks: (id, courseId) ->
+  render: ->
     Type = @getType()
+    {courseId} = @context.router.getCurrentParams()
+    id = @getId()
     <LoadableItem
       id={id}
       store={TaskPlanStore}
@@ -71,15 +73,4 @@
       renderItem={-> <Type id={id} courseId={courseId} />}
     />

-  render: ->
-    id = @getId()
-    {courseId} = @context.router.getCurrentParams()
-    <LoadableItem
-      id={courseId}
-      store={CourseStore}
-      actions={CourseActions}
-      renderItem={ => @loadTasks(id, courseId) }
-    />
-
-
 module.exports = {ReadingShell, HomeworkShell}


retweet: correct regexp match groups for uname & tweet.

--- src/tweeter.coffee
+++ src/tweeter.coffee
@@ -59,7 +59,6 @@
     return

   robot.respond /tweet\@([^\s]+)\s(.+)$/i, (msg) ->
-
     username = msg.match[1].toLowerCase()

     unless Helpers.accountIsSetup(config, username)
@@ -108,8 +107,8 @@
         return msg.reply "Hmmm. I'm not sure if the tweet was deleted. Check the account: http://twitter.com/#{username}"

   robot.respond /r(etwee)?t\@([^\s]+)\s(.*)/i, (msg) ->
-    username        = msg.match[1]
-    tweet_url_or_id = msg.match[2]
+    username        = msg.match[2]
+    tweet_url_or_id = msg.match[3]

     tweet_id = Helpers.extractTweetId(tweet_url_or_id)
     unless tweet_id


type casting

type casting

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -160,8 +160,8 @@
     bl =
       created: moment().format()
       expires: moment().add(1, 'months').format()
-      type: msg.match[1]
-      val: msg.match[2]
+      type: String(msg.match[1])
+      val: String(msg.match[2])
       creator: who

     if bl.val.toLowerCase().indexOf('https://') == 0
@@ -209,8 +209,8 @@
     return unless is2fa robot, msg

     who = msg.envelope.user.name
-    bl_type = msg.match[1]
-    bl_search = msg.match[2]
+    bl_type = String(msg.match[1])
+    bl_search = String(msg.match[2])

     if bl_search.toLowerCase().indexOf('https://') == 0
       usermsg = "Blacklisting of https links not supported."
@@ -233,7 +233,7 @@
         new_bl.push obj
         continue
       if bl_search and obj.val.indexOf(bl_search) == -1
-        val = obj.val
+        val = String(obj.val)
         console.log 'search: indexOf['+ bl_search +'] not found in ['+ val +']'
         new_bl.push obj
         continue


Fixed an instance of "course/period" when it should have been "section".

--- src/course/modify-registration.cjsx
+++ src/course/modify-registration.cjsx
@@ -47,7 +47,7 @@
     if course.isIncomplete()
       <EnrollmentCodeInput
         course={course}
-        title={"Leave #{original.description()} for new course/period"} />
+        title={"Leave #{original.description()} for new section"} />
     else if course.isPending()
       <ConfirmJoin
         course={course}


re-default notifies

re-default notifies

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -39,8 +39,8 @@
 glApi = null;
 s3bucket = 'ua-oit-security-pub'
 s3pathPrefix = 'stats'
-defaultNotifyAdmin = 'gfjohnson' # normally null, for 'all'
-defaultNotifySubscribers = 'gfjohnson' # normally null, for 'all'
+defaultNotifyAdmin = 'all' # normally null, for 'all'
+defaultNotifySubscribers = 'all' # normally null, for 'all'

 # enable or disable auto-banning
 attackAutoBanEnabled = true


note type of block in user response msg

note type of block in user response msg

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -254,20 +254,17 @@
   fwdata[list_name].push entry
   writeData()

-  usermsg = "Added `#{entry.val}` to fw #{list_name}."
-  if expires != 'undefined'
-    usermsg += "  Expires `#{entry.expires}`."
-  unless isTerse who
-    usermsg += "  Change will be applied in < 5 minutes."
+  usermsg = "Added `#{entry.val}` (#{entry.type}) to fw #{list_name}."
+  usermsg += "  Expires `#{entry.expires}`." if expires isnt 'undefined'
+  usermsg += "  Change will be applied in < 5 minutes." unless isTerse who
   msg.send usermsg

   logmsg = "#{modulename}: robot responded to #{who}: " +
     "added entry to #{list_name}"
   robot.logger.info logmsg

-  notifymsg = "#{who} added `#{entry.val}` to fw #{list_name}."
-  if expires isnt 'undefined'
-    notifymsg += "  Expires `#{entry.expires}`."
+  notifymsg = "#{who} added `#{entry.val}` (#{entry.type}) to fw #{list_name}."
+  notifymsg += "  Expires `#{entry.expires}`." if expires isnt 'undefined'
   notifySubscribers notifymsg, who

   # be terse after the first utterance


Whine about optimization in a comment

Whine about optimization in a comment

--- lib/marker-view.coffee
+++ lib/marker-view.coffee
@@ -46,6 +46,8 @@
       @appendRegion(1, { row: range.end.row, column: 0 }, range.end)

   appendRegion: (rows, start, end) ->
+    # FIXME: pixelPositionForScreenPosition is a major bottleneck with many
+    # markers on the page.
     { lineHeight, charWidth } = @editor
     css = @editor.pixelPositionForScreenPosition(start)
     css.height = lineHeight * rows


Reset page to 1 if data changes

Reset page to 1 if data changes

--- backbone.tableview.coffee
+++ backbone.tableview.coffee
@@ -165,6 +165,8 @@

     # Set data and update collection
     setData: (key, val, key2, val2) =>
+        if key != "page" and key2 != "page"
+            @data.page = 1
         if val
             @data[key] = val
         else if key