Ensure all fields are passed to be removed

--- src/adaptorMethods/destroy.coffee
+++ src/adaptorMethods/destroy.coffee
@@ -7,7 +7,7 @@
   return new Error 'Can\'t find object\'s id' if !@id
   removeFields = {}
   for attr in Object.keys(@constructor.classAttributes)
-    removeFields['remove_' + attr] = this[attr] if this[attr]
+    removeFields['remove_' + attr] = (this[attr] || "")
   redisUpdate.apply(@constructor, [@id, removeFields]).then (newProps) =>
     new Promise (resolve, reject) =>
       @constructor.redis.del @constructor.className + ':' + @id, (err, response) ->


Revert "remove role=button from PublishButtonView"

This reverts commit e1d44862b999194e59e972f5c6099a6ace56fa1d.

this commit caused a critical a11y regression on the modules page, breaking KO support

test plan: the publish buttons on the modules page should operate with the keyboard

fixes LS-1197

Change-Id: I9d3385adc886dbe58bb6cceee151a798283213dc
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/241317
Tested-by: Service Cloud Jenkins <9144042a601061f88f1e1d7a1753ea3e2972119d@instructure.com>
Reviewed-by: Jackson Howe <318c10a818c1b1f2ec50cdcb1827b2c6c5ebd7dc@instructure.com>
QA-Review: Jackson Howe <318c10a818c1b1f2ec50cdcb1827b2c6c5ebd7dc@instructure.com>
Product-Review: Jeremy Stanley <b3f594e10a9edcf5413cf1190121d45078c62290@instructure.com>

--- app/coffeescripts/views/PublishButtonView.coffee
+++ app/coffeescripts/views/PublishButtonView.coffee
@@ -152,6 +152,7 @@
   # render

   render: ->
+    @$el.attr 'role', 'button'
     @$el.attr 'tabindex', '0'
     @$el.html ''
     @cacheEls()


replaced @listenTo with collectionEvents

--- app/scripts/views/layouts/articles_layout.coffee
+++ app/scripts/views/layouts/articles_layout.coffee
@@ -8,19 +8,16 @@
   class ArticlesLayout extends Marionette.LayoutView
     template: JST['templates/layouts/articles_layout']

+    collectionEvents:
+      'add': 'onAddNewArticle'
+
     regions:
       articlesRegion: '#articles_list_region'
       writeArticleRegion: '#write_article_region'

-    initialize: ->
-      @listenTo @collection, 'add', @onAddNewArticle
-
     onRender: ->
       @articlesRegion.show(new ArticlesListView(collection: @collection))
-      @_showFormView()
+      @writeArticleRegion.show(new ArticlesFormView(collection: @collection))

     onAddNewArticle: ->
-      @_showFormView()
-
-    _showFormView: ->
       @writeArticleRegion.show(new ArticlesFormView(collection: @collection))


fixed bug in transformIntoParentSpace

fixed bug in transformIntoParentSpace

--- src/art/engine/core/element.coffee
+++ src/art/engine/core/element.coffee
@@ -1240,8 +1240,9 @@
       {x, y} = v
       element = @
       while element
-        x = element._elementToParentMatrix.transformX x, y
-        y = element._elementToParentMatrix.transformY x, y
+        x1 = element._elementToParentMatrix.transformX x, y
+        y1 = element._elementToParentMatrix.transformY x, y
+        x = x1; y = y1
         element = element.parent
         return point x, y if element == ancestor
       null


highlighting hyphenated common names when non-hyphenated query in S+

--- app/assets/javascripts/species/helpers/handlebars_helpers.js.coffee
+++ app/assets/javascripts/species/helpers/handlebars_helpers.js.coffee
@@ -23,9 +23,11 @@
   query = Ember.Handlebars.get(this, options.hash.query, options)
   return suggestion unless query
   query = query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&")
+  # 'red collared' should highlight 'red-collared'
+  queryWithHyphens = query.replace(/\s/, '-')
   transform = ($1, match) ->
     "" + match + ""
-  new Handlebars.SafeString(suggestion.replace(new RegExp("(" + query + ")", "gi"), transform))
+  new Handlebars.SafeString(suggestion.replace(new RegExp("(" + queryWithHyphens + '|' + query + ")", "gi"), transform))
 )

 Ember.Handlebars.registerHelper 'stringToArray', (string, options) ->


Adds usage rights to the models retrieved for new files

refs CNVS-15867, CNVS-15865, CNVS-15868

Test Plan:
- Nothing really easy to test with this commit.
- You could put a breakpoint in your developer tools and
check to see if a file has usage_rights associated, but
that's rather pointless at this point. Future commits
will be based on this one.

Change-Id: I37677ab79c82d9f08f981f5123a661dc84038793
Reviewed-on: https://gerrit.instructure.com/44544
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Dan Minkevitch <2591e5f46f28d303f9dc027d475a5c60d8dea17a@instructure.com>
QA-Review: Clay Diffrient <9dff2e5c98626d20d2848250c411e8009465bb87@instructure.com>
Product-Review: Clay Diffrient <9dff2e5c98626d20d2848250c411e8009465bb87@instructure.com>

--- app/coffeescripts/react_files/utils/updateAPIQuerySortParams.coffee
+++ app/coffeescripts/react_files/utils/updateAPIQuerySortParams.coffee
@@ -8,7 +8,7 @@
   # use this to change the api url of the collection
   updateAPIQuerySortParams = (collection, queryParams) ->
     newParams =
-      include: ['user']
+      include: ['user', 'usage_rights']
       per_page: 20
       sort: queryParams.sort || ''
       order: queryParams.order || ''


move subscribed before curation fetch

move subscribed before curation fetch

--- desktop/apps/editorial_features/routes.coffee
+++ desktop/apps/editorial_features/routes.coffee
@@ -40,10 +40,11 @@
 @venice = (req, res, next) ->
   @curation = new Curation(id: sd.EF_VENICE)
   @videoGuide = new Article(id: sd.EF_VIDEO_GUIDE)
+  user = res.locals.sd.CURRENT_USER
+  subscribedToEditorial user.email
   @curation.fetch
     success: (curation) =>
       cbs = [
-        subscribedToEditorial user.email
         @videoGuide.fetch(
           headers: 'X-Access-Token': req.user?.get('accessToken') or ''
         )
@@ -58,7 +59,6 @@
           unless videoIndex or videoIndex is 0
             return res.redirect 301, '/venice-biennale'
         res.locals.sd.VIDEO_INDEX = videoIndex
-        console.log @isSubscribed
         res.render 'components/venice_2017/templates/index',
           videoIndex: videoIndex
           curation: curation
@@ -84,4 +84,3 @@
   sailthru.apiGet 'user', { id: email }, (err, response) ->
     if response.vars?.receive_editorial_email
       @isSubscribed = true
-      console.log @isSubscribed


[content] only create profile wrapper if about tab is shown

--- src/coffee/content/profile.coffee
+++ src/coffee/content/profile.coffee
@@ -235,6 +235,10 @@
         if $root.length is 0
             $root = $ '#' + oid + '-co-about-page'

+        if $root.length is 0 or $root.is '.o-xc-Bd'
+            # profile tab isn't shown
+            return
+
         $elem = $root.find 'div.iidentity-profile-wrapper'

         if dot is $root.attr 'data-iidentity'


Revert editable settings

Revert editable settings

--- components/editable_attribute/client/editable_attribute_view.coffee
+++ components/editable_attribute/client/editable_attribute_view.coffee
@@ -30,7 +30,7 @@

     md.setOptions
       renderer: new md.Renderer(),
-      gfm: false
+      gfm: true
       tables: true
       breaks: true
       pedantic: false


Query selector on element.ownerDocument

Query selector on element.ownerDocument

So it works in other contexts like an iframe

--- src/css-selector-generator.coffee
+++ src/css-selector-generator.coffee
@@ -60,7 +60,7 @@
     )
       sanitized_id = "##{@sanitizeItem id}"
       # ID must match single element
-      if document.querySelectorAll(sanitized_id).length is 1
+      if element.ownerDocument.querySelectorAll(sanitized_id).length is 1
         return sanitized_id

     null


update default options

update default options

--- src/ansi_to_html.coffee
+++ src/ansi_to_html.coffee
@@ -70,7 +70,9 @@
 defaults =
 	fg: '#FFF'
 	bg: '#000'
+	newLine: false
 	escapeXML: false
+	stream: false

 class Filter
 	constructor: (options = {}) ->


Converted api/channels.coffee generators to coffeescript.

--- src/api/channels.coffee
+++ src/api/channels.coffee
@@ -7,6 +7,7 @@
 server = require "../server"
 polling = require "../polling"
 authMiddleware = require '../middleware/authorisation'
+utils = require "../utils"

 isPathValid = (channel) ->
   if channel.routes?
@@ -19,18 +20,11 @@
 ###
 # Retrieves the list of active channels
 ###
-exports.getChannels = `function *getChannels() {
-  try {
-    this.body = yield authorisation.getUserViewableChannels(this.authenticated);
-  }
-  catch (e) {
-    // Error! So inform the user
-    logger.error('Could not fetch all channels via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
-
+exports.getChannels = ->
+  try
+    this.body = yield authorisation.getUserViewableChannels this.authenticated
+  catch err
+    utils.logAndSetResponse this, 'internal server error', "Could not fetch all channels via the API: #{err}", 'error'

 processPostAddTriggers = (channel) ->
   if channel.type and authMiddleware.isChannelEnabled channel
@@ -39,93 +33,74 @@
     else if channel.type is 'polling'
       polling.registerPollingChannel channel, (err) -> logger.error err if err

-
 ###
 # Creates a new channel
 ###
-exports.addChannel = `function *addChannel() {
+exports.addChannel = ->
+  # Test if the user is authorised
+  if authorisation.inGroup('admin', this.authenticated) is false
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to addChannel denied.", 'info'
+    return

-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to addChannel denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to addChannel denied.'
-    this.status = 'forbidden';
-    return;
-  }
+  # Get the values to use
+  channelData = this.request.body

-  // Get the values to use
-  var channelData = this.request.body;
+  try
+    channel = new Channel channelData

-  try {
-    var channel = new Channel(channelData);
+    if not isPathValid channel
+      this.body = 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]'
+      this.status = 'bad request'
+      return

-    if (!isPathValid(channel)) {
-      this.body = 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]';
-      this.status = 'bad request';
-      return;
-    }
+    result = yield Q.ninvoke channel, 'save'

-    var result = yield Q.ninvoke(channel, 'save');
+    # All ok! So set the result
+    this.body = 'Channel successfully created'
+    this.status = 'created'
+    logger.info 'User %s created channel with id %s', this.authenticated.email, channel.id

