keepalive test

keepalive test

--- src/qqbot.coffee
+++ src/qqbot.coffee
@@ -17,317 +17,320 @@
    plugins: 插件
 ###
 class QQBot
-    constructor: (@cookies, @auth, @config) ->
-      # PROTOCOL `用户分组信息格式`
-      @buddy_info = {}
-      # PROTOCOL `群分组信息格式`
-      @group_info = {}
-      # PROTOCOL `群用户信息`
-      @groupmember_info = {}
-
-      # discuss group
-      @dgroup_info = {}
-      @dgroupmember_info = {}
-
-      api.cookies @cookies
-      @api = api
-      @dispatcher = new Dispatcher(@config.plugins,@)
-
-
-    # @format PROTOCOL `群用户信息`
-    save_group_member: (group,info)->
-      @groupmember_info[group.gid] = info
+  constructor: (@cookies, @auth, @config) ->
+    # PROTOCOL `用户分组信息格式`
+    @buddy_info = {}
+    # PROTOCOL `群分组信息格式`
+    @group_info = {}
+    # PROTOCOL `群用户信息`
+    @groupmember_info = {}

-    # 获取用户信息
-    # @return {nick,uin,flag,face}
-    get_user: (uin) ->
-      users = @buddy_info.info.filter (item)-> item.uin == uin
+    # discuss group
+    @dgroup_info = {}
+    @dgroupmember_info = {}
+
+    api.cookies @cookies
+    @api = api
+    @dispatcher = new Dispatcher(@config.plugins,@)
+
+
+  # @format PROTOCOL `群用户信息`
+  save_group_member: (group,info)->
+    @groupmember_info[group.gid] = info
+
+  # 获取用户信息
+  # @return {nick,uin,flag,face}
+  get_user: (uin) ->
+    users = @buddy_info.info.filter (item)-> item.uin == uin
+    users.pop()
+
+  # 获取群用户信息
+  get_user_ingroup: (uin, gid)->
+    info = @groupmember_info[gid]
+    users = info.minfo.filter (item)-> item.uin == uin
+    users.pop()
+
+
+  # 获取群信息，只支持群 ，支持多关键词搜索
+  # @options {key:value}
+  # @return {gid,code,name,flag}
+  get_group: (options)->
+      groups = @group_info.gnamelist.filter (item)->
+          for key ,value of options
+              return item[key] == value
+      groups.pop()
+
+  # @options {key:value}
+  # @return {name, did}
+  get_dgroup: (options)->
+    try
+      groups = @dgroup_info.dnamelist.filter (item)->
+        for key,value of options
+          return item[key] == value
+      groups.pop()
+
+
+  get_user_in_dgroup: (uin, did)->
+    try
+      info = @dgroupmember_info[did]
+      users = (user for user in info.mem_info when user.uin == uin)
       users.pop()
-
-    # 获取群用户信息
-    get_user_ingroup: (uin, gid)->
-      info = @groupmember_info[gid]
-      users = info.minfo.filter (item)-> item.uin == uin
-      users.pop()
-
-
-    # 获取群信息，只支持群 ，支持多关键词搜索
-    # @options {key:value}
-    # @return {gid,code,name,flag}
-    get_group: (options)->
-        groups = @group_info.gnamelist.filter (item)->
-            for key ,value of options
-                return item[key] == value
-        groups.pop()
-
-    # @options {key:value}
-    # @return {name, did}
-    get_dgroup: (options)->
-      try
-        groups = @dgroup_info.dnamelist.filter (item)->
-          for key,value of options
-            return item[key] == value
-        groups.pop()
-
-
-    get_user_in_dgroup: (uin, did)->
-      try
-        info = @dgroupmember_info[did]
-        users = (user for user in info.mem_info when user.uin == uin)
-        users.pop()
+
+
+  # 获取群列表
+  # @callback {ret:bool,error}
+  update_group_list: (callback)->
+    @api.get_group_list @auth, (ret , e)=>
+        log.error e  if e
+        @group_info = ret.result if ret.retcode == 0
+        callback( ret.retcode == 0, e || 'retcode isnot 0' ) if callback
+
+  # 获取好友列表
+  # @callback (ret:bool, error)
+  update_buddy_list: (callback)->
+      @api.get_buddy_list @auth , (ret,e)=>
+          @buddy_info = ret.result if ret.retcode == 0
+          callback( ret.retcode == 0, e || 'retcode isnot 0' ) if callback
+
+  # 更新群成员
+  # @options {key:value} or group obj
+  # @callback (ret:bool , error)
+  update_group_member: (options, callback)->
+      group = if options.code then options else @get_group(options)
+      @api.get_group_member group.code , @auth , (ret,e)=>
+          if ret.retcode == 0
+            @save_group_member(group,ret.result)
+          callback(ret.retcode == 0 , e) if callback
+
+  update_dgroup_list: (callback)->
+    log.info "update discuss group list"
+    @api.get_discuss_list @auth, (ret,e)=>
+      log.error e if e
+      @dgroup_info = ret.result if ret.retcode == 0
+      # log.info jsons @dgroup_info
+      callback(ret.retcode == 0, e||'retcode isnot 0') if callback
+
+  update_dgroup_member: (dgroup,callback)->
+    log.info "update discuss group member #{dgroup.did}"
+    did = dgroup.did
+    @api.get_discuss_member did, @auth, (ret,e)=>
+      if ret.retcode == 0
+        @dgroupmember_info[did] = ret.result
+        # log.info jsons ret.result
+      callback(ret.retcode == 0 , e) if callback
+
+
+  # 更新所有群成员
+  # @callback (success,total_count,success_count)
+  update_all_group_member: (callback)->
+    finished = successed = 0
+    groups = @group_info.gnamelist || []
+    all = groups.length
+    for group in groups
+      @update_group_member group , (ret,error)->
+        finished += 1; successed += ret
+        log.debug "groupmember all#{all} fin#{finished} succ#{successed}"
+        callback(successed == all, finished ,successed) if finished == all
+
+  # 更新好友和群成员
+  # @callback (ret?,actions) 是否全部更新成功 , actions每个状态
+  update_all_members: (callback)->
+    # [0,0] -> [finished,success]
+    actions= buddy:[0,0],group:[0,0],groupmember:[0,0]
+    check = ->
+      finished = successed = 0 ; all = Object.keys(actions).length
+      stats = (value for key,value of actions)
+      for item in stats
+        finished += item[0] ; successed += item[1]
+      log.debug("updating all: all #{all} finished #{finished} success #{successed}")
+      callback(successed==all) if finished == all
+
+    # fetching...
+    log.info 'fetching buddy list...'
+    @update_buddy_list (ret)->
+      actions.buddy = [1, ret]
+      check()
+
+    log.info 'fetching group list...'
+    @update_group_list (ret)=>
+      actions.group = [1, ret]
+      unless ret
+        callback(false)
+        return
+
+      log.info 'fetching all groupmember...'
+      @update_all_group_member (ret,all,successed)->
+        actions.groupmember = [1,ret]
+        check()
+
+    log.info 'fetching discuss group list'
+    @update_dgroup_list()
+
+
+  # 长轮询
+  # @callback
+  runloop: (callback)->
+    @api.long_poll @auth , (ret,e)=>
+        @handle_poll_responce ret,e
+        callback(ret,e) if callback
+
+  # 回复消息
+  # @param message 收到的message
+  # @param content:string 回复信息
+  # @callback ret, error
+  reply_message: (message, content, callback)->
+      log.info "发送消息：",content
+      switch message.type
+        when MsgType.Group
+          @api.send_msg_2group  message.from_gid , content , @auth, callback
+        when MsgType.Default
+          @api.send_msg_2buddy message.from_uin , content , @auth , callback
+        when MsgType.Discuss
+          @api.send_msg_2discuss message.from_did, content, @auth, callback
+
+  # 发送群消息
+  # @param gid_or_group
+  # @callback (ret,e)
+  send_message_to_group: (gid_or_group, content, callback)->
+    gid = if typeof gid_or_group is 'object' then gid_or_group.gid else gid_or_group
+    log.info "send msg #{content} to group#{gid}"
+    api.send_msg_2group  gid , content , @auth, callback
+
+  send_message_to_discuss: (did, content, callback)->
+    log.info "send msg #{content} to discuss#{did}"
+    api.send_msg_2discuss did, content, @auth, callback
+
+  # 自杀
+  die: (message,info)->
+    #TODO: 这里 log.error 似乎看不到日志输出，试试console
+      console.log "QQBot die! message: #{message}" if message
+      console.log "QQBot die! info #{JSON.stringify info}" if info
+      process.exit(1)
+
+  # 处理poll返回的内容
+  handle_poll_responce: (resp,e)->
+    log.error "poll with error #{e}" if e
+    code = if resp then resp.retcode else -1
+    switch code
+      when -1  then log.error "resp is null, error on parse ret"
+      when 0   then @_handle_poll_event(event) for event in resp.result
+      when 102 then 'nothing happened, waiting for next loop'
+      when 103
+        log.error "登录异常 #{code}", resp, 'token失效，但是偶尔也有情况返回'
+        @relogin
+      when 116 then @_update_ptwebqq(resp)
+      when 121 then @die("登录异常 #{code}",resp)
+      else log.debug resp
+
+  ###
+  重新登录获取token
+  @callback success:bool
+  ###
+  relogin: (callback)->
+    log.info "relogin..."
+    auth.cookies @cookies
+
+    auth.login_token @auth.clientid, @auth.psessionid, (ret,client_id,ptwebqq) =>
+      if ret.retcode != 0
+        log.error "relogin failed"
+        log.info ret
+        callback(false) if callback
+        return
+
+      log.debug 'before',@auth
+      auth_new =
+        psessionid: ret.result.psessionid
+        clientid  : client_id
+        ptwebqq   : ptwebqq
+        uin       : ret.result.uin
+        vfwebqq   : ret.result.vfwebqq

+      @auth = auth_new
+      log.debug 'after',@auth
+      callback(true) if callback
+
+
+  # 更新token ptwebqq的值，返回值{116 ,p=token}
+  _update_ptwebqq: (ret)->
+    log.debug 'need to update ptwebqq ',ret
+    @auth['ptwebqq'] = ret.p
+
+  _handle_poll_event : (event) ->
+    switch event.poll_type
+      when MsgType.Default, MsgType.Group, MsgType.Discuss
+        @_on_message(event, event.poll_type)
+      when 'input_notify'  then ""
+      when 'buddies_status_change' then ""
+      else log.warning "unimplemented event",event.poll_type , "content: ", jsons event
+
+  _on_message : (event, msg_type)->
+    value = event.value
+    msg =
+      content : value.content.slice(-1).pop().trim()
+      time    : new Date(value.time * 1000)
+      from_uin: value.from_uin
+      type    : msg_type
+      uid     : value.msg_id
+
+    if msg_type == MsgType.Group
+      msg.from_gid = msg.from_uin
+      msg.group_code = value.group_code
+      msg.from_uin = value.send_uin # 这才是用户,group消息中 from_uin 是gid
+      msg.from_group = @get_group( {gid:msg.from_gid} )
+      msg.from_user  = @get_user_ingroup( msg.from_uin ,msg.from_gid )
+      # 更新
+      @update_group_list unless msg.from_group
+      @update_group_member {gid:msg.from_gid} unless msg.from_user
+
+      msg.from_group ?= {}
+      msg.from_user  ?= {}
+      try log.debug "[群组消息]","[#{msg.from_group.name}] #{msg.from_user.nick}:#{msg.content} #{msg.time}"
+    else if msg_type == MsgType.Discuss
+      msg.from_did = value.did
+      msg.from_uin = value.send_uin
+      msg.from_dgroup = @get_dgroup({did:value.did})
+      msg.from_user = @get_user_in_dgroup(msg.from_uin,msg.from_did)
+
+      # 更新
+      @update_dgroup_list() unless msg.from_dgroup
+      @update_dgroup_member {did:value.did} unless msg.from_user
+      msg.from_dgroup ?= {}
+      msg.from_user  ?= {}
+
+      try log.debug "[讨论组消息]","[#{msg.from_dgroup.name}] #{msg.from_user.nick}:#{msg.content} #{msg.time}"
+    else if msg_type == MsgType.Default
+      msg.from_user = @get_user( msg.from_uin )
+      #  更新
+      @update_buddy_list unless msg.from_user
+      try log.debug "[好友消息]","#{msg.from_user.nick}:#{msg.content} #{msg.time}"

