tab className attribute becomes tabClassName

--- src/components/course-settings/roster.cjsx
+++ src/components/course-settings/roster.cjsx
@@ -62,7 +62,7 @@
           {period.name}
         </BS.OverlayTrigger>

-      <BS.Tab key={period.id} eventKey={index} title={name} className={className}>
+      <BS.Tab key={period.id} eventKey={index} title={name} tabClassName={className}>
         <PeriodRoster
         period={period}
         courseId={@props.courseId}


:smile:

:smile:

--- src/match-iterator.coffee
+++ src/match-iterator.coffee
@@ -4,7 +4,7 @@
 class SingleLineSearchCallbackArgument
   lineTextOffset: 0

-  Object.defineProperty SingleLineSearchCallbackArgument.prototype, 'range',
+  Object.defineProperty this.prototype, 'range',
     get: ->
       @computedRange ?= Range(
         Point(@row, @lineOffset + @match.index),
@@ -13,7 +13,7 @@

     set: (@computedRange) ->

-  Object.defineProperty SingleLineSearchCallbackArgument.prototype, 'lineText',
+  Object.defineProperty this.prototype, 'lineText',
     get: -> @buffer.lineForRow(@row)

   constructor: (@buffer, @row, @match, @lineOffset) ->
@@ -101,7 +101,7 @@
 class MultiLineSearchCallbackArgument
   lineTextOffset: 0

-  Object.defineProperty SingleLineSearchCallbackArgument.prototype, 'range',
+  Object.defineProperty this.prototype, 'range',
     get: ->
       return @computedRange if @computedRange?

@@ -116,7 +116,7 @@
     set: (range) ->
       @computedRange = range

-  Object.defineProperty SingleLineSearchCallbackArgument.prototype, 'lineText',
+  Object.defineProperty this.prototype, 'lineText',
     get: -> @buffer.lineForRow(@range.start.row)

   constructor: (@buffer, @match, @lengthDelta) ->


revert -> checkoutHead

revert -> checkoutHead

--- src/git.coffee
+++ src/git.coffee
@@ -100,7 +100,7 @@

     fileName = basename(filePath)

-    revert = =>
+    checkoutHead = =>
       editor.buffer.reload() if editor.buffer.isModified()
       @checkoutHead(filePath)

@@ -109,10 +109,10 @@
         message: 'Confirm Discard Changes'
         detailedMessage: "Are you sure you want to discard all changes to \"#{fileName}\"?"
         buttons:
-          OK: revert
+          OK: checkoutHead
           Cancel: null
     else
-      revert()
+      checkoutHead()

   # Public: Destroy this `Git` object. This destroys any tasks and subscriptions
   # and releases the underlying libgit2 repository handle.


wait for parse to finish before resolving load promise for xhtml files

--- scripts/gh-book/xhtml-file.coffee
+++ scripts/gh-book/xhtml-file.coffee
@@ -123,7 +123,7 @@
     # This promise is resolved once the file is parsed so we know which images to load
     _imagesLoaded: new $.Deferred()
     _loadComplex: (originalPromise) ->
-      return @_imagesLoaded
+      return $.when(@_imagesLoaded, originalPromise)

     parse: (json) ->
       html = json.content


Remove template

Remove template

--- examples/app/source/template/session.coffee
+++ examples/app/source/template/session.coffee
@@ -1,15 +0,0 @@
-# session = new Atoms.Template.Session
-#   parent    : document.body
-#   title     : "Atoms App"
-#   logo      : "http://cdn.tapquo.com/photos/soyjavi.jpg"
-#   copyright : "Tapquo S.L. 2013"
-#   inputs: [
-#     input: placeholder: "Nickname...", value: "@soyjavi", name: "nickname"
-#   ,
-#     input: name: "pass"
-#   ]
-#   buttons: [
-#     button: style: "cancel", text: "Log in"
-#   ]
-
-# session.bind "validate", -> console.log "argu", arguments


Polyfill method should be versatile

Polyfill method should be versatile

--- js/outer.coffee
+++ js/outer.coffee
@@ -15,10 +15,7 @@
       # Sammy eats hashes, so we need to reapply it to land at the right anchor on the page
       @scroll(window.location.hash)

-      # Apply polyfill(s) if they exists
-      @polyfill() if @polyfill?
-
-      # Fetch libraries endemic to page
+      # Fetch page-specific libraries
       @library() if @library?

     load: ->
@@ -49,9 +46,9 @@
   # Docs
   article = new Doc("article", "Sample template for an Article")

-  # Per-Page Polyfills
-  about.polyfill = placeholder
-  home.polyfill = placeholder
+  # Per-Page Libs
+  home.library = placeholder
+  about.library = placeholder

   placeholder = ->
     if !Modernizr.input.placeholder


Update programmer-excuses.coffee

Update programmer-excuses.coffee

--- scripts/programmer-excuses.coffee
+++ scripts/programmer-excuses.coffee
@@ -27,6 +27,6 @@
       msg.send dialog

 module.exports = (robot) ->
-  robot.hear /bug/i, action
-  robot.hear /excuse/i, action
-  robot.hear /what (is|was) the (problem|bug|issue)/i, action+  robot.hear /\sbug\s/i, action
+  robot.hear /\sexcuse\s/i, action
+  robot.hear /what (is|was) the (problem|bug|issue)/i, action


log connected db

log connected db

--- src/model/server.coffee
+++ src/model/server.coffee
@@ -5,6 +5,7 @@
 ObjectID = mongo.ObjectID
 db = undefined

+console.log("Conecting to #{config.db_uri}")
 mongo.connect config.db_uri, (err, database) ->
     throw err if err?
     db = database


Remove duplicated shutdown

Remove duplicated shutdown

--- src/RemoteSubGraph.coffee
+++ src/RemoteSubGraph.coffee
@@ -25,6 +25,7 @@

   shutdown: ->
     @runtime.stop()
+    @runtime.disconnect()
     super()

   setDefinition: (definition) ->
@@ -100,9 +101,6 @@
     port = @outPorts[name]
     port.send packet.payload

-  shutdown: ->
-    @runtime.disconnect()
-
 exports.RemoteSubGraph = RemoteSubGraph
 exports.getComponent = (metadata) -> new RemoteSubGraph metadata
 exports.getComponentForRuntime = (runtime, baseDir) ->


Client: remove unused variable

Client: remove unused variable

--- client/admin/lib/views/api/apitokenlist.coffee
+++ client/admin/lib/views/api/apitokenlist.coffee
@@ -17,8 +17,6 @@

   deleteItem: (item) ->

-    stack = item.getData()
-
     overlay = new KDOverlayView cssClass: 'second-overlay'

     modal   = KDModalView.confirm


app-updates: We're forcing app updates just for forceUpdate enabled apps.

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -223,7 +223,7 @@
       warn "AppManager doesn't know what to run, no options passed!"
       return

-    if @isAppUpdateAvailable(manifest.name, manifest.version) and not manifest.devMode and not @skipUpdate
+    if @isAppUpdateAvailable(manifest.name, manifest.version) and not manifest.devMode and manifest.forceUpdate
       @showUpdateRequiredModal manifest
       return callback?()

@@ -620,6 +620,7 @@
       multiple      : no
       background    : no
       hiddenHandle  : no
+      forceUpdate   : no
       openWith      : "lastActive"
       behavior      : "application"
       version       : "0.1"
@@ -628,6 +629,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
@@ -642,7 +644,8 @@
         type        : "tab"
       icns          :
         "128"       : "./resources/icon.128.png"
-      menu: []
+      menu          : []
+      fileTypes     : []

     json = JSON.stringify raw, null, 2


fixed for mr notes.

fixed for mr notes.

--- client/app/MainApp/maintabs/maintabview.coffee
+++ client/app/MainApp/maintabs/maintabview.coffee
@@ -25,6 +25,12 @@

     mainViewController = KD.getSingleton "mainViewController"
     mainViewController.on "UILayoutNeedsToChange", @bound "changeLayout"
+
+    @on "PaneDidShow", @bound "focusActiveSubPane"
+
+  focusActiveSubPane: (pane)->
+    {tabView} = pane.getMainView()
+    tabView.getActivePane().getHandle().$().click()  if tabView

   changeLayout:(options)->


maintabs: broken (white) activity tab fixed / custom titles for tabhandles fixed

--- client/app/MainApp/maintabs/maintabview.coffee
+++ client/app/MainApp/maintabs/maintabview.coffee
@@ -117,8 +117,9 @@
     # this is a temporary hack
     # for reviving the main tabs
     # a better solution tbdl - SY
-    o.domId         = "maintabpane-#{@utils.slugify options.name}"
-    o.domId        += "-#{@utils.getRandomNumber()}" if document.getElementById o.domId
+    if document.getElementById o.domId
+      o.domId         = "maintabpane-#{@utils.slugify options.name}"
+
     o.name          = options.name
     o.behavior      = options.behavior
     o.hiddenHandle  = options.hiddenHandle
@@ -127,8 +128,9 @@

     paneInstance.once "viewAppended", =>
       @applicationPaneReady paneInstance, mainView
-      if options.appInfo?.title?
-        paneInstance.setTitle options.appInfo.title
+      appController = appManager.getByView mainView
+      {appInfo}     = appController.getOptions()
+      paneInstance.setTitle appInfo.title  if appInfo?.title

     @addPane paneInstance


client: sidebar: removed legacy deps/methods

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -6,8 +6,6 @@
 KDReactorMixin                 = require 'app/flux/base/reactormixin'
 SidebarNoStacks                = require 'app/components/sidebarstacksection/sidebarnostacks'
 SidebarStackSection            = require 'app/components/sidebarstacksection'
-SidebarChannelsSection         = require 'app/components/sidebarchannelssection'
-SidebarMessagesSection         = require 'app/components/sidebarmessagessection'
 SidebarStackHeaderSection      = require 'app/components/sidebarstacksection/sidebarstackheadersection'
 SidebarSharedMachinesSection   = require 'app/components/sidebarsharedmachinessection'
 SharingMachineInvitationWidget = require 'app/components/sidebarmachineslistitem/sharingmachineinvitationwidget'
@@ -159,16 +157,8 @@
       machines={machines}/>

-  renderChannels: ->
-    <SidebarChannelsSection
-      selectedId={@state.selectedThreadId}
-      threads={@state.publicChannels} />

-  renderMessages: ->
-    <SidebarMessagesSection
-      selectedId={@state.selectedThreadId}
-      threads={@state.privateChannels} />

   renderLogo: ->


Environment: Change variable names

Environment: Change variable names

--- client/app/lib/flux/environment/actions.coffee
+++ client/app/lib/flux/environment/actions.coffee
@@ -211,19 +211,19 @@
       resolve()

-setSelectedWorkspaceId = (id) ->
-
-  kd.singletons.reactor.dispatch actions.WORKSPACE_SELECTED, id
-
-
-showDeleteWorkspaceWidget = (id) ->
-
-  kd.singletons.reactor.dispatch actions.SHOW_DELETE_WORKSPACE_WIDGET, id
-
-
-hideDeleteWorkspaceWidget = (id) ->
-
-  kd.singletons.reactor.dispatch actions.HIDE_DELETE_WORKSPACE_WIDGET, id
+setSelectedWorkspaceId = (workspaceId) ->
+
+  kd.singletons.reactor.dispatch actions.WORKSPACE_SELECTED, workspaceId
+
+
+showDeleteWorkspaceWidget = (workspaceId) ->
+
+  kd.singletons.reactor.dispatch actions.SHOW_DELETE_WORKSPACE_WIDGET, workspaceId
+
+
+hideDeleteWorkspaceWidget = (workspaceId) ->
+
+  kd.singletons.reactor.dispatch actions.HIDE_DELETE_WORKSPACE_WIDGET, workspaceId

 module.exports = {


Fixed: typeahead was disabled after adding a contact

--- client/app/views/popover_screens/guests.coffee
+++ client/app/views/popover_screens/guests.coffee
@@ -12,11 +12,6 @@
         "click .add-new-guest": "onNewGuest"
         "click .guest-delete": "onRemoveGuest"
         'keyup input[name="guest-name"]': "onKeyup"
-
-
-    # Flag used in order to prevent the typeahead from being reinitialized at
-    # each render.
-    _configured: false

     getRenderData: ->
@@ -55,39 +50,36 @@

     # Configure the auto-complete on contacts.
     configureGuestTypeahead: ->
-        # If it's already configured, no need to do it again.
-        unless @_configured
-            @_configured = true
-            @$('input[name="guest-name"]').typeahead
-                source: app.contacts.asTypeaheadSource()
-                matcher: (contact) ->
-                    old = $.fn.typeahead.Constructor::matcher
-                    return old.call this, contact.display
-                sorter: (contacts) ->
-                    beginswith = []
-                    caseSensitive = []
-                    caseInsensitive = []
+        @$('input[name="guest-name"]').typeahead
+            source: app.contacts.asTypeaheadSource()
+            matcher: (contact) ->
+                old = $.fn.typeahead.Constructor::matcher
+                return old.call this, contact.display
+            sorter: (contacts) ->
+                beginswith = []
+                caseSensitive = []
+                caseInsensitive = []

-                    while (contact = contacts.shift())
-                        item = contact.display
-                        if not item.toLowerCase().indexOf(this.query.toLowerCase())
-                            beginswith.push contact
-                        else if ~item.indexOf this.query
-                            caseSensitive.push contact
-                        else caseInsensitive.push contact
+                while (contact = contacts.shift())
+                    item = contact.display
+                    if not item.toLowerCase().indexOf(this.query.toLowerCase())
+                        beginswith.push contact
+                    else if ~item.indexOf this.query
+                        caseSensitive.push contact
+                    else caseInsensitive.push contact

-                    return beginswith.concat caseSensitive, caseInsensitive
+                return beginswith.concat caseSensitive, caseInsensitive

-                highlighter: (contact) ->
-                    old = $.fn.typeahead.Constructor::highlighter
-                    imgPath = if contact.hasPicture
-                        "contacts/#{contact.id}.jpg"
-                    else
-                        "img/defaultpicture.png"
-                    img = ' '
-                    return img + old.call this, contact.display
+            highlighter: (contact) ->
+                old = $.fn.typeahead.Constructor::highlighter
+                imgPath = if contact.hasPicture
+                    "contacts/#{contact.id}.jpg"
+                else
+                    "img/defaultpicture.png"
+                img = ' '
+                return img + old.call this, contact.display

-                updater: @onNewGuest.bind(@)
+            updater: @onNewGuest.bind(@)

     onRemoveGuest: (event) ->


Fix recovery token route

Fix recovery token route

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -163,7 +163,8 @@
       '/:name?/Activity/:activitySlug'  : goToContent.Activity
       '/:name?/Apps/:appSlug'           : goToContent.Apps

-      '/recover/:recoveryToken': ({params:{recoveryToken}})->
+      '/:name?/Recover/:recoveryToken': ({params:{recoveryToken}})->
+        return if recoveryToken is 'Password'
         mainController.appReady =>
           # TODO: DRY this one
           $('body').addClass 'login'
@@ -181,9 +182,8 @@
                   """
             else
               {loginScreen} = mainController
-              loginScreen.resetForm.addCustomData {recoveryToken}
-              loginScreen.animateToForm "reset"
-            # @utils.defer => @clear()
+              loginScreen.headBannerShowRecovery recoveryToken
+            @clear()

       '/invitation/:inviteToken': ({params:{inviteToken}})->
         inviteToken = decodeURIComponent inviteToken


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -6,7 +6,7 @@
     height: 32

   # This is kind of gross, but we need to make sure
-  #
+  # App is around
   App ||= {}
   App.width ||= 480
   App.height ||= 320


Added a failing test for #3

Added a failing test for #3

--- test/wordnet_test.coffee
+++ test/wordnet_test.coffee
@@ -1,6 +1,8 @@
 chai = require('chai')
 chai.use(require('chai-as-promised'))
 should = chai.should()
+
+async = require('async')

 Wordnet = require('../lib/wordnet')

@@ -145,6 +147,12 @@
       wordnet.validForms 'find#v', (results) ->
         should.exist(results)
         results.should.eql(['find#v'])
+        done()
+
+    it 'should succeed for are#v', (done) ->
+      wordnet.validForms 'are#v', (results) ->
+        should.exist(results)
+        results.should.eql(['be#v'])
         done()

     it 'should succeed for repeated queries', (done) ->
@@ -156,6 +164,16 @@
           results.should.eql(['farther#r', 'far#r'])
           done()

+    it 'should succeed for a set of queries pushed asynchronously', (done) ->
+      query = (item, callback) ->
+        wordnet.validForms item, (results) ->
+          callback null, results
+
+      async.map ['be#v', 'are#v'], query, (err, results) ->
+        should.exist(results)
+        results.should.deep.eql([['be#v'], ['be#v']])
+        done()
+

   describe 'validFormsAsync', () ->


Updating help message

Updating help message

--- src/cli/messages.coffee
+++ src/cli/messages.coffee
@@ -3,13 +3,15 @@
 module.exports = class

   @helpCommands: ->
-    echo 'Type --help to see other available commands'
+    echo 'Type --help to see the options available'

   @fileNotFound: (filepath) =>
     echo ".authrc file not found".red
     echo "Be sure the file exists in: #{filepath}"
-    echo 'You can pass the --path argument with your file path'
-    echo 'Use the "create" command to create a new .authrc file'
+    echo ''
+    echo 'You can pass file path using the --path argument'
+    echo 'Or use the global file using the --global argument'
+    echo 'Aditionally you can use the "create" command to create a new file'
     echo ''
     @helpCommands()


fix `Cannot read property '1' of null` when an issue has more than one label

--- src/modules/issues.coffee
+++ src/modules/issues.coffee
@@ -45,9 +45,14 @@
                 { labels, number } = issue
                 number ?= '?'
                 return false unless labels
-                switch ( {} for { name } in labels when name and regex.test(name) ).length
+                # Find size labels only
+                size_labels = _.filter(labels, (label) ->
+                    regex.test label.name
+                )
+                switch size_labels.length
                     when 0 then false
                     when 1
+                        name = size_labels[0].name
                         # Provide the size attribute on the issue.
                         total += issue.size = parseInt name.match(regex)[1]
                         true


Add View::parentView() (use instead of view.parent_view)

--- src/marbles/view.js.coffee
+++ src/marbles/view.js.coffee
@@ -34,7 +34,7 @@
     @trackInstance()
     @initTemplates()

-    for k in ['el', 'parent_view', 'container', 'render_method']
+    for k in ['el', 'parent_view', '_parent_view_cid', 'container', 'render_method']
       @set(k, options[k]) if options[k]

     @on 'ready', @bindViews
@@ -67,7 +67,7 @@
           _init = false
           el.view_cids ?= {}
           unless (_view_cid = el.view_cids[view_class_name]) && (view = viewClass.instances.all[_view_cid])
-            view = new viewClass el: el, parent_view: @
+            view = new viewClass el: el, parent_view: @, _parent_view_cid: @cid
             _init = true
           @_child_views[view_class_name] ?= []
           @_child_views[view_class_name].push view.cid
@@ -90,13 +90,16 @@
   childViews: (view_class_name) =>
     _.map @_child_views[view_class_name], (cid) => @constructor.find(cid)

+  parentView: =>
+    Marbles.View.find(@_parent_view_cid)
+
   findParentView: (view_name) =>
     key = "_parent_#{view_name}_cid"
     return view if @[key] && (view = Marbles.View.find(@[key]))

     view = @
     while view && view.constructor.view_name != view_name
-      view = view.parent_view
+      view = view.parentView()
     @[key] = view.cid if view
     view


Added news

Added news

--- server/materials/data/news.coffee
+++ server/materials/data/news.coffee
@@ -3,24 +3,19 @@

 export default allNews = () ->
     return news([
-        newsItem("Возможны задержки в тестировании решений", """
+        newsItem("Про очные занятия в новом учебном году", String.raw"""
+
Я очень надеюсь, что в новом учебном году будут очные занятия (как всегда, для нижегородских школьников), но на данный момент, насколько я понимаю, никаких официальных разъяснений насчет проведения дополнительных занятий нет (основные школьные занятия вроде как начнутся 1 сентября, но наши занятия — это дополнительные занятия, про них не понятно). Вы можете, как всегда, заниматься через сайт откуда угодно (из дома, из школы и т.д.), но очные занятия начнутся скорее всего не раньше, чем Роспотребнадзор официально разрешит проведение дополнительных занятий; и даже если это разрешение будет в сентябре, наши занятия скорее всего не начнутся раньше 1 октября.

+
При этом я готов проводить онлайн-занятия через zoom или другие сервисы видеоконференций, если будут желающие участвовать в таких занятиях. Формат занятий будет как всегда — никаких лекций, вы сами решаете задачи, просто вам будет легче меня спросить о чем-нибудь. Пишите, если вы хотите участвовать в таком занятии, сразу указывайте время, когда вам удобнее. Думаю, если такие занятия будут, то участвовать в них смогут все желающие (не только нижегородские школьники), хотя я буду отдавать приоритет нижегородским школьникам (в первую очередь в плане выбора удобного времени).

+
Также я планирую организовывать онлайн-занятия для старших уровней, это особо важно с учетом того, что олимпиадный сезон никто не отменял :) Занятия будут устроены аналогично тому, как были устроены такие занятия в прошлом году в ННГУ — я буду готовить некоторый контест на codeforces, вы его будете решать (в виртуальном режиме, контест будет доступен несколько дней), после чего мы будет в онлайн-режиме обсуждать и разбирать задачи и обсуждать разные олимпиадные новости. Время такого обсуждения можно выбрать; пишите, если вы планируете участвовать и у вас есть пожелания по времени. Это занятия тоже будут доступны для всех желающих, хотя если ваш уровень ниже чем примерно 2В или 3А, то скорее всего вам будет сложно; и опять-таки приоритет будет отдаваться сильным нижегородским школьникам.

+        """),
+        newsItem("Возможны задержки в тестировании решений", String.raw"""
             На информатиксе проходят технические работы, возможна задержка решений в статусе "Отправка" или "Тестируется".
         """),
-        newsItem("Добавлена возможность редактирования профиля", """
+        newsItem("Добавлена возможность редактирования профиля", String.raw"""
             Добавлена возможность редактировать профиль (имя, аккаунт на codeforces, класс и т.д.) Пожалуйста, указывайте свои настоящие данные,
             а то придется возможность редактирования отключить.
         """),
-        newsItem("Переход на новый учебный год и деактивация аккаунтов", String.raw"""
-            В связи с переходом на новый учебный год я деактивировал почти все аккаунты. Если вы планируете продолжать заниматься, напишите мне, я активирую ваш аккаунт. Если вы школьник,
-            то заодно напишите мне, в какой класс и какой школы вы перешли.
-        """),
-        newsItem("Стажировка на алгопроге", String.raw"""
-            Как и в прошлом году, этим летом вы можете «постажироваться» на алгопроге, дописав алгопрогу новую функциональность. Подробнее
-        """),
-        newsItem("Про очные занятия в этом учебном году и летом", String.raw"""
-            В этом учебном году очных занятий больше не будет. Возможно, будет несколько занятий летом для желающих, следите за объявлениями на сайте. Очные занятия в следующем учебном году начнутся в середине сентября (если не продлят карантин), следите за объявлениями.
-        """),
-        newsItem("Опрос про алгопрог (обновлен!)", String.raw"""
+        newsItem("Опрос про алгопрог", String.raw"""
             Ответьте, пожалуйста, на несколько вопросов. Тем, кто уже отвечал — я там добавил несколько вопросов, можете ответить еще раз.
         """),
     ])


[quickfix] pass box to client's toast

[quickfix] pass box to client's toast

--- server/models/mailbox_imap.coffee
+++ server/models/mailbox_imap.coffee
@@ -73,7 +73,7 @@

         return ops

-    .tap (ops) ->
+    .tap (ops) =>

         nbTasks = ops.toFetch.length +
                 ops.toRemove.length +


Include default field_type:repeating_group on RepeatingGroup models

--- src/form_components/repeating_group.coffee
+++ src/form_components/repeating_group.coffee
@@ -1,5 +1,6 @@
 FormRenderer.Models.ResponseFieldRepeatingGroup = FormRenderer.Models.BaseFormComponent.extend
   group: true
+  field_type: 'repeating_group'

   initialize: ->
     FormRenderer.Models.BaseFormComponent::initialize.apply @, arguments


Fixes #545: defaultChannelConfig.name sparse index

--- src/model/mediators.coffee
+++ src/model/mediators.coffee
@@ -26,6 +26,8 @@
   "_configModifiedTS":      Date
   "_uptime":                Number
   "_lastHeartbeat":         Date
+
+MediatorSchema.index "defaultChannelConfig.name", sparse: true

 # Model for describing a collection of mediators that have registered themselves with core
 exports.Mediator = connectionDefault.model 'Mediator', MediatorSchema


Tooltip needs to correct for scroll position

--- src/modules/tooltip.coffee
+++ src/modules/tooltip.coffee
@@ -54,6 +54,7 @@
       # Place tooltip in middle of editor viewport
       left = @quill.container.offsetWidth/2 - @container.offsetWidth/2
       top = @quill.container.offsetHeight/2 - @container.offsetHeight/2
+    top += @quill.container.scrollTop
     return [left, top]

   show: (reference) ->


Fix order of extract_data_from_query

Fix order of extract_data_from_query

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -391,14 +391,18 @@
                     if previous_char of @matching_opening_bracket
                         next_char = @get_next_char()
                         if next_char is @matching_opening_bracket[previous_char]
-                            @remove_next()
-                            return true
+                            num_not_closed_bracket = @count_not_closed_brackets previous_char
+                            if num_not_closed_bracket <= 0
+                                @remove_next()
+                                return true
                     # If the user remove the first quote of an empty string, we remove both quotes
                     else if previous_char is '"' or previous_char is "'"
                         next_char = @get_next_char()
                         if next_char is previous_char and @get_previous_char(2) isnt '\\'
-                            @remove_next()
-                            return true
+                            num_quote = @count_char char_to_insert
+                            if num_quote%2 is 0
+                                @remove_next()
+                                return true
                     return true

                 char_to_insert = String.fromCharCode event.which
@@ -447,7 +451,7 @@
                                     @move_cursor 1
                                     event.preventDefault()
                                 return true
-            return true
+            return false

         get_next_char: =>
             cursor_end = @codemirror.getCursor()
@@ -723,10 +727,12 @@
                         query_after_cursor += query_lines[i] + '\n'
                     else
                         query_after_cursor += query_lines[i]
-            @query_last_part = query_after_cursor

             # Initialize @current_element, which tracks what the user typed before they hit TAB (to auto-complete).
             # Tracking this helps us let the user loop over all the suggestions available for the fragment they typed (and go back to the fragment).
+            # We now compute this stack earlier, because we need it for @pair_char
+            # BUT we don't want to save data too early
+            previous_element = @current_element
             @current_element = ''

             stack = @extract_data_from_query
@@ -734,6 +740,51 @@
                 position: 0

             @pair_char(event, stack)
+
+            current_element = @current_element
+            @current_element = previous_element
+
+            # If there is a hilighted suggestion, we want to catch enter
+            if @$('.suggestion_name_li_hl').length > 0
+                if event?.which is 13
+                    event.preventDefault()
+                    @handle_keypress()
+                    return true
+
+            # We are scrolling in history
+            if @history_displayed_id isnt 0 and event?
+                # We catch ctrl, shift, alt,
+                if event.ctrlKey or event.shiftKey or event.altKey or event.which is 16 or event.which is 17 or event.which is 18 or event.which is 20 or event.which is 91 or event.which is 92 or event.type of @mouse_type_event
+                    return false
+
+            # Avoid arrows+home+end+page down+pageup
+            # if event? and (event.which is 24 or event.which is ..)
+            # 0 is for firefox...
+            if not event? or (event.which isnt 37 and event.which isnt 38 and event.which isnt 39 and event.which isnt 40 and event.which isnt 33 and event.which isnt 34 and event.which isnt 35 and event.which isnt 36 and event.which isnt 0)
+                @history_displayed_id = 0
+                @draft = @codemirror.getValue()
+
+            if event?.which isnt 9 # has to be before create_suggestion()
+                @cursor_for_auto_completion = @codemirror.getCursor()
+            # We just look at key up so we don't fire the call 3 times
+            if event?.type? and event.type isnt 'keyup' and event.which isnt 9 and event.type isnt 'mouseup'
+                return false
+            if event?.which is 16 # We don't do anything with Shift.
+                return false
+
+            # Tab is an exception, we let it pass (tab bring back suggestions) - What we want is to catch keydown
+            if @ignore_tab_keyup is true and event?.which is 9
+                if event.type is 'keyup'
+                    @ignore_tab_keyup = false
+                return true
+
+            @current_highlighted_suggestion = -1
+            @.$('.suggestion_name_list').empty()
+
+            # Valid step, let's save the data
+            @query_last_part = query_after_cursor
+            @current_element = current_element
+
             # If a selection is active, we just catch shift+enter
             if @codemirror.getSelection() isnt ''
                 @hide_suggestion_and_description()
@@ -748,43 +799,6 @@
                     return false
                 else
                     return true
-
-            # If there is a hilighted suggestion, we want to catch enter
-            if @$('.suggestion_name_li_hl').length > 0
-                if event?.which is 13
-                    event.preventDefault()
-                    @handle_keypress()
-                    return true
-
-            # We are scrolling in history
-            if @history_displayed_id isnt 0 and event?
-                # We catch ctrl, shift, alt,
-                if event.ctrlKey or event.shiftKey or event.altKey or event.which is 16 or event.which is 17 or event.which is 18 or event.which is 20 or event.which is 91 or event.which is 92 or event.type of @mouse_type_event
-                    return false
-
-            # Avoid arrows+home+end+page down+pageup
-            # if event? and (event.which is 24 or event.which is ..)
-            # 0 is for firefox...
-            if not event? or (event.which isnt 37 and event.which isnt 38 and event.which isnt 39 and event.which isnt 40 and event.which isnt 33 and event.which isnt 34 and event.which isnt 35 and event.which isnt 36 and event.which isnt 0)
-                @history_displayed_id = 0
-                @draft = @codemirror.getValue()
-
-            if event?.which isnt 9 # has to be before create_suggestion()
-                @cursor_for_auto_completion = @codemirror.getCursor()
-            # We just look at key up so we don't fire the call 3 times
-            if event?.type? and event.type isnt 'keyup' and event.which isnt 9 and event.type isnt 'mouseup'
-                return false
-            if event?.which is 16 # We don't do anything with Shift.
-                return false
-
-            # Tab is an exception, we let it pass (tab bring back suggestions) - What we want is to catch keydown
-            if @ignore_tab_keyup is true and event?.which is 9
-                if event.type is 'keyup'
-                    @ignore_tab_keyup = false
-                return true
-
-            @current_highlighted_suggestion = -1
-            @.$('.suggestion_name_list').empty()

             result =
                 status: null


adding course name, switching to BS Nav

adding course name, switching to BS Nav

--- src/components/nav.cjsx
+++ src/components/nav.cjsx
@@ -2,7 +2,8 @@
 BS = require 'react-bootstrap'
 Router = require 'react-router'

-{CurrentUserActions} = require '../flux/current-user'
+{CurrentUserActions, CurrentUserStore} = require '../flux/current-user'
+{CourseStore} = require '../flux/course'

 module.exports = React.createClass
   displayName: 'Navigation'
@@ -10,40 +11,61 @@
   contextTypes:
     router: React.PropTypes.func

+  getInitialState: ->
+    course: undefined
+
+  getCourseAsStudent: ->
+    {courseId} = @context.router.getCurrentParams()
+    course = CourseStore.get(courseId)
+
+  componentWillMount: ->
+    course = @getCourseAsStudent()
+    @setState({course}) if course
+
+  componentWillReceiveProps: ->
+    course = @getCourseAsStudent()
+    @setState({course}) if course
+
   logout: -> CurrentUserActions.logout()

+  renderCourseLink: (course) ->
+    courseId = course.id
+
+    <Router.Link to='viewStudentDashboard' params={{courseId}} className='navbar-brand'>
+      {course.name}
+    </Router.Link>
+
+  renderCourseItems: (course) ->
+    courseId = course.id
+
+    items = [
+      <BS.MenuItem
+      href={@context.router.makeHref('viewStudentDashboard', {courseId})}
+      eventKey={2}>Dashboard</BS.MenuItem>
+      <BS.MenuItem
+        href={@context.router.makeHref('viewGuide', {courseId})}
+        eventKey={3}>Learning Guide</BS.MenuItem>
+      <BS.MenuItem divider />
+    ]
+
   render: ->
-

-

-

-          <button
-              type='button'
-              className='navbar-toggle collapsed'
-              data-toggle='collapse'
-              data-target='#ui-navbar-collapse'>
-            Toggle navigation
-
-
-
-          </button>

-          <Router.Link to='dashboard' className='navbar-brand'>
-
-          </Router.Link>
+    name = 'Pretend Name'

-

+    brand = <Router.Link to='dashboard' className='navbar-brand'>
+
+            </Router.Link>

-

-

-

-              <Router.Link to='dashboard'>Dashboard</Router.Link>
-

-

-

-

-              <BS.Button bsStyle='link' onClick={@logout}>Sign out!</BS.Button>
-

-

-

-

-

+    if @state.course?
+      course = @renderCourseLink(@state.course)
+      courseItems = @renderCourseItems(@state.course)
+
+    <BS.Navbar brand={brand} fixedTop fluid>
+      {course}
+      <BS.Nav right>
+        <BS.DropdownButton eventKey={1} title={name}>
+          {courseItems}
+          <BS.MenuItem eventKey={4} onClick={@logout}>Sign Out!</BS.MenuItem>
+        </BS.DropdownButton>
+      </BS.Nav>
+    </BS.Navbar>


"fix" the JLine fork issue

"fix" the JLine fork issue

--- src/jvm.coffee
+++ src/jvm.coffee
@@ -30,7 +30,8 @@
   'os.name':'doppio', 'os.arch': 'js', 'os.version': '0',
   'java.awt.headless': (not node?).toString(),  # true if we're using the console frontend
   'java.awt.graphicsenv': 'classes.awt.CanvasGraphicsEnvironment',
-  'useJavaUtilZip': 'true'  # hack for sun6javac, avoid ZipFileIndex shenanigans
+  'useJavaUtilZip': 'true',  # hack for sun6javac, avoid ZipFileIndex shenanigans
+  'jline.terminal': 'jline.UnsupportedTerminal' # we can't shell out to `stty`
 }

 root.read_classfile = (cls, cb, failure_cb) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -10,6 +10,7 @@
     height: 17
     x: 15
     y: 30
