Set sidebar contents max-height on window resize

Closes #124.

--- src/views.coffee
+++ src/views.coffee
@@ -171,18 +171,22 @@
         add_listeners: ->
             @listenTo @search_results, 'reset', ->
                 @change 'search'
+
             @listenTo @selected_units, 'reset', (unit, coll, opts) ->
                 unless @selected_units.isEmpty()
                     @change 'details'
             @listenTo @selected_units, 'remove', (unit, coll, opts) ->
                 @change null

-            # Listeners for event view
             @listenTo @selected_events, 'reset', (unit, coll, opts) ->
                 unless @selected_events.isEmpty()
                     @change 'event'
             @listenTo @selected_events, 'remove', (unit, coll, opts) ->
                 @change null
+
+            $(window).resize( =>
+                @contents.currentView?.set_max_height?()
+            )
         right_edge_coordinate: ->
             if @opened
                 @$el.offset().left + @$el.outerWidth()


remove artwork delta call

remove artwork delta call

--- apps/artwork/client/view.coffee
+++ apps/artwork/client/view.coffee
@@ -9,7 +9,6 @@
 CurrentUser = require '../../../models/current_user.coffee'
 SaveButton = require '../../../components/save_button/view.coffee'
 RelatedArticlesView = require '../../../components/related_articles/view.coffee'
-analytics = require '../../../lib/analytics.coffee'
 { acquireArtwork } = require '../../../components/acquire/view.coffee'
 BelowTheFoldView = require './below_the_fold.coffee'
 { trackArtworkImpressions } = require '../../../components/analytics/impression_tracking.coffee'
@@ -99,10 +98,6 @@

     @belowTheFoldView.setupFair fair

-    $scripts = $('#scripts')
-    analytics.delta 'fair_artist_view', { fair: fair.get('_id'), id: @artist.get('_id') }, $scripts
-    analytics.delta 'fair_partner_view', { fair: fair.get('_id'), id: @artwork.get('partner')._id }, $scripts
-
   handleSales: (sales) ->
     return unless sales.length


IS ROb Home

IS ROb Home

--- src/scripts/main.coffee
+++ src/scripts/main.coffee
@@ -6,7 +6,7 @@

 class RobCountdown

-  date_rob_arrives: (new Date(2015, 11, 14, 9, 0, 0, 0))
+  date_rob_arrives: Date.now

   start: ->
     @isHeBack()


Removing before and after function composing helpers

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -1,25 +1,4 @@
 module "Function"
-
-test "#before and #after", ->
-  calledAfter = false
-  calledBefore = false
-
-  testBefore = ->
-    calledBefore = true
-
-  testAfter = ->
-    calledAfter = true
-
-  testFn = ->
-    ok calledBefore, "Called before function while inside test function"
-    ok !calledAfter, "Have not called after function while inside test function"
-
-  testFn = testFn.withBefore(testBefore).withAfter(testAfter)
-
-  ok !calledBefore, "Haven't called before yet"
-  ok !calledAfter, "Haven't called after yet"
-  testFn()
-  ok calledAfter, "Called after"

 test "#once", ->
   score = 0


Refactor tile synthesis into smaller, more semantic functions

--- src/scripts/tael.coffee
+++ src/scripts/tael.coffee
@@ -16,42 +16,58 @@

 newTile = (tiles, parent_id) ->
     # Add a new tile to the `tiles` array.
+    setTileClass = (tile, float) ->
+        # Set the class of a tile which is represented as a `ref`.
+        tile.value.content.dom.attr 'class', 'tael-node-leaf-' + float
+
+    setTileId = (tile, index) ->
+        # Set the ID of a tile which is represented as a `ref`.
+        tile.value.content.dom.attr 'id', 'tael-node-' + index
+
     pushTile = (parent) ->
-        # Push a new tile to the `tiles` array and return its index.
-        index = (
+        # Push a new tile to the `tiles` array and return its index. Not safe
+        # to use without some sort of wrapper, because no ID or class is
+        # attached to the created tile in the DOM.
+        (
             tiles.push new ref
                 type: 'leaf'
                 content:
                     dom:
                         ($ '<div>')
-                        .addClass 'tael-node-leaf'
                         .text 'Hello, world! I am a leaf node.'
                         .appendTo parent.tile.value.content.dom
                     data: null
         ) - 1
-        tiles[index].value.content.dom.attr 'id', 'tael-node-'+index
+
+    createTile = (parent, float) ->
+        # Append a new tile to the `tiles` array and return its index.
+        index = pushTile parent
+        tile = tiles[index]
+
+        setTileClass tile, float
+        setTileId tile, index
         index

     addLeafToContainer = (parent) ->
         # Create only one tile and add it as the child of the supplied parent.
         # As the container tile is the top-level tile, it has only a single
         # child tile.
-        parent.tile.value.child = pushTile parent
+        parent.tile.value.child = createTile parent, 'clear'

     addLeavesToLeaf = (parent) ->
         # Create two tiles; convert the 'leaf' tile into a 'branch' tile
         # before inserting the two new 'leaf' tiles into the newly-classified
         # 'branch' tile.
         emptyInnerHtml = (jquery_element) ->
-            return jquery_element.html ''
+            jquery_element.html ''

         emptyInnerHtml parent.tile.value.content.dom

         parent.tile.value =
             type: 'branch'
             children:
-                left: pushTile parent
-                right: pushTile parent
+                left: createTile parent, 'left'
+                right: createTile parent, 'right'
             content:
                 dom:
                     parent.tile.value.content.dom


chore (Module) Print better debug on unknown error

--- src/steroids/httpRequest.coffee
+++ src/steroids/httpRequest.coffee
@@ -10,7 +10,7 @@
       else if res.statusCode == 200 or res.statusCode == 201
         resolve data
       else
-        reject new Error "Failed with unknown error"
+        reject new Error "Failed with unknown error, HTTP status '#{res.statusCode}', url '#{res.request.uri.href}'"
     )

 runAuthenticated = (method, url) ->


Fixing broken test. Linux is not forgiving of change of casing in a path. Apparently Mac version is.

--- test/unit/SpurErrorsSpec.coffee
+++ test/unit/SpurErrorsSpec.coffee
@@ -4,12 +4,10 @@

   beforeEach ->

-
   it "should exist", ->
     expect(SpurErrors).to.exist

   it "test error", ->
-

     notFound1 = SpurErrors.NotFoundError.create("could not find it")

@@ -22,11 +20,10 @@
     console.log e

   it "test callee", ->
-    callee = require "../Fixtures/Callee"
+    callee = require "../fixtures/Callee"
     console.error callee.run().stack

   it "errorByStatusCode()", ->
     expect(SpurErrors.errorByStatusCode(999)).to.equal undefined
     expect(SpurErrors.errorByStatusCode(503))
       .to.equal SpurErrors.ServiceUnavailableError
-


Fixed use of i18n in popover

Fixed use of i18n in popover

--- app/views/clans/ClansView.coffee
+++ app/views/clans/ClansView.coffee
@@ -72,16 +72,16 @@
     @supermodel.addRequestResource('user_names', options, 0).load()

   setupPrivateInfoPopover: ->
-    popoverTitle = "
" + data-i18n="teachers.sub_includes_7" + "
"
+    popoverTitle = "
" + $.i18n.t('teachers.sub_includes_7') + "
"
     popoverContent = "
"
-    popoverContent += "
" + data-i18n="clans.track_concepts1" + " " + data-i18n="clans.track_concepts2b"
-    popoverContent += "
" + data-i18n="clans.track_concepts3b"
-    popoverContent += "
" + data-i18n="clans.track_concepts4b" + " " + data-i18n="clans.track_concepts5" + ""
-    popoverContent += "
" + data-i18n="clans.track_concepts6b"
-    popoverContent += "
" + data-i18n="clans.track_concepts7" + " " + data-i18n="clans.track_concepts8"
+    popoverContent += "
" + $.i18n.t('clans.track_concepts1') + " " + $.i18n.t('clans.track_concepts2b')
+    popoverContent += "
" + $.i18n.t('clans.track_concepts3b')
+    popoverContent += "
" + $.i18n.t('clans.track_concepts4b') + " " + $.i18n.t('clans.track_concepts5') + ""
+    popoverContent += "
" + $.i18n.t('clans.track_concepts6b')
+    popoverContent += "
" + $.i18n.t('clans.track_concepts7') + " " + $.i18n.t('clans.track_concepts8')
     popoverContent += "
"
     popoverContent += "