-    // All ok! So set the result
-    this.body = 'Channel successfully created';
-    this.status = 'created';
-    logger.info('User %s created channel with id %s', this.authenticated.email, channel.id);
-
-    processPostAddTriggers(channel);
-  }
-  catch (e) {
-    // Error! So inform the user
-    logger.error('Could not add channel via the API: ' + e);
-    this.body = e.message;
-    this.status = 'bad request';
-  }
-}`
+    processPostAddTriggers channel
+  catch err
+    # Error! So inform the user
+    util.logAndSetResponse this, 'bad request', "Could not add channel via the API: #{err}", 'error'

 ###
 # Retrieves the details for a specific channel
 ###
-exports.getChannel = `function *getChannel(channelId) {
-  // Get the values to use
-  var id = unescape(channelId);
+exports.getChannel = (channelId) ->
+  # Get the values to use
+  id = unescape channelId

-  try {
-    // Try to get the channel
-    var result = null;
-    var accessDenied = false;
-    // if admin allow acces to all channels otherwise restrict result set
-    if (authorisation.inGroup('admin', this.authenticated) === false) {
-      result = yield Channel.findOne({ _id: id, txViewAcl: { $in: this.authenticated.groups } }).exec();
-      var adminResult = yield Channel.findById(id).exec();
-      if (!!adminResult) {
-        accessDenied = true;
-      }
-    } else {
-      result = yield Channel.findById(id).exec();
-    }
+  try
+    # Try to get the channel
+    result = null
+    accessDenied = false
+    # if admin allow acces to all channels otherwise restrict result set
+    if authorisation.inGroup('admin', this.authenticated) is false
+      result = yield Channel.findOne({ _id: id, txViewAcl: { $in: this.authenticated.groups } }).exec()
+      adminResult = yield Channel.findById(id).exec()
+      if adminResult?
+        accessDenied = true
+    else
+      result = yield Channel.findById(id).exec()

-    // Test if the result if valid
-    if (result === null) {
-      if (accessDenied) {
-        // Channel exists but this user doesn't have access
-        this.body = "Access denied to channel with Id: '" + id + "'.";
-        this.status = 'forbidden';
-      } else {
-        // Channel not found! So inform the user
-        this.body = "We could not find a channel with Id:'" + id + "'.";
-        this.status = 'not found';
-      }
-    }
-    else { this.body = result; } // All ok! So set the result
-  }
-  catch (e) {
-    // Error! So inform the user
-    logger.error('Could not fetch channel by Id ' +id+ ' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
-
+    # Test if the result if valid
+    if result is null
+      if accessDenied
+        # Channel exists but this user doesn't have access
+        this.body = "Access denied to channel with Id: '#{id}'."
+        this.status = 'forbidden'
+      else
+        # Channel not found! So inform the user
+        this.body = "We could not find a channel with Id:'#{id}'."
+        this.status = 'not found'
+    else
+      # All ok! So set the result
+      this.body = result
+  catch err
+    # Error! So inform the user
+    utils.logAndSetResponse this, 'internal server error', "Could not fetch channel by Id '#{id}' via the API: #{err}", 'error'

 processPostUpdateTriggers = (channel) ->
   if channel.type
@@ -141,52 +116,39 @@
       else
         polling.removePollingChannel channel, (err) -> logger.error err if err

-
 ###
 # Updates the details for a specific channel
 ###
-exports.updateChannel = `function *updateChannel(channelId) {
+exports.updateChannel = (channelId) ->

-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to updateChannel denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to updateChannel denied.'
-    this.status = 'forbidden';
-    return;
-  }
+  # Test if the user is authorised
+  if authorisation.inGroup('admin', this.authenticated) is false
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to updateChannel denied.", 'info'
+    return

-  // Get the values to use
-  var id = unescape(channelId);
-  var channelData = this.request.body;
+  # Get the values to use
+  id = unescape channelId
+  channelData = this.request.body

-  //Ignore _id if it exists, user cannot change the internal id
-  if (typeof channelData._id !== 'undefined') {
-    delete channelData._id;
-  }
+  # Ignore _id if it exists, user cannot change the internal id
+  if typeof channelData._id isnt 'undefined'
+    delete channelData._id

-  if (!isPathValid(channelData)) {
-    this.body = 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]';
-    this.status = 'bad request';
-    return;
-  }
+  if not isPathValid channelData
+    utils.logAndSetResponse this, 'bad request', 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]', 'info'
+    return

-  try {
-    var channel = yield Channel.findByIdAndUpdate(id, channelData).exec();
+  try
+    channel = yield Channel.findByIdAndUpdate(id, channelData).exec()

-    // All ok! So set the result
-    this.body = 'The channel was successfully updated';
-    logger.info('User %s updated channel with id %s', this.authenticated.email, id);
+    # All ok! So set the result
+    this.body = 'The channel was successfully updated'
+    logger.info 'User %s updated channel with id %s', this.authenticated.email, id

-    processPostUpdateTriggers(channel);
-  }
-  catch (e) {
-    // Error! So inform the user
-    logger.error('Could not update channel by id: ' +id+ ' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
-
+    processPostUpdateTriggers channel
+  catch err
+    # Error! So inform the user
+    utils.logAndSetResponse this, 'internal server error', "Could not update channel by id: #{id} via the API: #{e}", 'error'

 processPostDeleteTriggers = (channel) ->
   if channel.type
@@ -198,42 +160,33 @@
 ###
 # Deletes a specific channels details
 ###
-exports.removeChannel = `function *removeChannel(channelId) {
+exports.removeChannel = (channelId) ->

-  // Test if the user is authorised
-  if (authorisation.inGroup('admin', this.authenticated) === false) {
-    logger.info('User ' +this.authenticated.email+ ' is not an admin, API access to removeChannel denied.')
-    this.body = 'User ' +this.authenticated.email+ ' is not an admin, API access to removeChannel denied.'
-    this.status = 'forbidden';
-    return;
-  }
+  # Test if the user is authorised
+  if authorisation.inGroup('admin', this.authenticated) is false
+    utils.logAndSetResponse this, 'forbidden', "User #{this.authenticated.email} is not an admin, API access to removeChannel denied.", 'info'
+    return

-  // Get the values to use
-  var id = unescape(channelId);
+  # Get the values to use
+  id = unescape channelId

-  try {
-    var numExistingTransactions = yield Transaction.count({ channelID: id }).exec();
+  try
+    numExistingTransactions = yield Transaction.count({ channelID: id }).exec()

-    // Try to get the channel (Call the function that emits a promise and Koa will wait for the function to complete)
-    var channel;
-    if (numExistingTransactions === 0) {
-      //safe to remove
-      channel = yield Channel.findByIdAndRemove(id).exec();
-    } else {
-      //not safe to remove. just flag as deleted
-      channel = yield Channel.findByIdAndUpdate(id, { status: 'deleted' }).exec();
-    }
+    # Try to get the channel (Call the function that emits a promise and Koa will wait for the function to complete)
+    channel
+    if numExistingTransactions is 0
+      # safe to remove
+      channel = yield Channel.findByIdAndRemove(id).exec()
+    else
+      # not safe to remove. just flag as deleted
+      channel = yield Channel.findByIdAndUpdate(id, { status: 'deleted' }).exec()

-    // All ok! So set the result
-    this.body = 'The channel was successfully deleted';
-    logger.info('User %s removed channel with id %s', this.authenticated.email, id);
+    # All ok! So set the result
+    this.body = 'The channel was successfully deleted'
+    logger.info "User #{this.authenticated.email} removed channel with id #{id}"

-    processPostDeleteTriggers(channel);
-  }
-  catch (e) {
-    // Error! So inform the user
-    logger.error('Could not remove channel by id: ' +id+ ' via the API: ' + e);
-    this.body = e.message;
-    this.status = 'internal server error';
-  }
-}`
+    processPostDeleteTriggers channel
+  catch err
+    # Error! So inform the user
+    utils.logAndSetResponse this, 'internal server error', "Could not remove channel by id: #{id} via the API: #{e}", 'error'


LineMap.bufferRowsForScreenRows stops traversing line map when endScreenRow is exceeded

--- src/app/line-map.coffee
+++ src/app/line-map.coffee
@@ -34,7 +34,7 @@
   linesForBufferRows: (startRow, endRow) ->
     @linesByDelta('bufferDelta', startRow, endRow)

-  bufferRowsForScreenRows: (startRow, endRow=@lastScreenRow())->
+  bufferRowsForScreenRows: (startRow, endRow=@lastScreenRow()) ->
     bufferRows = []
     currentScreenRow = -1
     @traverseByDelta 'screenDelta', [startRow, 0], [endRow, 0], ({ screenDelta, bufferDelta }) ->
@@ -158,6 +158,8 @@
     traversalDelta = new Point
     screenDelta = new Point
     bufferDelta = new Point
+    startPosition = Point.fromObject(startPosition)
+    endPosition = Point.fromObject(endPosition)

     for lineFragment, index in @lineFragments
       iterator({ lineFragment, screenDelta, bufferDelta }) if traversalDelta.isGreaterThanOrEqual(startPosition) and iterator?


fix(admin) Check if user is admin on plugins page :lock:


:lipstick:

:lipstick:

--- src/atom/project.coffee
+++ src/atom/project.coffee
@@ -6,5 +6,8 @@
   constructor: (@url) ->

   getFilePaths: ->
-    fs.async.listFiles(@url, true)
+    projectUrl = @url
+    fs.async.list(@url, true).pipe (urls) ->
+      urls = (url.replace(projectUrl, "") for url in urls when fs.isFile(url))
+      urls


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -25,7 +25,7 @@
   # => 2.5 # half the player's width

   player.I.y
-  # => 311.5 # The defa
+  # => 311.5 # The default App


See #1333 and #1437. fix vue nested templates formatted incorrectly

--- src/beautifiers/vue-beautifier.coffee
+++ src/beautifiers/vue-beautifier.coffee
@@ -12,7 +12,7 @@

   beautify: (text, language, options) ->
     return new @Promise((resolve, reject) ->
-      regexp = /(^<(template|script|style)[^>]*>)((\s|\S)*?)^<\/\2>/gim
+      regexp = /(^<(template|script|style)[^>]*>)((\s|\S)*)^<\/\2>/gim

       resolve(text.replace(regexp, (match, begin, type, text) ->
         lang = /lang\s*=\s*['"](\w+)["']/.exec(begin)?[1]


Update with more 'is' replacements

Update with more 'is' replacements

--- src/hubot/hipchat.coffee
+++ src/hubot/hipchat.coffee
@@ -75,7 +75,7 @@
       "method" : method
       "headers": headers

-    if method == "POST"
+    if method is "POST"
       body.auth_token = @options.token
       body = JSON.stringify(body)
       headers["Content-Type"] = "application/json"
@@ -100,7 +100,7 @@
       response.on "error", (err) ->
         callback err, { }

-    if method == "POST"
+    if method is "POST"
       request.end(body, 'binary')
     else
       request.end()


Add most useful roots

Add most useful roots

--- assets/javascripts/react_nodes/root_nodes.coffee
+++ assets/javascripts/react_nodes/root_nodes.coffee
@@ -5,8 +5,36 @@
   of:
     defaultArgs: "1,2,3"
     useScheduler: false
+  just:
+    defaultArgs: "5"
+    useScheduler: true
   fromTime:
     defaultArgs: "{500: 1, 1000: 2, 3000: 3}"
+    useScheduler: true
+  interval:
+    defaultArgs: "1000"
+    useScheduler: true
+  timer:
+    defaultArgs: "500, 2000"
+    useScheduler: true
+  repeat:
+    defaultArgs: "42, 3"
+    useScheduler: true
+  generate:
+    defaultArgs: "0, function(x){ return x < 3;}, function(x) { return x + 1;}, function(x) { return x; }"
+    useScheduler: true
+  generateWithRelativeTime:
+    defaultArgs: "1, function(x) { return x < 4;}, function(x) { return x + 1;}, function(x) { return x; }, function(x) { return 500 * x; }"
+    useScheduler: true
+  never:
+    useScheduler: false
+  empty:
+    useScheduler: true
+  range:
+    defaultArgs: "1, 5"
+    useScheduler: true
+  pairs:
+    defaultArgs: "{foo: 42, bar: 56, baz: 78}"
     useScheduler: true

 SelectRoot = React.createClass


Keep rects local for link hints.

Keep rects local for link hints.

Previously, we passed each hints rect to every other frame.  However,
the rect is only actually needed in its own frame.

Here, we instead keep (and use) each hint's rect locally only.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -79,8 +79,8 @@
       @localHints = LocalHints.getLocalHints()
       console.log "getHintDescriptors", frameId, "[#{@localHints.length}]" if @debug
       @sendMessage "postHintDescriptors", hintDescriptors:
-        @localHints.map ({rect, linkText, showLinkText, hasHref, reason}, localIndex) ->
-          {rect, linkText, showLinkText, hasHref, reason, frameId, localIndex}
+        @localHints.map ({linkText, showLinkText, hasHref, reason}, localIndex) ->
+          {linkText, showLinkText, hasHref, reason, frameId, localIndex}

   # We activate LinkHintsMode() in every frame and provide every frame with exactly the same hint descriptors.
   # We also propagate the key state between frames.  Therefore, the hint-selection process proceeds in lock
@@ -188,18 +188,17 @@
     marker =
       if desc.frameId == frameId
         el = DomUtils.createElement "div"
-        el.style.left = desc.rect.left + window.scrollX + "px"
-        el.style.top = desc.rect.top  + window.scrollY  + "px"
+        el.rect = HintCoordinator.getLocalHintMarker(desc).rect
+        el.style.left = el.rect.left + window.scrollX + "px"
+        el.style.top = el.rect.top  + window.scrollY  + "px"
         extend el, className: "vimiumReset internalVimiumHintMarker vimiumHintMarker"
       else
         {}

-    # Extract other relevant fields from the hint descriptor and a stable sort key.
     extend marker,
       hintDescriptor: desc
       linkText: desc.linkText
       showLinkText: desc.showLinkText
-      rect: desc.rect
       isLocalMarker: desc.frameId == frameId
       stableSortCount: ++@stableSortCount

@@ -319,7 +318,7 @@

     installKeyboardBlocker = (startKeyboardBlocker) ->
       if linkMatched.isLocalMarker
-        flashEl = DomUtils.addFlashRect linkMatched.hintDescriptor.rect
+        flashEl = DomUtils.addFlashRect linkMatched.rect
         HintCoordinator.onExit.push -> DomUtils.removeElement flashEl

       if document.hasFocus()
@@ -332,7 +331,7 @@
     else if userMightOverType
       installKeyboardBlocker (callback) -> new TypingProtector 200, callback
     else if linkMatched.isLocalMarker
-      DomUtils.flashRect linkMatched.hintDescriptor.rect
+      DomUtils.flashRect linkMatched.rect
       HintCoordinator.sendMessage "exit", isSuccess: true

   #


Use title attribute for link text.

Use title attribute for link text.

Google seems to be using a lot of "title" attributes on buttons.  This
makes it possible to type the text in filtered link-hints mode.  (And
you can often guess the text; e.g. "close").

We could also show the title text.  That is very visually noisy though,
in practice.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -858,8 +858,12 @@
     else if hint.reason?
       linkText = hint.reason
       showLinkText = true
+    else if 0 < element.textContent.length
+      linkText = element.textContent[...256]
+    else if element.hasAttribute "title"
+      linkText = element.getAttribute "title"
     else
-      linkText = element.textContent[...256] || element.innerHTML[...256]
+      linkText = element.innerHTML[...256]

     {linkText: linkText.trim(), showLinkText}


Prevent passing through <ctrl> in LinkHints

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -187,7 +187,7 @@
     # TODO(philc): Ignore keys that have modifiers.
     if (KeyboardUtils.isEscape(event))
       @deactivateMode()
-    else if (event.keyCode != keyCodes.shiftKey)
+    else if (event.keyCode != keyCodes.shiftKey or event.keyCode != keyCodes.ctrlKey)
       keyResult = @markerMatcher.matchHintsByKey(hintMarkers, event)
       linksMatched = keyResult.linksMatched
       delay = keyResult.delay ? 0


change content type

change content type

--- static/js/services/GuidesFactory.js.coffee
+++ static/js/services/GuidesFactory.js.coffee
@@ -16,7 +16,6 @@
         'expected': 0
         'actual': 0
         'processed': 0
-
     # expected --> how many we would get if user's file is 100% correct.
     # actual   --> how much we could actually derived from the user's file.

@@ -75,11 +74,6 @@
               if gene.name.toUpperCase() == geneText.toUpperCase() or gene.ensembl_id == geneText
                 this_.data.genes.push(gene)
                 break
-            # replace (for gene in .... break) with below, if you don't need EXACT comparisons.
-            # matchedGenes = $filter('propsFilter')(this.available.genes, {name: geneText, ensembl_id: geneText})
-            # matchedGenes.sort()
-            # if matchedGenes.length > 0
-            #   this.data.genes.push(matchedGenes[0])
         this_.data.gene_statistics.expected = expected_genes_num
         this_.data.gene_statistics.actual = this_.data.genes.length
         resolve()
@@ -112,6 +106,8 @@
       $http {
         url: '/status/' + task_id
         method: 'GET'
+        headers:
+          'Content-Type': 'application/json'
       }
       .success (data) ->
         # check if we are finished


Remove transasções ao deletar conta

Remove transasções ao deletar conta

--- Operador-Interno/server/methods/conta.coffee
+++ Operador-Interno/server/methods/conta.coffee
@@ -31,4 +31,6 @@

 		contas.remove _id: data.conta

+		transacoes.remove conta: data.conta
+
 		return true


Slider

Slider

- fix bug (can’t use transform because its based on the elements dimensions instead of the parents)

--- src/elements/Slider/Slider.coffee
+++ src/elements/Slider/Slider.coffee
@@ -116,7 +116,7 @@

 	displayValue: ->
 		percent = @getValue() / @_max * 100
-		CUI.DOM.setStyle(@__handle, transform: "translateX(#{percent}%")
+		CUI.DOM.setStyle(@__handle, left: percent +'%')
 		@_onUpdate?(@, @getValue())

 	checkValue: (v, flags) ->


add route for voting on band

add route for voting on band

--- routes/index.coffee
+++ routes/index.coffee
@@ -42,12 +42,13 @@
 ), (req, res) ->
   res.redirect req.session.returnTo or '/'

-# this exists out of protected group because we want unsigned in users
-# to be able to see votes
+# this exists out of protected group because we want unsigned
+# in users to be able to see votes
 router.get '/api/bands', bandsController.index

 api.get '/bands/:id', bandsController.show
 api.post '/bands', bandsController.create
+api.post '/bands/:id/vote', bandsController.vote
 api.put '/bands/:id', bandsController.update
 api.delete '/bands/:id', bandsController.delete


[master] Start implemeting basic algorithm

--- routes/index.coffee
+++ routes/index.coffee
@@ -1,3 +1,7 @@
+predict = (data) ->
+  time = (i[0] for i in data)
+  return
+
 exports.index = (req, res) ->
   res.render 'index.html'
   return
@@ -5,13 +9,15 @@
 exports.api = (req, res) ->
   fs = require 'fs'
   path = require 'path'
-  fs.readFile path.join(__dirname, '../data/table'), 'ascii', (err, data) ->
+  fs.readFile path.join(__dirname, '../data/CU.intraday.csv.fmt'), 'ascii', (err, data) ->
     if err
       throw err
-    list = []
-    raw = data.split('\n')[1...-1]
-    list.push [i.split(',')[0], parseFloat(i.split(',')[4])] for i in raw
+    raw = JSON.parse data
+    predict raw
+    list = (raw[i] for i in [0..raw.length - 1] by raw.length // 300)
+    list.reverse()
     list.push ['Time', 'Close']
     list.reverse()
     res.json {'query': req.params.name, 'data': list}
   return
+


Remove unused require

Remove unused require

--- lib/directory-view.coffee
+++ lib/directory-view.coffee
@@ -1,7 +1,6 @@
 {Subscriber} = require 'emissary'
 Directory = require './directory'
 FileView = require './file-view'
-File = require './file'

 class DirectoryView extends HTMLElement
   Subscriber.includeInto(this)


Revert "Added support.function to the scopes list"

This reverts commit 2f7cb3e307ba1689e24091c3fcb480755bb340cf.

--- lib/fold-functions.coffee
+++ lib/fold-functions.coffee
@@ -83,15 +83,13 @@
     # line of the file.
     if autofold and atom.config.get('fold-functions.skipAutofoldWhenNotFirstLine')
       onFirstLine = true
-      bufferRow = null
       for cursor in editor.getCursors()
         if cursor.getBufferRow() > 0
-          bufferRow = cursor.getBufferRow()
           onFirstLine = false
           break

       if not onFirstLine
-        @debugMessage('fold function: not on first line, skipping autofold', bufferRow)
+        @debugMessage('fold function: not on first line, skipping autofold')
         autofold = false

     # figure out if we should skip autofolding because there is only one
@@ -192,8 +190,7 @@
         'meta.function',
         'meta.method',
         'storage.type.arrow',
-        'entity.name.function.constructor',
-        'support.function'
+        'entity.name.function.constructor'
       )
       @debugMessage('fold functions: is foldable', (foldable and isFunction and not isCommented))
       if foldable and isFunction and not isCommented


Check previous row for functions

Check previous row for functions

--- lib/fold-functions.coffee
+++ lib/fold-functions.coffee
@@ -218,6 +218,12 @@
         row,
         scopes
       )
+      if !isFunction and row > 1
+        isFunction = @hasScopeAtBufferRow(
+          editor,
+          row - 1,
+          scopes
+        )
       @debugMessage 'fold functions: is foldable',
         lines,
         (foldable and isFunction and not isCommented),


Better confirmation

Better confirmation

--- src/layout/mixins/programs.coffee
+++ src/layout/mixins/programs.coffee
@@ -110,7 +110,7 @@

   program_row = (name, contents) ->
     buttons = make.buttons "action", contents, my.action, ((button, args) ->
-        if sprite.get('editable') and confirm('Are you sure?')
+        if sprite.get('editable') and confirm('Are you sure you want to remove that action?')
           button.up.remove_child(button)
           button.send 'click'
           button.send 'brick', -1


Better function name

Better function name

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

               @post reply, 'reply'

-          if @hasValidBeatmap()
+          if @canBeRepliedTo()
             el BeatmapDiscussions.NewReply,
               currentUser: @props.currentUser
               beatmapset: @props.beatmapset
@@ -117,7 +117,7 @@

     topClasses = "#{vbn} #{vbn}--#{type}"
     topClasses += " #{vbn}--inactive" if score != 0
-    disabled = @isOwner() || (type == 'down' && !@canDownvote()) || !@hasValidBeatmap()
+    disabled = @isOwner() || (type == 'down' && !@canDownvote()) || !@canBeRepliedTo()

     button
       className: topClasses
@@ -162,7 +162,7 @@
     @props.currentUser.is_admin || @props.currentUser.is_gmt || @props.currentUser.is_qat || @props.currentUser.is_bng

-  hasValidBeatmap: =>
+  canBeRepliedTo: =>
     !@props.discussion.beatmap_id? || !@props.currentBeatmap.deleted_at?


Fix review top bar colour

Fix review top bar colour

--- resources/assets/coffee/react/beatmap-discussions/discussion.coffee
+++ resources/assets/coffee/react/beatmap-discussions/discussion.coffee
@@ -77,8 +77,7 @@
         div className: "#{bn}__discussion",
           div
             className: "#{bn}__top"
-            style:
-              color: osu.groupColour(group)
+            style: osu.groupColour(group)
             div className: "#{bn}__discussion-header",
               el UserCard,
                 user: user


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -11,6 +11,8 @@
     x: 15
     y: 30
     speed:  2
+
+  player.include Contro

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


Minor cleanup

Minor cleanup

--- resources/assets/coffee/react/beatmapset-page/header.coffee
+++ resources/assets/coffee/react/beatmapset-page/header.coffee
@@ -20,14 +20,6 @@
 el = React.createElement

 class BeatmapsetPage.Header extends React.Component
-  toggleFavourite: (e) ->
-    e.preventDefault()
-
-    if !currentUser.id?
-      userLogin.show e.target
-    else
-      $.publish 'beatmapset:favourite:toggle'
-
   render: ->
     dateFormat = 'MMM D, YYYY'

@@ -86,9 +78,11 @@
           el BeatmapsetMapping, beatmapset: @props.beatmapset

           if currentUser.id?
-            div {},
+            [
               if @props.beatmapset.availability
-                div className: 'beatmapset-header__availability-info',
+                div
+                  key: 'availability'
+                  className: 'beatmapset-header__availability-info',
                   if @props.beatmapset.availability.download_disabled
                     osu.trans 'beatmaps.beatmapset.availability.disabled'
                   else
@@ -98,40 +92,48 @@
                     div className: 'beatmapset-header__availability-link',
                       a href: @props.beatmapset.availability.more_information, target: '_blank', osu.trans 'beatmaps.beatmapset.availability.more-info'

-              unless @props.beatmapset.availability?.download_disabled
-                div className: 'beatmapset-header__buttons',
-                  if @props.beatmapset.video
-                    [
+              div
+                key: 'buttons'
+                className: 'beatmapset-header__buttons'
+                unless @props.beatmapset.availability?.download_disabled
+                  [
+                    if @props.beatmapset.video
+                      [
+                        @downloadButton
+                          key: 'video'
+                          href: Url.beatmapDownload @props.beatmapset.id, true
+                          bottomTextKey: 'video'
+
+                        @downloadButton
+                          key: 'no-video'
+                          href: Url.beatmapDownload @props.beatmapset.id, false
+                          bottomTextKey: 'no-video'
+                      ]
+                    else
                       @downloadButton
-                        key: 'video'
-                        href: Url.beatmapDownload @props.beatmapset.id, true
-                        bottomTextKey: 'video'
+                        key: 'default'
+                        href: Url.beatmapDownload @props.beatmapset.id, false

-                      @downloadButton
-                        key: 'no-video'
-                        href: Url.beatmapDownload @props.beatmapset.id, false
-                        bottomTextKey: 'no-video'
-                    ]
-                  else
                     @downloadButton
-                      href: Url.beatmapDownload @props.beatmapset.id, false
+                      key: 'direct'
+                      topTextKey: 'direct'
+                      href:
+                        if currentUser.isSupporter
+                          Url.beatmapDownloadDirect @props.beatmapset.id
+                        else
+                          laroute.route 'support-the-game'
+                  ]

-                  @downloadButton
-                    topTextKey: 'direct'
-                    href:
-                      if currentUser.isSupporter
-                        Url.beatmapDownloadDirect @props.beatmapset.id
-                      else
-                        laroute.route 'support-the-game'
-
-                  if @props.beatmapset.discussion_status.enabled
-                    el BigButton,
-                      modifiers: ['beatmapset-header']
-                      text:
-                        top: osu.trans 'beatmaps.beatmapset.show.discussion'
-                      icon: 'comments-o'
-                      props:
-                        href: laroute.route 'beatmaps.beatmapsets.show.discussion', beatmapset: @props.beatmapset.id
+                if @props.beatmapset.discussion_status.enabled
+                  el BigButton,
+                    key: 'discussion'
+                    modifiers: ['beatmapset-header']
+                    text:
+                      top: osu.trans 'beatmaps.beatmapset.show.discussion'
+                    icon: 'comments-o'
+                    props:
+                      href: laroute.route 'beatmaps.beatmapsets.show.discussion', beatmapset: @props.beatmapset.id
+            ]

         div className: 'beatmapset-header__box beatmapset-header__box--stats',
           el BeatmapsetPage.Stats,
@@ -158,5 +160,13 @@
     BeatmapsetPageHash.generate mode: mode

+  toggleFavourite: (e) ->
+    e.preventDefault()
+
+    if !currentUser.id?
+      userLogin.show e.target
+    else
+      $.publish 'beatmapset:favourite:toggle'
+
   togglePreview: (e) =>
     $.publish 'beatmapset:preview:toggle', !@props.isPreviewPlaying


[LoadingFree] change signature of koding home script

--- workers/social/lib/social/render/loggedin/kodinghome.coffee
+++ workers/social/lib/social/render/loggedin/kodinghome.coffee
@@ -1,4 +1,4 @@
-module.exports = ({account, client}, callback)->
+module.exports = ({account, client, bongoModels}, callback)->

   getStyles       = require './../styleblock'
   fetchScripts    = require './../scriptblock'
@@ -61,7 +61,7 @@
     </html>
     """

-  fetchScripts {client, intro : no}, (err, scripts)->
+  fetchScripts {bongoModels, client, intro : no}, (err, scripts)->
     callback null, prepareHTML {scripts}


Changed form submission so that hidden forms are not sent

--- app/assets/javascripts/admin/admin_in_place_editor.js.coffee
+++ app/assets/javascripts/admin/admin_in_place_editor.js.coffee
@@ -14,7 +14,7 @@

   initModals: () ->
     $('.modal .modal-footer .save-button').click () ->
-      $(@).closest('.modal').find('form').submit()
+      $(@).closest('.modal').find('form:visible').submit()

     $('.modal').on 'hidden', () ->
       form = $(@).find('form')


Only show real name for users that have one set.

Resolves #346

--- app/assets/javascripts/components/overview/inline_users.cjsx
+++ app/assets/javascripts/components/overview/inline_users.cjsx
@@ -6,7 +6,11 @@
   render: ->
     key = @props.title + '_' + @props.role
     if @props.users.length > 0 && @props.users[0].real_name?
-      user_list = @props.users.map (user) ->  "#{user.real_name} (#{user.wiki_id}#{if user.email? then " / " + user.email else ""})"
+      user_list = @props.users.map (user) ->
+        if user.real_name?
+          "#{user.real_name} (#{user.wiki_id}#{if user.email? then " / " + user.email else ""})"
+        else
+          user.wiki_id
     else
       user_list = _.pluck(@props.users, 'wiki_id')
     user_list = user_list.join(', ')


Clarified message more

Clarified message more

--- core/app/backbone/modules/non_confirmed_email_warning.coffee
+++ core/app/backbone/modules/non_confirmed_email_warning.coffee
@@ -9,7 +9,7 @@
     return if currentUser.get('confirmed')

     FactlinkApp.NotificationCenter.error """
-      Your email address has not yet been confirmed, please check your inbox.
+      Your email address has not yet been confirmed, please check your email inbox.
       <a href="/users/confirmation/new" target="_blank" class="js-close">
         Resend email.
       </a>


Removed check for in tour

Removed check for in tour

--- core/app/backbone/modules/non_confirmed_email_warning.coffee
+++ core/app/backbone/modules/non_confirmed_email_warning.coffee
@@ -1,14 +1,6 @@
 FactlinkApp.module "NonConfirmedEmailWarning", (NonConfirmedEmailWarning, FactlinkApp, Backbone, Marionette, $, _) ->

-  inTour = ->
-    try
-      return /^\/p\/(tour)/.test(window.top.location.pathname)
-    catch # DOMException from window.top if loaded in client
-      return false
-
-
   checkConfirmedEmail = ->
-    return if inTour()
     return unless FactlinkApp.signedIn()
     return if currentUser.get('confirmed')
     return if currentUser.justCreated()


Track sign in with Facebook on mixpanel

Track sign in with Facebook on mixpanel

--- app/assets/javascript/fancy_login/login_register_fancybox.coffee
+++ app/assets/javascript/fancy_login/login_register_fancybox.coffee
@@ -70,6 +70,9 @@
     $('#login_form').submit (e) =>
       mixingpanel_tracker.track "Login", {"action":"signin", "location":"login fancybox", "url": document.URL }
       @formSubmit e, $('#login_form').data('url')
+
+    $('#loginbox-sign-in .btn-facebook').click (e) =>
+      mixingpanel_tracker.track "Login", {"action":"signin with facebook", "location":"login fancybox", "url": document.URL }

     $('#register_form').submit (e) =>
       mixingpanel_tracker.track "Register", {"action":"register", "location":"login fancybox", "url": document.URL }


more defensive

more defensive

--- app/assets/javascripts/effective_datatables/initialize.js.coffee
+++ app/assets/javascripts/effective_datatables/initialize.js.coffee
@@ -168,7 +168,7 @@

 destroyDataTables = ->
   $('.effective-datatables-inline-expanded').removeClass('effective-datatables-inline-expanded')
-  $('table.effective-datatable').each -> $(this).removeClass('initialized').DataTable().destroy()
+  $('table.effective-datatable').each -> try $(this).removeClass('initialized').DataTable().destroy()

 $ -> initializeDataTables()
 $(document).on 'page:change', -> initializeDataTables()


Also recalculate when used in a bootstrap collapse

--- app/assets/javascripts/effective_datatables/responsive.js.coffee
+++ app/assets/javascripts/effective_datatables/responsive.js.coffee
@@ -2,3 +2,6 @@
   $($(event.target).attr('href')).find('table.dataTable').each ->
     $(this).DataTable().columns.adjust().responsive.recalc()

+$(document).on 'shown.bs.collapse', (event) ->
+  $(event.target).find('table.dataTable').each ->
+    $(this).DataTable().columns.adjust().responsive.recalc()


hide the tooltip on mouseout; refactor

hide the tooltip on mouseout; refactor

--- app/assets/javascripts/states/monitoring/helpers/graph.js.coffee
+++ app/assets/javascripts/states/monitoring/helpers/graph.js.coffee
@@ -131,9 +131,7 @@
   g._bixby_touch_enabled = false # default to disabled

   # add a hidden point which we can use to draw a tooltip
-  $(div).append("

")
-  tooltip_anchor = $(div).find("div.tooltip_anchor")
-  tooltip_anchor.tooltip({trigger: "manual", container: div, placement: "left"})
+  tooltip_anchor = Bixby.monitoring.create_tooltip(div)

   # set callbacks - have to do this after initial graph created
   opts = {
@@ -181,6 +179,18 @@

   return g

+# Create the tooltip
+Bixby.monitoring.create_tooltip = (div) ->
+  $(div).append("

")
+  tooltip_anchor = $(div).find("div.tooltip_anchor")
+  tooltip_anchor.tooltip({trigger: "manual", container: div, placement: "left"})
+
+  $(div).find("div.graph").mouseout (e) ->
+    tooltip_anchor.tooltip("hide")
+
+  return tooltip_anchor
+
+# Show the tooltip - called from the graph highlightCallback
 Bixby.monitoring.show_tooltip = (g, div, el, pts, text) ->
   if g._bixby_dragging
     el.tooltip("hide")


Use actual bootstrap javascript, not some weird homebrew version? :3

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -1,5 +1,5 @@
 #= require jquery
-#= require bootstrap-fix
+#= require bootstrap
 #= require jquery_ujs
 #= require jquery-ui.js
 #= require jquery.cookie


Fix adding current awards when removing initial one

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -430,7 +430,7 @@

         if can_add
           add_eg = add_eg.replace(/(form\[(\w+|_)\]\[)(\d+)\]/g, "$1#{li_size+1}]")
-          question.find(".list-add").append("
#{add_eg}
")
+          question.find(".list-add").append("
#{add_eg}
")

           clear_example = question.find(".list-add").attr("data-need-to-clear-example")
           if (typeof(clear_example) != typeof(undefined) && clear_example != false)


don't load metadata twice

don't load metadata twice

--- app/assets/javascripts/models/host.js.coffee
+++ app/assets/javascripts/models/host.js.coffee
@@ -42,7 +42,7 @@
       $.getJSON @url() + "/update_facts", callback

     get_metadata: (callback) ->
-      return if @_metadata_loaded?
+      return if @_metadata_loaded? || @has("metadata")
       $.getJSON @url() + "/metadata", callback
       @_metadata_loaded = true


show day time in window title bar if timer is running

--- app/assets/javascripts/time_sheets.js.coffee
+++ app/assets/javascripts/time_sheets.js.coffee
@@ -1,3 +1,5 @@
+document.originalTitle = document.title
+
 $(document).on 'ajax:error', '.reveal-modal.time form', (xhr, status, error) ->
   console.log xhr, status, error

@@ -85,6 +87,10 @@

   window.updateTimes = ->
     $.getJSON $('.ajax.summary_for_date').attr('href'), (data) ->
+      # title bar
+      if window.timerStarted
+        document.title = '[' + data.total + '] ' + document.originalTitle
+
       $('.time.total').text data.total
       $('.time.bonus').text data.bonus


Fixing sprints counter in backlog

Fixing sprints counter in backlog

--- app/coffee/modules/backlog/lightboxes.coffee
+++ app/coffee/modules/backlog/lightboxes.coffee
@@ -167,8 +167,8 @@
 CreateSprint = ($repo, $rs, $rootscope) ->
     link = ($scope, $el, attrs) ->
         $scope.milestonesCounter = "--"
-        bindOnce $scope, "stats", (stats) ->
-            $scope.milestonesCounter = stats.milestones.length
+        bindOnce $scope, "sprints", (sprints) ->
+            $scope.milestonesCounter = sprints.length

         submit = ->
             form = $el.find("form").checksley()


Inject RPC dynamically into form service only when needed

--- app/coffee/modules/packages/core/form.coffee
+++ app/coffee/modules/packages/core/form.coffee
@@ -1,7 +1,7 @@

 exports.$directive = [
-	'$compile', '$q', 'core/form', 'require', 'comm/rpc'
-	($compile, $q, form, require, rpc) ->
+	'$compile', '$injector', '$q', 'core/form', 'require'
+	($compile, $injector, $q, form, require) ->

 		link: (scope, element, attrs) ->

@@ -68,14 +68,19 @@
 									continue if field.type is 'submit'
 									fields[name] = scope[name]

-								rpc.call(
-									dottedFormKey
-									fields
-								).then(
-									(result) -> handler null, result
-									(error) -> handler error
-								)
-
+								$injector.invoke [
+									'comm/rpc'
+									(rpc) ->
+
+										rpc.call(
+											dottedFormKey
+											fields
+										).then(
+											(result) -> handler null, result
+											(error) -> handler error
+										)
+								]
+
 						$input = angular.element(
 							'<input type="submit">'
 						)


Normalized: http responses

Normalized: http responses

--- app/controllers/attachment_controller.coffee
+++ app/controllers/attachment_controller.coffee
@@ -45,7 +45,7 @@
     db.get params.id, (err, doc) =>
         if err and err.error == "not_found"
             app.locker.removeLock @lock
-            deleteFiles req, -> send error: err.error, 404
+            deleteFiles req, -> send error: "not found", 404
         else if err
             console.log "[Attachment] err: " + JSON.stringify err
             app.locker.removeLock @lock
@@ -92,14 +92,14 @@
                 console.log "[Attachment] err: " + JSON.stringify err
                 deleteFiles req, -> send error: err.error, 500
             else
-                deleteFiles req, -> send success: true, msg: 'created', 201
+                deleteFiles req, -> send success: true, 201

         fs.createReadStream(file.path).pipe(stream)

     else
         console.log "no doc for attachment"

-        send error: true, msg: "No file send", 400
+        send error: "No file sent", 400

 # GET /data/:id/attachments/:name
@@ -108,11 +108,11 @@

     stream = db.getAttachment @doc.id, name, (err) ->
         if err and err.error = "not_found"
-            send error: err.error, 404
+            send error: "not found", 404
         else if err
             send error: err.error, 500
         else
-            send 200
+            send success: true, 200

     if req.headers['range']?
         stream.setHeader('range', req.headers['range'])
@@ -129,9 +129,9 @@

     db.removeAttachment @doc, name, (err, res) ->
         if err and err.error = "not_found"
-            send error: err.error, 404
+            send error: "not found", 404
         else if err
             console.log "[Attachment] err: " + JSON.stringify err
             send error: err.error, 500
         else
-            send success: true, msg: 'deleted', 204
+            send success: true, 204


close HTTP keep-alive connections when shutting down

--- services/filestore/app.coffee
+++ services/filestore/app.coffee
@@ -62,6 +62,12 @@
 app.get  "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.getFile
 app.post "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.insertFile

+app.use (req, res, next) ->
+	if not appIsOk
+		# when shutting down, close any HTTP keep-alive connections
+		res.set 'Connection', 'close'
+	next()
+
 app.put "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.copyFile
 app.del "/project/:project_id/file/:file_id", keyBuilder.userFileKey, fileController.deleteFile


Handle ECONNRESET in the same way as EPIPE

--- services/real-time/app.coffee
+++ services/real-time/app.coffee
@@ -141,7 +141,8 @@
 	if Settings.errors?.catchUncaughtErrors
 		process.removeAllListeners('uncaughtException')
 		process.on 'uncaughtException', (error) ->
-			if error.code == 'EPIPE'
+			if ['EPIPE', 'ECONNRESET'].includes(error.code)
+				Metrics.inc('disconnected_write')
 				return logger.warn err: error, 'attempted to write to disconnected client'
 			logger.error err: error, 'uncaught exception'
 			if Settings.errors?.shutdownOnUncaughtError


Clean up gradient convertor

Clean up gradient convertor

--- lib/hacks/gradient.coffee
+++ lib/hacks/gradient.coffee
@@ -9,36 +9,22 @@
   @names = ['linear-gradient', 'repeating-linear-gradient',
             'radial-gradient', 'repeating-radial-gradient']

-  # Smarter regexp for gradients
-  regexp: ->
-    @regexpCache ||= new RegExp('(^|\\s|,)' + @name + '\\((.+)\\)', 'gi')
-
   # Change degrees for webkit prefix
   replace: (string, prefix) ->
     values = list.comma(string).map (value) =>
-      value.replace @regexp(), (all, before, args) =>
-        params = list.comma(args)
-        params = @newDirection(params)
+      return value if value[0..@name.length] != @name + '('

-        if prefix == '-webkit- old'
-          return all if @name != 'linear-gradient'
-          return all if params[0] and params[0].indexOf('deg') != -1
-          return all if args.indexOf('-corner') != -1
-          return all if args.indexOf('-side')   != -1
+      close  = value.lastIndexOf(')')
+      after  = value[close + 1..-1]
+      args   = value[@name.length + 1..close - 1]
+      params = list.comma(args)
+      params = @newDirection(params)

-          params = @oldDirection(params)
-          params = @colorStops(params)
-
-          '-webkit-gradient(linear, ' + params.join(', ') + ')'
-        else
-          if params.length > 0
-            if params[0][0..2] == 'to '
-              params[0] = @fixDirection(params[0])
-            else if params[0].indexOf('deg') != -1
-              params[0] = @fixAngle(params[0])
-            else if params[0].indexOf(' at ') != -1
-              @fixRadial(params)
-          before + prefix + @name + '(' + params.join(', ') + ')'
+      if prefix == '-webkit- old'
+        @oldWebkit(value, args, params, after)
+      else
+        @convertDirection(params)
+        prefix + @name + '(' + params.join(', ') + ')' + after

     values.join(', ')

@@ -73,6 +59,28 @@

     params

+  # Convert to old webkit syntax
+  oldWebkit: (value, args, params, after) ->
+    return value if @name != 'linear-gradient'
+    return value if params[0] and params[0].indexOf('deg') != -1
+    return value if args.indexOf('-corner') != -1
+    return value if args.indexOf('-side')   != -1
+
+    params = @oldDirection(params)
+    params = @colorStops(params)
+
+    '-webkit-gradient(linear, ' + params.join(', ') + ')' + after
+
+  # Change new direction to old
+  convertDirection: (params) ->
+    if params.length > 0
+      if params[0][0..2] == 'to '
+        params[0] = @fixDirection(params[0])
+      else if params[0].indexOf('deg') != -1
+        params[0] = @fixAngle(params[0])
+      else if params[0].indexOf(' at ') != -1
+        @fixRadial(params)
+
   # Replace `to top left` to `bottom right`
   fixDirection: (param) ->
     param = param.split(' ')
@@ -91,11 +99,6 @@
     param = Math.abs(450 - param) % 360
     param = @roundFloat(param, 3)
     "#{param}deg"
-
-  # Fix radial direction syntax
-  fixRadial: (params) ->
-    first = params[0].split(/\s+at\s+/)
-    params.splice(0, 1, first[1], first[0])

   oldDirection: (params) ->
     params if params.length == 0
@@ -135,6 +138,11 @@
       else
         "color-stop(#{color})"

+  # Fix radial direction syntax
+  fixRadial: (params) ->
+    first = params[0].split(/\s+at\s+/)
+    params.splice(0, 1, first[1], first[0])
+
   # Remove old WebKit gradient too
   old: (prefix) ->
     if prefix == '-webkit-'


Add remote path for mathjax

Add remote path for mathjax

--- services/web/Gruntfile.coffee
+++ services/web/Gruntfile.coffee
@@ -88,6 +88,7 @@
 					preserveLicenseComments: false
 					paths:
 						"moment": "libs/moment-2.7.0"
+						"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"
 					shim:
 						"libs/pdfListView/PdfListView":
 							deps: ["libs/pdf"]


Fix bug list display issue

Fix bug list display issue

--- lib/assets/javascripts/bug_file_formatter.js.coffee
+++ lib/assets/javascripts/bug_file_formatter.js.coffee
@@ -21,7 +21,7 @@
 #
 root.formatBugFile = (bug) ->
   #TODO don't guess, record this information
-  if bug.file =~ /^\[S\] /
+  if bug.file.match(/^\[S\] /)
     "<simple blamer>"
   else
     parts = bug.file.split('/')


Autolink urls not wrapped in markdown link syntax

--- lib/assets/javascripts/helpers/formatting.js.coffee
+++ lib/assets/javascripts/helpers/formatting.js.coffee
@@ -93,11 +93,45 @@
     _truncated

   formatTentMarkdown: (text = '', mentions = []) ->
+    parsePara = (para, callback) ->
+      new_para = for item in para
+        if _.isArray(item) && item[0] in ['para', 'strong', 'em', 'del']
+          parsePara(item, callback)
+        else if typeof item is 'string'
+          callback(item)
+        else
+          item
+      new_para
+
+    autoLinkPreprocessor = (jsonml) ->
+      return jsonml unless jsonml[0] is 'para'
+
+      jsonml = parsePara jsonml, (item) ->
+        urls = TentStatus.Helpers.extractUrlsWithIndices(String(item))
+        return item unless urls.length
+        new_item = []
+        for u in urls
+          before = item.slice(0, u.indices[0])
+          after = item.slice(u.indices[1], item.length)
+          link = ['link', { href: u.url }, TentStatus.Helpers.truncate(TentStatus.Helpers.formatUrlWithPath(item.slice(u.indices[0], u.indices[1])), TentStatus.config.URL_TRIM_LENGTH)]
+          if before || after
+            new_item.push(
+              'span',
+              before,
+              link
+              after
+            )
+          else
+            new_item.push(link...)
+        new_item
+
+      jsonml
+
     externalLinkPreprocessor = (jsonml) ->
       return jsonml unless jsonml[0] is 'link'
       return jsonml unless TentStatus.Helpers.isURLExternal(jsonml[1]?.href)
       jsonml[1]['data-view'] = 'ExternalLink'
       jsonml

-    markdown.toHTML(text, 'Tent', { footnotes: mentions, preprocessors: [externalLinkPreprocessor] })
+    markdown.toHTML(text, 'Tent', { footnotes: mentions, preprocessors: [autoLinkPreprocessor, externalLinkPreprocessor] })


Move util method to the top

Move util method to the top

--- lib/iridium/casperjs/integration_test_runner.coffee
+++ lib/iridium/casperjs/integration_test_runner.coffee
@@ -4,6 +4,15 @@
 f = utils.format
 includes = []
 tests = []
+
+formatBacktrace = (trace) ->
+  formatted = []
+
+  for e in trace
+    line = (!e.file ? "(casperjs)" : e.file) + ":" + e.line
+    line = "#{line} in #{e.function}" if e.function
+
+  formatted

 casper = require('casper').create({
   exitOnError: false
@@ -31,14 +40,6 @@
   @running = true; # this.running is set back to false with done()
   @exec(testFile)

-formatBacktrace = (trace) ->
-  formatted = []
-
-  for e in trace
-    line = (!e.file ? "(casperjs)" : e.file) + ":" + e.line
-    line = "#{line} in #{e.function}" if e.function
-
-  formatted

 # register listeners needed to capture events
 # to generate results to pass back to Iridium


Add beta plugin check

Add beta plugin check

--- lib/plugin-manager.coffee
+++ lib/plugin-manager.coffee
@@ -57,6 +57,7 @@
     @config_ =
       mirror: null
       proxy: null
+      betaCheck: null

     # @const
     @VALID = 0
@@ -93,18 +94,21 @@
     fs.readJsonAsync(@mirrorPath).then (@mirrors_) =>
       mirrorConf = config.get 'packageManager.mirrorName', if navigator.language is 'zh-CN' then  "taobao" else "npm"
       proxyConf = config.get "packageManager.proxy", false
-      @selectConfig(mirrorConf, proxyConf).then =>
+      betaCheck = config.get "packageManager.enableBetaPluginCheck", false
+      @selectConfig(mirrorConf, proxyConf, betaCheck).then =>
         @mirrors_

   # select a mirror and set proxy config
-  # @param {object, object} mirror name, is proxy enabled
+  # @param {object, object, object} mirror name, is proxy enabled, is check beta plugin
   # @return {Promise<Object>} return the npm config
-  selectConfig: (name, enable) ->
+  selectConfig: (name, enable, check) ->
     @getMirrors().then =>
       @config_.mirror = @mirrors_[name]
       config.set "packageManager.mirrorName", name
       @config_.proxy = enable
       config.set "packageManager.proxy", enable
+      @config_.betaCheck = check
+      config.set "packageManager.enableBetaPluginCheck", check
       new Promise (resolve) =>
         npmConfig =
           prefix: PLUGIN_PATH
@@ -119,10 +123,12 @@
   setUpdateInformation: ->
     @getMirrors().then =>
       @getInstalledPlugins().then =>
-        Promise.all(@plugins.map (plugin) =>
-          @getLastestVersionOfPlugin(plugin).then (version) ->
-            plugin.lastestVersion = version
-            plugin.isOutdated = semver.lt plugin.version, plugin.lastestVersion
+        Promise.all(
+          @plugins.map (plugin) =>
+            @getLastestVersionOfPlugin(plugin).then (distTag) ->
+              version = if @config_.betaCheck && distTag.beta? then distTag.beta else distTag.latest
+              plugin.lastestVersion = version
+              plugin.isOutdated = semver.lt plugin.version, plugin.lastestVersion
         )

   # get the current plugins
@@ -292,8 +298,8 @@
     # after getting mirrors, at least one mirror is set
     @getMirrors().then ->
       new Promise (resolve) ->
-        npm.commands.outdated [plugin.packageName], (err, data) ->
-          resolve data[4] or null
+        npm.commands.distTag ['ls', plugin.packageName], (err, data) ->
+          resolve data or null

   # update one plugin
   # @param {Plugin} plugin
@@ -302,7 +308,7 @@
     plugin.isUpdating = true
     @getMirrors().then ->
       new Promise (resolve) ->
-        npm.commands.update [plugin.packageName], ->
+        npm.commands.install ["#{plugins[index].packageName}@#{plugin.version}"], ->
         plugin.isUpdating = false
         resolve()


fixed extendCube

fixed extendCube

--- app/assets/javascripts/model.coffee
+++ app/assets/javascripts/model.coffee
@@ -409,9 +409,9 @@
 				for x in [0...cubeSize[0]]
 					cube.push if _cube?
 						index =
-							(x - _offset[0]) +
-							(y - _offset[1]) * _size[0] +
-							(z - _offset[2]) * _size[0] * _size[1]
+							(x + cubeOffset[0] - _offset[0]) +
+							(y + cubeOffset[1] - _offset[1]) * _size[0] +
+							(z + cubeOffset[2] - _offset[2]) * _size[0] * _size[1]
 						_cube[index]
 					else
 						null


Only use outdoor source.

Only use outdoor source.

--- app/assets/javascripts/nodes.coffee
+++ app/assets/javascripts/nodes.coffee
@@ -261,7 +261,12 @@

   streetViewService = new google.maps.StreetViewService

-  streetViewService.getPanorama { location: position }, (data, status) ->
+  streetViewLocationRequest =
+    location: position,
+    preference: google.maps.StreetViewPreference.NEAREST,
+    source: google.maps.StreetViewSource.OUTDOOR
+
+  streetViewService.getPanorama streetViewLocationRequest, (data, status) ->
     unless status == google.maps.StreetViewStatus.OK
       return


removed videos.coffee file

removed videos.coffee file

--- app/assets/javascripts/video.coffee
+++ app/assets/javascripts/video.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/


Fixing attachments on wiki edition

Fixing attachments on wiki edition

--- app/coffee/modules/wiki/main.coffee
+++ app/coffee/modules/wiki/main.coffee
@@ -64,15 +64,7 @@
             @scope.membersById = groupBy(project.memberships, (x) -> x.user)
             return project

-    loadWiki: ->
-        return @rs.wiki.get(@scope.wikiId).then (wiki) =>
-            @scope.wiki = wiki
-
-    loadWikiLinks: ->
-        return @rs.wiki.listLinks(@scope.projectId).then (wikiLinks) =>
-            @scope.wikiLinks = wikiLinks
-
-    loadInitialData: ->
+    loadWikiSlug: ->
         params = {
             pslug: @params.pslug
             wikipage: @params.slug
@@ -83,12 +75,32 @@
             @scope.projectId = data.project
             return data

-        promise.then null, =>
+          promise.then null, =>
             @location.path("/project/#{@params.pslug}/wiki/#{@params.slug}/edit")
+
+    loadWiki: ->
+        if @scope.wikiId
+            return @rs.wiki.get(@scope.wikiId).then (wiki) =>
+                @scope.wiki = wiki
+        else
+            return @scope.wiki = {
+                content: ""
+            }
+
+    loadWikiLinks: ->
+        return @rs.wiki.listLinks(@scope.projectId).then (wikiLinks) =>
+            @scope.wikiLinks = wikiLinks
+
+    loadInitialData: ->
+        # Resolve project slug
+        promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
+            @scope.projectId = data.project
+            return data

         return promise.then(=> @.loadProject())
                       .then(=> @.loadUsersAndRoles())
                       .then(=> @.loadWikiLinks())
+                      .then(=> @.loadWikiSlug())
                       .then(=> @.loadWiki())
                       .then(=> @.loadAttachments(@scope.wikiId))

@@ -120,43 +132,6 @@
 #############################################################################

 class WikiEditController extends WikiDetailController
-    loadInitialData: ->
-        deferred = @q.defer()
-        params = {
-            pslug: @params.pslug
-        }
-
-        promise = @repo.resolve(params)
-
-        promise.then (data) =>
-            @scope.wikiId = data.wikipage
-            @scope.projectId = data.project
-            return data
-
-        promise.then(=> @.loadProject())
-               .then(=> @.loadUsersAndRoles())
-               .then(=> @.loadWikiLinks())
-               .then(=> @.loadAttachments(@scope.wikiId))
-
-        params = {
-            pslug: @params.pslug
-            wikipage: @params.slug
-        }
-
-        promise2 = @repo.resolve(params)
-
-        promise2.then (data) =>
-            @scope.wikiId = data.wikipage
-            return data
-
-        promise2.then => @.loadWiki()
-        promise2.then null, =>
-           @scope.wiki = {
-               content: ""
-           }
-
-        return @q.all(promise, promise2)
-
     save: ->
         onSuccess = =>
             @confirm.notify("success")


Fix server busy problem after unexpected exceptions on result callback

--- lib/server-process.coffee
+++ lib/server-process.coffee
@@ -31,7 +31,11 @@
       @ready = true
       if ~chunk.indexOf("END-OF-#{@lastRequestType}")
         [before, after] = chunk.toString().split("END-OF-#{@lastRequestType}")
-        @onResult((buffer + before).trim())
+        try
+          @onResult((buffer + before).trim())
+        catch e
+          console.error(e)
+
         @busy = false
         if after
           buffer = after


added icon rotation to vehicle markers

added icon rotation to vehicle markers

--- lib/vehicle_marker.coffee
+++ lib/vehicle_marker.coffee
@@ -23,11 +23,14 @@
   init: ->
     @set('movingIcon', new SM.VehicleMovingIcon())
     @set('stoppedIcon', new SM.VehicleStoppedIcon())
+    # TODO: Leaflet won't let allow setIcon to be used on a marker object until it has
+    # been added to the map, hence I need to set all vehicles to display the stoppedIcon
+    # to begin with
     @mapObject = new L.Marker(new L.LatLng(@get('lat'),@get('lon')), icon: @get('stoppedIcon'))
   bounds: ->
     latlng = @get('mapObject')._latlng
     new L.LatLngBounds(latlng,latlng)
-  stateDidChange: ( ->
+  _stateDidChange: ( ->
     state = @getPath('vehicle.state')
     @set('icon', @get("#{state}Icon"))
   ).observes('vehicle.state')
@@ -36,9 +39,15 @@
     mapObject = @get('mapObject')
     mapObject.setLatLng(new L.LatLng(@getPath('vehicle.lat'),@getPath('vehicle.lon')))
   ).observes('vehicle.lat')
-  iconDidChange: ( ->
+  _iconDidChange: ( ->
     mapObject = @get('mapObject')
     if mapObject
       mapObject.setIcon @get('icon')
       mapObject._reset() # A reset is needed to redraw the layer
   ).observes('icon')
+  _rotationDidChange: ( ->
+    $img = $(@get('mapObject')._icon)
+    rotation = @getPath('vehicle.heading')
+    webkitTransform = $img.css('-webkit-transform')
+    $img.css('-webkit-transform', webkitTransform + " rotate(#{rotation}deg)")
+  ).observes('vehicle.heading')


fix(draw-route): fix support for pathname in sharable link

GitHub Pages hosts all websites under a namespace, a pathname.
For instance, `seasponge` in
http://mozilla.github.io/seasponge/

See #41

--- app/scripts/controllers/draw.coffee
+++ app/scripts/controllers/draw.coffee
@@ -98,7 +98,7 @@
         str = JSON.stringify(serialized)
         data = encodeURIComponent(str)
         # console.log(serialized, data)
-        link = "#{location.origin}/#/load?model=#{data}"
+        link = "#{location.origin}#{location.pathname}#/load?model=#{data}"
         $scope.shareLink = link

     # Drag'n'Drop Stencil support


Add success/failure management in PairingRequest

--- app/src/m2fa/pairing_request.coffee
+++ app/src/m2fa/pairing_request.coffee
@@ -12,6 +12,7 @@
     WAITING: 0
     CHALLENGING: 1
     FINISHING: 2
+    DEAD: 3

   @Errors:
     InconsistentState: "Inconsistent state"
@@ -55,5 +56,9 @@
     @emit 'cancel'

   _failure: (reason) ->
+    @_currentState = ledger.m2fa.PairingRequest.States.DEAD
+    @_onComplete.fail(reason)

-  _success: () ->+  _success: () ->
+    @_currentState = ledger.m2fa.PairingRequest.States.DEAD
+    @_onComplete.success()


Separate git ahead and behind sections.

Separate git ahead and behind sections.

--- prompt.coffee
+++ prompt.coffee
@@ -52,17 +52,21 @@
     foreground: 'green'
     background: 'black'

-  section 'git:aheadBehind',
-    content: [git.ahead, git.behind]
+  section 'git:ahead',
+    content: git.ahead
+    background: 'green'
+    foreground: 'white'
+    when: git.isRepo
+    format: (ahead) ->
+      "#{ahead} ahead" if ahead
+
+  section 'git:behind',
+    content: git.behind
     background: 'red'
     foreground: 'white'
     when: git.isRepo
-    format: (ahead, behind) ->
-      outputs = []
-      outputs.push "#{ahead} ahead" if ahead
-      outputs.push "#{behind} behind" if behind
-
-      outputs.join ' & '
+    format: (behind) ->
+      "#{behind} behind" if behind

   section 'end',
     content: '\n$'


Display functions are in right quadrant, but tests fail (python funcs the problem)

--- attitude/display/assets/math.coffee
+++ attitude/display/assets/math.coffee
@@ -32,8 +32,8 @@
   c1 = if axes[2][2] > 0 then 1 else -1

   stepFunc = (angle)->
-    e = [Math.cos(angle)*s[0],
-         Math.sin(angle)*s[1]]
+    e = [Math.sin(angle)*s[0],
+         Math.cos(angle)*s[1]]

     if sheet == 'upper'
       e[2] = s[2]*c1


Make tests run with promise and non-promise package deactivate

--- spec/image-editor-spec.coffee
+++ spec/image-editor-spec.coffee
@@ -25,8 +25,11 @@
       runs ->
         expect(atom.workspace.getActivePaneItem().getTitle()).toBe 'binary-file.png'
         atom.workspace.destroyActivePaneItem()
-        atom.packages.deactivatePackage('image-view')

+      waitsFor ->
+        Promise.resolve(atom.packages.deactivatePackage('image-view'))
+
+      runs ->
         atom.workspace.open(path.join(__dirname, 'fixtures', 'binary-file.png'))

       waitsFor ->


Move the poller into the lib and mix it with the other classmixer stuffs. Update the test.

--- dashboard_spec.coffee
+++ dashboard_spec.coffee
@@ -1,10 +1,10 @@
-#= require dashboard
+#= require lib/dashboard_poller
 #= require mock-ajax

 describe "Dashboard - getData()", ->
   beforeEach ->
     jasmine.Ajax.useMock()
-    @dashboard = Dashboard
+    @dashboard = ttm.lib.DashboardPoller.build()

   describe "when the report is ready on the first iteration", ->
     it "should return the dashboard data", ->


Fix not building with some webpack configurations.

--- engine/texture.coffee
+++ engine/texture.coffee
@@ -96,8 +96,8 @@
                         type = 'image/jpeg'
                     f = data.file_name+(new Error).stack.split('\n')[1]
                     # TODO Move out of here
-                    if AndroidReadFile? and not data.data_uri and base[0] != '/'
-                        data.data_uri = Buffer.from(AndroidReadFile(base + data.file_name))
+                    if readFileForGetPixels? and not data.data_uri and base[0] != '/'
+                        data.data_uri = readFileForGetPixels(base + data.file_name)
                     # get-pixels expects a file uri, an url, a data uri or a node buffer
                     getPixels data.data_uri or (base + data.file_name), type, (err, pixels) =>
                         if err?


Workaround for users type parsing

Workaround for users type parsing

--- src/hubot/talker.coffee
+++ src/hubot/talker.coffee
@@ -64,15 +64,19 @@

     @socket = tls.connect @port, @domain, ->
       # callback called only after successful socket socket
-      console.log "Connected to " + @domain
+      console.log "Connected to " + self.domain
       self.socket.setEncoding @encoding
       callback()

     #callback
     @socket.addListener 'data', (data) ->
       console.log data
-      message = JSON.parse(data)
-
+
+      if data.indexOf('{"type":"users"') == 0
+        message = JSON.parse(data)
+      else
+        message = {"type": "none"}
+
       if message.type == "connected"
         console.log "Succesfully connected, listing users:"
       if message.type == "message"
@@ -81,6 +85,8 @@
         self.emit "EnterMessage", message
       if message.type == "leave"
         self.emit "LeaveMessage", message
+      if message.type == "error"
+        self.disconnect message.message

     @socket.addListener "eof", ->
       console.log "eof"


only allow parameters that represent an attribute for where conditions

--- src/controllers/mixins/resource.coffee
+++ src/controllers/mixins/resource.coffee
@@ -133,6 +133,10 @@
           Some parameter names are reserved and have a special meaning
       ###
       reservedParams = ["sort","include","includes","limit","offset","method","controller","action","format"]
+
+      # only accept parameters that represent an attribute for where conditions
+      allowedWhereAttributes = []
+      allowedWhereAttributes.push key for key,value of App[@resourceOptions.resourceClass].metadata().attributes

       conditions = {}

@@ -168,7 +172,8 @@
                   conditions.includes = []
               conditions.includes.push value
           continue
-
+        if key not in allowedWhereAttributes
+          continue
         # all other parameter names are treated as where conditions
         unless conditions.where?
           conditions.where = {}


webapp/assistant: no immutable js for merging dictionaries

--- src/smc-webapp/assistant/header.cjsx
+++ src/smc-webapp/assistant/header.cjsx
@@ -36,13 +36,14 @@
     flexWrap       : 'nowrap'
     justifyContent : 'space-between'

-inner_style = immutable.Map
+inner_style =
     marginLeft  : '10px'
     marginRight : '10px'

-title_style  = inner_style.merge({flex: '0 0 auto'}).toJS()
-nav_style    = inner_style.merge({flex: '1 0 auto'}).toJS()
-search_style = inner_style.merge({flex: '0 1 auto', marginRight: '50px'}).toJS()
+# this is similar to Object.assign, but compatible with IE
+title_style  = _.defaults({flex: '0 0 auto'}, inner_style)
+nav_style    = _.defaults({flex: '1 0 auto'}, inner_style)
+search_style = _.defaults({flex: '0 1 auto', marginRight: '50px'}, inner_style)

 exports.ExamplesHeader = rclass
     displayName : 'ExamplesHeader'


Revert "get rid of cookie entirely; also add back hooking up data handler..."

This reverts commit 904193f5495794bbd9534e6f340c54d2454dbd92.

--- src/smc-webapp/client_browser.coffee
+++ src/smc-webapp/client_browser.coffee
@@ -246,6 +246,8 @@
                 factor  : 1.3
                 retries : 100000

+        misc_page.delete_cookie('SMCSERVERID3')
+        @_delete_websocket_cookie()
         conn = new Primus(url, opts)

         @_conn = conn
@@ -258,7 +260,6 @@
             @_num_attempts = 0

             conn.removeAllListeners('data')
-            conn.on("data", ondata)

             if auth_token?
                 @sign_in_using_auth_token
@@ -299,6 +300,7 @@
             @_num_attempts = opts.attempt
             @emit("disconnected", "close") # This just informs everybody that we *are* disconnected.
             conn.removeAllListeners('data')
+            @_delete_websocket_cookie()
             log("reconnect scheduled (attempt #{opts.attempt} out of #{opts.retries})")

         conn.on 'reconnect', =>
@@ -327,8 +329,13 @@
         if @_connected
             return @_conn.latency

+    _delete_websocket_cookie: =>
+        console.log('websocket -- delete cookie')
+        misc_page.delete_cookie('SMCSERVERID3')
+
     _fix_connection: =>
         console.log("websocket --_fix_connection... ")
+        @_delete_websocket_cookie()
         @_conn.end()
         @_conn.open()


Fix old course_project_id usage

Fix old course_project_id usage

--- src/smc-webapp/course/actions.coffee
+++ src/smc-webapp/course/actions.coffee
@@ -529,7 +529,7 @@
         else if not users?.get(student_account_id)?   # users might not be set yet if project *just* created
             invite(student_account_id)
         # Make sure all collaborators on course project are on the student's project:
-        target_users = @redux.getStore('projects').get_users(s.course_project_id)
+        target_users = @redux.getStore('projects').get_users(s.get('course_project_id'))
         if not target_users?
             return  # projects store isn't sufficiently initialized, so we can't do this yet...
         target_users.map (_, account_id) =>


set first two columns to be fixed

set first two columns to be fixed

--- src/components/performance/index.cjsx
+++ src/components/performance/index.cjsx
@@ -79,10 +79,6 @@
       classes = @state.sortOrder
     else
       classes = ''
-    if heading.title is 'Student'
-      fixed = true
-    else
-      fixed = false
     if i is @state.colResizeKey
       @state.colSetWidth = @state.colResizeWidth
     else
@@ -123,18 +119,18 @@
         className={'header-cell ' + classes}>
           {heading.title}
       </div>
-
+    isFixed = i < 2
     <ColumnGroup
       key={i}
       groupHeaderRenderer={-> customGroupHeader}
-      fixed={fixed}>
+      fixed={isFixed}>
       <Column
         label={heading.title}
         headerRenderer={-> customHeader}
         cellRenderer={-> @cellData}
         width={@state.colSetWidth}
         flexGrow={1}
-        fixed={fixed}
+        fixed={isFixed}
         isResizable=false
         dataKey={i} />
     </ColumnGroup>


fixed add readings button logic

fixed add readings button logic

--- src/components/task-plan/reading.cjsx
+++ src/components/task-plan/reading.cjsx
@@ -137,7 +137,7 @@

     if @state?.invalid then formClasses.push('is-invalid-form')

-    if not TaskPlanStore.isVisibleToStudents()
+    if not TaskPlanStore.isVisibleToStudents(id)
       addReadingsButton = <BS.Button id='reading-select'
         onClick={@showSectionTopics}
         bsStyle='default'>+ {addReadingText}


minor comment change

minor comment change

--- src/compiler.coffee
+++ src/compiler.coffee
@@ -141,7 +141,7 @@
   #
   # Example = yang
   #  .load('module example { leaf test { type string; } }')
-  #   .resolve('module','example')
+  #  .resolve('example')
   #
   # ex = new Example test: 'hi'
   # console.log(ex.get());


Fix VSM shadow bias conversion.

Fix VSM shadow bias conversion.

--- engine/loader.coffee
+++ engine/loader.coffee
@@ -339,7 +339,9 @@
                     frustum_size: data.frustum_size
                     clip_start: data.clip_start
                     clip_end: data.clip_end
-                    bias: data.shadow_bias
+                    # arbitrary conversions performed in blender code
+                    # (gpu_lamp_from_blender() in gpu_material.c)
+                    bias: data.shadow_bias * 0.02 * 0.25
                     bleed_bias: data.bleed_bias
                 if context.render_manager.enable_shadows
                     ob.init_shadow()


Layer.on error on missing events

Layer.on error on missing events

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -956,10 +956,13 @@
 		@_addListener(eventName, listener)

 	addListener: (eventName, listener) =>
+		throw Error("Layer.on needs an event name") unless eventName
+		throw Error("Layer.on needs an event listener") unless listener
 		super(eventName, listener)
 		@_addListener(eventName, listener)

 	removeListener: (eventName, listener) ->
+		throw Error("Layer.off needs an event name") unless eventName
 		super(eventName, listener)
 		@_removeListener(eventName, listener)


Added irregular zombie inflection

Added irregular zombie inflection

see https://github.com/rails/rails/pull/2457

--- src/inflect/default_inflections.coffee
+++ src/inflect/default_inflections.coffee
@@ -68,6 +68,7 @@
     inflect.irregular('theirs', 'theirs')
     inflect.irregular('sex', 'sexes')
     inflect.irregular('cow', 'kine')
+    inflect.irregular('zombie', 'zombies')

     inflect.uncountable('advice', 'energy', 'excretion', 'digestion', 'cooperation', 'health', 'justice', 'jeans')
     inflect.uncountable('labour', 'machinery', 'equipment', 'information', 'pollution', 'sewage', 'paper', 'money')


Link inline JS

Link inline JS

--- etc/atom/config.cson
+++ etc/atom/config.cson
@@ -34,7 +34,8 @@
   linter:
     lintOnChange: false
     showErrorPanel: false
-  "linter-jshint": {}
+  "linter-jshint":
+    lintInlineJavaScript: true
   "symbols-tree-view":
     autoToggle: true
   tabs:


fix(HomeCtrl): throttle `$scope.$apply()` for both upload and download


DateTime

DateTime

- in "DateTime.display": use all known formats to parse date.

--- src/elements/DateTime/DateTime.coffee
+++ src/elements/DateTime/DateTime.coffee
@@ -1269,7 +1269,7 @@
 			opts.input_types = null

 		dt = new DateTime(opts)
-		mom = dt.parse(datestr_or_moment)
+		mom = dt.parseValue(datestr_or_moment)

 		if not mom.isValid()
 			return null


ItemList: Right and left buttons work like 'down' and 'up' buttons to preselect the items. See #50925

--- src/elements/ItemList/ItemList.coffee
+++ src/elements/ItemList/ItemList.coffee
@@ -311,6 +311,14 @@
 					when "Return"
 						@__activatePreSelected(event)
 						break
+					when "Right"
+						event.preventDefault()
+						@__preActivateNextButton()
+						break
+					when "Left"
+						event.preventDefault()
+						@__preActivatePreviousButton()
+						break
 					else
 						if keyboardKey
 							@__keyboardKeys.push(keyboardKey)


Fix weirdness when clicking on play multiple times on empty sidebar

--- app/scripts/app.coffee
+++ app/scripts/app.coffee
@@ -49,7 +49,8 @@
     else
       $body = $ "body"
       $body.addClass "play-empty"
-      setTimeout (-> $body.removeClass "play-empty"), 1000
+      clearTimeout(@playEmptyTimeout) if @playEmptyTimeout
+      @playEmptyTimeout = setTimeout (-> $body.removeClass "play-empty"), 1000

   playAnimation: (options = {}) =>
     bounce = options.bounceObject or @preferences.getBounceObject()


Fix object picker img dataURL when cropping is disabled

When cropping is disabled in the object picker image, the image returned
to not take care of natural image dimensions (because image is masked in
the DOM). This patch forces the transition canvas to get the natural
dimensions of the image instead of DOM ones.

--- client/app/views/object-picker.coffee
+++ client/app/views/object-picker.coffee
@@ -209,8 +209,8 @@
             ctx.drawImage( img, d.sx, d.sy, d.sWidth,
                            d.sHeight, 0, 0, IMAGE_DIMENSION, IMAGE_DIMENSION)
         else
-            canvas.width  = img.width
-            canvas.height = img.height
+            canvas.width  = img.naturalWidth
+            canvas.height = img.naturalHeight
             ctx.drawImage img, 0, 0
         return dataUrl =  canvas.toDataURL 'image/jpeg'


add isPromise method to utils

add isPromise method to utils

--- client/lanes/lib/utilFunctions.coffee
+++ client/lanes/lib/utilFunctions.coffee
@@ -152,5 +152,6 @@
                 last
         else
             ''
-
+    isPromise: (obj) ->
+        !!obj && (_.isObject(obj) || _.isFunction(obj)) && _.isFunction(obj.then)
 })


copyFrom and sum methods

copyFrom and sum methods

--- client/lanes/models/Collection.coffee
+++ client/lanes/models/Collection.coffee
@@ -9,6 +9,11 @@
                     break
             break if found != -1
         found
+
+    sum: ->
+        args = _.toArray(arguments)
+        args.unshift(this.models)
+        return _.sum.apply(_, args)

     # convenience method to create a new subCollection
     subcollection: (options) ->
@@ -25,6 +30,11 @@
             )
             return this
         )
+
+    # replace these models from other collection
+    copyFrom: (collection) ->
+        this.set(collection)
+

 }

@@ -54,8 +64,12 @@
         else
             this.sync('read', this, options)

+    getOrFetch: (id, options) ->
+        @get(id) || @fetchId(id, options)
+
     # Fetch a single model with the given id
     fetchId: (id, options = {}) ->
+        return if id == @requestInProgress?.query?[@model::idAttribute]
         this.sync('read', this, _.extend({
             limit: 1, query: {"#{@model::idAttribute}": id}
         }, options))
@@ -163,17 +177,17 @@
 ## Override a few methods on the standard collection to ensure that
 # models are fetched correctly and have the fk set when they're created
 class Lanes.Models.AssociationCollection extends Lanes.Models.Collection
-    constructor: (models, options) ->
-        @model = options.model
-        @associationFilter = options.filter
+    constructor: (models, @options) ->
+        @model = @options.model
+        @associationFilter = @options.filter
         super

     _prepareModel: (attrs, options = {}) ->
-        model = super
-        model.set(@associationFilter)
-        model
+        _.extend(attrs, @associationFilter)
+        super

     fetch: (options) ->
         options.query ||= {}
         _.extend(options.query, @associationFilter)
+        _.extend(options, @options)
         super(options)


Database version incremented for last_message_received_timestamp

--- src/flux/stores/database-store.coffee
+++ src/flux/stores/database-store.coffee
@@ -14,7 +14,7 @@
  generateTempId,
  isTempId} = require '../models/utils'

-DatabaseVersion = 4
+DatabaseVersion = 5

 ###
 Public: Nylas Mail is built on top of a custom database layer modeled after


Modal: Added exposed getter methods.

Modal: Added exposed getter methods.

--- src/lib/components/modal/Modal.coffee
+++ src/lib/components/modal/Modal.coffee
@@ -136,6 +136,47 @@
     positionTop  = Math.max viewportSize.height / 2 - modalSize.height / 2, 0

     goog.style.setPosition modalElement, positionLeft, positionTop
+
+
+  ###*
+    Returns Overlay instance if exists.
+
+    @export
+    @return {spark.components.Overlay|null}
+  ###
+  getOverlay: ->
+    return @overlay or null
+
+
+  ###*
+    Returns button instance by its title.
+
+    @export
+    @param {!string} title Button title.
+    @return {spark.components.Button|null}
+  ###
+  getButtonByTitle: (title) ->
+    return @buttons[title] or null
+
+
+  ###*
+    Returns title view.
+
+    @export
+    @return {spark.core.View}
+  ###
+  getTitleView: ->
+    return @titleView
+
+
+  ###*
+    Returns content view.
+
+    @export
+    @return {spark.core.View}
+  ###
+  getContentView: ->
+    return @contentView

   ###*


Set @components only after successful read

--- src/lib/nodejs/ComponentLoader.coffee
+++ src/lib/nodejs/ComponentLoader.coffee
@@ -76,7 +76,6 @@
     @processing = true

     manifestOptions = @prepareManifestOptions()
-    @components = {}

     if @options.cache and not @failedCache
       @readCache manifestOptions, (err, modules) =>
@@ -85,6 +84,7 @@
           @processing = false
           @listComponents callback
           return
+        @components = {}
         @readModules modules, (err) =>
           @ready = true
           @processing = false
@@ -92,6 +92,7 @@
           callback @components if callback
       return

+    @components = {}
     manifest.list.list @baseDir, manifestOptions, (err, modules) =>
       @readModules modules, (err) =>
         @ready = true


governor: Keep past value of workers

governor: Keep past value of workers

--- src/governor.coffee
+++ src/governor.coffee
@@ -32,6 +32,7 @@
     else
       history = extractHistory window, name, 'estimated_workers'
       currentWorkers = extractHistory(window, name, 'current_workers')[history.length-1]
+      s.previous_workers = currentWorkers
       workers = scale.scaleWithHistory role, name, history, currentWorkers, s.current_jobs
       s.estimated_workers = workers.estimate
       if workers.next?


Modifying redis brain lookup

Modifying redis brain lookup

--- src/incident.coffee
+++ src/incident.coffee
@@ -30,14 +30,14 @@
   robot.respond /incident (start|initiate)/i, (msg) ->
     # Probably set to the brain via the notify portion of the bot in non-hacky version
     robot.brain.set 'incidentNumber', 8675309
-    robot.brain.get('incidentNumber')
+    incidentNumber = robot.brain.get('incidentNumber')
     msg.send "INCIDENT NOTIFY: Incident #{incidentNumber} has been started"

   robot.respond /incident (ack|acknowledge)/i, (msg) ->
-    robot.brain.get('incidentNumber')
+    incidentNumber = robot.brain.get('incidentNumber')
     msg.send "INCIDENT NOTIFY: Acknowledged incident #{incidentNumber}"

   robot.respond /incident (end|resolve)/i, (msg) ->
-    robot.brain.get('incidentNumber')
+    incidentNumber = robot.brain.get('incidentNumber')
     msg.send "INCIDENT NOTIFY: Resolved incident #{incidentNumber}"


:lipstick: Use :: instead of .prototype.

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -189,7 +189,7 @@
       atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'

       waitsFor ->
-        SymbolsView.prototype.moveToPosition.callCount == 1
+        SymbolsView::moveToPosition.callCount is 1

       runs ->
         expect(editor.getCursorBufferPosition()).toEqual [2,0]
@@ -211,7 +211,7 @@
         symbolsView.confirmed(symbolsView.items[0])

       waitsFor ->
-        SymbolsView.prototype.moveToPosition.callCount == 1
+        SymbolsView::moveToPosition.callCount is 1

       runs ->
         expect(atom.workspaceView.getActivePaneItem().getPath()).toBe atom.project.resolve("tagged-duplicate.js")
@@ -233,14 +233,14 @@
         atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'

         waitsFor ->
-          SymbolsView.prototype.moveToPosition.callCount == 1
+          SymbolsView::moveToPosition.callCount is 1

         runs ->
           expect(editor.getCursorBufferPosition()).toEqual [2,0]
           atom.workspaceView.getActiveView().trigger 'symbols-view:return-from-declaration'

         waitsFor ->
-          SymbolsView.prototype.moveToPosition.callCount == 2
+          SymbolsView::moveToPosition.callCount is 2

         runs ->
           expect(editor.getCursorBufferPosition()).toEqual [6,24]


Update pudgewars version

Update pudgewars version


spec(container): added export tests for container

--- spec_src/containerSpec.coffee
+++ spec_src/containerSpec.coffee
@@ -32,9 +32,22 @@
   it 'has single container', ->
     expect(typeof @post.PostData).toBe 'function'

+  it 'has first class objects from container', ->
+    expect(typeof @post.title).toBe 'function'
+    expect(typeof @post.content).toBe 'function'
+
+    @post.title 'Post Title'
+    @post.content 'Post Content'
+
   it 'can add comments', ->
     expect(typeof @post.comments_add).toBe 'function'

     @post.comments_add
       title:  'Test Title'
-      conent: 'Test Content'
+      content: 'Test Content'
+
+  it 'exports containers', ->
+    data = @post.export()
+
+    expect(data).toEqual jasmine.objectContaining({ PostData: { title: 'Post Title', content: 'Post Content' } })
+    expect(data).toEqual jasmine.objectContaining({ comments: [ {title: 'Test Title', content: 'Test Content'} ] })


changed email to use unix mail client.

changed email to use unix mail client.

--- src/scripts/email.coffee
+++ src/scripts/email.coffee
@@ -14,19 +14,19 @@
 #   earlonrails
 #
 # Additional Requirements
-#   Mutt email installed on the system
+#   unix mail client installed on the system

-sys = require 'sys'
+util = require 'util'
 child_process = require 'child_process'
 exec = child_process.exec

 module.exports = (robot) ->
   emailTime = null
   sendEmail = (addresses, subject, msg) ->
-    muttCommand = """echo '#{msg}' | mutt -s '#{subject}' -- #{addresses}"""
-    exec muttCommand, (error, stdout, stderr) ->
-      sys.print 'stdout: ' + stdout
-      sys.print 'stderr: ' + stderr
+    mailCommand = """echo '#{msg}' | mail -s '#{subject}' '#{addresses}'"""
+    exec mailCommand, (error, stdout, stderr) ->
+      util.print 'stdout: ' + stdout
+      util.print 'stderr: ' + stderr

-  robot.respond /email (^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$) -s (.*) -m (.*)/i, (msg) ->
-    sendEmail msg.match[0], msg.match[1], msg.match[2]
+  robot.respond /email (.*) -s (.*) -m (.*)/i, (msg) ->
+    sendEmail msg.match[1], msg.match[2], msg.match[3]


Escape dot character in Mongo docs as *DOT*

--- models.coffee
+++ models.coffee
@@ -61,7 +61,21 @@

     @fromDoc: (doc) ->
-        fields = EJSON.fromJSONValue doc
+        unescapeDot = (key) ->
+            key.replace /\*DOT\*/g, '.'
+
+        getUnescaped = (subDoc) ->
+            if J.util.isPlainObject subDoc
+                ret = {}
+                for key, value of subDoc
+                    ret[unescapeDot key] = getUnescaped value
+                ret
+            else if _.isArray subDoc
+                getUnescaped x for x in subDoc
+            else
+                subDoc
+
+        fields = EJSON.fromJSONValue getUnescaped doc

         for fieldName of @fieldSpecs
             if fieldName not of fields
@@ -196,13 +210,16 @@
         unless @alive
             throw new Meteor.Error "Can't call toDoc on dead #{@modelClass.name} instance"

+        escapeDot = (key) ->
+            key.replace /\./g, '*DOT*'
+
         toPrimitiveEjsonObj = (value) =>
             if _.isArray(value)
                 (toPrimitiveEjsonObj v for v in value)
             else if J.util.isPlainObject(value)
                 ret = {}
                 for k, v of value
-                    ret[k] = toPrimitiveEjsonObj v
+                    ret[escapeDot k] = toPrimitiveEjsonObj v
                 ret
             else if value instanceof J.Model
                 value.toDoc denormalize


remove

remove

--- src/CanvasGradientMove.coffee
+++ src/CanvasGradientMove.coffee
@@ -230,8 +230,8 @@

     #mask描画
     if @options.maskImg
-      @ctx.globalCompositeOperation = 'xor';
-      @ctx.drawImage(@options.maskImg,0,0,canvasWidth,canvasHeight);
+      @ctx.globalCompositeOperation = 'xor'
+      @ctx.drawImage(@options.maskImg,0,0,canvasWidth,canvasHeight)

   #canvasの初期化
   canvasClear: =>


bug fix

bug fix

--- src/plugins/request-portals.coffee
+++ src/plugins/request-portals.coffee
@@ -280,11 +280,8 @@

                     async.each t.tile.gameEntities, (entity, callback) ->

-                        if t.tile.deletedGameEntityGuids.indexOf(entity[0]) is -1
-                            Entity.add entity[0], entity[1], entity[2], (type) ->
-                                Tile.data[id].portals++ if type is 'portal'
-                                callback()
-                        else
+                        Entity.add entity[0], entity[1], entity[2], (type) ->
+                            Tile.data[id].portals++ if type is 'portal'
                             callback()

                     , (err) ->


Follow mixin

Follow mixin

--- test/follow.coffee
+++ test/follow.coffee
@@ -17,6 +17,6 @@

   enemy.follow(player)

-  ok enemy.I.velocity,
+  ok enemy.I.velocity.equa

 module()


Don't let multiple pace-done classes get appended.  Closes #97.

--- pace.coffee
+++ pace.coffee
@@ -200,7 +200,7 @@
       @el = document.createElement 'div'
       @el.className = "pace pace-active"

-      document.body.className = document.body.className.replace 'pace-done', ''
+      document.body.className = document.body.className.replace /pace-done/g, ''
       document.body.className += ' pace-running'

       @el.innerHTML = '''