Fixed select add

Fixed select add

--- app/assets/javascripts/forms.coffee
+++ app/assets/javascripts/forms.coffee
@@ -70,8 +70,10 @@

   # Adds an option to select and selects that option
   setSelectValues = (el, vals) ->
-    $el.append("<option value='vals.id'>#{vals.to_s}</option>")
-    $el.val(vals.id)
+    $el = $(el)
+    desc = vals.to_s || vals.name || vals.description
+    $el.append("<option value='#{vals.id}'>#{desc}</option>")
+    $el.val("#{vals.id}")

   # Calls the events afser ajax call on ajax form
   callEvents = (data, resp) ->
@@ -86,7 +88,7 @@
         when $el.hasClass('autocomplete')
           setAutocompleteValues($el, resp)
         when $el.get(0).nodeName is 'SELECT'
-          setSelectValues($el resp)
+          setSelectValues(data.elem, resp)

   ##########################################


fix default tool selection

fix default tool selection

--- client/code/model/view.coffee
+++ client/code/model/view.coffee
@@ -34,7 +34,7 @@
   findByToolName: (name, callback) ->
     tool = app.tools().findByName name
     callback @find (view) ->
-      tool is tool and view.get('state') isnt 'deleted'
+      view.get('tool') is tool and view.get('state') isnt 'deleted'

   visible: ->
     visibles = @filter (t) -> t.isVisible()


use storage.getRepos() in /:user/:repo route

--- routes/repos.coffee
+++ routes/repos.coffee
@@ -32,13 +32,14 @@
   unless name? and repo?
     return res.reply 'index', error: "No repo for '/#{name}/#{repo}'."

-  argumenta.storage.getUser name, (err, user) ->
-    return res.notFound "User '#{name}' not found." if err
+  key  = [name, repo]
+  keys = [key]
+  argumenta.storage.getRepos keys, (err, repos) ->
+    return res.notFound "Repo '#{name}/#{repo}' not found." if err

-    argumenta.storage.getRepoTarget name, repo, (err, commit, target) ->
-      return res.notFound "Repo '/#{name}/#{repo}' not found." if err
-      return res.reply 'users/repo'
-        user: user
-        repo: repo
-        commit: commit
-        argument: target
+    repo = repos[0]
+    return res.reply 'users/repo'
+      user: repo.user
+      repo: repo.reponame
+      commit: repo.commit
+      argument: repo.target


a bit of organizing

a bit of organizing

--- bootstrap-magic.coffee
+++ bootstrap-magic.coffee
@@ -44,30 +44,14 @@
     # override with user defaults
     @setMany 'defaults', obj

-
-###
-# UI
-###
-
-getCurrentCategory = ->
-  myCat = BootstrapMagic.dictionary.currentCategory.get()
-  return _.where(bootstrap_magic_variables, { category: myCat })
-
-getCurrentSubCategory = ->
-  subCatId = BootstrapMagic.dictionary.currentSubCategory.get()
-  return _.find bootstrap_magic_variables, (group) -> group._id is subCatId
-
-Template._bootstrap_magic.onCreated ->
-  BootstrapMagic.start() if BootstrapMagic.start
-
-Template._bootstrap_magic.onRendered ->
-  BootstrapMagic.dictionary.currentCategory.set bootstrap_magic_variables[0].category
-  BootstrapMagic.dictionary.currentSubCategory.set bootstrap_magic_variables[0]._id
-
 # default starting script: load hard-coded defaults
 # this function can be overriden by other packages for integrations
 BootstrapMagic.on 'start', -> @setDefaults {}

+
+###
+# UI
+###
 mapVariableOverrides = (obj) ->
   obj.isOverride = false
   obj.isReference = false
@@ -92,6 +76,21 @@
       obj.reference = obj.reference.reference

   return obj
+
+getCurrentCategory = ->
+  myCat = BootstrapMagic.dictionary.currentCategory.get()
+  return _.where(bootstrap_magic_variables, { category: myCat })
+
+getCurrentSubCategory = ->
+  subCatId = BootstrapMagic.dictionary.currentSubCategory.get()
+  return _.find bootstrap_magic_variables, (group) -> group._id is subCatId
+
+Template._bootstrap_magic.onCreated ->
+  BootstrapMagic.start() if BootstrapMagic.start
+
+Template._bootstrap_magic.onRendered ->
+  BootstrapMagic.dictionary.currentCategory.set bootstrap_magic_variables[0].category
+  BootstrapMagic.dictionary.currentSubCategory.set bootstrap_magic_variables[0]._id

 camelToSnake = (str) -> str.replace(/\W+/g, '_').replace(/([a-z\d])([A-Z])/g, '$1-$2')


Fixes to regex for name search

Fixes to regex for name search

--- areas/search/client/scripts/searchResults.coffee
+++ areas/search/client/scripts/searchResults.coffee
@@ -3,13 +3,13 @@

 findByUsersTags = (name, tags)->
   Meteor.users.find(
-    name: { $regex: ".*"+name+".*"}
+    name: { $regex: "^"+name+".*", $options: "i"}
     tags: { $all: tags }
   ).fetch()

 findByProjectTags = (name, tags)->
   Projects.find(
-    name: { $regex: ".*"+name+".*"}
+    name: { $regex: "^"+name+".*", $options: "i"}
     tags: { $all: tags }
   ).fetch()


Make naming consistent

Make naming consistent

--- app/pages/admin/project-status.cjsx
+++ app/pages/admin/project-status.cjsx
@@ -28,7 +28,7 @@
   'Gravity Spy Gold Standard'
   'allow workflow query'
   'expert comparison summary'
-  'persist answers'
+  'persist annotations'
 ]

 ProjectToggle = React.createClass


add getNextlevelupTime test

add getNextlevelupTime test

--- test/test_scfes.coffee
+++ test/test_scfes.coffee
@@ -69,3 +69,40 @@
         return
       )
       clock.tick(15 * 6 * 60 * 1000)
+
+  describe "getNextLevelupTime", ->
+    beforeEach (done) ->
+      @scfes = new HubotScfes
+      @clock = sinon.useFakeTimers(0, "setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date")
+      done()
+
+    describe "correct return date", ->
+      it.skip "expart", (done) ->
+        #e expart is 83 exp
+        date = @scfes.getNextlevelupTime(830 + 50, "ex")
+        expect(date).to.eql(new Date(6 * 60 * 25 * 11))
+        done()
+
+      it.skip "hard", (done) ->
+        # hard is 46 exp
+        date = @scfes.getNextlevelupTime(230, "hard")
+        expect(date).to.eql(new Date(6 * 60 * 15 * 5))
+        done()
+
+      it.skip "normal", (done) ->
+        # normal is 26 exp
+        date = @scfes.getNextlevelupTime(520, "normal")
+        expect(date).to.eql(new Date(6 * 60 * 10 * 20))
+        done()
+
+      it.skip "easy", (done) ->
+        # easy is 12 exp
+        date = @scfes.getNextlevelupTime(24, "easy")
+        expect(date).to.eql(new Date(6 * 60 * 5 * 2))
+        done()
+
+    describe "boundary value", ->
+      it.skip "small value", (done) ->
+        date = @scfes.getNextlevelupTime(1, "hard")
+        expect(date).to.eql(new Date(6 * 60 * 25 * 1))
+        done()


Fix sidecomments bug when generating author name & improve its behavior

This commit fixes a bug that breaks sidecomments if `Meteor.user().emails`
is undefined. It now also can find a name when the user is authenticated
via Google and checks `Meteor.user().profile.name` as well.

--- client/views/blog/blog.coffee
+++ client/views/blog/blog.coffee
@@ -27,7 +27,21 @@
         id: 0
     else if Meteor.user()
       # check username
-      name = if Meteor.user().username then Meteor.user().username else Meteor.user().emails[0].address
+      the_user = Meteor.user()
+      possible_names = [                 # if more than one variable
+        the_user.username                # contains a usable name, the match
+        the_user.services?.google?.name  # with the lowest index will be chosen
+        the_user.profile?.name
+        the_user.emails?[0].address
+      ]
+      chosen_name = null
+      possible_names.every (name) ->
+        if name?
+          if typeof name is "string" and name.length > 0
+            chosen_name = name
+            false
+         else
+            true
       if Meteor.user().profile?[settings.userImg]
         avatar = Meteor.user().profile[settings.userImg]
       else


Fix enter keystroke on TypeAheads

Fix enter keystroke on TypeAheads

--- controls/TypeAhead/Mixin.jsx.coffee
+++ controls/TypeAhead/Mixin.jsx.coffee
@@ -26,7 +26,7 @@

   onKeyUp: (event) ->
     switch event.key
-      when 'Return' or 'Enter'
+      when 'Return', 'Enter'
         if @state.highlightedIndex >= 0
           # User hits enter on a selection from the dropdown
           @selectItem @state.options[@state.highlightedIndex]