"
-    popoverContent += "
" + data-i18n="clans.private_require_sub" + "
"
+    popoverContent += "
" + $.i18n.t('clans.private_require_sub') + "
"
     @$el.find('.private-more-info').popover(
       animation: true
       html: true


determine when worked late

determine when worked late

--- tutor/src/components/student-dashboard/event-info-icon.cjsx
+++ tutor/src/components/student-dashboard/event-info-icon.cjsx
@@ -19,11 +19,12 @@
     isIncomplete = event.complete_exercise_count isnt event.exercise_count
     pastDue      = event.type is 'homework' and dueAt.isBefore(now)
     isDueToday   = now.isBetween(dueAt.clone().subtract(1, 'day'), dueAt)
+    workedLate   = moment(event.last_worked_at).isAfter(dueAt)

-    unless @props.event.type is 'homework' and ( pastDue or (isIncomplete and isDueToday))
+    unless @props.event.type is 'homework' and ( (isIncomplete or workedLate) and (pastDue or isDueToday ))
       return null

-    status = if isDueToday then 'incomplete' else 'late'
+    status = if (workedLate or pastDue) then 'late' else 'incomplete'

     tooltip =
       <BS.Tooltip id="event-info-icon-#{event.id}">


log(sync-state): Send `Account State Delta` to mixpanel

--- internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee
+++ internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee
@@ -150,6 +150,11 @@
   _handleAccountDeltas: (deltas) =>
     for delta in deltas
       Actions.updateAccount(delta.account_id, {syncState: delta.sync_state})
+      Actions.recordUserEvent('Account State Delta', {
+        accountId: delta.account_id
+        accountEmail: delta.email_address
+        syncState: delta.sync_state
+      })

   _handleDeltaDeletion: (delta) =>
     klass = NylasAPI._apiObjectToClassMap[delta.object]


Prevent server crash in some situation

Prevent server crash in some situation

--- server/models/account.coffee
+++ server/models/account.coffee
@@ -358,13 +358,10 @@

         , (err, {mailboxes, counts, totalUnread}) ->
             return callback err if err
-
-
-
             rawObject.totalUnread = totalUnread
             rawObject.mailboxes = mailboxes.map (row) ->
                 box = row.doc
-                id = box.id or row.id
+                id = box?.id or row.id
                 count = counts[id]
                 return clientBox =
                     id       : id


Fixed #2464. Well, let's see if it works.

--- server/queues/scoring.coffee
+++ server/queues/scoring.coffee
@@ -262,8 +262,6 @@
     'level.original': levelID
     'level.majorVersion': levelMajorVersion
     submitted: true
-    submittedCode:
-      $exists: true
     team: opposingTeam

   sortParameters =
@@ -609,8 +607,6 @@
       'level.original': levelOriginalID
       'level.majorVersion': levelMajorVersion
       submitted: true
-      submittedCode:
-        $exists: true
       team: opposingTeam

     if opponentSessionTotalScore < 30


so there _was_ a reason to set the id on the object itself

--- app/adapters/application.coffee
+++ app/adapters/application.coffee
@@ -52,6 +52,7 @@

     createRecord: (store, type, record) -> new Promise (resolve) =>
         key = guid()
+        record.id = key
         value = serialize record, store.serializerFor type.typeKey

         localStorage.setItem key, value


debug output removed

debug output removed

--- app/charts/bar-hierarchy.coffee
+++ app/charts/bar-hierarchy.coffee
@@ -374,7 +374,6 @@

   updateBreadcrumb = (node) ->
     path = breadcrumbPath node
-    console.log path

     breadcrumbList = breadcrumb.select("ul")
     # enter


missed on renaming

missed on renaming

--- app/components/eui-popup.coffee
+++ app/components/eui-popup.coffee
@@ -94,7 +94,7 @@
     popup.container = popup.get('targetObject.container')
     popup.appendTo '.ember-application'

-    popup.updateHeight()
+    popup.updateListHeight()

     Ember.run.next this, -> @position(options.targetObject, popup)
     popup


Fixes problenm with build_expression_from_javascript_object

--- math/build_expression_from_javascript_object_spec.coffee
+++ math/build_expression_from_javascript_object_spec.coffee
@@ -58,7 +58,22 @@
     expect(expression).toBeInstanceOf @components.expression
     expect(sub_exp).toBeInstanceOf @components.expression

-  it "uses object syntax with label 'open_expression' to signify an open expression", ->
-    expression = @builder open_expression: [10]
-    sub_exp = expression.first()
-    expect(sub_exp.isOpen()).toEqual(true)
+  describe "open expressions", ->
+    it "uses object syntax with label 'open_expression' to signify an open expression", ->
+      expression = @builder open_expression: [10]
+      sub_exp = expression.first()
+      expect(sub_exp.isOpen()).toEqual(true)
+
+    it "correctly constructs nested open expressions", ->
+      expression = @builder open_expression: open_expression: [10]
+
+      first_open = expression.first()
+      expect(first_open.isOpen()).toEqual(true)
+
+      second_open = first_open.first()
+      expect(second_open.isOpen()).toEqual(true)
+
+      ten = second_open.first()
+      expect(ten.value()).toEqual 10
+
+


fix redactor plugins;

fix redactor plugins;

--- app/assets/javascripts/redactor/input-redactor_character.coffee
+++ app/assets/javascripts/redactor/input-redactor_character.coffee
@@ -24,37 +24,31 @@
   _redactor_options: ->
     @_trigger_change = true

+    config = @_get_default_config()
     @config.redactorOptions ?= {}

-    # workaround plugins & custom configuration to include loft
-    # and optional fixedtoolbar
-    plugins = @config.redactorOptions.plugins || []
-    delete @config.redactorOptions.plugins
+    $.extend(config, @config.redactorOptions)

-    if ! chr.isMobile()
-      plugins.push('fixedtoolbar')
+    if (! chr.isMobile()) && config.plugins.indexOf('fixedtoolbar') == -1
+      config.plugins.push('fixedtoolbar')

-    if Loft?
-      plugins.push('loft')
-
-    config = @_get_default_config(plugins)
+    if Loft? && config.plugins.indexOf('loft') == -1
+      config.plugins.push('loft')

     if chr.isMobile()
       config.toolbarFixed = false
       # config.toolbarFixedTopOffset = 40

-    $.extend(config, @config.redactorOptions)
-
     return config

-  _get_default_config: (plugins) ->
+  _get_default_config: () ->
     focus:            false
     imageFloatMargin: '20px'
     buttonSource:     true
     pastePlainText:   true
     scrollTarget:     chr.module.view.$content
-    plugins:          plugins
+    plugins:          []
     buttons:          [ 'html',
                         'formatting',
                         'bold',
@@ -64,7 +58,6 @@
                         'orderedlist',
                         'link' ]

-
     # to have caching working we need to trigger 'change' event for textarea
     # when content got changed in redactor, but skip this when updating value
     # via `updateValue` method
@@ -73,7 +66,6 @@
         @$input.trigger('change')
       @_trigger_change = true

-
     initCallback: ->
       new RedactorImages(this)


mock out router

mock out router

--- tutor/specs/components/helpers/component-testing.coffee
+++ tutor/specs/components/helpers/component-testing.coffee
@@ -1,3 +1,6 @@
+jest.mock('../../../src/helpers/router')
+Router = require '../../../src/helpers/router'
+
 _ = require 'underscore'
 chai = require('chai')
 expect = chai.expect
@@ -18,6 +21,7 @@
 sandbox = null
 Sinon = {}

+
 ROUTER = null
 CURRENT_ROUTER_PARAMS = null
 CURRENT_ROUTER_PATH   = null
@@ -26,6 +30,9 @@
 beforeEach ->
   sandbox = sinon.sandbox.create()
   ROUTER  = new TestRouter
+  Router.currentParams.mockReturnValue({})
+  Router.currentQuery.mockReturnValue({})
+
 afterEach ->
   sandbox.restore()

@@ -51,6 +58,8 @@
     CURRENT_ROUTER_PARAMS = options.routerParams or {}
     CURRENT_ROUTER_QUERY = options.routerQuery or {}
     CURRENT_ROUTER_PATH   = options.routerPath   or '/'
+    Router.currentParams.mockReturnValue(CURRENT_ROUTER_PARAMS)
+    Router.currentQuery.mockReturnValue(CURRENT_ROUTER_QUERY)
     root = document.createElement('div')
     promise = new Promise( (resolve, reject) ->
       props = _.clone(options.props)
@@ -98,7 +107,7 @@

 module.exports = {
-  Testing, sinon, React, _, ReactTestUtils,
+  Testing, sinon, React, ReactDOM, _, ReactTestUtils,
   spyOnComponentMethod, stubComponentMethod,
   SnapShot, pause, Wrapper
 }


only set router mocks if it's mocked elsewhere

Some specs don't like it being mocked globally, specs/components/new-course/build-course.spec.cjsx

--- tutor/specs/components/helpers/component-testing.coffee
+++ tutor/specs/components/helpers/component-testing.coffee
@@ -1,4 +1,3 @@
-jest.mock('../../../src/helpers/router')
 Router = require '../../../src/helpers/router'

 _ = require 'underscore'
@@ -30,8 +29,6 @@
 beforeEach ->
   sandbox = sinon.sandbox.create()
   ROUTER  = new TestRouter
-  Router.currentParams.mockReturnValue({})
-  Router.currentQuery.mockReturnValue({})

 afterEach ->
   sandbox.restore()
@@ -58,8 +55,8 @@
     CURRENT_ROUTER_PARAMS = options.routerParams or {}
     CURRENT_ROUTER_QUERY = options.routerQuery or {}
     CURRENT_ROUTER_PATH   = options.routerPath   or '/'
-    Router.currentParams.mockReturnValue(CURRENT_ROUTER_PARAMS)
-    Router.currentQuery.mockReturnValue(CURRENT_ROUTER_QUERY)
+    Router.currentParams.mockReturnValue?(CURRENT_ROUTER_PARAMS)
+    Router.currentQuery.mockReturnValue?(CURRENT_ROUTER_QUERY)
     root = document.createElement('div')
     promise = new Promise( (resolve, reject) ->
       props = _.clone(options.props)


Use TutorInput directly

Use TutorInput directly

--- tutor/src/components/course-settings/rename-course.cjsx
+++ tutor/src/components/course-settings/rename-course.cjsx
@@ -6,34 +6,31 @@
 {TutorInput} = require '../tutor-input'
 classnames = require 'classnames'

-RenameCourseField = React.createClass
+# RenameCourseField = React.createClass

-  displayName: 'RenameCourseField'
-  propTypes:
-    courseId: React.PropTypes.string
-    label: React.PropTypes.string.isRequired
-    name: React.PropTypes.string.isRequired
-    default: React.PropTypes.string.isRequired
-    onChange: React.PropTypes.func.isRequired
-    autofocus: React.PropTypes.bool
-    validate: React.PropTypes.func.isRequired
+#   displayName: 'RenameCourseField'
+#   propTypes:
+#     courseId: React.PropTypes.string
+#     label: React.PropTypes.string.isRequired
+#     name: React.PropTypes.string.isRequired
+#     default: React.PropTypes.string.isRequired
+#     onChange: React.PropTypes.func.isRequired
+#     autofocus: React.PropTypes.bool
+#     validate: React.PropTypes.func.isRequired

-  componentDidMount: ->
-    @refs.input.focus() if @props.autofocus
-    @refs.input.cursorToEnd() if @props.autofocus

-  onChange: (value) ->
-    @props.onChange(value)
+#   onChange: (value) ->
+#     @props.onChange(value)

-  render: ->
-    <TutorInput
-      ref='input'
-      label={@props.label}
-      default={@props.default}
-      required={true}
-      onChange={@onChange}
-      validate={@props.validate}
-      />
+#   render: ->
+#     <TutorInput
+#       ref='input'
+#       label={@props.label}
+#       default={@props.default}
+#       required={true}
+#       onChange={@onChange}
+#       validate={@props.validate}
+#       />

 RenameCourse = React.createClass
   propTypes:
@@ -75,13 +72,14 @@
       </BS.Modal.Header>

-        <RenameCourseField
-        label='Course Name'
-        name='course-name'
-        default={@props.course.name}
-        onChange={(val) => @setState(course_name: val)}
-        validate={@validate}
-        autofocus />
+        <TutorInput
+          label='Course Name'
+          name='course-name'
+          default={@props.course.name}
+          onChange={(val) => @setState(course_name: val)}
+          validate={@validate}
+          autofocus
+        />


Fix branch switch protection

Fix branch switch protection

--- lib/advanced-web-editor.coffee
+++ lib/advanced-web-editor.coffee
@@ -420,7 +420,7 @@
           action = 'publish' if state.state == 'unpublished'
           branches = ''
           branches = "Involved branches: " + state.branches.join(",") + ".\n" if state.branches?
-
+
           atom.confirm
             message: "Detected #{state.state} changes."
             detailedMessage: "#{branches}Do you want to #{action} them now?"
@@ -441,7 +441,6 @@
                 deferred.resolve true

         else
-          @lifeCycle.statusReady()
           return @lifeCycle.updateMaster()
       .then () =>
         return @lifeCycle.updateDevelop() if !keepEditing


Clarify error message.

Clarify error message.

--- app/javascripts/modeling.coffee
+++ app/javascripts/modeling.coffee
@@ -12,13 +12,12 @@
         if not assocPcl?
           assocPcl = accum[modeling.protocol] = {}
         if not modeling.source?
-          throw new ReferenceError("Modeling does not have a source" +
-                                   " attribute:" +
-                                   " #{ Object.keys(modeling) }")
+          throw new ReferenceError("The modeling object does not reference" +
+                                   " a source")
         pairs = _.toPairs(modeling.source)
         if pairs.length > 1
-          throw new ReferenceError("Modeling source cannot reference" +
-                                   " more than one source type:" +
+          throw new ReferenceError("The modeling source references more" +
+                                   " than one source type:" +
                                    " #{ Object.keys(modeling.source) }")
         [srcType, srcId] = pairs[0]
         assocSrc = assocPcl[srcType]


Properly normalized data for job:received pusher event

--- assets/scripts/app/pusher.coffee
+++ assets/scripts/app/pusher.coffee
@@ -88,7 +88,7 @@
     switch event
       when 'build:started', 'build:finished'
         data
-      when 'job:created', 'job:started', 'job:requeued', 'job:finished', 'job:log', 'job:canceled'
+      when 'job:created', 'job:started', 'job:requeued', 'job:finished', 'job:log', 'job:canceled', 'job:received'
         data.queue = data.queue.replace('builds.', '') if data.queue
         { job: data }
       when 'worker:added', 'worker:updated', 'worker:removed'


Support for groups

Support for groups

Added support for fetching all events of a group when adding a group.

--- app/meetup.coffee
+++ app/meetup.coffee
@@ -1,21 +1,24 @@
 ###
-Module to fetch meetup events
+meetup is a module that handles fetching events and groups from Meetup.com.
+It handles single event URLs as well as group URLs. It only fetches open events.
+The Meetup.com API has some rate limiting, which might prevent the module from
+fetching.
+
+@example usage
+	meetup = require 'meetup'
+
+	testEventHandle = () ->
+		meetup.handle
+			url: "http://www.meetup.com/NNUG-Bergen/events/221170619/"
+			onSuccess: (result) ->
+				console.log "RESULT", result
+			onError: (error) ->
+				console.log error.error
+
+	testEventHandle()

 @author Snorre Magnus Davøen
-###
-
-###
-{
-	"title": "Event title",
-	"source": "http://url.com",
-	"date": [timestamp],
-	"location": {
-	"address": "Addressen til greien",
-	"name": "hvis det er et navn på stedet (f.eks Kvarteret/Landmark)",
-	"lon": 10
-	"lat": 10
-	}
-}
+@copyright Kompiler 2015
 ###

 muAPIKey = (require '../config.coffee').meetup
@@ -40,14 +43,20 @@
 # @param [string] id
 # @param [function] onError
 # @param [function] onSuccess
-getEvents = (eventQuery) ->
-	mu.getEvents
-		event_id: eventQuery.ids
+# @private
+_getEvents = (eventQuery) ->
+	if eventQuery.ids?
+		muQuery = event_id: eventQuery.ids
+	if eventQuery.name?
+		muQuery = group_urlname: eventQuery.name
+
+	mu.getEvents(
+		muQuery
 		(error, events) ->
 			if error
 				eventQuery.onError error
 			else
-				eventQuery.onSuccess events.results
+				eventQuery.onSuccess events.results)

 # Get Meetup group by group 'urlname'
@@ -55,9 +64,9 @@
 # @param [string] name
 # @param [function] onError
 # @param [function] onSuccess
-getGroup = (groupQuery) ->
+_getGroups = (groupQuery) ->
 	mu.getGroups
-		group_urlname: groupQuery.name
+		group_urlname: groupQuery.names
 		(error, groups) ->
 			if error
 				groupQuery.onError error
@@ -67,7 +76,8 @@

 # Parse a Meetup event JSON object
 # @param [object] event
-parseEvent = (event) ->
+_parseEvent = (event) ->
+	id: "meetup-event-#{event.id}"
 	title: event.name
 	source: event.event_url
 	date: new Date(event.time)
@@ -80,8 +90,10 @@

 # Parse a Meetup group JSON object
 # @param [object] group
-parseGroup = (group) ->
+_parseGroup = (group) ->
+	id: "meetup-group-#{group.id}"
 	source: group.link
+	raw: group

 # Handle a Meetup event add query
@@ -89,18 +101,42 @@
 # @option query [object] event
 # @option query [function] onSuccess
 # @option query [function] onError
-handleEvent = (query) ->
+# @private
+_handleEvent = (query) ->
 	id = (muRegex.exec query.url)[1]
-	getEvents
+	_getEvents
 		ids: [id]
 		onError: (error) ->
 			query.onError
 				error: error
 				module: 'meetup'
 		onSuccess: (muEvents) ->
-			event = parseEvent muEvents[0]
+			event = _parseEvent muEvents[0]
 			db.set
 				events: [event]
+				onSuccess: query.onSuccess
+				onError: query.onError
+
+
+# Handle Meetup group events
+# @param [object] query
+# @option query [object] event
+# @option query [function] onSuccess
+# @option query [function] onError
+# @private
+_handleGroupEvents = (query) ->
+	_getEvents
+		name: query.name
+		onError: (error) ->
+			query.onError
+				error: error
+				module: 'meetup'
+		onSuccess: (muEvents) ->
+			events = []
+			for event in muEvents
+				events.push _parseEvent event
+			db.set
+				events: events
 				onSuccess: query.onSuccess
 				onError: query.onError

@@ -110,26 +146,22 @@
 # @option query [object] event
 # @option query [function] onSuccess
 # @option query [function] onError
-handleGroup = (query) ->
+_handleGroup = (query) ->
 	name = (muGroupRegex.exec query.url)[1]
-	getGroup
-		name: name
+	_getGroups
+		names: [name]
 		onError: (error) ->
 			query.onError
 				error: error
 				module: 'meetup'
 		onSuccess: (muGroup) ->
-			group = parseGroup muGroup
+			group = _parseGroup muGroup
+			query.name = group.raw.urlname
 			db.setGroup
 				group: group
-				onSuccess: query.onSuccess
+				onSuccess: () -> _handleGroupEvents query
 				onError: query.onError

-
-# Check if this is a Meetup url
-# @param [string] url
-#exports.canHandle = (url) ->
-#  return url.match(muRegex)? or url.match(muGroupRegex)?

 # Import and url
 # @param [object] query
@@ -138,13 +170,11 @@
 # @option query [function] onError
 exports.handle = (query) ->
 	unless query.url.match(muRegex)? or query.url.match(muGroupRegex)?
-		console.log "is false"
 		return false
 	else
 		isEvent = query.url.match(muRegex)?
 		if isEvent
-			console.log "Is event true"
-			handleEvent query
+			_handleEvent query
 		else
-			handleGroup query
+			_handleGroup query
 		return true


'support cd'

'support cd'

--- lib/command-output-view.coffee
+++ lib/command-output-view.coffee
@@ -3,6 +3,8 @@
 ansihtml = require 'ansi-html-stream'
 readline = require 'readline'
 {addClass, removeClass} = require 'domutil'
+{resolve} = require 'path'
+fs = require 'fs'

 lastOpenedView = null

@@ -28,6 +30,7 @@
         @subview 'cmdEditor', new EditorView(mini: true, placeholderText: 'input your command here')

   initialize: ->
+    @cwd = atom.project.path
     atom.workspaceView.command "cli-status:toggle-output", =>
       @toggle()

@@ -36,11 +39,6 @@
       htmlStream = ansihtml()
       @cmdEditor.hide()
       @cliOutput.append "\n$>#{inputCmd}\n"
-      showCmd = =>
-        @cmdEditor.show()
-        @cmdEditor.getEditor().selectAll()
-        @cmdEditor.focus()
-        @scrollToBottom()

       htmlStream.on 'data', (data) =>
         @cliOutput.append data
@@ -50,8 +48,10 @@
       inputCmd.replace /("[^"]*"|'[^']*'|[^\s'"]+)/g, (s) ->
         args.push s
       cmd = args.shift()
+      if cmd == 'cd'
+        @cd args
       try
-        @program = spawn cmd, args, stdio: 'pipe', env: process.env, cwd: atom.project.path
+        @program = spawn cmd, args, stdio: 'pipe', env: process.env, cwd: @cwd
         @program.stdout.pipe htmlStream
         @program.stderr.pipe htmlStream
         removeClass @statusIcon, 'status-success'
@@ -63,10 +63,10 @@
           removeClass @statusIcon, 'status-running'
           @program = null
           addClass @statusIcon, code == 0 and 'status-success' or 'status-error'
-          showCmd()
+          @showCmd()
         @program.on 'error', (err) =>
           @cliOutput.append err.message
-          showCmd()
+          @showCmd()
           addClass @statusIcon, 'status-error'
         @program.stdout.on 'data', () =>
           @flashIconClass 'status-info'
@@ -76,7 +76,13 @@

       catch err
         @cliOutput.append err.message
-        showCmd()
+        @showCmd()
+
+  showCmd: ->
+    @cmdEditor.show()
+    @cmdEditor.getEditor().selectAll()
+    @cmdEditor.focus()
+    @scrollToBottom()

   scrollToBottom: ->
     @cliOutput.scrollTop 10000000
@@ -123,3 +129,16 @@
       @close()
     else
       @open()
+
+  cd: (args)->
+    dir = resolve @cwd, args[0]
+    fs.stat dir, (err, stat) =>
+      @showCmd()
+      if err
+        if err.code == 'ENOENT'
+          return @cliOutput.append "cd: #{args[0]}: No such file or directory"
+        return @cliOutput.append err.message
+      if not stat.isDirectory()
+        return @cliOutput.append "cd: not a directory: #{args[0]}"
+      @cwd = dir
+      @cliOutput.append "cwd: #{@cwd}"


Fix block splitting for location ranges ending in a newline

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -172,10 +172,17 @@
         start.index += 1
       start.offset = 0

-      endBlock = @getBlockAtIndex(end.index)
-      end.offset = endBlock.findLineBreakInDirectionFromPosition("forward", end.offset)
-      unless end.offset is endBlock.getBlockBreakPosition()
-        @insertBlockBreakAtLocationRange(Trix.LocationRange.forLocationWithLength(end, 1))
+      if end.offset is 0 and end.index > start.index
+        end.index -= 1
+        end.offset = @getBlockAtIndex(end.index).getBlockBreakPosition()
+      else
+        endBlock = @getBlockAtIndex(end.index)
+        if endBlock.text.getStringAtRange([end.offset - 1, end.offset]) is "\n"
+          end.offset -= 1
+        else
+          end.offset = endBlock.findLineBreakInDirectionFromPosition("forward", end.offset)
+        unless end.offset is endBlock.getBlockBreakPosition()
+          @insertBlockBreakAtLocationRange(Trix.LocationRange.forLocationWithLength(end, 1))

     new Trix.LocationRange start, end


Fix attachment resizing

Fix attachment resizing

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -147,7 +147,7 @@
     attachments

   getTextAndRangeOfAttachment: (attachment) ->
-    @blockList.eachObject ({text}) ->
+    for {text} in @blockList.toArray()
       if range = text.getRangeOfAttachment(attachment)
         return {text, range}

@@ -160,11 +160,11 @@
   getAttachmentAtIndexById: (index, id) ->
     @getTextAtIndex(index).getAttachmentById(id)

-  resizeAttachmentToDimensions: (attachment) ->
+  resizeAttachmentToDimensions: edit (attachment, dimensions) ->
     {text} = @getTextAndRangeOfAttachment(attachment)
-    if index = @findIndexForText(text)
-      @blockList = @blockList.editObjectAtIndex index, (block) ->
-        block.copyWithText(text.resizeAttachmentToDimensions(attachment, dimensions))
+    index = @findIndexForText(text)
+    @blockList = @blockList.editObjectAtIndex index, (block) ->
+      block.copyWithText(text.resizeAttachmentToDimensions(attachment, dimensions))

   rangeFromLocationRange: (locationRange) ->
     leftPosition = @blockList.findPositionAtIndexAndOffset(locationRange.start.index, locationRange.start.position)


New method for destroy all instances of a determinate Hamsa.Class

--- source/hamsa.coffee
+++ source/hamsa.coffee
@@ -24,7 +24,7 @@
   @observer = undefined

   ###
-  Finds a determinate instance with a field attribute.
+  Returns all instances of the Class
   @method all
   @return {array}     Array of all repository instances.
   ###
@@ -32,6 +32,15 @@
     records = []
     records.push record for uid, record of @records
     return records
+
+  ###
+  Destroy all instances of the Class
+  @method destroyAll
+  @return {array}     Empty array of all repository instances.
+  ###
+  @destroyAll: ->
+    delete @records[uid] for uid of @records
+    return @records

   ###
   Finds a determinate instance with a field attribute.


Improve logging.

Improve logging.

--- app/assets/javascripts/application/app_controller.js.coffee
+++ app/assets/javascripts/application/app_controller.js.coffee
@@ -17,7 +17,7 @@
   seedsSyncControllerDidSync: () ->
     @update()
   update: () ->
-    logger.verbose 'AppController#update'
+    logger.verbose 'AppController#update current=', @current
     if @current == 'categories'
       @displayCategories()
     else if @current == 'pods'


when user creates new bucket idea from group page, then group field is preselected with that group

--- app/components/create-bucket-page/create-bucket-page.coffee
+++ app/components/create-bucket-page/create-bucket-page.coffee
@@ -9,6 +9,9 @@
   controller: (CurrentUser, Error, ipCookie, $location, Records, $scope, Toast, $window) ->
     $scope.accessibleGroups = CurrentUser().groups()
     $scope.bucket = Records.buckets.build()
+
+    if document.referrer
+      $scope.bucket.groupId = ipCookie('currentGroupId')

     $scope.cancel = () ->
       if ipCookie('currentGroupId')


lexer: improved consistency, preferring `charAt`, `slice` and single quotes

--- src/lexer.coffee
+++ src/lexer.coffee
@@ -105,7 +105,7 @@
   # Be careful not to interfere with ranges-in-progress.
   numberToken: ->
     return false unless number = @match NUMBER
-    return false if @tag() is '.' and starts number, '.'
+    return false if @tag() is '.' and number.charAt(0) is '.'
     @i += number.length
     @token 'NUMBER', number
     true
@@ -113,12 +113,12 @@
   # Matches strings, including multi-line strings. Ensures that quotation marks
   # are balanced within the string's contents, and within nested interpolations.
   stringToken: ->
-    return false unless starts(@chunk, '"') or starts(@chunk, "'")
+    return false unless @chunk.charAt(0) in ["'", '"']
     return false unless string =
       @balancedToken(['"', '"'], ['#{', '}']) or
       @balancedToken ["'", "'"]
     @interpolateString string.replace MULTILINER, '\\\n'
-    @line += count string, "\n"
+    @line += count string, '\n'
     @i += string.length
     true

@@ -148,7 +148,7 @@

   # Matches JavaScript interpolated directly into the source via backticks.
   jsToken: ->
-    return false unless starts @chunk, '`'
+    return false unless @chunk.charAt(0) is '`'
     return false unless script = @balancedToken ['`', '`']
     @token 'JS', script.slice 1, -1
     @i += script.length
@@ -251,13 +251,13 @@

   # Generate a newline token. Consecutive newlines get merged together.
   newlineToken: (newlines) ->
-    @token 'TERMINATOR', "\n" unless @tag() is 'TERMINATOR'
+    @token 'TERMINATOR', '\n' unless @tag() is 'TERMINATOR'
     true

   # Use a `\` at a line-ending to suppress the newline.
   # The slash is removed here once its job is done.
   suppressNewlines: ->
-    @tokens.pop() if @value() is "\\"
+    @tokens.pop() if @value() is '\\'
     true

   # We treat all other single characters as a token. Eg.: `( ) , . !`
@@ -327,10 +327,10 @@
         attempt = if match[1]? then match[1] else match[2]
         indent = attempt if not indent? or 0 < attempt.length < indent.length
     indent or= ''
-    doc = doc.replace(new RegExp("^" + indent, 'gm'), '')
+    doc = doc.replace(new RegExp('^' + indent, 'gm'), '')
     return doc if options.herecomment
-    doc = doc.replace(/^\n/, '')
-    doc.replace(MULTILINER, "\\n")
+    doc.replace(/^\n/, '')
+       .replace(MULTILINER, '\\n')
        .replace(new RegExp(options.quote, 'g'), "\\#{options.quote}")

   # A source of ambiguity in our grammar used to be parameter lists in function
@@ -372,8 +372,9 @@
     slash = delimited[0][0] is '/'
     levels = []
     i = 0
-    while i < str.length
-      if levels.length and starts str, '\\', i
+    slen = str.length
+    while i < slen
+      if levels.length and str.charAt(i) is '\\'
         i += 1
       else
         for pair in delimited
@@ -387,12 +388,12 @@
             levels.push(pair)
             i += open.length - 1
             break
-      break if not levels.length or slash and starts str, '\n', i
+      break if not levels.length or slash and str.charAt(i) is '\n'
       i += 1
     if levels.length
       return false if slash
       throw new Error "SyntaxError: Unterminated #{levels.pop()[0]} starting on line #{@line + 1}"
-    if not i then false else str.substring(0, i)
+    if not i then false else str[0...i]

   # Expand variables and expressions inside double-quoted strings using
   # [ECMA Harmony's interpolation syntax](http://wiki.ecmascript.org/doku.php?id=strawman:string_interpolation)
@@ -405,19 +406,20 @@
   # token stream.
   interpolateString: (str, options) ->
     options or= {}
-    if str.length < 3 or not starts str, '"'
+    if str.length < 3 or str.charAt(0) isnt '"'
       @token 'STRING', str
     else
       lexer   = new Lexer
       tokens  = []
-      quote   = str.substring 0, 1
+      quote   = str.charAt 0
       [i, pi] = [1, 1]
-      while i < str.length - 1
-        if starts str, '\\', i
+      end = str.length - 1
+      while i < end
+        if str.charAt(i) is '\\'
           i += 1
-        else if expr = @balancedString(str.substring(i), [['#{', '}']])
-          tokens.push ['STRING', quote + str.substring(pi, i) + quote] if pi < i
-          inner = expr.substring(2, expr.length - 1)
+        else if expr = @balancedString str[i..], [['#{', '}']]
+          tokens.push ['STRING', quote + str[pi...i] + quote] if pi < i
+          inner = expr.slice 2, -1
           if inner.length
             inner = inner.replace new RegExp('\\\\' + quote, 'g'), quote if options.heredoc
             nested = lexer.tokenize "(#{inner})", line: @line
@@ -429,7 +431,7 @@
           i += expr.length - 1
           pi = i + 1
         i += 1
-      tokens.push ['STRING', quote + str.substring(pi, i) + quote] if pi < i and pi < str.length - 1
+      tokens.push ['STRING', quote + str[pi...i] + quote] if i > pi < str.length - 1
       tokens.unshift ['STRING', '""'] unless tokens[0][0] is 'STRING'
       interpolated = tokens.length > 1
       @token '(', '(' if interpolated
@@ -438,7 +440,7 @@
         if tag is 'TOKENS'
           @tokens = @tokens.concat value
         else if tag is 'STRING' and options.escapeQuotes
-          escaped = value.substring(1, value.length - 1).replace(/"/g, '\\"')
+          escaped = value.slice(1, -1).replace(/"/g, '\\"')
           @token tag, "\"#{escaped}\""
         else
           @token tag, value
@@ -472,48 +474,48 @@
   # Attempt to match a string against the current chunk, returning the indexed
   # match if successful, and `false` otherwise.
   match: (regex, index) ->
-    return false unless m = @chunk.match regex
-    if m then m[index] else false
+    if m = @chunk.match regex then m[index or 0] else false

   # Are we in the midst of an unfinished expression?
   unfinished: ->
-    prev = @prev(2)
-    @value() and @value().match and @value().match(NO_NEWLINE) and
-      prev and (prev[0] isnt '.') and not @value().match(CODE) and
-      not @chunk.match ASSIGNED
+    prev  = @prev 2
+    value = @value()
+    value and NO_NEWLINE.test(value) and
+      prev and prev[0] isnt '.' and not CODE.test(value) and
+      not ASSIGNED.test(@chunk)

 # Constants
 # ---------

 # Keywords that CoffeeScript shares in common with JavaScript.
 JS_KEYWORDS = [
-  "if", "else",
-  "true", "false",
-  "new", "return",
-  "try", "catch", "finally", "throw",
-  "break", "continue",
-  "for", "in", "while",
-  "delete", "instanceof", "typeof",
-  "switch", "super", "extends", "class",
-  "this", "null", "debugger"
+  'if', 'else'
+  'true', 'false'
+  'new', 'return'
+  'try', 'catch', 'finally', 'throw'
+  'break', 'continue'
+  'for', 'in', 'while'
+  'delete', 'instanceof', 'typeof'
+  'switch', 'super', 'extends', 'class'
+  'this', 'null', 'debugger'
 ]

 # CoffeeScript-only keywords, which we're more relaxed about allowing. They can't
 # be used standalone, but you can reference them as an attached property.
-COFFEE_ALIASES =  ["and", "or", "is", "isnt", "not"]
+COFFEE_ALIASES =  ['and', 'or', 'is', 'isnt', 'not']
 COFFEE_KEYWORDS = COFFEE_ALIASES.concat [
-  "then", "unless", "until", "loop",
-  "yes", "no", "on", "off",
-  "of", "by", "where", "when"
+  'then', 'unless', 'until', 'loop'
+  'yes', 'no', 'on', 'off'
+  'of', 'by', 'where', 'when'
 ]

 # The list of keywords that are reserved by JavaScript, but not used, or are
 # used by CoffeeScript internally. We throw an error when these are encountered,
 # to avoid having a JavaScript error at runtime.
 RESERVED = [
-  "case", "default", "do", "function", "var", "void", "with",
-  "const", "let", "enum", "export", "import", "native",
-  "__hasProp", "__extends", "__slice"
+  'case', 'default', 'do', 'function', 'var', 'void', 'with'
+  'const', 'let', 'enum', 'export', 'import', 'native'
+  '__hasProp', '__extends', '__slice'
 ]

 # The superset of both JavaScript keywords and reserved words, none of which may


Proper ServerTestView connection teardown.

--- app/views/ServerTestView.coffee
+++ app/views/ServerTestView.coffee
@@ -6,7 +6,7 @@
   id: 'server-test-view'
   template: require 'templates/server-test-view'
   status: 'Off'
-
+
   events:
     'click .show-stack-button': 'onClickShowStackButton'
     'click #toggle': 'onToggleTesting'
@@ -16,17 +16,17 @@
     @specFiles = []
     @subPath = options.params[0] or ''
     @subPath = @subPath[1..] if @subPath[0] is '/'
-
+
     $.ajax('/server-test/list', {
       success: _.bind(@onTestListLoaded, @)
       error: FrimFram.onAjaxError
     })
-
+
     $.ajax('/server-test/running', {
       success: _.bind(@onServerRunningLoaded, @)
       error: FrimFram.onAjaxError
     })
-
+
     @runTests = _.bind(_.debounce(@runTests, 200), @)

   onServerRunningLoaded: (testing) ->
@@ -35,15 +35,16 @@
       @listenForChanges()
     else
       @setStatus('Off')
-
+
   listenForChanges: ->
     @setStatus('Listening In')
     self = @
-    connection = new WebSocket('ws://localhost:3002')
-    connection.onopen = -> self.runTests()
-    connection.onerror = (error) -> console.error 'web socket errored', error
-    connection.onmessage = (message) -> self.runTests()
-    connection.onclose = -> self.setupServerTesting() if self.testing
+    @connection?.close()
+    @connection = new WebSocket('ws://localhost:3002')
+    @connection.onopen = -> self.runTests()
+    @connection.onerror = (error) -> console.error 'web socket errored', error
+    @connection.onmessage = (message) -> self.runTests()
+    @connection.onclose = -> self.setupServerTesting() if self.testing

   runTests: ->
     @setStatus('Running Tests')
@@ -56,7 +57,7 @@
       success: _.bind(@onReportsLoaded, @)
       error: FrimFram.onAjaxError
     })
-
+
   setupServerTesting: =>
     @setTesting(true)
     @setStatus('Initializing Testing System')
@@ -65,58 +66,59 @@
       success: _.bind(@listenForChanges, @)
       error: _.bind(@onServerTestingSetupFailed, @)
     })
