should pass error, not string

should pass error, not string

--- test/test-worker-client.coffee
+++ test/test-worker-client.coffee
@@ -191,7 +191,7 @@
         new Worker 'test_shutdown_completion', (payload, worker) ->
           worker.parent.shutdown( ->
             console.warn "shutting down"
-            cb "called shutdown method before done called" if worker.parent.work_in_progress
+            cb new Error("called shutdown method before done called") if worker.parent.work_in_progress
           )
           assert.equal payload, 1, 'first worker should only have the chance to work on first job'
           setTimeout (-> worker.done()), 2000 # more than the timeout for re-checking done-ness
@@ -219,7 +219,7 @@
         new Worker 'test_err_shutdown_completion', (payload, worker) ->
           worker.parent.shutdown( ->
             console.warn 'err shutting down'
-            cb "called shutdown method before done called" if worker.parent.work_in_progress
+            cb new Error("called shutdown method before done called") if worker.parent.work_in_progress
           )
           assert.equal payload, 1, 'first worker should only have the chance to work on first job'
           setTimeout (-> worker.done("Worker Error")), 2000 # more than the timeout for re-checking done-ness


Use new ubiquitous token format

Use new ubiquitous token format

--- grammar.coffee
+++ grammar.coffee
@@ -10,9 +10,7 @@
 module.exports = compiler.grammar
   name: 'c',

-  separators: [' ', '\t', '\r', '\n']
-
-  ubiquitous: ["comment"]
+  ubiquitous: -> [@comment, /\s/]

   rules:
     program: -> repeat(choice(


Disallowing Splats outside of ParamLists and ArgLists ... where they belong. This is in anticipation of the next commit...

--- grammar.coffee
+++ grammar.coffee
@@ -97,7 +97,6 @@
     o "Switch"
     o "Extends"
     o "Class"
-    o "Splat"
     o "Existence"
     o "Comment"
   ]
@@ -361,10 +360,16 @@
   # (i.e. comma-separated expressions). Newlines work as well.
   ArgList: [
     o "",                                       -> []
-    o "Expression",                             -> [$1]
-    o "ArgList , Expression",                   -> $1.concat [$3]
-    o "ArgList OptComma TERMINATOR Expression", -> $1.concat [$4]
+    o "Arg",                                    -> [$1]
+    o "ArgList , Arg",                          -> $1.concat [$3]
+    o "ArgList OptComma TERMINATOR Arg",        -> $1.concat [$4]
     o "ArgList OptComma INDENT ArgList OptComma OUTDENT", -> $1.concat $4
+  ]
+
+  # Valid arguments are Expressions or Splats.
+  Arg: [
+    o "Expression"
+    o "Splat"
   ]

   # Just simple, comma-separated, required arguments (no fancy syntax). We need


JGroup: update edit groups permissions

JGroup: update edit groups permissions

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -19,8 +19,8 @@
   { throttle, extend }     = require 'underscore'

   PERMISSION_EDIT_GROUPS = [
-    { permission: 'edit groups' }
-    { permission: 'edit own groups', validateWith: Validators.own }
+    { permission: 'edit groups',     superadmin: yes }
+    { permission: 'edit own groups', validateWith: Validators.group.admin }
   ]

   @trait __dirname, '../../traits/filterable'


Slugifable: Let Slugifable.slugify works

--- workers/social/lib/social/traits/slugifiable.coffee
+++ workers/social/lib/social/traits/slugifiable.coffee
@@ -61,6 +61,8 @@
       slug.replace(/^-+|-+$/g, "")

     return slug
+
+  @slugify = slugify

   getNextCount =(name)->            # the name is something like `name: "foo-bar-42"`
     count = name


comments

comments

--- workers/social/lib/social/traits/slugifiable.coffee
+++ workers/social/lib/social/traits/slugifiable.coffee
@@ -14,34 +14,29 @@
       .replace(/^-+|-+$/g, "")      # trim leading and trailing hyphens
       .substr 0, 256                # limit these to 256-chars (pre-suffix), for sanity

-  getNextCount =(name)->
+  getNextCount =(name)->            # the name is something like `name: "foo-bar-42"`
     count = name
       .map ({name})->
-        [d] = (/\d+$/.exec name) ? [0]
-        +d
+        [d] = (/\d+$/.exec name) ? [0]; +d # take the digit part, cast it to a number.
       .sort (a, b)->
         a - b
-      .pop()
-    if isNaN count then ''
-    else count + 1
+      .pop()                        # the last item is the highest, pop from the tmp array
+    if isNaN count then ''          # show empty string instead of zero...
+    else "-#{count + 1}"            # otherwise, try the next integer.

   generateUniqueSlug =(konstructor, slug, i, template, callback)->
     [callback, template] = [template, callback]  unless callback
     template or= '#{slug}'
     JName = require '../models/name'
-    nameRE = RegExp "^#{template.replace '#\{slug\}', slug}(-\\d+)?$"
-    selector = {name:nameRE}
+    selector = name: RegExp "^#{template.replace '#\{slug\}', slug}(-\\d+)?$"
     JName.someData selector, {name:1}, {sort:name:-1}, (err, cursor)->
       if err then callback err
       else cursor.toArray (err, names)->
         if err then callback err
         else
           nextCount = getNextCount names
-          nextName = "#{slug}#{
-            if nextCount is '' then '' else "-#{nextCount}"
-          }"
+          nextName = "#{slug}#{nextCount}"
           nextNameFull = template.replace '#{slug}', nextName
-          # selector = {name: nextName, constructorName, usedAsPath: 'slug'}
           JName.claim nextNameFull, konstructor, 'slug', (err, nameDoc)->
             if err?.code is 11000
               console.log err


login: fix position of email input placeholder in register form when input is autofilled

--- client/Login/Views/registerform.coffee
+++ client/Login/Views/registerform.coffee
@@ -148,6 +148,7 @@
       {origin} = invite
       @invitationCode.input.setValue invite.code
       @email.input.setValue invite.email
+      @email.placeholder.setClass "out"
       if origin.constructorName is 'JAccount'# instanceof KD.remote.api.JAccount
         KD.remote.cacheable [origin], (err, [account])=>
           @addSubView new AvatarStaticView({size: width : 30, height : 30}, account), '.invited-by .wrapper'


Social: implement all api functions for SocialMessage

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -34,13 +34,17 @@
         return callback err if err
         return callback null, mapChannel result

-  message :
-   edit :(rest..., callback)->
-    KD.remote.api.SocialMessage.edit rest..., (err, res)->
+  messageApiFunc = (name, rest..., callback)->
+    KD.remote.api.SocialMessage[name] rest..., (err, res)->
       return callback null, mapActivity res
-   post :(rest..., callback)->
-    KD.remote.api.SocialMessage.post rest..., (err, res)->
-      return callback null, mapActivity res
+
+  message:
+   edit   :(rest...)-> messageApiFunc 'edit', rest...
+   post   :(rest...)-> messageApiFunc 'post', rest...
+   reply  :(rest...)-> messageApiFunc 'reply', rest...
+   delete :(rest...)-> KD.remote.api.SocialMessage.delete rest...
+   like   :(rest...)-> KD.remote.api.SocialMessage.like rest...
+   unlike :(rest...)-> KD.remote.api.SocialMessage.unlike rest...

   mapActivities = (messages)->
     # if no result, no need to do something


client: don't turn null values into immutable lists

--- client/app/lib/util/toImmutable.coffee
+++ client/app/lib/util/toImmutable.coffee
@@ -3,6 +3,7 @@
 module.exports = toImmutable = (js) ->

   return js  if typeof js isnt 'object'
+  return js  if js is null

   seq = immutable.Seq(js).map(toImmutable)


Cite my source.

Cite my source.

--- client/js/concerns/team_parsing.coffee
+++ client/js/concerns/team_parsing.coffee
@@ -1,3 +1,6 @@
+# Ported from Pokemon Showdown:
+# https://github.com/Zarel/Pokemon-Showdown-Client/blob/8beb563042cac1acb64ec88c552ef21fb28f0f39/js/client-teambuilder.js#L1236-L1352
+
 @BattleTower ?= {}
 @BattleTower.parseTeam = (teamString) ->
   text = teamString.split('\n')


OrderTotal removed

OrderTotal removed

--- client/skr/components/OrderTotals.cjsx
+++ client/skr/components/OrderTotals.cjsx
@@ -1,14 +0,0 @@
-class Skr.Components.OrderTotals extends Lanes.React.Component
-
-    propTypes:
-        model: Lanes.PropTypes.Model.isRequired
-
-    render: ->
-        <BS.Row className='order-total'>
-            <BS.Col md=10 className="text-right">
-                Total:
-            </BS.Col>
-            <BS.Col md=2>
-                <Skr.Components.Currency amount={@model.total} />
-            </BS.Col>
-        </BS.Row>


Use proxy_pass'd api

Use proxy_pass'd api

--- project.coffee
+++ project.coffee
@@ -3,8 +3,16 @@
 team_page = require './templates/team-page'
 team = require './content/team'

+[apiHost, apiProxyPath] = if window.location.hostname is 'www.orchidobservers.org'
+  ['http://www.orchidobservers.org', '/_ouroboros_api/proxy']
+else
+  [null, null]
+
 module.exports =
   id: 'orchid'
+  apiHost: apiHost
+  apiProxyPath: apiProxyPath
+
   background: 'background.jpg'
   subjectGroup: false


TAP5-2225: properly handle ElementWrapper instances

--- tapestry-core/src/main/preprocessed-coffeescript/org/apache/tapestry5/t5-core-dom.coffee
+++ tapestry-core/src/main/preprocessed-coffeescript/org/apache/tapestry5/t5-core-dom.coffee
@@ -906,11 +906,13 @@

     eventName = eventName.toLowerCase()

-    if element instanceof jQuery
+    if element is null
+      element = document.body
+    else if element instanceof jQuery
       element = element[0];
-
-    if element is null
-      element = document.getElementsByTagName('body')[0]
+    else if element instanceof ElementWrapper
+      element = element.element;
+

     # Look for event data in itself first, then in the preceding siblings
     # if not found


stop testing config

stop testing config

--- apps/home/test/routes.coffee
+++ apps/home/test/routes.coffee
@@ -33,10 +33,7 @@
     routes.sendLinkViaSMS { body: { phone_number: '555 111 2222' } }, { json: resStub = sinon.stub() }

   it 'sends a link with a valid phone number', ->
-    twilioConstructorArgs[0].length.should.be.above 5
-    twilioConstructorArgs[1].length.should.be.above 5
     twilioSendSmsArgs[0].to.should.equal '555 111 2222'
-    twilioSendSmsArgs[0].from.should.equal '(917) 746-8750'
     twilioSendSmsArgs[0].body.should.include "Download the new Artsy iPhone app here: "
     twilioSendSmsArgs[1] null, 'SUCCESS!'
     resStub.args[0][1].message.should.include 'Message sent.'


reset all

reset all

--- resources/assets/coffee/_classes/forum.coffee
+++ resources/assets/coffee/_classes/forum.coffee
@@ -189,10 +189,9 @@
     @highlightPost post

-  highlightPost: (post) =>
-    @highlightedPost?.classList.remove 'js-forum-post--highlighted'
-    @highlightedPost = post
-    @highlightedPost?.classList.add 'js-forum-post--highlighted'
+  highlightPost: (post) ->
+    $('.js-forum-post--highlighted').removeClass('js-forum-post--highlighted')
+    $(post).addClass('js-forum-post--highlighted')

   initialScrollTo: =>


Do not force string as setting value

Do not force string as setting value

--- packages/rocketchat-lib/server/methods/saveSetting.coffee
+++ packages/rocketchat-lib/server/methods/saveSetting.coffee
@@ -2,7 +2,6 @@
 	saveSetting: (_id, value) ->

 		check _id, String
-		check value, String

 		if Meteor.userId()?
 			user = Meteor.users.findOne Meteor.userId()


Listen to input changes

Listen to input changes

--- assets/js/gimme/gimme.coffee
+++ assets/js/gimme/gimme.coffee
@@ -23,6 +23,21 @@
 			url: window.originalImage
 	)

+	colorInput = document.getElementById "colorTextInput"
+	intensityInput = document.getElementById "intensityInput"
+	colorValue = colorInput.value
+	intensityValue = parseInt intensityInput.value
+
+	onColorParamsChanged = () ->
+		if @type == "text" then colorValue = @value else intensityValue = parseInt @value || 0
+		console.log @type, colorValue, intensityValue
+
+		camanImage.colorize(colorValue, intensityValue).render();
+
+	["paste", "change"].forEach (eventName) ->
+		colorInput.addEventListener eventName, onColorParamsChanged, false
+		intensityInput.addEventListener eventName, onColorParamsChanged, false
+
 	Caman.remoteProxy = "proxy";
 	Caman "#uploaded-marker", () ->
 		@colorize("#ff0000", 80)