+  s

   # hold the left arrow key, then
   # update the player


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -1,6 +1,6 @@
 ###*
 The Controllable module adds simple movement
-when up, down
+when up, down, left, right ar

   # create a player and include ClampBounds


modified

modified

--- src/jsonsina.coffee
+++ src/jsonsina.coffee
@@ -38,7 +38,7 @@
     json: false
     forever:true
     timeout: 7000
-    maxAttempts: 5  #// (default) try 5 times
+    maxAttempts: 9  #// (default) try 5 times
     retryDelay: 1000  #// (default) wait for 5s before trying again
     retryStrategy: myRetryStrategy


fix iteration dates

fix iteration dates

--- src/data/pivotal.coffee
+++ src/data/pivotal.coffee
@@ -62,7 +62,7 @@
       curatedIterations = _.map iterations, (iteration)=>
         iteration.start = new Date(iteration.start)
         iteration.finish = new Date(iteration.finish)
-        stories: _.map iteration.stories, (story)=>
+        iteration.stories = _.map iteration.stories, (story)=>
           if _.contains @inProgressStoryTypes, story.current_state
             promises.push @_queryPivotal("projects/#{projectId}/stories/#{story.id}/tasks").then (tasks)->
               story.tasks = tasks
@@ -73,6 +73,8 @@

           story

