More test assertions.

More test assertions.

--- tests.coffee
+++ tests.coffee
@@ -183,6 +183,7 @@
   test.equal Post.Meta.fields.subscribers.sourceDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.subscribers.targetDocument.Meta.collection, Persons
   test.equal Post.Meta.fields.subscribers.fields, []
+  test.instanceOf Post.Meta.fields.reviewers, Person._ReferenceField
   test.equal Post.Meta.fields.reviewers.ancestorArray, 'reviewers'
   test.isTrue Post.Meta.fields.reviewers.required
   test.equal Post.Meta.fields.reviewers.sourcePath, 'reviewers'
@@ -194,6 +195,7 @@
   test.equal Post.Meta.fields.reviewers.targetDocument.Meta.collection, Persons
   test.equal Post.Meta.fields.reviewers.fields, [username: 1]
   test.equal _.size(Post.Meta.fields.subdocument), 3
+  test.instanceOf Post.Meta.fields.subdocument.person, Person._ReferenceField
   test.isNull Post.Meta.fields.subdocument.person.ancestorArray, Post.Meta.fields.subdocument.person.ancestorArray
   test.isFalse Post.Meta.fields.subdocument.person.required
   test.equal Post.Meta.fields.subdocument.person.sourcePath, 'subdocument.person'
@@ -204,6 +206,7 @@
   test.equal Post.Meta.fields.subdocument.person.sourceDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.subdocument.person.targetDocument.Meta.collection, Persons
   test.equal Post.Meta.fields.subdocument.person.fields, ['username']
+  test.instanceOf Post.Meta.fields.subdocument.persons, Person._ReferenceField
   test.equal Post.Meta.fields.subdocument.persons.ancestorArray, 'subdocument.persons'
   test.isTrue Post.Meta.fields.subdocument.persons.required
   test.equal Post.Meta.fields.subdocument.persons.sourcePath, 'subdocument.persons'
@@ -214,7 +217,19 @@
   test.equal Post.Meta.fields.subdocument.persons.sourceDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.subdocument.persons.targetDocument.Meta.collection, Persons
   test.equal Post.Meta.fields.subdocument.persons.fields, ['username']
+  test.instanceOf Post.Meta.fields.subdocument.slug, Person._GeneratedField
+  test.isNull Post.Meta.fields.subdocument.slug.ancestorArray, Post.Meta.fields.subdocument.slug.ancestorArray
+  test.isTrue _.isFunction Post.Meta.fields.subdocument.slug.generator
+  test.equal Post.Meta.fields.subdocument.slug.sourcePath, 'subdocument.slug'
+  test.equal Post.Meta.fields.subdocument.slug.sourceDocument, Post
+  test.equal Post.Meta.fields.subdocument.slug.targetDocument, Post
+  test.equal Post.Meta.fields.subdocument.slug.sourceCollection, Posts
+  test.equal Post.Meta.fields.subdocument.slug.targetCollection, Posts
+  test.equal Post.Meta.fields.subdocument.slug.sourceDocument.Meta.collection, Posts
+  test.equal Post.Meta.fields.subdocument.slug.targetDocument.Meta.collection, Posts
+  test.equal Post.Meta.fields.subdocument.slug.fields, ['body', 'subdocument.body']
   test.equal _.size(Post.Meta.fields.nested), 3
+  test.instanceOf Post.Meta.fields.nested.required, Person._ReferenceField
   test.equal Post.Meta.fields.nested.required.ancestorArray, 'nested'
   test.isTrue Post.Meta.fields.nested.required.required
   test.equal Post.Meta.fields.nested.required.sourcePath, 'nested.required'
@@ -225,6 +240,7 @@
   test.equal Post.Meta.fields.nested.required.sourceDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.nested.required.targetDocument.Meta.collection, Persons
   test.equal Post.Meta.fields.nested.required.fields, ['username']
+  test.instanceOf Post.Meta.fields.nested.optional, Person._ReferenceField
   test.equal Post.Meta.fields.nested.optional.ancestorArray, 'nested'
   test.isFalse Post.Meta.fields.nested.optional.required
   test.equal Post.Meta.fields.nested.optional.sourcePath, 'nested.optional'
@@ -235,6 +251,18 @@
   test.equal Post.Meta.fields.nested.optional.sourceDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.nested.optional.targetDocument.Meta.collection, Persons
   test.equal Post.Meta.fields.nested.optional.fields, ['username']
+  test.instanceOf Post.Meta.fields.nested.slug, Person._GeneratedField
+  test.equal Post.Meta.fields.nested.slug.ancestorArray, 'nested'
+  test.isTrue _.isFunction Post.Meta.fields.nested.slug.generator
+  test.equal Post.Meta.fields.nested.slug.sourcePath, 'nested.slug'
+  test.equal Post.Meta.fields.nested.slug.sourceDocument, Post
+  test.equal Post.Meta.fields.nested.slug.targetDocument, Post
+  test.equal Post.Meta.fields.nested.slug.sourceCollection, Posts
+  test.equal Post.Meta.fields.nested.slug.targetCollection, Posts
+  test.equal Post.Meta.fields.nested.slug.sourceDocument.Meta.collection, Posts
+  test.equal Post.Meta.fields.nested.slug.targetDocument.Meta.collection, Posts
+  test.equal Post.Meta.fields.nested.slug.fields, ['body', 'nested.body']
+  test.instanceOf Post.Meta.fields.slug, Person._GeneratedField
   test.isNull Post.Meta.fields.slug.ancestorArray, Post.Meta.fields.slug.ancestorArray
   test.isTrue _.isFunction Post.Meta.fields.slug.generator
   test.equal Post.Meta.fields.slug.sourcePath, 'slug'
@@ -245,6 +273,17 @@
   test.equal Post.Meta.fields.slug.sourceDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.slug.targetDocument.Meta.collection, Posts
   test.equal Post.Meta.fields.slug.fields, ['body', 'subdocument.body']
+  test.instanceOf Post.Meta.fields.tags, Person._GeneratedField
+  test.isNull Post.Meta.fields.tags.ancestorArray, Post.Meta.fields.tags.ancestorArray
+  test.isTrue _.isFunction Post.Meta.fields.tags.generator
+  test.equal Post.Meta.fields.tags.sourcePath, 'tags'
+  test.equal Post.Meta.fields.tags.sourceDocument, Post
+  test.equal Post.Meta.fields.tags.targetDocument, Post
+  test.equal Post.Meta.fields.tags.sourceCollection, Posts
+  test.equal Post.Meta.fields.tags.targetCollection, Posts
+  test.equal Post.Meta.fields.tags.sourceDocument.Meta.collection, Posts
+  test.equal Post.Meta.fields.tags.targetDocument.Meta.collection, Posts
+  test.equal Post.Meta.fields.tags.fields, ['body', 'subdocument.body', 'nested.body']

   test.equal UserLink.Meta.collection, UserLinks
   test.equal _.size(UserLink.Meta.fields), 1


Increased timeouts even more.

Increased timeouts even more.

--- tests.coffee
+++ tests.coffee
@@ -471,7 +471,7 @@
     # (triggered by person inserts, but pending) run afterwards, then they can patch things which
     # should in fact be done by source observers (on post), like setting usernames in post's
     # references to persons
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person1 = Persons.findOne @person1Id
@@ -525,7 +525,7 @@
         @postId = postId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -601,7 +601,7 @@
     # Sleep so that observers have time to update documents
     # so that persons updates are not merged together to better
     # test the code for multiple updates
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     Persons.update @person3Id,
@@ -628,7 +628,7 @@
     test.equal @person3.displayName, 'Person 3'

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -688,7 +688,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -737,7 +737,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -769,7 +769,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -828,7 +828,7 @@
         @circularSecondId = circularSecondId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     if Meteor.isServer
@@ -869,7 +869,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -898,7 +898,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -928,7 +928,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -958,7 +958,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -984,7 +984,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -1020,7 +1020,7 @@
         @circularFirstId = circularFirstId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     if Meteor.isServer
@@ -1054,7 +1054,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -1080,7 +1080,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update document
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @circularFirst = CircularFirsts.findOne @circularFirstId,
@@ -1118,7 +1118,7 @@
         @recursive2Id = recursive2Id

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive1 = Recursives.findOne @recursive1Id,
@@ -1145,7 +1145,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive1 = Recursives.findOne @recursive1Id,
@@ -1174,7 +1174,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive1 = Recursives.findOne @recursive1Id,
@@ -1204,7 +1204,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive1 = Recursives.findOne @recursive1Id,
@@ -1234,7 +1234,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive1 = Recursives.findOne @recursive1Id,
@@ -1260,7 +1260,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive1 = Recursives.findOne @recursive1Id,
@@ -1288,7 +1288,7 @@
         @recursiveId = recursiveId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive = Recursives.findOne @recursiveId,
@@ -1309,7 +1309,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive = Recursives.findOne @recursiveId,
@@ -1331,7 +1331,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive = Recursives.findOne @recursiveId,
@@ -1349,7 +1349,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @recursive = Recursives.findOne @recursiveId,
@@ -1367,7 +1367,7 @@
         _id: 'nonexistent'

     # Sleep so that observers have time to update documents
-    Meteor._sleepForMs(750)
+    Meteor._sleepForMs(1000)

     intercepted = Log._intercepted()

@@ -1384,7 +1384,7 @@
       subscribers: 'foobar'

     # Sleep so that observers have time to update documents
-    Meteor._sleepForMs(750)
+    Meteor._sleepForMs(1000)

     intercepted = Log._intercepted()

@@ -1401,7 +1401,7 @@
       author: null

     # Sleep so that observers have time to update documents
-    Meteor._sleepForMs(750)
+    Meteor._sleepForMs(1000)

     intercepted = Log._intercepted()

@@ -1418,7 +1418,7 @@
       user: null

     # Sleep so that observers have time to update documents
-    Meteor._sleepForMs(750)
+    Meteor._sleepForMs(1000)

     intercepted = Log._intercepted()

@@ -1537,7 +1537,7 @@
         @postId = postId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -1600,7 +1600,7 @@
         @postLinkId = postLinkId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @postLink = PostLinks.findOne @postLinkId,
@@ -1631,7 +1631,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person2 = Persons.findOne @person2Id
@@ -1664,7 +1664,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @postLink = PostLinks.findOne @postLinkId,
@@ -1686,7 +1686,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @postLink = PostLinks.findOne @postLinkId,
@@ -1776,7 +1776,7 @@
         @postId = postId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -1840,7 +1840,7 @@
     # Sleep so that observers have time to update documents
     # so that persons updates are not merged together to better
     # test the code for multiple updates
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -1906,7 +1906,7 @@
     # Sleep so that observers have time to update documents
     # so that persons updates are not merged together to better
     # test the code for multiple updates
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
    (test, expect) ->
     @post = Posts.findOne @postId,
@@ -1972,7 +1972,7 @@
     # Sleep so that observers have time to update documents
     # so that persons updates are not merged together to better
     # test the code for multiple updates
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
    (test, expect) ->
     @post = Posts.findOne @postId,
@@ -2036,7 +2036,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -2095,7 +2095,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -2561,7 +2561,7 @@
         @identityGeneratorId = identityGeneratorId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     if Meteor.isServer