-
+
   onServerTestingSetupFailed: ->
     @setStatus('Server Down, Polling...')
     setTimeout(@setupServerTesting, 3000)
-
+
   teardownServerTesting: ->
     @setTesting(false)
+    @connection?.close()
     @setStatus('Restarting Server')
     $.ajax('/server-test/teardown', {
       type: 'POST'
       success: _.bind(@onTeardownDone, @)
       error: FrimFram.onAjaxError
     })
-
+
   onTeardownDone: ->
     @setStatus('Off')
-
+
   setStatus: (@status) ->
     @$el.find('#status').text(@status)

   onToggleTesting: ->
     if @testing then @teardownServerTesting() else @setupServerTesting()
-
+
   setTesting: (@testing) ->
     toggle = @$el.find('#toggle')
     toggle.removeClass('btn-danger btn-success')
     toggle.addClass(if @testing then 'btn-success' else 'btn-danger')
     toggle.text(if @testing then 'Turn Off Testing' else 'Turn On Testing')
-
+
   #- Report digestion. Tests were parsed from XML with odd names. Make the reports sensical.
-
+
   onReportsLoaded: (result) ->
     @setStatus('Standing By')
     @stacks = []
     @rootDir = result.rootDir
-
+
     if result.consoleError
       @consoleError = @digestStack(result.consoleError)