+        iteration
+
       RSVP.all(promises).then ->
         resolve curatedIterations


update

update

--- app/assets/javascripts/common.js.coffee
+++ app/assets/javascripts/common.js.coffee
@@ -12,8 +12,8 @@
   $("#sidebar").hide()
   $("#menu").click ->
     if menubool is on
-      $("#sidebar").show("fast")
+      $("#sidebar").show()
     else
-      $("#sidebar").hide("fast")
+      $("#sidebar").hide()
     menubool = !menubool


Fix adding new gists in firefox

Fix adding new gists in firefox

No clue how Chrome was working, should have been broken.

--- app/assets/javascripts/editor.js.coffee
+++ app/assets/javascripts/editor.js.coffee
@@ -4,7 +4,7 @@

 jQuery ->
   jQuery('.editor').ace({ theme: 'github', lang: 'markdown', width: '100%', indent: 2 })
-  jQuery('.editor-meta select').change event, ->
+  jQuery('.editor-meta select').change (event) ->
     ace = jQuery(event.srcElement)
       .closest('.editor-container')
       .find('.editor')
@@ -13,7 +13,7 @@
   jQuery('.initial-focus').focus()

   # For the nested form.
-  document.body.addEventListener "DOMNodeInserted", ((event) ->
+  document.addEventListener "DOMNodeInserted", ((event) ->
     elementJustAdded = jQuery(event.target)
     if elementJustAdded.hasClass("fields")
       jQuery(".editor").ace


allow non-strict key scale

allow non-strict key scale

--- app/assets/javascripts/zvg/point.coffee
+++ app/assets/javascripts/zvg/point.coffee
@@ -100,11 +100,13 @@
       )
       .entries(data)

-    @round_max_value(d3.round(_max_value, 0))
+    @round_max_value(_max_value)
     return r

   round_max_value: (max) ->
-    @_max_value = d3.round(max, 0)
+    rounded = d3.round(max, 0)
+    rounded += 1 if max > rounded
+    @_max_value = rounded
     @

   render: ->
@@ -161,6 +163,12 @@
       return @
     @_strict_scale

+  key_scale: (obj) ->
+    if obj
+      @_key_scale = obj
+      return @
+    @_key_scale
+
   render_y_scale: ->
     lines = @y_scale.selectAll('line.scale_line').data(@ticks())
     lines.enter()
@@ -184,16 +192,16 @@
       .text((d) -> d)
     labels.exit().remove()

-    if @_strict_scale
-      data = (({ value: value, label: label }) for value, label of @_strict_scale when "#{value}" isnt label)
-      keys = @y_scale.selectAll('text.key_label').data(data)
-      keys.enter()
-        .append('text').attr('class', 'key_label')
-        .style('fill', ZVG.flatUIColors['CONCRETE'])
-      keys.attr('x', @x_offset + 5)
-        .attr('y', (d) => @value_domain(d.value))
-        .text((d) -> d.label)
-      keys.exit().remove()
+    scale_obj = @_strict_scale or @_key_scale or {}
+    data = (({ value: value, label: label }) for value, label of scale_obj when "#{value}" isnt label)
+    keys = @y_scale.selectAll('text.key_label').data(data)
+    keys.enter()
+      .append('text').attr('class', 'key_label')
+      .style('fill', ZVG.flatUIColors['CONCRETE'])
+    keys.attr('x', @x_offset + 5)
+      .attr('y', (d) => @value_domain(d.value))
+      .text((d) -> d.label)
+    keys.exit().remove()

   render_series_3: ->


Fixed a disassembler bug (how was that working until now...?)

--- src/disassembler.coffee
+++ src/disassembler.coffee
@@ -57,11 +57,11 @@
   format_decimal = (val,type_char) ->
     valStr = val.toString()
     if type_char == 'f'
-      if val is util.FLOAT_POS_INFINITY or Number.POSITIVE_INFINITY
+      if val is util.FLOAT_POS_INFINITY or val is Number.POSITIVE_INFINITY
         valStr = "Infinity"
-      else if val is util.FLOAT_NEG_INFINITY or Number.NEGATIVE_INFINITY
+      else if val is util.FLOAT_NEG_INFINITY or val is Number.NEGATIVE_INFINITY
         valStr = "-Infinity"
-      else if util.is_float_NaN(val)
+      else if val is NaN
         valStr = "NaN"

     if valStr.match(/-?(Infinity|NaN)/)
@@ -109,7 +109,8 @@
     const_attr = f.get_attribute 'ConstantValue'
     if const_attr?
       entry = pool.get(const_attr.ref)
-      rv += "  Constant value: #{entry.type} #{entry.deref?() or entry.value}\n"
+      # If it's a string, dereference the value. Otherwise, format the numeric value.
+      rv += "  Constant value: #{entry.type} #{entry.deref?() or format(entry)}\n"
     sig = f.get_attribute 'Signature'
     if sig?
       rv += "  Signature: length = 0x#{sig.raw_bytes.length.toString(16)}\n"


:lipstick:

:lipstick:

--- src/packages/gfm.tmbundle/Syntaxes/gfm.cson
+++ src/packages/gfm.tmbundle/Syntaxes/gfm.cson
@@ -39,15 +39,12 @@
   {
     'match': '\\!?\\[([^\\]]*)\\]\\(([^\\)]+)\\)'
     'captures':
-      '1':
-        'name': 'entity.gfm'
-      '2':
-        'name': 'markup.underline.gfm'
+      '1': 'name': 'entity.gfm'
+      '2': 'name': 'markup.underline.gfm'
   }
   {
     'match': '^\\s*(\\*)\\s+'
     'captures':
-      '1':
-        'name': 'constant.gfm'
+      '1': 'name': 'constant.gfm'
   }
 ]