@@ -2597,7 +2597,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     if Meteor.isServer
@@ -2784,7 +2784,7 @@
         @postId = postId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -2917,7 +2917,7 @@
     # Sleep so that observers have time to update documents
     # so that persons updates are not merged together to better
     # test the code for multiple updates
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     Persons.update @person3Id,
@@ -2944,7 +2944,7 @@
     test.equal @person3.displayName, 'Person 3'

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -3067,7 +3067,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person1 = Persons.findOne @person1Id
@@ -3195,7 +3195,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person2 = Persons.findOne @person2Id
@@ -3313,7 +3313,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person3 = Persons.findOne @person3Id
@@ -3421,7 +3421,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person1 = Persons.findOne @person1Id
@@ -3530,7 +3530,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person2 = Persons.findOne @person2Id
@@ -3649,7 +3649,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @person3 = Persons.findOne @person3Id
@@ -3778,7 +3778,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -3901,7 +3901,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4024,7 +4024,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4147,7 +4147,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4269,7 +4269,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4388,7 +4388,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4512,7 +4512,7 @@
         test.isTrue res

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4637,7 +4637,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4703,7 +4703,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4733,7 +4733,7 @@
         test.isFalse error, error?.toString?() or error

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     @post = Posts.findOne @postId,
@@ -4755,7 +4755,7 @@
         @identityGeneratorId = identityGeneratorId

     # Sleep so that observers have time to update documents
-    Meteor.setTimeout expect(), 750
+    Meteor.setTimeout expect(), 1000
 ,
   (test, expect) ->
     if Meteor.isServer


Call callback at end of links commmand

Call callback at end of links commmand

--- src/links.coffee
+++ src/links.coffee
@@ -47,6 +47,9 @@
         realpath = '???'.red
       "#{path.basename(link).yellow} -> #{realpath}"

-  run: ->
+  run: (options) ->
+    {callback} = options
+
     @logLinks(@devPackagesPath)
     @logLinks(@packagesPath)
+    callback()


- Remove debugger

- Remove debugger

--- client/landing/app/Applications/Activity.kdapplication/ActivityActions.coffee
+++ client/landing/app/Applications/Activity.kdapplication/ActivityActions.coffee
@@ -52,7 +52,6 @@
       KDEventTypes  : "Click"
       listener      : @
       callback      : =>
-        debugger
         if KD.isLoggedIn()
           # oldCount = @likeCount.data.meta.likes
           activity.like (err)=>


Translate '\' back to '/' after normalize on windows platform (#14)

Will accept this PR but still need to assess whether to retain the path.normalize during .locate XPATH routine since path library is platform adaptive while XPATH expression always has fixed path syntax.

--- src/yang.coffee
+++ src/yang.coffee
@@ -95,7 +95,7 @@

   locate: (xpath) ->
     return unless typeof xpath is 'string' and !!xpath
-    xpath = path.normalize(xpath).replace /\s/g, ''
+    xpath = path.normalize(xpath).replace(/\\/g, '/').replace /\s/g, ''
     if (/^\//.test xpath) and not @root
       return @parent.locate xpath
     [ key, rest... ] = xpath.split('/').filter (e) -> !!e


Remove _.map appearances.

Remove _.map appearances.

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -181,7 +181,7 @@
         'handler argument must be function'

     # Add an event namespace, bind handler it to view.
-    list = _.map eventName.split(' '), (event) => "#{event}.delegate#{@cid}"
+    list = ("#{event}.delegate#{@cid}" for event in eventName.split ' ')
     events = list.join(' ')
     bound = _.bind handler, this
     @$el.on events, (selector or null), bound
@@ -234,7 +234,7 @@
             'second argument must be a string'
         handler = third

-      list = _.map eventName.split(' '), (event) => "#{event}.delegate#{@cid}"
+      list = ("#{event}.delegate#{@cid}" for event in eventName.split ' ')
       events = list.join(' ')
       @$el.off events, (selector or null)
     else


Updated specs.

Updated specs.

--- spec/bracket-matcher-spec.coffee
+++ spec/bracket-matcher-spec.coffee
@@ -50,16 +50,16 @@
         editor.moveCursorLeft()
         editor.moveCursorLeft()
         expect(editorView.underlayer.find('.bracket-matcher:visible').length).toBe 2
-        expect(editorView.underlayer.find('.bracket-matcher:last').position()).toEqual editorView.pixelPositionForBufferPosition([12,0])
-        expect(editorView.underlayer.find('.bracket-matcher:first').position()).toEqual editorView.pixelPositionForBufferPosition([0,28])
+        expect(editorView.underlayer.find('.bracket-matcher:first').position()).toEqual editorView.pixelPositionForBufferPosition([12,0])
+        expect(editorView.underlayer.find('.bracket-matcher:last').position()).toEqual editorView.pixelPositionForBufferPosition([0,28])

     describe "when the cursor is after an ending pair", ->
       it "highlights the starting pair and ending pair", ->
         editor.moveCursorToBottom()
         editor.moveCursorLeft()
         expect(editorView.underlayer.find('.bracket-matcher:visible').length).toBe 2
-        expect(editorView.underlayer.find('.bracket-matcher:last').position()).toEqual editorView.pixelPositionForBufferPosition([12,0])
-        expect(editorView.underlayer.find('.bracket-matcher:first').position()).toEqual editorView.pixelPositionForBufferPosition([0,28])
+        expect(editorView.underlayer.find('.bracket-matcher:first').position()).toEqual editorView.pixelPositionForBufferPosition([12,0])
+        expect(editorView.underlayer.find('.bracket-matcher:last').position()).toEqual editorView.pixelPositionForBufferPosition([0,28])

     describe "when there are unpaired brackets", ->
       it "highlights the correct start/end pairs", ->
@@ -112,8 +112,8 @@
         editor.moveCursorLeft()
         atom.config.set('editor.fontSize', editorView.getFontSize() + 10)
         expect(editorView.underlayer.find('.bracket-matcher:visible').length).toBe 2
-        expect(editorView.underlayer.find('.bracket-matcher:last').position()).toEqual editorView.pixelPositionForBufferPosition([12,0])
-        expect(editorView.underlayer.find('.bracket-matcher:first').position()).toEqual editorView.pixelPositionForBufferPosition([0,28])
+        expect(editorView.underlayer.find('.bracket-matcher:first').position()).toEqual editorView.pixelPositionForBufferPosition([12,0])
+        expect(editorView.underlayer.find('.bracket-matcher:last').position()).toEqual editorView.pixelPositionForBufferPosition([0,28])

     describe "pair balancing", ->
       describe "when a second starting pair preceeds the first ending pair", ->


client-side uses the same comparison method as in backend  fix #103

--- src/client/common_stat.coffee
+++ src/client/common_stat.coffee
@@ -23,6 +23,25 @@
                 $("tbody tr:eq(\"#{i}\") td:first", table).html(i + 1);
     })

+    $.tablesorter.addParser({
+      id: 'ratingToString'
+      format: (s, table, cell, cellIndex)->
+        return {
+        gamesCitizen: parseInt $(cell).attr('games-citizen')
+        gamesSheriff: parseInt $(cell).attr('games-sheriff')
+        gamesMafia: parseInt $(cell).attr('games-mafia')
+        gamesDon: parseInt $(cell).attr('games-don')
+        winsCitizen: parseInt $(cell).attr('wins-citizen')
+        winsSheriff: parseInt $(cell).attr('wins-sheriff')
+        winsMafia: parseInt $(cell).attr('wins-mafia')
+        winsDon: parseInt $(cell).attr('wins-don')
+        bestPlayer: parseInt $(cell).attr('best-player')
+        firstKilledNight: parseInt $(cell).attr('first-killed-at-night')
+        rating: parseFloat $(cell).attr('rating')
+        }
+      type: 'text'
+    })
+
     extended = (players)->
         result = []
         for player in players
@@ -46,7 +65,11 @@
         $('.stat-tables').show()
         players = extended(players)
         for player in players