-
+
     if result.reports
       suites = (obj.testsuites.testsuite for obj in result.reports)
       suites = _.flatten(suites)
       @suites = (@digestSuite(suite) for suite in suites)
-
+
     @render()

   digestStack: (stack) ->
     short = []
     lastLineWasRemoved = false
-
+
     # shorten the stack by ...'ing all jasmine-node lines, and "removing" the rootDir.
-
+
     lineIsUseless = (line) ->
       return _.any([
           'node_modules/jasmine-node'
@@ -125,7 +127,7 @@
           'module.js'
         ], (substring) -> _.contains(line, substring)
       )
-
+
     for line, index in stack.split('\n')
       if lineIsUseless(line)
         continue if lastLineWasRemoved
@@ -141,9 +143,9 @@
         else if index > 0
           line = '(/¯◡ ‿ ◡)/¯ ~ ~ ~ /' + line
         short.push(line)
-
+
     summary = short.shift()
-
+
     result = {
       summary: summary
       raw: stack
@@ -151,18 +153,18 @@
     }
     @stacks.push result
     return result
-
+
   digestSuite: (suite) ->
     return {
-      name: suite.$.name
-      errors: parseInt(suite.$.errors)
-      failures: parseInt(suite.$.failures)
-      tests: (@digestTest(test) for test in suite.testcase)
-      time: @digestTime(suite.$.time)
-    }
-
+    name: suite.$.name
+    errors: parseInt(suite.$.errors)
+    failures: parseInt(suite.$.failures)
+    tests: (@digestTest(test) for test in suite.testcase)
+    time: @digestTime(suite.$.time)
+    }
+
   digestTime: (time) -> "#{(parseFloat(time)*1000).toFixed(0)} ms"
-
+
   digestTest: (test) ->
     result = {
       name: test.$.name
@@ -171,15 +173,15 @@
     if test.failure
       result.failures = (@digestFailure(failure) for failure in test.failure)
     return result
-
+
   digestFailure: (failure) ->
     return {
-      stack: @digestStack(failure._)
-      type: failure.$.type # always expect? Is something broken?
-      message: failure.$.message
-    }
-
-
+    stack: @digestStack(failure._)
+    type: failure.$.type # always expect? Is something broken?
+    message: failure.$.message
+    }
+
+
   #- other

   onClickShowStackButton: (e) ->
@@ -203,8 +205,12 @@
     c.stacks = @stacks or []
     c.status = @status
     c
-
+
   onRender: ->
     @setTesting(@testing)

+  destroy: ->
+    @connection?.close()
+    super()
+
 module.exports = ServerTestView


Use single method to get all expenses

Use single method to get all expenses

--- workers/social/lib/social/models/recurly/payment.coffee
+++ workers/social/lib/social/models/recurly/payment.coffee
@@ -186,20 +186,29 @@

   # List group's payments
   @getGroupPayments = (group, callback)->
-    # TBI
+    @getExpenses
+      buyer     : "group_#{group._id}"
+      active    : yes
+    , callback

   # List user's payments
   @getUserPayments = (account, callback)->
-    # TBI
+    @getExpenses
+      buyer     : "user_#{account._id}"
+      active    : yes
+    , callback

   # List a user's payments expensed to group
   @getUserExpenses = (group, account, callback)->
-    stack = []
-    JRecurlyPayment.some
+    @getExpenses
       buyer     : "group_#{group._id}"
       user      : "user_#{account._id}"
       active    : yes
-    , {subscription: 1}, (err, items)->
+    , callback
+
+  @getExpenses = (pattern, callback)->
+    stack = []
+    JRecurlyPayment.some pattern, {subscription: 1}, (err, items)->
       if err
         return callback new KodingError "Unable to query user balance: #{err}"
       items.forEach (item)->
@@ -212,6 +221,9 @@

       expenses = 0

+      # TODO: Make sure this calculation is enough.
+      # Not tested for expired/canceled subscriptions.
+
       async = require 'async'
       async.parallel stack, (err, results)->
         if err