Remove checkpoint

Remove checkpoint

--- src/scenes/Intro.coffee
+++ src/scenes/Intro.coffee
@@ -57,7 +57,7 @@
   Crafty('Player').each ->
     @reset()
     @one 'Activated', ->
-      Crafty.enterScene('Game', checkpoint: 1) # checkpoint: value
+      Crafty.enterScene('Game') # checkpoint: value
   Crafty.e('Delay').delay ->
       Crafty.enterScene('Scores')
     , 20000


missed main.coffee from previous commit

missed main.coffee from previous commit

--- src/scripts/main.coffee
+++ src/scripts/main.coffee
@@ -1,4 +1,7 @@
 emptyOrWhitespaceRegex = RegExp(/^\s*$/)
+DELETE_KEYCODE = 8
+ENTER_KEYCODE = 13
+

 class FormbuilderModel extends Backbone.DeepModel
   sync: -> # noop
@@ -77,6 +80,27 @@
     'click .js-remove-option': 'removeOption'
     'click .js-default-updated': 'defaultUpdated'
     'input .option-label-input': 'forceRender'
+    'keydown .option-label-input': 'handleSpecialKeysDuringOptionEditing'
+
+  handleSpecialKeysDuringOptionEditing: (evt) ->
+    if true and (evt.which == DELETE_KEYCODE or evt.keyCode == DELETE_KEYCODE)
+      currLabel = evt.currentTarget.value
+      if (currLabel == "")
+        deletionIndex = $(evt.currentTarget).parent().index()
+        @removeOptionAtIndex deletionIndex
+
+        # focus on another option
+        focusIndex = (if deletionIndex == 0 then 0 else deletionIndex - 1)
+        newFocusFields = $(".edit-response-field .sortableParentContainer .option .option-label-input")
+        if (newFocusFields.length > 0)
+          (newFocusFields[focusIndex]).focus()
+
+        return false
+    else if evt.which == ENTER_KEYCODE or evt.keyCode == ENTER_KEYCODE
+      @addOption evt
+
+    # console.log evt
+    # console.log this

   initialize: (options) ->
     {@parentView} = options
@@ -247,7 +271,12 @@

         translationData.options = _.clone(@model.get(Formbuilder.options.mappings.OPTIONS))

-    console.log "label set to [" + translationData.pseudoLabel + "]"
+
+      # when we switch to one of the multiple choice field types, let's add some default options
+      if (!translationData.options or translationData.options.length == 0)
+        translationData.options = Formbuilder.generateDefaultOptionsArray()
+
+    # console.log "label set to [" + translationData.pseudoLabel + "]"

     # we've extracted the relevant data; now let's update the model
     # delete stuff that may not exist on the destination type; it'll get re-created if necessary
@@ -329,11 +358,19 @@

     @model.set Formbuilder.options.mappings.OPTIONS, options
     @model.trigger "change:#{Formbuilder.options.mappings.OPTIONS}"
+
+    # let's focus on the newly added element...
+    targetSlot = 1 * (if i > -1 then i + 1 else options.length - 1)
+    ($(".edit-response-field .sortableParentContainer .option .option-label-input")[targetSlot]).focus()
+
     @forceRender()

   removeOption: (e) ->
     $el = $(e.currentTarget)
     index = @$el.find(".js-remove-option").index($el)
+    @removeOptionAtIndex(index)
+
+  removeOptionAtIndex: (index) ->
     options = @model.get Formbuilder.options.mappings.OPTIONS
     options.splice index, 1
     @model.set Formbuilder.options.mappings.OPTIONS, options
@@ -361,7 +398,17 @@
     'click .fb-tabs a': 'showTab'
     'click .fb-add-field-types a': 'addField'

+  # unless the user is editing text, let's intercept delete keypresses. otherwise too easy to go back in the history
+  captureDelete: (evt) ->
+    if (evt.which == DELETE_KEYCODE or evt.keyCode == DELETE_KEYCODE)
+      if (evt.target and evt.target.type == "text")
+        return true
+      else
+        return false
+
   initialize: (options) ->
+    $(document).keydown(@captureDelete)
+
     {selector, @formBuilder, @bootstrapData} = options

     if (!(@bootstrapData instanceof Array))


Comment heuristic for deciding whether to do a _reIndex in SetSort::_handleItemsAdded

--- src/set/set_sort.coffee
+++ src/set/set_sort.coffee
@@ -34,42 +34,39 @@
     @fire('itemWasMoved', item, newIndex, oldIndex) unless oldIndex == newIndex

   _handleItemsAdded: (items) ->
-    if false and items.length > Math.log(@_storage.length) * 5
-      @_reIndex()
-    else
-      oldStorage = @_storage.slice()
-      addedItems = []
-      addedIndexes = []
+    # if items.length > Math.log(@_storage.length) * 5
+    #   @_reIndex()
+    # else
+    oldStorage = @_storage.slice()
+    addedItems = []
+    addedIndexes = []

-      for item in items
-        index = @constructor._binarySearch(@_storage, item, @compareElements, false)
-        if index >= 0
-          @_storage.splice(index, 0, item)
-          addedItems.push(item)
-          addedIndexes.push(index)
+    for item in items
+      index = @constructor._binarySearch(@_storage, item, @compareElements, false)
+      if index >= 0
+        @_storage.splice(index, 0, item)
+        addedItems.push(item)
+        addedIndexes.push(index)

-      @set('length', @_storage.length)
-      @fire('itemsWereAdded', addedItems, addedIndexes)
-      @property('_storage').fire(@_storage, oldStorage, '_storage')
+    @set('length', @_storage.length)
+    @fire('itemsWereAdded', addedItems, addedIndexes)
+    @property('_storage').fire(@_storage, oldStorage, '_storage')

   _handleItemsRemoved: (items) ->
-    if false and items.length > Math.log(@_storage.length) * 5
-      @_reIndex()
-    else
-      oldStorage = @_storage.slice()
-      removedItems = []
-      removedIndexes = []
+    oldStorage = @_storage.slice()
+    removedItems = []
+    removedIndexes = []

-      for item in items
-        index = @_indexOfItem(item)
-        if index >= 0
-          @_storage.splice(index, 1)
-          removedItems.push(item)
-          removedIndexes.push(index)
+    for item in items
+      index = @_indexOfItem(item)
+      if index >= 0
+        @_storage.splice(index, 1)
+        removedItems.push(item)
+        removedIndexes.push(index)

-      @set('length', @_storage.length)
-      @fire('itemsWereRemoved', removedItems, removedIndexes)
-      @property('_storage').fire(@_storage, oldStorage, '_storage')
+    @set('length', @_storage.length)
+    @fire('itemsWereRemoved', removedItems, removedIndexes)
+    @property('_storage').fire(@_storage, oldStorage, '_storage')

   toArray: -> @get('_storage').slice()


try to make tempremental test more reliable

--- lineman/spec-e2e/helpers/groups_helper.coffee
+++ lineman/spec-e2e/helpers/groups_helper.coffee
@@ -135,6 +135,7 @@
     element(By.css('.leave-group-form__add-coordinator')).click()

   clickStartSubgroupLink: ->
+    browser.sleep(2000) # this has been a tempremantal test, ugh
     element(By.css('.subgroups-card__add-subgroup-link')).click()

   fillInSubgroupName: (name) ->


Extend Backbone.history rather than creating a new class

--- src/scripts/helpers/backbone/history.coffee
+++ src/scripts/helpers/backbone/history.coffee
@@ -1,11 +1,12 @@
 define (require) ->