Tweak 21da3fcafbb29540788037dbcdbdce79ad14e650.

--- content_scripts/mode_key_handler.coffee
+++ content_scripts/mode_key_handler.coffee
@@ -56,8 +56,7 @@
       # We will possibly be handling a subsequent keypress event, so suppress propagation of this event to
       # prevent triggering page event listeners (e.g. Google instant Search).
       @keydownEvents[event.keyCode] = true
-      DomUtils.suppressPropagation event
-      @passEventToPage
+      @suppressPropagation
     else
       @continueBubbling

@@ -76,8 +75,7 @@
   onKeyup: (event) ->
     return @continueBubbling unless event.keyCode of @keydownEvents
     delete @keydownEvents[event.keyCode]
-    DomUtils.suppressPropagation event
-    @passEventToPage
+    @suppressPropagation

   # This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).
   isMappedKey: (keyChar) ->


Visual/edit modes: miscellaneous improvements.

--- content_scripts/mode_visual_edit.coffee
+++ content_scripts/mode_visual_edit.coffee
@@ -144,6 +144,7 @@
     @commands = {}
     @alterMethod = options.alterMethod || "extend"
     @keyQueue = ""
+    @keyPressCount = 0
     @yankedText = ""
     super options

@@ -160,6 +161,7 @@
     @push
       _name: "#{@id}/keypress"
       keypress: (event) =>
+        @keyPressCount += 1
         unless event.metaKey or event.ctrlKey or event.altKey
           @keyQueue += String.fromCharCode event.charCode
           # We allow at most three characters for a command or movement mapping.
@@ -274,6 +276,7 @@
       when "Caret"
         # Try to start with a visible selection.
         @moveInDirection(forward) or @moveInDirection backward unless options.underEditMode
+        @scrollIntoView() if @selection.type == "Range"

     defaults =
       name: "visual"
@@ -284,25 +287,33 @@
     super extend defaults, options

     extend @commands,
+      "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange()
       "y": ->
         # Special case: "yy" (the first from edit mode, and now the second).
-        @selectLine() if @options.yYanksLine
+        @selectLine() if @options.yYanksLine and @keyPressCount == 1
         @yank()
-      "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange()

     if @options.underEditMode
       extend @commands,
-        "d": -> @yank deleteFromDocument: true
         "c": -> @yank(); enterInsertMode()
-    else
+        "d": ->
+          # Special case: "dd" (the first from edit mode, and now the second).
+          @selectLine() if @options.dYanksLine and @keyPressCount == 1
+          @yank deleteFromDocument: true
+
+    unless @options.underEditMode
       @installFindMode()

+    # Grab the initial clipboard contents.  We'll try to keep them intact until we get an explicit yank.
     @clipboardContents = ""
-    @paste (text) => @clipboardContents = text
+    @paste (text) =>
+      @clipboardContents = text if text
+    #
+    # End of VisualMode constructor.

   protectClipboard: (func) ->
     func()
-    @copy @clipboardContents
+    @copy @clipboardContents if @clipboardContents

   copy: (text) ->
     super @clipboardContents = text
@@ -399,10 +410,8 @@

       "Y": -> @enterVisualMode runMovement: "Y"
       "y": => @enterVisualMode yYanksLine: true
-      "d": => @enterVisualMode deleteFromDocument: true
-      "c": => @enterVisualMode
-        deleteFromDocument: true
-        onYank: enterInsertMode
+      "d": => @enterVisualMode deleteFromDocument: true, dYanksLine: true
+      "c": => @enterVisualMode deleteFromDocument: true, onYank: enterInsertMode

       "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true
       "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode
@@ -420,6 +429,7 @@

   enterVisualMode: (options = {}) ->
     defaults =
+      badge: ""
       underEditMode: true
       initialCount: @countPrefix
       oneMovementOnly: true


Send e-mail on first username set

Send e-mail on first username set

--- packages/rocketchat-lib/server/functions/setUsername.coffee
+++ packages/rocketchat-lib/server/functions/setUsername.coffee
@@ -15,6 +15,10 @@
 		return false

 	previousUsername = user.username
+
+	# If first time setting username, send Enrollment Email
+	if not previousUsername and RocketChat.settings.get 'Accounts_Enrollment_Email'
+		Accounts.sendEnrollmentEmail(user._id)

 	# Username is available; if coming from old username, update all references
 	if previousUsername


Change to process.exit(1) to restart server

--- packages/rocketchat-lib/server/methods/restartServer.coffee
+++ packages/rocketchat-lib/server/methods/restartServer.coffee
@@ -7,7 +7,7 @@
 			throw new Meteor.Error 'not-authorized', '[methods] restart_server -> Not authorized'

 		Meteor.setTimeout ->
-			process.exit(0)
+			process.exit(1)
 		, 2000

 		return {} =


call `setUserInteraction` as part of `updateSearchResult`

--- resources/assets/coffee/_classes/store-supporter-tag.coffee
+++ resources/assets/coffee/_classes/store-supporter-tag.coffee
@@ -84,7 +84,6 @@

     .always =>
       @searching = false
-      @setUserInteraction(@user?)
       @updateCart(@user)
       @updateSearchResult()

@@ -100,8 +99,11 @@
     if !@searching
       @searching = true
       @updateSearchResult()
-      @setUserInteraction(false)
     @debouncedGetUser(event.currentTarget.value)
+
+  setUserInteraction: (enabled) =>
+    $(@el).toggleClass('store-supporter-tag--disabled', !enabled)
+    $('.js-slider').slider({ 'disabled': !enabled })

   sliderValue: (price) ->
     price * @RESOLUTION
@@ -113,7 +115,9 @@
     $('#product-form').data 'disabled', disabled

   updateSearchResult: =>
-    return @inputFeedback.textContent = 'searching' if @searching
+    if @searching
+      @inputFeedback.textContent = 'searching'
+      return @setUserInteraction(false)

     [avatarUrl, text] = if @user
                           [@user.avatarUrl, '']
@@ -124,6 +128,7 @@
     $(@el.querySelectorAll('.js-avatar')).css(
       'background-image': "url(#{avatarUrl})"
     )
+    @setUserInteraction(@user?)

   updatePrice: (obj) =>
     @el.querySelector('input[name="item[cost]"').value = obj.price()
@@ -132,10 +137,6 @@
     @durationElement.textContent = obj.durationText()
     @discountElement.textContent = "save #{obj.discount()}%"

-  setUserInteraction: (enabled) =>
-    $(@el).toggleClass('store-supporter-tag--disabled', !enabled)
-    $('.js-slider').slider({ 'disabled': !enabled })
-
   updateSliderPresets: (values) =>
     @updateSliderPreset(elem, values) for elem in @sliderPresets


Always show beatmapset discussion link if enabled

--- resources/assets/coffee/react/beatmapset-page/header.coffee
+++ resources/assets/coffee/react/beatmapset-page/header.coffee
@@ -81,76 +81,71 @@

           el BeatmapsetMapping, beatmapset: @props.beatmapset