-          $("
	#{player.name}	#{(playercomparator.average(player) + playercomparator.experience(player)).toFixed(2)}	#{player.gamesTotal}	#{player.winsTotal}	#{player.rating}
").appendTo('.common-rating tbody')
+          gamesAttrs = "games-citizen=#{player.gamesCitizen} games-sheriff=#{player.gamesSheriff} games-mafia=#{player.gamesMafia} games-don=#{player.gamesDon}"
+          winsAttrs = "wins-citizen=#{player.winsCitizen} wins-sheriff=#{player.winsSheriff} wins-mafia=#{player.winsMafia} wins-don=#{player.winsDon}"
+          impactAttrs = "best-player=#{player.bestPlayer} first-killed-at-night=#{player.firstKilledNight}"
+          ratingsAttrs = "rating=#{player.rating}"
+          $("
	#{player.name}	#{(playercomparator.average(player) + playercomparator.experience(player)).toFixed(2)}	#{player.gamesTotal}	#{player.winsTotal}	#{player.rating}
").appendTo('.common-rating tbody')
           $("
	#{player.name}	#{player.winsTotal}	#{player.winsCitizen}	#{player.winsSheriff}	#{player.winsMafia}	#{player.winsDon}
").appendTo('.wins tbody')
           $("
	#{player.name}	#{player.gamesTotal}	#{player.gamesCitizen}	#{player.gamesSheriff}	#{player.gamesMafia}	#{player.gamesDon}
").appendTo('.roles tbody')
           $("
	#{player.name}	#{player.likes}	#{player.bestPlayer}	#{player.bestMoveAccuracy}	#{player.firstKilledNight}	#{player.firstKilledDay}	#{player.fouls}
").appendTo('.impact tbody')
@@ -66,13 +89,18 @@
             success: (data, textStatus) ->
                 renderTables(data)
                 $('.common-rating').tablesorter({
-                    headers: {
-                      0: { sorter: false}
-                    }
-                    sortList : [[2, 1]]
-                    theme: "bootstrap",
-                    widgets: ['uitheme', 'indexFirstColumn'],
-                    headerTemplate: '{content} {icon}'
+                  headers: {
+                    0: { sorter: false}
+                    2: { sorter: 'ratingToString'}
+                  }
+                  sortList : [[2, 1]]
+                  textSorter: {
+                    2: (a, b, direction, column, table)->
+                      return playercomparator.compare(a, b)
+                  }
+                  theme: "bootstrap",
+                  widgets: ['uitheme', 'indexFirstColumn'],
+                  headerTemplate: '{content} {icon}'
                 })
                 ['wins', 'roles', 'impact'].forEach((className)->
                   $(".#{className}").tablesorter({


:white_check_mark: Add tests for controls size and position with minimap on left

--- spec/minimap-element-spec.coffee
+++ spec/minimap-element-spec.coffee
@@ -728,10 +728,21 @@
           expect(controlsRect.right).toEqual(canvasRect.right)

         describe 'when the displayMinimapOnLeft setting is enabled', ->
+          beforeEach ->
+            atom.config.set('minimap.displayMinimapOnLeft', true)
+            nextAnimationFrame()
+
+          it 'adjusts the size of the control div to fit in the minimap', ->
+            expect(controls.clientWidth).toEqual(minimapElement.canvas.clientWidth)
+
+          it 'positions the controls div over the canvas', ->
+            controlsRect = controls.getBoundingClientRect()
+            canvasRect = minimapElement.canvas.getBoundingClientRect()
+            expect(controlsRect.left).toEqual(canvasRect.left)
+            expect(controlsRect.right).toEqual(canvasRect.right)
+
           describe 'clicking on the div', ->
             beforeEach ->
-              atom.config.set('minimap.displayMinimapOnLeft', true)
-
               workspaceElement = atom.views.getView(atom.workspace)
               jasmineContent.appendChild(workspaceElement)


Make prev and next consistent with after and before when not part of a collection.

--- src/model.coffee
+++ src/model.coffee
@@ -48,13 +48,13 @@
     Returns the model after this model in its collection. If it's not in a
     collection this will return null.
     ###
-    next: -> @collection?.after(@)
+    next: -> @collection?.after(@) ? null

     ###
     Returns the model before this model in its collection. If it's not in a
     collection this will return null.
     ###
-    prev: -> @collection?.before(@)
+    prev: -> @collection?.before(@) ? null

     ###
     Returns a clone of the attributes object.
@@ -94,6 +94,8 @@
             continue if val is null
             if @cast[key] then attrs[key] = @_cast(val, @cast[key])

+        # Do this before derived values so any events for what was actually set
+        # are triggered before derived change events are fired.
         ret = super(attrs, options)

         for derived, definition of @derived


Implement line style for line chart

Implement line style for line chart

--- src/chart-d3-line.coffee
+++ src/chart-d3-line.coffee
@@ -34,17 +34,18 @@
     @renderXAxis(xScale, chartHeight)
     @renderYAxis(yScale)

-    @renderLines(data, labels, xScale, yScale)
+    @renderLines(data, labels, xScale, yScale, chartHeight, options)

     @updateGridTickStyle(options)
     @updateScaleStrokeStyle(options)
     @updateScaleTextStyle(options)
     this

-  renderLines: (data, labels, xScale, yScale) =>
+  renderLines: (data, labels, xScale, yScale, chartHeight, options) =>
     line = d3.svg.line()
       .x((d, i) -> xScale(labels[i]))
       .y((d) -> yScale(d))
+      .interpolate('cardinal')
     @getRootElement()
       .select('.margin-convention-element')
       .append('g')
@@ -59,5 +60,6 @@
       .append('path')
       .classed('line', true)
       .attr('d', (d) -> line(d.data))
+      .attr('stroke', (d) -> d.strokeColor)
+      .attr('stroke-width', options.datasetStrokeWidth)
       .attr('fill', 'none')
-      .attr('stroke', 'black')


lastIndexOf does not work in IE7/8

lastIndexOf does not work in IE7/8

--- coffee/chosen.jquery.coffee
+++ coffee/chosen.jquery.coffee
@@ -402,9 +402,11 @@
     @scopes.push(item.value)

   result_expand: (item) ->
-    idx = @scopes.lastIndexOf(item.value)
-    @scopes = @scopes.slice(0, idx) if idx > -1
+    for v, i in @scopes by -1
+      if v == item.value
+        @scopes = @scopes.slice(0, i)
     return true
+    return false

   result_clear_scope: ->
     @scopes = []


dynamically setting router baseUrl

dynamically setting router baseUrl

--- coffeescripts/routes.coffee
+++ coffeescripts/routes.coffee
@@ -5,11 +5,11 @@
   ($locationProvider, $stateProvider) ->
     $locationProvider.html5Mode(true).hashPrefix '!'

-    urlRoot = "/~jburton/action-items"
+    urlRoot = window.location.pathname

     $stateProvider
       .state 'home',
-        url: "#{urlRoot}/"
+        url: "#{urlRoot}"
         templateUrl: 'partials/pages/home.html'
         controller: 'HomePageController as controller'
 ]


fixed appstore connection

fixed appstore connection

--- collections/appstore.coffee
+++ collections/appstore.coffee
@@ -1,10 +1,17 @@
 Meteor.startup ->
-  if Settings.findOne().remoteAppstoreUrl
-    central = DDP.connect Settings.findOne().remoteAppstoreUrl
-    @AppStore = new Mongo.Collection 'appstore',
-      connection: central
+  connectToAppStore = =>
+    if Settings.findOne()?.remoteAppstoreUrl
+      console.log 'has appstore', Settings.findOne()?.remoteAppstoreUrl
+      central = DDP.connect Settings.findOne().remoteAppstoreUrl
+      @AppStore = new Mongo.Collection 'appstore',
+        connection: central

-    Meteor.startup ->
       central.subscribe 'appstore'
-  else
-    @AppStore = new Mongo.Collection 'appstore'
+    else
+      console.log 'NO appstore'
+      @AppStore = new Mongo.Collection 'appstore'
+
+  Settings.find().observe
+    added: connectToAppStore
+    removed: connectToAppStore
+    changed: connectToAppStore


Fixing logging error

Fixing logging error

--- src/packages/carpool-notifications/client/NotificationClient.coffee
+++ src/packages/carpool-notifications/client/NotificationClient.coffee
@@ -5,10 +5,10 @@
     window.alert = navigator.notification?.alert

   Push.addListener 'startup', (notification) ->
-    da ["trip-notifications"] "Startup on notification:", notification
+    da ["trip-notifications"], "Startup on notification:", notification

   Push.addListener 'message', (notification) ->
     # Called on every message
     alertDismissed = ->
       NotificationHistory.update { _id: notification.payload.historyId }, $set: 'recievedAt': new Date
-    da ["trip-notifications"] "Notification received:", notification
+    da ["trip-notifications"], "Notification received:", notification


Apparently a cat is a friggen owl :cat2::bird:

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -106,6 +106,9 @@
   # All hosts are dongs
   if /(dev|dong|dodge)host/i.test(name)
     name = 'dong'
+
+  if /meowth/i.test(name)
+    name = 'o_rly'

   return name


Use proper block comments for coffeescript.

--- src/coffee/stockexport.coffee
+++ src/coffee/stockexport.coffee
@@ -4,10 +4,11 @@
 CommonUpdater = require('sphere-node-sync').CommonUpdater
 InventoryUpdater = require('sphere-node-sync').InventoryUpdater

-/**
- * Elastic.io component exporting inventory items from Sphere.io.
- * For each inventory item a XML file will be written.
- */
+
+###
+Elastic.io component exporting inventory items from Sphere.io.
+For each inventory item a XML file will be written.
+###
 class StockExport extends CommonUpdater
   constructor: (options = {}) ->
     super options
@@ -16,22 +17,22 @@
     @rest = @inventoryUpdater.rest
     @

-  /**
-   * Fetch inventory items from Sphere and export xml files for all inventory items.
-   * @param {string} msg The message to process.
-   * @param {hash} cfg The component's configuration.
-   * @param {function} next The callback function to be invoked when the component finished its work.
-   * @param {string} snapshot The optional snapshot parameter which is used by a component for deduplication.
-   * @return The result of the callback.
-   */
+  ###
+  Fetch inventory items from Sphere and export xml files for all inventory items.
+  @param {string} msg The message to process.
+  @param {hash} cfg The component's configuration.
+  @param {function} next The callback function to be invoked when the component finished its work.
+  @param {string} snapshot The optional snapshot parameter which is used by a component for deduplication.
+  @return The result of the callback.
+  ###
   elasticio: (msg, cfg, next, snapshot) ->
     @returnResult false, 'Not yet implemented', next

-  /**
-   * Fetch inventory items from Sphere and create xml for all inventory items.
-   * @param {function} callback The callback function to be invoked when the method finished its work.
-   * @return The result of the callback.
-   */
+  ###
+  Fetch inventory items from Sphere and create xml for all inventory items.
+  @param {function} callback The callback function to be invoked when the method finished its work.
+  @return The result of the callback.
+  ###
   run: (callback) ->
     @inventoryUpdater.allInventoryEntries(@rest).then (allInventoryEntries) =>
       xml = @createXml()
@@ -44,10 +45,10 @@
     .fail (msg) ->
       @returnResult false, msg, callback

-  /**
-   * Creates an XML instance.
-   * @return {number} The result of adding num1 and num2.
-   */
+  ###
+  Creates an XML instance.
+  @return {number} The result of adding num1 and num2.
+  ###
   createXml: () ->
     ops =
       version: '1.0'
@@ -57,12 +58,12 @@
     xml.e('xsdVersion').t '0.1'
     xml

-  /**
-   * Adds inventory data to given xml instance.
-   * @param {xml} xml The xml instance to be used.
-   * @param {inventory} entry The inventory data to be used in the template.
-   * @return {xml} The xml with inventory data.
-   */
+  ###
+  Adds inventory data to given xml instance.
+  @param {xml} xml The xml instance to be used.
+  @param {inventory} entry The inventory data to be used in the template.
+  @return {xml} The xml with inventory data.
+  ###
   createXmlEntry: (xml, entry) ->
     xe = xml.e('entry')
     @add xe, entry, 'id'
@@ -70,11 +71,11 @@
     @add xe, entry, 'sku'
     @add xe, entry, 'quantityOnStock'

-  /**
-   * Writes an xml file..
-   * @param {xml} xml The xml file to be written.
-   * @return {promise} The promise for writing the xml file.
-   */
+  ###
+  Writes an xml file..
+  @param {xml} xml The xml file to be written.
+  @return {promise} The promise for writing the xml file.
+  ###
   writeOutput: (xml) ->
     deffered = Q.defer()
     content = xml.end(pretty: true, indent: '  ', newline: "\n")
@@ -85,14 +86,14 @@
         deffered.resolve @output
     deffered.promise

-  /**
-   * Adds an element to given xml instance.
-   * @param {xml} xml The xml instance to be used.
-   * @param {element} elem The xml element to be added.
-   * @param {string} attr The name of the element.
-   * @param {string} xAttr ???
-   * @return {xml} The xml with added element.
-   */
+  ###
+  Adds an element to given xml instance.
+  @param {xml} xml The xml instance to be used.
+  @param {element} elem The xml element to be added.
+  @param {string} attr The name of the element.
+  @param {string} xAttr ???
+  @return {xml} The xml with added element.
+  ###
   add: (xml, elem, attr, xAttr) ->
     xAttr = attr unless xAttr
     value = elem[attr]


change the wording slightly

change the wording slightly

--- src/command/revoke_proof.iced
+++ src/command/revoke_proof.iced
@@ -74,10 +74,10 @@
     err = null
     if rp? and (v = rp[@service_name])
       await prompt_yn {
-        prompt : "Cancel your proof of #{v} at #{@service_name}?",
+        prompt : "Revoke your proof of #{v} at #{@service_name}?",
         defval : false }, defer err, ok
       if not err? and not ok
-        err = new E.CancelError "cancelled"
+        err = new E.CancelError "Cancellation canceled! Did nothing."
     else
       err = E.NotFoundError "No proof found for service '#{@service_name}'"
     cb err


fix(iscroll): destory iscroll instance on state change

--- modules/directives/iscroll/index.coffee
+++ modules/directives/iscroll/index.coffee
@@ -41,3 +41,9 @@

     # schedule IScroll instantication
     $timeout instanciateIScroll, options.delay
+
+    scope.$on '$destroy', ->
+      iscroll.destroy()
+
+    scope.$on '$stateChangeStart', ->
+      iscroll.destroy()


Fix properties.coffee and add support for boolean

--- bokehjs/src/coffee/renderer/properties.coffee
+++ bokehjs/src/coffee/renderer/properties.coffee
@@ -47,61 +47,99 @@
     string: (styleprovider, attrname) ->
       @[attrname] = {}

-      default_value = styleprovider.mget(attrname)
-      if not default_value?
-      else if _.isString(default_value)
-        @[attrname].default = default_value
-      else
-        logger.warn("string property '#{attrname}' given invalid default value: #{default_value}")
+      value = styleprovider.mget(attrname)
+      if not value?
+        null
+      else if _.isString(value)
+        @[attrname].value = value
+      else if _.isObject(value)
+        @[attrname] = _.extend(@[attrname], value)
+      else
+        logger.warn("string property '#{attrname}' given invalid value: #{value}")
+
+    boolean: (styleprovider, attrname) ->
+      @[attrname] = {}
+
+      value = styleprovider.mget(attrname)
+      if not value?
+        null
+      else if _.isBoolean(value)
+        @[attrname].value = value
+      else if _.isString(value)
+        @[attrname].field = value
+      else if _.isObject(value)
+        @[attrname] = _.extend(@[attrname], value)
+      else
+        logger.warn("boolean property '#{attrname}' given invalid value: #{value}")

     number: (styleprovider, attrname) ->
-      @[attrname] = { } #typed: true }
-
-      default_value = styleprovider.mget(attrname)
-      if not default_value?
-      else if _.isNumber(default_value)
-        @[attrname].default = default_value
-      else
-        logger.warn("number property '#{attrname}' given invalid default value: #{default_value}")
-
-      units_value = styleprovider.mget(attrname+'_units') ? 'data'
+      @[attrname] = {}
+
+      units_value = styleprovider.mget(attrname + '_units') ? 'data'
       @[attrname].units = units_value

+      value = styleprovider.mget(attrname)
+      if not value?
+        null
+      else if _.isNumber(value)
+        @[attrname].value = value
+      else if _.isString(value)
+        @[attrname].field = value
+      else if _.isObject(value)
+        @[attrname] = _.extend(@[attrname], value)
+      else
+        logger.warn("number property '#{attrname}' given invalid value: #{value}")
+
     color: (styleprovider, attrname) ->
       @[attrname] = {}

-      default_value = styleprovider.mget(attrname)
-      if _.isUndefined(default_value)
-        @[attrname].default = null
-      else if _.isString(default_value) and (svg_colors[default_value]? or default_value.substring(0, 1) == "#") or _.isNull(default_value)
-        @[attrname].default = default_value
-      else
-        logger.warn("color property '#{attrname}' given invalid default value: #{default_value}")
+      value = styleprovider.mget(attrname)
+      if not value?
+        null
+      else if _.isString(value)
+        if svg_colors[value]? or value.substring(0, 1) == "#"
+          @[attrname].value = value
+        else
+          @[attrname].field = value
+      else if _.isObject(value)
+        @[attrname] = _.extend(@[attrname], value)
+      else
+        logger.warn("color property '#{attrname}' given invalid value: #{value}")

     array: (styleprovider, attrname) ->
       @[attrname] = {}
-
-      default_value = styleprovider.mget(attrname)
-      if not default_value?
-      else if _.isArray(default_value)
-        @[attrname].default = default_value
-      else
-        logger.warn("array property '#{attrname}' given invalid default value: #{default_value}")

       units_value = styleprovider.mget(attrname+"_units") ? 'data'
       @[attrname].units = units_value

+      value = styleprovider.mget(attrname)
+      if not value?
+        null
+      else if _.isString(value)
+        @[attrname].field = value
+      else if _.isArray(value)
+        @[attrname].value = value
+      else if _.isObject(value)
+        @[attrname] = _.extend(@[attrname], value)
+      else
+        logger.warn("array property '#{attrname}' given invalid value: #{value}")
+
     enum: (styleprovider, attrname, vals) ->
       @[attrname] = {}

       levels = vals.split(" ")
-
       default_value = styleprovider.mget(attrname)
-      if _.isNull(default_value)
-      else if _.isString(default_value) and default_value in levels
-        @[attrname] = {default: default_value}
-      else
-        logger.warn("enum property '#{attrname}' given invalid default value: #{default_value}")
+      if not value?
+        null
+      else if _.isString(value)
+        if value in levels
+          @[attrname].value = value
+        else
+          @[attrname].field = value
+      else if _.isObject(value)
+        @[attrname] = _.extend(@[attrname], value)
+      else
+        logger.warn("enum property '#{attrname}' given invalid value: #{value}")
         logger.warn(" - acceptable values:" + levels)

     setattr: (styleprovider, attrname, attrtype) ->
@@ -109,12 +147,12 @@
       if attrtype.indexOf(":") > -1
         [attrtype, values] = attrtype.split(":")

-      if      attrtype == "string" then @string(styleprovider, attrname)
-      else if attrtype == "number" then @number(styleprovider, attrname)
-      else if attrtype == "color"  then @color(styleprovider, attrname)
-      else if attrtype == "array"  then @array(styleprovider, attrname)
-      else if attrtype == "enum" and values
-        @enum(styleprovider, attrname, values)
+      if      attrtype == "string"          then @string(styleprovider, attrname)
+      else if attrtype == "boolean"         then @boolean(styleprovider, attrname)
+      else if attrtype == "number"          then @number(styleprovider, attrname)
+      else if attrtype == "color"           then @color(styleprovider, attrname)
+      else if attrtype == "array"           then @array(styleprovider, attrname)
+      else if attrtype == "enum" and values then @enum(styleprovider, attrname, values)
       else
         logger.warn("Unknown type '#{attrtype}' for glyph property: #{attrname}")


Make renderQueue (and other render state) per-instance instead of global. Fixes #46.

--- src/core/camaninstance.coffee
+++ src/core/camaninstance.coffee
@@ -10,19 +10,17 @@

   @toString = Caman.toString

-  pixelStack: []  # Stores the pixel layers
-  layerStack: []  # Stores all of the layers waiting to be rendered
-  renderQueue: [] # Stores all of the render operatives
-  canvasQueue: [] # Stores all of the canvases to be processed
-  currentLayer: null
-  analyze: null
-
   # All of the arguments given to the Caman() function are simply thrown here.
   constructor: (args, type = CamanInstance.Type.Canvas) ->
     # Every instance gets a unique ID. Makes it much simpler to check if two variables are the
     # same instance.
     @id = Util.uniqid.get()
     @analyze = new Analyze @
+    @pixelStack = []  # Stores the pixel layers
+    @layerStack = []  # Stores all of the layers waiting to be rendered
+    @renderQueue = [] # Stores all of the render operatives
+    @canvasQueue = [] # Stores all of the canvases to be processed
+    @currentLayer = null

     # DOM initialization check (if applicable)
     if type is CamanInstance.Type.Node


tests for Lists initialisation via the more traditional "array notation" of other languages

--- tests.coffee
+++ tests.coffee
@@ -320,16 +320,97 @@
   "( [object of class \"Console\"] print 1 + 2 ) - ( [object of class \"Console\"] print 1 + 2 )"

   # ---------------------------------------------------------------------------
-  """
-  """
-  """
-  """
-  """
-  """
-  """
-  """
-  """
-  """
+  # initialising Lists via the "more normal" "array literal" notation.
+  # Note that there are no "Array"s in FizzyLogo, only "List"s.
+  # This is just a very very convenient way to initialise them
+  # see the "ListLiteralArrayNotation" file for more insight.
+
+  """
+  a = [-2]
+  console print a length
+  console print a[0]
+  """
+  "1-2"
+
+  """
+  a = [0,-2]
+  console print a length
+  console print a[0]
+  console print a[1]
+  """
+  "20-2"
+
+  """
+  someVariable = 1
+  a = [someVariable,1+2,"a"+"b",-2,-2,1-2]
+  console print a length
+  console print a
+  """
+  """
+  6( 1 3 "ab" -2 -2 -1 )
+  """
+
+  """
+  a = [(1+2),true,(not not true)]
+  console print a length
+  console print a
+  """
+  """
+  3( 3 true true )
+  """
+
+  """
+  a = [1,2,[3,4],5]
+  console print a length
+  console print a
+  console print a[2] length
+  console print a[2]
+  """
+  """
+  4( 1 2 ( 3 4 ) 5 )2( 3 4 )
+  """
+
+  # doing
+  #    a = []
+  # is same as doing
+  #    a = '()
+  """
+  a = []
+  console print a length
+  """
+  "0"
+
+  """
+  a = [[]]
+  console print a length
+  console print a
+  console print a[0] length
+  console print a[0]
+  """
+  "1( empty message )0empty message"
+
+  """
+  someVariable = not not true
+  someOtherVariable = [(1+2),true,someVariable]
+  a = [ 1+2,"a"+"b",someOtherVariable,-2,-2,1-2]
+  console print a length
+  console print a
+  """
+  """
+  6( 3 "ab" ( 3 true true ) -2 -2 -1 )
+  """
+
+  """
+  someVariable = not not true
+  a = [ 1+2,"a"+"b",[(1+2),true,someVariable],-2,-2,1-2]
+  console print a length
+  console print a
+  """
+  """
+  6( 3 "ab" ( 3 true true ) -2 -2 -1 )
+  """
+
+  # ---------------------------------------------------------------------------
   "'a←5;'b←'a;console print b;console print a"
   "a5"


Remove trailing whitespace from test

Remove trailing whitespace from test

--- test/test.coffee
+++ test/test.coffee
@@ -119,4 +119,4 @@

         setImmediate ->
           expect(request.Request.prototype.abort.called).to.be.true
-          done()
+          done()


set default timeout to 10s for all tests

--- test/test.coffee
+++ test/test.coffee
@@ -7,6 +7,7 @@

 module.exports =
   scratch: (context) ->
+    context.timeout 10000
     context.beforeEach (next) ->
       mecano.rm scratch, ->
         mecano.mkdir scratch, next


Inline all keymaps into package.json

Inline all keymaps into package.json

--- build/tasks/compile-packages-slug-task.coffee
+++ build/tasks/compile-packages-slug-task.coffee
@@ -12,6 +12,12 @@
     menu = CSON.readFileSync(menuPath) if fs.isFileSync(menuPath)
     rm menusPath
     menu ? {}
+
+  getKeymaps = (appDir) ->
+    keymapsPath = path.join(appDir, 'keymaps')
+    keymaps = fs.listSync(keymapsPath, ['.cson', '.json']).map (keymapPath) -> CSON.readFileSync(keymapPath)
+    rm keymapsPath
+    keymaps ? []

   grunt.registerTask 'compile-packages-slug', 'Add bundled package metadata information to the main package.json file', ->
     appDir = fs.realpathSync(grunt.config.get('atom.appDir'))
@@ -58,5 +64,6 @@
     metadata = grunt.file.readJSON(path.join(appDir, 'package.json'))
     metadata._atomPackages = packages
     metadata._menu = getMenu(appDir)
+    metadata._keymaps = getKeymaps(appDir)

     grunt.file.write(path.join(appDir, 'package.json'), JSON.stringify(metadata))


Fix typo in log message

Fix typo in log message

--- services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee
+++ services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee
@@ -13,14 +13,14 @@

 	activateSudoMode: (userId, callback=(err)->) ->
 		if !userId?
-			return callback(new Error('[SudoMode] user must not be supplied'))
+			return callback(new Error('[SudoMode] user must be supplied'))
 		duration = TIMEOUT_IN_SECONDS
 		logger.log {userId, duration}, "[SudoMode] activating sudo mode for user"
 		rclient.set SudoModeHandler._buildKey(userId), '1', 'EX', duration, callback

 	isSudoModeActive: (userId, callback=(err, isActive)->) ->
 		if !userId?
-			return callback(new Error('[SudoMode] user must not be supplied'))
+			return callback(new Error('[SudoMode] user must be supplied'))
 		rclient.get SudoModeHandler._buildKey(userId), (err, result) ->
 			if err?
 				return callback(err)


Update chat UI immediately

Update chat UI immediately

--- services/web/public/coffee/ide/chat/services/chatMessages.coffee
+++ services/web/public/coffee/ide/chat/services/chatMessages.coffee
@@ -14,11 +14,27 @@
 				nextBeforeTimestamp: null
 				newMessage: null
 		}
+
+		justSent = false
+		ide.socket.on "new-chat-message", (message) =>
+			if message.user.id == ide.$scope.user.id and justSent
+				# Nothing to do
+			else
+				ide.$scope.$apply () ->
+					appendMessage(message)
+			justSent = false

-		ide.socket.on "new-chat-message", (message) =>
-			ide.$scope.$apply () ->
-				chat.state.newMessage = message
-				appendMessage(message)
+		chat.sendMessage = (message) ->
+			body =
+				content: message
+				_csrf : window.csrfToken
+			justSent = true
+			appendMessage({
+				user: ide.$scope.user
+				content: message
+				timestamp: Date.now()
+			})
+			return $http.post(MESSAGES_URL, body)

 		chat.loadMoreMessages = () ->
 			return if chat.state.atEnd
@@ -60,6 +76,8 @@
 				prependMessage(message)

 		appendMessage = (message) ->
+			chat.state.newMessage = message
+
 			lastMessage = chat.state.messages[chat.state.messages.length - 1]
 			shouldGroup = lastMessage? and
 				lastMessage.user.id == message.user.id and
@@ -74,10 +92,4 @@
 					contents: [message.content]
 				})

-		chat.sendMessage = (message) ->
-			body =
-				content: message
-				_csrf : window.csrfToken
-			return $http.post(MESSAGES_URL, body)
-
 		return chat


avoid exception when switching between full and split pdf view

--- services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
+++ services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
@@ -516,7 +516,15 @@

 					first = highlights[0]

-					pageNum = scope.pages[first.page].pageNum
+					# switching between split and full pdf views can cause
+					# highlights to appear before rendering
+					if !scope.pages
+						return # ignore highlight scroll if still rendering
+
+					pageNum = scope.pages[first.page]?.pageNum
+
+					if !pageNum?
+						return # ignore highlight scroll if page not found

 					# use a visual offset of 72pt to match the offset in PdfController syncToCode
 					scope.document.getPdfViewport(pageNum).then (viewport) ->


bootstrapping the ember app

bootstrapping the ember app

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -6,5 +6,6 @@
 #= require_self
 #= require theoj

+
 # for more details see: http://emberjs.com/guides/application/
 window.Theoj = Ember.Application.create()


fix financial year end dates changes

fix financial year end dates changes

https://trello.com/c/omdTDSwN/809-qae18-as-an-applicant-i-want-to-be-able-to-enter-my-financial-figures-for-2018

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -155,9 +155,9 @@
       fy_year = new Date().getFullYear()

     # Conditional latest year
-    # If from 3rd of September to December -> then previous year
-    # If from January to 2nd of September -> then current year
-    if (parseInt(fy_month, 10) == 9 && parseInt(fy_day, 10) >= 3) || parseInt(fy_month, 10) > 9
+    # If from 12th of September to December -> then previous year
+    # If from January to 12th of September -> then current year
+    if (parseInt(fy_month, 10) == 9 && parseInt(fy_day, 10) >= 13) || parseInt(fy_month, 10) > 9
       fy_year = parseInt(fy_year, 10) - 1

     # Updates the latest changed financial year input


Track all

Track all

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -37,3 +37,5 @@
 #= require avatar.errors

+
+ahoy.trackAll();


Fix mutual exclusivity for emoji only comments.

--- app/assets/javascripts/awards_handler.coffee
+++ app/assets/javascripts/awards_handler.coffee
@@ -131,7 +131,10 @@
       @createEmoji votesBlock, emoji

-  getVotesBlock: -> return $ '.js-awards-block.current'
+  getVotesBlock: ->
+
+    currentBlock = $ '.js-awards-block.current'
+    return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0

   getAwardUrl: -> return @getVotesBlock().data 'award-url'


Render frequent emoji block once.

Render frequent emoji block once.

--- app/assets/javascripts/awards_handler.coffee
+++ app/assets/javascripts/awards_handler.coffee
@@ -65,7 +65,7 @@
         $addBtn.removeClass 'is-loading'
         $menu = $('.emoji-menu')
         @positionMenu($menu, $addBtn)
-        @renderFrequentlyUsedBlock()
+        @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered

         setTimeout =>
           $menu.addClass 'is-visible'
@@ -343,6 +343,8 @@

       $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))

+    @frequentEmojiBlockRendered = yes
+

   setupSearch: ->


Changed availability to load_factor in merit order chart

--- app/assets/javascripts/d3/merit_order.coffee
+++ app/assets/javascripts/d3/merit_order.coffee
@@ -27,9 +27,9 @@
         gquery_key: "merit_order_#{k}_capacity_in_merit_order_table"
       @gquery_y = new ChartSerie
         gquery_key: "merit_order_#{k}_operating_costs_in_merit_order_table"
-      @gquery_availability = new ChartSerie
-        gquery_key: "merit_order_#{k}_availability_in_merit_order_table"
-      D3.merit_order.series.push @gquery_x, @gquery_y, @gquery_availability
+      @gquery_load_factor = new ChartSerie
+        gquery_key: "merit_order_#{k}_load_factor_in_merit_order_table"
+      D3.merit_order.series.push @gquery_x, @gquery_y, @gquery_load_factor

     value_x: => @gquery_x.safe_future_value()

@@ -38,7 +38,7 @@

     original_y_value: => @gquery_y.safe_future_value()

-    availability_value: -> Metric.ratio_as_percentage @gquery_availability.safe_future_value()
+    load_factor_value: -> Metric.ratio_as_percentage @gquery_load_factor.safe_future_value()

     label: => @get('label') || @get('key')

@@ -174,7 +174,7 @@

                   Operating costs: #{Metric.autoscale_value d.original_y_value(), 'euro', 2}

-                  Availability: #{d.availability_value()}
+                  Load factor: #{d.load_factor_value()}
                   "
           if d.get('key') == 'must_run'
             html += '*
* Must run plants do not participate in merit order'


fix console lightbox js

fix console lightbox js

--- app/assets/javascripts/folio/lightbox.coffee
+++ app/assets/javascripts/folio/lightbox.coffee
@@ -56,7 +56,7 @@
 window.makeFolioLightbox = (selector, opts = {}) ->
   window.folioLightboxInstances ?= []

-  $(document).on 'turbolinks:load', ->
+  init = ->
     $items = $(selector)
     return if $items.length is 0
     if opts.individual
@@ -76,9 +76,14 @@
     else
       window.folioLightboxInstances.push(new window.FolioLightbox(selector))

-  $(document).on 'turbolinks:before-cache', ->
-    return unless window.folioLightboxInstances.length > 0
+  if Turbolinks?
+    $(document).on 'turbolinks:load', init

-    instance.destroy() for instance in window.folioLightboxInstances
+    $(document).on 'turbolinks:before-cache', ->
+      return unless window.folioLightboxInstances.length > 0

-    window.folioLightboxInstances = []
+      instance.destroy() for instance in window.folioLightboxInstances
+
+      window.folioLightboxInstances = []
+  else
+    $ init


move some options from captureproof

move some options from captureproof

--- app/assets/javascripts/s3_form_for.js.coffee
+++ app/assets/javascripts/s3_form_for.js.coffee
@@ -20,11 +20,10 @@
     before_add: null
     remove_completed_progress_bar: true
     remove_failed_progress_bar: false
-    progress_bar_target: null
+    progress_bar_target: $uploadForm.find('.progress')
     click_submit_target: null
     allow_multiple_files: true
     used_fields: ['utf8', 'key', 'acl', 'AWSAccessKeyId', 'policy', 'signature', 'success_action_status', 'X-Requested-With', 'content-type', 'file', 'x-amz-server-side-encryption']
-    url: null
     disable_fields_after_submit: null
     allow_send_form_without_file: false

@@ -37,27 +36,37 @@
     disable_fields: 'textarea,input[type=text],input[type=checkbox],select'
     file_name_for_upload: '#file_name_for_upload'

-#  enable_all_field: ->
-#    $(@selectors.s3_form).find(@selectors.disable_fields).removeAttr('disabled')
-#
-#  reset_s3_uploder: ->
-#    $('.upload_uploading').hide()
-#    $('.upload_picking').show()
-#    $('.progress-bar').removeAttr('style')
-#
-#  show_submit_button: ->
-#    $(@selectors.submit).show()
-#    $(@selectors.cancel_upload).addClass('hide')
-#
-#  show_cancel_button: ->
-#    $(@selectors.submit).hide()
-#    $(@selectors.cancel_upload).removeClass('hide')
-
   $.extend settings, options

   current_files = []
   forms_for_submit = []
   validation_form = true
+
+  $uploadForm.find('.file-field').on 'change', ->
+    # Populate thumb
+    if this.files && this.files[0] && /^image/.test(this.files[0].type) && window.FileReader isnt 'undefined' && Modernizr.canvas
+      # Browser supports File and Canvas APIs
+      file = this.files[0]
+
+      loadImage.parseMetaData file, (img) ->
+        # Get orientation
+        ornt = if img.exif? then img.exif.get("Orientation") else 1
+
+        # Set thumbnail
+        loadImage file, ((img) ->
+            imgElem = $(img)
+            imgElem.addClass 'img-responsive'
+            container = $("#upload_thumbnail").parent()
+            container.children().remove()
+            container.css 'text-align': 'center'
+            container.append imgElem
+          ),
+          maxHeight: 90
+          orientation: ornt
+          canvas: true
+    else
+      # Fallback; just use a placeholder
+      $('#upload_thumbnail').show().attr('src', assetPath('media/mrx-placeholder-120x90.png'))

   if settings.click_submit_target
     settings.click_submit_target.click =>
@@ -89,7 +98,7 @@

   setUploadForm = ->
     $uploadForm.fileupload
-      url: settings.url
+      url: $uploadForm.data('s3-url')
       add: (e, data) ->
         file = data.files[0]
         file.unique_id = Math.random().toString(36).substr(2,16)


Handle correctly manual submission of forms with users' answers

[#73893586]

E.g. when you manually call .submit() instead of relying on
auto-save feature.

--- app/assets/javascripts/save-on-change.coffee
+++ app/assets/javascripts/save-on-change.coffee
@@ -78,20 +78,36 @@
     @scheduled_job      = null
     @previous_value     = null
     @setupEvents()
+    @setupManualSubmitSupport()
     # don't send events until the doc
     # is ready
     $(document).ready () =>
       @checker = new CompleteChecker(@$form)
       @check_for_answer()

-
   setupEvents: ->
     @$form.find('input,textarea,select').on 'change blur', (e) =>
       @schedule(@changeInterval)
     @$form.find('input,textarea').on 'keyup',  (e) =>
       @schedule(@keyUpinterval)

-  saveElement:(async = true) ->
+  setupManualSubmitSupport: ->
+    # We can manually call form.submit() and everything should be handled as expected:
+    # - saving notification is displayed
+    # - answer is announced ('answer_for' and 'no_answer_for' events)
+    previous_data = null
+    @$form
+      .on('submit', =>
+        # This happens prior to the actual submission
+        previous_data = @$form.serialize()
+        @page.saving()
+      ).on('ajax:success', (e, data, status, xhr) =>
+        @save_success(previous_data)
+      ).on('ajax:error', (e, xhr, status, error) =>
+        @save_error()
+      )
+
+  saveElement: (async = true) ->
     data = @$form.serialize()
     @page.saving()
     $.ajax({
@@ -100,13 +116,19 @@
       url: @$form.attr( 'action' ),
       data: @$form.serialize(),
       success: (response) =>
-        @previous_value = data
-        @page.saved(@)
-        @dirty = false
-        @check_for_answer()
+        @save_success(data)
       error: (jqxhr, status, error) =>
-        @page.failed(@)
+        @save_error()
     })
+
+  save_success: (previous_data) ->
+    @previous_value = previous_data
+    @page.saved(@)
+    @dirty = false
+    @check_for_answer()
+
+  save_error: ->
+    @page.failed(@)

   check_for_answer: ->
     if @checker.is_answered()


disable debug for VocalEditor

disable debug for VocalEditor

--- app/assets/javascripts/vocal/vocal.js.coffee
+++ app/assets/javascripts/vocal/vocal.js.coffee
@@ -37,8 +37,7 @@
   highlight: (code, lang, callback) ->
     Prism.highlight(code, Prism.languages[lang])

-
-editor = new VocalEditor('.markdown', '.preview', true)
+editor = new VocalEditor('.markdown', '.preview')
 editor.init()

 # This is a helper to remove duplicated code


Parse the entire packet as JSON or forward as-is

--- components/ParseJson.coffee
+++ components/ParseJson.coffee
@@ -1,7 +1,7 @@
-noflo = require("noflo")
-_ = require("underscore")
+noflo = require "noflo"
+_ = require "underscore"

-class TryParseJson extends noflo.Component
+class ParseJson extends noflo.Component
   constructor: ->
     @inPorts =
       in: new noflo.Port()
@@ -9,16 +9,13 @@
       out: new noflo.Port()

     @inPorts.in.on "begingroup", (group) =>
-      @outPorts.out.beginGroup(group)
+      @outPorts.out.beginGroup group

     @inPorts.in.on "data", (data) =>
-      if _.isObject(data)
-        for key, value of data
-          try
-            data[key] = JSON.parse(value)
-          catch e
-
-      @outPorts.out.send(data)
+      try
+        @outPorts.out.send JSON.parse data
+      catch e
+        @outPorts.out.send data

     @inPorts.in.on "endgroup", =>
       @outPorts.out.endGroup()
@@ -26,4 +23,4 @@
     @inPorts.in.on "disconnect", =>
       @outPorts.out.disconnect()

-exports.getComponent = -> new TryParseJson
+exports.getComponent = -> new ParseJson


Add file upload date on attachment date

Add file upload date on attachment date

--- app/coffee/modules/common/attachments.coffee
+++ app/coffee/modules/common/attachments.coffee
@@ -267,7 +267,7 @@
 AttachmentDirective = ->
     template = _.template("""

-        <a href="<%- url %>" title="<%- name %>" target="_blank">
+        <a href="<%- url %>" title="<%- name %> uploaded on <%- created_date %>" target="_blank">

             <%- name %>

@@ -291,7 +291,7 @@
     templateEdit = _.template("""

-        <a href="<%- url %>" title="<%- name %>" target="_blank"><%- name %>
+        <a href="<%- url %>" title="<%- name %> uploaded on <%- created_date %>" target="_blank"><%- name %>

         <%- size %>
@@ -320,6 +320,7 @@
             ctx = {
                 id: attachment.id
                 name: attachment.name
+                created_date: moment(attachment.created_date).format("DD MMM YYYY [at] hh:mm") #TODO: i18n
                 url: attachment.url
                 size: sizeFormat(attachment.size)
                 description: attachment.description


keeping track of what files are compiling so that things aren't recompiled fifty bajillion times

--- src/fs_utils/file_list.coffee
+++ src/fs_utils/file_list.coffee
@@ -15,7 +15,7 @@
 module.exports = class FileList extends EventEmitter
   # Maximum time between changes of two files that will be considered
   # as a one compilation.
-  RESET_TIME: 65
+  RESET_TIME: 165

   constructor: (@config) ->
     @files = []
@@ -23,6 +23,7 @@
     @on 'change', @_change
     @on 'unlink', @_unlink
     @compiling = []
+    @compiled = []
     @copying = []

   getAssetErrors: ->
@@ -70,6 +71,7 @@

       if @compiling.length is 0 and @copying.length is 0
         @emit 'ready'
+        @compiled = []
       else
         @resetTimer()
     , @RESET_TIME
@@ -86,16 +88,22 @@
         dependent.dependencies.length > 0
       .filter (dependent) =>
         path in dependent.dependencies
+      .filter (dependent) =>
+        dependent not in @compiled
       .forEach(@compile)

   compile: (file) =>
-    @compiling.push(file)
-    file.compile (error) =>
-      @compiling.splice @compiling.indexOf(file), 1
+    if @compiling.indexOf(file) == -1
+      @compiling.push(file)
+      file.compile (error) =>
+        @compiling.splice @compiling.indexOf(file), 1
+        @resetTimer()
+        return if error?
+        debug "Compiled file '#{file.path}'..."
+        @compiled.push file
+        @compileDependentFiles file.path
+    else
       @resetTimer()
-      return if error?
-      debug "Compiled file '#{file.path}'"
-      @compileDependentFiles file.path

   copy: (asset) =>
     @copying.push asset


Support copy, cut, and plain text pastes

--- src/trix/input.coffee
+++ src/trix/input.coffee
@@ -1,5 +1,5 @@
 class Trix.Input
-  @events: "keydown keypress drop cut copy paste input".split(" ")
+  @events: "keydown keypress drop cut paste input".split(" ")
   @keys:
     0x08: "backspace"
     0x0D: "return"
@@ -12,9 +12,10 @@
     if keyName = @constructor.keys[event.keyCode]
       if handler = @[keyName]
         handler.call this, event
-        event.preventDefault()

   keypress: (event) =>
+    return if event.metaKey
+
     if event.which is null
       character = String.fromCharCode event.keyCode
     else if event.which isnt 0 and event.charCode isnt 0
@@ -28,13 +29,12 @@
     @logAndCancel(event)

   cut: (event) =>
-    @logAndCancel(event)
-
-  copy: (event) =>
-    @logAndCancel(event)
+    @responder?.deleteBackward()

   paste: (event) =>
-    @logAndCancel(event)
+    if text = event.clipboardData.getData("text/plain")
+      @responder?.insertString(text)
+    event.preventDefault()

   input: (event) =>
     @responder?.render()
@@ -42,9 +42,11 @@

   backspace: (event) =>
     @responder?.deleteBackward()
+    event.preventDefault()

   return: (event) =>
     @responder?.insertString("\n")
+    event.preventDefault()

   logAndCancel: (event) =>
     console.log "trapped event:", event


uniform composable opinion views

uniform composable opinion views

--- @client/opinion_views.coffee
+++ @client/opinion_views.coffee
@@ -3,38 +3,131 @@

-update_opinion_views = (filter) ->
-  users = fetch '/users'
-  filter_out = fetch 'filtered'
-
-  return if !users.users
+save
+  key: 'opinion_views'
+  active_views: {}
+
+activate_opinion_view = (view) ->
+
+  opinion_views = fetch('opinion_views')
+  opinion_views.active_views ?= {}
+  active_views = opinion_views.active_views
+
+  # TODO: In the future, we'll allow conjunctive views, but
+  #       for now, only allow one at a time
+  to_delete = []
+  for k,v of active_views
+    if v.created_by == 'activate_opinion_view'
+      to_delete.push k
+  for k in to_delete
+    delete active_views[k]
+
+  view_name = view.key or view.label
+
+  active_views[view_name] =
+    created_by: 'activate_opinion_view'
+    get_salience: (u, opinion, proposal) ->
+      if view.salience?
+        view.salience u, opinion, proposal
+      else if !view.pass || view.pass(u, opinion, proposal)
+        1
+      else if opinion_views.enable_comparison
+        .1
+      else
+        0
+    get_weight: (u, opinion, proposal) ->
+      if view.weight?
+        view.weight(u, opinion, proposal)
+      else if opinion_views.enable_comparison || view.pass?(u, opinion, proposal)
+        1
+      else
+        0
+
+  invalidate_proposal_sorts()
+  save opinion_views
+
+
+
+opinion_view_cache = {}
+window.compose_opinion_views = (opinions, proposal, opinion_views) ->
+  opinion_views ?= fetch('opinion_views')
+  opinion_views.active_views ?= {}
+  active_views = opinion_views.active_views
+
+  if !opinions
+    opinions = opinionsForProposal(proposal)
+
+  weights = {}
+  salience = {}
+  groups = {}
+
+  for o in opinions
+    weight = 1
+    min_salience = 1
+    groups = []
+    u = o.user.key or o.user
+
+    for view_name, view of active_views
+      if view.get_salience?
+        s = view.get_salience(u, o, proposal)
+        if s < min_salience
+          min_salience = s
+      if view.get_weight?
+        weight *= view.get_weight(u, o, proposal)
+      if view.groups?
+        groups.concat view.groups(u, o, proposal)
+    weights[u] = weight
+    salience[u] = min_salience
+    groups[u] = groups
+  {weights, salience, groups}

-  filter_out.users = {}
-
-  if filter_out.current_filter == filter
-    filter_out.current_filter = null
-  else
-    filter_out.current_filter = filter
-
-  filter_func = filter_out.current_filter?.pass
-
-  if filter_func
-    for user in users.users
-      passes_filter = filter_func(user)
-      if !passes_filter
-        filter_out.users[user.key] = 1
-
-  invalidate_proposal_sorts()
-  save filter_out
-
-set_comparison_mode = (enabled) ->
-  filter_out = fetch 'filtered'
-
-  if enabled == null # toggle if argument not passed
-    filter_out.enable_comparison = !filter_out.enable_comparison
-  else
-    filter_out.enable_comparison = enabled
-  save filter_out
+
+window.get_opinions_for_proposal = (opinions, proposal, weights) ->
+  if !opinions
+    opinions = opinionsForProposal proposal
+  if !weights
+    {weights, salience, groups} = compose_opinion_views(opinions, proposal)
+
+  (o for o in opinions when weights[o.user] > 0)
+
+
+
+default_views =
+  everyone:
+    key: 'everyone'
+    label: 'everyone'
+    tooltip: null
+    pass: (u) -> true
+
+  just_you:
+    key: 'just_you'
+    label: 'just you'
+    tooltip: null
+    pass: (u) ->
+      user = fetch(u)
+      user.key == fetch('/current_user').user
+
+  weighed_by_activity:
+    key: 'weighed_by_activity'
+    label: 'weighed by reasons given'
+    weight: (u, opinion, proposal) ->
+      point_inclusions = opinion.point_inclusions?.length or 0
+      .1 + point_inclusions
+
+  weighed_by_recency:
+    key: 'weighed_by_recency'
+    label: 'weighed by recency'
+    weight: (u, opinion, proposal) ->
+      if !proposal.time_created
+        proposal.time_created = new Date(proposal.created_at).getTime()
+      earliest = proposal.time_created
+
+      ot = new Date(opinion.updated_at).getTime()
+
+      time_since_creation = ot - earliest
+
+      # based on days since creation
+      Math.log .1 + time_since_creation / (1000 * 60 * 60 * 24)

 OpinionViews = ReactiveComponent
@@ -42,28 +135,17 @@

   render : ->

-    filter_out = fetch 'filtered'
-    users = fetch '/users' # fetched here so its ready for update_opinion_views func
-
-    return DIV null if !users.users
     custom_filters = customization 'opinion_views'
+    opinion_views = fetch 'opinion_views'

     is_admin = fetch('/current_user').is_admin

-    filters = [{
-                label: 'just you'
-                tooltip: null
-                pass: (u) ->
-                  user = fetch(u)
-                  user.key == fetch('/current_user').user
-              }]
+    filters = [default_views.just_you]

     if !customization('hide_opinions') || is_admin
-      filters.unshift {
-        label: 'everyone'
-        tooltip: null
-        pass: (u) -> true
-      }
+      filters.unshift default_views.everyone
+      filters.push default_views.weighed_by_activity
+      filters.push default_views.weighed_by_recency

     if custom_filters && !customization('hide_opinions')
@@ -71,7 +153,7 @@
         if filter.visibility == 'open' || is_admin
           filters.push filter

-    if !filter_out.current_filter
+    if !@local.current_filter
       dfault = customization('opinion_filters_default') or 'everyone'
       initial_filter = null
       for filter in filters
@@ -81,9 +163,11 @@

       initial_filter ||= filters[0]

-      update_opinion_views initial_filter
-
-    current_filter = filter_out.current_filter
+      activate_opinion_view initial_filter
+      @local.current_filter = initial_filter
+      save @local
+
+    current_filter = @local.current_filter

     DIV
       style: (@props.style or {})
@@ -102,10 +186,7 @@
             # fontSize: 20
             position: 'relative'

-          if true || current_filter?.label in ['everyone', 'just you']
-            TRANSLATE "engage.opinion_filter.label", 'show opinion of'
-          else
-            TRANSLATE "engage.opinion_filter.label_short", 'showing'
+          TRANSLATE "engage.opinion_filter.label", 'show opinion of'

           ": "

@@ -114,11 +195,14 @@

             open_menu_on: 'activation'

-            selection_made_callback: update_opinion_views
+            selection_made_callback: (filter) =>
+              activate_opinion_view filter
+              @local.current_filter = filter
+              save @local

             render_anchor: ->

-              key = if current_filter?.label not in ['everyone', 'just you'] then "/translations/#{fetch('/subdomain').name}" else null
+              key = if (current_filter?.key or current_filter?.label) not in ['everyone', 'just_you', 'weighed_by_activity', 'weighed_by_recency'] then "/translations/#{fetch('/subdomain').name}" else null
               SPAN
                 style:
                   fontWeight: 'bold'
@@ -188,7 +272,7 @@
               color: 'white'
               backgroundColor: focus_color()

-          if current_filter.label != 'everyone' && (is_admin || !customization('hide_opinions'))
+          if current_filter.key != 'everyone' && (is_admin || !customization('hide_opinions'))
             DIV
               style: @props.enable_comparison_wrapper_style or {}

@@ -196,10 +280,16 @@
               INPUT
                 type: 'checkbox'
                 id: 'enable_comparison'
-                checked: filter_out.enable_comparison
+                checked: opinion_views.enable_comparison
                 ref: 'enable_comparison'
                 onChange: =>
-                  set_comparison_mode @refs.enable_comparison.getDOMNode().checked
+                  enabled = @refs.enable_comparison.getDOMNode().checked
+
+                  if enabled == null # toggle if argument not passed
+                    opinion_views.enable_comparison = !opinion_views.enable_comparison
+                  else
+                    opinion_views.enable_comparison = enabled
+                  save opinion_views

               LABEL
                 htmlFor: 'enable_comparison'
@@ -364,48 +454,48 @@
                       user.tags.verified

-window.opinion_trickle = ->
-  filter_out = fetch 'filtered'
+# window.opinion_trickle = ->
+#   filter_out = fetch 'filtered'

-  proposals = fetch '/proposals'
-
-  users = {}
-  for prop in proposals.proposals
-    for o in prop.opinions
-      users[o.user] = o.created_at or o.updated_at
-
-  users = ([k,v] for k,v of users)
-  users.sort (a,b) ->
-    i = new Date(a[1])
-    j = new Date(b[1])
-    i.getTime() - j.getTime()
-
-  users = (u[0] for u in users)
-  cnt = users.length
-
-  steps = 1
-  tick = (interval) =>
-    if cnt >= 0
-      setTimeout =>
-        filter_out.users = {}
-        for user, idx in users
-          filter_out.users[user] = 1
-          break if idx > cnt
-
-        cnt--
-        #cnt -= Math.ceil(steps / 2)
-        #tick(interval * .9)
-        tick(interval * .9)
-        steps++
-        dirty = true
-        setTimeout ->
-          if dirty
-            save filter_out
-            dirty = false
-        , 2000
-      , interval
-
-  tick 1000
+#   proposals = fetch '/proposals'
+
+#   users = {}
+#   for prop in proposals.proposals
+#     for o in prop.opinions
+#       users[o.user] = o.created_at or o.updated_at
+
+#   users = ([k,v] for k,v of users)
+#   users.sort (a,b) ->
+#     i = new Date(a[1])
+#     j = new Date(b[1])
+#     i.getTime() - j.getTime()
+
+#   users = (u[0] for u in users)
+#   cnt = users.length
+
+#   steps = 1
+#   tick = (interval) =>
+#     if cnt >= 0
+#       setTimeout =>
+#         filter_out.users = {}
+#         for user, idx in users
+#           filter_out.users[user] = 1
+#           break if idx > cnt
+
+#         cnt--
+#         #cnt -= Math.ceil(steps / 2)
+#         #tick(interval * .9)
+#         tick(interval * .9)
+#         steps++
+#         dirty = true
+#         setTimeout ->
+#           if dirty
+#             save filter_out
+#             dirty = false
+#         , 2000
+#       , interval
+
+#   tick 1000

 window.OpinionViews = OpinionViews


fix for resizing bug

fix for resizing bug

--- @client/proposal_item.coffee
+++ @client/proposal_item.coffee
@@ -240,7 +240,7 @@

   .opinion-block-wrapper {
-    padding-right: 48px;
+    // padding-right: 48px;

     position: relative;
     z-index: 1;
@@ -258,20 +258,21 @@

   .bottom_closer {
     position: absolute;
-    left: calc(50% - 20px);
+    left: calc(50% - 26px);
     bottom: -26px;
     cursor: pointer;
     background-color: transparent;
     border: none;
-  }
-  .is_expanded .bottom_closer {
+    opacity: 0;
+  }
+
+  :not(.expanding).is_expanded .bottom_closer {
     transition: opacity #{2 * ANIMATION_SPEED_ITEM_EXPANSION}s;
     opacity: 1;
     display: inline-block;
   }
   .is_collapsed .bottom_closer {
     pointer-events: none;
-    opacity: 0;
   }

@@ -438,14 +439,6 @@
     flex-shrink: 1;
   }

-
-  .debugging .proposal-text {
-    background-color: #aaa;
-  }
-
-  .debugging .proposal-avatar-spacing, .debugging .proposal-left-spacing {
-    background-color: green;
-  }

 """

@@ -624,14 +617,6 @@
     border-color: #000;
   }

-
-
-
-
-
-
-
-
   .ProposalText .proposal-description-wrapper {
     max-width: 600px;
   }
@@ -672,20 +657,6 @@
   }
   .ProposalItem:hover .edit_and_delete_block, .is_expanded .edit_and_delete_block {
     opacity: 1;
-  }
-
-
-
-  .debugging .ProposalText .proposal-title {
-    background-color: #aa7711;
-  }
-
-  .debugging .ProposalText .proposal-description-wrapper {
-    background-color: #f1f1f1;
-  }
-
-  .debugging .ProposalText .proposal-metadata {
-    background-color: #11aa77;
   }

 """
@@ -868,11 +839,20 @@

   setCollapsedSizes: (expand_after_set) ->
-    return if !@waitForFonts(=> @setCollapsedSizes(expand_after_set)) || @is_expanded || @local.collapsed_title_height? || !@local.in_viewport #!@refs.root.closest(".ProposalItem[data-in-viewport='true']")
+    return if !@waitForFonts(=> @setCollapsedSizes(expand_after_set)) || @local.collapsed_title_height? || !@local.in_viewport #!@refs.root.closest(".ProposalItem[data-in-viewport='true']")

-    title_el = @refs.proposal_title
-    collapsed_item_width ?= title_el.clientWidth
-    @local.collapsed_title_height = title_el.clientHeight
+    title_el = @refs.proposal_title_text
+
+    if !@is_expanded && !collapsed_item_width?
+      collapsed_item_width = title_el.clientWidth
+
+
+    if @is_expanded
+      @local.collapsed_title_height = title_el.getBoundingClientRect().height
+
+    else
+      @local.collapsed_title_height = title_el.clientHeight
+
     save @local

     # wait to make the update so that we don't continuously trigger layout reflow
@@ -1084,7 +1064,7 @@
           proposal_editing.callback = =>
             delete @local.collapsed_title_height
             save @local
-
+
           save proposal_editing

           e.stopPropagation()


Resurrection: groups appcontroller restructured

--- client/landing/Groups/AppController.coffee
+++ client/landing/Groups/AppController.coffee
@@ -53,64 +53,6 @@
       if pageInfo.topLevel
       then router.handleRoute "#{pageInfo.path}"
       else router.handleRoute "#{pageInfo.path}", {entryPoint}
-
-    @groups = {}
-    @currentGroupData = new GroupData
-
-  getCurrentGroup:->
-    throw 'FIXME: array should never be passed'  if Array.isArray @currentGroupData.data
-    return @currentGroupData.data
-
-  openGroupChannel:(group, callback=->)->
-    @groupChannel = KD.remote.subscribe "group.#{group.slug}",
-      serviceType : 'group'
-      group       : group.slug
-      isExclusive : yes
-
-    @forwardEvent @groupChannel, "MemberJoinedGroup"
-    @forwardEvent @groupChannel, "FollowHappened"
-
-    @groupChannel.once 'setSecretNames', callback
-
-  changeGroup:(groupName='', callback=->)->
-
-    groupName or= 'koding' # KD.defaultSlug
-    return callback()  if @currentGroupName is groupName
-
-    oldGroupName        = @currentGroupName
-    @currentGroupName   = groupName
-
-    unless groupName is oldGroupName
-      KD.remote.cacheable groupName, (err, models)=>
-        if err then callback err
-        else if models?
-          [group] = models
-          if group.bongo_.constructorName isnt 'JGroup'
-            @isReady = yes
-          else
-            @setGroup groupName
-            @currentGroupData.setGroup group
-            @isReady = yes
-            callback null, groupName, group
-            @emit 'GroupChanged', groupName, group
-            @openGroupChannel group, => @emit 'GroupChannelReady'
-
-  getUserArea:->
-    @userArea ? group:
-      if KD.config.entryPoint?.type is 'group'
-      then KD.config.entryPoint.slug
-      else (KD.getSingleton 'groupsController').currentGroupName
-
-  setUserArea:(userArea)->
-    @userArea = userArea
-
-  getGroupSlug:-> @currentGroupName
-
-  setGroup:(groupName)->
-    @currentGroupName = groupName
-    @setUserArea {
-      group: groupName, user: KD.whoami().profile.nickname
-    }

   onboardingText =
     everything : """
@@ -357,25 +299,6 @@
               modal.destroy()
               callback null

-  joinGroup:(group, callback)->
-    group.join (err, response)=>
-      unless err
-        callback err, response
-        KD.track "Groups", "JoinedGroup", group.slug
-        KD.getSingleton('mainController').emit 'JoinedGroup'
-
-  acceptInvitation:(group, callback)->
-    KD.whoami().acceptInvitation group, (err, res)=>
-      mainController = KD.getSingleton "mainController"
-      mainController.once "AccountChanged", callback.bind this, err, res
-      mainController.accountChanged KD.whoami()
-
-  ignoreInvitation:(group, callback)->
-    KD.whoami().ignoreInvitation group, callback
-
-  cancelGroupRequest:(group, callback)->
-    KD.whoami().cancelRequest group, callback
-
   openPrivateGroup:(group)->
     group.canOpenGroup (err, hasPermission)=>
       if err
@@ -432,15 +355,6 @@

       modal = new KDModalView modalOptions

-  cancelMembershipPolicyChange:(policy, membershipPolicyView, modal)->
-    membershipPolicyView.enableInvitations.setValue policy.invitationsEnabled
-
-  updateMembershipPolicy:(group, policy, formData, membershipPolicyView, callback)->
-    group.modifyMembershipPolicy formData, (err)->
-      unless err
-        policy.emit 'MembershipPolicyChangeSaved'
-        new KDNotificationView {title:"Membership policy has been updated."}
-      KD.showError err

   editPermissions:(group)->
     group.getData().fetchPermissions (err, permissionSet)->


Tests: Collaboration: Remove before hook.

--- client/test/lib/collaboration/start.coffee
+++ client/test/lib/collaboration/start.coffee
@@ -62,8 +62,6 @@

 module.exports =

-  before: -> utils.generateUsers()
-
   start: (browser) ->

     firstUser    = utils.getUser no, 0


apply forward

apply forward

--- src/layout/test_layout.coffee
+++ src/layout/test_layout.coffee
@@ -74,7 +74,7 @@
     forward = sprite.get('language')['forward']
     message = {target: sprite, action: forward}
     t.ok sprite.call('apply', message), "apply message"
-    # test_position t, [2,1]
+    test_position t, [2,1]
     t.end()

   test "layout inspector", (t) ->


serv.coffee: Call with the correct parameters

--- code/serv.coffee
+++ code/serv.coffee
@@ -1,2 +1,3 @@
 server = require 'server'
-server.start()
+server.start (err, app) ->
+  console.log "Server started."


limit line grouping in @5eb0c50e163a67f114047f7064d5be0aa555338a to groups of lines that do not intersect and thus are safe to group

implemented using dirty rectangles since actually performing intersection would be too slow

--- AI2ASS.coffee
+++ AI2ASS.coffee
@@ -82,8 +82,59 @@
   output = {
     pathCnt: null
     processedPathCnt: 0
-
-    assSections: []
+    layers: []
+
+    makeLayer: (emptyPrefix) ->
+      layer = {
+        groups: []
+        currGroupIdx: -1
+        currGroup: null
+        emptyPrefix: null
+
+        makeMergeGroup: ->
+          {
+            dirtyRects: []
+            lines: {}
+
+            addPath: (path, prefix) ->
+              unless @isZeroArea path.visibleBounds
+                @dirtyRects.push path.visibleBounds
+                drawing = ASS_createDrawingFromPoints path.pathPoints
+
+                if @lines[prefix]?
+                  Array.prototype.push.apply @lines[prefix], drawing
+                else @lines[prefix] = drawing
+
+            isZeroArea: (bounds) ->
+              return bounds[2]-bounds[0] == 0 and bounds[3]-bounds[1] == 0
+
+            isMergeable: (path) ->
+              bounds = path.visibleBounds
+
+              if @isZeroArea bounds
+                return true
+
+              for rect in @dirtyRects
+                if bounds[2] > rect[0] and bounds[0] < rect[2] and bounds[3] < rect[1] and bounds[1] > rect[3]
+                  return false
+
+              return true
+          }
+
+        addGroup: ->
+          @currGroupIdx += 1
+          @currGroup = @makeMergeGroup()
+          @groups[@currGroupIdx] = @currGroup
+
+        addPath: (path, prefix) ->
+          unless @currGroup.isMergeable path
+            @addGroup()
+          @currGroup.addPath path, prefix
+      }
+
+      layer.addGroup()
+      layer.emptyPrefix = emptyPrefix
+      return layer

     process: ( obj, clip, opacity = 100 ) ->
       if not @pathCnt?
@@ -94,15 +145,18 @@

         switch obj.typename
           when "Document"
-            for layer in obj.layers
+            for layer in obj.layers by -1
               @process layer

           when "Layer"
-            for subPageItem in obj.pageItems
-              @process subPageItem, null, opacity
+            if obj.pageItems.length == 0
+              @layers[obj.zOrderPosition] = @makeLayer @emptyPrefix obj.zOrderPosition, obj.name
+            else
+              for subPageItem in obj.pageItems by -1
+                @process subPageItem, null, opacity

           when "CompoundPathItem"
-            for path in obj.pathItems
+            for path in obj.pathItems by -1
               @process path, clip, opacity

           when "GroupItem"
@@ -115,7 +169,7 @@
                 clip = makeClip clipPath
               @processedPathCnt += 1

-            for subPageItem in obj.pageItems when not subPageItem.clipping
+            for subPageItem in obj.pageItems by -1 when not subPageItem.clipping
               @process subPageItem, clip, opacity

           when "PathItem"
@@ -138,31 +192,33 @@

       prefix = @prefix stroke, fill, clip, alpha, layerNum, layerName

-      if not @assSections[layerNum]?
-        @assSections[layerNum] = {}
-
-      if not @assSections[layerNum][prefix]?
-        @assSections[layerNum][prefix] = []
-
-      Array.prototype.push.apply @assSections[layerNum][prefix], ASS_createDrawingFromPoints path.pathPoints
+      layer = @layers[layerNum]
+      unless layer?
+        layer = @makeLayer()
+        @layers[layerNum] = layer
+
+      layer.addPath path, prefix

     prefix: (stroke, fill, clip, alpha) ->
       "{\\an7\\pos(0,0)#{stroke}#{fill}#{alpha}#{clip}\\p1}"

+    emptyPrefix: -> ""
     suffix: -> "{\\p0}"

-    get: (includeEmptyLayers = false) ->
+    get: (includeEmptyLayers) ->
       fragments = []
       suffix = @suffix()

-      for shapes, layerNum in @assSections
-        if shapes?
-          for prefix, paths of shapes
+      for layer in @layers when layer?
+        if includeEmptyLayers && layer.emptyPrefix?
+            fragments.push layer.emptyPrefix
+            fragments.push "\n"
+
+        for mergeGroup in layer.groups
+          for prefix, drawing of mergeGroup.lines
             fragments.push prefix
-            fragments.push paths.join " "
+            fragments.push drawing.join " "
             fragments.push suffix
-            fragments.push "\n"
-        else if includeEmptyLayers
             fragments.push "\n"

       fragments.pop()
@@ -226,6 +282,8 @@
       output.prefix = (stroke, fill, clip, alpha, layerNum, layerName) ->
         "Dialogue: #{layerNum},0:00:00.00,0:00:00.00,AI,#{layerName},0,0,0,,{\\an7\\pos(0,0)#{stroke}#{fill}#{alpha}#{clip}\\p1}"
       output.suffix = -> ""
+      output.emptyPrefix = (layerNum, layerName) ->
+        "Dialogue: #{layerNum},0:00:00.00,0:00:00.00,AI,#{layerName},0,0,0,,"

   alert "Your colorspace needs to be RGB if you want colors." if doc.documentColorSpace == DocumentColorSpace.CMYK


Fix autofolding with new file message links

--- client/message.coffee
+++ client/message.coffee
@@ -224,14 +224,10 @@
   attachment = @data.file
   #images = Session.get 'images'
   subimages = @images = []
-  $(@firstNode).children('.panel-body').children('.message-file').find('img[src^="/file/"]')
-  .each ->
-    id = url2file @getAttribute('src')
-    #console.log 'target', id
-    initImage id
-    images[id].attachment = tid
-    if images[id].count > 0 and not messageFolded.get(images[id].attachment)?
-      messageFolded.set images[id].attachment, true
+  initImage tid
+  images[tid].attachment = tid
+  if images[tid].count > 0 and not messageFolded.get tid
+    messageFolded.set tid, true
   ## If message is deleted or otherwise default-folded,
   ## don't check for images it references.
   unless messageFolded.get @data._id


Inject showdown

Inject showdown

--- client/src/app.coffee
+++ client/src/app.coffee
@@ -2,12 +2,10 @@
 jquery = require 'jquery';
 window.jQuery = jquery;
 window.$ = jquery;
-lodash = require 'lodash'
-window._ = lodash
-moment = require 'moment'
-window.moment = moment
-d3 = require 'd3'
-window.d3 = d3
+window._ = require 'lodash'
+window.moment = require 'moment'
+window.d3 = require 'd3'
+window.showdown = require 'showdown'

 require 'angular-material/angular-material.min.css'
 require 'angular-material-data-table/dist/md-data-table.min.css'
@@ -36,7 +34,7 @@
 require 'd3-bdc'
 require 'c3'
 require 'MimeJS/dist/mime-js.js'
-require 'showdown'
+
 require 'highcharts'


Remove debugging log statement

Remove debugging log statement

--- src/d3.timeslider.coffee
+++ src/d3.timeslider.coffee
@@ -28,7 +28,6 @@

         # create a custom formatter for labeling ticks
         customFormatter = (formats) =>
-            console.log formats
             (date) ->
                 i = formats.length - 1
                 f = formats[i]


Removing peripheral extensions from Corelib

--- src/debug_console.coffee
+++ src/debug_console.coffee
@@ -1,38 +0,0 @@
-DebugConsole = () ->
-  REPL = (input, output) ->
-    print = (message) ->
-      output.append($ "<li />",
-        text: message
-      )
-
-    run: () ->
-      source = input.val()
-      try
-        code = CoffeeScript.compile source, noWrap: true
-        result = eval(code)
-        print " => #{result}"
-        input.val('')
-      catch error
-        if error.stack
-          print error.stack
-        else
-          print error.toString()
-
-  container = $ "<div />",
-    class: "console"
-
-  input = $ "<textarea />"
-  output = $ "<ul />"
-  runButton = $ "<button />",
-    text: "Run"
-
-  repl = REPL(input, output)
-
-  container.append(output).append(input).append(runButton)
-
-  $ ->
-    runButton.click () ->
-      repl.run()
-
-    $("body").append(container)
-


Changed tsclient to use the errormessage callback when getting a batch finished message

--- turkserver-js-client/src/tsclient.coffee
+++ turkserver-js-client/src/tsclient.coffee
@@ -252,7 +252,8 @@
         # disconnect from the server and tell worker to return the hit
         @unsubscribe()
         $.cometd.disconnect()
-        alert(data.msg)
+        @errorMessage_cb?(status, data.msg)
+        # alert(data.msg)

   @submitHIT: (data) =>
     @channelSend "/service/user",