+  _ = require('underscore')
   Backbone = require('backbone')

   hashStripper = /[#].*$/
   routeStripper = /^[#\/]|\s+$/g
   trailingSlash = /\/$/

-  return Backbone.history = new class History extends Backbone.History
+  return _.extend Backbone.history, {
     getFragment: (fragment, forcePushState) ->
       if not fragment
         fragment = @location.pathname + @location.search
@@ -30,3 +31,4 @@
       @history[if options.replace then 'replaceState' else 'pushState']({}, document.title, url)

       if options.trigger then return @loadUrl(fragment)
+  }


Map view by state now scaled, zooms in on smaller states and out on larger ones.


Fix I18NEditLevelView for empty noteGroups in levels

--- app/views/i18n/I18NEditLevelView.coffee
+++ app/views/i18n/I18NEditLevelView.coffee
@@ -44,6 +44,7 @@
     # sprite dialogues
     for script, scriptIndex in @model.get('scripts') ? []
       for noteGroup, noteGroupIndex in script.noteChain ? []
+        continue unless noteGroup
         for spriteCommand, spriteCommandIndex in noteGroup.sprites ? []
           pathPrefix = ['scripts', scriptIndex, 'noteChain', noteGroupIndex, 'sprites', spriteCommandIndex, 'say']


style

style

--- src/flamethrower.coffee
+++ src/flamethrower.coffee
@@ -1,6 +1,6 @@
 Campfire = require('../vendor/campfire').Campfire

-campfire = new Campfire { ssl: true, token: process.env.CAMPFIRE_TOKEN, account: 'edgecase' }
+campfire = new Campfire(ssl: true, token: process.env.CAMPFIRE_TOKEN, account: 'edgecase')
 roomId   = process.env.CAMPFIRE_ROOM || '416570'
 bot      = {}
 users    = {}
@@ -8,11 +8,11 @@
 campfire.me (response) ->
   bot = response.user

-campfire.room roomId, ( room ) ->
+campfire.room roomId, (room) ->
   room.join ->
     console.log "Joining #{room.name}"

-    room.listen ( msg ) ->
+    room.listen (msg) ->
       if msg.type is 'TextMessage' and msg.user_id isnt bot.id
         findUser msg.user_id, (user) ->
           postMessage msg.body, user.name


On second thought, don't sort by decayed_merch just yet

--- src/mobile/components/filter2/index.coffee
+++ src/mobile/components/filter2/index.coffee
@@ -18,7 +18,6 @@
       page: 1
       size: 10
       aggregations: aggregations
-      sort: '-decayed_merch'

     if stuckParam
       params.set stuckParam, stuckFacet.id


kontrol: don't auto-reconnect

kontrol: don't auto-reconnect

--- client/Main/kite/kodingkontrol.coffee
+++ client/Main/kite/kodingkontrol.coffee
@@ -6,10 +6,11 @@
     @kites = {}

   getAuthOptions: ->
-    url     : KD.config.newkontrol.url
-    auth    :
-      type  : 'sessionID'
-      key   : Cookies.get 'clientId'
+    autoConnect : no
+    url         : KD.config.newkontrol.url
+    auth        :
+      type      : 'sessionID'
+      key       : Cookies.get 'clientId'

   reauthenticate: ->
     # disconnect the old kontrol kite


removed useless timestamp from graphs in flow deploy dashboard

--- client/components/flow-dashboard.cjsx
+++ client/components/flow-dashboard.cjsx
@@ -52,20 +52,17 @@

       <AvgElapsedTimeGauge
         title="Flow Deploy Average Time"
-        avgElapsedTime={@state.avgElapsedTime}
-        timestamp={@state._timestamp} />
+        avgElapsedTime={@state.avgElapsedTime} />

       <OverTimeGauge
         title="Flow Deploy Success Over Time"
         suffix="%"
-        elapsedTimeChartData={@state.flowDeployOverTime}
-        timestamp={@state._timestamp} />
+        elapsedTimeChartData={@state.flowDeployOverTime} />

       <OverTimeGauge
         title="Flow Deploy Average Over Time"
         suffix="s"
-        elapsedTimeChartData={@state.elapsedTimeChartData}
-        timestamp={@state._timestamp} />
+        elapsedTimeChartData={@state.elapsedTimeChartData} />
     </div>

 module.exports = FlowDashboard


fixed error message quotes

fixed error message quotes

--- src/compiler/types.coffee
+++ src/compiler/types.coffee
@@ -64,7 +64,7 @@
     throw new NoMatch(msg, "'#{next}'")
   for k,v of props
     unless next[k] is v
-      throw new NoMatch(msg, next)
+      throw new NoMatch(msg, "'#{next}'")
   return semantic s.consume_next()

 IdentType = (value) -> TokenType("'#{value}'", IdentToken, {value})


fixed bug

fixed bug

--- src/compiler/types.coffee
+++ src/compiler/types.coffee
@@ -108,7 +108,7 @@
     @expected = "'#{@token}'"
     if @token instanceof SS.DelimToken
       @tokenClass = SS.DelimToken
-      @props = {value: token.value}
+      @props = {value: @token.value}
     else if @token instanceof SS.SimpleToken
       return
     else


stray console log

stray console log

--- apps/artwork/client/related-show.coffee
+++ apps/artwork/client/related-show.coffee
@@ -21,8 +21,6 @@
   setupFollowButton: ->
     @following = new Following(null, kind: 'profile') if @currentUser?
     @following.syncFollows [@partner.get('default_profile_id')]
-
-    console.log @$('.plus-follow-button')

     @followButton = new FollowButton
       analyticsFollowMessage: 'Followed partner, via show on artwork page'


simplify showing link editor code

simplify showing link editor code

--- demo/scripts/pretty.coffee
+++ demo/scripts/pretty.coffee
@@ -1,6 +1,6 @@
 curLink = linkTooltip = null

-showLinkEditor = (editor, link, change = false) ->
+showLinkEditor = (editor, link) ->
   curLink = link
   url = $(link).attr('href')
   $('.url', linkTooltip).text(url).attr('href', url)
@@ -41,7 +41,7 @@
       'display': 'inline'
   )
   linkTooltip = $('.link-tooltip').appendTo(editor.doc.root.parentNode)
-  $('a', editor.doc.root).on('click', (event) ->
+  $(editor.doc.root).on('click', 'a', (event) ->
     showLinkEditor(editor, this)
   )
   $('.url-editor', linkTooltip).keyup((event) ->
@@ -91,6 +91,10 @@
   toolbar.on(Scribe.Toolbar.events.FORMAT, (format, value) ->
     if _.indexOf(dropkickFormats, format) > -1
       $("#formatting-container .#{format}").dropkick('set', ' ')
+    else if format == 'link'
+      selection = editor.getSelection()
+      node = selection.start.leafNode or selection.end.leafNode
+      $(node).click() if node?
   )
   editor.on(Scribe.Editor.events.SELECTION_CHANGE, (selection) ->
     formats = selection.getFormats()
@@ -101,26 +105,6 @@
         else
           $("#formatting-container .#{key}").val(value).change()
     )
-  )
-  $(".formatting-container .link").click( ->
-    selection = editor.getSelection()
-    link = if $(this).parent().hasClass('active') then false else selection.getText()
-    editor.selection.format('link', link)
-    selection = editor.getSelection()
-    boundries = [selection.start.leafNode, selection.end.leafNode]
-    _.each(boundries, (node, index) ->
-      while node.parentNode?.childNodes.length == 1
-        boundries[index] = node.parentNode
-        node = node.parentNode
-    )
-    unless _.any(boundries, (node) ->
-      if node.tagName == 'A'
-        showLinkEditor(editor, node)
-        return true
-      return false
-    )
-      $link = $('a', boundries[0]).first() or $('a', boundries[0]).first()
-      showLinkEditor(editor, $link.get(0)) if $link
   )


Fix bug in initialization Check if _picker is already defined Fixes #57

--- coffee/angular-daterangepicker.coffee
+++ coffee/angular-daterangepicker.coffee
@@ -21,17 +21,19 @@

     _setStartDate = (newValue) ->
       $timeout () ->
-        m = moment(newValue)
-        if (_picker.endDate < m)
-          _picker.setEndDate(m)
-        _picker.setStartDate(m)
+        if (_picker)
+          m = moment(newValue)
+          if (_picker.endDate < m)
+            _picker.setEndDate(m)
+          _picker.setStartDate(m)

     _setEndDate = (newValue) ->
       $timeout () ->
-        m = moment(newValue)
-        if (_picker.startDate > m)
-          _picker.setStartDate(m)
-        _picker.setEndDate(m)
+        if (_picker)
+          m = moment(newValue)
+          if (_picker.startDate > m)
+            _picker.setStartDate(m)
+          _picker.setEndDate(m)

     #Watchers enable resetting of start and end dates
     $scope.$watch 'model.startDate', (newValue) ->
@@ -112,8 +114,7 @@
           modelCtrl.$setViewValue({startDate: start, endDate: end})
         )
         modelCtrl.$render()
-
-    _picker = el.data('daterangepicker')
+        _picker = el.data('daterangepicker')

     _init()


Pull request proposed by @sylvainbannier Should fix #38

--- coffee/angular-daterangepicker.coffee
+++ coffee/angular-daterangepicker.coffee
@@ -90,8 +90,8 @@
         $timeout(->
           $scope.$apply(->
             modelCtrl.$setViewValue({
-              startDate: start.toDate()
-              endDate: end.toDate()
+              startDate: start
+              endDate: end
             })
             modelCtrl.$render()
           ))


* add the es6 class supports

* add the es6 class supports

--- src/custom-factory.coffee
+++ src/custom-factory.coffee
@@ -3,11 +3,14 @@
 inherits              = require('inherits-ex/lib/inherits')
 isInheritedFrom       = require('inherits-ex/lib/isInheritedFrom')
 createObject          = require('inherits-ex/lib/createObject')
+getPrototypeOf        = require('inherits-ex/lib/getPrototypeOf')
 extend                = require('./extend')
 isFunction            = (v)-> 'function' is typeof v
 isString              = (v)-> 'string' is typeof v
 isObject              = (v)-> v? and 'object' is typeof v
 getObjectKeys         = Object.keys
+
+getParentClass = (ctor)-> ctor.super_ || getPrototypeOf(ctor)

 exports = module.exports = (Factory, aOptions)->
   if isObject aOptions
@@ -21,12 +24,12 @@
     getClassList: (ctor)->
       result = while ctor and ctor isnt Factory
         item = ctor
-        ctor = ctor.super_
+        ctor = getParentClass ctor
         item
     getClassNameList: getClassNameList = (ctor)->
       result = while ctor and ctor isnt Factory
         item = ctor::name
-        ctor = ctor.super_
+        ctor = getParentClass ctor
         item
     path: (aClass, aRootName)->
       '/' + @pathArray(aClass, aRootName).join '/'
@@ -65,7 +68,7 @@
         result = result.substring(0, len-vFactoryName.length)
       ###
       if aBaseNameOnly # then remove Parent Name if any
-        aParentClass = aClass.super_ unless aParentClass
+        aParentClass = getParentClass(aClass) unless aParentClass
         names = getClassNameList aParentClass
         names.push Factory.name
         if names.length
@@ -163,8 +166,8 @@
           if result
             aName = vClass::name
             #TODO:should I unregister all children?
-            while vClass and vClass.super_ and vClass.super_ isnt Factory
-              vClass = vClass.super_
+            while vClass and (vParentClass = getParentClass(vClass)) and vParentClass isnt Factory
+              vClass = getParentClass vClass
               delete vClass[aName]
             delete registeredObjects[aName]
             delete Factory[aName]


Error with ajax messages

Error with ajax messages

--- src/quo.ajax.coffee
+++ src/quo.ajax.coffee
@@ -53,15 +53,16 @@
                 _xhrTimeout xhr, settings
             , settings.timeout)

-        if settings.data
-            parameters = _serializeParameters(settings.data)
-            settings.data = parameters.substr(1, parameters.length)
+        xhr.send settings.data

-        try
-            xhr.send settings.data
-        catch error
-            xhr = error
-            _xhrError "Resource not found", xhr, settings
+        # if settings.data
+        #     parameters = $$.serializeParameters(settings.data)
+        #     settings.data = parameters.substr(1, parameters.length)
+        # try
+        #     xhr.send settings.data
+        # catch error
+        #     xhr = error
+        #     _xhrError "Resource not found", xhr, settings

         (if (settings.async) then xhr else _parseResponse(xhr, settings))


More unused stuff

More unused stuff

--- resources/assets/coffee/osu_common.coffee
+++ resources/assets/coffee/osu_common.coffee
@@ -177,28 +177,6 @@
       window.scrollTo position[0], position[1]