Added search result cache feature.

Added search result cache feature.

--- app/assets/javascripts/app/controllers/navigation.js.coffee
+++ app/assets/javascripts/app/controllers/navigation.js.coffee
@@ -115,6 +115,9 @@

   render: () ->

+    # reset result cache
+    @searchResultCache = {}
+
     user = App.Session.get()
     @html App.view('navigation')(
       user: user
@@ -127,6 +130,11 @@
     @renderPersonal()

     searchFunction = =>
+
+      # use cache for search result
+      if @searchResultCache[@term]
+        @renderResult( @searchResultCache[@term] )
+
       App.Ajax.request(
         id:    'search'
         type:  'GET'
@@ -138,6 +146,9 @@

           # load assets
           App.Collection.loadAssets( data.assets )
+
+          # cache search result
+          @searchResultCache[@term] = data.result

           result = data.result
           for area in result
@@ -222,7 +233,7 @@
       return if term is @term
       @term = term
       @$('.search').toggleClass('filled', !!@term)
-      @delay( searchFunction, 220, 'search' )
+      @delay( searchFunction, 200, 'search' )
     )

     # bind to empty search


Show loading... tab right after tab creation.

--- app/assets/javascripts/app/lib/app_post/task_manager.coffee
+++ app/assets/javascripts/app/lib/app_post/task_manager.coffee
@@ -191,6 +191,8 @@
         active:   params.show
       )
       @allTasksByKey[params.key] = task.attributes()
+
+      @touch(params.key)

       # save new task and update task collection
       ui = @
@@ -515,7 +517,7 @@
                   persistent: true
                   init:       true
                 )
-              task_count * 250
+              task_count * 350
               undefined
               'task'
             )
@@ -534,7 +536,7 @@
               persistent: false
               init:       true
             )
-          task_count * 250
+          task_count * 350
           undefined
           'task'
         )


Tabbed focusing on custom date will still use custom date js, addresses #395

--- app/assets/javascripts/cleantheme/surveyAnimation.js.coffee
+++ app/assets/javascripts/cleantheme/surveyAnimation.js.coffee
@@ -162,13 +162,13 @@
       if e.keyCode is 38 or e.keyCode is 40 or e.keyCode is 13
         e.preventDefault()
       # Specifically targeting custom date input
-      if $(".survey-container.active").find(".survey-custom-date").is(":focus")
+      if $(".survey-custom-date").is(":focus")
         # Allow regular command and left/right keys
         if e.metaKey or e.keyCode is 37 or e.keyCode is 39
           return
         # Ensure proper blurring of date input
         else if e.keyCode is 13
-          $(".survey-container.active").find(".survey-custom-date").blur()
+          $(".survey-custom-date").blur()
           return
         # Autocomplete date when tab or / is pressed
         else if e.keyCode is 9 or e.keyCode is 191


Fix issues with clicks registering long after mouse down

--- app/assets/javascripts/modules/map/granules_layer.js.coffee
+++ app/assets/javascripts/modules/map/granules_layer.js.coffee
@@ -180,6 +180,7 @@
       map.on 'popupclose', @_onPopupClose
       map.on 'edsc.hover', @_onHover
       map.on 'edsc.hoverout', @_onHoverOut
+      map.on 'mousedown', @_onMouseDown

     onRemove: (map) ->
       @_map = map
@@ -200,11 +201,15 @@
       map.off 'popupclose', @_onPopupClose
       map.off 'edsc.hover', @_onHover
       map.off 'edsc.hoverout', @_onHoverOut
+      map.off 'mousedown', @_onMouseDown

       @_map = null

     _getMapElement: (className) ->
       @_map.getContainer().getElementsByClassName('granule-' + className)[0]
+
+    _onMouseDown: =>
+      @_clickStart = new Date()

     _onSelectionChange: (newValue) =>
       try
@@ -225,7 +230,10 @@
       setTimeout(fn, 0)

     _onClick: (e) =>
-      if @_tooltipLayer.isEnabled() && @_granuleDetails.granules()?
+      duration = 0
+      duration = new Date() - @_clickStart if @_clickStart?
+      @_clickStart = null
+      if duration < 200 && @_tooltipLayer.isEnabled() && @_granuleDetails.granules()?
         map = @_map

         unless @_popup?


add back this plugin enable switch, fix syntax error

Signed-off-by: Huang Cheng <8ef188e1917e6f607a1ed78972b8f4f0faf9504a@gmail.com>

--- lib/highlight-line-view.coffee
+++ lib/highlight-line-view.coffee
@@ -7,6 +7,7 @@

 module.exports =
   configDefaults:
+    allEnable: true
     enableBackgroundColor: true
     backgroundRgbColor: "100, 100, 100"
     opacity: "50%"
@@ -80,7 +81,8 @@

   updateSelectedLine: =>
     @resetBackground()
-    @showHighlight()
+    if atom.config.get('highlight-line.allEnable')
+      @showHighlight()

   resetBackground: ->
     $('.line').css('background-color', '')
@@ -93,7 +95,7 @@
       bgColor = @wantedColor('backgroundRgbColor')
       bgRgba = "rgba(#{bgColor}, #{@wantedOpacity()})"
       styleAttr += "background-color: #{bgRgba};"
-    if underlineStyleInUsed isnt
+    if underlineStyleInUsed
       ulColor = @wantedColor('underlineRgbColor')
       ulRgba = "rgba(#{ulColor},1)"
       styleAttr += "border-bottom: 1px #{underlineStyleInUsed} #{ulRgba};"


Feeder: fixed a referenceError by removing _windowDidResize

--- client/Social/Feeder/Controllers/FeederResultsController.coffee
+++ client/Social/Feeder/Controllers/FeederResultsController.coffee
@@ -2,7 +2,7 @@

   constructor:(options = {}, data)->

-    options.view                or= new FeederTabView hideHandleCloseIcons : yes
+    options.view                or= new FeederTabView hideHandleContainer : yes
     options.paneClass           or= KDTabPaneView
     options.itemClass           or= KDListItemView
     options.listControllerClass or= KDListViewController
@@ -15,11 +15,11 @@

     @createTab name, filter for own name, filter of options.filters

+
   loadView:(mainView)->

-    mainView.hideHandleContainer()
     mainView.showPaneByIndex 0
-    @utils.defer mainView.bound "_windowDidResize"
+

   openTab:(filter, callback)->
     tabView = @getView()


reverted this to @ - fixed in another branch - lets not clash

--- client/app/MainApp/filetree/controllers/findercontroller.coffee
+++ client/app/MainApp/filetree/controllers/findercontroller.coffee
@@ -18,7 +18,7 @@
     treeOptions.maxRecentFolders  = options.maxRecentFolders  or= 10
     treeOptions.useStorage        = options.useStorage         ?= no
     treeOptions.loadFilesOnInit   = options.loadFilesOnInit    ?= no
-    treeOptions.delegate          = this
+    treeOptions.delegate          = @

     super options, data


cleanup inquire view

cleanup inquire view

--- apps/artwork/client/view.coffee
+++ apps/artwork/client/view.coffee
@@ -22,7 +22,6 @@
     'click .circle-icon-button-share'       : 'openShare'
     'click .circle-icon-button-save'        : 'saveArtwork'
     'click .artwork-additional-image'       : 'changeImage'
-    'click .artwork-inquiry-button'         : 'inquire'
     'click .artwork-download-button'        : 'trackDownload'
     'click .artwork-auction-results-button' : 'trackComparable'
     'change .aes-radio-button'              : 'selectEdition'


web socketはプロトコルはwsでしたね

web socketはプロトコルはwsでしたね

--- coffee/index.coffee
+++ coffee/index.coffee
@@ -4,7 +4,7 @@

 SocketService = require('./SocketService')

-socketService = new SocketService('https://erl-editor.herokuapp.com//websocket')
+socketService = new SocketService('erl-editor.herokuapp.com//websocket')

 global.React = require('react')
 md2react = require('md2react')


Fix spurious updates at startup

Fix spurious updates at startup

--- lib/minimap-editor-view.coffee
+++ lib/minimap-editor-view.coffee
@@ -12,8 +12,6 @@
       @tag 'canvas', {
         outlet: 'lineCanvas'
         class: 'minimap-canvas'
-        width: 120
-        height: 1000
         id: 'line-canvas'
       }

@@ -43,10 +41,17 @@

   setEditorView: (@editorView) ->
     @editor = @editorView.getModel()
-    @buffer = @editorView.getEditor().buffer
+    @buffer = @editorView.getEditor().getBuffer()
+    @displayBuffer = @editor.displayBuffer

     @subscribe @editor, 'screen-lines-changed.minimap', (changes) =>
-      @pendingChanges.push changes
+      #@pendingChanges.push changes
+      @requestUpdate()
+
+    @subscribe @editor, 'contents-modified.minimap', =>
+      @requestUpdate()
+
+    @subscribe @displayBuffer, 'tokenized.minimap', =>
       @requestUpdate()

   requestUpdate: ->
@@ -72,9 +77,6 @@
     @cachedScrollTop = scrollTop
     @requestUpdate()

-
-  registerBufferChanges: (event) =>
-    @pendingChanges.push event

   getMinimapHeight: -> @getLinesCount() * @getLineHeight()
   getLineHeight: -> 2
@@ -133,6 +135,7 @@

   update: =>
     return unless @editorView?
+    return unless @displayBuffer.tokenizedBuffer.fullyTokenized

     #reset canvas virtual width/height
     @lineCanvas[0].width = @lineCanvas[0].offsetWidth


Fixes #3584 - Attachment order is wrong.

--- app/assets/javascripts/app/controllers/ticket_zoom/sidebar_article_attachments.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom/sidebar_article_attachments.coffee
@@ -17,12 +17,12 @@
       @el.html("<div>#{App.i18n.translateInline('none')}</div>")
       return
     html = ''
-    for ticket_article_id, index in @ticket.article_ids
+    for ticket_article_id in @ticket.article_ids.sort((a, b) -> a - b)
       if App.TicketArticle.exists(ticket_article_id)
         article = App.TicketArticle.find(ticket_article_id)
         attachments = App.TicketArticle.contentAttachments(article)
         if !_.isEmpty(attachments)