-    # 获取群列表
-    # @callback {ret:bool,error}
-    update_group_list: (callback)->
-      @api.get_group_list @auth, (ret , e)=>
-          log.error e  if e
-          @group_info = ret.result if ret.retcode == 0
-          callback( ret.retcode == 0, e || 'retcode isnot 0' ) if callback
-
-    # 获取好友列表
-    # @callback (ret:bool, error)
-    update_buddy_list: (callback)->
-        @api.get_buddy_list @auth , (ret,e)=>
-            @buddy_info = ret.result if ret.retcode == 0
-            callback( ret.retcode == 0, e || 'retcode isnot 0' ) if callback
-
-    # 更新群成员
-    # @options {key:value} or group obj
-    # @callback (ret:bool , error)
-    update_group_member: (options, callback)->
-        group = if options.code then options else @get_group(options)
-        @api.get_group_member group.code , @auth , (ret,e)=>
-            if ret.retcode == 0
-              @save_group_member(group,ret.result)
-            callback(ret.retcode == 0 , e) if callback
-
-    update_dgroup_list: (callback)->
-      log.info "update discuss group list"
-      @api.get_discuss_list @auth, (ret,e)=>
-        log.error e if e
-        @dgroup_info = ret.result if ret.retcode == 0
-        # log.info jsons @dgroup_info
-        callback(ret.retcode == 0, e||'retcode isnot 0') if callback
-
-    update_dgroup_member: (dgroup,callback)->
-      log.info "update discuss group member #{dgroup.did}"
-      did = dgroup.did
-      @api.get_discuss_member did, @auth, (ret,e)=>
-        if ret.retcode == 0
-          @dgroupmember_info[did] = ret.result
-          # log.info jsons ret.result
-        callback(ret.retcode == 0 , e) if callback
-
-
-    # 更新所有群成员
-    # @callback (success,total_count,success_count)
-    update_all_group_member: (callback)->
-      finished = successed = 0
-      groups = @group_info.gnamelist || []
-      all = groups.length
-      for group in groups
-        @update_group_member group , (ret,error)->
-          finished += 1; successed += ret
-          log.debug "groupmember all#{all} fin#{finished} succ#{successed}"
-          callback(successed == all, finished ,successed) if finished == all
-
-    # 更新好友和群成员
-    # @callback (ret?,actions) 是否全部更新成功 , actions每个状态
-    update_all_members: (callback)->
-      # [0,0] -> [finished,success]
-      actions= buddy:[0,0],group:[0,0],groupmember:[0,0]
-      check = ->
-        finished = successed = 0 ; all = Object.keys(actions).length
-        stats = (value for key,value of actions)
-        for item in stats
-          finished += item[0] ; successed += item[1]
-        log.debug("updating all: all #{all} finished #{finished} success #{successed}")
-        callback(successed==all) if finished == all
-
-      # fetching...
-      log.info 'fetching buddy list...'
-      @update_buddy_list (ret)->
-        actions.buddy = [1, ret]
-        check()
-
-      log.info 'fetching group list...'
-      @update_group_list (ret)=>
-        actions.group = [1, ret]
-        unless ret
-          callback(false)
-          return
-
-        log.info 'fetching all groupmember...'
-        @update_all_group_member (ret,all,successed)->
-          actions.groupmember = [1,ret]
-          check()
-
-      log.info 'fetching discuss group list'
-      @update_dgroup_list()
-
-
-    # 长轮询
-    # @callback
-    runloop: (callback)->
-      @api.long_poll @auth , (ret,e)=>
-          @handle_poll_responce ret,e
-          callback(ret,e) if callback
-
-    # 回复消息
-    # @param message 收到的message
-    # @param content:string 回复信息
-    # @callback ret, error
-    reply_message: (message, content, callback)->
-        log.info "发送消息：",content
-        switch message.type
-          when MsgType.Group
-            @api.send_msg_2group  message.from_gid , content , @auth, callback
-          when MsgType.Default
-            @api.send_msg_2buddy message.from_uin , content , @auth , callback
-          when MsgType.Discuss
-            @api.send_msg_2discuss message.from_did, content, @auth, callback
-
-    # 发送群消息
-    # @param gid_or_group
-    # @callback (ret,e)
-    send_message_to_group: (gid_or_group, content, callback)->
-      gid = if typeof gid_or_group is 'object' then gid_or_group.gid else gid_or_group
-      log.info "send msg #{content} to group#{gid}"
-      api.send_msg_2group  gid , content , @auth, callback
-
-    send_message_to_discuss: (did, content, callback)->
-      log.info "send msg #{content} to discuss#{did}"
-      api.send_msg_2discuss did, content, @auth, callback
-
-    # 自杀
-    die: (message,info)->
-      #TODO: 这里 log.error 似乎看不到日志输出，试试console
-        console.log "QQBot die! message: #{message}" if message
-        console.log "QQBot die! info #{JSON.stringify info}" if info
-        process.exit(1)
-
-    # 处理poll返回的内容
-    handle_poll_responce: (resp,e)->
-      log.error "poll with error #{e}" if e
-      code = if resp then resp.retcode else -1
-      switch code
-        when -1  then log.error "resp is null"
-        when 0   then @_handle_poll_event(event) for event in resp.result
-        when 102 then 'nothing happened, waiting for next loop'
-        when 103 then log.error "登录异常 #{code}", resp, 'token失效，但是偶尔也有情况返回'
-        when 116 then @_update_ptwebqq(resp)
-        when 121 then @die("登录异常 #{code}",resp)
-        else log.debug resp
-
-    ###
-    @callback success:bool
-    ###
-    relogin: (callback)->
-      log.info "relogin..."
-      auth.cookies @cookies
-      # (client_id=null,psessionid=null,callback) ->
-      auth.login_token @auth.clientid, @auth.psessionid, (ret,client_id,ptwebqq) ->
-        if ret.retcode != 0
-          log.error "relogin failed"
-          log.info ret
-          callback(false) if callback
-          return
-
-        log.debug 'before',@auth
-        auth_new =
-          psessionid: ret.result.psessionid
-          clientid  : client_id
-          ptwebqq   : ptwebqq
-          uin       : ret.result.uin
-          vfwebqq   : ret.result.vfwebqq
-
-        @auth = auth_new
-        log.debug 'after',@auth
-        callback(true) if callback
-
-
-    # 更新token ptwebqq的值，返回值{116 ,p=token}
-    _update_ptwebqq: (ret)->
-      log.info 'need to update ptwebqq ',ret
-      @auth['ptwebqq'] = ret.p
-
-    _handle_poll_event : (event) ->
-      switch event.poll_type
-        when MsgType.Default, MsgType.Group, MsgType.Discuss
-          @_on_message(event, event.poll_type)
-        when 'input_notify'  then ""
-        when 'buddies_status_change' then ""
-        else log.warning "unimplemented event",event.poll_type , "content: ", jsons event
-
-    _on_message : (event, msg_type)->
-      value = event.value
-      msg =
-          content : value.content.slice(-1).pop().trim()
-          time    : new Date(value.time * 1000)
-          from_uin: value.from_uin
-          type    : msg_type
-          uid     : value.msg_id
-
-      if msg_type == MsgType.Group
-        msg.from_gid = msg.from_uin
-        msg.group_code = value.group_code
-        msg.from_uin = value.send_uin # 这才是用户,group消息中 from_uin 是gid
-        msg.from_group = @get_group( {gid:msg.from_gid} )
-        msg.from_user  = @get_user_ingroup( msg.from_uin ,msg.from_gid )
-        # 更新
-        @update_group_list unless msg.from_group
-        @update_group_member {gid:msg.from_gid} unless msg.from_user
-
-        msg.from_group ?= {}
-        msg.from_user  ?= {}
-        try log.debug "[群组消息]","[#{msg.from_group.name}] #{msg.from_user.nick}:#{msg.content} #{msg.time}"
-      else if msg_type == MsgType.Discuss
-        msg.from_did = value.did
-        msg.from_uin = value.send_uin
-        msg.from_dgroup = @get_dgroup({did:value.did})
-        msg.from_user = @get_user_in_dgroup(msg.from_uin,msg.from_did)
-
-        # 更新
-        @update_dgroup_list() unless msg.from_dgroup
-        @update_dgroup_member {did:value.did} unless msg.from_user
-        msg.from_dgroup ?= {}
-        msg.from_user  ?= {}
-
-        try log.debug "[讨论组消息]","[#{msg.from_dgroup.name}] #{msg.from_user.nick}:#{msg.content} #{msg.time}"
-      else if msg_type == MsgType.Default
-        msg.from_user = @get_user( msg.from_uin )
-        #  更新
-        @update_buddy_list unless msg.from_user
-        try log.debug "[好友消息]","#{msg.from_user.nick}:#{msg.content} #{msg.time}"
-
-
-      # 消息处理
-      replied = false
-      reply = (content)=>
-          @reply_message(msg,content) unless replied
-          replied = true
-
-      @dispatcher.dispatch(msg.content ,reply, @ , msg)
-
-
-
-    # 监听特定群并返回群对象
-    # @callback (group对象, error = null)
-
-    listen_group : (name , callback) ->
-      log.info 'fetching group list'
-      @update_group_list (ret, e) =>
-        log.info '√ group list fetched'
-
-        log.info "fetching groupmember #{name}"
-        @update_group_member {name:name} ,(ret,error)=>
-          log.info '√ group memeber fetched'
-
-          groupinfo = @get_group {name:name}
-          group = new Group(@, groupinfo.gid)
-          @dispatcher.add_listener [group,"dispatch"]
-          callback group
+
+    # 消息处理
+    replied = false
+    reply = (content)=>
+        @reply_message(msg,content) unless replied
+        replied = true
+
+    @dispatcher.dispatch(msg.content ,reply, @ , msg)
+
+
+
+  # 监听特定群并返回群对象
+  # @callback (group对象, error = null)
+
+  listen_group : (name , callback) ->
+    log.info 'fetching group list'
+    @update_group_list (ret, e) =>
+      log.info '√ group list fetched'
+
+      log.info "fetching groupmember #{name}"
+      @update_group_member {name:name} ,(ret,error)=>
+        log.info '√ group memeber fetched'
+
+        groupinfo = @get_group {name:name}
+        group = new Group(@, groupinfo.gid)
+        @dispatcher.add_listener [group,"dispatch"]
+        callback group

 ###


Remove attr()

Remove attr()

--- src/quill.coffee
+++ src/quill.coffee
@@ -11,7 +11,6 @@
     theme: 'snow'
   $quill = @$('.quill')
   $quill.data('quill', @quill)
-  $quill.attr('data-schema-key', )
   value = @data.value
   @quill.setHTML(value) if value?


Update keep alive variables

Update keep alive variables