-  getOS: (fallback='Windows') ->
-    nAgnt = navigator.userAgent
-    os = undefined
-    if /Windows (.*)/.test(nAgnt)
-      return 'Windows'
-    # Test for mobile first
-    if /Mobile|mini|Fennec|Android|iP(ad|od|hone)/.test(navigator.appVersion)
-      return fallback
-    if /(macOS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/.test(nAgnt)
-      return 'macOS'
-    if /(Linux|X11)/.test(nAgnt)
-      return 'Linux'
-    fallback
-
-
-  otherOS: (os) ->
-    choices = ['macOS', 'Linux', 'Windows']
-    index = choices.indexOf os
-    choices.splice index, 1
-    choices
-
-
   popup: (message, type = 'info') ->
     $popup = $('#popup-container')
     $alert = $('.popup-clone').clone()


fix cnx-recipes issue #213 & webview issue #1579

--- src/scripts/modules/media/body/body.coffee
+++ src/scripts/modules/media/body/body.coffee
@@ -83,7 +83,7 @@
     goToPage: (pageNumber, href) ->
       @model.setPage(pageNumber)

-      router.navigate href, {replace: false}, => @parent.parent.parent.trackAnalytics()
+      router.navigate(href, {trigger: true, replace: false}, => @parent.parent.parent.trackAnalytics())

     getCoach: ->
       moduleUUID = @model.getUuid()?.split('?')[0]


hub/custom software: adapt testing code

hub/custom software: adapt testing code

--- src/smc-hub/postgres-server-queries.coffee
+++ src/smc-hub/postgres-server-queries.coffee
@@ -2662,7 +2662,8 @@
         create = (idx, cb) =>
             rnd  = sample(idx, 3)
             id   = rnd[...2].join('-') + "-#{idx}"
-            src = "https://github.com/#{rnd[2]}/#{id}.git"
+            provider = ['github.com', 'gitlab.com', 'bitbucket.org'][idx % 3]
+            src = "https://#{provider}/#{rnd[2]}/#{id}.git"

             # not all of them have a display-title, url, desc, ...
             if random() > .25
@@ -2685,11 +2686,14 @@
             if random() > .5
                 if random() > .5
                     verylong = Array(100).fill('very long *text* for **testing**, ').join(" ")
+                if url?
+                    other_page = ", or point to [yet another page](#{url})"
+                else
+                    other_page = ""
                 desc = """
                        This is some text describing what **#{disp or id}** is.
                        Here could also be an [external link](https://doc.cocalc.com).
-                       It might also mention `#{id}`, or point to
-                       [yet another page](#{url}).
+                       It might also mention `#{id}`#{other_page}.

                        #{verylong ? ''}
                        """
@@ -2709,6 +2713,7 @@
                     "desc    :: TEXT     " : desc
                     "display :: TEXT     " : disp
                     "path    :: TEXT     " : path
+                    "url     :: TEXT     " : url
                     "disabled:: BOOLEAN  " : idx == 1
                 cb     : cb


Finally extracting doc comments

Finally extracting doc comments

--- quidoc.coffee
+++ quidoc.coffee
@@ -3,15 +3,17 @@
 ###

 regexCommentedMembers = ///
-  (                     # First group captures the comment
-    (?:\n\x20\x20# .*)  # A comment line indented two spaces
-    +                   # Any number of comment lines
+  (                         # First group captures the comment
+    (?:                     # Non-capturing group
+      \r?\n\x20\x20# .*     # A comment line indented two spaces
+    )
+    +                       # Any number of comment lines
   )
-  \n\x20\x20            # Two spaces at start of identifier line
-  (                     # Second group captures the identifier
-    [a-zA-Z0-9$][\w]+   # JavaScript member identifier
+  \r?\n\x20\x20             # Identifier line indented two spaces
+  (                         # Second group captures the identifier
+    [a-zA-Z0-9$][\w]+       # JavaScript member identifier
   )
-  :                     # Colon terminates identifier
+  :                         # Colon terminates identifier
   ///gm

 fs = require "fs"
@@ -29,7 +31,6 @@

 docComments = ( source ) ->
-  console?.log source
   comments = {}
   match = regexCommentedMembers.exec source
   while match != null
@@ -47,117 +48,17 @@
   console?.log comments
 ###

-source = """
-###
-Lets user navigate content by moving left (backward) and right (forward).
-###
-
-class window.LateralNavigator extends Control
-
-  inherited:
-    content:
-      control: HorizontalPanels
-      ref: "panels"
-      content:
-        html: "div"
-        ref: "LateralNavigator_content"
-      leftClass: "VerticalAlign"
-      left:
-        control: BasicButton
-        ref: "LateralNavigator_previousButton"
-        class: "flat navigatorButton quiet"
-        content: "◀"
-      rightClass: "VerticalAlign"
-      right:
-        control: BasicButton
-        ref: "LateralNavigator_nextButton"
-        class: "navigatorButton quiet"
-        content: "▶"
-      tabindex: -1 # To get keyboard events
-    generic: true
-
-  # True if it's possible to navigate forward.
-  # The default implementation always returns true. Subclasses can override this
-  # to provide custom logic.
-  canGoNext: -> true
-
-  # True if it's possible to navigate backward.
-  # The default implementation always returns true. Subclasses can override this
-  # to provide custom logic.
-  canGoPrevious: -> true
-
-  # The content for the current position in the sequence.
-  content: Control.chain( "$LateralNavigator_content", "content", ->
-    @_updateButtons()
-  )
-
-  # The control class used to render the content.
-  contentClass: Control.property.class ( contentClass ) ->
-    $new = @$LateralNavigator_content().transmute contentClass, true
-    @referencedElement "LateralNavigator_content", $new
-
-  initialize: ->
-    @$LateralNavigator_previousButton().click => @_previousClick()
-    @$LateralNavigator_nextButton().click => @_nextClick()
-    @on
-      keydown: ( event ) =>
-        switch event.which
-          when 37 # Left
-            if !@$LateralNavigator_previousButton().disabled()
-              @_previousClick()
-          when 39 # Right
-            if !@$LateralNavigator_nextButton().disabled()
-              @_nextClick()
-      sizeChanged: =>
-        # HACK: Shouldn't need to directly reference HorizontalPanel elements.
-        this.$panels().$SimpleFlexBox_panel1().checkForSizeChange();
-        this.$panels().$SimpleFlexBox_panel2().checkForSizeChange();
-    @_updateButtons()
-
-  # Move forward.
-  # The default implementation does nothing.
-  next: ->
-
-  # The content of the "Next" button. Default is a right-pointing arrow.
-  nextButtonContent: Control.chain "$LateralNavigator_nextButton", "content"
-
-  # True if the Next button should be disabled. Default is false.
-  nextButtonDisabled: Control.chain "$LateralNavigator_nextButton", "disabled"
-
-  # Move backward.
-  # The default implementation does nothing.
-  previous: ->
-
-  # The content of the "Previous" button. Default is a left-pointing arrow.
-  previousButtonContent: Control.chain "$LateralNavigator_previousButton", "content"
-
-  # True if the Previous button should be disabled. Default is false.
-  previousButtonDisabled: Control.chain "$LateralNavigator_previousButton", "disabled"
-
-  _nextClick: ->
-    @next()
-    @_updateButtons()
-    @trigger "next"
-
-  _previousClick: ->
-    @previous()
-    @_updateButtons()
-    @trigger "previous"
-
-  _updateButtons: ->
-    @nextButtonDisabled !@canGoNext()
-    @previousButtonDisabled !@canGoPrevious()
-"""
-
 source2 = fs.readFileSync "c:/Source/quickui-catalog/coffee/LateralNavigator.coffee", "utf8"

-#console?.log docComments source2
+console?.log docComments source2

+###
 #regex = /((?:\n\x20\x20# .*)+)\n\x20\x20([a-zA-Z0-9$][\w]+):/gm
-regex = /((\n\x20\x20# .*)+)/gm
-match = regex.exec source2
+regex = /((?:\r?\n\x20\x20# .*)+)\r?\n\x20\x20([a-zA-Z0-9$][\w]+):/g
+text = source2
+match = regex.exec text
 while match != null
   [ full, capture ] = match
-  console.log capture
-  console.log ""
-  match = regex.exec source2+  console?.log "<#{capture}>"
+  match = regex.exec text
+###


handle erros in synchronous mode

handle erros in synchronous mode

--- src/run.coffee
+++ src/run.coffee
@@ -81,28 +81,26 @@
               else emit_action statusOrMsg
               next err
             run = =>
-              # Synchronous action
-              if action.callback.length is 1
-                merge action, action.callback.call ctx, ctx
-                process.nextTick ->
-                  action.timeout = -1
-                  done null, ctx.DISABLED
-              # Asynchronous action
-              else
-                merge action, action.callback.call ctx, ctx, (err, statusOrMsg) =>
-                  actionRun.end() if statusOrMsg is ctx.STOP
-                  done err, statusOrMsg
-            # Prevent "Maximum call stack size exceeded" when
-            # timeout, interval and setImmediate is used inside the callback
+              try
+                # Synchronous action
+                if action.callback.length is 1
+                  merge action, action.callback.call ctx, ctx
+                  process.nextTick ->
+                    action.timeout = -1
+                    done null, ctx.DISABLED
+                # Asynchronous action
+                else
+                  merge action, action.callback.call ctx, ctx, (err, statusOrMsg) =>
+                    actionRun.end() if statusOrMsg is ctx.STOP
+                    done err, statusOrMsg
+              catch e then done e
             # Timeout, default to 100s
             action.timeout ?= 100000
             if action.timeout > 0
               timeout = setTimeout ->
                 done new Error 'TIMEOUT'
               , action.timeout
-            try
-              setImmediate run
-            catch e then done e
+            run()
           .on 'both', (err) =>
             @emit 'server', ctx, if err then ctx.FAILED else ctx.OK
             if err


Cache versions that do not match the range

--- src/module-cache.coffee
+++ src/module-cache.coffee
@@ -7,12 +7,17 @@
   constructor: ->
     super
     @matchedVersions = new Set()
+    @unmatchedVersions = new Set()

   test: (version) ->
     return true if @matchedVersions.has(version)
+    return false if @unmatchedVersions.has(version)

     matches = super
-    @matchedVersions.add(version) if matches
+    if matches
+      @matchedVersions.add(version)
+    else
+      @unmatchedVersions.add(version)
     matches

 nativeModules = process.binding('natives')


Fix require('preact') by tweaking NODE_PATH

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -1455,9 +1455,21 @@
 window?.svgtiler = svgtiler

 if require?.main == module and not window?
-  ## Enable require('svgtiler') (as autoimported by `svgtiler` access)
-  ## to load this module (needed if the module is installed globally).
-  process.env.NODE_PATH = path.join __dirname, '..', '..'
+  paths = [
+    ## Enable require('svgtiler') (as autoimported by `svgtiler` access)
+    ## to load this module (needed if the module is installed globally).
+    path.join __dirname, '..', '..'
+    ## Enable require('preact') (as autoimported by `preact` access)
+    ## to load SVG Tiler's copy of preact.
+    path.join __dirname, '..', 'node_modules'
+  ]
+  paths.push process.env.NODE_PATH if process.env.NODE_PATH
+  process.env.NODE_PATH = paths.join (
+    if require('process').platform == 'win32'
+      ';'
+    else
+      ':'
+  )
   require('module').Module._initPaths()

   main()


Show help if the config file cant be read

--- src/sew.coffee
+++ src/sew.coffee
@@ -95,6 +95,8 @@
       @options[key] = value for key, value of @defaults
       @options[key] = value for key, value of config
       return true
+
+    opti.showHelp()
     process.exit(1)

   walk: (path, callback) ->


Add isEquals to notifications

Add isEquals to notifications

--- src/notification.coffee
+++ src/notification.coffee
@@ -15,6 +15,11 @@

   getDetail: -> @options.detail

+  isEqual: (other) ->
+    @getMessage() == other.getMessage() \
+      and @getType() == other.getType() \
+      and @getDetail() == other.getDetail()
+
   isClosable: ->
     !!@options.closable


EDF konnector: Fix error with nom mensualities accounts. (#617)

* EDF konnector : add error check on each request response.

* Fix vendorlink : Fix #629.

* Re-activate Edelia data in EDF konnector.

--- server/konnectors/edf.coffee
+++ server/konnectors/edf.coffee
@@ -8,6 +8,7 @@

 localization = require '../lib/localization_manager'

+fetcher = require '../lib/fetcher'
 updateOrCreate = require '../lib/update_or_create'
 File = require '../models/file'
 Folder = require '../models/folder'
@@ -52,6 +53,11 @@
     edfRequestPost path, body, (err, result) ->
         return callback err if err

+        errorCode = getF(result, 'ns:enteteSortie', 'ent:codeRetour')
+        if errorCode and errorCode isnt '0000'
+            K.logger.error getF result, \
+                'ns:enteteSortie', 'ent:libelleRetour '
+
         token = getF result['ns:msgReponse'], 'ns:corpsSortie', 'ns:jeton'

         if token?
@@ -82,6 +88,14 @@
     edfRequestPost path, body, (err, result) ->
         return callback err if err
         try
+            errorCode = getF result, 'tns:EnteteSortie', 'tns:CodeErreur'
+            if errorCode and errorCode isnt 'PSC0000'
+                K.logger.error getF result, \
+                    'tns:EnteteSortie', 'tns:LibelleErreur'
+
+                return callback 'request error'
+
+
             client =
                 vendor: 'EDF'
                 docTypeVersion: K.docTypeVersion
@@ -314,6 +328,14 @@
     edfRequestPost path, body, (err, result) ->
         return callback err if err
         try
+            errorCode = getF result, 'ns:enteteSortie', 'ent:codeRetour'
+            if errorCode and errorCode isnt '0'
+                K.logger.error getF result, \
+                    'tns:enteteSortie', 'tns:libelleRetour'
+
+                return callback() # Continue on error.
+
+
             partnerElem = getF result["ns:msgReponse"], \
                         "ns:corpsSortie", "ns:partenaire"
             client = {}
@@ -380,6 +402,15 @@
     edfRequestPost path, body, (err, result) ->
         return callback err if err
         try
+            errorCode = getF result, 'ns:enteteSortie', 'ent:codeErreur'
+            if errorCode and errorCode isnt '0'
+                K.logger.error getF result, \
+                    'tns:enteteSortie', 'tns:libelleErreur'
+
+                return callback() # Continue on error.
+
+
+
             acoElem = getF result["ns:msgReponse"], "ns:corpsSortie", \
                 "ns:listeAccordCommerciaux", "ns:acordcommercial"

@@ -474,6 +505,14 @@
     edfRequestPost path, body, (err, result) ->
         return callback err if err
         try
+            # Does API send an error ?
+            errorCode = getF result, 'ns:msgReponse', 'ns:enteteSortie', \
+                 'ent:codeRetour'
+            if errorCode and errorCode isnt '0'
+                K.logger.error getF result, 'ns:msgReponse', \
+                    'ns:enteteSortie' , 'ent:libelleRetour'
+                return callback() # Continue, whitout error.
+
             listeEcheances = getF(result["ns:msgReponse"], "ns:corpsSortie", \
                 "ns:calendrierDePaiement")["ns:listeEcheances"]

@@ -600,6 +639,15 @@
             return callback err if err

             try
+
+                errorCode = getF result, 'ns:enteteSortie', 'ent:codeRetour'
+                if errorCode and errorCode isnt '0'
+                    K.logger.error getF result, \
+                        'tns:enteteSortie', 'tns:libelleRetour'
+
+                return callback() # Continue on error.
+
+
                 unless "ns:corpsSortie" of result["ns:msgReponse"]
                     K.logger.info "No histoConsos to fetch"
                     return callback null, []
@@ -687,12 +735,12 @@
     K.logger.info "fetchEdeliaToken"
     request.post 'https://api.edelia.fr/authorization-server/oauth/token',
         form:
-            client_id: requiredFields.edeliaClientId
+            client_id:
+                'sha1pae0Pahngee6uwiphooDie7thaiquahf2xohd6IeFeiphi9ziu0uw3am'
             grant_type: 'edf_sso'
             jeton_sso: data.edfToken
             bp: entries.clients[0].clientId
-            # TODO : one procedure per contract !!!
-            pdl: entries.contracts[0].pdl
+            pdl: data.contract.pdl
         json: true
     , (err, response, result) ->
         if err
@@ -702,7 +750,6 @@

         K.logger.info 'Fetched edelia token'
         data.edeliaToken = result.access_token
-        data.contract = entries.contracts[0]
         callback()

@@ -722,7 +769,7 @@
             if obj.errorCode and obj.errorCode is "403"
                 data.noEdelia = true
                 K.logger.warn "No edelia: #{obj.errorDescription}"
-                throw null
+                throw new Error('no edelia')

             doc =
                 pdl: data.pdl
@@ -856,6 +903,9 @@

             objs.forEach (obj) ->
                 statement = data.consumptionStatementByYear[obj.year]
+                unless statement
+                    K.logger.warn "No yearly statement for #{obj.date.year}"
+                    return
                 statement.similarHomes =
                     site: obj.energies.site
                     average: obj.energies.similarHomes.SH_AVERAGE_CONSUMING
@@ -890,6 +940,10 @@
             objs.forEach (obj) ->
                 statement = data
                     .consumptionStatementByMonth[obj.date.slice(0, 7)]
+                unless statement
+                    K.logger.warn "No monthly statement for\
+                     #{obj.date.slice(0, 7)}"
+                    return

                 statement.statements = statement.statements || []
                 statement.statements.push obj
@@ -982,6 +1036,7 @@

         callback error

+
 fetchEdeliaSimilarHomeYearlyGasComparisions = (
 requiredFields, entries, data, callback) ->
     return callback() if data.noEdelia or data.noGas
@@ -1005,6 +1060,10 @@

             objs.forEach (obj) ->
                 statement = data.consumptionStatementByYear[obj.year]
+                unless statement
+                    K.logger.warn "No yearly statement for #{obj.date.year}"
+                    return
+
                 statement.similarHomes =
                     site: obj.energies.site
                     average: obj.energies.similarHomes.SH_AVERAGE_CONSUMING
@@ -1040,7 +1099,10 @@
             objs.forEach (obj) ->
                 statement = data
                     .consumptionStatementByMonth[obj.date.slice(0, 7)]
-
+                unless statement
+                    K.logger.warn "No monthly statement for\
+                     #{obj.date.slice(0, 7)}"
+                    return
                 statement.statements = statement.statements || []
                 statement.statements.push obj

@@ -1143,13 +1205,42 @@

     next()

+fetchEdeliaData = (requiredFields, entries, data, next) ->
+    async.eachSeries entries.contracts, (contract, callback) ->
+        data.contract = contract
+        importer = fetcher.new()
+        operations = [
+            fetchEdeliaToken
+            fetchEdeliaProfile
+            fetchEdeliaMonthlyElecConsumptions
+            fetchEdeliaSimilarHomeYearlyElecComparisions
+            fetchEdeliaElecIndexes
+            fetchEdeliaMonthlyGasConsumptions
+            fetchEdeliaSimilarHomeYearlyGasComparisions
+            fetchEdeliaGasIndexes
+        ]
+        operations.forEach (operation) -> importer.use operation
+        importer.args requiredFields, entries, data
+        importer.fetch (err, fields, entries) ->
+            if err
+                K.logger.error 'Error while fetching Edelia data'
+                K.logger.error err
+                # Continue on error.
+            callback()
+    , next
+
 # Konnector
 K = module.exports = require('../lib/base_konnector').createNew
     name: 'EDF'
     slug: 'edf'
     description: 'konnector description edf'
+<<<<<<< HEAD
     vendorLink: 'https://particulier.edf.fr/fr'
     category: 'energy',
+=======
+    vendorLink: 'https://particulier.edf.fr'
+
+>>>>>>> ebe8132... EDF konnector: Fix error with nom mensualities accounts. (#617)
     fields:
         email:
             type: "text"
@@ -1164,6 +1255,7 @@
         'consumption'
     ]

+<<<<<<< HEAD
     # TODO : get one edeliaClientId: 'text'

     models: [
@@ -1174,6 +1266,9 @@
         EDFConsumptionStatement,
         EDFBill
     ]
+=======
+    models: [Client, Contract, PaymentTerms, Home, ConsumptionStatement, Bill]
+>>>>>>> ebe8132... EDF konnector: Fix error with nom mensualities accounts. (#617)

     fetchOperations: [
         prepareEntries
@@ -1186,15 +1281,7 @@
         fetchRecupereDocumentContractuelListx
         fetchVisualiserHistoConso

-        # Deactivate Edelia, while clientId isn't ready.
-        #fetchEdeliaToken
-        #fetchEdeliaProfile
-        #fetchEdeliaMonthlyElecConsumptions
-        #fetchEdeliaSimilarHomeYearlyElecComparisions
-        #fetchEdeliaElecIndexes
-        #fetchEdeliaMonthlyGasConsumptions
-        #fetchEdeliaSimilarHomeYearlyGasComparisions
-        #fetchEdeliaGasIndexes
+        fetchEdeliaData

         updateOrCreate logger, EDFClient, ['clientId', 'vendor']
         updateOrCreate logger, EDFContract, ['number', 'vendor']
@@ -1205,7 +1292,6 @@
         updateOrCreate logger, EDFBill, ['vendor', 'number']
         saveMissingBills
         buildNotifContent
-        #displayData
     ]

 # Helpers
@@ -1257,13 +1343,7 @@
         return callback 'request error' if err
         parser.parseString data, (err, result) ->
             return callback 'request error' if err
-            errorCode = getF result['ns:msgReponse'], \
-                        'ns:enteteSortie', 'ent:codeErreur'
-
-            if errorCode is '0' # means success.
-                errorCode = null
-
-            callback errorCode, result
+            callback null, result

 getEdelia = (accessToken, path, callback) ->


fix for default port 80/443 not being explicit

--- static/index.coffee
+++ static/index.coffee
@@ -14,7 +14,8 @@
   base = do () ->
     link = document.createElement 'a'
     link.setAttribute 'href', window.location.toString()
-    "#{link.protocol}//#{link.hostname}:#{link.port}#{link.pathname}"
+    link.port = ":#{link.port}"  if link.port.length
+    "#{link.protocol}//#{link.hostname}#{link.port}#{link.pathname}"

   getDomContent = () ->
     $content[0].innerText
@@ -127,7 +128,8 @@
     html = html.replace /src=(['"])/g, 'src=$1tastes/'
     $content.html html
     $('a', $content).each (index, link) ->
-      simplifiedLink = "#{link.protocol}//#{link.hostname}:#{link.port}#{link.pathname}"
+      link.port = ":#{link.port}"  if link.port.length
+      simplifiedLink = "#{link.protocol}//#{link.hostname}#{link.port}#{link.pathname}"
       if simplifiedLink.indexOf(base) is 0
         $(this).on 'click', (evt) ->
           evt.preventDefault()


Getting ids through scopes added; visibility managemenet improved

--- style.coffee
+++ style.coffee
@@ -88,6 +88,7 @@
 		if 'text' of @item or (@item.$ isnt null and 'text' of @item.$)
 			@updateText()

+		@item.visible = @visible
 		@updateVisibility()

 		for name of @attrs
@@ -103,6 +104,7 @@
 			if @isScope
 				@item.document.hide()
 			@item.parent = null
+		@item.visible = false

 		for child in @children
 			child.revert()
@@ -243,13 +245,18 @@
 					log.warn "Style file `#{id}` can't be find"
 				return
 		else
-			parent = @parent
 			while parent and not scope = parent.scope
 				parent = parent.parent

-			scope ?= windowStyle
-			@item = scope.ids[id] or scope.mainItem.$?[id]
-			@item ?= scope.styles(id)
+
+			parent = @parent
+			loop
+				scope = parent?.scope or windowStyle
+				@item = scope.ids[id] or scope.mainItem.$?[id]
+				@item ?= scope.styles(id)
+				if @item or scope is windowStyle
+					break
+				parent = parent.parent

 			unless @item
 				unless File.Input.test id


remove jshint errors

remove jshint errors

--- src/ramda-extras.coffee
+++ src/ramda-extras.coffee
@@ -1,6 +1,6 @@
 R = require 'ramda'
 lo = require 'lodash'
-{isNil, curry, concat, I, apply, compose, composeP, reduce, reduceRight, mapObjIndexed, chain, length, filter, slice, eqDeep, prop, get, has, func, keys, pickAll, merge, assoc, type, all, max, min, path, functions} = R #auto_require:funp
+{curry, concat, compose, composeP, reduce, reduceRight, mapObjIndexed, slice, eqDeep, prop, func, keys, pickAll, merge, type} = R #auto_require:funp

 # ----------------------------------------------------------------------------------------------------------
@@ -42,7 +42,7 @@
 	value = g.apply this, arguments
 	if isThenable value
 		if f._fail then value.fail (x) -> func.call context, x
-		else res.then (x) -> func.call context, x
+		else value.then (x) -> func.call context, x
 	else f.call this, value

 # _composeP = (f, g) ->


sort a copy instead or the original

sort a copy instead or the original

--- resources/assets/coffee/react/beatmap-discussions/discussions.coffee
+++ resources/assets/coffee/react/beatmap-discussions/discussions.coffee
@@ -148,7 +148,7 @@

   sortedDisussions: ->
-    discussions = @props.currentDiscussions[@props.mode]
+    discussions = @props.currentDiscussions[@props.mode].slice(0)
     discussions.sort sortPresets[@state.sortField].sort


delete test employees.coffee

delete test employees.coffee

--- test/dummy/app/assets/javascripts/employees.coffee
+++ test/dummy/app/assets/javascripts/employees.coffee
@@ -1,13 +0,0 @@
-# $ ->
-#   $(document).on 'blur', '.checkMyBsn',  ->
-#     if new ElfProef(@value).validBsn()
-#       $(".bsn_message").text("ok")
-#     else
-#       $(".bsn_message").text("geen geldig bsn-nummer")
-
-#    $(document).on 'blur', '.checkMyRekening',  ->
-#     if new ElfProef(@value).validRekening()
-#       $(".rekening_message").text("ok")
-#     else
-#       $(".rekening_message").text("geen geldig rekening-nummer")
-


fix teapsoon spec

fix teapsoon spec

--- test/javascripts/routes/paper_index_test.js.coffee
+++ test/javascripts/routes/paper_index_test.js.coffee
@@ -3,16 +3,16 @@
 moduleFor 'route:paperIndex', 'Unit: route/PaperIndex',
   needs: ['model:paper', 'route:paper'],
   setup: ->
-    sinon.stub(@subject(), 'transitionTo')
+    sinon.stub(@subject(), 'replaceWith')

 test 'transition to edit route if paper has been not submitted', ->
   unsubmittedPaper = Ember.Object.create submitted: false
   @subject().afterModel unsubmittedPaper

-  ok @subject().transitionTo.calledWithExactly('paper.edit', unsubmittedPaper)
+  ok @subject().replaceWith.calledWithExactly('paper.edit', unsubmittedPaper)

 test 'does not transition to edit route if paper has been submitted', ->
   submittedPaper = Ember.Object.create submitted: true
   @subject().afterModel submittedPaper

-  ok !@subject().transitionTo.called
+  ok !@subject().replaceWith.called


Add World/Screen coordinate conversion utilities to SceneView class.

--- src/scene/sceneView.coffee
+++ src/scene/sceneView.coffee
@@ -5,7 +5,38 @@
     throw new Error "A Canvas is required" if not @canvas
     @context = @canvas.getContext("2d")
     throw new Error "Could not retrieve Canvas context" if not @context
-    @camera = new Rect(0,0,100,100)
+    @context.webkitImageSmoothingEnabled = false
+    @context.mozImageSmoothingEnabled = false
+
+    @camera = new Rect 10, 10, 10, 10
+    @cameraZoom = 1.0
+
+  worldToScreen: (value) ->
+    if value instanceof Rect
+      return new Rect(value).scale Screen.UNIT
+    else if value instanceof Point
+      return new Point(value).multiply Screen.UNIT
+    value * Screen.UNIT
+
+  screenToWorld: (value) ->
+    if value instanceof Rect
+      return new Rect(value).scale 1 / Screen.UNIT
+    else if value instanceof Point
+      return new Point(value).multiply 1 / Screen.UNIT
+    value * (1 / Screen.UNIT)

   render: (object) ->

+    # FPS Counter debug
+#    @context.save()
+#    @context.fillStyle = "rgba(255,255,255,0.7)";
+#    @context.font = "bold 24px Arial";
+#    @context.fillText("FPS: #{@scene.fps.toFixed(0)}",15,35);
+#    @context.restore()
+
+    # Camera Debug
+#    @context.save()
+#    @context.strokeStyle = "2px rgba(255,0,0,0.7)";
+#    screenCamera = @worldToScreen @camera
+#    @context.strokeRect(screenCamera.point.x,screenCamera.point.y,screenCamera.extent.x,screenCamera.extent.y);
+#    @context.restore()


Add command for starting timers.

Add command for starting timers.

--- src/scripts/harvest.coffee
+++ src/scripts/harvest.coffee
@@ -11,11 +11,21 @@
 #     you should set this to "yourcompany" (without the quotes).
 #
 # Commands:
-#   hubot remember my harvest account <email> with password <password> - Saves your Harvest credentials to allow Hubot to track time for you
+#   hubot remember my harvest account <email> with password <password> - Saves your Harvest credentials
+#                                                                        to allow Hubot to track time for you
 #   hubot forget my harvest account - Deletes your account credentials from Hubot's memory
-#   hubot start harvest at <project>: notes - TODO
+#   hubot start harvest at <project>/<task>: <notes> - Start a timer for a task at a project
+#                                                      (both of which may be abbreviated, Hubot
+#                                                      will ask you if your input is ambigious).
+#                                                      An existing timer (if any) will be stopped.
 #   hubot finish harvest at <project> - TODO
 #   hubot daily harvest [of <user>] - Hubot responds with your/a specific user's entries for today
+#
+# Notes:
+# All commands and command arguments are case-insenstive. If you work
+# on a project "FooBar", hubot will unterstand "foobar" as well. This
+# is also true for abbreviations, so if you don't have similary named
+# projects, "foob" will do as expected.
 #
 # Author:
 #   Quintus
@@ -70,6 +80,19 @@
       else
         msg.reply "Request failed with status #{status}."

+  # Kick off a new timer, stopping the previously running one, if any.
+  robot.respond /start harvest at (.+)\/(.+): (.*)/i, (msg) ->
+    project = msg.match[1]
+    task    = msg.match[2]
+    notes   = msg.match[3]
+    msg.message.user.harvest_account.start msg, project, task, notes, (status, body) ->
+      if 200 <= status <= 299
+        if body.hours_for_previously_running_timer?
+          msg.reply "Previously running timer stopped at #{body.hours_for_previously_running_timer}h."
+        msg.reply "Started tracking. Back to your work, #{msg.message.user.name}!"
+      else
+        msg.reply "Request failed with status #{status}."
+
 # Class managing the Harvest account associated with
 # a user. Keeps track of the user's credentials and can
 # be used to query the Harvest API on behalf of that user.
@@ -103,6 +126,18 @@
     this.request(msg).path("daily").get() (err, res, body) ->
       callback res.statusCode, JSON.parse(body)

+  # Issues /daily/add to the Harvest API to add a new timer
+  # starting from now.
+  start: (msg, target_project, target_task, notes, callback) ->
+    this.find_project_and_task msg, target_project, target_task, (project, task) =>
+      # OK, task and project found. Start the tracker.
+      data =
+        notes: notes
+        project_id: project.id
+        task_id: task.id
+      this.request(msg).path("/daily/add").post(JSON.stringify(data)) (err, res, body) ->
+        callback res.statusCode, JSON.parse(body)
+
   # (internal method)
   # Assembles the basic parts of a request to the Harvest
   # API, i.e. the Content-Type/Accept and authorization
@@ -115,3 +150,46 @@
       "Accept": "application/json"
     .auth(@email, @password)
     return req
+
+  # (internal method)
+  # Searches through all projects available to the sender of
+  # `msg` for a project whose name inclues `target_project`.
+  # If exactly one is found, query all tasks available for the
+  # sender in this projects, and if exactly one is found,
+  # execute the callback with the project object as the first
+  # and the task object as the second argument. If more or
+  # less than one project or task are found to match the query,
+  # reply accordingly to the user (the callback never gets
+  # executed in this case).
+  find_project_and_task: (msg, target_project, target_task, callback) ->
+    this.daily msg, (status, body) ->
+      # Search through all possible projects for the matching ones
+      projects = []
+      for project in body.projects
+        if project.name.toLowerCase().indexOf(target_project.toLowerCase()) != -1
+          projects.push(project)
+      # Ask the user if the project name is ambivalent
+      if projects.length == 0
+        msg.reply "Sorry, no matching projects found."
+        return
+      else if projects.length > 1
+        msg.reply "I found the following #{projects.length} projects for your query, please be more precise:"
+        for project in projects
+          msg.reply "* #{project.name}"
+        return
+
+      # Repeat the same process for the tasks
+      tasks = []
+      for task in projects[0].tasks
+        if task.name.toLowerCase().indexOf(target_task.toLowerCase()) != -1
+          tasks.push(task)
+      if tasks.length == 0
+        msg.reply "Sorry, no matching tasks found."
+      else if tasks.length > 1
+        msg.reply "I found the following #{tasks.length} tasks for your query, please be more pricese:"
+        for task in tasks
+          msg.reply "* #{task.name}"
+        return
+
+      # Execute the callback with the results
+      callback projects[0], tasks[0]


Added active listening to #NNN issues

Added active listening to #NNN issues

Added `hear` command that allows the bot to listen to `#NNNN` issue numbers and prints information about the ticket.
This also adds a ignored users option so that the robot does not respond to other redmine plugins that may talk in the channel.

This was only tested in hipchat environments.

--- src/scripts/redmine.coffee
+++ src/scripts/redmine.coffee
@@ -1,5 +1,7 @@
 # Description:
 #   Showing of redmine issue via the REST API
+#   It also listens for the #nnnn format and provides issue data and link
+#   Eg. "Hey guys check out #273"
 #
 # Dependencies:
 #   None
@@ -8,6 +10,7 @@
 #   HUBOT_REDMINE_SSL
 #   HUBOT_REDMINE_BASE_URL
 #   HUBOT_REDMINE_TOKEN
+#   HUBOT_REDMINE_IGNORED_USERS
 #
 # Commands:
 #   hubot (redmine|show) me <issue-id> - Show the issue status
@@ -220,6 +223,31 @@
             _.push "    #{journal.notes}\n"

       msg.reply _.join "\n"
+
+  # Listens to #NNNN and gives ticket info
+  robot.hear /.*(#(\d+)).*/, (msg) ->
+    id = msg.match[1].replace /#/, ""
+
+    ignoredUsers = process.env.HUBOT_REDMINE_IGNORED_USERS or ""
+
+    #Ignore cetain users, like Redmine plugins
+    if msg.message.user.name in ignoredUsers.split(',')
+      return
+
+    if isNaN(id)
+      return
+
+    params = []
+
+    redmine.Issue(id).show params, (err, data, status) ->
+      unless status == 200
+        # Issue not found, don't say anything
+        return false
+
+      issue = data.issue
+
+      url = "#{redmine.url}/issues/#{id}"
+      msg.send "#{issue.tracker.name} ##{issue.id} (#{issue.project.name}): #{issue.subject} (#{issue.status.name}) [#{issue.priority.name}]"

 # simple ghetto fab date formatter this should definitely be replaced, but didn't want to
 # introduce dependencies this early


Change youtube script to use robot.http

Change youtube script to use robot.http

--- src/scripts/youtube.coffee
+++ src/scripts/youtube.coffee
@@ -6,7 +6,7 @@
 module.exports = (robot) ->
   robot.respond /(youtube|yt)( me)? (.*)/i, (msg) ->
     query = msg.match[3]
-    msg.http("http://gdata.youtube.com/feeds/api/videos")
+    robot.http("http://gdata.youtube.com/feeds/api/videos")
       .query({
         orderBy: "relevance"
         'max-results': 15


Remove math.coffee for now, since the backend google thing has been taken down

--- src/scripts/math.coffee
+++ src/scripts/math.coffee
@@ -1,22 +0,0 @@
-# Description:
-#   Allows Hubot to do mathematics.
-#
-# Commands:
-#   hubot math me <expression> - Calculate the given expression.
-#   hubot convert me <expression> to <units> - Convert expression to given units.
-module.exports = (robot) ->
-  robot.respond /(calc|calculate|calculator|convert|math|maths)( me)? (.*)/i, (msg) ->
-    msg
-      .http('https://www.google.com/ig/calculator')
-      .query
-        hl: 'en'
-        q: msg.match[3]
-      .headers
-        'Accept-Language': 'en-us,en;q=0.5',
-        'Accept-Charset': 'utf-8',
-        'User-Agent': "Mozilla/5.0 (X11; Linux x86_64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1"
-      .get() (err, res, body) ->
-        # Response includes non-string keys, so we can't use JSON.parse here.
-        json = eval("(#{body})")
-        msg.send json.rhs || 'Could not compute.'
-