-          html = App.view('ticket_zoom/sidebar_article_attachment')(article: article, attachments: attachments) + html
+          html += App.view('ticket_zoom/sidebar_article_attachment')(article: article, attachments: attachments)
     @el.html(html)
     @el.delegate('.js-attachments img', 'click', (e) =>
       @imageView(e)


:memo: More documentation for the MinimapRenderView

--- lib/minimap-render-view.coffee
+++ lib/minimap-render-view.coffee
@@ -102,7 +102,6 @@
   # Performs an update of the minimap.
   update: =>
     return unless @editorView?
-    # return unless @tokenized

     #reset canvas virtual width/height
     @lineCanvas[0].width = @lineCanvas[0].offsetWidth
@@ -152,7 +151,6 @@
     @offscreenLastRow = null
     @requestUpdate()

-
   # Registers changes in the minimap for the next render call.
   #
   # Registering changes on an instance will trigger an update request.
@@ -188,33 +186,88 @@
   #    ##        ##    ##  ##     ## ##        ##    ##
   #    ##        ##     ##  #######  ##         ######

+  # Returns the height of the minimap in pixels. Note that the returned
+  # height is not the actual height of the minimap canvas but rather
+  # the computation of the minimap height if all the lines were
+  # rendered.
+  #
+  # This function is used when comparing the minimap to its editor
+  # to compute the scale factor.
+  #
+  # Returns a {Number}.
   getMinimapHeight: -> @getLinesCount() * @getLineHeight()

+  # Returns the height in pixels of the minimap lines.
+  #
+  # The height of the minimap lines can be changed using the
+  # `minimap.lineHeight` setting.
+  #
+  # Returns a {Number}.
   getLineHeight: -> @lineHeight

+  # Returns the height in pixels of a character rendered in the minimap.
+  #
+  # The height of the minimap characters can be changed using the
+  # `minimap.charHeight` setting.
+  #
+  # The characters height is dissociated from the line height in order
+  # to allow for some spacing between lines when rendering the minimap.
+  #
+  # Returns a {Number}.
   getCharHeight: -> @charHeight

+  # Returns the width in pixels of a character rendered in the minimap.
+  #
+  # The width of the minimap characters can be changed using the
+  # `minimap.charWidth` setting.
+  #
+  # Returns a {Number}.
   getCharWidth: -> @charWidth

+  # Returns the opacity at which the text are rendered in the minimap.
+  #
+  # Returns a {Number}.
   getTextOpacity: -> @textOpacity

+  # Returns the number of lines in the `Editor`.
+  #
+  # Returns a {Number}
   getLinesCount: -> @editorView.getEditor().getScreenLineCount()

-  getMinimapScreenHeight: -> @minimapView.height() #/ @minimapView.scaleY
-
+  # Returns the height of the minimap on screen.
+  #
+  # It differs from {::getMinimapHeight} in that the former returns
+  # the height of the whole minimap when this method returns the height
+  # of the visible part.
+  #
+  # Returns a {Number}.
+  getMinimapScreenHeight: -> @minimapView.height()
+
+  # Returns the number of lines the visible area of the minimap covers.
+  #
+  # Returns a {Number}.
   getMinimapHeightInLines: -> Math.ceil(@getMinimapScreenHeight() / @getLineHeight())

+  # Returns the index of the first visible row.
+  #
+  # Returns a {Number}.
   getFirstVisibleScreenRow: ->
     screenRow = Math.floor(@scrollTop() / @getLineHeight())
     screenRow = 0 if isNaN(screenRow)
     screenRow

+  # Returns the index of the last visible row.
+  #
+  # Returns a {Number}.
   getLastVisibleScreenRow: ->
     calculatedRow = Math.ceil((@scrollTop() + @getMinimapScreenHeight()) / @getLineHeight()) - 1
     screenRow = Math.max(0, Math.min(@editor.getScreenLineCount() - 1, calculatedRow))
     screenRow = 0 if isNaN(screenRow)
     screenRow

+  # Returns the bounds of the whole minimap.
+  #
+  # Returns an {Object}.
   getClientRect: ->
     canvas = @lineCanvas[0]
     {
@@ -222,6 +275,16 @@
       height: @getMinimapHeight()
     }

+  # Returns a pixel position corresponding to a character's screen
+  # position.
+  #
+  # position - A screen position {Object} with the following properties:
+  #            :row - The row {Number} of the character.
+  #            :column - The column {Number} of the character.
+  #
+  # Returns an {Object} with the following properties:
+  # :top - The position {Number} from top.
+  # :left - The position {Number} from left.
   pixelPositionForScreenPosition: (position) ->
     {row, column} = @buffer.constructor.Point.fromObject(position)
     actualRow = Math.floor(row)


sidebar: register to privateChannelThreads updates

--- client/activity/lib/components/sidebarsections/index.coffee
+++ client/activity/lib/components/sidebarsections/index.coffee
@@ -12,8 +12,9 @@

   getDataBindings: ->
     return {
-      publicChannels: getters.followedPublicChannelThreads
-      selectedThreadId: getters.selectedChannelThreadId
+      publicChannels   : getters.followedPublicChannelThreads
+      privateChannels  : getters.followedPrivateChannelThreads
+      selectedThreadId : getters.selectedChannelThreadId
     }


Fetch all members when refresh method is called

--- client/admin/lib/views/members/teammemberscommonview.coffee
+++ client/admin/lib/views/members/teammemberscommonview.coffee
@@ -287,3 +287,4 @@
   refresh: ->

     @resetListItems()
+    @fetchMembers()


Set show to false on viewport props when hidden

Otherwise any unrelated change in the props causes show to be resent

--- client/lanes/components/modal/Modal.cjsx
+++ client/lanes/components/modal/Modal.cjsx
@@ -11,7 +11,8 @@
         size:      React.PropTypes.string

     contextTypes:
-        uistate: Lanes.PropTypes.State
+        viewport: Lanes.PropTypes.State
+        uistate:  Lanes.PropTypes.State

     getDefaultProps: ->
         size: 'large'
@@ -35,6 +36,7 @@
         @setState(show: nextProps.show) if nextProps.show?

     _hide: ->
+        @context.viewport.modalProps.show = false
         @setState(show: false)

     hide: ->
@@ -54,7 +56,7 @@
         cls = _.classnames('lanes-dialog', @props.className, @context.uistate?.layout_size)

         <BS.Modal.Dialog className={cls} bsSize={@props.size} onHide={@_hide}>
-            <BS.Modal.Header closeButton>
+            <BS.Modal.Header>
                 <BS.Modal.Title>{@props.title}</BS.Modal.Title>
             </BS.Modal.Header>


Use the date object instead of a param

Use the date object instead of a param

--- src/lunch.coffee
+++ src/lunch.coffee
@@ -18,5 +18,5 @@
   robot.hear /lunch/i, (msg) ->
     if ! isPastLunchTime() then msg.reply "Hey #{msg.message.user.name}, isn't it a bit early to start talking about lunch?"

-isPastLunchTime = (time) ->
-  return time.getHours() > 12
+isPastLunchTime = ->
+  return new Date().getHours() > 12


Update default config by specifying its type

--- lib/notational-velocity.coffee
+++ lib/notational-velocity.coffee
@@ -1,8 +1,10 @@
 {CompositeDisposable} = require 'atom'

 module.exports =
-  configDefaults:
-    directory: 'testdata'
+  config:
+    directory:
+      type: 'string'
+      default: 'testdata'

   notationalVelocityView: null


Don't remove on uninstall

Don't remove on uninstall

--- lib/package-update-view.coffee
+++ lib/package-update-view.coffee
@@ -51,8 +51,6 @@
       if error?
         @setButtonsEnabled(true)
         @setStatusIcon('alert')
-      else
-        @remove()

   setButtonsEnabled: (enabled) ->
     @upgradeButton.prop('disabled', not enabled)


fix(window-state): Per electron#7278, save state in beforeunload

Related:
https://github.com/electron/electron/issues/7278
https://github.com/atom/atom/pull/3223/files

As of Chromium 36, unload is async and @zcbenz does not think it happens reliabily in Electron. Move saving of window state to beforeunload, following suit with Atom.

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -39,13 +39,12 @@
       # throttled in case more work needs to be done before closing

       # In Electron, returning any value other than undefined cancels the close.
-      canCloseWindow = @runUnloadCallbacks()
-      return undefined if canCloseWindow
+      if @runUnloadCallbacks()
+        # Good to go! Window will be closing...
+        NylasEnv.storeWindowDimensions()
+        NylasEnv.saveStateAndUnloadWindow()
+        return undefined
       return false
-
-    window.onunload = =>
-      NylasEnv.storeWindowDimensions()
-      NylasEnv.saveStateAndUnloadWindow()

     NylasEnv.commands.add document.body, 'window:toggle-full-screen', ->
       NylasEnv.toggleFullScreen()


Fix minor test assertion

Fix minor test assertion

--- client/src/tests/hypergraph.tests.coffee
+++ client/src/tests/hypergraph.tests.coffee
@@ -22,7 +22,7 @@
     throttle$
   ]
   for func in funcs
-    t.ok typeof func is 'function'
+    t.equal (typeof func), 'function'
   t.end()

 test 'hypergraph edge should not fail when unlinked', (t) ->


Tests: Register: Updated test names.

Tests: Register: Updated test names.

--- client/test/lib/register/register.coffee
+++ client/test/lib/register/register.coffee
@@ -12,7 +12,7 @@
 module.exports =

-  registerUserWithGravatarEmail: (browser) ->
+  registerWithGravatarEmail: (browser) ->

     user =
       email    : 'kodingtestuser@gmail.com'
@@ -36,7 +36,7 @@
           browser.end()

-  registerUserWithoutGravatarEmail: (browser) ->
+  registerWithoutGravatarEmail: (browser) ->

     helpers.doRegister(browser)
     browser.end()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -23,7 +23,7 @@

   @name every
   @methodOf TimedEvents#
-  @param {Number} n The number of
+  @param {Number} n The number of frames to wait before e
   @returns {Number} Distance between the two objects
   ###
   every: (n, fn) ->


esplode?

esplode?

--- app/assets/javascripts/components/sort_filter_form.coffee
+++ app/assets/javascripts/components/sort_filter_form.coffee
@@ -9,7 +9,7 @@
 		@state.order && @state.filta
 	handleSubmitOrderFilta: (e) ->
 		e.preventDefault()
-		$.get '', { orderFilta: @state }, (data) =>
+		$.post '', { orderFilta: @state }, (data) =>
 			@props.handleNewOrderFilta data
 		, 'JSON'
 		@setState @getInitialState()


move adding kudo to application controller

--- app/assets/javascripts/routes/application_route.js.coffee
+++ app/assets/javascripts/routes/application_route.js.coffee
@@ -1,6 +1,10 @@
 Sks.ApplicationRoute = Ember.Route.extend
   events:
     addKudo: (user) ->
+      self = @
+      token = $('meta[name="csrf-token"]').attr 'content'
+      currentUserController = @controllerFor 'currentUser'
+
       showFlash = (type, message) ->
         $("#flash")
           .addClass(type)
@@ -11,16 +15,11 @@
           .delay(2000)
           .fadeOut 'slow'

-      self = this
-      token = $('meta[name="csrf-token"]').attr('content')
-      cuController = Sks.Router.get 'currentUserController'
-      console.log cuController.kudosLeft
-
-      jQuery.post("/kudos", user_id: user.get("id"), authenticity_token: token)
+      $.post("/kudos", user_id: user.get("id"), authenticity_token: token)
       .done((data, status) =>
-        kudosLeft = self.get 'controllers.currentUser.kudosLeft'
+        kudosLeft = currentUserController.get 'kudosLeft'
         if kudosLeft > 0
-          self.decrementProperty "controllers.currentUser.kudosLeft"
+          currentUserController.set 'kudosLeft', kudosLeft - 1
           showFlash 'alert-success', 'You\'ve added a kudo!'
         else
           showFlash 'alert-error', 'There\'re no kudos left!'


don't provide completions in comments

don't provide completions in comments

--- lib/runtime/completions.coffee
+++ lib/runtime/completions.coffee
@@ -31,8 +31,13 @@
   processCompletions: (completions, prefix) ->
     completions.map((c) => @toCompletion c, prefix)

+  validScope: ({scopes}) ->
+    for scope in scopes.slice 1
+      return false if scope.startsWith 'comment'
+    return true
+
   getSuggestions: (data) ->
-    return [] unless client.isConnected()
+    return [] unless client.isConnected() and @validScope data.scopeDescriptor
     @rawCompletions(data).then ({completions, prefix, mod}) =>
       return @fromCache mod, prefix if not completions?
       @processCompletions completions, prefix


Chrome testing as not first env bug fixed

--- lib/testing/cli/builder.coffee
+++ lib/testing/cli/builder.coffee
@@ -34,7 +34,6 @@
 Callback function is called when build is ready.
 ###
 exports.buildProject = (target, env, callback) ->
-    fs.removeSync './build/browser'
     args = utils.clone BUILD_ARGS
     args[1] = target # target
     args[3] += config.getPlatformOutFolder(target) # out


Removed hard-coded zoom levels on Mercator Tile calculations (#6733)

--- bokehjs/src/coffee/models/tiles/mercator_tile_source.coffee
+++ bokehjs/src/coffee/models/tiles/mercator_tile_source.coffee
@@ -16,7 +16,7 @@

   initialize: (options) ->
     super(options)
-    @_resolutions = (@get_resolution(z) for z in [0..30])
+    @_resolutions = (@get_resolution(z) for z in [@min_zoom..@max_zoom])

   _computed_initial_resolution: () ->
     if @initial_resolution?


Fix sql escaping

Fix sql escaping

--- storage/geocache-service.coffee
+++ storage/geocache-service.coffee
@@ -111,20 +111,19 @@
                 .toString()
             result = yield client.queryAsync sql
             sql = if result.rowCount is 0
-                @db.insert()
+                @db.insert numberedParameters: true
                     .into 'geocaches'
                     .set 'id', id
                     .set 'updated', updated or 'CURRENT_TIMESTAMP', dontQuote: not updated?
                     .set 'data', JSON.stringify data
-                    .toString()
+                    .toParam()
             else
-                @db.update()
+                @db.update numberedParameters: true
                     .table 'geocaches'
                     .set 'updated', updated or 'CURRENT_TIMESTAMP', dontQuote: not updated?
                     .set 'data', JSON.stringify data
                     .where 'id = ?', id
-                    .toString()
-            console.log sql
+                    .toParam()
             result = yield client.queryAsync sql
             throw new Error "Insert/update had no effect: #{sql}" if result.rowCount is 0
             yield client.queryAsync 'COMMIT'


remove the catch block (syntax error)

remove the catch block (syntax error)

--- node_modules_koding/kite-amqp/lib/kite-amqp/index.coffee
+++ node_modules_koding/kite-amqp/lib/kite-amqp/index.coffee
@@ -84,7 +84,7 @@
   handleMessage:(message, headers, deliveryInfo)->
     {routingKey} = deliveryInfo
     messageData = (try parse message.data) or message
-    catch e then @emit 'error', e
+    # catch e then @emit 'error', e
     switch routingKey
       when 'auth.join'  then @join messageData
       when 'auth.leave' then @leave messageData


Unit test accept changes in the ranges manager.

--- test/unit/coffee/RangesManager/RangesManagerTests.coffee
+++ test/unit/coffee/RangesManager/RangesManagerTests.coffee
@@ -10,6 +10,7 @@
 		@RangesManager = SandboxedModule.require modulePath,
 			requires:
 				"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
+
 		@doc_id = "doc-id-123"
 		@project_id = "project-id-123"
 		@user_id = "user-id-123"
@@ -181,24 +182,104 @@

 	describe "acceptChanges", ->
 		beforeEach ->
-			@ranges = { entries: "mock", comments: "mock" }
+			@RangesManager = SandboxedModule.require modulePath,
+				requires:
+					"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
+					"./RangesTracker":@RangesTracker = SandboxedModule.require "../../../../app/js/RangesTracker.js"
+
+			@ranges = {
+				comments: []
+				changes: [{
+					id: "a1"
+					op:
+						i: "lorem"
+						p: 0
+				}, {
+					id: "a2"
+					op:
+						i: "ipsum"
+						p: 10
+				}, {
+					id: "a3"
+					op:
+						i: "dolor"
+						p: 20
+				}, {
+					id: "a4"
+					op:
+						i: "sit"
+						p: 30
+				}, {
+					id: "a5"
+					op:
+						i: "amet"
+						p: 40
+				}]
+			}
+			@removeChangeIdsSpy = sinon.spy @RangesTracker.prototype, "removeChangeIds"

 		describe "successfully with a single change", ->
-			beforeEach ->
-				@change_id = "mock-change-id"
-				@RangesManager.acceptChanges [ @change_id ], @ranges
+			beforeEach (done) ->
+				@change_ids = [ @ranges.changes[1].id ]
+				@RangesManager.acceptChanges @change_ids, @ranges, (err, ranges) =>
+					@rangesResponse = ranges
+					done()

 			it "should log the call with the correct number of changes", ->
 				@logger.log
 					.calledWith("accepting 1 changes in ranges")
 					.should.equal true

+			it "should delegate the change removal to the ranges tracker", ->
+				@removeChangeIdsSpy
+					.calledWith(@change_ids)
+					.should.equal true
+
+			it "should remove the change", ->
+				expect(@rangesResponse.changes
+					.find((change) => change.id ==  @ranges.changes[1].id))
+					.to.be.undefined
+
+			it "should return the original number of changes minus 1", ->
+				@rangesResponse.changes.length
+					.should.equal @ranges.changes.length - 1
+
+			it "should not touch other changes", ->
+				for i in [ 0, 2, 3, 4]
+					expect(@rangesResponse.changes
+						.find((change) => change.id ==  @ranges.changes[i].id))
+						.to.deep.equal @ranges.changes[i]
+
 		describe "successfully with multiple changes", ->
-			beforeEach ->
-				@change_ids = [ "mock-change-id-1", "mock-change-id-2", "mock-change-id-3", "mock-change-id-4" ]
-				@RangesManager.acceptChanges @change_ids, @ranges
+			beforeEach (done) ->
+				@change_ids = [ @ranges.changes[1].id, @ranges.changes[3].id, @ranges.changes[4].id ]
+				@RangesManager.acceptChanges @change_ids, @ranges, (err, ranges) =>
+					@rangesResponse = ranges
+					done()

 			it "should log the call with the correct number of changes", ->
 				@logger.log
 					.calledWith("accepting #{ @change_ids.length } changes in ranges")
 					.should.equal true
+
+			it "should delegate the change removal to the ranges tracker", ->
+				@removeChangeIdsSpy
+					.calledWith(@change_ids)
+					.should.equal true
+
+			it "should remove the changes", ->
+				for i in [ 1, 3, 4]
+					expect(@rangesResponse.changes
+						.find((change) => change.id ==  @ranges.changes[1].id))
+						.to.be.undefined
+
+			it "should return the original number of changes minus the number of accepted changes", ->
+				@rangesResponse.changes.length
+					.should.equal @ranges.changes.length - 3
+
+			it "should not touch other changes", ->
+				for i in [ 0, 2 ]
+					expect(@rangesResponse.changes
+						.find((change) => change.id ==  @ranges.changes[i].id))
+						.to.deep.equal @ranges.changes[i]
+


Add some tests

Add some tests


fixed copy/paste bug

fixed copy/paste bug

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -2340,7 +2340,7 @@
           if (next.done) {
             this.win(next.value);
           } else {
-            next.value.then(this.thenHandle, this.failHandle);
+            next.value.promise.then(this.thenHandle, this.failHandle);
           }
         }
       };