* Rename to HEROKU_URL (must include the http(s)://)
* Add trailing slash if missing

--- src/robot.coffee
+++ src/robot.coffee
@@ -193,7 +193,8 @@

     @connect.listen process.env.PORT || 8080

-    hostname = process.env.HEROKU_HOSTNAME
+    hostname = process.env.HEROKU_URL
+    hostname = "#{hostname}/" unless /\/$/.test hostname

     if hostname
       setInterval =>


Stop listening to uncaughtException events on shutdown

I noticed this when running tests for a Hubot instance that created a
lot of Robot instances. Eventually Node warned that there were more than
10 listeners for the uncaughtException event.

--- src/robot.coffee
+++ src/robot.coffee
@@ -63,9 +63,9 @@

     @on 'error', (err, msg) =>
       @invokeErrorHandlers(err, msg)
-    process.on 'uncaughtException', (err) =>
+    @onUncaughtException = (err) =>
       @emit 'error', err
-
+    process.on 'uncaughtException', @onUncaughtException

   # Public: Adds a Listener that attempts to match incoming messages based on
   # a Regex.
@@ -447,6 +447,7 @@
   # Returns nothing.
   shutdown: ->
     clearInterval @pingIntervalId if @pingIntervalId?
+    process.removeListener 'uncaughtException', @onUncaughtException
     @adapter.close()
     @brain.close()


Removes last vestige of url processing.

Removes last vestige of url processing.

--- src/slack.coffee
+++ src/slack.coffee
@@ -75,8 +75,8 @@

       if hubot_msg
         # Convert markup into plain url string.
-        hubot_msg = hubot_msg.replace(/<((\bhttps?)[^|]+)(\|(.*))+>/g, '< $1 >')
-        hubot_msg = hubot_msg.replace(/<((\bhttps?)(.*))?>/g, '< $1 >')
+        hubot_msg = hubot_msg.replace(/<((\bhttps?)[^|]+)(\|(.*))+>/g, '$1')
+        hubot_msg = hubot_msg.replace(/<((\bhttps?)(.*))?>/g, '$1')

         # Unescape
         hubot_msg = hubot_msg.replace(/&/g, '&')


Remove search bar focus on auto suggestion select

Closes #120.

--- src/views.coffee
+++ src/views.coffee
@@ -94,6 +94,8 @@
             search_el.on 'typeahead:opened', (ev) =>
                 app.commands.execute 'clearSearch'
         autosuggest_show_details: (ev, data, _) ->
+            # Remove focus from the search box to hide keyboards on touch devices.
+            $('.search-container input').blur()
             model = null
             object_type = data.object_type
             switch object_type


Implement preCompile

Implement preCompile

--- src/watch.coffee
+++ src/watch.coffee
@@ -1,6 +1,7 @@
 'use strict'

 each = require 'async-each'
+waterfall = require 'async-waterfall'
 chokidar = require 'chokidar'
 debug = require('debug')('brunch:watch')
 sysPath = require 'path'
@@ -260,7 +261,7 @@
 # startTime  - Number. Timestamp of a moment when compilation started.
 #
 # Returns Function.
-getCompileFn = (config, joinConfig, fileList, optimizers, watcher, callback) -> (startTime, watcherReady) ->
+getCompileFn = (config, joinConfig, fileList, optimizers, watcher, preCompile, callback) -> (startTime, watcherReady) ->
   assetErrors = fileList.getAssetErrors()
   if assetErrors?
     assetErrors.forEach (error) -> logger.error error
@@ -292,7 +293,14 @@

     fileList.initial = false

-  fs_utils.write fileList, config, joinConfig, optimizers, startTime, writeCb
+  if preCompile
+    preCompile (error) ->
+      if error?
+        logger.error error
+      else
+        fs_utils.write fileList, config, joinConfig, optimizers, startTime, writeCb
+  else
+    fs_utils.write fileList, config, joinConfig, optimizers, startTime, writeCb

 # Generate function that restarts brunch process.
 #
@@ -405,14 +413,14 @@

       # Does the user's config say this plugin should definitely be used?
       return true if plugin.brunchPluginName in alwaysEnabled
-
+
       # If the plugin is an optimizer that doesn't specify a defaultEnv
       # decide based on the config.optimize setting
       return config.optimize if plugin.optimize and not plugin.defaultEnv
-
+
       # Use plugin-specified defaultEnv or assume it's meant for any env
       env = plugin.defaultEnv ?= '*'
-
+
       # Finally, is it meant for either any environment or an active environment?
       env is '*' or env in config.env

@@ -422,6 +430,15 @@
     compilers  = plugins.filter propIsFunction 'compile'
     linters    = plugins.filter propIsFunction 'lint'
     optimizers = plugins.filter propIsFunction 'optimize'
+
+    # Get plugin preCompile callbacks
+    preCompilers = plugins.filter(propIsFunction 'preCompile').map((plugin) -> (args..., cb) -> plugin.preCompile(cb))
+
+    # Add preCompile callback from config
+    if typeof config.preCompile is 'function'
+      preCompilers.push (args..., cb) -> config.preCompile(cb)
+    callPreCompillers = (cb) ->
+      waterfall(preCompilers, cb)

     # Get plugin onCompile callbacks
     callbacks  = plugins.filter(propIsFunction 'onCompile').map((plugin) -> (args...) -> plugin.onCompile args...)
@@ -445,7 +462,7 @@
       initWatcher config, (error, watcher) ->
         return callback error if error?
         # Get compile and reload functions.
-        compile = getCompileFn config, joinConfig, fileList, optimizers, watcher, callCompileCallbacks
+        compile = getCompileFn config, joinConfig, fileList, optimizers, watcher, callPreCompillers, callCompileCallbacks
         reload = getReloadFn config, options, onCompile, watcher, server, plugins
         includes = getPluginIncludes(plugins)
         callback error, {


Call the callback when done

Call the callback when done

--- src/components/Graph.coffee
+++ src/components/Graph.coffee
@@ -80,11 +80,10 @@
       @on 'ready', =>
         @start callback
       return
-
+    super()
     return callback null unless @network
     @network.start (err) =>
       return callback err if err
-      super()
       do callback

   checkComponent: (name, process) ->
@@ -177,8 +176,9 @@
       callback = ->
     return callback() unless @started
     return callback null unless @network
-    @network.stop (err) ->
+    @network.stop (err) =>
       return callback err if err
       super()
+      do callback

 exports.getComponent = (metadata) -> new Graph metadata


Remove unused vertical (y) scale

Remove unused vertical (y) scale

--- src/d3.timeslider.coffee
+++ src/d3.timeslider.coffee
@@ -31,8 +31,6 @@
             x: d3.time.scale.utc()
                 .domain([ @options.start, @options.end ])
                 .range([0, @options.width])
-            y: d3.scale.linear()
-                .range([ 0, @options.height ])

         # axis
         @axis =


added events for current tabs

added events for current tabs

--- src/render/ui-control.coffee
+++ src/render/ui-control.coffee
@@ -13,13 +13,38 @@
 ipcRenderer = electron.ipcRenderer
 remote = electron.remote
 dialog = remote.require 'dialog'
+events =  require 'events'
+path = require 'path'
+fs =  require 'fs'
+
+BaseDir =  path.resolve(__dirname)

 $shell_tabs =  $('.chrome-tabs-shell')

+
+tabsEmitter =  new events.EventEmitter()
+
+
+tabListener = () ->
+    tabsEmitter.on 'tab-click', (data) ->
+        console.log data
+
+    tabsEmitter.on 'tab-close', (data) ->
+        console.log data
+
 # window first pops up
 initDisplay = () ->
-    chromeTabs.init({$shell: $shell_tabs, minWidth: 45, maxWidth: 160})
+    chromeTabs.init({$shell: $shell_tabs, tabEmitter: tabsEmitter, minWidth: 45, maxWidth: 160})
+
+    fs.readFile(BaseDir + '\\tabs\\welcome.html',{encoding:'utf-8'},(error, data) ->
+            if error
+                $('div.viewer').html(error)
+            else
+                $('div.viewer').html(data)
+
+         )
+
     chromeTabs.addNewTab($shell_tabs, {title:"Welcome"})

 Create_New_Case = () ->
@@ -39,12 +64,20 @@

     ipcRenderer.on 'actionReply', (event, treeView) ->
         console.log treeView
+        treelist = []
+        treelist.push treeView.tree
+        $('#tree').treeview({data: treelist})
+
+
 Analyse_Dropbox = () ->
-    consle.log 'anazing dropbox'
+    console.log 'anazing dropbox'
     ipcRenderer.send 'dropbox', ''

     ipcRenderer.on 'actionReply', (event, treeView) ->
         console.log treeView
+        treelist = []
+        treelist.push treeView.tree
+        $('#tree').treeview({data: treelist})

 Analyze_OneDrive = () ->
     console.log 'analying one drive'
@@ -59,3 +92,4 @@

 initDisplay()
+tabListener()


Throw error if grammar cannot be resolved for path

--- src/app/language-mode.coffee
+++ src/app/language-mode.coffee
@@ -55,6 +55,7 @@
       @grammar = @buffer.project.grammarForFilePath(path, pathContents)
     else
       @grammar = syntax.grammarForFilePath(path, pathContents)
+    throw new Error("No grammar found for path: #{path}") unless @grammar
     previousGrammar isnt @grammar

   isQuote: (string) ->


Fixes for set management

Fixes for set management

--- apps/personalize/client/views/favorites.coffee
+++ apps/personalize/client/views/favorites.coffee
@@ -76,16 +76,16 @@
     @disableArtworkLinks()
     @fadeInHearts()

-  # Disable links to the artworks and delegate to the favorite button
+  # Disable links to the artworks
   disableArtworkLinks: ->
     @$('.artwork-item-image-link')
       .attr('href', null)
-      .click -> $(this).find('.overlay-button-save').click()
+      .css('cursor', 'default')

   # Fades in a column of hearts at a time
   fadeInHearts: ->
     $buttonColumns = _.map @artworkColumnsView.$columns, (column) ->
-      $(column).find('.overlay-button-save').css 'opacity', 0
+      $(column).find('.overlay-container > *').css 'opacity', 0
     _.delay =>
       _.each $buttonColumns, (buttonColumn, i) ->
         _.delay (=> $(buttonColumn).css 'opacity', 1), (i + 1) * 50


Only flex vertically if prop is set

Only flex vertically if prop is set

--- client/lanes/components/shared/ScreenWrapper.cjsx
+++ client/lanes/components/shared/ScreenWrapper.cjsx
@@ -5,8 +5,8 @@

     render: ->
         classes = _.classnames(
-            'screen-wrapper',   @props.identifier,
-            'flex-vertically': !@props.noFlexLayout
+            'screen-wrapper',  @props.identifier,
+            'flex-vertically': @props.flexVertical
         )
         <div className={classes}>
             {@props.children}


Remove left over reveal.js view

Remove left over reveal.js view

--- client/src/views/section_presentation_view.coffee
+++ client/src/views/section_presentation_view.coffee
@@ -1,29 +0,0 @@
-window.Backbone ||= {}
-window.Backbone.Views ||= {}
-
-class Backbone.Views.SectionPresentationView extends Backbone.Diorama.NestingView
-  template: Handlebars.templates['section-presentation.hbs']
-  tagName: 'section'
-  className: 'section-view'
-
-  initialize: (options) ->
-    @section = options.section
-
-  render: =>
-    @closeSubViews()
-    @$el.html(@template(
-      thisView: @
-      section: @section.toJSON()
-      narrative: @section.get('narrative')
-      visualisation: @section.get('visualisation')
-    ))
-    @renderSubViews()
-
-    return @
-
-  addNarrative: =>
-    @section.set('narrative', new Backbone.Models.Narrative())
-    @render()
-
-  onClose: ->
-    @closeSubViews()


Streamline set-current

Streamline set-current

--- src/config/inspector.coffee
+++ src/config/inspector.coffee
@@ -58,13 +58,9 @@
     strategy.add_child display_program(child.label(), child.get('value'))
   strategy.put 'height', my.row.spacing*programs._child_count()

-set_current = (world, args) ->
-  current = world.get('current')
-  if args?
-    current = args
-    world.put('current', current)
-  my.assert current, "No current turtle"
-  current
+set_current = (world, current) ->
+  world.put('current', current) if current?
+  world.get('current')

 exports.inspector = {
   _LABEL: "inspector"


Fix prev commit.

Fix prev commit.

--- src/chaplin/lib/route.coffee
+++ src/chaplin/lib/route.coffee
@@ -31,7 +31,7 @@
         # Escape magic characters
         .replace(escapeRegExp, '\\$&')
         # Replace named parameters, collecting their names
-        .replace(/:([a-zA-Z0-9%\-]+)/g, @addParamName)
+        .replace(/:(\w+)/g, @addParamName)

       # Create the actual regular expression
       # Match until the end of the URL or the begin of query string
@@ -45,7 +45,7 @@
       # Save parameter name
       @paramNames.push paramName
       # Replace with a character class
-      '([\\w-]+)'
+      '([\\w.-%]+)'

     # Test if the route matches to a path (called by Backbone.History#loadUrl)
     test: (path) ->


buffer.file is not necessarily defined

buffer.file is not necessarily defined

--- lib/util.coffee
+++ lib/util.coffee
@@ -11,7 +11,7 @@
   getRootDir: (buffer) ->
     [dir] = atom.project.getDirectories().filter (dir) ->
       dir.contains(buffer.getUri())
-    dir ? atom.project.getDirectories()[0] ? buffer.file.getParent() ? new Directory
+    dir ? atom.project.getDirectories()[0] ? buffer.file?.getParent?() ? new Directory

   getProcessOptions: (rootPath) ->
     env = {}


Remove workspace opener on deactivate

Remove workspace opener on deactivate

--- lib/styleguide.coffee
+++ lib/styleguide.coffee
@@ -1,4 +1,5 @@
-styleguideUri = 'atom://styleguide'
+{CompositeDisposable} = require 'atom'
+StyleguideUri = 'atom://styleguide'

 createStyleguideView = (state) ->
   StyleguideView = require './styleguide-view'
@@ -10,11 +11,11 @@

 module.exports =
   activate: ->
-    atom.workspace.addOpener (filePath) ->
-      createStyleguideView(uri: styleguideUri) if filePath is styleguideUri
-
-    @disposable = atom.commands.add 'atom-workspace', 'styleguide:show', ->
-      atom.workspace.open(styleguideUri)
+    @subscriptions = new CompositeDisposable
+    @subscriptions.add atom.workspace.addOpener (filePath) ->
+      createStyleguideView(uri: StyleguideUri) if filePath is StyleguideUri
+    @subscriptions.add atom.commands.add 'atom-workspace', 'styleguide:show', ->
+      atom.workspace.open(StyleguideUri)

   deactivate: ->
-    @disposable.dispose()
+    @subscriptions.dispose()


Fix test task

Fix test task

--- lib/tasks/test.coffee
+++ lib/tasks/test.coffee
@@ -11,4 +11,4 @@

   options.singleRun = true unless gutil.env.dev

-  karma.start(options, done)
+  karma.start(options, -> done())


:art: Refactored clearBlankLineOnNewline

Editor.transact should not be called unless we are going to make
changes. Now checks for the situation where we need to make changes,
then calls the editor.transact. Updated to use event.range now incase we
have multiple cursors.

--- lib/whitespace.coffee
+++ lib/whitespace.coffee
@@ -32,10 +32,12 @@
           @ensureSingleTrailingNewline(editor)

     editorTextInsertedSubscription = editor.onDidInsertText (event) =>
-      editor.transact =>
-        scopeDescriptor = editor.getRootScopeDescriptor()
-        if atom.config.get('whitespace.removeTrailingWhitespace', scope: scopeDescriptor)
-          unless atom.config.get('whitespace.ignoreWhitespaceOnlyLines', scope: scopeDescriptor)
+      return unless event.text is '\n'
+      scopeDescriptor = editor.getRootScopeDescriptor()
+
+      if atom.config.get('whitespace.removeTrailingWhitespace', scope: scopeDescriptor)
+        unless atom.config.get('whitespace.ignoreWhitespaceOnlyLines', scope: scopeDescriptor)
+          editor.transact =>
             @clearBlankLineOnNewline(editor, event)

     editorDestroyedSubscription = editor.onDidDestroy =>
@@ -99,9 +101,8 @@
       buffer.scan new RegExp(spacesText, 'g'), ({replace}) -> replace('\t')

   clearBlankLineOnNewline: (editor, event) ->
-    return unless event.text is "\n"
     buffer = editor.getBuffer()
-    previousRow = editor.getCursorBufferPosition().row - 1
+    previousRow = event.range.start.row

     if buffer.isRowBlank(previousRow)
       editor.setIndentationForBufferRow(previousRow, 0)


Maintain selection ranges when adding a newline

--- lib/whitespace.coffee
+++ lib/whitespace.coffee
@@ -43,14 +43,12 @@

   ensureSingleTrailingNewline: (editor) ->
     buffer = editor.getBuffer()
-    cursor = editor.getCursor()
     lastRow = buffer.getLastRow()
-    originalCursorPosition = cursor.getBufferPosition()

     if buffer.lineForRow(lastRow) is ''
       row = lastRow - 1
       buffer.deleteRow(row--) while row and buffer.lineForRow(row) is ''
     else
+      selectedBufferRanges = editor.getSelectedBufferRanges()
       buffer.append('\n')
-
-    cursor.setBufferPosition(originalCursorPosition)
+      editor.setSelectedBufferRanges(selectedBufferRanges)


Get the author name from author shared object

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -69,7 +69,7 @@
       if expected_pos isnt sel.from
         # There is unselected text. $retain to the next selection
         unselected_insert_content = text_content.splice(
-          0, sel.from-expected_pos)
+          0, sel.from-expected_pos )
           .join('')
         deltas.push
           insert: unselected_insert_content


Add checks for App Name in UI

Add checks for App Name in UI

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -416,6 +416,8 @@
         log app, "app approved"
         callback?()

+  # We will move that to Server-Side GG
+
   compileApp:(name, callback)->

     kallback = (app)=>
@@ -567,8 +569,12 @@
                   color           : "#444444"
                   diameter        : 12
                 callback          : =>
+                  unless newAppModal.modalTabs.forms.form.inputs.name.validate()
+                    newAppModal.modalTabs.forms.form.buttons.Create.hideLoader()
+                    return
                   name        = newAppModal.modalTabs.forms.form.inputs.name.getValue()
                   type        = newAppModal.modalTabs.forms.form.inputs.type.getValue()
+                  name        = name.replace(/[^a-zA-Z0-9\/\-.]/g, '') if name
                   manifestStr = defaultManifest type, name
                   manifest    = JSON.parse manifestStr
                   appPath     = getAppPath manifest
@@ -601,6 +607,11 @@
                 label             : "Name:"
                 name              : "name"
                 placeholder       : "name your application..."
+                validate          :
+                  rules           :
+                    regExp        : /^[a-z\d]+([-][a-z\d]+)*$/i
+                  messages        :
+                    regExp        : "For Application name only lowercase letters and numbers are allowed!"

     newAppModal.once "KDObjectWillBeDestroyed", ->
       newAppModal = null
@@ -610,6 +621,7 @@

     type        = if isBlank then "blank" else "sample"
     name        = if name is "" then null else name
+    name        = name.replace(/[^a-zA-Z0-9\/\-.]/g, '') if name
     manifestStr = defaultManifest type, name
     manifest    = JSON.parse manifestStr
     appPath     = getAppPath manifest
@@ -631,17 +643,17 @@
               contents  : manifestStr
           , cb

-        if isBlank
-          stack.push (cb)=>
-            @kiteController.run
-              method      : "uploadFile"
-              withArgs    :
-                path      : escapeFilePath "#{fsFolder.path}/index.coffee"
-                contents  : """
-                              do->
-
-                            """
-            , cb
+        # if isBlank
+        #   stack.push (cb)=>
+        #     @kiteController.run
+        #       method      : "uploadFile"
+        #       withArgs    :
+        #         path      : escapeFilePath "#{fsFolder.path}/index.coffee"
+        #         contents  : """
+        #                       do->
+
+        #                     """
+        #     , cb

         stack.push (cb)=>
           @kiteController.run


undo/redo not called with modal open

undo/redo not called with modal open

--- client/src/app/game/undo/undoController.coffee
+++ client/src/app/game/undo/undoController.coffee
@@ -8,6 +8,8 @@

 # Remove code in LocalStorage
 clearCodeInCache = (programId) -> localStorage.removeItem(programId)
+
+isModalShowing = -> $(".modal, .large-modal").length > 0

 angular.module('gamEvolve.game.undo', ['gamEvolve.model.undo'])
@@ -60,12 +62,12 @@
   else
     ["ctrl+z", "ctrl+y"]
   Mousetrap.bind undoKey, ->
-    $scope.$apply ->
-      console.log "Starting undo"
-      $scope.undo()
+    if isModalShowing() then return false
+
+    $scope.$apply(-> $scope.undo())
     return false # Block "normal" browser undo
   Mousetrap.bind redoKey, ->
-    $scope.$apply ->
-      console.log "Starting redo"
-      $scope.redo()
+    if isModalShowing() then return false
+
+    $scope.$apply(-> $scope.redo())
     return false # Block "normal" browser redo


Fix case in require.

Fix case in require.

--- src/controls/control.coffee
+++ src/controls/control.coffee
@@ -1,6 +1,6 @@
 CrowdControl = require 'crowdcontrol'
 m = require '../mediator'
-Events = require '../Events'
+Events = require '../events'
 riot = require 'riot'

 scrolling = false


Use span instead of array for killing warning

--- index.cjsx
+++ index.cjsx
@@ -10,7 +10,7 @@
 module.exports =
   name: "quest-info"
   priority: 2
-  displayName: [<FontAwesome key={0} name='indent' />, " 任务信息"]
+  displayName: <FontAwesome key={0} name='indent' /> 任务信息
   description: "任务信息查询 & 任务面板强化"
   author: "马里酱"
   link: "https://github.com/malichan"


fix creation of controllerset in AppView

--- src/main/webapp/lib/extensions/Scenario.coffee
+++ src/main/webapp/lib/extensions/Scenario.coffee
@@ -45,7 +45,6 @@
   @set 'settings', new window.beats.Settings
   @set 'networklist', new window.beats.NetworkList
   @set 'controllerset', new window.beats.ControllerSet
-  @get('controllerset').set 'controller', new window.beats.Controller
   @set 'sensorlist', new window.beats.SensorList
   @set 'signallist', new window.beats.SignalList

@@ -166,7 +165,6 @@
             splitratioprofileset.get('splitratios').push(node.get('splitratios'))
       )

-window.beats.Scenario::createController= ->
+window.beats.Scenario::createController = ->
   @set('controllerset', new window.beats.ControllerSet)
-  @get('controllerset').set('controller', [])
   @get('controllerset').get('controller')


qs module added

qs module added

--- modules/gui/platforms/node/config/server.coffee
+++ modules/gui/platforms/node/config/server.coffee
@@ -2,26 +2,27 @@
 ##
 ##
 ##
-module.exports =
-
+module.exports =
+
     type: 'http.Server'
-
+
     data:
         public: [floyd.system.libdir+'/modules/gui/public/']
-
+
         lib:
             modules: ['gui', 'crypto']
-
-            node_modules: ['floyd/node_modules/markdown', 'floyd/node_modules/sanitizer']
-
-            aliases:
+
+            node_modules: ['floyd/node_modules/markdown', 'floyd/node_modules/sanitizer', 'floyd/node_modules/qs']
+
+            aliases:
                 markdown: '/node_modules/floyd/node_modules/markdown'
                 sanitizer: '/node_modules/floyd/node_modules/sanitizer'
-
-
+                querystring: '/node_modules/floyd/node_modules/qs'
+
+
+
             prepend: [
                 floyd.system.libdir+'/modules/gui/public/js/jquery-1.7.2.min.js'
                 floyd.system.libdir+'/modules/gui/public/js/jquery_single_double_click.js'
                 floyd.system.libdir+'/modules/gui/public/js/jquery.highlight-3.min.js'
             ]
-


Improve inheritance hierarchy

Improve inheritance hierarchy

--- mun.coffee
+++ mun.coffee
@@ -78,17 +78,25 @@
       @set 'current', @get('current') + 1
       (@get 'list')[@get 'current']

-  class BaseSLView extends Backbone.View
+  class BaseView extends Backbone.View
+    initialize: (n, c) ->
+      @name = n
+      @cls  = c
+      Master.register @, n, c
+      log "$ #{@name} initialized.", DEBUG
+    terminate: ->
+      Master.unregister @cls
+      @undelegateEvents()
+      log "$ #{@name} terminated.", DEBUG
+
+  class BaseSLView extends BaseView
     el: $ "#main-activity"
-    events:
+    events: ->   # for children to extend
       "keypress": 'enterKey'
       "click #btn-gsl-next": 'nextCountry'
     initialize: (n, c) ->
-      @name = n
-      @cls  = c
+      super n, c
       @listenTo @model, "change:list", @renderList
-      Master.register @, n, c
-      log '$ BaseSLView initialized.', DEBUG
     render: ->
       @$el.html ''
       @renderCurrent()
@@ -129,41 +137,39 @@
         @terminate()
       return
     terminate: ->
-      Master.unregister @cls
-      @undelegateEvents()
+      super()
       info "#{@name} exhausted."
-      log @name + ' terminated.', DEBUG

   class GSLView extends BaseSLView
     initialize: ->
-      super("General Speaker's List", 'gsl-view')
+      super "General Speaker's List", 'gsl-view'
       @listenTo @model, "change:time", @renderTimer
       log '\t$ GSLView initialized.', DEBUG

   class MCView extends BaseSLView
-    events:
-      "click #btn-exit-umc": 'terminate'
+    events: ->
+      _.extend {}, super, "click #btn-exit-mc": 'terminate'
     initialize: (ops)->
       @params = ops  # topic, time_tot, time_each
-      super("Moderacted Caucus", 'mc-view')
+      super "Moderacted Caucus", 'mc-view'
       log '\t$ MCView initialized.', DEBUG
     render: ->
       @$el.html "
Topic: #{@params.topic}

"
       @renderCurrent()
       @renderList()
       @renderTimer()
-      @$el.append '
<button class="btn btn-info" id="btn-exit-umc">Close this Moderated Caucus</button>'
+      @$el.append '
<button class="btn btn-info" id="btn-exit-mc">Close this Moderated Caucus</button>'
       $("#sl-add-country").tooltip(trigger: 'focus').focus()
       @
     renderTimer: ->
       timerView.setTime @params.time_each, @params.time_tot

-  class UMCView extends Backbone.View
+  class UMCView extends BaseView
     el: $ "#main-activity"
-    events:
+    events:
       "click #btn-exit-umc": "terminate"
     initialize: (ops) ->
-      Master.register @, "Un-moderated Caucus", 'umc-view'
+      super 'UMCView', 'umc-view'
       timerView.setTime ops.time
       @render()
       log '\t$ UMCView initialized.', DEBUG
@@ -172,19 +178,17 @@
       h += '<button class="btn btn-info" id="btn-exit-umc">Close this Un-moderated Caucus</button>'
       @$el.html h

-  class RollCallView extends Backbone.View
+  class RollCallView extends BaseView
     el: $ "#main-activity"
     events:
       "click #btn-roll-call-present": "countryPresent"
       "click #btn-roll-call-absent":  "countryAbsent"
-    className: 'roll-call-view'
     initialize: ->
       @current = 0
       @countryList = Master.get 'countryList'
-      Master.register @, 'Roll Call', @className
       Master.set 'presentList', []
       appView.disable 'btn-roll-call'
-      log '$ RollCallView initialized.', DEBUG
+      super 'RollCallView', 'roll-call-view'
     render: ->
       if @current == @countryList.length
         @terminate()
@@ -208,13 +212,11 @@
       @current++
       @render()
     terminate: ->
+      super()
+      success 'Roll Call completed.'
       appView.enable 'btn-roll-call'
       appView.enable 'btn-gsl'
       appView.enable 'btn-motion'
-      Master.unregister @className
-      @undelegateEvents()
-      success 'Roll Call completed.'
-      log 'RollCallView terminated', DEBUG

   class SettingsModalView extends Backbone.View
     el: $ "#modal-settings"
@@ -546,6 +548,4 @@
   initModal.initSession()
   controlView.initRollCall()

-  timerView.setTime 2, 5
-
   return


handle inline templates from output

handle inline templates from output

--- src/crystal/generate.coffee
+++ src/crystal/generate.coffee
@@ -397,9 +397,10 @@
 			# get content from output
 			if output.template
 				output_path = ".crystal/template/#{output.template}"
-				if not fs.existsSync output_path
-					throw new Error "Template (#{output.template}) does not exist at path (#{output_path})"
-				template = fs.readFileSync(output_path, 'utf8')
+				if fs.existsSync output_path
+					template = fs.readFileSync(output_path, 'utf8')
+				else
+					template = output.template
 			else
 				template = generator.template
 			if engine


Fix editor scope callback

Fix editor scope callback

--- src/directive/editor.coffee
+++ src/directive/editor.coffee
@@ -15,7 +15,7 @@
     {
         restrict: 'E'
         templateUrl: '/directive/editor.html'
-        scope: ngModel: '=ngModel', ngChange: '=ngChange'
+        scope: ngModel: '=ngModel', ngChange: '&ngChange'
         link: (scope, element, attrs) ->
             options = defaults

@@ -41,6 +41,9 @@
                     scope.$apply()

             scope.$watch 'ngModel', ->
+                if scope.ngChange
+                    scope.ngChange()
+
                 if session.getValue() isnt scope.ngModel
                     dont = true
                     engine.setValue scope.ngModel


Move add to be above remove

Move add to be above remove

--- src/grammar-registry.coffee
+++ src/grammar-registry.coffee
@@ -74,28 +74,6 @@
   grammarForScopeName: (scopeName) ->
     @grammarsByScopeName[scopeName]

-  # Public: Remove a grammar from this registry.
-  #
-  # * `grammar` The {Grammar} to remove.
-  #
-  # Returns undefined.
-  removeGrammar: (grammar) ->
-    _.remove(@grammars, grammar)
-    delete @grammarsByScopeName[grammar.scopeName]
-    _.remove(@injectionGrammars, grammar)
-    @grammarUpdated(grammar.scopeName)
-    undefined
-
-  # Public: Remove the grammar with the given scope name.
-  #
-  # * `scopeName` A {String} such as `"source.js"`.
-  #
-  # Returns the removed {Grammar} or undefined.
-  removeGrammarForScopeName: (scopeName) ->
-    grammar = @grammarForScopeName(scopeName)
-    @removeGrammar(grammar) if grammar?
-    grammar
-
   # Public: Add a grammar to this registry.
   #
   # A 'grammar-added' event is emitted after the grammar is added.
@@ -110,6 +88,28 @@
     @emit 'grammar-added', grammar
     @emitter.emit 'did-add-grammar', grammar

+  # Public: Remove a grammar from this registry.
+  #
+  # * `grammar` The {Grammar} to remove.
+  #
+  # Returns undefined.
+  removeGrammar: (grammar) ->
+    _.remove(@grammars, grammar)
+    delete @grammarsByScopeName[grammar.scopeName]
+    _.remove(@injectionGrammars, grammar)
+    @grammarUpdated(grammar.scopeName)
+    undefined
+
+  # Public: Remove the grammar with the given scope name.
+  #
+  # * `scopeName` A {String} such as `"source.js"`.
+  #
+  # Returns the removed {Grammar} or undefined.
+  removeGrammarForScopeName: (scopeName) ->
+    grammar = @grammarForScopeName(scopeName)
+    @removeGrammar(grammar) if grammar?
+    grammar
+
   # Public: Read a grammar synchronously but don't add it to the registry.
   #
   # * `grammarPath` A {String} absolute file path to a grammar file.


fix latex filename with funny characters issue

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -2557,12 +2557,7 @@
                 opts.cb(false, {n:n, x:x, y:y})

     default_tex_command: () =>
-        a = "pdflatex -synctex=1 -interact=nonstopmode "
-        if @filename_tex.indexOf(' ') != -1
-            a += "'#{@filename_tex}'"
-        else
-            a += @filename_tex
-        return a
+        return "pdflatex -synctex=1 -interact=nonstopmode '#{@filename_tex}'"

     # runs pdflatex; updates number of pages, latex log, parsed error log
     update_pdf: (opts={}) =>


[Frontend] Delete useless code in room.coffee

--- src/frontend/room.coffee
+++ src/frontend/room.coffee
@@ -141,8 +141,7 @@
             $(elems[i]).stop(true, true).fadeOut 400
             i++
         $('div#endMessage').stop(true, true).fadeOut 400
-        $("#tutorialMode").css "background-color", "#00aeef"
-        $("#tutorialMode").css "box-shadow", "inset 0 1px #15DBCB"
+        do @colorTutorialBtn

     # Color the tutorial button.
     colorTutorialBtn   : () =>


Grid for hidden elements fixed

Grid for hidden elements fixed

--- impl/base/level1/grid.coffee
+++ impl/base/level1/grid.coffee
@@ -40,12 +40,14 @@
 			# sum columns positions
 			last = 0
 			for column, i in columnsPositions
-				last = columnsPositions[i] += last + columnSpacing
+				val = columnsPositions[i]
+				last = columnsPositions[i] += last + (if val then columnSpacing else 0)

 			# sum rows positions
 			last = 0
 			for row, i in rowsPositions
-				last = rowsPositions[i] += last + rowSpacing
+				val = rowsPositions[i]
+				last = rowsPositions[i] += last + (if val then rowSpacing else 0)

 			# set positions
 			for childId, i in children


koding-payment: desc => description

koding-payment: desc => description

--- node_modules_koding/koding-payment/lib/models.coffee
+++ node_modules_koding/koding-payment/lib/models.coffee
@@ -98,7 +98,7 @@

     @title        = xmlJs.name[0]
     @planCode     = xmlJs.plan_code[0]
-    @desc         = ifString xmlJs.description[0]
+    @description  = ifString xmlJs.description[0]
     @feeAmount    = xmlJs.unit_amount_in_cents[0].USD
     @feeInitial   = xmlJs.setup_fee_in_cents[0].USD
     @feeInterval  = xmlJs.plan_interval_length


Add fairs to the check on related collections

--- models/artwork.coffee
+++ models/artwork.coffee
@@ -308,7 +308,7 @@
   # e.g. @relatedCollections['sales'] # => Backbone.Collection
   setupRelatedCollections: ->
     # ['sales', 'shows', 'fairs']
-    @relatedCollections = _.reduce ['sales'], (memo, aspect) =>
+    @relatedCollections = _.reduce ['sales', 'fairs'], (memo, aspect) =>
       memo[aspect] = @[aspect] = new Backbone.Collection
       @[aspect].url = "#{sd.ARTSY_URL}/api/v1/related/#{aspect}?artwork[]=#{@id}"
       @[aspect].kind = _.capitalize aspect


Get ready for longs!

Get ready for longs!

--- test/long.coffee
+++ test/long.coffee
@@ -27,3 +27,11 @@
          assert utf8.valid q = utf8 z
          assert z == utf8.code q
          random.test4 false, q
+
+  it 'may be valid if needed', ->
+    @skip() if 4 != valid8.maxBytes
+    try
+      valid8.maxBytes = 8
+      # ...
+    finally
+      valid8.maxBytes = 4


chekc the path

chekc the path

--- test/main.coffee
+++ test/main.coffee
@@ -18,6 +18,7 @@
     myFunction.once "data", (newFile) ->
       should.exist newFile
       should.exist newFile.contents
+      should.equal newFile.path, 'test/fixtures/normal.json'
       String(newFile.contents).should.equal String fs.readFileSync path.join __dirname, "/expected/normal.json"
       done()


fix specs

fix specs

--- test/test.coffee
+++ test/test.coffee
@@ -3,127 +3,141 @@
 describe 'scraper', ->

   describe 'scraper.search(title, opts)', ->
-    it 'should return an array of search results', -->

+    results = []
+
+    before -->
       results = yield scraper.search 'Game of Thrones'
+
+    it 'should return an array of search results', ->
       results.should.be.an.Array

-      describe 'search result', ->
-        searchResult = results[0]
+    describe 'search result', ->

-        it 'shold have a name', ->
-          searchResult.should.have.property 'name'
-          searchResult.name.should.match /game.of.thrones/i
+      it 'shold have a name', ->
+        results[0].should.have.property 'name'
+        results[0].name.should.match /game.of.thrones/i

-        it 'should have upload date', ->
-          searchResult.should.have.property 'uploadDate'
-          ###
-          # Valid dates:
-          #  31 mins ago
-          #  Today 02:18
-          #  Y-day 22:14
-          #  02-10 03:36
-          #  06-21 2011
-          ###
-          searchResult.uploadDate.should.match /(\d*\smins\sago)|(Today|Y-day)\s\d\d:\d\d|\d\d-\d\d\s(\d\d:\d\d|\d{4})/
+      it 'should have upload date', ->
+        results[0].should.have.property 'uploadDate'
+        ###
+        # Valid dates:
+        #  31 mins ago
+        #  Today 02:18
+        #  Y-day 22:14
+        #  02-10 03:36
+        #  06-21 2011
+        ###
+        results[0].uploadDate.should.match /(\d*\smins\sago)|(Today|Y-day)\s\d\d:\d\d|\d\d-\d\d\s(\d\d:\d\d|\d{4})/

-        it 'should have size', ->
-          searchResult.should.have.property 'size'
-          ###
-          # Valid sizes:
-          #  529.84 MiB
-          #  2.04 GiB
-          #  598.98 KiB
-          ###
-          searchResult.size.should.match /\d+\.\d+\s(G|M|K)iB/
+      it 'should have size', ->
+        results[0].should.have.property 'size'
+        ###
+        # Valid sizes:
+        #  529.84 MiB
+        #  2.04 GiB
+        #  598.98 KiB
+        ###
+        results[0].size.should.match /\d+\.\d+\s(G|M|K)iB/

-        it 'should have seeders and leechers count', ->
-          searchResult.should.have.property 'seeders'
-          searchResult.should.have.property 'leechers'
-          (~~searchResult.leechers).should.be.within 0, 100000
-          (~~searchResult.seeders).should.be.within 0, 100000
+      it 'should have seeders and leechers count', ->
+        results[0].should.have.property 'seeders'
+        results[0].should.have.property 'leechers'
+        (~~results[0].leechers).should.be.within 0, 100000
+        (~~results[0].seeders).should.be.within 0, 100000

-        it 'should have a link', ->
-          searchResult.should.have.property 'link'
-          #e.g http://thepiratebay.se/torrent/9897245/Game_of_Thrones_Season_3_[Hard_Subs_in_English__AVI_format]
-          searchResult.link.should.match /http:\/\/thepiratebay.se\/torrent\/\d+\/.+/
+      it 'should have a link', ->
+        results[0].should.have.property 'link'
+        #e.g http://thepiratebay.se/torrent/9897245/Game_of_Thrones_Season_3_[Hard_Subs_in_English__AVI_format]
+        results[0].link.should.match /http:\/\/thepiratebay.se\/torrent\/\d+\/.+/

-        it 'should have a magnet link', ->
-          searchResult.should.have.property 'magnetLink'
-          #e.g magnet:?xt=urn:btih:36c56414e0e491f824e25dcde0e8a4d520f8705c....
-          searchResult.magnetLink.should.match /magnet:\?xt=.+/
+      it 'should have a magnet link', ->
+        results[0].should.have.property 'magnetLink'
+        #e.g magnet:?xt=urn:btih:36c56414e0e491f824e25dcde0e8a4d520f8705c....
+        results[0].magnetLink.should.match /magnet:\?xt=.+/

-        it 'may have a torrent link', ->
-          searchResult.should.have.property 'torrentLink'
-          #//piratebaytorrents.info/9897245/Game_of_Thrones_Season_3_[Hard_Subs_in_English__AVI_format].9897245.TPB.torrent
-          searchResult.torrentLink.should.match /\/\/piratebaytorrents\.info\/\d+\/.+\.torrent/ unless searchResult.torrentLink isnt ''
+      it 'may have a torrent link', ->
+        results[0].should.have.property 'torrentLink'
+        #//piratebaytorrents.info/9897245/Game_of_Thrones_Season_3_[Hard_Subs_in_English__AVI_format].9897245.TPB.torrent
+        results[0].torrentLink.should.match /\/\/piratebaytorrents\.info\/\d+\/.+\.torrent/ unless results[0].torrentLink isnt ''

-        it 'should have a category', ->
-          searchResult.should.have.property 'category'
-          searchResult.category.id.should.match /[1-6]00/
-          searchResult.category.name.should.match /\w+/
+      it 'should have a category', ->
+        results[0].should.have.property 'category'
+        results[0].category.id.should.match /[1-6]00/
+        results[0].category.name.should.match /\w+/

-        it 'should have a subcategory', ->
-          searchResult.should.have.property 'subcategory'
-          searchResult.subcategory.id.should.match /[1-6][09][1-9]/
-          searchResult.subcategory.name.should.match /[a-zA-Z0-9 ()/-]/
+      it 'should have a subcategory', ->
+        results[0].should.have.property 'subcategory'
+        results[0].subcategory.id.should.match /[1-6][09][1-9]/
+        results[0].subcategory.name.should.match /[a-zA-Z0-9 ()/-]/

   describe 'scraper.getCategories()', ->

-    it 'should return an array of categories', -->
+    categories = []
+
+    before -->
       categories = yield scraper.getCategories()
+
+    it 'should return an array of categories', ->
       categories.should.be.an.Array

-      describe 'category', ->
-        category = categories[0]
+    describe 'category', ->
+
+      it 'should have an id', ->
+        categories[0].should.have.property 'id'
+        categories[0].id.should.match /\d00/
+
+      it 'should have a name', ->
+        categories[0].should.have.property 'name'
+        categories[0].name.should.be.a.String
+
+      it 'name should match id', ->
+        video = categories.find (elem) -> elem.name is 'Video'
+        video.id.should.be.equal '200'
+
+      it 'shold have subcategories array', ->
+        categories[0].should.have.property 'subcategories'
+        categories[0].subcategories.should.be.an.Array
+
+      describe 'subcategory', ->

         it 'should have an id', ->
-          category.should.have.property 'id'
-          category.id.should.match /\d00/
+          subcategory = categories[0].subcategories[0]
+          subcategory.should.have.property 'id'
+          subcategory.id.should.match /\d{3}/

         it 'should have a name', ->
-          category.should.have.property 'name'
-          category.name.should.be.a.String
-
-        it 'name should match id', ->
-          video = categories.find (elem) -> elem.name is 'Video'
-          video.id.should.be.equal '200'
-
-        it 'shold have subcategories array', ->
-          category.should.have.property 'subcategories'
-          category.subcategories.should.be.an.Array
-
-          describe 'subcategory', ->
-            subcategory = category.subcategories[0]
-
-            it 'should have an id', ->
-              subcategory.should.have.property 'id'
-              subcategory.id.should.match /\d{3}/
-
-            it 'should have a name', ->
-              subcategory.should.have.property 'name'
-              subcategory.name.should.be.a.String
+          subcategory = categories[0].subcategories[0]
+          subcategory.should.have.property 'name'
+          subcategory.name.should.be.a.String

   describe 'scraper.topTorrents(category, opts)', ->
-    it 'should return an array of top torrents of the selected category', -->
+
+    results = []
+
+    before -->
       results = yield scraper.topTorrents '205'
+
+    it 'should return an array of top torrents of the selected category', ->
       results.should.be.an.Array

-      describe 'search result', ->
-        searchResult = results[0]
-
-        it 'category and subcategory shoud match specified category', ->
-          searchResult.category.name.should.be.equal 'Video'
-          searchResult.subcategory.name.should.be.equal 'TV shows'
+    describe 'search result', ->
+      it 'category and subcategory shoud match specified category', ->
+        results[0].category.name.should.be.equal 'Video'
+        results[0].subcategory.name.should.be.equal 'TV shows'

   describe 'scraper.recentTorrents()', ->
-    it 'should return an array of the most recent torrents', -->
+    results = []
+
+    before -->
       results = yield scraper.recentTorrents()
+
+    it 'should return an array of the most recent torrents', ->
       results.should.be.an.Array

-      describe 'recent torrent', ->
+    describe 'recent torrent', ->
+      it 'should be uploaded recently', ->
         recentTorrent = results[0]
-        it 'should be uploaded recently', ->
-          recentTorrent.uploadDate.should.match /\d+\smins?\sago/
+        recentTorrent.uploadDate.should.match /\d+\smins?\sago/


fixed

fixed

--- test/test.coffee
+++ test/test.coffee
@@ -15,11 +15,13 @@
   name         : 'testApp'
   repository   : 'testApp'
   organization : 'sailorjs'
+  description  : 'A new Sailor Proyect'

 options_module =
   name         : 'sailor-module-test'
   repository   : 'sailor-module-test'
   organization : 'sailorjs'
+  description  : 'A new Sailor Proyect'

 options_lift =
   log: level: 'silent'


Delete unused cson

Delete unused cson

--- test/test.coffee
+++ test/test.coffee
@@ -1,6 +1,5 @@
 fs           = require 'fs'
 path         = require 'path'
-CSON         = require 'cson'
 chai         = require 'chai'
 expect       = chai.expect
 posthtml     = require 'posthtml'


added new test cases

added new test cases

--- test/test.coffee
+++ test/test.coffee
@@ -5,15 +5,26 @@
 vows
 	.describe('Logger')
 	.addBatch
-		"when instantiating a new logger with a custom config showMillis = true":
-			topic: () ->
-				config =
-					showMillis: true
-				log = new logger config
+		"when instantiating a new logger":
+			"with a custom config showMillis = true":
+				topic: () ->
+					config =
+						showMillis: true
+					log = new logger config

-			"the value of the logger config for showMillis is true and stringifyJSON is still true": (topic) ->
-				assert.equal topic.config.showMillis,true
-				assert.equal topic.config.stringifyJSON,true
+				"the value of the logger config for showMillis is true and stringifyJSON is still true": (topic) ->
+					assert.equal topic.config.showMillis,true
+					assert.equal topic.config.stringifyJSON,true
+
+			"with a custom config showMillis = false":
+				topic: () ->
+					config =
+						showMillis: false
+					log = new logger config
+
+				"the value of the logger config for showMillis is false and stringifyJSON is still true": (topic) ->
+					assert.equal topic.config.showMillis,false
+					assert.equal topic.config.stringifyJSON,true

 		"when padding a one-digit number with zeros to 2 digits":
 			topic: () ->
@@ -21,5 +32,16 @@

 			"the result is the one-digits number left-padded with a zero": (topic) ->
 				assert.equal topic.padZeros(3,2),'03'
+
+		"when logging a message":
+			topic: () ->
+				config =
+					showMillis: true
+					prefix: "Test"
+				log = new logger config
+
+			"the log method doesn't throw an error": (topic) ->
+				assert.doesNotThrow () ->
+					topic.info "test message"

 .exportTo(module)


hub/raw server: show number of projects

hub/raw server: show number of projects

--- src/smc-hub/share/raw.coffee
+++ src/smc-hub/share/raw.coffee
@@ -43,26 +43,49 @@

     router.get '/', (req, res) ->
         # simple listing by first 2 characters in project_id
-        # CREATE INDEX IF NOT EXISTS project_id_2char ON public_paths (substring(project_id::text from 0 for 2));
-        links = ("#{l}..." for l in HEXCHARS)
-        links = links.join('
')
-        out = """
-
Public Projects

-        #{links}
-        """
-        res.send(out)
+        # CREATE INDEX IF NOT EXISTS project_id_2char ON public_paths (substring(project_id::text from 1 for 2));
+        # CREATE INDEX IF NOT EXISTS project_id_1char ON public_paths (substring(project_id::text from 1 for 1));
+
+        opts.database._query
+            query : '''
+                    SELECT DISTINCT(substring(project_id::text from 1 for 1)) AS prefix, COUNT(DISTINCT project_id) AS num FROM public_paths
+                    GROUP BY prefix
+                    ORDER BY prefix
+                    '''
+            cb    : (err, result) ->
+                if err
+                    res.send(JSON.stringify(err))
+                else
+                    out = """
+
Public Projects

+                    """
+                    for row in result.rows
+                        out += "#{row.prefix}... (#{row.num})
"
+                    res.send(out)

     router.get /^\/[0-9a-z]$/, (req, res) ->
         # matches one uuid char
         c1 = req.path[1]
-        links = ("#{c1}#{l}..." for l in HEXCHARS)
-        links = links.join('
')
-        out = """
-
Public Projects

-        UP

-        #{links}
-        """
-        res.send(out)
+
+        opts.database._query
+            query : '''
+                    SELECT DISTINCT(substring(project_id::text from 1 for 2)) AS prefix, COUNT(DISTINCT project_id) AS num FROM public_paths
+                    WHERE substring(project_id::text from 1 for 1) = $1::TEXT
+                    GROUP BY prefix
+                    ORDER BY prefix
+                    '''
+            params : [c1]
+            cb    : (err, result) ->
+                if err
+                    res.send(JSON.stringify(err))
+                else
+                    out = """
+
Public Projects

+                    UP

+                    """
+                    for row in result.rows
+                        out += "#{row.prefix}... (#{row.num})
"
+                    res.send(out)

     router.get /^\/[0-9a-z]{2}$/, (req, res) ->
         # matches two uuid char


fix https://github.com/sagemathinc/cocalc/issues/3538 by wrapping all calls to refresh in try/catch

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -688,7 +688,7 @@
         @hide_startup_message()
         @element.find(".webapp-editor-codemirror-content").show()
         for cm in @codemirrors()
-            cm.refresh()
+            cm_refresh(cm)

     hide_startup_message: () =>
         @element.find(".webapp-editor-codemirror-startup-message").hide()
@@ -908,7 +908,7 @@
         # the getScrollInfo function below will return the sizing data about
         # the cm instance before the above css font-size change has been rendered.
         f = () =>
-            cm.refresh()
+            cm_refresh(cm)
             scroll_after = cm.getScrollInfo()
             x = (scroll_before.left / scroll_before.width) * scroll_after.width
             y = (((scroll_before.top+scroll_before.clientHeight/2) / scroll_before.height) * scroll_after.height) - scroll_after.clientHeight/2
@@ -1429,9 +1429,9 @@

         refresh = (cm) =>
             return if not cm?
-            cm.refresh()
+            cm_refresh(cm)
             # See https://github.com/sagemathinc/cocalc/issues/1327#issuecomment-265488872
-            setTimeout((=>cm.refresh()), 1)
+            setTimeout((=>cm_refresh(cm)), 1)

         for cm in @codemirrors()
             refresh(cm)
@@ -2001,3 +2001,13 @@
         f         : (project_id, path, opts) -> codemirror_session_editor(project_id, path, opts)
         is_public : false

+
+
+# See https://github.com/sagemathinc/cocalc/issues/3538
+cm_refresh = (cm) ->
+    if not cm?
+        return
+    try
+        cm.refresh()
+    catch err
+        console.warn("cm refresh err", err)


Removed old tooltip

Removed old tooltip

--- core/app/backbone/client/facts_new_view.coffee
+++ core/app/backbone/client/facts_new_view.coffee
@@ -25,12 +25,6 @@
   render: ->
     @$('.fact-wheel').on 'click.whatsyouropinion', =>
       @close()
-
-      if FactlinkApp.guided
-        $('#submit').tooltip(
-          title: "Great! Click here to finish",
-          trigger: "manual"
-        ).tooltip("show");

     $(window).on 'resize.whatsyouropinion', =>
       @showTooltip();


removed code :-)

removed code :-)

--- src/driver/driverUtil.coffee
+++ src/driver/driverUtil.coffee
@@ -407,19 +407,6 @@
   throw new TypeError('bad filter type')
   return

-# I do not like this method -VO
-exports.removeWithinFilter = removeWithinFilter = (filter) ->
-  if filter.type is 'within'
-    return null
-
-  if filter.type is 'not'
-    filter.filter = removeWithinFilter(filter.filter)
-
-  if filter.type in ['and', 'or']
-    filter.filters = filter.filters.map(removeWithinFilter).filter((filter) -> return filter?)
-
-  return filter
-

 # Flattens the split tree into an array
 #


tests(categoryOrderHint): remove unnecessary integration test

--- src/spec/integration/products-sync.spec.coffee
+++ src/spec/integration/products-sync.spec.coffee
@@ -38,15 +38,6 @@
     .then (result) =>
       expect(result.statusCode).toBe 201
       @productType = result.body
-      debug 'Creating a Category'
-      @client.categories.save(
-        name:
-          en: 'myFancyCategory'
-        slug:
-          en: _.uniqueId "c#{new Date().getTime()}_"
-      )
-    .then (result) =>
-      @testCategory = result.body
       done()
     .catch (error) -> done(_.prettify(error))

@@ -71,15 +62,10 @@
       debug "Deleting #{payload.body.total} product types"
       Promise.all _.map payload.body.results, (productType) =>
         @client.productTypes.byId(productType.id).delete(productType.version)
-    .then (results) =>
+    .then () ->
       debug "Deleted #{results.length} product types"
-      debug 'Delete Category'
-      @client.categories.byId(@testCategory.id).delete(@testCategory.version)
-    .then () ->
       done()
     .catch (error) -> done(_.prettify(error.body))
-
-
   , 60000 # 1min

   it 'should create and sync product', (done) ->
@@ -115,14 +101,6 @@
       name: {de: pName}
       slug: {de: pSlug}
       description: {de: 'A foo product'}
-      categoryOrderHints:
-        "#{@testCategory.id}": '0.9'
-      categories:[
-        {
-          typeId: 'category',
-          id: @testCategory.id
-        }
-      ]
       metaTitle: {de: 'The Foo'}
       metaDescription: {de: 'The Foo product'}
       masterVariant:
@@ -151,19 +129,6 @@
     debug 'Create initial product to be synced'
     @client.products.create(OLD_PROD)
     .then (result) =>
-      debug 'assing product to test category'
-      @client.products.byId(result.body.id).update(
-        version: result.body.version
-        actions: [
-          {
-            action: 'addToCategory'
-            category:
-              typeId: "category",
-              id: @testCategory.id
-          }
-        ]
-      )
-    .then (result) =>
       debug 'Fetch projection of created product'
       @client.productProjections.byId(result.body.id).staged(true).fetch()
     .then (result) =>
@@ -175,13 +140,11 @@
     .then (result) =>
       debug 'Fetch projection of updated product'
       @client.productProjections.byId(result.body.id).staged(true).fetch()
-    .then (result) =>
+    .then (result) ->
       updated = result.body
       expect(updated.name).toEqual {de: pName}
       expect(updated.slug).toEqual {de: pSlug}
       expect(updated.description).toEqual {de: 'A foo product'}
-      expect(updated.categoryOrderHints).toEqual { "#{@testCategory.id}": '0.9' }
-      expect(updated.categories).toEqual [ { typeId: 'category', id: @testCategory.id } ]
       expect(updated.metaTitle).toEqual {de: 'The Foo'}
       expect(updated.metaDescription).toEqual {de: 'The Foo product'}
       expect(updated.masterVariant.prices[0].value.centAmount).toBe 1500


Support AVERAGEA

Support AVERAGEA

--- src/formula/evaluator.coffee
+++ src/formula/evaluator.coffee
@@ -137,6 +137,24 @@
     if size == 0
       @error(FormulaError.DIV0)
     sum / size
+
+  AVERAGEA: (args)->
+    @checkArgumentSize(args, 1, Number.MAX_VALUE)
+    args = @expandRange(TYPE_ANY, args)
+    sum = 0
+    size = 0
+    for arg in args
+      if Utils.isPureNumber(arg) || Utils.isBoolean(arg)
+        sum += Number(arg)
+      else if arg instanceof FormulaError
+        throw arg
+      if arg != undefined
+        size++
+
+    if size == 0
+      @error(FormulaError.DIV0)
+    sum / size
+
   BASE: (args)->
     @checkArgumentSize(args, 2, 3)
     [n, base, len] = args


Ensure that the first Trix field on a page is autofocused

--- src/trix/elements/trix_document_element.coffee
+++ src/trix/elements/trix_document_element.coffee
@@ -1,4 +1,4 @@
-{handleEvent, handleEventOnce} = Trix
+{handleEvent, handleEventOnce, defer} = Trix

 Trix.defineElement class extends Trix.Element
   @tagName: "trix-document"
@@ -24,8 +24,10 @@

   attachedCallback: ->
     super
-    if @hasAttribute("autofocus") and document.querySelector("[autofocus]") is this
-      @focus()
+    defer =>
+      unless document.querySelector(":focus")
+        if @hasAttribute("autofocus") and document.querySelector("[autofocus]") is this
+          @focus()

   makeEditable = (element) ->
     return if element.hasAttribute("contenteditable")


Fix buy_service broadcast to return the buy, not the event

--- app/assets/javascripts/services/buy_service.coffee
+++ app/assets/javascripts/services/buy_service.coffee
@@ -16,7 +16,9 @@

     getFromResponse= (resp)-> resp.data.buys || resp.data.buy

-    broadcastNewBuy = (buy)-> $rootScope.$broadcast 'buys:new', buy
+    broadcastNewBuy = (buy)->
+      $rootScope.$broadcast 'buys:new', buy
+      buy

     latest = (params)-> $http.get('/buys', {params}).then(convert).then(getFromResponse)


Styleblock: change the style of conditional statement

--- workers/social/lib/social/render/styleblock.coffee
+++ workers/social/lib/social/render/styleblock.coffee
@@ -1,5 +1,5 @@
 module.exports = (options={})->
-  unless options.favicon then options.favicon = "/a/images/favicon.ico"
+  options.favicon or= "/a/images/favicon.ico"
   """
   <meta charset="utf-8" />
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />


Account: reverted loggedin check

Account: reverted loggedin check

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -555,7 +555,7 @@
     homePageOptions = extend options, {
       renderedAccount : account
       account         : this
-      isLoggedIn      : account.type isnt 'registered'
+      isLoggedIn      : account.type is 'unregistered'
     }

     JAccount.renderHomepage homePageOptions, callback


removed copy paste crap from

removed copy paste crap from

--- workers/social/lib/social/render/profile.coffee
+++ workers/social/lib/social/render/profile.coffee
@@ -98,40 +98,40 @@

-                    <button type="button" class="kdbutton kdwhitebtn profilefollowbtn w-loader" id="kd-274"><canvas width="18" height="18"></canvas><canvas style="display: none;" width="18" height="18"></canvas>
+                    <button type="button" class="kdbutton kdwhitebtn profilefollowbtn w-loader"><canvas width="18" height="18"></canvas><canvas style="display: none;" width="18" height="18"></canvas>

                       Follow
                     </button>
-                    @#{validator.sanitize(nickname).xss()}
+                    @#{validator.sanitize(nickname).xss()}

-
#{validator.sanitize(firstName).xss()} #{validator.sanitize(lastName).xss()}

-

#{if locationTags then validator.sanitize(locationTags[0]).xss() else 'Earth'}

+
#{validator.sanitize(firstName).xss()} #{validator.sanitize(lastName).xss()}

+

#{if locationTags then validator.sanitize(locationTags[0]).xss() else 'Earth'}

                         #{validator.sanitize(nickname).xss()}.koding.com
                         member for #{if amountOfDays < 2 then 'a' else amountOfDays} day#{if amountOfDays > 1 then 's' else ''}.

-                          #{counts.followers} Followers
+                          #{counts.followers} Followers

-                          #{counts.following} Following
+                          #{counts.following} Following

-                          #{counts.likes} Likes
+                          #{counts.likes} Likes

-                          Contact#{validator.sanitize(firstName).xss()}
+                          Contact#{validator.sanitize(firstName).xss()}

-
#{validator.sanitize(about).xss()}

+
#{validator.sanitize(about).xss()}

SKILLS

...

@@ -175,7 +175,7 @@

-                                    NEED HELP? Learn Personal feed
+                                    NEED HELP? Learn Personal feed

@@ -203,7 +203,7 @@

-                                            Loading...<canvas width="16" height="16"></canvas><canvas style="display: none;" width="16" height="16"></canvas>
+                                            Loading...<canvas width="16" height="16"></canvas><canvas style="display: none;" width="16" height="16"></canvas>


Support releasing from plugin interface; restore scrollbar on release

--- src/jquery.monoscroll.coffee
+++ src/jquery.monoscroll.coffee
@@ -61,11 +61,14 @@

       @$frame.on 'scroll', @handleScroll

-      this
+      new Binding
+        target: @target
+        controller: this

     releaseTarget: ->
       return unless @target
       @$frame.off 'scroll', @handleScroll
+      @$el.css 'height', 'auto'
       @target.$el.css 'overflow', val if val = @target.info.oldOverflow
       @target = null

@@ -82,6 +85,14 @@
     $.data el, key, (if value is undefined then UNDEF else value)
     value

+  class Binding
+    constructor: (opts) ->
+      @target = opts.target
+      @controller = opts.controller
+
+    release: ->
+      @controller.releaseTarget() if @controller.target?.el is @target.el
+
   monoscroll = (target, opts) ->
     $controller = $ opts?.controller ? 'body'
     controller = setDefault $controller, 'monoscroll.controller', ->
@@ -92,14 +103,28 @@
     delete newOpts.controller
     delete newOpts.frame
     controller.setTarget target, newOpts
-    null

   $.extend $.fn,
-    monoscroll: (options) ->
+    monoscroll: (optionsOrMethod, args...) ->
       if @length > 1
         $.error "Can't use monoscroll on jQuery collection with more than one element."
-      monoscroll this, options
-      this

-  # This is a jQuery plugin so we don't want to return anything from our module.
-  null
+      plugin = @data 'monoscroll.instance'
+      method = optionsOrMethod if typeof optionsOrMethod is 'string'
+      options = optionsOrMethod ? {} unless method
+
+      unless plugin
+        if method
+          $.error "You can't call the monoscroll method '#{ method }' without
+                   first initializing the plugin by calling monoscroll() on the
+                   jQuery object."
+
+        plugin = monoscroll this, options
+        @data 'monoscroll.instance', plugin
+
+      if method
+        unless typeof plugin[method] is 'function'
+          $.error "Method '#{ method }' does not exist on jQuery.monoscroll"
+        return plugin[method] args...
+
+      plugin


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -58,6 +58,7 @@
   this.charAt(0).toUpperCase() + this.substring(1).toLowerCase()

 ( ->
+
 ###*
 Return the class or constant named in this string.

@@ -78,7 +79,7 @@
     that
   else
     throw "String#constantize: '#{this}' is not a valid constant name."
-
+)()
 ###*
 Returns a new string that is a more human readable version.


Layer

Layer

- When preventing "click" upon Layer click-thru close,
  don't just stop the Event propgation but also preventDefault.
  This fixes a problem when a FileUploadButton opened
  a menu (the FileUpload would open altough the user would expect
  the menu to close first).

--- src/base/Layer/Layer.coffee
+++ src/base/Layer/Layer.coffee
@@ -1179,8 +1179,7 @@
 						only_once: true
 						call: (ev) =>
 							console.error "eating click on layer opening element"
-							ev.stopPropagation()
-							return
+							return ev.stop()

 				ev.cui_layer_closed = true


fix format

fix format

--- Isengard/app/assets/javascripts/periods.js.coffee
+++ Isengard/app/assets/javascripts/periods.js.coffee
@@ -15,8 +15,8 @@
     autoclose: true,
     weekStart: 1,
     language: 'nl',
-    startDate: $.format.date(Date(), "yyyy-MM-dd")
-    #showTimePicker: false
+    startDate: $.format.date(Date(), "yyyy-MM-dd"),
+    format: 'yyyy-mm-dd'
   };

   $('#period-start').datetimepicker(datePickerOptions);


:lipstick:

:lipstick:

--- build/tasks/build-task.coffee
+++ build/tasks/build-task.coffee
@@ -61,10 +61,9 @@
     ]
     ignoredPaths = ignoredPaths.map (ignoredPath) -> _.escapeRegExp(ignoredPath)

-    # Put patterns here that shouldn't be escaped
-    ignoredPaths.push _.escapeRegExp(path.join('build', 'Release') + path.sep) + '.*\\.pdb'
     # Add .* to avoid matching hunspell_dictionaries.
     ignoredPaths.push _.escapeRegExp(path.join('spellchecker', 'vendor', 'hunspell') + path.sep) + ".*"
+    ignoredPaths.push _.escapeRegExp(path.join('build', 'Release') + path.sep) + '.*\\.pdb'

     # Hunspell dictionaries are only not needed on OS X.
     if process.platform is 'darwin'


Removing remaining ENOENT throws

Removing remaining ENOENT throws

--- command.coffee
+++ command.coffee
@@ -170,24 +170,24 @@
   prevStats = null
   compileTimeout = null

-  compile = ->
-    clearTimeout compileTimeout
-    compileTimeout = setTimeout ->
-      fs.stat source, (err, stats) ->
-        throw err if err
-        return if prevStats and (stats.size is prevStats.size and
-          stats.mtime.getTime() is prevStats.mtime.getTime())
-        prevStats = stats
-        fs.readFile source, (err, code) ->
-          throw err if err
-          compileScript(source, code.toString(), base)
-    , 25
-
   watchErr = (e) ->
     if e.code is 'ENOENT'
       removeSource source, base, yes
       compileJoin()
     else throw e
+
+  compile = ->
+    clearTimeout compileTimeout
+    compileTimeout = setTimeout ->
+      fs.stat source, (err, stats) ->
+        return watchErr err if err
+        return if prevStats and (stats.size is prevStats.size and
+          stats.mtime.getTime() is prevStats.mtime.getTime())
+        prevStats = stats
+        fs.readFile source, (err, code) ->
+          return watchErr err if err
+          compileScript(source, code.toString(), base)
+    , 25

   try
     watcher = fs.watch source, callback = (event) ->
@@ -240,7 +240,7 @@
     path.exists jsPath, (exists) ->
       if exists
         fs.unlink jsPath, (err) ->
-          throw err if err
+          throw err if err and err.code isnt 'ENOENT'
           timeLog "removed #{source}"

 # Get the corresponding output JavaScript path for a source file.


Using process.stderr in command.coffee (fixes #1798)

--- command.coffee
+++ command.coffee
@@ -17,12 +17,12 @@
 helpers.extend CoffeeScript, new EventEmitter

 printLine = (line) -> process.stdout.write line + '\n'
-printWarn = (line) -> process.binding('stdio').writeError line + '\n'
+printWarn = (line) -> process.stderr.write line + '\n'

 # The help banner that is printed when `coffee` is called without arguments.
 BANNER = '''
   Usage: coffee [options] path/to/script.coffee
-
+
   If called without options, `coffee` will run your script.
          '''


Prettify `watch`

Prettify `watch`

--- command.coffee
+++ command.coffee
@@ -186,43 +186,42 @@
 # time the file is updated. May be used in combination with other options,
 # such as `--print`.
 watch = (source, base) ->
-
-  prevStats = null
+  watcher        = null
+  prevStats      = null
   compileTimeout = null

-  watchErr = (e) ->
-    if e.code is 'ENOENT'
-      return if sources.indexOf(source) is -1
-      try
-        rewatch()
-        compile()
-      catch e
-        removeSource source, base
-        compileJoin()
-    else throw e
+  watchErr = (err) ->
+    throw err unless err.code is 'ENOENT'
+    return unless source in sources
+    try
+      rewatch()
+      compile()
+    catch
+      removeSource source, base
+      compileJoin()

   compile = ->
     clearTimeout compileTimeout
     compileTimeout = wait 25, ->
       fs.stat source, (err, stats) ->
         return watchErr err if err
-        return rewatch() if prevStats and stats.size is prevStats.size and
-          stats.mtime.getTime() is prevStats.mtime.getTime()
+        return rewatch() if prevStats and
+                            stats.size is prevStats.size and
+                            stats.mtime.getTime() is prevStats.mtime.getTime()
         prevStats = stats
         fs.readFile source, (err, code) ->
           return watchErr err if err
           compileScript(source, code.toString(), base)
           rewatch()

-  try
-    watcher = fs.watch source, compile
-  catch e
-    watchErr e
-
   rewatch = ->
     watcher?.close()
     watcher = fs.watch source, compile

+  try
+    watcher = fs.watch source, compile
+  catch err
+    watchErr err

 # Watch a directory of files for new additions.
 watchDir = (source, base) ->


Don't know how it worked before on my local machine...

--- admin/static/coffee/namespaces/replicas.coffee
+++ admin/static/coffee/namespaces/replicas.coffee
@@ -703,8 +703,8 @@

         on_success_off: =>
             data_to_set = @data_cached
-            for dc, value in @model.get('replica_affinities')
-                if not data_to_set['replica_affinities']?
+            for dc, value of @model.get('replica_affinities')
+                if not data_to_set['replica_affinities'][dc]?
                     data_to_set['replica_affinities'][dc] = value

             @model.set data_to_set
@@ -854,8 +854,8 @@

         on_success_pin: =>
             data_to_set = @data_cached
-            for dc, value in @model.get('replica_affinities')
-                if not data_to_set['replica_affinities']?
+            for dc, value of @model.get('replica_affinities')
+                if not data_to_set['replica_affinities'][dc]?
                     data_to_set['replica_affinities'][dc] = value

             @model.set data_to_set


Factored out broadcast logic

Factored out broadcast logic

--- tubular.coffee
+++ tubular.coffee
@@ -1,7 +1,5 @@

 window.tubular = (rootModel, rootTemplate) ->
-  watchList = []
-
   createGetter = (model, path) ->
     if model is undefined
       # always is undefined
@@ -23,6 +21,45 @@
             v = if v is undefined then undefined else v[n]
           v

+  createNotifier = ->
+    list = []
+
+    (callback) ->
+      if typeof callback isnt 'function'
+        # invoke watches and cull ones that request to be removed
+        # for speed, we just rewrite the portion of the list in place every time, eliminating cleared watches
+        # @todo surely, this should be wrapped in try/catch
+        count = list.length
+        index = 0
+        compactedCount = 0
+
+        while index < count
+          listener = list[index]
+          index += 1
+
+          if listener[0]
+            # preserve the listener for next run
+            list[compactedCount] = listener
+            compactedCount += 1
+
+            listener[0]()
+
+        # truncate compacted listener list (preserving any watches that have been added during this run)
+        list.splice(compactedCount, count - compactedCount)
+
+        console.log 'watch count', list.length
+
+      else
+        listener = [ callback ]
+        list.push listener
+
+        # return a handle to be able to unwatch
+        ->
+          # delayed removal for safety
+          listener[0] = null
+
+  modelNotify = createNotifier()
+
   runTemplate = (model, viewPrototype, template, preInitMap) ->
     viewModel =
       fork: (map, subTemplate) ->
@@ -37,25 +74,17 @@
         getter = createGetter model, path
         value = getter()

-        watch = ->
+        clear = modelNotify ->
           # get and compare with cached values
           newValue = getter()
           if newValue isnt value
             value = newValue
             runTemplate value, viewModel, subTemplate

-        watch.isCleared = false
-
-        watchList.push watch
-
         runTemplate value, viewModel, subTemplate

         # return a handle to be able to unwatch
-        {
-          clear: ->
-            # delayed removal for safety
-            watch.isCleared = true
-        }
+        { clear: clear }

       apply: (run) ->
         # fail fast on error
@@ -65,28 +94,7 @@
         # @todo wrap in an try/catch? or just let it bubble up?
         run.call(model)

-        # invoke watches and cull ones that request to be removed
-        # for speed, we just rewrite the portion of the list in place every time, eliminating cleared watches
-        # @todo surely, this should be wrapped in try/catch
-        watchCount = watchList.length
-        watchIndex = 0
-        compactedWatchCount = 0
-
-        while watchIndex < watchCount
-          watch = watchList[watchIndex]
-          watchIndex += 1
-
-          if not watch.isCleared
-            # preserve the watch for next run
-            watchList[compactedWatchCount] = watch
-            compactedWatchCount += 1
-
-            watch()
-
-        # truncate compacted watch list (preserving any watches that have been added during this run)
-        watchList.splice(compactedWatchCount, watchCount - compactedWatchCount)
-
-        console.log 'watch count', watchList.length
+        modelNotify()

     if preInitMap
       viewModel[n] = v for n, v of preInitMap


Client: enable/disable add button according api access state

--- client/admin/lib/views/api/adminapiview.coffee
+++ client/admin/lib/views/api/adminapiview.coffee
@@ -14,20 +14,33 @@

     super options, data

+    @initialAPIAccessState = getGroup().isApiEnabled is yes
     @createSwitch()
     @createTabView()

   createSwitch: ->

-    @addSubView @settingsView = new kd.CustomHTMLView
+    @addSubView settingsView = new kd.CustomHTMLView
       partial: 'Enable API Access'
       cssClass: 'settings-row'

-    @settingsView.addSubView @apiSwitch = new KodingSwitch
-      callback      : (state) ->
-        getGroup().modify { isApiEnabled : state }, (err) ->
-          showError err  if err
+    settingsView.addSubView apiSwitch = new KodingSwitch
+      callback : (state) =>
+        getGroup().modify { isApiEnabled : state }, (err) =>
+          if err
+            showError err
+            # revert switch state in case of error
+            if state
+            then apiSwitch.setOff no
+            else apiSwitch.setOn no
+
+          else
+            if state
+            then @addNewButton.enable()
+            else @addNewButton.disable()
+
+    apiSwitch.setDefaultValue @initialAPIAccessState

   createTabView: ->
@@ -35,7 +48,7 @@
     data    = @getData()
     tabView = new kd.TabView hideHandleCloseIcons: yes

-    tabView.tabHandleContainer.addSubView new kd.ButtonView
+    @addNewButton = tabView.tabHandleContainer.addSubView new kd.ButtonView
       cssClass : 'solid compact green add-new'
       title    : 'Add new API Token'
       callback : =>
@@ -43,6 +56,10 @@
           return showError err  if err
           kd.utils.defer =>
             @apiTokenListView.listController.addItem apiToken
+
+    if @initialAPIAccessState
+    then @addNewButton.enable()
+    else @addNewButton.disable()

     tabView.addPane apiTokens = new kd.TabPaneView name: 'API Tokens'

@@ -56,10 +73,3 @@
     @addSubView tabView

-  viewAppended: ->
-
-    super
-    status = getGroup().isApiEnabled is yes
-    @apiSwitch.setDefaultValue status
-
-


Client: use toggleButtonState helper

Client: use toggleButtonState helper

--- client/admin/lib/views/api/adminapiview.coffee
+++ client/admin/lib/views/api/adminapiview.coffee
@@ -36,9 +36,7 @@
             else apiSwitch.setOn no

           else
-            if state
-            then @addNewButton.enable()
-            else @addNewButton.disable()
+            toggleButtonState @addNewButton, state

     apiSwitch.setDefaultValue @initialAPIAccessState

@@ -57,9 +55,7 @@
           kd.utils.defer =>
             @apiTokenListView.listController.addItem apiToken

-    if @initialAPIAccessState
-    then @addNewButton.enable()
-    else @addNewButton.disable()
+    toggleButtonState @addNewButton, @initialAPIAccessState

     tabView.addPane apiTokens = new kd.TabPaneView name: 'API Tokens'

@@ -73,3 +69,8 @@
     @addSubView tabView

+toggleButtonState = (button, state) ->
+
+  if state
+  then button.enable()
+  else button.disable()


default-apps: Added forceUpdate, authorNickname and fileTypes to default app manifest.

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -607,6 +607,7 @@
       multiple      : no
       background    : no
       hiddenHandle  : no
+      forceUpdate   : no
       openWith      : "lastActive"
       behavior      : "application"
       version       : "0.1"
@@ -615,6 +616,7 @@
       path          : "~/Applications/#{name or type.capitalize()}.kdapp"
       homepage      : "#{profile.nickname}.koding.com/#{__utils.slugify name or type}"
       author        : "#{fullName}"
+      authorNick    : "#{profile.nickname}"
       repository    : "git://github.com/#{profile.nickname}/#{__utils.slugify name or type}.kdapp.git"
       description   : "#{name or type} : a Koding application created with the #{type} template."
       category      : "web-app" # can be web-app, add-on, server-stack, framework, misc
@@ -629,7 +631,8 @@
         type        : "tab"
       icns          :
         "128"       : "./resources/icon.128.png"
-      menu: []
+      menu          : []
+      fileTypes     : []

     json = JSON.stringify raw, null, 2