@@ -80,4 +80,4 @@
         <label className="col-sm-2 control-label" htmlFor={this.props.dataKey}>{this.props.displayName}</label>
       )`

-module.exports = TypeAheadMixin+module.exports = TypeAheadMixin


Extract method uninstall_all_mocked_methods()


#1643: Bugfix in heredoc in test for 1643

--- test/assignment.coffee
+++ test/assignment.coffee
@@ -313,21 +313,21 @@
   for access in accesses
     for i,j in [1,2,3] #position can matter
       code =
-      """
-      nonce = {}; nonce2 = {}; nonce3 = {};
-      @o = o = new (class C then a:{}); f = -> o
-      [#{new Array(i).join('x,')}#{access}...] = [#{new Array(i).join('0,')}nonce, nonce2, nonce3]
-      unless #{access}[0] is nonce and #{access}[1] is nonce2 and #{access}[2] is nonce3 then throw new Error('[...]')
-      """
+        """
+        nonce = {}; nonce2 = {}; nonce3 = {};
+        @o = o = new (class C then a:{}); f = -> o
+        [#{new Array(i).join('x,')}#{access}...] = [#{new Array(i).join('0,')}nonce, nonce2, nonce3]
+        unless #{access}[0] is nonce and #{access}[1] is nonce2 and #{access}[2] is nonce3 then throw new Error('[...]')
+        """
       eq nonce, unless (try CoffeeScript.run code, bare: true catch e then true) then nonce
   # subpatterns like `[[a]...]` and `[{a}...]`
   subpatterns = ['[sub, sub2, sub3]', '{0: sub, 1: sub2, 2: sub3}']
   for subpattern in subpatterns
     for i,j in [1,2,3]
       code =
-      """
-      nonce = {}; nonce2 = {}; nonce3 = {};
-      [#{new Array(i).join('x,')}#{subpattern}...] = [#{new Array(i).join('0,')}nonce, nonce2, nonce3]
-      unless sub is nonce and sub2 is nonce2 and sub3 is nonce3 then throw new Error('[sub...]')
-      """
+        """
+        nonce = {}; nonce2 = {}; nonce3 = {};
+        [#{new Array(i).join('x,')}#{subpattern}...] = [#{new Array(i).join('0,')}nonce, nonce2, nonce3]
+        unless sub is nonce and sub2 is nonce2 and sub3 is nonce3 then throw new Error('[sub...]')
+        """
       eq nonce, unless (try CoffeeScript.run code, bare: true catch e then true) then nonce


Update i18n-zh-tw.coffee

Update i18n-zh-tw.coffee

--- addon/initializers/i18n-zh-tw.coffee
+++ addon/initializers/i18n-zh-tw.coffee
@@ -1,12 +1,12 @@
 I18nTranslationsZhTw =
   "socket_offline_error": "正在嘗試連接至 app.goodcity.hk..."
   "offline_error": "無法完成動作，請檢查您的網絡連線"
-  "offline_page": "無法開啟應用程式，請檢查您網絡連線"
+  "offline_page": "無法開啟應用程式，請檢查您的網絡連線"
   "try_again": "再試一次"
   "send": "傳送"
-  "delete_confirm": "一旦確認，將無法還原。你確定嗎？"
-  "online": "Online"
-  "not_connected": "Not Connected"
+  "delete_confirm": "一旦刪除，將無法還原。你確定嗎？"
+  "online": "在線上"
+  "not_connected": "未連線"

   "crossroads" : "十字路會"
   "language": "繁體中文"
@@ -18,7 +18,7 @@
   "prev" : "上一步"
   "back" : "回到上一頁"
   "end" : "結束"
-  "done": "Done"
+  "done": "完成"
   "error.message" : "出錯了，請見諒！"
   "images" : "圖片"
   "delete" : "刪除"
@@ -31,24 +31,24 @@
   "index.donate_goods" : "捐贈剩餘物資，幫助需要的人"
   "index.how" : "要怎樣做呢？"
   "index.start" : "立刻行動"
-  "full_name" : "{{firstName}} {{lastName}}"
+  "full_name" : "{{lastName}} {{firstName}}"
   "select_district" : "選擇地區"
   "offer_not_found": "找不到您搜索的捐獻項目"
   "item_not_found": "找不到您搜索的項目"
   "404_error" : "不好意思，您搜尋的資料並不存在"
-  "403_error" : "You are not authorized to take this action."
+  "403_error" : "你沒有權限執行這項行動。"
   "not_found": "不好意思，你搜尋的地點似乎不存在了！"
-  "okay": "可以了"
+  "okay": "確定"
   "cancel": "取消"
   "save": "儲存"
   "must_login": "您必須登入！"
   "unexpected_error": "出錯了"
   "all": "全部"
   "thank_you": "多謝"
-  "go_to_top": "Go to Top"
+  "go_to_top": "回到頁面頂端"
   "input_length_error_message" : "InputWithCounter doesn\'t work without a maxlength attribute'"
   "loading_timeout_error": "Loading view timeout reached."
-  "items_text": "items"
+  "items_text": "項目"
   "day":
     "today": "今天"
     "morning": "早上"
@@ -57,16 +57,16 @@

   "time_ago":
     "right_now" : "現正進行"
-    "1m": "1m"
-    "m": "m"
-    "1h": "1h"
-    "h": "h"
-    "1d": "1d"
-    "d": "d"
-    "1mths": "1mths"
-    "mths": "mths"
-    "1y": "1y"
-    "y": "y"
+    "1m": "1分鐘"
+    "m": "分鐘"
+    "1h": "1小時"
+    "h": "小時"
+    "1d": "1天"
+    "d": "天"
+    "1mths": "1個月"
+    "mths": "月"
+    "1y": "1年"
+    "y": "年"

   "application":
     "home" : "主頁"
@@ -117,9 +117,9 @@
     "view": "查看"

   "delivery":
-    "select_district": "Select Destrict"
+    "select_district": "選擇地區"
     "thank_offer":
-      "staff_will_contact": "我們的職員會於下星期五致電 {{mobile}} 給 {{name}} on {{mobile}} 以確認"
+      "staff_will_contact": "我們的職員會於下星期五致電 {{mobile}} 給 {{name}} 以確認"
     "book_timeslot":
       "hint": "請選擇可行的日期及時間。可選擇的時間已在下方列出。"
       "select_time": "選擇時間"
@@ -134,7 +134,7 @@
       "call_ggv": "聯絡GoGoVan"
       "back": "回到上一頁"
       "cancel_booking": "取消預約"
-      "confirm_ggv_cancel": "只要GoGoVan確認預約取消，您就能取消預約"
+      "confirm_ggv_cancel": "當GoGoVan確認預約取消，您就能取消預約"

   "delivery_details":
     "name": "聯絡人姓名"
@@ -146,24 +146,24 @@
     "vehicle_reg": "車輛登記"
     "fee": "基本收費"
     "booked": "已確認預約"
-    "driver_completed": "Picked up"
+    "driver_completed": "司機已確認預約"
     "waiting": "正在等候司機確認"
     "display_driver_details": "司機確認您的預約後，您即能查閱司機及車輛的資料"
     "time": "預約時間"
-    "id": "預約ID"
+    "id": "預約帳號"
     "driver_screen": "司機介面"
     "modify": "修改"
     "cancel_booking": "取消預約"
-    "accepted_items": "正在等候運送、已接受的物資"
-    "driver_picked_up_items": "Items picked up by GGV Driver"
+    "accepted_items": "已接受物資（正在等候運送）"
+    "driver_picked_up_items": "GoGoVan司機已接收物資"
     "charity_sale": "您願意慈善義賣捐贈的物資嗎？"

   "items":
     "add_item":
-      "condition": "物資狀態如何？"
+      "condition": "物資狀況如何？"
       "edit_image" : "修改圖片"
       "save" : "儲存資料"
-      "cancel": "Cancel"
+      "cancel": "取消"
       "description_placeholder" : "物品的描述、大小及數量等。如要增加或修改這項物品的相片，請點擊圖像。"

   "edit_images":
@@ -175,26 +175,26 @@
     "fullscreen_tooltip" : "全螢幕"
     "favourite_tooltip" : "設定為封面圖片"
     "delete_tooltip" : "刪除圖片"
-    "image_uploading": "Image Uploading "
-    "remove_image": "Remove image"
-    "cancel_item": "Cancel item"
+    "image_uploading": "圖片上載中"
+    "remove_image": "移除圖片"
+    "cancel_item": "取消項目"
     "cannot_provide_photo": "Cannot provide photo"
     "last_image_with_item": "This is the only image associated with this item"
-    "cancelling_item_will_cancel_offer" : "Cancelling the last item will cancel this offer and any associated transport booking."
+    "cancelling_item_will_cancel_offer" : "刪除最後一項物品即會刪除此捐獻及已預約的運輸。"

   "plan_delivery":
     "title" : "安排運輸"
     "fastest" :
       "title" : "最快速"
-      "info" : "
推薦租用車輛，兩三下子就能安排妥當！"
+      "info" : "
方便快捷，推介使用！"
       "cost" : "費用由 港元${{cost}}起"
-      "booking_button" : "預約"
+      "booking_button" : "了解詳情及預約時間"
       "learn_more": "了解詳情"
       "gogovan_charges": "預約GoGoVan"

     "alternate" :
       "title" : "其他選項"
-      "info" : "只須等候14十四日左右，
我們的團隊就能親身到您的社區，收集物資"
+      "info" : "只須等候十四日左右，
我們的團隊就能親身到您的社區，收集物資"
       "cost" : "費用由 港元 ${{cost}}起"
       "booking_button" : "預約"
       "collection_charges": "收集費用"
@@ -202,33 +202,33 @@
     "crossroads" :
       "title" : "親身運送"
       "info" : "於辦公時間，
親身將物資送到我們於屯門的辦公室"
-      "free" : "全免"
+      "free" : "費用全免"
       "booking_button" : "了解詳情及預約時間"

   "collection_charges":
       "info": '''
         十字路會的義工和社會企業合作組成了這項物資收集服務，協助有需要人士重投勞動市場。

-        為了支付人工及車輛費用（包括購買車輛、維修、保險等費用），我們很需要企業的贊助。同時，我們亦希望捐出物資的各位能幫忙分擔各項開支（燃油、停泊、拖車、保養等）。視乎物資所需的空間大小，一天的運輸開支平均為600元。因此您每次捐贈，我們均要求75元至600元不等的補助費。
-
-        由於我們的收集服務非常受歡迎，如選擇以這形式運送物資的話，可能需要等候數星期。為了節省您寶貴的時間，假如您自行安排車輛或親自運送，就再好不過了！
-
-        您的贊助讓我們能夠擴充收集服務，提供更快更好的服務，並幫助更多有需要的人！要是您的企業願意贊運輸費，我們將於運輸車輛上展示您的商標，展現您對社會的貢獻！請即電郵至 {{email}} ，查詢贊助的詳情吧！
+        為了支付人工及車輛費用（包括購買車輛、維修、保險等費用），我們很需要企業的贊助。同時，我們亦希望捐贈者能分擔各項開支（燃油、停泊、拖車、保養等）。視乎物資所需的空間大小，一天的運輸開支平均為600元。因此您每次捐贈，我們均要求75元至600元不等的補助費。
+
+        由於我們的收集服務非常受歡迎，如選擇以這形式運送物資的話，可能需要等候數星期。為了節省您寶貴的時間，假如您自行安排車輛或親自運送，就最好不過了！
+
+        您的贊助讓我們能夠擴充收集服務，提升服務質素，並幫助更多有需要的人！要是您的企業願意贊助運輸費，我們將於運輸車輛上展示您的商標，展現您對社會的貢獻！請即電郵至 {{email}} ，查詢贊助的詳情吧！
         '''

   "gogovan_charges":
     "info": '''
       GoGoVan與好人好市合作，方便您租用貨車，運送物資！

-      現在，您可以根據您居住的地區，及切合您所需的貨車類型，預先得到租用費的報價。我們的義工會先評估，基於您的需要，租用小型貨車還是大貨車，並給予報價。
-
-      GoGoVan的報價並不包括搬運費、隧道費及其他額外收費。在確認預約前，您必須先與司機商討費用安排。
-
-      搬運費：
根據過往經驗，為確保安全搬運大型物件，您通常需要兩位工人協助，而每位工人的收費大約為每小時$100元。
-
-      獨立營運的司機會使用GoGoVan運送物資，但他們並非受雇於GoGoVan。不同的司機可能會為「移除物資」作出不同程度或形式的協助，但一般而言，司機通常不會協助裝拆物資及將分散的部件裝箱。為了使運送過程更快更方便，您可以預先包裝物資，或與司機商討協助，他們通常能立刻為您報價。
-
-      同時，要是您居住的地點可能對搬運構成任何困難（例如樓梯、泊車，只有行人道或其他障礙物），請不要隱瞞，務必清楚相告，讓司機能按情況需要收費。要是地點較難到達，部分司機可能無法接收此一工作，請見諒。
+      現在，您可以根據您居住的地區，及符合您所需的貨車類型，預先得到報價。我們的義工會根據您的需要，決定貨車種類及大小，並給予報價。
+
+      GoGoVan的報價並不包括搬運費、隧道費及其他額外收費。在確認預約前，您必須先與司機商討收費。
+
+      搬運費：
根據過往經驗，為確保能安全搬運大型物件，您通常需要兩位工人協助，而每位工人的收費大約為每小時$100元。
+
+      獨立營運的司機會使用GoGoVan運送物資，但他們並非受僱於GoGoVan。不同的司機可能會為「搬運物資」作出不同程度的協助，但一般而言，司機通常不會協助裝拆物資及將分散的部件裝箱。為了使運送過程更快更方便，您可以預先包裝物資，或與司機商討，他們通常能立刻為您報價。
+
+      同時，要是您居住的地點可能增加搬運的困難（例如樓梯、泊車，只有行人道或其他障礙物），請清楚相告，讓司機能按情況收費。要是地點較難到達，部分司機可能無法接收此工作，請見諒。
       '''

   "gogovan":
@@ -240,16 +240,16 @@
       "obstacles" : "路程有沒有樓梯或障礙物"
       "park_van" : "停泊車輛是否無須看管"
       "help" : "您及您的朋友是否能幫忙"
-      "unwilling_driver" : "要是捐贈大型傢俱以及/或搬運有困難的話，司機可能不願意協助搬運，或須收取高昂費用。"
-      "thanks" : "明白了，謝謝！"
+      "unwilling_driver" : "要是捐贈大型傢俱以及/或搬運有困難，司機可能不願意協助搬運，或須收取較高昂費用。"
+      "thanks" : "確定，謝謝！"

     "confirm_van" :
       "other-msg" : "並不包括隧道費及泊車費等費用"
       "title" : "確定車輛"
       "base_fee" : "基本費用"
       "porterage" : "搬運"
-      "speak_english" : "須以英語溝通"
-      "trolley" : "須借用手推車"
+      "speak_english" : "需以英語溝通"
+      "trolley" : "需借用手推車"
       "details" : "詳情"
       "other" : "其他"
       "confirm_with_driver" : "在搬運前，請先與司機商討總價格"
@@ -257,7 +257,7 @@
       "phone" : "電話"
       "book_delivery" : "確認及預約"
       "negotiate" : "與司機商討"
-      "no_porterage" : "不必要"
+      "no_porterage" : "不需要"

     "book_van" :
       "title" : "預約貨車"
@@ -265,22 +265,22 @@
       "select_day" : "日期及時間"
       "crossroads_time" : "只限十字路會辦公時間"
       "requirements" : "額外要求"
-      "speak_english" : "須以英語溝通"
-      "trolly_cost" : "須借用手推車 + 每輛20元"
-      "porterage" : "須協助搬運（須與司機商量）"
+      "speak_english" : "需以英語溝通"
+      "trolly_cost" : "需借用手推車（每輛20元）"
+      "porterage" : "需協助搬運（需與司機商量）"
       "extra_time_charge" : "額外時間（已包括15分鐘搬運時間，超過15分鐘可能需要額外收費）"
       "get_quote" : "報價"
-      "date": "Date"
-      "time": "Time"
-      "am": "AM"
-      "pm": "PM"
-      "vehicle": "Vehicle Type"
+      "date": "日期"
+      "time": "時間"
+      "am": "上午"
+      "pm": "下午"
+      "vehicle": "車輛種類"

   "drop-off":
     "select_schedule": "選擇時間"
     "expect_delivery": "物資大約何時到達？"
-    "find_crossroads": "如何到達十字路會"
-    "open_new_tab": "將在新標籤打開"
+    "find_crossroads": "如何到達十字路會？"
+    "open_new_tab": "將在新視窗打開"
     "arrive_on_time": "煩請於我們的辦公時間內到達，以確保我們能順利接收您的物資。"
     "book_schedule": "預約時間"


Don't do anything if not logged in

Don't do anything if not logged in

--- resources/assets/coffee/_classes/account-edit-blocklist.coffee
+++ resources/assets/coffee/_classes/account-edit-blocklist.coffee
@@ -26,6 +26,8 @@

   updateBlockCount: =>
+    return unless currentUser.id?
+
     $("#{@jsClass}-count").text osu.trans('users.blocks.blocked_count', count: currentUser.blocks.length)


Utils: getAppIcon let it works, we need to remove this from here.

--- client/Main/utils.coffee
+++ client/Main/utils.coffee
@@ -382,21 +382,21 @@
     ]
     view.setCss "backgroundImage", rule for rule in rules

-  getAppIcon:(appManifest)->
-    {authorNick, name, version, icns} = appManifest
-
-    resourceRoot = "#{KD.appsUri}/#{authorNick}/#{name}/#{version}/"
-
-    if appManifest.devMode # TODO: change url to https when vm urls are ready for it
-      resourceRoot = "http://#{KD.getSingleton('vmController').defaultVm}/.applications/#{__utils.slugify name}/"
+  getAppIcon:(name)->
+
+    # resourceRoot = "#{KD.appsUri}/#{authorNick}/#{name}/#{version}/"
+
+    # if appManifest.devMode # TODO: change url to https when vm urls are ready for it
+    #   resourceRoot = "http://#{KD.getSingleton('vmController').defaultVm}/.applications/#{__utils.slugify name}/"
+
+
+    # for size in [64, 128, 160, 256, 512]
+    #   if icns and icns[String size]
+    #     thumb = "#{resourceRoot}/#{icns[String size]}"
+    #     break

     image  = if name is "Ace" then "icn-ace" else "default.app.thumb"
     thumb  = "#{KD.apiUri}/images/#{image}.png"
-
-    for size in [64, 128, 160, 256, 512]
-      if icns and icns[String size]
-        thumb = "#{resourceRoot}/#{icns[String size]}"
-        break

     img = new KDCustomHTMLView
       tagName     : "img"


Update richtext.coffee

Update richtext.coffee

--- examples/richtext.coffee
+++ examples/richtext.coffee
@@ -30,11 +30,11 @@
 Received: by 10.216.145.157 with SMTP id p29mr6779990wej.65.1338693628411;
  Sat, 02 Jun 2012 20:20:28 -0700 (PDT)
 Received: by 10.223.79.7 with HTTP; Sat, 2 Jun 2012 20:20:28 -0700 (PDT)
-Reply-To: davidroberts@gmail.com
+Reply-To: chazroberts@gmail.com
 Date: Sat, 2 Jun 2012 20:20:28 -0700
 Message-ID: <CAAf=x5XAZM6WHs=OCbk9trbZhF72X+GrT7sk3-J+TSnK7eVucw@mail.gmail.com>
 Subject: asdfasdf
-From: David Roberts <davidroberts@gmail.com>
+From: David Roberts <chazroberts@gmail.com>
 To: test@nodejs.io
 Content-Type: multipart/alternative; boundary=0016e6dab0742f40d504c188e601

@@ -45,16 +45,15 @@
 as
 --
 David K Roberts
-davidroberts@gmail.com
+chazroberts@gmail.com
 cell:312-772-2167
-skype: djwglpuppy

 --0016e6dab0742f40d504c188e601
 Content-Type: text/html; charset=ISO-8859-1

-asdf asd fasdfasdf

as
--
David K Roberts
davidroberts@gmail.com
cell:312-772-2167
skype: djwglpuppy

+asdf asd fasdfasdf

as
--
David K Roberts
davidroberts@gmail.com
cell:312-772-2167
skype: djwglpuppy

 --0016e6dab0742f40d504c188e601--
 .

-""")+""")


add test for index with format option

add test for index with format option

--- test/index-test.coffee
+++ test/index-test.coffee
@@ -56,3 +56,68 @@
         </body>
       </html>
     '''
+    format = (nodes, options) ->
+      assert nodes.length is 2
+      assert nodes[0].type is 'doctype'
+      assert nodes[0].value is 'html'
+      assert nodes[1].type is 'element'
+      assert nodes[1].name is 'html'
+      assert nodes[1].attributes.length is 1
+      assert nodes[1].attributes[0].type is 'attribute'
+      assert nodes[1].attributes[0].name is 'lang'
+      assert nodes[1].attributes[0].value is 'en'
+      assert nodes[1].children.length is 2
+      head = nodes[1].children[0]
+      assert head.type is 'element'
+      assert head.name is 'head'
+      assert head.attributes.length is 0
+      assert head.children.length is 2
+      assert head.children[0].type is 'empty element'
+      assert head.children[0].name is 'meta'
+      assert head.children[0].attributes.length is 1
+      assert head.children[0].attributes[0].type is 'attribute'
+      assert head.children[0].attributes[0].name is 'charset'
+      assert head.children[0].attributes[0].value is 'utf-8'
+      assert head.children[0].children.length is 0
+      assert head.children[1].name is 'title'
+      assert head.children[1].attributes.length is 0
+      assert head.children[1].children.length is 1
+      assert head.children[1].children[0].type is 'text'
+      assert head.children[1].children[0].value is 'TITLE'
+      body = nodes[1].children[1]
+      assert body.type is 'element'
+      assert body.name is 'body'
+      assert body.attributes.length is 0
+      assert body.children.length is 3
+      assert body.children[0].type is 'comment'
+      assert body.children[0].value is 'comment'
+      assert body.children[1].type is 'element'
+      assert body.children[1].name is 'h1'
+      assert body.children[1].attributes.length is 1
+      assert body.children[1].attributes[0].type is 'attribute'
+      assert body.children[1].attributes[0].name is 'class'
+      assert body.children[1].attributes[0].value is 'title'
+      assert body.children[1].children.length is 1
+      assert body.children[1].children[0].type is 'text'
+      assert body.children[1].children[0].value is 'HEADLINE'
+      assert body.children[2].type is 'element'
+      assert body.children[2].name is 'p'
+      assert body.children[2].attributes.length is 0
+      assert body.children[2].children.length is 3
+      assert body.children[2].children[0].type is 'text'
+      assert body.children[2].children[0].value is 'Hello, b-html!'
+      assert body.children[2].children[1].type is 'text'
+      assert body.children[2].children[1].value is '@bouzuya'
+      img = body.children[2].children[2]
+      assert img.type is 'empty element'
+      assert img.name is 'img'
+      assert img.attributes.length is 2
+      assert img.attributes[0].type is 'attribute'
+      assert img.attributes[0].name is 'alt'
+      assert img.attributes[0].value is 'sample image'
+      assert img.attributes[1].type is 'attribute'
+      assert img.attributes[1].name is 'src'
+      assert img.attributes[1].value is '/images/sample.png'
+      assert img.children.length is 0
+      assert.deepEqual options, { format }
+    bHtml source, { format }


Resistance Sympathizer fix

Resistance Sympathizer fix

This pilot wasn't showing in the YT-1300 list. I assume the reason was that lower case "t" in Yt-1300.

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -814,7 +814,7 @@
             ship: "T-70 Ala-X"
         "Resistance Sympathizer":
             name: "Simpatizante de la Resistencia"
-            ship: "Yt-1300"
+            ship: "YT-1300"
         "Rey":
             text: """Cuando ataques o te defiendas, si la nave enemiga está dentro de tu arco de fuego, puedes volver a tirar hasta 2 dados en los que hayas sacado caras vacías."""
         'Han Solo (TFA)':


fix(tap): back button detection in child states

--- modules/directives/tap/index.coffee
+++ modules/directives/tap/index.coffee
@@ -41,12 +41,6 @@
     if element.parents('[bp-iscroll]').length
       element.attr 'bp-bound-margin', '5'
       options.boundMargin = 5
-    # * Apply `bp-button-back` class if tap will result in reverse transition.
-    if element.is 'bp-button'
-      toStateName = (attrs.bpTap.match /to\(('|")([A-Za-z]+)('|")/)?[2]
-      if toStateName and angular.isFunction scope.getDirection
-        dir = scope.getDirection to: toStateName
-        if dir is 'reverse' then element.addClass 'bp-button-back'

     touch = {}


Add max lengths to Device names as per spec

--- lib/device.coffee
+++ lib/device.coffee
@@ -33,11 +33,12 @@

     # build device element
     buildDevice = ->
+        name = config.app.name.substr(0, 64)
         [
             { deviceType: makeDeviceType(config) }
-            { friendlyName: config.app.name }
-            { manufacturer: config.app.name }
-            { modelName: config.app.name + ' Media Server' }
+            { friendlyName: name }
+            { manufacturer: name }
+            { modelName: name.substr(0, 32) }
             { UDN: config.device.uuid }
         ]


Focus rootView directlly instead of by selector

--- lib/dialog.coffee
+++ lib/dialog.coffee
@@ -32,7 +32,7 @@

   close: ->
     @remove()
-    $('#root-view').focus()
+    rootView.focus()

   cancel: ->
     @remove()


fix spacing in signed number

fix spacing in signed number


removed client.request

removed client.request

--- lib/atom-ternjs-autocomplete.coffee
+++ lib/atom-ternjs-autocomplete.coffee
@@ -41,7 +41,6 @@
         return suggestions

     callPreBuildSuggestions: (force) ->
-        return if @client.request
         cursor = @_editor.getLastCursor()
         prefix = cursor.getCurrentWordPrefix()
         if force || /^[a-z0-9.\"\']$/i.test(prefix[prefix.length - 1])


Delete DoSomething.coffee

Delete DoSomething.coffee

--- components/DoSomething.coffee
+++ components/DoSomething.coffee
@@ -1,25 +0,0 @@
-noflo = require 'noflo'
-
-exports.getComponent = ->
-  c = new noflo.Component
-
-  # Define a meaningful icon for component from http://fontawesome.io/icons/
-  c.icon = 'cog'
-
-  # Provide a description on component usage
-  c.description = 'do X'
-
-  # Add input ports
-  c.inPorts.add 'in',
-    datatype: 'string'
-    process: (event, payload) ->
-      # What to do when port receives a packet
-      return unless event is 'data'
-      c.outPorts.out.send payload
-
-  # Add output ports
-  c.outPorts.add 'out',
-    datatype: 'string'
-
-  # Finally return the component instance
-  c


fix julia boot on windows

fix julia boot on windows

--- lib/connection/process/basic.coffee
+++ lib/connection/process/basic.coffee
@@ -82,20 +82,21 @@
         client.stderr "PowerShell version < 3 encountered. Running without wrapper (interrupts won't work)."
         @getUnix path, args
       else
-        @freePort().then (wrapPort) =>
-          jlargs = [args..., '"`"' + paths.script('boot.jl') + '`""', port]
-          proc = child_process.spawn("powershell",
-                                     ["-NoProfile", "-ExecutionPolicy", "bypass",
-                                      "& \"#{paths.script "spawnInterruptible.ps1"}\"
-                                      -wrapPort #{wrapPort}
-                                      -jlpath \"#{path}\"
-                                      -jlargs #{jlargs}"])
+        tcp.listen().then (port) =>
+          @freePort().then (wrapPort) =>
+            jlargs = [args..., '"`"' + paths.script('boot.jl') + '`""', port]
+            proc = child_process.spawn("powershell",
+                                       ["-NoProfile", "-ExecutionPolicy", "bypass",
+                                        "& \"#{paths.script "spawnInterruptible.ps1"}\"
+                                        -wrapPort #{wrapPort}
+                                        -jlpath \"#{path}\"
+                                        -jlargs #{jlargs}"])

-          @createProc proc,
-            wrapper: true
-            kill: => @sendSignalToWrapper 'KILL', wrapPort
-            interrupt: => @sendSignalToWrapper 'SIGINT', wrapPort
-            socket: @clientSocket proc
+            @createProc proc,
+              wrapper: true
+              kill: => @sendSignalToWrapper 'KILL', wrapPort
+              interrupt: => @sendSignalToWrapper 'SIGINT', wrapPort
+              socket: @clientSocket proc

   get_: (a...) ->
     if process.platform is 'win32'


fix pagination

fix pagination

--- lib/carte/client/views/list.cjsx
+++ lib/carte/client/views/list.cjsx
@@ -76,36 +76,61 @@
               if @props.cards.query.order == 'random'

-
-
-
+                    {
+                      if @props.cards.page
+
+
+
+                      else
+
+
+
+                    }

               else
-                if @props.cards.page
-

+

+

                     {
-                      if @props.cards.page.current > 1
-

-
-                            «
-
-

+                      if @props.cards.page
+                        if @props.cards.page.current > 1
+                          href = "/#/?" + @pageParam(@props.cards.page.current - 1)
+                        else
+                          href = "/#/?" + @pageParam(@props.cards.page.total)
+                      else
+                        href = "javascript:void(0)"
+
+                        «
+
                     }
-

-
-                        {@props.cards.page.current} / {@props.cards.page.total}
+

+

+                    {
+                      if @props.cards.page
+
+                          {@props.cards.page.current} / {@props.cards.page.total}
+
+                      else
+
+
+
+                    }
+

+

+                    {
+                      if @props.cards.page
+                        if @props.cards.page.current < @props.cards.page.total
+                          href = "/#/?" + @pageParam(@props.cards.page.current + 1)
+                        else
+                          href = "/#/?" + @pageParam(1)
+                      else
+                        href = "javascript:void(0)"
+
+                        »

-

-                    {
-                      if @props.cards.page.current < @props.cards.page.total
-

-
-                            »
-
-

                     }
-

+

+

             }


Update direction comment

Update direction comment

--- lib/getETA.coffee
+++ lib/getETA.coffee
@@ -2,7 +2,7 @@
   getETA: (stop,route,direction)->
     check stop,String # This should be the stop_id
     check route,String # This should be the route_id
-    check direction,String # This should be the direction, as specified in the feed
+    check direction,String # This should be the direction_id, as specified in the trips feed

     # Get data from backend (check http://docs.meteor.com/#http for usage)
     #HTTP.get 'http://localhost/get...'


DEFUN detects documentation string in body, discards

--- ya.coffee
+++ ya.coffee
@@ -126,6 +126,8 @@
       _eval.call(this, args) # ['fun', 100, 200]

   DEFUN: (name, parameterNames, body...)->
+    # body may include optional documentation string
+    documentation = body.shift() if typeof(body[0]) == 'string'
     this.previousFrame.bind(name, _eval.call(this, ['LAMBDA', parameterNames, body...]))

   TRACE: (funcNames...) ->


Math correction for watchedscores.

Math correction for watchedscores.

--- scripts/watched.coffee
+++ scripts/watched.coffee
@@ -79,7 +79,7 @@
             hourcount = msg.match[1]
             savedhours = robot.brain.get 'topwatchers'

-            watchedscores = [timename + " has " + hours[timename]] for timename of savedhours
+            watchedscores = [timename + " has " + hours[timename] / 100 + " hours watched."] for timename of savedhours

             hourscore = watchedscores.sort((a, b) ->
                 b.hours[timename] - a.hours[timename]


add resize method

add resize method

--- src/scripts/app.cjsx
+++ src/scripts/app.cjsx
@@ -17,6 +17,26 @@
   changeFactorType: (e) ->
     @setState
       factorType: e.target.id
+    @resize()
+
+  componentDidMount: ->
+    @resize()
+    window.addEventListener 'resize', @resize
+
+  componentWillUnmount: ->
+    window.removeEventListener 'resize', @resize
+
+  resize: ->
+    height = $('body').height() + 20
+    console.log height
+    window.top.postMessage(
+      JSON.stringify(
+        kinja:
+          sourceUrl: window.location.href
+          resizeFrame:
+            height: height
+      ), '*'
+    )

   changeFactor: (e) ->
     @setState


:lipstick: Remove unnecessary double conditions

--- src/selection.coffee
+++ src/selection.coffee
@@ -514,7 +514,7 @@
     if maintainClipboard
       {text: clipboardText, metadata} = atom.clipboard.readWithMetadata()

-      if metadata? and metadata.selections?
+      if metadata?.selections?
         metadata.selections.push(text)
       else
         metadata = { selections: [clipboardText, text] }


Update parse_tex_log w some ad'l file heuristics

These were added to the ST version and are useful in some circumstances

--- lib/parsers/parse-tex-log.coffee
+++ lib/parsers/parse-tex-log.coffee
@@ -138,6 +138,8 @@
   file_useless1_rx = /\{\"?(?:\.|\.\.\/)*[^\.]+\.[^\{\}]*\"?\}(.*)/
   # Useless file #2: <filename.ext>; capture subsequent text
   file_useless2_rx = /<\"?(?:\.|\.\.\/)*[^\.]+\.[^>]*\"?>(.*)/
+  # attempt to filter out package continuation messages
+  file_package_rx = /^\s*\([a-zA-Z]+\)\s{4,}.+/
   pagenum_begin_rx = /\s*\[\d*(.*)/
   line_rx = /^l\.(\d+)\s(.*)/    # l.nn <text>
   warning_rx = /^(.*?) Warning: (.+)/ # Warnings, first line
@@ -231,6 +233,11 @@
       # HEURISTIC: check first if we just have a long "(.../file.tex" (or similar) line
       # A bit inefficient as we duplicate some of the code below for filename matching
       file_match = file_rx.exec(line)
+      if file_match
+        if line.startsWith('runsystem') or file_package_rx.exec(line)
+          debug("Ignoring possible file: #{line}")
+          file_match = false
+
       if file_match
         debug("MATCHED (long line)")
         file_name = file_match[1]
@@ -271,7 +278,12 @@
           # HEURISTIC: if extra line begins with "Package:" "File:" "Document Class:",
           # or other "well-known markers",
           # we just had a long file name, so do not add
-          if extralen>0 && (extra.slice(0,5)=="File:" || extra.slice(0,8)=="Package:" || extra.slice(0,15)=="Document Class:") || (extra.slice(0,9)=="LaTeX2e <") || extra.match(assignment_rx)
+          if extralen > 0 and (
+            extra.slice(0,5) == "File:" or
+            extra.slice(0,8) == "Package:" or
+            extra.slice(0,11) == "Dictionary:" or
+            extra.slice(0,15) == "Document Class:"
+          ) or extra.slice(0,9) == "LaTeX2e <" or extra.match(assignment_rx)
             debug("Found File: and friends, or LaTeX2e, or assignment_rx match")
             extend_line = false
             # no need to recycle extra, as it's nothing we are interested in
@@ -283,6 +295,15 @@
             debug("Found [...]")
             extend_line = false
             recycle_extra = true # make sure we process the "l.<nn>" line!
+          # if "extra" (next line) starts with a ( and we already have a valid
+          # file, this is likely the start of something else we need to process
+          # as a file, so add a space
+          else if extralen > 0 and extra[0] == '(' and isfile(file_name)
+            line = "#{line} #{extra}"
+            debug("Extended #{line}")
+            linelen += extralen + 1
+            if extralen < 79
+              extend_line = false
           else
             line += extra
             debug("Extended: " + line)
@@ -341,7 +362,12 @@
       debug(line)

     # Skip things that are clearly not file names, though they may trigger false positives
-    if line.length>0 && (line.slice(0,5)=="File:" || line.slice(0,8)=="Package:" || line.slice(0,15)=="Document Class:") || (line.slice(0,9)=="LaTeX2e <")
+    if line.length > 0 and (
+      line.slice(0,5) == "File:" or
+      line.slice(0,8) == "Package:" or
+      line.slice(0,11) == "Dictionary:" or
+      line.slice(0,15) == "Document Class:"
+    ) or line.slice(0,9) == "LaTeX2e <" or line.match(assignment_rx)
       continue

     # Are we done? Get rid of extra spaces, just in case (we may have extended a line, etc.)


If request is for the root path (/), pathparts was null (no match)

--- lib/malifi.coffee
+++ lib/malifi.coffee
@@ -43,7 +43,7 @@
         decoded_path: pathname

     siteStack= loader.site_lookup.call(pathinfo,req).concat(baseSiteStack)
-    pathparts= pathname.match(extractNameParts)
+    pathparts= pathname.match(extractNameParts) || []

     # Add a malifi object to req.  It contains the current metadata, a
     # reference to an object that can look up any metadata, the site stack and
@@ -60,8 +60,8 @@
         #   path.dot_extension=             .txt
         #   path.extension=                  txt
         relative: pathname
-        relative_base: pathparts[1]
-        base: pathparts[2]
+        relative_base: pathparts[1] || ''
+        base: pathparts[2] || ''
         dot_extension: pathparts[3] || ''
         extension: pathparts[4] || ''


No longer override this.inclusive in visual-mode since it's not referenced in logic.

Yes in visual-mode, all motion act as inclusive, but it’s should be
done by checking if it’s in visual-mode.

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -45,9 +45,7 @@
   constructor: ->
     super

-    # visual mode can overwrite default wise
     if @vimState.mode is 'visual'
-      @inclusive = true
       @wise = @vimState.submode
     @initialize()


WIP for motion refactoring.

WIP for motion refactoring.

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -43,6 +43,31 @@
     @editor.moveCursors (cursor) =>
       @moveCursor(cursor)

+  getSelectedBufferRow: (selection, which) ->
+    rows = selection.getBufferRowRange()
+    rows = rows.reverse() if selection.isReversed()
+    [tail, head] = rows
+    switch which
+      when 'head' then head
+      when 'tail' then tail
+
+  # selectLinewise: (selection, options) ->
+  #   selection.modifySelection =>
+  #     rowFrom = @getSelectedBufferRow(selection, 'tail')
+  #     if selection.isEmpty()
+  #       selection.cursor.moveRight()
+  #     unless selection.isEmpty() or selection.isReversed()
+  #       if selection.cursor.isAtBeginningOfLine()
+  #         selection.cursor.moveLeft()
+  #     @moveCursor(selection.cursor)
+  #     if selection.isEmpty()
+  #       selection.cursor.moveRight()
+  #     if selection.cursor.isAtBeginningOfLine()
+  #       selection.cursor.moveRight()
+  #     rowTo = @getSelectedBufferRow(selection, 'head')
+  #     selection.selectLine(rowFrom)
+  #     selection.selectLine(rowTo)
+
   selectLinewise: (selection, options) ->
     selection.modifySelection =>
       [oldStartRow, oldEndRow] = selection.getBufferRowRange()
@@ -82,6 +107,13 @@
       else
         # for forward motion, add the ending character of the motion
         selection.cursor.moveRight()
+
+  isSingleColumnSelection: (selection) ->
+    selection.getBufferRange().toDelta().isEqual([0, 1])
+
+  # selectVisual: (selection, options) ->
+  #   selection.modifySelection =>
+  #     @moveCursor(selection.cursor, options)

   selectVisual: (selection, options) ->
     selection.modifySelection =>


Added server side username update method

--- server/accounts.coffee
+++ server/accounts.coffee
@@ -39,6 +39,22 @@
     Accounts.sendEnrollmentEmail userId

     invited._id
+
+  'set-username': (newUsername) ->
+    check newUsername, String
+
+    # TODO: Check new username
+    throw new Meteor.Error 400, 'Invalid username' if FORBIDDEN_USERNAME_REGEX.test newUsername
+
+    Meteor.users.update
+      _id: Meteor.userId()
+    ,
+      $set:
+        username: newUsername
+
+    user = Meteor.users.findOne
+      _id: Meteor.userId()
+

 Accounts.onCreateUser (options, user) ->
   # Idea is that only server side knows invite secret and we can


:memo:

:memo:

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -317,6 +317,8 @@
       cursor.moveDown()
     @skipTrailingWhitespace(cursor)

+# MoveToFirstCharacterOfLine faimily
+# ------------------------------------
 class MoveToFirstCharacterOfLine extends Motion
   @extend()
   moveCursor: (cursor) ->


Don’t lock globally when file changes occurs

Fixes potential problems where multiple files are changed at the same time or with plugins that are really slow.

Refs #235

--- src/core/server.coffee
+++ src/core/server.coffee
@@ -66,11 +66,11 @@

   # tasks that will block the request until completed
   block =
-    contentChange: false
     contentsLoad: false
     templatesLoad: false
     viewsLoad: false
     localsLoad: false
+    files: {}

   isReady = ->
     ### Returns true if we have no running tasks ###
@@ -128,11 +128,17 @@
           return true
       return false
     ignoreInitial: true
+
   contentWatcher.on 'change', (path) ->
-    return if not contents? or block.contentsLoad or block.contentChange
-    # ignore if we dont have the tree loaded or it's loading
-
-    block.contentChange = true
+
+    # ignore if we dont have the tree loaded or it's still loading
+    return if not contents? or block.contentsLoad
+
+    # also ignore if we are already working on this file
+    # (windows sometimes sends multiple change events for the same file)
+    return if block.files[path] is true
+
+    block.files[path] = true

     content = null
     for item in ContentTree.flatten(contents)
@@ -157,7 +163,6 @@
       if error?
         contents = null
         lookup = {}
-        block.contentChange = false
         return

       # replace old contents
@@ -172,7 +177,7 @@
       delete lookup[normalizeUrl(content.url)]
       lookup[normalizeUrl(newContent.url)] = newContent

-      block.contentChange = false
+      delete block.files[path]
       env.emit 'change', content.filename

   # reload entire tree if a file is removed or added


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -147,7 +147,8 @@
           moduleName = Module
           Module = Module.constantize()
         else
-          # Attempt to discover module na
+          # Attempt to discover module name
+
         self.extend Module(I, self) unless I.included_modules.include moduleName

       return self


change selected cell color opacity so borders aren't hidden in some browsers

--- app/assets/volume/spreadsheet.coffee
+++ app/assets/volume/spreadsheet.coffee
@@ -846,7 +846,7 @@
           if info.t == 'rec'
             for c, ci in cell.classList when c.startsWith('ss-rec_')
               styles.set('.' + c + '{background-color:' +
-                (if c.includes('_', 7) then '#e8e47f' else 'rgba(242,238,100,0.4)') +
+                (if c.includes('_', 7) then 'rgba(226,217,0,0.6)' else 'rgba(242,238,100,0.4)') +
                 ';\n text-}')

           edit(cell, info) if editing


no need to filter files, process only local fonts

--- app/templates/gulp/tasks/fonts.coffee
+++ app/templates/gulp/tasks/fonts.coffee
@@ -2,7 +2,6 @@
 helpers = require '../helpers'
 $ = helpers.gulpLoad [
   'if'
-  'filter'
   'rev'
   'flatten'
 ]
@@ -10,7 +9,6 @@

 module.exports = ->
   gulp.src PROP.path.fonts()
-    .pipe $.filter PROP.path.fonts("pattern")
     .pipe $.if !PROP.isDev, $.rev.font()
     .pipe $.flatten()
     .pipe gulp.dest PROP.path.fonts("dest")


Require login.

Require login.

--- lib/router.coffee
+++ lib/router.coffee
@@ -71,6 +71,17 @@
     action: ->
       Router.go "listsShow", Lists.findOne()

+requireLogin = (pause) ->
+  unless Meteor.user()
+    if Meteor.loggingIn()
+      @render @loadingTemplate
+      pause()
+    else
+      @render "signin"
+      pause()
+
+Router.onBeforeAction requireLogin
+
 if Meteor.isClient
   Router.onBeforeAction "loading",
     except: [


cmd line

cmd line

--- server/kcServer.coffee
+++ server/kcServer.coffee
@@ -8,15 +8,18 @@
 	sio: require('socket.io')
 	sha: require('sha1')
 	aes: require('aes256')
-	readline: require('readline')
+	rl: require('readline')

 # Initialize modules
-WebServer = https.createServer
-	key: fs.readFileSync ResourceDir + 'privkey.pem'
-	cert: fs.readFileSync ResourceDir + 'cert.pem'
+WebServer = Modules.https.createServer
+	key: Modules.fs.readFileSync ResourceDir + 'privkey.pem'
+	cert: Modules.fs.readFileSync ResourceDir + 'cert.pem'
 , (req, res) ->
 	res.end 'You were not supposed to visit here with a browser. Use a KentChat client!'
-SIOServer = sio(WebServer)
+SIOServer = Modules.sio(WebServer)
+Stdin = Modules.rl.createInterface
+	input: process.stdin
+	output: process.stdout

 # Declare options and variables
 Options =
@@ -30,6 +33,7 @@
 loggedUsers = []
 hostBlacklist = JSON.parse Modules.fs.readFileSync ResourceDir + "blacklist.json"
 getFileJSON = (fname) -> JSON.parse Modules.fs.readFileSync "#{ResourceDir}#{fname}.json"
+writeFileJSON = (json, fname) -> Modules.fs.writeFileSync "#{ResourceDir}#{fname}.json", JSON.stringify(json)
 GlobalDebug = yes

 # Declare object types using classes
@@ -53,18 +57,18 @@
 	toString: () -> "[User id=#{@id} powerLevel=#{@powerLevel}]"
 	onConnect: (client) ->
 		@connID = client.id
-		if not this in loggedUsers then loggedUsers.push this else KcCoreLogger.warn "user #{@id} onConnect called twice"
+		if not (this in loggedUsers) then loggedUsers.push(this) else KcCoreLogger.warn "user #{@id} onConnect called twice"
 	onDisconnect: () ->
 		@connID = null if @connID?
-		if this in loggedUsers then loggedUsers.pop this else KCCoreLogger.warn "user #{@id} onDisconnect called twice"
+		if this in loggedUsers then loggedUsers.pop this else KcCoreLogger.warn "user #{@id} onDisconnect called twice"
 	authenticate: (pwd) -> Modules.sha pwd is @auth

 	send: (event, msg) ->
-		throw Error "#{@toString()} Message undefined!" if not msg?
-		(SIOServer.to @connID).emit event, @cipher.encrypt msg
+		(SIOServer.to @connID).emit event, @cipher.encrypt(if msg is '' then ' ' else JSON.stringify(msg))
+		KcCoreLogger.info "Sent encrypted message to #{@id}: #{event} -> #{msg}"

 	decipher: (msg) -> @cipher.decrypt msg
-	isConnected: -> @connID isnt null
+	isConnected: -> @connID isnt null and this in loggedUsers

 	toJSON: ->
 		id: @id
@@ -105,10 +109,11 @@
 	DIRECT_MESSAGE: 0
 	GROUP_MESSAGE: 1

-	constructor: (@type, @sourceId, @destId, @message, @read = no) ->
-		if not (@type in [@DIRECT_MESSAGE..@GROUP_MESSAGE])
-			throw Error "Unknown message type: #{@type}"
-		@id = (String.fromCharCode(Math.floor(Math.random() * \
+	constructor: (type, @sourceId, @destId, @message, @read = no, @id) ->
+		if not (type in [@DIRECT_MESSAGE..@GROUP_MESSAGE])
+			throw Error "Unknown message type: #{type}"
+		@type = type
+		@id ?= (String.fromCharCode(Math.floor(Math.random() * \
 			(Options.phRand[1] - Options.phRand[0])) + Options.phRand[0]) for _ in [0..13]).join ''

 	toJSON: () ->
@@ -117,6 +122,9 @@
 		destId: @destId
 		message: @message
 		read: @read
+		id: @id
+
+	toString: -> JSON.stringify @toJSON()

 getMessageById = (id) ->
 	return msg for msg in messageRegistry when msg.id is id
@@ -128,10 +136,9 @@
 		green: `'\033[0;32m'`
 		yellow: `'\033[0;33m'`
 		blue: `'\033[0;34m'`
-
 	constructor: (@name, @includeTimeStamp=yes) -> @levels = []
 	log: (message, prefix='', suffix='', type='log') ->
-		console.log "#{prefix}#{if @includeTimeStamp then ("[" + new Date().toTimeString().substr 0,8 + "]") else ''}#{@name} #{type} -> #{level+" -> " for level in @levels}#{message}#{suffix}"
+		console.log "#{prefix}#{if @includeTimeStamp then ("[" + new Date().toTimeString().substr(0,8) + "] ") else ''}#{@name} #{type} -> #{level+" -> " for level in @levels}#{message}#{suffix}"
 	error: (message) -> @log message, @Colors.red, @Colors.reset, 'ERROR'
 	warn: (message) -> @log message, @Colors.yellow, @Colors.reset, 'WARN'
 	info: (message) -> @log message, @Colors.blue, @Colors.reset, 'info'
@@ -141,39 +148,50 @@

 User.fromJSON = (jsonExpression) -> new User(jsonExpression.id, jsonExpression.powerLevel, jsonExpression.auth)
 Group.fromJSON = (jsonExpression) -> new Group(jsonExpression.id, getUserById userExp for userExp in jsonExpression.members, jsonExpression.passphrase)
-Message.fromJSON = (je) -> new Message je.type, je.sourceId, je.destId, je.message, je.read
+Message.fromJSON = (je) -> new Message je.type, je.sourceId, je.destId, je.message, je.read, je.id

 # Set up variables and functions that use the defined classes
 userRegistry.push User.fromJSON json for json in getFileJSON 'users'
 groupRegistry.push Group.fromJSON json for json in getFileJSON 'groups'
 messageRegistry.push Message.fromJSON json for json in getFileJSON 'messages'

+onUserModification = -> writeFileJSON (u.toJSON() for u in userRegistry), 'users'
+onGroupModification = -> writeFileJSON (g.toJSON() for g in groupRegistry), 'groups'
+onMessageModification = -> writeFileJSON (m.toJSON() for m in messageRegistry), 'messages'
+
 KcCoreLogger = new Logger "KentChat"
 KcMessageLogger = new Logger "MessageLogger"
+TerminalColors = KcCoreLogger.Colors

 containsUser = (id) -> (getUserById id)?
 containsGroup = (id) -> (getGroupById id)?
 messagesFrom = (id) -> (msg for msg in messageRegistry when msg.sourceId is id)
-messagesToUser = (id) -> (msg for msg in messageRegistry when msg.type is Message.DIRECT_MESSAGE and msg.destId is id).concat \
-	(msg for msg in messageRegistry when msg.type is Message.GROUP_MESSAGE and id in (getGroupById msg.destId).members)
+messagesToUser = (id) -> (msg for msg in messageRegistry when msg.type is 0 and msg.destId is id).concat \
+	(msg for msg in messageRegistry when msg.type is 1 and id in (getGroupById msg.destId).members)
 groupIDs = () -> (group.id for group in groupRegistry)
-userIDs = () -> (user.id for group in groupRegistry)
-connectionIsLoggedIn = (conn) -> if user.connID is conn.id for user in userRegistry then yes else no
+userIDs = () -> (user.id for user in userRegistry)
+connectionIsLoggedIn = (conn) ->
+	for user in loggedUsers
+		return yes if user.connID is conn.id
+	return no
 getUserBySocketID = (socketID) ->
 	return user for user in userRegistry when user.connID is socketID
 User::unreadMessages = () -> (msg for msg in messagesToUser @id when not msg.read)
 User::sentMessages = () -> messagesFrom @id
-User::onRead = (message) -> (message.read = yes) if message.type is Message.DIRECT_MESSAGE and message.destId is @id and not message.read
+User::onRead = (message) -> (message.read = yes) if message.type is 0 and message.destId is @id and not message.read
 User::sendMessage = (destId, message, type) ->
 	msg = new Message type, @id, destId, message
 	messageRegistry.push msg
-	@send {type: "newMessage", id: msg.id}
-Group::getMessages = () -> (msg for msg in messageRegistry when msg.type is Message.GROUP_MESSAGE and msg.destId is @id)
+	onMessageModification()
+	@send 'data', {type: "newMessage", id: msg.id}
+Group::getMessages = () -> (msg for msg in messageRegistry when msg.type is 1 and msg.destId is @id)

 # Event handler common functions
 fail = (reason) -> ['failure', reason]
 succeed = (info) -> if info? then ['success', info] else ['success']
-data = (raw) -> ['data', (if raw instanceof Array then raw.join ', ' else raw)]
+data = (raw) ->
+	output = if typeof raw is "object" then JSON.stringify raw else raw
+	return ['data', output]
 # Event handler modules
 eventHandlers = [
 	{
@@ -182,7 +200,7 @@
 		expectedArgs: ["dest", "message", "toUser"]
 		onMessage: (msg, source) ->
 			(return fail "target user or group does not exist") if (msg.toUser and not containsUser msg.dest) or (not msg.toUser and not containsGroup msg.dest)
-			source.sendMessage msg.dest, msg.message, if msg.toUser then Message.DIRECT_MESSAGE else Message.GROUP_MESSAGE
+			source.sendMessage(msg.dest, msg.message, (if msg.toUser then 0 else 1))
 			KcMessageLogger.log "message sent from #{source.toString()} to #{msg.dest}: #{msg.message}"
 			return succeed()
 	}
@@ -223,6 +241,7 @@
 				return fail 'this group already exists'
 			else
 				groupRegistry.push new Group msg.id, [source.id]
+				onGroupModification()
 				return succeed "group #{msg.id} created"
 	}
 	{
@@ -238,7 +257,7 @@
 		onMessage: (msg, source) ->
 			(return fail "group #{msg.group} does not exist") if not msg.group in groupIDs()
 			source.sendMessage msg.target, "I invite you to join the group named #{msg.group}. \
-				Use \"kc.join('#{msg.group}')\" to join. (generated by KentChat)", Message.DIRECT_MESSAGE
+				Use \"kc.join('#{msg.group}')\" to join. (generated by KentChat)", 0
 			return succeed "invitation sent"
 	}
 	{
@@ -257,8 +276,9 @@
 		expectedPerm: UserPowerLevels.admin
 		expectedArgs: ['id', 'auth', 'powerLevel']
 		onMessage: (msg, source) ->
-			(return fail "#{msg.id} exists") if containsUser source.id
+			(return fail "#{msg.id} exists") if containsUser msg.id
 			userRegistry.push new User msg.id, msg.powerLevel, Modules.sha msg.auth
+			onUserModification()
 			return succeed "#{msg.id} created"
 	}
 ]
@@ -267,7 +287,7 @@
 # Login procedure
 SIOServer.on 'connection', (socket) ->
 	socket.on 'login', (data) ->
-		if not data.id? or not data.auth?
+		if not data or not data.id? or not data.auth?
 			socket.emit 'failure', 'expected data.id and data.auth, missing'
 			return
 		if connectionIsLoggedIn socket
@@ -284,27 +304,104 @@
 			socket.emit 'failure', 'bad password'
 			return
 		user.onConnect socket
-		socket.emit 'success', {}
+		socket.emit 'success', 'logged in'
+		KcCoreLogger.info "New login from #{user.id}: #{socket.id}"
 		for handler in eventHandlers
-			socket.on handler.expectedType, (data) ->
-				if not connectionIsLoggedIn socket
-					socket.emit 'failure', 'You are not logged in'
-					socket.disconnect()
-					return
-				source = getUserBySocketID socket.id
-				if not source.isQualifiedFor handler.expectedPerm
-					source.send 'failure', "You are not qualified to '#{handler.expectedType}', \
-						required power level is #{handler.expectedPerm}, you have #{source.powerLevel}"
-					return
-				if not data[arg]? for arg in handler.expectedArgs
-					source.send 'failure', "Missing arguments; expected #{handler.expectedArgs.join ', '}"
-					return
-				result = handler.onMessage data, source
-				if not (result instanceof Array) or result.length isnt 2
-					KcCoreLogger.error "Unexpected result from #{handler.expectedType} eventHandler: #{result}, expected 2-element array"
-					source.send 'failure', 'Server internal error, please report to the owners'
-					return
-				source.send result[0], result[1]
+			((hand) ->
+				socket.on hand.expectedType, (data) ->
+					if not connectionIsLoggedIn socket
+						socket.emit 'failure', 'You are not logged in'
+						socket.disconnect()
+						return
+					source = getUserBySocketID socket.id
+					if not data?
+						socket.emit 'failure', 'Empty data'
+						return
+					if not source.isQualifiedFor hand.expectedPerm
+						source.send 'failure', "You are not qualified to '#{hand.expectedType}', \
+							required power level is #{hand.expectedPerm}, you have #{source.powerLevel}"
+						return
+					for arg in hand.expectedArgs
+						if not (data[arg]?)
+							source.send 'failure', "Missing argument(s); expected #{hand.expectedArgs.join ', '}"
+							return
+					result = hand.onMessage data, source
+					if not (result instanceof Array)
+						KcCoreLogger.error "Unexpected result from #{hand.expectedType} eventHandler: #{result}, expected array"
+						source.send 'failure', 'Server internal error, please report to the owners'
+						return
+					source.send (if not (typeof result[0] is 'string') then JSON.stringify(result[0]) else result[0]), result[1]
+			)(handler)
 	socket.on 'disconnect', ->
 		source = getUserBySocketID socket.id
 		source.onDisconnect() if source?
+WebServer.listen 7476
+
+# Command interation
+proceedCommands = true
+commands =
+	"New-User":
+		args: "username,password,powerLevel".split ','
+		onCommand: (args) ->
+			# args: username, password, powerlevel
+			return "#{args[0]} exists" if containsUser args[0]
+			userRegistry.push new User args[0], parseInt(args[2]), Modules.sha args[1]
+			onUserModification()
+			return "#{args[0]} user created"
+	"New-Group":
+		args: "groupID,ownerID,members (optional),passphrase (optional)".split ','
+		minimumArgLength: 2
+		onCommand: (args) ->
+			# args: groupID, ownerID, (members), (passphrase)
+			return "#{args[0]} exists" if containsGroup args[0]
+			return "#{args[1]} does not exist" if not containsUser args[1]
+			groupRegistry.push new Group args[0], args[1], args[2], args[3]
+			onGroupModification()
+			return "#{args[0]} group created"
+	"List-Users":
+		args: []
+		onCommand: ->
+			return (user.id for user in userRegistry).join ", "
+	"List-LoggedUsers":
+		args: []
+		onCommand: ->
+			return (user.id for user in loggedUsers).join ", "
+	"List-Message":
+		args: []
+		onCommand: ->
+			return (msg.toString() for msg in messageRegistry).join ", "
+	"Exit":
+		args: []
+		onCommand: ->
+			sock.disconnect true for sock in SIOServer.sockets.sockets
+			SIOServer.close()
+			WebServer.close()
+			Stdin.close()
+			KcCoreLogger.info "Socket.io, WebServer and readline closed"
+			onGroupModification()
+			onUserModification()
+			onMessageModification()
+			KcCoreLogger.info "JSON data flushed, goodbye world"
+			proceedCommands = false
+
+CommandHandler = (input) ->
+	process.stdout.write TerminalColors.reset
+	args = input.replace(/\\\s/g, String.fromCharCode 180).split(' ').map (item) -> item.replace new RegExp(String.fromCharCode(180), 'g'), ' '
+	if args[0] is 'Help'
+		console.log "Available commands: #{Object.keys(commands).join ", "}"
+	else
+		commandPresent = input is ''
+		for label, cmd of commands
+			if args[0] is label
+				mal = if cmd.minimumArgLength? then cmd.minimumArgLength else cmd.args.length
+				if args.length <= mal
+					console.log TerminalColors.red + "Error: #{label} command requires #{mal} arguments, #{args.length-1} present" + TerminalColors.reset
+					commandPresent = yes
+					break
+				console.log cmd.onCommand args.slice(1)
+				commandPresent = yes
+				break
+		(console.log TerminalColors.red + "#{args[0]}: unknown command" + TerminalColors.reset) if not commandPresent
+	if proceedCommands then Stdin.question `'\033[0;35m-> \033[0;36m'`, CommandHandler
+
+Stdin.question `'\033[0;35m-> \033[0;36m'`, CommandHandler


upgrade and optimize the transfer toolkit

--- library/membrane/transfer.coffee
+++ library/membrane/transfer.coffee
@@ -38,8 +38,9 @@
 http = require "http"
 util = require "util"

+{Barebones} = require "./skeleton"
 {BowerSupport} = require "./bower"
-{Barebones} = require "./skeleton"
+{remote, external} = require "./remote"
 {Extending} = require "../nucleus/extends"
 {Composition} = require "../nucleus/compose"
 {Archetype} = require "../nucleus/arche"
@@ -109,6 +110,30 @@
         return sources.call this if compilable or null
         throw new Error invalid # nothing valid found

+    # This external method will be automatically inlined by transfer
+    # toolkit into the visual context. It is intended to equate the
+    # environment for logging information and debug messages on the
+    # server and the client sites to be as similar as possible. Do
+    # refer to the method implementation for more on-topic infos.
+    mimicLoggingFacade: external ->
+        return unless _.isObject Console or undefined
+        return unless _.isObject logger or log or null
+        d = ["debug", "error", "info", "warn", "trace"]
+        c = ["blue", "cyan", "green", "magenta", "red"]
+        c.push "white", "black", "grey", "gray", "yellow"
+        gen = (ll) -> -> log[ll] _.sprintf arguments...
+        assert logger[level] = gen level for level in d
+        logger.debug = logger.info # otherwise, no colors
+        return if _.isObject try Console.traces or null
+        colors = ([col, "color: #{col}"] for col in c)
+        assert b = "font-weight: bold" # CSS for bold
+        assert u = "text-decoration: underline" # under
+        Console.attach() # attach the external console
+        Console.styles.attach() # attach empty styling
+        Console.styles.register _.object colors # set
+        Console.styles.register underline: u, bold: b
+        Console.traces = this # mark the deal done
+
     # This server side method is called on the context prior to the
     # context being compiled and flushed down to the client site. The
     # method is wired in an asynchronous way for greater functionality.
@@ -119,14 +144,15 @@
         assert _.isObject(context or 0), "not context"
         assert _.isObject(request or 0), "not request"
         assert _.isString(symbol or null), "not symbol"
-        assert context.inline -> `assert = chai.assert`
-        assert context.inline -> `assert(logger = log)`
-        assert context.inline -> `assert($logger = log)`
-        assert context.inline -> try logger.enableAll()
-        (context.inline -> logger.disableAll()) if quiet
-        context.inline -> try _.mixin _.string.exports()
-        context.inline -> assert $(document).ready =>
-            try this.emit "document", document, this
+        if context.isRoot and context.toplevel then do =>
+            context.inline -> try _.mixin _.string.exports()
+            context.inline -> `assert = chai.assert || null`
+            context.inline -> `logger = Object.create(log)`
+            (context.inline -> logger.disableAll()) if quiet
+            assert context.inline (this.mimicLoggingFacade)
+            assert context.inline -> try logger.enableAll()
+        assert inl = (try context.inline.bind context)
+        inl -> $(document).ready => this.emit "document"
         assert remotes = this.constructor.remotes or []
         assert uniques = _.unique remotes or new Array()
         @inject context, rem, null for rem in uniques


use localization in the platings

use localization in the platings

--- library/semantic/platings.coffee
+++ library/semantic/platings.coffee
@@ -59,12 +59,12 @@
     # submits it to the backend and reacts to the response it got.
     confirmedFormsSubmission: @awaiting "positive", ->
         try this.forms.container.addClass "loading"
-        @ewrong ?= "Please check the entered information"
+        wrong = @t "Please check the information entered"
         assert _.isObject data = try @forms.download yes
         this.dataSubmission data, (success, values) =>
             this.forms.container.removeClass "loading"
             assert values and _.isObject values or null
-            @forms.upload values; @forms.messages @ewrong
+            @forms.upload values; @forms.messages wrong
             return undefined unless success and values
             @paragraph = $ "<p>", class: "right aligned"
             iconical = "icon checkmark green massive ok"


Fix minor typos in the docs

Fix minor typos in the docs

--- library/visualize/physics.coffee
+++ library/visualize/physics.coffee
@@ -43,9 +43,9 @@
 extendz = require "./../nucleus/extends"
 {Element, Content, Attribute} = trees

-# Pusher is the continuation of the `Monitor` class implementation.
+# Physics is the continuation of the `Monitor` class implementation.
 # It takes care of one specific class of event and data passing, the
 # passing of the structural events and data. That is, when a tree
-# changes it is the pusher who delivers the structural changes info
+# changes it is the physics who delivers the structural changes info
 # to the remote site, utilizing the transport channel of the monitor.
 module.exports.Physics = class Physics extends monitor.Monitor