Include textarea as a possible focus field

--- vendor/assets/javascripts/fustrate/components/modal.coffee
+++ vendor/assets/javascripts/fustrate/components/modal.coffee
@@ -73,7 +73,7 @@
     # Focus requires a slight physical scroll on iOS 8.4
     return true if /iPad|iPhone|iPod/g.test navigator.userAgent

-    $('input, select', @modal)
+    $('input, select, textarea', @modal)
       .filter(':visible:not(:disabled):not([readonly])')
       .first()
       .focus()


Ensure FILE is optional

Ensure FILE is optional

--- client/lanes/models/Base.coffee
+++ client/lanes/models/Base.coffee
@@ -53,10 +53,11 @@
         @parent?.triggerChangeSet?(triggered, change)

     modelTypeIdentifier: ->
-        _.dasherize(_.last(@FILE.path))
+        _.dasherize(_.last(@FILE?.path || ''))

     api_path: ->
-        @FILE.extension.identifier + '/' + _.pluralize(@modelTypeIdentifier())
+        id = @FILE?.extension.identifier || ''
+        id + '/' + _.pluralize(@modelTypeIdentifier())

     urlRoot: ->
         Lanes.config.api_path + _.result(this, 'api_path')
@@ -103,8 +104,9 @@

     serialize: (options = {}) ->
         options.depth ||= 1
-        options.depth += 1
         return if options.depth > 5
+        depth = options.depth + 1
+        options = _.extend({}, options, {depth})
         _.extend(super,
             this.associations?.serialize(this, options)
         )


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/Physical.coffee
+++ src/Physical.coffee
@@ -1,55 +0,0 @@
-( ($) ->
-  SCALE = 0.1
-
-  {b2Vec2} = Box2D.Common.Math
-  {b2BodyDef, b2Body, b2FixtureDef, b2Fixture, b2World} = Box2D.Dynamics
-  {b2PolygonShape, b2CircleShape, b2MassData} = Box2D.Collision.Shapes
-
-  window.Physical = (I, self) ->
-    $.reverseMerge I,
-      density: 1.0
-      dynamic: false
-      friction: 0.1
-      restitution: 0.5
-      rotatable: false
-
-    fixDef = new b2FixtureDef()
-    fixDef.density = I.density
-    fixDef.friction = I.friction
-    fixDef.restitution = I.restitution
-    fixDef.shape = new b2PolygonShape()
-    fixDef.shape.SetAsBox(I.width / 2 * SCALE, I.height / 2 * SCALE)
-
-    bodyDef = new b2BodyDef()
-
-    if I.dynamic
-      bodyDef.type = b2Body.b2_dynamicBody
-      bodyDef.fixedRotation = !I.rotatable
-    else
-      bodyDef.type = b2Body.b2_staticBody
-
-    center = self.center().scale(SCALE)
-
-    bodyDef.position = new b2Vec2(
-      center.x,
-      center.y
-    )
-
-    body = I.world.CreateBody(bodyDef)
-    body.CreateFixture(fixDef)
-
-    body.SetUserData(self)
-
-    self.bind "step", ->
-      I.x = (body.GetPosition().x / SCALE) - (I.width / 2)
-      I.y = (body.GetPosition().y / SCALE) - (I.height / 2)
-      I.rotation = body.GetAngle()
-
-    applyImpulse: (vector) ->
-      body.ApplyImpulse(new b2Vec2(vector.x, vector.y), body.GetPosition())
-
-    body: ->
-      body
-
-)(jQuery)
-


Use subarrays as browsers (chrome at least) don't support slice.

Works correctly in node, also.

--- src/validate-ssh.coffee
+++ src/validate-ssh.coffee
@@ -5,7 +5,7 @@
 	len = arr.length
 	return 0 if len == 0

-	return arr[len-1] + (uint8ArrayToInt(arr[...len-1]) << 8)
+	return arr[len-1] + (uint8ArrayToInt(arr.subarray(0, len-1)) << 8)

 validate = (key) ->
 	# See http://crypto.stackexchange.com/a/5948.
@@ -17,9 +17,9 @@

 	arr = base64binary.decode(key)

-	secondLength = uint8ArrayToInt(arr[11..14])
+	secondLength = uint8ArrayToInt(arr.subarray(11, 15))
 	lengthSoFar = 4 + 7 + 4 + secondLength
-	thirdLength = uint8ArrayToInt(arr[lengthSoFar..lengthSoFar+3])
+	thirdLength = uint8ArrayToInt(arr.subarray(lengthSoFar, lengthSoFar+4))
 	lengthSoFar += 4 + thirdLength

 	if lengthSoFar != arr.length


Theoricus doesn't depend on Toaster anymore.

--- toaster.coffee
+++ toaster.coffee
@@ -1,50 +0,0 @@
-# a single build's config
-#  ~> many can be specified repeating everything bellow this line
-toast
-
-  name: 'theoricus'
-
-  # src folders (relative to this!)
-  dirs:[
-    'www/src'
-  ]
-
-  # main module
-  main: 'theoricus/theoricus'
-
-  # release folder
-  release_dir: 'lib/js'
-
-  # excluded items (default=[])
-  exclude: []
-
-  # removes coffee safety wrapper (default=true - duplicates the AMD wrapper)
-  bare: true
-
-  # minifies release files (default=true)
-  minify: false
-
-  # infos for simple static server (with -s option)
-  webroot: 'lib'
-  port: 3000
-
-  # optimization configs (when specified, means toaster's loader will be used)
-  # the resulting will probably not be usefull with another AMD loader
-  optimize:
-
-    # base url to reach your release folder through http
-    base_url: 'js'
-
-    # vendors to consider (can be local or remote)
-    vendors:
-      'history': 'www/vendors/history.js/scripts/bundled/html4+html5/native.history.js'
-      'jade': 'www/vendors/jade/runtime.js'
-      'inflection': 'www/vendors/inflection.js'
-      'jquery': 'www/vendors/jquery.js'
-      'json': 'www/vendors/JSON-js/json2.js'
-
-    # each layer wil hold all the definitions needed (all levels) in order to
-    # work. if layers share dpendencies, they will not repeat acros layers.
-    # layers:
-    #   'main': ['app/app']
-    #   'users': ['app/controllers/users']


Apply email sanitization procedure to emails from all domains except koding.com

--- workers/social/lib/social/models/user/emailsanitize.coffee
+++ workers/social/lib/social/models/user/emailsanitize.coffee
@@ -1,13 +1,4 @@
 sanitize = (email, options = {}) ->
-
-  email = email.trim().toLowerCase()
-
-  return switch
-    when checkGmail email then sanitizeGmail email, options
-    else email
-
-
-sanitizeGmail = (email, options = {}) ->

   options.excludeDots or= no
   options.excludePlus or= no
@@ -36,6 +27,9 @@
 checkGmail = (email) -> /^(.)+@(gmail|googlemail).com/.test email

+checkKoding = (email) -> /^(.)+@koding.com/.test email
+
+
 excludeDots = (username) -> username.replace /\./g, ''

@@ -43,4 +37,10 @@
   parts[0]  if (parts = username.split '+').length

-module.exports = sanitize
+module.exports = (email, options = {}) ->
+
+  email = email.trim().toLowerCase()
+
+  if checkKoding email
+  then email
+  else sanitize email, options


Clear selected source id when going back to doc list

--- client/controllers/curatorInbox/curatorSourceDetails.coffee
+++ client/controllers/curatorInbox/curatorSourceDetails.coffee
@@ -136,7 +136,9 @@
     _markReviewed(instance)

   'click .back-to-list': (event, instance) ->
-    instance.data.currentPaneInView.set('')
+    instanceData = instance.data
+    instanceData.selectedSourceId.set('')
+    instanceData.currentPaneInView.set('')

   'click .tabs a': (event, instance) ->
     instance.selectedIncidentTab.set(instance.$(event.currentTarget).data('tab'))


Fixed for socials show on login for hackathon

--- client/landing/site.boilerplate/coffee/login/AppView.coffee
+++ client/landing/site.boilerplate/coffee/login/AppView.coffee
@@ -570,8 +570,8 @@
         @formHeader.show()
         @formHeader.updatePartial @generateFormHeaderPartial()
         @loginForm.username.input.setFocus()
-        if @goToRecoverLink.is ':hidden' then @goToRecoverLink.show()
-        if @github.is ':hidden' then @github.show()
+        @goToRecoverLink.show()
+        @github.show()
       when "recover"
         @$('.flex-wrapper').addClass 'one'
         @github.hide()


fixed lint errors

fixed lint errors

--- test/test.coffee
+++ test/test.coffee
@@ -60,8 +60,9 @@
 			assert.equal model.get('fullName'), 'FIRST last'

 	describe '#computedFields', ->