-          if currentUser.id?
-            [
-              if @props.beatmapset.availability
-                div
-                  key: 'availability'
-                  className: 'beatmapset-header__availability-info',
-                  if @props.beatmapset.availability.download_disabled
-                    osu.trans 'beatmapsets.availability.disabled'
-                  else
-                    osu.trans 'beatmapsets.availability.parts-removed'
+          if currentUser.id? && @props.beatmapset.availability
+            div
+              className: 'beatmapset-header__availability-info',
+              if @props.beatmapset.availability.download_disabled
+                osu.trans 'beatmapsets.availability.disabled'
+              else
+                osu.trans 'beatmapsets.availability.parts-removed'

-                  if @props.beatmapset.availability.more_information
-                    div className: 'beatmapset-header__availability-link',
-                      a href: @props.beatmapset.availability.more_information, target: '_blank', osu.trans 'beatmapsets.availability.more-info'
+              if @props.beatmapset.availability.more_information
+                div className: 'beatmapset-header__availability-link',
+                  a href: @props.beatmapset.availability.more_information, target: '_blank', osu.trans 'beatmapsets.availability.more-info'

-              div
-                key: 'buttons'
-                className: 'beatmapset-header__buttons'
+          div
+            className: 'beatmapset-header__buttons'

-                el BigButton,
-                  props:
-                    onClick: @toggleFavourite
-                    href:
-                      laroute.route 'beatmapsets.update-favourite',
-                        beatmapset: @props.beatmapset.id
-                        action: favouriteButton.action
-                    title: osu.trans "beatmapsets.show.details.#{favouriteButton.action}"
-                  modifiers: ['beatmapset-header-square', "beatmapset-header-square-#{favouriteButton.action}"]
-                  icon: favouriteButton.icon
+            if currentUser.id?
+              el BigButton,
+                props:
+                  onClick: @toggleFavourite
+                  href:
+                    laroute.route 'beatmapsets.update-favourite',
+                      beatmapset: @props.beatmapset.id
+                      action: favouriteButton.action
+                  title: osu.trans "beatmapsets.show.details.#{favouriteButton.action}"
+                modifiers: ['beatmapset-header-square', "beatmapset-header-square-#{favouriteButton.action}"]
+                icon: favouriteButton.icon

-                unless @props.beatmapset.availability?.download_disabled
+            if currentUser.id? && !@props.beatmapset.availability?.download_disabled
+              [
+                if @props.beatmapset.video
                   [
-                    if @props.beatmapset.video
-                      [
-                        @downloadButton
-                          key: 'video'
-                          href: laroute.route 'beatmapsets.download', beatmapset: @props.beatmapset.id
-                          bottomTextKey: 'video'
-
-                        @downloadButton
-                          key: 'no-video'
-                          href: laroute.route 'beatmapsets.download', beatmapset: @props.beatmapset.id, noVideo: 1
-                          bottomTextKey: 'no-video'
-                      ]
-                    else
-                      @downloadButton
-                        key: 'default'
-                        href: laroute.route 'beatmapsets.download', beatmapset: @props.beatmapset.id, noVideo: 1
+                    @downloadButton
+                      key: 'video'
+                      href: laroute.route 'beatmapsets.download', beatmapset: @props.beatmapset.id
+                      bottomTextKey: 'video'

                     @downloadButton
-                      key: 'direct'
-                      topTextKey: 'direct'
-                      osuDirect: true
-                      href:
-                        if currentUser.isSupporter
-                          Url.beatmapDownloadDirect @props.beatmapset.id
-                        else
-                          laroute.route 'support-the-game'
+                      key: 'no-video'
+                      href: laroute.route 'beatmapsets.download', beatmapset: @props.beatmapset.id, noVideo: 1
+                      bottomTextKey: 'no-video'
                   ]
+                else
+                  @downloadButton
+                    key: 'default'
+                    href: laroute.route 'beatmapsets.download', beatmapset: @props.beatmapset.id, noVideo: 1

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

         div className: 'beatmapset-header__box beatmapset-header__box--stats',
           el BeatmapsetPage.Stats,


fixed typo

fixed typo

--- generators/app/templates/scripts/states/boot.coffee
+++ generators/app/templates/scripts/states/boot.coffee
@@ -8,7 +8,7 @@
   create: ->

     @game.stage.backgroundColor = 0x000000
-    @scale.scaleMode = Phaser.ScaleManager.SHOW_AL
+    @scale.scaleMode = Phaser.ScaleManager.SHOW_ALL
     @game.state.start 'preload'


bootstrap-custom.js.coffee:removed scroll spy

--- app/assets/javascripts/bootstrap-custom.js.coffee
+++ app/assets/javascripts/bootstrap-custom.js.coffee
@@ -1,5 +1,3 @@
-$ ->
-  $(".topbar").scrollspy()
 $ ->
   $(".tabs").tabs()
 $ ->


Add WAI/ARIA states for filter accordeon

--- app/assets/javascripts/modules/filter_form.coffee
+++ app/assets/javascripts/modules/filter_form.coffee
@@ -16,18 +16,29 @@

     if !$('.filter-form__expander').length
       $filterForm.prepend '
' + expandLabel + '
'
+      $filterForm.prepend '
' + expandLabel + '
'

     $expander = $('.filter-form__expander')
+
+    $expander.attr 'aria-expanded', false

     $expander.on 'click', ->

       if $filterForm.hasClass('filter-form--isCollapsed')
         $expander.html collapseLabel
+        $expander
+            .html collapseLabel
+            .attr 'aria-expanded', true
+
         $filterForm
             .css 'height', 'auto'
             .removeClass 'filter-form--isCollapsed'
       else
         $expander.html expandLabel
+        $expander
+            .html expandLabel
+            .attr 'aria-expanded', false
+
         $filterForm
               .css 'height', startHeight
               .addClass 'filter-form--isCollapsed'


Show percentage change for 'change' metrics

--- app/assets/javascripts/prosperity/graph.js.coffee
+++ app/assets/javascripts/prosperity/graph.js.coffee
@@ -18,6 +18,8 @@
           xkey: "x"
           ykeys: []
           labels: []
+
+        chartOptions.postUnits = '%' if json.key == 'change'

         chart = new Morris.Line(chartOptions)
         data = []


Fix the debounce function

Fix the debounce function

--- app/assets/javascripts/rglossa/react/utils.coffee
+++ app/assets/javascripts/rglossa/react/utils.coffee
@@ -10,6 +10,7 @@
   # N milliseconds. If `immediate` is passed, trigger the function on the
   # leading edge, instead of the trailing.
   debounce: (func, wait, immediate) ->
+    timeout = null
     ->
       context = @
       args = arguments


Add back current_files.push (fix s3_uploads_complete callback)

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -43,11 +43,12 @@
         file.unique_id = Math.random().toString(36).substr(2,16)

         unless settings.before_add and not settings.before_add(file)
+          current_files.push data
           if $('#template-upload').length > 0
-            data.context = $($.trim(tmpl("template-upload", file)))
+            data.context = $($.trim(tmpl("template-upload", file)))
             $(data.context).appendTo(settings.progress_bar_target || $uploadForm)
           else if !setting.allow_multiple_files
-            data.context = settings.progress_bar_target
+            data.context = settings.progress_bar_target
           if settings.click_submit_target
             if settings.allow_multiple_files
               forms_for_submit.push data


Update sable_antelope.coffee

Update sable_antelope.coffee

--- app/lib/field-guide-content/sable_antelope.coffee
+++ app/lib/field-guide-content/sable_antelope.coffee
@@ -3,7 +3,7 @@
     The sable antelope is a rotund, barrel-chested antelope with a short neck, long face, and dark mane. They are best known for their impressive ringed horns that rise vertically and curve backward. Sable antelopes have a short, glossy coat that appears black in males and sorrel to rich chestnut in females and young. As they grow older, sable antelopes change their coloration. Calves are born reddish-brown, with virtually no markings. As they age, white markings will appear and their coat darkens.
   '''

-  scientificName: '(Hippotragus niger)'
+  scientificName: 'Hippotragus niger'
   mainImage: 'assets/fieldguide-content/mammals/sable_antelope/sable-feature.jpg'

   conservationStatus: 'Least Concern' # Options are Least Concern, Near Threatened, Vulnerable, and Endangered; all have their own little icons as well.


Tune fase 2 of bossfight

Tune fase 2 of bossfight

--- app/scripts/lazerscripts/stage1/stage1boss.coffee
+++ app/scripts/lazerscripts/stage1/stage1boss.coffee
@@ -47,10 +47,12 @@
       )
       @repeat 2, @sequence(
         @fireRockets(2, yes)
-        @wait 3000
+        @wait 600
         @fireRockets(2, yes)
         @wait 600
-        @fireRockets(2)
+        @fireRockets(2, yes)
+        @wait 900
+        @fireRockets(4)
         @wait 300
       )
     )
@@ -213,6 +215,10 @@

   bombRaid: (armed = no) ->
     @sequence(
+      @bigExplosion()
+      @wait 200
+      @bigExplosion()
+
       @moveTo(y: .1, speed: 200, easing: 'easeInOutQuad')
       @while(
         @moveTo(x: -100, speed: 400)
@@ -234,6 +240,7 @@
               location: @location()
               armed: armed
           )
+          @smoke('light')
           @wait 300
         )
       )


use path

use path

--- apps/articles/test/client/gallery_insights.coffee
+++ apps/articles/test/client/gallery_insights.coffee
@@ -2,6 +2,7 @@
 benv = require 'benv'
 sinon = require 'sinon'
 Backbone = require 'backbone'
+{ resolve } = require 'path'
 { stubChildClasses } = require '../../../../test/helpers/stubs'

 describe 'ArticleIndexView', ->
@@ -12,7 +13,8 @@
       $.fn.waypoint = sinon.stub()
       Backbone.$ = $
       $el = $('

')
-      @GalleryInsightsView = benv.require require.resolve '../../client/gallery_insights'
+      @GalleryInsightsView = benv.require resolve(
+        __dirname, '../../client/gallery_insights')
       stubChildClasses @GalleryInsightsView, this,
         ['CTABarView']
         ['previouslyDismissed', 'render', 'transitionIn', 'transitionOut']


Update en.coffee

Update en.coffee


Update zh-HANT.coffee

Update zh-HANT.coffee

--- app/locale/zh-HANT.coffee
+++ app/locale/zh-HANT.coffee
@@ -643,17 +643,17 @@
     autosave: "自動保存修改"
     me_tab: "我"
     picture_tab: "頭像"
-#    delete_account_tab: "Delete Your Account"
-#    wrong_email: "Wrong Email"
+    delete_account_tab: "刪除您的帳戶"
+    wrong_email: "錯誤的Email"
     upload_picture: "上傳圖片"
-#    delete_this_account: "Delete this account permanently"
+    delete_this_account: "永久性的刪除帳戶"
     god_mode: "上帝模式"
     password_tab: "密碼"
     emails_tab: "郵件"
     admin: "管理員"
     new_password: "新密碼"
     new_password_verify: "確認密碼"
-#    type_in_email: "Type in your email to confirm the deletion"
+    type_in_email: "輸入您的Email來確認刪除"
     email_subscriptions: "訂閱"
     email_subscriptions_none: "無Email訂閱"
     email_announcements: "通知"


chore: more fixtures

chore: more fixtures

--- app/models/product.coffee
+++ app/models/product.coffee
@@ -12,4 +12,14 @@
     name: 'Herp Derp'
     items: ['1-1', '1-2', '1-3', '1-4']
   }
+  {
+    id: 2
+    name: 'Derpy Herp'
+    items: []
+  }
+  {
+    id: 3
+    name: 'Duderp'
+    items: []
+  }
 ]


sortable table can be searched

sortable table can be searched

--- app/assets/javascripts/droom/sort.js.coffee
+++ app/assets/javascripts/droom/sort.js.coffee
@@ -8,21 +8,27 @@
     constructor: (element, opts) ->
       @table = $(element)
       @body = @table.find('tbody')
+      @search = $('input#q')
+
       @options = $.extend {}, opts
       @options.url ?= @body.attr("data-url") ? "/documents.js"
       @options.sort ?= @table.attr("data-sort") ? "created"
       @options.order ?= @table.attr("data-order") ? "desc"
-      @_original_content = @body.children()
+
       @sort = $.params("sort") ? @options.sort
       @order = $.params("order") ? @options.order
+      @query = null

       @headers = @table.find('th a.sorter')
       $.each @headers, (i, header) =>
         header = new SortLink header, @
         $.headers.push header
+
       @activate()
+      @table.bind "refresh", @update
+      @search.bind "keyup", @setQuery

-      @table.bind "refresh", @update
+      @_original_content = @body.children()
       if Modernizr.history
         $(window).bind 'popstate', @restoreState

@@ -31,12 +37,12 @@
       order ?= "asc"
       @sort = sort
       @order = order
-      @get("#{@options.url}?sort=#{sort}&order=#{order}")
-
+      @get("#{@options.url}?sort=#{sort}&order=#{order}&q=#{@query}")
+
     update: () =>
       @body.fadeTo('fast', 0.2)
       $.ajax
-        url: "#{@options.url}?sort=#{@sort}&order=#{@order}"
+        url: "#{@options.url}?sort=#{@sort}&order=#{@order}&q=#{@query}"
         dataType: "html"
         success: @display

@@ -86,7 +92,17 @@
         @display(event.state.html)
         $.each $.headers, (i, header) =>
           header.check()
-
+
+    setQuery: (e) =>
+      kc = e.which
+      console.log "setQuery", kc
+
+      #   delete,     backspace,    alphanumerics,    number pad,        punctuation
+      if (kc is 8) or (kc is 46) or (47 < kc < 91) or (96 < kc < 112) or (kc > 145)
+        @query = @search.val()
+        @update()
+
+

   class SortLink
     constructor: (element, table) ->


Fix incorrect leaderboard id.

Fix incorrect leaderboard id.

--- app/assets/javascripts/lib/core/base.coffee
+++ app/assets/javascripts/lib/core/base.coffee
@@ -32,7 +32,7 @@
         @auth.update()

     initAds: ->
-      AdManager.init(@adConfig(), 'js-ad-leaderboard') # Remove the second param when dropping the old ad manager
+      AdManager.init(@adConfig(), 'ad-leaderboard') # Remove the second param when dropping the old ad manager

     showUserBasket: ->
       shopCart = new ShoppingCart()


Use LocalStore to feature flag the new sign in*.

* As opposed to attempting a call to the new url which makes the error logs messy.

--- app/assets/javascripts/lib/core/base.coffee
+++ app/assets/javascripts/lib/core/base.coffee
@@ -5,7 +5,11 @@
   class Base

     constructor: (args={})->
-      @authenticateUser()
+      if LocalStore.get('lp-new-sign-in')
+        @authenticateUser()
+      else
+        @oldAuthenticateUser()
+
       @showUserBasket()
       # Note: We need to add this back in when the switch to the new DFP server happens
       # @initAds() unless args.secure
@@ -31,10 +35,7 @@
       $.ajax
         url: @auth.getNewStatusUrl()
         dataType: "json"
-        error: =>
-          # Fallback to the old method for the moment.
-          AssetFetch.get "https://secure.lonelyplanet.com/sign-in/status", () =>
-            @auth.update()
+        error: @oldAuthenticateUser(@auth)
         success: (user) =>
           # The data returned is defined in community at: app/controllers/users_controller.rb@status
           window.lp.user = user
@@ -47,6 +48,11 @@
           window.referer = "null";

           @auth.update()
+
+    oldAuthenticateUser: (auth) ->
+      @auth = auth || new Authenticator()
+      AssetFetch.get "https://secure.lonelyplanet.com/sign-in/status", () =>
+        @auth.update()

     initAds: ->
       AdManager.init(@adConfig(), 'ad-leaderboard') # Remove the second param when dropping the old ad manager


remove markers when they get out of range

--- app/assets/javascripts/map_marker.js.coffee
+++ app/assets/javascripts/map_marker.js.coffee
@@ -1,6 +1,7 @@
 initialize = ->
 	placeMarker = (location) ->
 		marker = new google.maps.Marker position: location, map: map
+		markers.push(marker)
 		google.maps.event.addListener marker, "click", (event) -> deleteMarker marker
 		return marker

@@ -8,22 +9,27 @@
 		$.post "map_marker/create", { lon: marker.position.lng(), lat: marker.position.lat() }, (data) ->
 			console.log "successfully  created marker"

+	removeMarker = (marker) ->
+		marker.setMap null
+		marker = null
+
 	deleteMarker = (marker) ->
-		marker.setMap null
 		$.ajax
 			url: "map_marker/delete"
 			type: "delete"
 			data:
 				lon: marker.position.lng()
 				lat: marker.position.lat()
-
 			success: (data) ->
+				removeMarker marker
 				console.log "successfully  deleted marker"

 	getMarkers = ->
 		$.get "map_marker/#{myCenter.lng()}/#{myCenter.lat()}/300000", (serverMarkers) ->
+			markersToRemove = _(markers).filter (m) -> _(serverMarkers).every (sm) -> m.position.lng() != sm.lon_lat.lon or m.position.lat() != sm.lon_lat.lat
 			_(serverMarkers).each (sm) -> placeMarker(new google.maps.LatLng(sm.lon_lat.lat, sm.lon_lat.lon))

+	markers = []
 	myCenter = new google.maps.LatLng(51.508742, -0.120850)
 	mapProp =
 		center: myCenter


join tank

join tank

--- app/client/player_lobby/player_lobby.coffee
+++ app/client/player_lobby/player_lobby.coffee
@@ -1,3 +1,4 @@
+TANK_SIZE = 100
 pickTank = (e) ->
   # debugger;
   tid = $(e.target).attr("tankid")
@@ -13,8 +14,9 @@
   tank.joinRoom(battle, battle._id)

   # init tank position (hard coded for 4 tanks)
-  w = $(window).width()
-  h = $(window).height()
+  w = battle.w
+  h = battle.h
+
   console.log("w =" + w + " h = " + h)
   if tid == 1
       #upper left
@@ -22,15 +24,15 @@
       console.log("tank " + tid)
   else if tid == 2
       #upper right
-      tank.setProps({top:0, left:w})
+      tank.setProps({top:0, left:w - TANK_SIZE})
       console.log("tank " + tid)
   else if tid == 3
       #lower left
-      tank.setProps({top:h, left:0})
+      tank.setProps({top:h - TANK_SIZE, left:0})
       console.log("tank " + tid)
   else if tid == 4
       #lower right
-      tank.setProps({top:h, left:w})
+      tank.setProps({top:h - TANK_SIZE, left:w - TANK_SIZE})
       console.log("tank " + tid)

   Router.go("/player_remote/#{tid}")


privatized variables

privatized variables

--- app/coffee/drm-calendar/drm-calendar.coffee
+++ app/coffee/drm-calendar/drm-calendar.coffee
@@ -521,34 +521,34 @@
         # get form data and return an object
         # need to remove dashes from ids
         formInput = {}
-        fields = form.find(':input').not('button').not ':checkbox'
-        checkboxes = form.find 'input:checked'
-
-        if checkboxes.length isnt 0
-            boxIds = []
-
-            checkboxes.each ->
-                boxIds.push $(@).attr 'id'
-
-            boxIds = $.unique boxIds
-
-            $.each boxIds, ->
-                checkboxValues = []
-                boxes = form.find "input:checked##{@}"
-
-                boxes.each ->
+        _fields = form.find(':input').not('button').not ':checkbox'
+        _checkboxes = form.find 'input:checked'
+
+        if _checkboxes.length isnt 0
+            _boxIds = []
+
+            _checkboxes.each ->
+                _boxIds.push $(@).attr 'id'
+
+            _boxIds = $.unique boxIds
+
+            $.each _boxIds, ->
+                _checkboxValues = []
+                _boxes = form.find "input:checked##{@}"
+
+                _boxes.each ->
                     checkboxValues.push $.trim($(@).val())

                 formInput["#{@}"] = checkboxValues
                 return

-        $.each fields, ->
+        $.each _fields, ->
             that = $ @
-            id = that.attr 'id'
-
-            input = if $.trim(that.val()) is '' then null else $.trim(that.val())
-
-            if input? then formInput["#{id}"] = input
+            _id = that.attr 'id'
+
+            _input = if $.trim(that.val()) is '' then null else $.trim(that.val())
+
+            if _input? then formInput["#{id}"] = _input
             return

         formInput
@@ -565,71 +565,71 @@

     getDayOfWeek: (month, date, year) ->
         # returns the day of the week for a specific date
-        day = new Date year, month, date
-        day.getDay()
+        _day = new Date year, month, date
+        _day.getDay()

     getWeeksInMonth: (month, year) =>
         # gets the number of weeks in a month
-        firstDay = @getDayOfWeek month, 1, year
-        numberDays = @getDaysInMonth month, year
-        dayShift = if firstDay is @daysPerWeek then 0 else firstDay
-        Math.ceil (numberDays + dayShift) / @daysPerWeek
+        _firstDay = @getDayOfWeek month, 1, year
+        _numberDays = @getDaysInMonth month, year
+        _dayShift = if _firstDay is @daysPerWeek then 0 else _firstDay
+        Math.ceil (_numberDays + _dayShift) / @daysPerWeek

     getMonthWeekNum: (dayNum, day, month, year) =>
         # gets the week of the month which an event occurs
-        weeks = @calendar.find("div.#{@calendarInnerClass}").find '.drm-week'
-        firstDay = @getDayOfWeek month, 1, year
-        dayShift = if firstDay is @daysPerWeek then 0 else firstDay
-        numberWeeks = @getWeeksInMonth month, year
-        lastWeekLength = weeks.eq(numberWeeks).length
-
-        if dayNum is 'last' and dayShift <= day
-            eventWeekNum = if lastWeekLength < day then (numberWeeks - 2) else numberWeeks - 1
-        else if dayNum is 'last' and dayShift > day
-            eventWeekNum = numberWeeks - 2
+        _weeks = @calendar.find("div.#{@calendarInnerClass}").find '.drm-week'
+        _firstDay = @getDayOfWeek month, 1, year
+        _dayShift = if _firstDay is @daysPerWeek then 0 else _firstDay
+        _numberWeeks = @getWeeksInMonth month, year
+        _lastWeekLength = weeks.eq(numberWeeks).length
+
+        if dayNum is 'last' and _dayShift <= day
+            eventWeekNum = if _lastWeekLength < day then (_numberWeeks - 2) else _numberWeeks - 1
+        else if dayNum is 'last' and _dayShift > day
+            eventWeekNum = _numberWeeks - 2
         else
             eventWeekNum = parseInt(dayNum, 10) - 1

-        return if dayShift <= day then eventWeekNum else eventWeekNum + 1
+        return if _dayShift <= day then eventWeekNum else eventWeekNum + 1

     getDatesInWeek: (month, newDate, year) =>
-        firstDay = @getDayOfWeek month, 1, year
-        numberDays = @getDaysInMonth month, year
-        dayShift = if firstDay is @daysPerWeek then 0 else firstDay
-        currentDay = @getDayOfWeek month, newDate, year
-        numberWeeks = @getWeeksInMonth month, year
+        _firstDay = @getDayOfWeek month, 1, year
+        _numberDays = @getDaysInMonth month, year
+        _dayShift = if _firstDay is @daysPerWeek then 0 else _firstDay
+        _currentDay = @getDayOfWeek month, newDate, year
+        _numberWeeks = @getWeeksInMonth month, year
         weekInfo = {}
         weekInfo.datesInWeek = []

-        firstWeek = []
-        lastWeek = []
-
-        daysInFirstWeek = @daysPerWeek - dayShift
+        _firstWeek = []
+        _lastWeek = []
+
+        _daysInFirstWeek = @daysPerWeek - _dayShift

         i = 1
         # get the number of dates in each week
-        while i <= numberWeeks
+        while _i <= _numberWeeks
             dates = []

-            if i is 1
+            if _i is 1
             # first week of the month
-                j = 0
+                _j = 0
                 while j < daysInFirstWeek
-                    j = j + 1
-                    dates.push j
+                    _j = _j + 1
+                    dates.push _j
             # middle weeks
-            else if i < numberWeeks
-                if i is 2 then date = daysInFirstWeek
-                j = 0
-                while j < @daysPerWeek
-                    j = j + 1
-                    date = date + 1
-                    dates.push date
-            else if i is numberWeeks
+            else if _i < _numberWeeks
+                if _i is 2 then _date = _daysInFirstWeek
+                _j = 0
+                while _j < @daysPerWeek
+                    _j = _j + 1
+                    _date = _date + 1
+                    dates.push _date
+            else if _i is _numberWeeks
             # last week in month
-                while date < numberDays
-                    date = date + 1
-                    dates.push date
+                while _date < _numberDays
+                    _date = _date + 1
+                    dates.push _date

             # get the week number
             if newDate in dates
@@ -641,27 +641,27 @@

     getWeekNumber: (month, newDate, year) =>
         self = @
-        weekNum = 1
+        _weekNum = 1
         weekNums = []
-        weekInfo = self.getDatesInWeek month, newDate, year
+        _weekInfo = self.getDatesInWeek month, newDate, year

         $.each self.months, (key) ->
-            numberDays = self.getDaysInMonth key, year
-            firstDay = self.getDayOfWeek key, 1, year
-            dayShift = if firstDay is self.daysPerWeek then 0 else firstDay
-            numberWeeks = self.getWeeksInMonth month, year
-            week = 1
-            if $.isNumeric numberWeeks
-                while week <= numberWeeks
-                    if week is 1 and firstDay isnt 0
-                        weekNum = weekNum
+            _numberDays = self.getDaysInMonth key, year
+            _firstDay = self.getDayOfWeek key, 1, year
+            _dayShift = if _firstDay is self.daysPerWeek then 0 else _firstDay
+            _numberWeeks = self.getWeeksInMonth month, year
+            _week = 1
+            if $.isNumeric _numberWeeks
+                while _week <= _numberWeeks
+                    if _week is 1 and _firstDay isnt 0
+                        _weekNum = _weekNum
                     else
-                        weekNum = weekNum + 1
-                    week = week + 1
+                        _weekNum = _weekNum + 1
+                    _week = _week + 1
                     if month is key
-                        weekNums.push weekNum
+                        weekNums.push _weekNum

-        weekNums[weekInfo.weekNum]
+        weekNums[_weekInfo.weekNum]

     createEvent: (newEvent) =>
         _id = @events.length


i18n Course Activity date fields

i18n Course Activity date fields

fixes: CNVS-11070

Fixes path to the type of date format that needs to be used.  It now uses an
absolute path vs a relative path.

Test Plan:
  - Create a course and update the start at and concluded at
  - Open course activity logs and make sure the dates are in the correct format
    2014-02-10T22:02:42Z 2014-02-10T22:02:42Z should show as Feb 10, 2014 3:02pm

Change-Id: I32f3825254bfcffcceeaa84399b3a89e4bfc75e8
Reviewed-on: https://gerrit.instructure.com/29910
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Brian Palmer <3cb4e1df5ec4da2c7c4af7c52cec8cf340a55a10@instructure.com>
QA-Review: August Thornton <9adc7a1161ddf32ff608de792a7e50179545f026@instructure.com>
Product-Review: Nick Cloward <9cc571ed222c7ca8c35f018ff48a612f8cadeb73@instructure.com>

--- app/coffeescripts/models/CourseEvent.coffee
+++ app/coffeescripts/models/CourseEvent.coffee
@@ -40,7 +40,7 @@
       if _.isString(value)
         return blank if !value.length
         if value.match /^\d{4}-\d{2}-\d{2}(T| )\d{2}:\d{2}:\d{2}(.\d+)?Z$/
-          return I18n.l("date.formats.medium", value) + " " + I18n.l("time.formats.tiny", value)
+          return I18n.l("#date.formats.medium", value) + " " + I18n.l("#time.formats.tiny", value)
       return value

     presentLabel: (label) ->


don't fetch group post csv-upload

don't fetch group post csv-upload

--- app/components/admin-page/admin-page.coffee
+++ app/components/admin-page/admin-page.coffee
@@ -1,4 +1,4 @@
-module.exports =
+module.exports =
   resolve:
     userValidated: ($auth) ->
       $auth.validateUser()
@@ -24,7 +24,7 @@

     $scope.newGroup = Records.groups.build()

-    $scope.openCreateGroupDialog = ->
+    $scope.openCreateGroupDialog = ->
       Dialog.custom
         scope: $scope
         template: require('./create-group-dialog-content.tmpl.html')
@@ -63,9 +63,6 @@
     $scope.uploadPathForGroup = (groupId) ->
       "#{config.apiPrefix}/allocations/upload?group_id=#{groupId}"

-    $scope.onCsvUploadSuccess = (groupId) ->
-      Records.groups.findOrFetchById(groupId)
-
     $scope.onCsvUploadCompletion = ->
       Dialog.alert(title: 'upload complete!')

@@ -77,4 +74,4 @@
     $scope.viewGroup = (groupId) ->
       $location.path("/groups/#{groupId}")

-    return+    return


Update wild_dog.coffee

Update wild_dog.coffee

--- app/lib/field-guide-content/wild_dog.coffee
+++ app/lib/field-guide-content/wild_dog.coffee
@@ -3,7 +3,7 @@
     The African wild dog, also called the African painted dog, has a colorful coat with patches of red, black, brown, white, and yellow fur; large, batlike ears; and a bushy tail. Their face is primarily black, with a tan forehead. While African wild dogs resemble domestic dogs, these long-legged canines have only four toes per foot, unlike other dogs, which have five toes on their forefeet.
   '''

-  scientificName: '(Lycaon pictus)'
+  scientificName: 'Lycaon pictus'
   mainImage: 'assets/fieldguide-content/mammals/wild_dog/wild-dog-feature.jpg'

   conservationStatus: 'Endangered' # Options are Least Concern, Near Threatened, Vulnerable, and Endangered; all have their own little icons as well.


Fixed typo

Fixed typo

--- vendor/assets/javascripts/nested-form.js.coffee
+++ vendor/assets/javascripts/nested-form.js.coffee
@@ -1,7 +1,8 @@
 class NestedForm
   constructor: (@selector) ->
     # the jquery element should be the field_wrapper
-    if ($(selector).length) { this.j_el = $(selector) }
+    if ($(selector).length)
+      this.j_el = $(selector)
     if (this.j_el).length > 0
       this.bind_directional_buttons()
       this.set_directional_button_ability()


Upgrade to HTML5 Notification

Upgrade to HTML5 Notification

--- coffee/app.coffee
+++ coffee/app.coffee
@@ -91,7 +91,7 @@
     teaSelection.init()
 )

-angular.module('tea').controller("ControlPanelController", ($scope, $timeout, teaSelection, localize) ->
+angular.module('tea').controller("ControlPanelController", ($scope, $timeout, teaSelection, localize, Notification) ->
     self = this

     $scope.radio = {index: 0}
@@ -131,11 +131,8 @@
         teaSelection.broadcastSelection(currentSelection)

     $scope.start = () ->
-        permission = window.webkitNotifications.checkPermission()
-        if permission != 0
-            window.webkitNotifications.requestPermission($scope.onTimerStart)
-        else
-            $scope.onTimerStart()
+        Notification.checkPermission()
+        $scope.onTimerStart()

     $scope.onTimerStart = () ->
         console.log("Starting timer for: " + $scope.time)
@@ -149,9 +146,6 @@
         $scope.displayName = $scope.tea.title
         $scope.actualTime = $scope.time
         $scope.timer = true
-        if (window.webkitNotifications.checkPermission() != 0)
-            $("#notification_disabled").show()
-            return

         $('#countdownModal').modal()
         $('#countdownModal').on('hidden', ->
@@ -172,7 +166,7 @@
             resetTitle()
             #Display notification only if timer enabled!
             if $scope.timer
-                Utils.displayNotification(localize.getLocalizedString('_AppTitle_'),
+                Notification.display(localize.getLocalizedString('_AppTitle_'),
                     localize.getLocalizedString('_NotificationMessage_'))

     updateTitle = (time) ->
@@ -213,6 +207,47 @@

     this.setFromSelection(teaSelection.getSelection())
+)
+
+angular.module('tea').service('Notification', ($timeout) ->
+    snd = new Audio('snd/alarm.mp3')
+    popup = null
+    service = {}
+
+    service.supported = () ->
+        Notification
+
+    service.checkPermission = () ->
+        Notification.requestPermission() unless Notification.permission is "granted"
+
+    service.display = (title, message) ->
+        popup = new Notification(title, {
+            body: message
+            icon: "img/icon.png"
+        })
+        snd.play() if SOUND
+
+        popup.onclick = () ->
+            popup.close()
+
+        popup.onclose = () ->
+            snd.pause()
+
+        $timeout(() ->
+            #hide notification and stop sound
+            cancel()
+        , CANCEL_TIMEOUT)
+
+    cancel = () ->
+        popup.close() if popup
+        snd.pause()
+
+
+    return service
+)
+
+angular.module('tea').controller("RootController", ($scope, Notification) ->
+    $scope.notificationSupported = Notification.supported
 )

 #Utils
@@ -242,40 +277,7 @@

         return convertedTemp.toString()

-    Utils.displayNotification = (title, message) ->
-        permission = window.webkitNotifications.checkPermission()
-        console.log("Permission: #{permission}")
-        window.popup = window.webkitNotifications.createNotification("img/icon.png",
-            title,
-            message)
-
-        Utils.ding('snd/alarm.mp3', window.popup)
-        popup.show()
-        setTimeout("popup.cancel()", CANCEL_TIMEOUT)
-
-    Utils.ding = (mp3, popup) ->
-        snd = new Audio(mp3)
-        if SOUND
-            snd.play()
-            ###
-             stop playing alert sound when notification popup is closed
-             (synchronized with notification popup through CANCEL_TIMEOUT
-             constant which is same in both cases)
-             ###
-            pauseAudio = ->
-                snd.pause()
-            popup.onclose = pauseAudio
-            popup.onclick = pauseAudio
-            setTimeout(pauseAudio, CANCEL_TIMEOUT)
-
     Utils.gaTrack = (tea, degree, time) ->
         _gaq.push(['_trackEvent', 'start-tea', tea])
         _gaq.push(['_trackEvent', 'degree', degree])
         _gaq.push(['_trackEvent', 'start-time', time])
-
-#Initialization code
-$(document).ready ->
-    #check webkit notification
-    if (not window.webkitNotifications &&  not Notification)
-        $("#notification_not_found").show()
-        $('#btn-run').toggleClass('disabled')


Fix coffee bugs

Fix coffee bugs

--- coffee/vis.coffee
+++ coffee/vis.coffee
@@ -22,16 +22,17 @@

   # using an ordinal scale for X as our
   # data is categorical (the names of countries)
-  xScale = d3.scale.ordinal()
-    .rangeRoundBands([0, graphWidth], 0.1)
+  # xScale = d3.scale.ordinal()
+  #   .rangeRoundBands([0, graphWidth], 0.1)
+
+  xScale = d3.scale.linear().range([0, graphWidth])

   # names will also be used to color the bars
-  colorScale = d3.scale.ordinal()
-    .range(["#ff7f0e", "#1f77b4", "#2ca02c", "#d62728", "#8c564b", "#9467bd"])
+  #colorScale = d3.scale.ordinal()
+  #  .range(["#ff7f0e", "#1f77b4", "#2ca02c", "#d62728", "#8c564b", "#9467bd"])

   # yPadding is removed to make room for country names
-  yScale = d3.scale.linear()
-    .range([0, graphHeight - yPadding])
+  yScale = d3.scale.linear().range([0, graphHeight - yPadding])

   # This is the amount by which we will enlarge the small chart
   # when displaying it in detail display
@@ -92,36 +93,46 @@
     # element which the barchart will
     # live in
     base = d3.select(this)
+
+		#set up the background
+
     base.append("rect")
       .attr("width", graphWidth)
       .attr("height", graphHeight)
       .attr("class", "background")

+    # create the dots for the scatter plot
+    graph = base.append("g")
+    graph.selectAll("circle")
+      .data((d) -> d.values) #d now contains exp_ratio, ten_year_return, index
+      .enter().append("circle") # the code below will execute for each attr on circle
+      .attr("cx", (d) -> xScale(d.exp_ratio))
+      .attr("cy", (d) -> (graphHeight - yScale(d.ten_year_return) - yPadding)
+      .attr("r", 10)
+
     # create the bars
-    graph = base.append("g")
-    graph.selectAll(".bar")
-      .data((d) -> d.values)
-      .enter().append("rect")
-      .attr("x", (d) -> xScale(d.name))
-      .attr("y", (d) -> (graphHeight - yScale(d.value) - yPadding))
-      .attr("width", xScale.rangeBand())
-      .attr("height", (d) ->  yScale(d.value))
-      .attr("fill", (d) -> colorScale(d.name))
-      .on("mouseover", showAnnotation)
-      .on("mouseout", hideAnnotation)
+    # graph = base.append("g")
+    # graph.selectAll(".bar")
+    #   .data((d) -> d.values)
+    #   .enter().append("rect")
+    #   .attr("x", (d) -> xScale(d.name)) #
+    #   .attr("y", (d) -> (graphHeight - yScale(d.value) - yPadding))
+    #   .attr("width", xScale.rangeBand())
+    #   .attr("height", (d) ->  yScale(d.value))
+    #   .attr("fill", (d) -> colorScale(d.name))
+    #   .on("mouseover", showAnnotation)
+    #   .on("mouseout", hideAnnotation)

     # add the year title
     graph.append("text")
-      .text((d) -> d.symbol)
+      .text((d) -> d.symbol) #change to d.symbol
       .attr("class", "title")
       .attr("text-anchor", "middle")
       .attr("x", graphWidth / 2)
       .attr("dy", "1.3em")

-  # ---
   # This creates the additional text displayed for
   # the detail view.
-  # ---
   drawDetails = (d,i) ->
     # like in 'drawChart', 'this'
     # is the group element to draw
@@ -243,20 +254,20 @@
   # Serves as example of simple additional interactions
   # in detail view
   # ---
-  showAnnotation = (d) ->
-    graph = d3.select("#detail_view .main")
-    graph.selectAll(".subtitle").remove()
-
-    graph.selectAll(".subtitle")
-      .data([d]).enter()
-      .append("text")
-      .text("#{formatNumber(d.percent_world * 100)}% of Worldwide Emissions")
-      .attr("class", "subtitle")
-      .attr("fill", (d) -> colorScale(d.name))
-      .attr("text-anchor", "middle")
-      .attr("dy", "3.8em")
-      .attr("x", (d) -> graphWidth / 2)
-      .attr("font-size", 8)
+  # showAnnotation = (d) ->
+  #   graph = d3.select("#detail_view .main")
+  #   graph.selectAll(".subtitle").remove()
+  #
+  #   graph.selectAll(".subtitle")
+  #     .data([d]).enter()
+  #     .append("text")
+  #     .text("#{formatNumber(d.percent_world * 100)}% of Worldwide Emissions")
+  #     .attr("class", "subtitle")
+  #     .attr("fill", (d) -> colorScale(d.name))
+  #     .attr("text-anchor", "middle")
+  #     .attr("dy", "3.8em")
+  #     .attr("x", (d) -> graphWidth / 2)
+  #     .attr("font-size", 8)

   # ---
   # remove subtitle
@@ -271,14 +282,23 @@
   # data.
   # ---
   setScales = () ->
-    yMax = d3.max(data, (d) -> d3.max(d.values, (e) -> e.value))
+    #yMax = d3.max(data, (d) -> d3.max(d.values, (e) -> e.value)) #change to e.ten_year_return
+
+		yMax = d3.max(data, (d) -> d3.max(d.values, (e) -> e.ten_year_return))
+
     # this scale is expanded past its max to provide some white space
     # on the top of the bars
-    yScale.domain([0,yMax + 500000])
-
-    names = data[0].values.map (d) -> d.name
-    xScale.domain(names)
-    colorScale.domain(names)
+    #yScale.domain([0,yMax + 500000]) #change yMax + 500000 to yMax + 5
+
+		yScale.domain([0,yMax + 5]) #change yMax + 500000 to yMax + 5
+
+		xMax = d3.max(data, (d) -> d3.max(d.values, (e) -> e.exp_ratio))
+
+		xScale.domain([0,xMax + 0.5])
+
+    # names = data[0].values.map (d) -> d.name
+    # xScale.domain(names)
+    # colorScale.domain(names)

   # ---
   # Helper function to return the position
@@ -290,27 +310,27 @@
     pos = $(el).position()
     pos

-  createLegend = () ->
-    legend = d3.select("#legend")
-      .append("svg")
-      .attr("width", 100)
-      .attr("height", 300)
-
-    keys = legend.selectAll("g")
-      .data(data[0].values)
-      .enter().append("g")
-      .attr("transform", (d,i) -> "translate(#{0},#{40 * (i + 1)})")
-
-    keys.append("rect")
-      .attr("width", 30)
-      .attr("height", 30)
-      .attr("fill", (d) -> colorScale(d.name))
-
-    keys.append("text")
-      .text((d) -> d.name)
-      .attr("text-anchor", "left")
-      .attr("dx", "2.2em")
-      .attr("dy", "1.2em")
+  # createLegend = () ->
+  #   legend = d3.select("#legend")
+  #     .append("svg")
+  #     .attr("width", 100)
+  #     .attr("height", 300)
+  #
+  #   keys = legend.selectAll("g")
+  #     .data(data[0].values)
+  #     .enter().append("g")
+  #     .attr("transform", (d,i) -> "translate(#{0},#{40 * (i + 1)})")
+  #
+  #   keys.append("rect")
+  #     .attr("width", 30)
+  #     .attr("height", 30)
+  #     .attr("fill", (d) -> colorScale(d.name))
+  #
+  #   keys.append("text")
+  #     .text((d) -> d.name)
+  #     .attr("text-anchor", "left")
+  #     .attr("dx", "2.2em")
+  #     .attr("dy", "1.2em")

   return chart

@@ -374,7 +394,6 @@
   display = (data) ->
     plotData("#vis", data, plot)

-  d3.json("data/co2_kt_data.json", display)
-	#d3.json("data/funds.json", display)
+  # d3.json("data/co2_kt_data.json", display)
+  d3.json("data/funds.json", display)

-


Fix initial step

Fix initial step

--- shepherd.coffee
+++ shepherd.coffee
@@ -13,6 +13,17 @@
   ->
     id++

+extend = (out={}) ->
+  args = []
+  Array::push.apply(args, arguments)
+
+  console.log args
+  for obj in args[1..] when obj
+    for own key, val of obj
+      out[key] = val
+
+  out
+
 createFromHTML = (html) ->
   el = document.createElement('div')
   el.innerHTML = html
@@ -50,7 +61,9 @@
     return false

 parseShorthand = (obj, props) ->
-  if typeof obj is 'object'
+  if not obj?
+    return obj
+  else if typeof obj is 'object'
     return obj
   else
     vals = obj.split(' ')
@@ -71,7 +84,7 @@
     @on(event, handler, ctx, true)

   off: (event, handler) ->
-    return unless @bindings?
+    return unless @bindings?[event]?

     if not handler?
       delete @bindings[event]
@@ -84,7 +97,7 @@
           i++

   trigger: (event, args...) ->
-    if @bindings[event]
+    if @bindings?[event]
       i = 0
       while i < @bindings[event].length
         {handler, ctx, once} = @bindings[event][i]
@@ -97,17 +110,22 @@
           i++

 class Step extends Evented
-  constructor: (options) ->
+  constructor: (@tour, options) ->
     @setOptions options

   setOptions: (@options={}) ->
     @destroy()

-    @id = @options.id or @id or uniqueId()
+    @id = @options.id or @id or "step-#{ uniqueId() }"

     if @options.when
       for event, handler of @options.when
         @on event, handler, @
+
+    @options.buttons ?= [
+      text: 'Next'
+      action: @tour.next
+    ]

   bindAdvance: ->
     # An empty selector matches the step element
@@ -131,15 +149,26 @@
       throw new Error "Using the attachment feature of Shepherd requires the Tether library"

     opts = parseShorthand @options.attachTo, ['element']
+    opts ?= {}

     if typeof opts.element is 'string'
       opts.element = document.querySelector opts.element

-    @tether = new Tether
+      if not opts.element?
+        throw new Error "Shepherd step's attachTo was not found in the page"
+
+    attachment = ATTACHMENT[opts.on or 'right']
+    if not opts.element?
+      opts.element = 'viewport'
+      attachment = 'middle center'
+
+    tetherOpts =
       element: @el
       target: opts.element
       offset: opts.offset or '0 0'
-      attachment: ATTACHMENT[opts.on or 'right']
+      attachment: attachment
+
+    @tether = new Tether extend(tetherOpts, @options.tetherOptions)

   show: =>
     if not @el?
@@ -182,30 +211,35 @@
       @destroy()

     @el = createFromHTML "

"
+    console.log 'rendering', @el
+
+    content = document.createElement 'div'
+    content.className = 'drop-content'
+    @el.appendChild content

     if @options.title?
       header = document.createElement 'header'
       header.innerHTML = "
#{ @options.title }
"
-      @el.appendChild header
+      content.appendChild header

     if @options.text?
-      content = createFromHTML "

"
+      text = createFromHTML "

"

       paragraphs = @options.text
       if typeof paragraphs is 'string'
         paragraphs = [paragraphs]

       for paragraph in paragraphs
-        content.innerHTML += "
#{ paragraph }
"
-
-      @el.appendChild content
+        text.innerHTML += "
#{ paragraph }
"
+
+      content.appendChild text

     footer = document.createElement 'footer'

     if @options.buttons
       buttons = createFromHTML "

"

-      for cfg in @options.button
+      for cfg in @options.buttons
         button = createFromHTML "
#{ cfg.text }"
         buttons.appendChild button

@@ -213,12 +247,11 @@

       footer.appendChild buttons

-    @el.appendChild footer
+    content.appendChild footer

     document.body.appendChild @el

-    if @options.attachTo
-      @setupTether()
+    @setupTether()

     if @options.advanceOn
       @bindAdvance()
@@ -230,6 +263,11 @@
       cfg.events.click = cfg.action

     for event, handler of cfg.events
+      if typeof handler is 'string'
+        page = handler
+        handler = =>
+          @tour.show page
+
       addEventListener el, event, handler

     @on 'destroy', ->
@@ -237,28 +275,61 @@
         removeEventListener el, event, handler

 class Tour
-  initialize: (@options={}) ->
+  constructor: (@options={}) ->
     @steps = @options.steps ? []

-  addStep: (step) ->
-    @steps.push step
-
-  next: ->
-    index = 0
+  addStep: (name, step) ->
+    step = extend {}, @options.defaults, step
+
+    if not step?
+      step = name
+    else
+      step.id = name
+
+    @steps.push new Step(@, step)
+
+  getById: (id) ->
+    for step in @steps when step.id is id
+      return step
+
+  next: =>
+    index = -1
     if @currentStep
       index = @steps.indexOf(@currentStep)

-    @currentStep = @steps[index]
-
-    @currentStep.show @
+    @show(index + 1)
+
+  show: (key=0) ->
+    if @currentStep
+      @currentStep.hide()
+
+    if typeof key is 'string'
+      next = @getById key
+    else
+      next = @steps[key]
+
+    console.log 'next', next
+    if next
+      @currentStep = next
+      next.show()
+
+  start: ->
+    @currentStep = null
+    @next()

 tour = new Tour
+  defaults:
+    classes: 'drop drop-open drop-theme-arrows'
+
 tour.addStep 'start',
   title: "Welcome to KaPow!"
   text: "KaPow is the ultimate comic book marketplace.  We supply over six
   hundred tons of comic books to the waste management industry each year.  The
   burning of these books supplies enough energy to power eight hundred American
   homes."
+  tetherOptions:
+    attachment: 'middle center'
+    targetAttachment: 'middle center'

 tour.addStep
   title: "Are you a seller, or a waste management professional?"
@@ -275,7 +346,7 @@
     "Selling is the ultimate way to unload your useless 'art'.",
     "Begin by clicking the 'Start Selling' button."
   ]
-  attachTo: 'bottom button.start-selling'
+  attachTo: 'bottom a.small.button:first-of-type'
   advanceOn: 'click button.start-selling'
   classes: 'tour-wide'

@@ -286,7 +357,7 @@
     "Begin by entering your Waste Management Licence number below."
   ]
   attachTo:
-    element: 'form.wmp-start'
+    element: 'input[placeholder="large-12.columns"]'
     on: 'bottom'
     classes: 'tour-highlight'
     offset: '2px 0'
@@ -310,4 +381,4 @@
 tour.start
   hash: true

-tour.completed 'seller-start'
+#tour.completed 'seller-start'


Updates Events plugin to allow for nested elements

--- vendor/assets/javascripts/styleguide/plugins/tbg-ga-events.js.coffee
+++ vendor/assets/javascripts/styleguide/plugins/tbg-ga-events.js.coffee
@@ -52,6 +52,7 @@
   $.fn.gaEvent = ( option , type = "gaevent") ->
     this.each ->
       $this = $(@)
+      $this = if ($t = $(this)).is("[data-#{type}]") then $t else $t.closest("[data-#{type}]")
       data = $this.data 'gaeventplugin'
       if not (eventContent= $this.data type ) then return removeData($this, type)
       if !data then $this.data 'gaeventplugin', (data = new GAEvent @, eventContent)


run: catch error out of mecano

run: catch error out of mecano

--- lib/run.coffee
+++ lib/run.coffee
@@ -119,19 +119,29 @@
   .parallel true
   .call (context, callback) ->
     return callback() if params.hosts and multimatch(context.config.host, params.hosts).length is 0
+    error = false
     context.call ->
       @log.cli host: context.config.host, pad: host: 20, header: 60
       @log.md filename: context.config.host, basedir: './log'
       for id in context.services then do (id) =>
         service = config.services[id]
         return if !service.required and params.modules and multimatch(service.module, params.modules).length is 0
-        if service.commands['']
-          @call service.commands[''], (err) ->
-            console.log 'ERROR', context.config.host, id, err if err
-        if service.commands[params.command]
-          @call service.commands[params.command], (err) ->
-            console.log 'ERROR', context.config.host, id, err if err
-    context.then callback
+        try
+          if service.commands['']
+            @call service.commands[''], (err) ->
+              console.log 'ERROR', context.config.host, id, err if err
+              error = true if err
+          if service.commands[params.command]
+            @call service.commands[params.command], (err) ->
+              console.log 'ERROR', context.config.host, id, err if err
+              error = true if err
+        catch err
+          console.log 'ERROR', context.config.host, id, err if err
+          error = true
+          throw err
+    context.then (err) ->
+      console.log 'ERROR', err unless error
+      callback()
   .then (err) =>
     @emit 'end'
   @


only add buttons if they don't exist already

--- app/assets/javascripts/hat-trick.js.coffee
+++ app/assets/javascripts/hat-trick.js.coffee
@@ -183,11 +183,14 @@
         console.log "Setting reset button val to #{label}"
         $buttonsDiv.find('input:reset').val(label)
       else
-        console.log "Adding new #{name}:#{label} button"
-        $newButton = $buttonsDiv.append(this.createButton(name, label))
-        $newButton.click (event) =>
-          event.preventDefault()
-          this.goToStepId(name)
+        buttonSelector = """input:button[name="#{name}"][value="#{label}"]"""
+        $existingButtons = $buttonsDiv.find(buttonSelector)
+        if $existingButtons.length == 0
+          console.log "Adding new #{name}:#{label} button"
+          $newButton = $buttonsDiv.append(this.createButton(name, label))
+          $newButton.click (event) =>
+            event.preventDefault()
+            this.goToStepId(name)

   bindEvents: ->
     @form.bind "step_shown", (event, data) =>


OX-120 debug

OX-120 debug

--- app/assets/javascripts/model/binary.coffee
+++ app/assets/javascripts/model/binary.coffee
@@ -152,7 +152,7 @@

   pingImpl : (position, zoomSteps, direction) ->

-    unless position == @lastPosition and direction == @lastDirection and _.isEqual(zoomSteps, @lastZoomSteps)
+    unless _.isEqual(position, @lastPosition) and _.isEqual(direction, @lastDirection) and _.isEqual(zoomSteps, @lastZoomSteps)

       lastPosition = @lastPosition

@@ -197,6 +197,8 @@
         direction[2] * 0.8 + newDirection[2] * 0.2
       ]

+      console.log direction, newDirection, loadDirection
+
       directionMax = Math.abs(Math.max(direction[0], direction[1], direction[2]))

       loadDirection = direction
@@ -205,7 +207,6 @@
         loadDirection[1] /= directionMax;
         loadDirection[2] /= directionMax;

-      console.log direction, loadDirection

       delta_x = delta_y = delta_z = 0
       direction_x = direction_y = direction_z = 0


Deactivate quickjumps on ESC

Deactivate quickjumps on ESC

--- app/assets/javascripts/quickjump.js.coffee
+++ app/assets/javascripts/quickjump.js.coffee
@@ -51,6 +51,9 @@
     else if event.keyCode == @KEY_DOWN
       @parent.moveSelection(+1)
       false
+    else if event.keyCode == @KEY_ESC
+      @parent.deactivate()
+      false

   keyup: (event) ->
     query = event.target.value
@@ -163,8 +166,8 @@
       all = $(@selector).find(".result")
       anchor = $(all[@active_result])

-  hide: ->
-    $(@selector).modal("hide")
+  deactivate: ->
+

   markActiveResult: ->
     if @active_result?
@@ -238,6 +241,9 @@
     @controls = new OUT.QuickJump.Controls(this, @selector, @result_callback)
     this.setDefaultResults()

+  deactivate: ->
+    $(@selector).modal("hide")
+
   getDefaultResults: ->
     OUT.quick_jump_modal_defaults || []

@@ -254,6 +260,10 @@
     @controls = new OUT.QuickJump.Controls(this, @selector, @result_callback)
     this.setDefaultResults()

+  deactivate: ->
+    $(@selector).find("input").val("")
+    $(@selector).parents("li.dropdown").removeClass("open")
+
   getDefaultResults: ->
     OUT["quick_jump_#{@type}s_defaults"] || []


select all, none functions

select all, none functions

--- app/assets/javascripts/zvg/verbatim.coffee
+++ app/assets/javascripts/zvg/verbatim.coffee
@@ -39,6 +39,12 @@

     tag_spans.text((d) -> d)
     @renderCallback(@)
+
+  selectAllResponses: ->
+    @container.selectAll('tr.response').classed('selected', true)
+
+  selectNoResponses: ->
+    @container.selectAll('tr.response').classed('selected', false)

   initializeQuestionTable: ->
     @controls        = d3.select(@container).append('table').attr('class', 'controls')


[backport] Fixed checkout monitoring in analytics

--- app/coffee/modules/common/analytics.coffee
+++ app/coffee/modules/common/analytics.coffee
@@ -85,22 +85,6 @@

         @win.ga("send", "event", category, action, label, value)

-    ecStep: (step) ->
-        return if not @.initialized
-        return if not @win.ga
-
-        if step == "register"
-            stepId = 1
-        else if step == "change-plan"
-            stepId = 2
-        else if step == "select-plan"
-            stepId = 3
-        else if step == "confirm-plan"
-            stepId = 4
-        else if step == "plan-changed"
-            stepId = 5
-
-
     ecViewPlan: (plan) ->
         return if not @.initialized
         return if not @win.ga
@@ -171,13 +155,24 @@
             'quantity': 1,
             'position': 1,
         })