-		describe 'onlyOnce'
+		describe 'onlyOnce', ->
 			it 'should prevent recomputing the field if the field already exists'
+			it 'should allow setting the field during fetch'

 		model = null
 		options = null
@@ -143,7 +144,7 @@
 						converter: ->
 							this.get('firstName').substr(0, 1) + ' ' + this.get('lastName').substr(0, 1)

-		it 'should include depencies in JSON', ->
+		it 'should include dependencies in JSON', ->
 			model = new Backbone.Model attributes, options
 			json = model.toJSON()

@@ -162,5 +163,5 @@

 			assert.isDefined json.initials

-	describe '#get()'
+	describe '#get()', ->
 		it 'accepts an options parameter'


Fix test

Fix test

--- test/test.coffee
+++ test/test.coffee
@@ -309,8 +309,13 @@
         assert.strictEqual resp.statusCode, 200, 'Status code is 200'
         assert.strictEqual resp.headers['content-type'], 'text/xml; charset=utf-8', 'content-type'
         feedparser = new FeedParser
+        articles = []
         feedparser.on('error', (err) ->
-          assert.fail err, null, 'Error on parsing Atom feed'
+          assert.fail err, null, "Error on parsing Atom feed: #{err}"
+        )
+        feedparser.on('readable', ->
+          while item = this.read()
+            articles.push item
         )
         feedparser.on('end', ->
           meta = this.meta
@@ -318,7 +323,6 @@
           assert.strictEqual meta['atom:link'][0]['@'].href,
             'http://notes.kyu-mu.net/index.atom', 'atom:link'
           assert.strictEqual meta['atom:@'].xmlns, 'http://www.w3.org/2005/Atom', 'xmlns'
-          articles = this.articles
           assert.strictEqual articles.length, 8, '8 articles'
           assert.strictEqual articles[0].title, 'Bitter blog engine', 'articles[0].title'
           assert /<h1[^>]*>Bitter blog engine<\/h1>/.test(articles[0].description),
@@ -339,7 +343,7 @@
           assert.strictEqual articles[7].title, 'Day 26'
           done()
         )
-        feedparser.end body
+        feedparser.end body, 'utf8'

   describe 'reindex-needed', ->
     it 'should be used to update an index', (done) ->


fix incorrect size of menu button in user modding page

--- resources/assets/coffee/react/modding-profile/detail-bar.coffee
+++ resources/assets/coffee/react/modding-profile/detail-bar.coffee
@@ -88,7 +88,7 @@

     div className: "#{bn}__entry",
       button
-        className: 'profile-page-toggle js-click-menu'
+        className: 'profile-page-toggle profile-page-toggle--detail js-click-menu'
         title: osu.trans('common.buttons.show_more_options')
         'data-click-menu-target': "profile-page-bar-#{@id}"
         span className: 'fas fa-ellipsis-v'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/text.coffee
+++ test/text.coffee
@@ -1,4 +1,4 @@
-module "Text"
+module "TextEf"

 test "text moves up vertically", ->
   text = Text()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/animated.coffee
+++ src/animated.coffee
@@ -49,6 +49,8 @@
       frames: [0]
     currentFrameIndex: 0
     debugAnimation: false
+    hflip: false
+    vflip: false
     lastUpdate: new Date().getTime()
     useTimer: false
     transform: Matrix.IDENTITY


Add error properties to the Error object.

--- src/api/auth.coffee
+++ src/api/auth.coffee
@@ -71,7 +71,8 @@
         authenticating.resolve({})
       else
         error = new Error('Login failed')
-        error.reason = hash.error.reason
+        for k, v of hash.error
+          error[k] = v
         authenticating.reject(error)

       authenticating = null


namespaced unbind

namespaced unbind

--- src/bindable.coffee
+++ src/bindable.coffee
@@ -45,7 +45,7 @@
   ###
   bind: (events, callback) ->
     events.split(" ").each (namespacedEvent) ->
-      event = namespacedEvent.split()
+      [event =
       eventCallbacks[event] = eventCallbacks[event] || []

       eventCallbacks[event].push(callback)


Forgot to update the key used

Forgot to update the key used

--- resources/assets/coffee/react/profile-page/header-extra.coffee
+++ resources/assets/coffee/react/profile-page/header-extra.coffee
@@ -99,7 +99,7 @@
             if moment(@props.user.join_date).isBefore moment('2008-01-01')
               div
                 className: "#{bn}__row"
-                title: moment(@props.user.join_date).format(osu.trans('common.datetime.month_year.moment')),
+                title: moment(@props.user.join_date).format(osu.trans('common.datetime.year_month.moment')),
                   osu.trans 'users.show.first_members'
             else
               div
@@ -107,7 +107,7 @@
                 dangerouslySetInnerHTML:
                   __html:
                     osu.trans 'users.show.joined_at',
-                      date: rowValue moment(@props.user.join_date).format(osu.trans('common.datetime.month_year.moment'))
+                      date: rowValue moment(@props.user.join_date).format(osu.trans('common.datetime.year_month.moment'))
             div
               className: "#{bn}__row"
               dangerouslySetInnerHTML:


slack: render view according to oauth status

--- client/admin/lib/views/invitations/slackinviteview.coffee
+++ client/admin/lib/views/invitations/slackinviteview.coffee
@@ -29,7 +29,11 @@

     super options, data

-    @createInformationView()
+    $.ajax
+      method  : 'GET'
+      url     : CHANNELS_URL
+      success : @bound 'createInviterViews'
+      error   : @bound 'createInformationView'

   createInformationView : ->


yay more atom cruft

yay more atom cruft

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -15,6 +15,7 @@
       "tmp"
       "vendor"
     ]
+    telemetryConsent: "no"
     themes: [
       "unity-ui"
       "glacier-syntax"


add spec for incorrect quote detection with black slash

--- spec/text-object-spec.coffee
+++ spec/text-object-spec.coffee
@@ -317,6 +317,15 @@
           text: "' something in here and in 'here' ' and over here"
           cursor: [0, 9]

+      it "applies operators inside the current string with backslash", ->
+        set
+          text: "'some-key-here\\': 'here-is-the-val' # comment here"
+          cursor: [0, 2]
+
+        ensure "di'",
+          text: "'': 'here-is-the-val' # comment here"
+          cursor: [0, 1]
+
       it "applies operators inside the current string in operator-pending mode", ->
         ensure "di'",
           text: "''here' ' and over here"


Refactored toggleActiveOpinionType

Refactored toggleActiveOpinionType

Cleaned it and split the toggle to an `on` and `off` method

--- core/app/backbone/views/wheel/interactive_wheel_view.coffee
+++ core/app/backbone/views/wheel/interactive_wheel_view.coffee
@@ -8,7 +8,7 @@
       @setActiveOpinionType fact_id, opinion_type

   setActiveOpinionType: (fact_id, opinion_type) ->
-    @toggleActiveOpinionType opinion_type
+    @turnOnActiveOpinionType opinion_type
     $.ajax
       url: "/facts/#{fact_id}/opinion/#{opinion_type}s.json"
       type: "POST"
@@ -19,11 +19,13 @@
           opinion: opinion_type

       error: =>
-        @toggleActiveOpinionType opinion_type
+        # TODO: This is not a proper undo. Should be restored to the current
+        #       state when the request fails.
+        @turnOffActiveOpinionType opinion_type
         alert "Something went wrong while setting your opinion on the Factlink, please try again"

   unsetActiveOpinionType: (fact_id, opinion_type) ->
-    @toggleActiveOpinionType opinion_type
+    @turnOffActiveOpinionType opinion_type
     $.ajax
       type: "DELETE"
       url: "/facts/#{fact_id}/opinion.json"
@@ -33,17 +35,19 @@
           factlink: @options.fact.id

       error: =>
-        @toggleActiveOpinionType opinion_type
+        @turnOnActiveOpinionType opinion_type
         alert "Something went wrong while removing your opinion on the Factlink, please try again"

-  toggleActiveOpinionType: (toggle_type) ->
-    new_opinion_types = {}
-    for key, old_opinion_type of @model.get('opinion_types')
-      new_opinion_types[old_opinion_type.type] =
-        percentage: old_opinion_type.percentage
-        is_user_opinion: if old_opinion_type.type == toggle_type
-                           not old_opinion_type.is_user_opinion
-                         else
-                           false
+  turned_off_topinion_types: ->
+    believe:    is_user_opinion: false
+    disbelieve: is_user_opinion: false
+    doubt:      is_user_opinion: false
+
+  turnOffActiveOpinionType: (toggle_type) ->
+    @model.updateTo @model.get("authority"), @turned_off_topinion_types()
+
+  turnOnActiveOpinionType: (toggle_type) ->
+    new_opinion_types = @turned_off_topinion_types()
+    new_opinion_types[toggle_type].is_user_opinion = true

     @model.updateTo @model.get("authority"), new_opinion_types


Forgot

Forgot

--- twitter.coffee
+++ twitter.coffee
@@ -70,4 +70,6 @@
           if client and client.disconnected is false
             client.send JSON.stringify data

+    body = body.slice newline + 1
+
 request.end()


change js to coffee

change js to coffee


flag as troll wip

flag as troll wip

--- client/landing/app/Applications/Activity.kdapplication/ListItems/ActivityListItem.coffee
+++ client/landing/app/Applications/Activity.kdapplication/ListItems/ActivityListItem.coffee
@@ -147,11 +147,53 @@

     if superAdmin
       menu[0].items = [
-        { title : 'MARK USER AS TROLL', id : 1,  parentId : null, callback : => @getSingleton('mainController').emit 'TrollReported', data  }
-        { title : 'Delete', id : 2,  parentId : null, callback : => @confirmDeletePost data  }
+        { title : 'MARK USER AS TROLL', id : 1,  parentId : null, callback : => @markUserAsTroll data  }
+        { title : 'UNMARK USER AS TROLL', id : 1,  parentId : null, callback : => @unmarkUserAsTroll data  }
+        { title : 'Delete', id : 3,  parentId : null, callback : => @confirmDeletePost data  }
       ]

       return menu
+
+  unmarkUserAsTroll:(data)->
+
+    if data.originId
+      bongo.cacheable "JAccount", data.originId, (err, account)->
+        account.unflagAccount "exempt", (err, res)->
+          if err then warn err
+          else
+            new KDNotificationView
+              title : "@#{account.profile.nickname} won't be treated as a troll anymore!"
+
+  markUserAsTroll:(data)->
+
+    modal = new KDModalView
+      title          : "MARK USER AS TROLL"
+      content        : """
+

+                          This is what we call "Trolling the troll" mode.

+                          All of the troll's activity will disappear from the feeds, but the troll
+                          himself will think that people still gets his posts/comments.

+                          Are you sure you want to mark him as a troll?
+

+                       """
+      height         : "auto"
+      overlay        : yes
+      buttons        :
+        "YES, THIS USER IS DEFINITELY A TROLL" :
+          style      : "modal-clean-red"
+          loader     :
+            color    : "#ffffff"
+            diameter : 16
+          callback   : =>
+            # debugger
+            if data.originId
+              bongo.cacheable "JAccount", data.originId, (err, account)->
+                account.flagAccount "exempt", (err, res)->
+                  if err then warn err
+                  else
+                    modal.destroy()
+                    new KDNotificationView
+                      title : "@#{account.profile.nickname} marked as a troll!"

   confirmDeletePost:(data)->


--- src/compiler.coffee
+++ src/compiler.coffee
@@ -138,26 +138,37 @@
         @loading.push reload if @loading

         if opts.watch
-            # this is the filesystem listen routine
-            watcher = (curr, prev) ->
-                return if pending
-                pending = yes
-                if curr.mtime isnt prev.mtime
-                    # modified, wait a little before reloading
-                    # since modifications tend to come in waves
-                    setTimeout ( ->
-                        try
-                            do reload
-                        catch err
-                            opts.error(err)
-                    ), 11
-            # no listen on fielsystem for changes
-            if typeof opts.watch is 'object'
-                fs.watchFile(opts.src, opts.watch, watcher)
-            else
-                fs.watchFile(opts.src, watcher)
+            @watch(opts, reload)
         # done
         return elem
+
+    watch: (opts, callback) ->
+        # only watch one html file per compiler and only once
+        return @watchers.push(callback) if @watching
+
+        # initialize
+        @watching = yes
+        @watchers = [callback]
+
+        # this is the filesystem listen routine
+        watcher = (curr, prev) ->
+            return if pending
+            pending = yes
+            if curr.mtime isnt prev.mtime
+                # modified, wait a little before reloading
+                # since modifications tend to come in waves
+                setTimeout ( ->
+                    try
+                        do reload for reload in @watchers
+                    catch err
+                        opts.error(err)
+                ), 11
+        # no listen on fielsystem for changes
+        if typeof opts.watch is 'object'
+            fs.watchFile(@filename, opts.watch, watcher)
+        else
+            fs.watchFile(@filename, watcher)
+

 # exports