-        @.ecStep("confirm-plan")
+        @win.ga('ec:setAction','checkout', {'step': 1,})
+        @.trackEvent("ecommerce", "start-checkout", step, stepId)

     ecPurchase: (plan_id, plan_name, plan_price) ->
         return if not @.initialized
         return if not @win.ga

-        @.ecStep("plan-changed")
+        @win.ga('ec:addProduct', {
+            'id': plan_id,
+            'name': plan_name,
+            'price': plan_price,
+            'category': "plans",
+            'quantity': 1,
+            'position': 1,
+        })
+
+        @win.ga('ec:setAction','checkout', {'step': 2,})
+        @.trackEvent("ecommerce", "start-checkout", step, stepId)

         @win.ga('ec:addProduct', {
             'id': plan_id,


(fix) Fix correct button not working.

(fix) Fix correct button not working.

--- app/scripts/pages/overview/overview.coffee
+++ app/scripts/pages/overview/overview.coffee
@@ -15,7 +15,7 @@
       is: data.is
     @contingent.ratio = @contingent.is / @contingent.should

-  correct: -> window.location.hash = "#exercise/#{@exercise()}"
+  correct: -> window.location.hash = "#exercise/#{@exercise.id()}"

 class ViewModel
   constructor: ->


include text-row-tool dependencies; set default upper and lower mark positions

--- app/assets/javascripts/components/mark/tools/text-row-tool/index.cjsx
+++ app/assets/javascripts/components/mark/tools/text-row-tool/index.cjsx
@@ -2,6 +2,8 @@
 React = require 'react'
 Draggable = require 'lib/draggable'
 DeleteButton = require './delete-button'
+ResizeButton = require './resize-button'
+DoneCheckbox = require './done-checkbox'

 DEBUG = false

@@ -13,7 +15,10 @@
     mark: React.PropTypes.object.isRequired

   getInitialState: ->
-    mark: @props.mark
+    mark = @props.mark
+    mark.yUpper = @props.mark.y - 25
+    mark.yLower = @props.mark.y + 25
+    mark: mark

   componentWillReceiveProps: ->
     @setState
@@ -31,43 +36,73 @@
     @setState mark: mark

   render: ->
-
-    fillColor   = 'rgba(0,0,0,0.30)'
-    strokeColor = '#fff'
-    radius = 40
-    strokeWidth = 3
+    classString = 'textRow drawing-tool'

-    transform = "
-      translate(#{@state.mark.x}, #{@state.mark.y})
-      scale(#{1}, #{1})
-    "
+    markHeight = @state.mark.yLower - @state.mark.yUpper

-    <g className="point drawing-tool" transform={transform}>
+    <g
+      className = {classString}
+      transform = {"translate(#{Math.ceil @state.strokeWidth}, #{Math.round( @state.mark.y - markHeight/2 ) })"}
+    >

       { if DEBUG
-          <text fill='blue' fontSize='30'>
-            {@props.mark.key}
-          </text>
+        <text fontSize="40" fill="blue">{@state.mark.key}</text>
       }
+
+      <Draggable
+        onStart = {@props.handleMarkClick.bind @props.mark}
+        onDrag = {@handleDrag} >
+        <rect
+          className   = "mark-rectangle"
+          x           = 0
+          y           = 0
+          viewBox     = {"0 0 @props.imageWidth @props.imageHeight"}
+          width       = {Math.ceil( @props.imageWidth - 2*@state.strokeWidth ) }
+          height      = {markHeight}
+          fill        = {if @props.selected then "rgba(255,102,0,0.25)" else "rgba(0,0,0,0.5)"}
+          stroke      = {@state.strokeColor}
+          strokeWidth = {@state.strokeWidth}
+        />
+      </Draggable>

-      <Draggable
-        onStart={@props.handleMarkClick.bind null, @props.mark}
-        onDrag={@handleDrag} >
+      {

-        <g strokeWidth={strokeWidth}>
-          <circle
-            r={radius + (strokeWidth / 2)}
-            stroke={strokeColor}
-            fill={fillColor}
-          />
-        </g>
+        if @state.markStatus is 'mark'
+          <g>
+            <ResizeButton
+              viewBox = {"0 0 @props.imageWidth @props.imageHeight"}
+              className = "upperResize"
+              handleResize = {@props.handleUpperResize}
+              transform = {"translate( #{@props.imageWidth/2}, #{ - Math.round @props.scrubberHeight/2 } )"}
+              scrubberHeight = {@props.scrubberHeight}
+              scrubberWidth = {@props.scrubberWidth}
+              workflow = {@props.workflow}
+              isSelected = {@props.selected}
+            />

-      </Draggable>
-
-      { if @props.isSelected
-          <DeleteButton
-            transform="translate(#{radius}, #{-radius})"
-            onClick={@props.onClickDelete.bind null, @props.mark.key} />
+            <ResizeButton
+              className = "lowerResize"
+              handleResize = {@props.handleLowerResize}
+              transform = {"translate( #{@props.imageWidth/2}, #{ Math.round( markHeight - @props.scrubberHeight/2 ) } )"}
+              scrubberHeight = {@props.scrubberHeight}
+              scrubberWidth = {@props.scrubberWidth}
+              workflow = {@props.workflow}
+              isSelected = {@props.selected}
+            />
+
+            <DeleteButton
+              transform = "translate(50, #{Math.round markHeight/2})"
+              onClick = {@props.onClickDelete.bind null, @props.key}
+              workflow = {@props.workflow}
+              isSelected = {@props.selected}
+              buttonDisabled = {@state.mark.buttonDisabled}
+            />
+          </g>
       }
-
+      <DoneCheckbox
+        buttonDisabled = {@state.mark.buttonDisabled}
+        markStatus = {@state.markStatus}
+        onClickMarkButton = {@onClickMarkButton}
+        transform = {"translate( #{@props.imageWidth-250}, #{ Math.round markHeight/2 -@props.scrubberHeight/2 } )"}
+      />
     </g>


Connect to ColumnDataSource.change slot instead of data property

--- bokehjs/src/coffee/models/sources/cds_view.coffee
+++ bokehjs/src/coffee/models/sources/cds_view.coffee
@@ -27,14 +27,12 @@
       @compute_indices()
       @change.emit()
     )
+    if @source?.change?
+      @connect(@source.change, () -> @compute_indices())
     if @source?.streaming?
       @connect(@source.streaming, () -> @compute_indices())
     if @source?.patching?
       @connect(@source.patching, () -> @compute_indices())
-    if @source?
-      @connect(@source.properties.data.change, () ->
-        @compute_indices()
-      )

   compute_indices: () ->
     indices = (filter.compute_indices(@source) for filter in @filters)


warn student when incomplete and due within 24 hours, otherwise, dont show late icon unless past due date

--- tutor/src/components/student-dashboard/event-info-icon.cjsx
+++ tutor/src/components/student-dashboard/event-info-icon.cjsx
@@ -13,17 +13,17 @@
   render: ->
     {event} = @props

-    now   = TimeStore.getNow()
+    # TODO this is naive and not timezone aware.  As a hotfix, this should suffice.
+    now   = moment(TimeStore.getNow())
     dueAt = moment(event.due_at)
     isIncomplete = event.complete_exercise_count isnt event.exercise_count
-    pastDue      = event.type is 'homework' and dueAt.isBefore(now, 'd')
-    workedLate   = moment(event.last_worked_at).isAfter(dueAt)
+    pastDue      = event.type is 'homework' and dueAt.isBefore(now)
+    isDueToday   = now.isBetween(dueAt.clone().subtract(1, 'day'), dueAt)

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

-    # use 'day' granularity for checking if the due date is today or after today
-    status = if dueAt.isSame(now, 'd') then 'incomplete' else 'late'
+    status = if isDueToday then 'incomplete' else 'late'

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


KodingAppsController: Make sure every app just included once

--- client/Main/kodingappscontroller.coffee
+++ client/Main/kodingappscontroller.coffee
@@ -36,28 +36,27 @@
   # Please make sure about your changes on it.
   @putAppScript = (app, callback = noop)->

-    # Remove app from head if exists, just for sure
-    # $("head .internal-#{app.identifier}").remove()
-
-    if $("head .internal-style-#{app.identifier}").length is 0 and app.style
+    identifier = app.identifier.replace /\./g, '_'
+
+    @unloadAppScript app
+
+    if app.style and $("head .internal-style-#{identifier}").length is 0

       style        = new KDCustomHTMLView
         tagName    : "link"
-        cssClass   : "internal-style-#{app.identifier}"
-        bind       : 'load'
-        # load       : ->
+        cssClass   : "internal-style-#{identifier}"
         attributes :
           rel      : "stylesheet"
           href     : "#{app.style}?#{KD.utils.uniqueId()}"

       $('head')[0].appendChild style.getElement()

-    if $("head .internal-script-#{app.identifier}").length is 0 and app.script
+    if app.script and $("head .internal-script-#{identifier}").length is 0

       script       = new KDCustomHTMLView
         tagName    : "script"
-        cssClass   : "internal-script-#{app.identifier}"
-        bind       : 'load'
+        cssClass   : "internal-script-#{identifier}"
+        bind       : "load"
         load       : -> callback null
         attributes :
           type     : "text/javascript"
@@ -67,8 +66,12 @@

   @unloadAppScript = (app, callback = noop)->

-    $("head .internal-style-#{app.identifier}").remove()
-    $("head .internal-script-#{app.identifier}").remove()
+    identifier = app.identifier.replace /\./g, '_'
+
+    $("head .internal-style-#{identifier}").remove()
+    $("head .internal-script-#{identifier}").remove()
+
+    KD.utils.defer -> callback()

   @runApprovedApp = (jApp, options = {}, callback = noop)->


Update createTerminal method to working with multiple VMs.

--- client/ide/views/tabview/ideview.coffee
+++ client/ide/views/tabview/ideview.coffee
@@ -45,14 +45,15 @@

     @createPane_ editor, paneOptions, file

-  createTerminal: ->
-    @createPane_ new TerminalPane, { name: 'Terminal' }
+  createTerminal: (vm) ->
+    terminalPane = new TerminalPane { vm }
+    @createPane_ terminalPane,    { name: 'Terminal' }

   createDrawingBoard: ->
-    @createPane_ new DrawingPane,  { name: 'Drawing'  }
+    @createPane_ new DrawingPane, { name: 'Drawing'  }

   createPreview: ->
-    @createPane_ new PreviewPane,  { name: 'Browser'  }
+    @createPane_ new PreviewPane, { name: 'Browser'  }

   removeOpenDocument: ->
     # TODO: This method is legacy, should be reimplemented in ace bundle.


Set other screens to inactive after screen add

--- client/lanes/screens/Definitions.coffee
+++ client/lanes/screens/Definitions.coffee
@@ -5,7 +5,6 @@
         active: ['boolean', true, true]
         id: type: 'number', setOnce: true, required: true, default: ->
             _.uniqueId('screen')
-

     component: ->
         @screen.getScreen()
@@ -56,9 +55,6 @@
             deps: ['extension', 'model'], fn: ->
                 _.classify(@extension) + ".Screens." + @view

-    _setDisplaying: ->
-        new ScreenView( screen: this )
-
     getScreen: ->
         Lanes.u.getPath(@extension_path)

@@ -87,7 +83,7 @@

     display: ->
         this.ensureLoaded().then =>
-            @_setDisplaying()
+            new ScreenView( screen: @ )

 class ScreenViewSet extends Lanes.Models.BasicCollection
@@ -98,7 +94,7 @@
         @findWhere( active: true )

     initialize: (models, options = {}) ->
-        this.on( 'change:active', this.onActiveChange )
+        this.on( 'change:active add', this.onActiveChange )

     remove: (model) ->
         index = this.indexOf(model)
@@ -109,7 +105,7 @@
         this

     onActiveChange: (changed, active) ->
-        return unless active
+        return unless changed.active
         this.each (screen) ->
             screen.set( active: false ) unless screen == changed


Specify _finishedAt and _startedAt as dependencies for duration

Because of our partial loading implementation we need to specify
finishedAt and startedAt differently than we would normally do. For some
reason duration was not recalculated while loading record and specifying
raw attributes helps here.

--- assets/scripts/app/models/extensions.coffee
+++ assets/scripts/app/models/extensions.coffee
@@ -6,7 +6,7 @@
       duration
     else
       Travis.Helpers.durationFrom(@get('startedAt'), @get('finishedAt'))
-  ).property('_duration', 'finishedAt', 'startedAt', 'notStarted')
+  ).property('_duration', 'finishedAt', 'startedAt', 'notStarted', '_finishedAt', '_startedAt')

   updateTimes: ->
     unless @get('isFinished')


Deprecate error logging in failure handler function

--- workers/social/lib/social/models/sidebar.coffee
+++ workers/social/lib/social/models/sidebar.coffee
@@ -102,13 +102,9 @@

       return ->

-        failureFn = (err) ->
+        failureFn = ->

           filterQueue.fin()
-
-          return  unless err
-          console.error \
-            new KodingError "Sidebar decorate environment data: #{JSON.stringify err}"

         makeSuccessFn = (fn) ->


Disable tracker module on configuration option

--- workers/social/lib/social/models/tracker.coffee
+++ workers/social/lib/social/models/tracker.coffee
@@ -56,6 +56,8 @@

   @identify = (username, traits = {}) ->
+    return  unless KONFIG.sendEventsToSegment
+
     # use `forcedRecipient` for both username and email
     if forcedRecipient
       username     = forcedRecipient
@@ -82,6 +84,8 @@

   @track = (username, event, options = {}) ->
+    return  unless KONFIG.sendEventsToSegment
+
     _.extend options, @properties[event.subject]

     # use `forcedRecipient` for both username and email


Client: Activity: Tests: Updated BrowsePrivateChannelsModal tests

--- client/activity/lib/components/browseprivatechannelsmodal/test.coffee
+++ client/activity/lib/components/browseprivatechannelsmodal/test.coffee
@@ -1,68 +1,114 @@
-React                      = require 'kd-react'
-ReactDOM                   = require 'react-dom'
-expect                     = require 'expect'
-TestUtils                  = require 'react-addons-test-utils'
-BrowsePrivateChannelsModal = require '../browseprivatechannelsmodal'
+kd          = require 'kd'
+React       = require 'kd-react'
+expect      = require 'expect'
+Modal       = require './index'
+ModalView   = require './view'
+ReactDOM    = require 'react-dom'
+TestUtils   = require 'react-addons-test-utils'
+mock        = require '../../../../mocks/mockingjay'
+toImmutable = require 'app/util/toImmutable'
+
+{ createRenderer
+  renderIntoDocument
+  scryRenderedDOMComponentsWithClass } = TestUtils
+

 describe 'BrowsePrivateChannelsModal', ->

-  { createRenderer
-    renderIntoDocument
-    scryRenderedDOMComponentsWithClass } = TestUtils
-
-
-  it 'renders BrowsePrivateChannelsModal with PrivateChannelListModal and ChannelList-Modal classNames', ->
+  it 'should render BrowsePrivateChannelsModal-Container with methods', ->

     shallowRenderer = createRenderer()
-    shallowRenderer.render(<BrowsePrivateChannelsModal isOpen={yes}/>)
+    shallowRenderer.render(<Modal.Container isOpen={yes}/>)

     modal = shallowRenderer.getRenderOutput()

     expect(modal.props.isOpen).toBeTruthy()
-    expect(modal.props.className).toContain 'PrivateChannelListModal'
-    expect(modal.props.className).toContain 'ChannelList-Modal'
+    expect(typeof modal.props.onClose).toEqual 'function'
+    expect(typeof modal.props.onItemClick).toEqual 'function'

-  it 'checks rendered modal type and its child component props', ->
+  it 'renders BrowsePrivateChannelsModal-View with PrivateChannelListModal and ChannelList-Modal classNames', ->

     shallowRenderer = createRenderer()
-    shallowRenderer.render(<BrowsePrivateChannelsModal isOpen={yes}/>)
+    shallowRenderer.render(<ModalView isOpen={yes}/>)

-    modal = shallowRenderer.getRenderOutput()
-    { children } = modal.props
+    view = shallowRenderer.getRenderOutput()

-    expect(modal.type.name).toEqual 'Modal'
+    expect(view.props.className).toContain 'PrivateChannelListModal'
+    expect(view.props.className).toContain 'ChannelList-Modal'
+
+
+  it 'checks rendered BrowsePrivateChannelsModal-View type and its child component props', ->
+
+    shallowRenderer = createRenderer()
+    shallowRenderer.render(<ModalView isOpen={yes}/>)
+
+    view = shallowRenderer.getRenderOutput()
+    { children } = view.props
+
+    expect(view.type.name).toEqual 'Modal'
     expect(children.type.name).toEqual 'SidebarModalList'
     expect(children.props.threads).toExist()
     expect(children.props.title).toEqual 'Other Messages:'
     expect(children.props.searchProp).toEqual 'name'
     expect(children.props.onThresholdAction).toEqual 'loadFollowedPrivateChannels'
     expect(children.props.onItemClick).toExist()
-    expect(children.props.onItemClick()).toBeTruthy()
     expect(typeof children.props.onThresholdAction).toEqual 'string'
     expect(typeof children.props.itemComponent).toEqual 'function'

-  it 'renders BrowsePrivateChannelsModal and checks onItemClick method of modal', ->
+  it 'should render BrowsePrivateChannelsModal-Container and checks onItemClick method of modal', ->

-    shallowRenderer = createRenderer()
-    shallowRenderer.render(<BrowsePrivateChannelsModal isOpen={yes}/>)
+    modal = renderIntoDocument(<Modal.Container isOpen={no}/>)

-    modal = shallowRenderer.getRenderOutput()
-    { children } = modal.props
-
-    expect(modal.props.onClose()).toEqual(undefined)
-    expect(children.props.onItemClick()).toBeTruthy()
-    expect(modal.props.onClose()).toBeFalsy()
+    expect(modal.skipCloseHandling).toBeFalsy()
+    expect(modal.onItemClick()).toBeTruthy()
+    expect(modal.skipCloseHandling).toBeTruthy()

-  it 'does not render BrowsePrivateChannelsModal to the DOM and checks dom node existance', ->
+  it 'should not render BrowsePrivateChannelsModal-View to the DOM', ->

-    modal  = renderIntoDocument(<BrowsePrivateChannelsModal isOpen={no}/>)
-    modals = scryRenderedDOMComponentsWithClass modal, 'PrivateChannelListModal'
+    view  = renderIntoDocument(<ModalView isOpen={no}/>)
+    views = scryRenderedDOMComponentsWithClass view, 'PrivateChannelListModal'

-    expect(modal.refs.SidebarModalList).toNotExist()
-    expect(modal.props.isOpen).toBeFalsy()
-    expect(modals.length).toEqual 0
+    expect(view.refs.SidebarModalList).toNotExist()
+    expect(view.props.isOpen).toBeFalsy()
+    expect(views.length).toEqual 0

+  it 'should call router.handleRoute method with public route parameter', ->
+
+    routeTestHelper 'koding-dummy-channel', 'koding-public-channel', "/Channels/koding-dummy-channel"
+
+
+  it 'should call router.handleRoute method with private route parameter', ->
+
+    routeTestHelper 'koding-12345', 'privatemessage', "/Messages/koding-12345"
+
+
+routeTestHelper = (id, typeConstant, route) ->
+
+  modal = renderIntoDocument(<Modal.Container isOpen={no}/>)
+
+  expect.spyOn kd.singletons.router, 'handleRoute'
+
+  channelOptions = if typeConstant is 'privatemessage'
+  then { channelId: id, typeConstant }
+  else { channelName: id, typeConstant }
+
+  mockChannel = mock.getMockChannel channelOptions
+
+  threadOptions = if typeConstant is 'privatemessage'
+  then { channelId: id, channel: mockChannel }
+  else { channel: mockChannel }
+
+  mockThread  = mock.getMockThread threadOptions
+
+  modal.setState selectedThread: toImmutable mockThread
+  modal.onClose()
+
+  result = expect(kd.singletons.router.handleRoute)
+  result.toHaveBeenCalled()
+  result.toHaveBeenCalledWith(route)
+  result.toNotThrow()
+


Added autoAttach option, defaulted to true

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -19,6 +19,9 @@
   # Flag whether to render the view automatically on initialization.
   # As an alternative you might pass a `render` option to the constructor.
   autoRender: false
+
+  # Flag whether to attach the view automatically on render.
+  autoAttach: true

   # Automatic inserting into DOM
   # ----------------------------
@@ -90,7 +93,7 @@
         # Call the original method.
         render.apply this, arguments
         # Attach to DOM.
-        @attach arguments...
+        @attach arguments... if @autoAttach
         # Return the view.
         this


Don't use input.

Don't use input.

It's a proprietary jQuery selector and non-standard CSS3.

Also it doesn't work in Zepto.js.

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -220,7 +220,7 @@
     pass: (attribute, selector) ->
       @modelBind "change:#{attribute}", (model, value) =>
         $el = @$(selector)
-        if $el.is(':input')
+        if $el.is('input, textarea, select, button')
           $el.val value
         else
           $el.text value


Social: implement invitation checker endpoint

--- servers/lib/server/handlers/checktoken.coffee
+++ servers/lib/server/handlers/checktoken.coffee
@@ -3,10 +3,18 @@

 module.exports = (req, res, next) ->

-  { body }   = req
-  { JGroup } = koding.models
-  { token }  = body
+  { body }                = req
+  { JInvitation, JGroup } = koding.models
+  { token }               = body
+
+  return res.status(400).send "token is required"  unless token

   console.log token

-  res.status(200).send email : 'invitee@foo.com'
+  JInvitation.byCode token, (err, data) ->
+    if err
+      console.error "err while fetching token"
+      return res.status(500).send "internal server error"
+
+    return res.status(404).send "invitation not found"  unless data
+    return res.status(200).send data


log out if the command running is being used

--- services/clsi/app/coffee/CommandRunner.coffee
+++ services/clsi/app/coffee/CommandRunner.coffee
@@ -1,5 +1,7 @@
 spawn = require("child_process").spawn
 logger = require "logger-sharelatex"
+
+logger.info "using standard command runner"

 module.exports = CommandRunner =
 	run: (project_id, command, directory, image, timeout, callback = (error) ->) ->


Refer to Roxy as a Gangster

Refer to Roxy as a Gangster

--- src/coffee/game/ending.coffee
+++ src/coffee/game/ending.coffee
@@ -111,7 +111,7 @@
       "Dis. I can't believe you were really fool enough to undo it. To bring that monster back into
       this world. What the hell do you think you're doing?!?"

-      Ah, Roxy. Roxy the gang leader. Roxy the sorceress. As you suspected, she was Mrs. Brown's
+      Ah, Roxy. Roxy the gangster. Roxy the sorceress. As you suspected, she was Mrs. Brown's
       lover. She found Mr. Brown with the body and transformed him into the simulacrum that has been
       living as Mrs. Brown for the last few weeks. The one that hired you to look for the missing
       husband that was right under her nose the whole time.


scroll-visibility: now with repeat

scroll-visibility: now with repeat

--- app/_src/components/element-scroll-visibility.coffee
+++ app/_src/components/element-scroll-visibility.coffee
@@ -25,10 +25,14 @@
     CSS_ANIMATED_CLASS = 'has-animated'
     CSS_EXIT_CLASS = 'has-exited'
     DATA_OFFSET = 'offset'
+    DATA_REPEAT = 'scroll-repeat'

     constructor: (@$context, data) ->
         @context = @$context.get(0)
+        @repeat = @$context.data DATA_REPEAT or 0
         @$body = $(document.body)
+
+        @repeat = JSON.parse @repeat

         if data?.isPjax
             @$body.one FOURTEEN.PjaxNavigation.EVENT_ANIMATION_SHOWN, @addEventListeners
@@ -52,14 +56,24 @@
             @watcher.destroy()
             @watcher = null

+    reset: () =>
+        @$context.removeClass CSS_PARTIALLY_VISIBLE_CLASS
+        @$context.removeClass CSS_FULLY_VISIBLE_CLASS
+        @$context.removeClass CSS_ANIMATE_CLASS
+        @hasExited = false
+        @hasPartiallyPlayed = false
+        @hasFullyPlayed = false
+
     onEnterViewport: () =>
         return if @hasPartiallyPlayed
         @hasPartiallyPlayed = true
+        @$context.removeClass CSS_EXIT_CLASS
         @$context.addClass CSS_PARTIALLY_VISIBLE_CLASS

     onFullyEnterViewport: () =>
         return if @hasFullyPlayed
         @hasFullyPlayed = true
+        @$context.removeClass CSS_EXIT_CLASS
         @$context.addClass CSS_FULLY_VISIBLE_CLASS + " " + CSS_ANIMATE_CLASS

         animationEnd = FOURTEEN.Utils.whichAnimationEvent()
@@ -75,4 +89,7 @@
         return if @hasExited
         @hasExited = true
         @$context.addClass CSS_EXIT_CLASS
-        @removeEventListeners() if @hasPartiallyPlayed and @hasFullyPlayed+        if @repeat
+            @reset()
+        else if @hasPartiallyPlayed and @hasFullyPlayed
+            @removeEventListeners()