maybe working?

maybe working?

--- app/assets/javascripts/projects.coffee
+++ app/assets/javascripts/projects.coffee
@@ -33,7 +33,9 @@
         url: url
         success: (data) ->
           $("#editname").attr("value",data.name)
-          $("#editdescription").attr("value",data.description)
+          $("#editdescription").val(data.description)
+          $('#textarea1').trigger('autoresize');
+          console.log(data)
           Materialize.updateTextFields();
           return
         error: (req, msg, stat) ->
@@ -52,9 +54,10 @@
       console.log(active)
       active = $(this).attr("data")
       console.log(active)
+      return
     $("#editbut").click (e)->
       active = $(this).attr("data")
-
+      return
     $("#delete").submit (e) ->
       console.log("Boo!")
       url = "/api/project/"+active
@@ -76,7 +79,28 @@
           return
       e.preventDefault()
       return
-    return
+
+    $("#edit").submit (e) ->
+      console.log("Boo!")
+      url = "/api/project/"+active
+      $.ajax
+        type: 'PUT'
+        headers: {"Authorization": "Token token=" + $("#token").val()}
+        url: url
+        data: $("#edit").serialize()
+        success: (data) ->
+          location.reload();
+          return
+        error: (req, msg, stat) ->
+          console.log(req)
+          response = JSON.parse(req.responseText)
+          errorsArr = (msg.detail for msg in response.errors)
+          errorsHTML = errorsArr.join("\n")
+          alert(errorsHTML)
+          # $("#errorMsg").html(errorsHTML)
+          return
+      e.preventDefault()
+      return

     $("#add").submit (e) ->
       console.log("Boo!")


added Ben Vickers bio

added Ben Vickers bio

--- maps/contributors.coffee
+++ maps/contributors.coffee
@@ -311,7 +311,7 @@
     name: 'Ben Vickers'
     location: 'London'
     links: ['[Website](http://www.benvickers.net/)', '[@benvickers_](https://twitter.com/benvickers_)']
-    bio: 'Ben Vickers is a curator, writer, network analyst, technologist and luddite. He is currently Curator of Digital at the Serpentine Gallery, co-runs LIMAZULU Project Space, is an active member of EdgeRyders, leads Brighton University’s Professional \'Reality\' Development Program and facilitates the development of unMonastery, a new kind of social space designed to serve the local communities of towns or small cities throughout Europe in solving key social and infrastructural problems.'
+    bio: 'Ben Vickers is a curator, writer, explorer, technologist and luddite. Currently Curator of Digital at the Serpentine Galleries, is Co-Director of LIMAZULU Project Space, Co-Chair of Transmediale 2016, a Near Now Fellow and facilitator for the open-source development of unMonastery, a new civically minded social space prototyped in Matera, Southern Italy during 2014 and now set to replicate throughout Europe and further afield in 2015/16.'
     azone_username: ''
     azone_reviews: []
     azone_funds: []


Printing debug info one time, and one time only

--- tests/www/tests/runner.coffee
+++ tests/www/tests/runner.coffee
@@ -43,6 +43,12 @@
 #   'http://localhost:8080/'
 # ], stdio: 'inherit'

+if env isnt 'local'
+  console.log '------------------------------------'
+  console.log 'connecting using sauce credentials..'
+  console.log sauce_conf
+  console.log '------------------------------------'
+
 # mounting suites
 # ------------------------------------------------------------------------------

@@ -70,10 +76,6 @@
         browser = do wd.remote
       else
         browser = wd.remote sauce_conf
-        console.log '------------------------------------'
-        console.log 'connecting using sauce credentials..'
-        console.log sauce_conf
-        console.log '------------------------------------'

       # SET MOCHA HOOKS
       pass = hook @, browser, caps, base_url, notify_sauce_labs, coverage


deepthoughts fix: print text of thought

deepthoughts fix: print text of thought

--- scripts/deepthoughts.coffee
+++ scripts/deepthoughts.coffee
@@ -11,4 +11,4 @@
             # and makes an http get call
             .get() (error, response, body) ->
                 # passes back the complete reponse
-                msg.send body
+                msg.send body['thought']


3d: we now have points, both for webgl and canvas, and they look similar... and are round.

--- salvus/page/3d.coffee
+++ salvus/page/3d.coffee
@@ -295,9 +295,11 @@
                 @controls.autoRotateSpeed = @opts.spin
             @controls.autoRotate = true

-        @controls.addEventListener 'change', () => if @renderer_type=='dynamic' then @renderer.render(@scene, @camera)
-        # @controls.addEventListener 'end',    () => @set_static_renderer()
-        # @controls.addEventListener 'start',  () =>
+        @controls.addEventListener 'change', () =>
+            if @renderer_type=='dynamic'
+                @rescale_objects()
+                @renderer.render(@scene, @camera)
+

     add_camera: (opts) =>
@@ -352,10 +354,10 @@
         # make an HTML5 2d canvas on which to draw text
         width   = 300  # this determines max text width; beyond this, text is cut off.
         height  = 150
-        canvas  = $("<canvas style='border:1px solid black' width=#{width} height=#{height}>")[0]
-
-        # get the drawing context
-        context = canvas.getContext("2d")
+        canvas = document.createElement( 'canvas' )
+        canvas.width = width
+        canvas.height = height
+        context = canvas.getContext("2d")  # get the drawing context

         # set the fontsize and fix for our text.
         context.font = "Normal " + o.fontsize + "px " + o.fontface
@@ -416,30 +418,57 @@
             sizeAttenuation : false
         @show_canvas()

-        material = new THREE.PointCloudMaterial
-            color           : o.color
-            size            : o.size
-            sizeAttenuation : o.sizeAttenuation
+        # IMPORTANT: Below we use sprites instead of the more natural/faster PointCloudMaterial.
+        # Why?  Because usually people don't plot a huge number of points, and PointCloudMaterial is SQUARE.
+        # By using sprites, our points are round, which is something people really care about.
+
         switch dynamic_renderer_type
+
             when 'webgl'
-                geometry = new THREE.Geometry()
-                geometry.vertices.push(@vector(o.loc))
-                particle = new THREE.PointCloud(geometry, material)
-            #when 'canvas'
-            #    geometry = new THREE.Geometry()
-            #    geometry.vertices.push(@vector(o.loc))
-            #    particle = new THREE.PointCloud(geometry, material)
+                width   = 50
+                height  = 50
+                canvas = document.createElement( 'canvas' )
+                canvas.width = width
+                canvas.height = height
+                context = canvas.getContext("2d")  # get the drawing context
+
+                centerX = width / 2
+                centerY = height / 2
+                radius = 25
+                context.beginPath()
+                context.arc(centerX, centerY, radius, 0, 2*Math.PI, false)
+                context.fillStyle = o.color
+                context.fill()
+                texture = new THREE.Texture(canvas)
+                texture.needsUpdate = true
+                spriteMaterial = new THREE.SpriteMaterial(map: texture)
+                particle = new THREE.Sprite(spriteMaterial)
+                p = @aspect_ratio_scale(o.loc)
+                particle.position.set(p[0],p[1],p[2])
+                z = [particle, o.size/200]
+                if not @_points?
+                    @_points = [z]
+                else
+                    @_points.push(z)
+
             when 'canvas'
-                particle = new THREE.Particle(material)
-                particle.position.set(@aspect_ratio_scale(o.loc))
-                if @_frame_params?
-                    p = @_frame_params
-                    w = Math.min(Math.min(p.xmax-p.xmin, p.ymax-p.ymin),p.zmax-p.zmin)
+                # inspired by http://mrdoob.github.io/three.js/examples/canvas_particles_random.html
+                PI2 = Math.PI * 2
+                program = (context) ->
+                    context.beginPath()
+                    context.arc(0, 0, 0.5, 0, PI2, true)
+                    context.fill()
+                material = new THREE.SpriteCanvasMaterial
+                    color   : new THREE.Color(o.color)
+                    program : program
+                particle = new THREE.Sprite(material)
+                p = @aspect_ratio_scale(o.loc)
+                particle.position.set(p[0],p[1],p[2])
+                z = [particle, 4*o.size/@opts.width]
+                if not @_points?
+                    @_points = [z]
                 else
-                    w = 5 # little to go on
-                #console.log("w=",w)
-                particle.scale.x = particle.scale.y = Math.max(50/@opts.width, o.size * 5 * w / @opts.width)
-                #console.log(particle)
+                    @_points.push(z)
             else
                 throw "bug -- uknown dynamic_renderer_type = #{dynamic_renderer_type}"

@@ -762,17 +791,27 @@
             return

         # rescale all text in scene
-        if (new_pos or force) and @_center?
-            s = @camera.position.distanceTo(@_center) / 3
-            if @_text?
-                for sprite in @_text
-                    sprite.scale.set(s,s,s)
-            if @_frame_labels?
-                for sprite in @_frame_labels
-                    sprite.scale.set(s,s,s)
+        @rescale_objects()

         @renderer.render(@scene, @camera)

+    rescale_objects: () =>
+        if not @_center?
+            return
+        s = @camera.position.distanceTo(@_center) / 3
+        if Math.abs(@_last_scale - s) < 0.000001
+            return
+        @_last_scale = s
+        if @_text?
+            for sprite in @_text
+                sprite.scale.set(s,s,s)
+        if @_frame_labels?
+            for sprite in @_frame_labels
+                sprite.scale.set(s,s,s)
+        if @_points?
+            for z in @_points
+                c = z[1]
+                z[0].scale.set(s*c,s*c,s*c)

 $.fn.salvus_threejs = (opts={}) ->


3d: make it so rendering more than 7 times doesn't f everything up. (really working around a huge bug in Three.js)

--- salvus/page/3d.coffee
+++ salvus/page/3d.coffee
@@ -37,6 +37,11 @@
 rgb_to_hex = (r, g, b) -> "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b)

 _loading_threejs_callbacks = []
+
+VERSION = '59'
+#VERSION = '68'
+$.ajaxSetup(cache: true) # when using getScript, cache result.
+
 load_threejs = (cb) ->
     _loading_threejs_callbacks.push(cb)
     #console.log("load_threejs")
@@ -44,12 +49,12 @@
         #console.log("load_threejs: already loading...")
         return

-    load = (script, cb) -> $.getScript(script).done(()=>cb()).fail(()=>cb("error loading"))
+    load = (script, cb) -> $.getScript(script).done((script, textStatus)=>cb()).fail((jqxhr, settings, exception )=>cb("error loading -- #{exception}"))

     async.series([
-        (cb) -> load("/static/threejs/r59/three.min.js",cb)
-        (cb) -> load("/static/threejs/r59/TrackballControls.js",cb)
-        (cb) -> load("/static/threejs/Detector.js",cb)
+        (cb) -> load("/static/threejs/r#{VERSION}/three.min.js",cb)
+        (cb) -> load("/static/threejs/r#{VERSION}/TrackballControls.js",cb)
+        (cb) -> load("/static/threejs/r#{VERSION}/Detector.js",cb)
         (cb) ->
             f = () ->
                 if THREE?
@@ -82,7 +87,14 @@

     init: () =>
         @scene = new THREE.Scene()
-        @opts.width  = if opts.width? then opts.width else $(window).width()*.9
+        # IMPORTANT: There is a major bug in three.js -- if you make the width below more than .5 of the window
+        # width, then after 8 3d renders, things get foobared in WebGL mode.  This happens even with the simplest
+        # demo using the basic cube example from their site with R68.
+        if opts.width
+            @opts.width = Math.min(opts.width, $(window).width()*.5)
+        else
+            @opts.width  = $(window).width()*.5
+
         @opts.height = if opts.height? then opts.height else $(window).height()*.6

         if not @opts.renderer?
@@ -468,7 +480,7 @@
     render_scene: (force=false) =>
         #console.log('render', @opts.element.length)
         @opts.element.show()
-
+
         if @controls?
             @controls?.update()
         else


oops forgot to include timestamp in stats

--- salvus/rethink.coffee
+++ salvus/rethink.coffee
@@ -2099,6 +2099,8 @@
             if x.expire > now
                 y = misc.copy(x); delete y.expire
                 stats.hub_servers.push(y)
+
+        stats.time = new Date()
         return stats

     ###


Update loading text

Update loading text

--- app/assets/javascripts/home.coffee
+++ app/assets/javascripts/home.coffee
@@ -10,7 +10,7 @@
     b = baffle('.sentence-container')
     be = baffle('.error-container')
     b.text(->
-      'Generating...'
+      'Generating... (May take a minute or two)'
     ).reveal 3000
     $('.sentence-container').show 0


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -226,9 +226,9 @@
                 return color(i)
             )

-        #this.path.transition()
-        #    .duration(300)
-        #    .attrTween("d", that.pieTween);
+        path.transition()
+            .duration(300)
+            .attrTween("d", pieTween);

         #this.path.exit()
         #    .transition()


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -5,7 +5,8 @@
   onData: (data) ->
     $(@node).fadeOut().fadeIn()
     @buildPieStructure()
-    @render(data.data)
+    #@render(data.data)
+    @update(data.data)

   buildPieStructure: ->
     @width = 300


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -208,7 +208,7 @@
         path.transition().duration(2000).attrTween('d', (d,i) ->
             console.log('1. in tween - window.oldPieData[' + myInstanceDataId + '][' + i + ']: ',window.oldPieData[myInstanceDataId][i])
             console.log('1. in tween - d: ',d)
-            myInterpolater = d3.interpolate(window.oldPieData[myInstanceDataId][i] ? null, d)
+            myInterpolater = d3.interpolate(window.oldPieData[myInstanceDataId][i] ? d, d)
             window.oldPieData[myInstanceDataId][i] = myInterpolater(0) ? null
             console.log('2. in tween - window.oldPieData[' + myInstanceDataId + '][' + i + ']: ',window.oldPieData[myInstanceDataId][i])
             console.log('2. in tween - d: ',d)


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -105,7 +105,7 @@
         r = labelRadius
         if x <= 0
             r = -r
-        if x < 0 and y > 0
+        if x < 0 and y < 0
             x = x * mx
         return "translate(" + (x/h * labelRadius) + "," + (y/h * labelRadius) + ")"
         #return "translate(" + r + "," + y + ")"#(y/h * labelRadius) + ")"


use public interface of View

use public interface of View

--- app/assets/javascripts/view.coffee
+++ app/assets/javascripts/view.coffee
@@ -90,7 +90,7 @@
 				g.setVertices (View.createArrayBufferObject g.normalVertices), g.normalVertices.length

 				#sends current position to Model for preloading data
-				Model.Binary.ping(transMatrix)?.done(renderFunction)
+				Model.Binary.ping(transMatrix)?.done(View.draw)

 				#sends current position to Model for caching route
 				Model.Route.put cam.getPos(), null


inversed

inversed

--- app/assets/javascripts/view.coffee
+++ app/assets/javascripts/view.coffee
@@ -244,7 +244,7 @@

 		#Rotate up
 		if buttonDown
-			cam.pitchDistance (curCoords[1]-mouseY)/mouseRotateDivision
+			cam.pitchDistance -(curCoords[1]-mouseY)/mouseRotateDivision
 			curCoords[1] = mouseY

 		if keyboard.isKeyDown(KEY_UP)
@@ -252,7 +252,7 @@

 		#Rotate down
 		if buttonDown
-			cam.pitchDistance -(curCoords[1]-mouseY)/mouseRotateDivision
+			cam.pitchDistance (curCoords[1]-mouseY)/mouseRotateDivision
 			curCoords[1] = mouseY

 		if keyboard.isKeyDown(KEY_DOWN)


alert when finished (for QA only). Add details to Geordi logging.

--- app/models/experimental_subject.coffee
+++ app/models/experimental_subject.coffee
@@ -35,9 +35,9 @@
       subject = @first()
       if Experiments.ACTIVE_EXPERIMENT? && Experiments.ACTIVE_EXPERIMENT=="SerengetiInterestingAnimalsExperiment1"
         if Experiments.sources[subject.zooniverseId] == Experiments.SOURCE_INSERTED
-          AnalyticsLogger.logEvent 'insertion','specific'
+          AnalyticsLogger.logEvent 'insertion','specific',User.current?.zooniverse_id,subject.zooniverseId
         else if Experiments.sources[subject.zooniverseId] == Experiments.SOURCE_RANDOM
-          AnalyticsLogger.logEvent 'insertion','random'
+          AnalyticsLogger.logEvent 'insertion','random',User.current?.zooniverse_id,subject.zooniverseId
         @markAsSeen subject.zooniverseId
       subject.select()

@@ -72,7 +72,10 @@
           Experiments.currentParticipant = response.participant
           nextSubjectIDs = response.nextSubjectIDs
           if nextSubjectIDs? && nextSubjectIDs.length == 0
-            # TODO add some fallback code here (end of experiment)
+            Experiments.currentParticipant.cohort = Experiments.COHORT_CONTROL
+            Experiments.currentCohort = Experiments.COHORT_CONTROL
+            Experiments.currentParticipant.active = false
+            alert "You have now completed your experiment. Thanks for participating"
           else
             for subjectID in nextSubjectIDs
               do (subjectID) =>


Fix column positioning.

Fix column positioning.

--- lib/transpiler.coffee
+++ lib/transpiler.coffee
@@ -103,7 +103,7 @@
                 detail: "#{msgRet.err.message}\n \n#{msgRet.babelCoreUsed}\n \n#{msgRet.err.codeFrame}"
             # if we have a line/col syntax error jump to the position
             if msgRet.err.loc?.line? and textEditor?
-              textEditor.setCursorBufferPosition [msgRet.err.loc.line-1, msgRet.err.loc.column-1]
+              textEditor.setCursorBufferPosition [msgRet.err.loc.line-1, msgRet.err.loc.column]
         else
           if not config.suppressTranspileOnSaveMessages
             atom.notifications.addInfo "LB: Babel v#{msgRet.babelVersion} Transpiler Success",


deactivate bubbles if user clicks outside of a bubble

--- coffee/resume.coffee
+++ coffee/resume.coffee
@@ -206,8 +206,41 @@
 			# set options
 			_.extend @options, options

+			# attach DOM events
+			@attachEvents()
+
 			# render it!
 			@render()
+
+		attachEvents: ->
+
+			document.addEventListener 'click', (e) => @clickBody e
+
+		clickBody: (event) ->
+
+			element = event.target
+			isCircle = @isCircle element
+			isDetails = @getDetails element
+
+			if not isCircle and not isDetails
+
+				@deactivate()
+
+		isCircle: (element) ->
+
+			element.tagName is 'circle'
+
+		isDetails: (element) ->
+
+			element.id is 'details'
+
+		getDetails: (element) ->
+
+			while element isnt document
+
+				return element if @isDetails element
+
+				element = element.parentNode

 		render: ->

@@ -272,26 +305,35 @@

 						, 200

-		click: (element) ->
+		deactivate: ->
+
+			circle = document.querySelector 'circle.active'
+			pane = document.querySelector '.detail.active'
+
+			if circle
+				circle.classList.remove 'active'
+
+			if pane
+				pane.classList.remove 'active'
+
+		activate: (element) ->

 			id = element.node.getAttribute 'data-id'
-
-			# deactivate others?
-			active = document.querySelector('circle.active')
-			if active
-				active.classList.remove 'active'

 			# activate this
 			element.node.classList.add 'active'

-			# deactivate other details?
-			active = document.querySelector('.detail.active')
-			if active
-				active.classList.remove 'active'
-
 			# activate this detail panel
 			document.querySelectorAll('.detail')[id].classList.add 'active'

+		click: (element) ->
+
+			# deactivate others?
+			@deactivate()
+
+			# activate this
+			@activate element
+
 		over: (element) ->

 		out: (element) ->


ignore deleted tags and dont travel any further

--- src/linker.coffee
+++ src/linker.coffee
@@ -102,6 +102,7 @@
         do repeat
     # register checker for closing tags
     tpl.register 'end', (tag, next) ->
+        delete tag._elems if tag.closed is 'removed'
         elems = tag._elems
         # when this is a tag created from data structure
         return next(tag) unless elems?


It's just gonna be "urls"

It's just gonna be "urls"

--- app/pages/lab/project-details.cjsx
+++ app/pages/lab/project-details.cjsx
@@ -21,11 +21,11 @@

-          {for link, i in @props.project.external_links
+          {for link, i in @props.project.urls
             link._key ?= Math.random()

-              <input type="text" name="external_links.#{i}.label" value={link.label} onChange={@handleChange}/>
-              <input type="text" name="external_links.#{i}.url" value={link.url} onChange={@handleChange}/>
+              <input type="text" name="urls.#{i}.label" value={link.label} onChange={@handleChange}/>
+              <input type="text" name="urls.#{i}.url" value={link.url} onChange={@handleChange}/>

}

@@ -35,7 +35,7 @@

   handleAddLink: ->
     changes = {}
-    changes["external_links.#{@props.project.external_links.length}"] =
+    changes["urls.#{@props.project.urls.length}"] =
       label: 'Example'
       url: 'https://example.com/'
     @props.project.update changes


Use new Git::getCachedPathStatus API

Use new Git::getCachedPathStatus API

--- lib/file.coffee
+++ lib/file.coffee
@@ -57,7 +57,7 @@
     if repo.isPathIgnored(@path)
       newStatus = 'ignored'
     else
-      status = repo.statuses[@path]
+      status = repo.getCachedPathStatus(@path)
       if repo.isStatusModified(status)
         newStatus = 'modified'
       else if repo.isStatusNew(status)


Fix naming

Fix naming

--- app/scripts/lib/LazerScript.coffee
+++ app/scripts/lib/LazerScript.coffee
@@ -58,7 +58,7 @@

       return if times is 0
       WhenJS(block(sequence)).then =>
-        @repeat(times - 1, event)(sequence)
+        @repeat(times - 1, block)(sequence)

   runScript: (scriptClass, args...) ->
     (sequence) =>


handle bad results more elegantly

handle bad results more elegantly

--- scripts/dgshow.coffee
+++ scripts/dgshow.coffee
@@ -22,6 +22,8 @@
       .get() (err, res, body) ->
         if res.statusCode == 404
           msg.send 'Can\'t connect to the API.'
+        else if JSON.parse(body).length == 0
+          msg.send 'No posts!'
         else
           object = JSON.parse(body)[0]
           msg.send object.link
@@ -29,10 +31,12 @@
   robot.respond /dgshow ep(\d+)/i, (msg) ->
     num = "#{msg.match[1]}"

-    msg.http("http://dgshow.org/wp-json/posts?filter[s]=%23#{num}")
+    msg.http("http://dgshow.org/wp-json/posts?filter[s]=%23#{num}%3A")
       .get() (err, res, body) ->
         if res.statusCode == 404
-          msg.send 'Episode #{num} not found.'
+          msg.send 'Can\'t connect to the Wordpress API.'
+        else if JSON.parse(body).length == 0
+          msg.send "Episode #{num} not found."
         else
           object = JSON.parse(body)[0]
           msg.send object.link


Fixed ensure-login to properly redirect to login module.

--- src/server/common/AuthService.coffee
+++ src/server/common/AuthService.coffee
@@ -43,7 +43,7 @@
 service = {}

 service.authorization = [
-		ensure.ensureLoggedIn()
+		ensure.ensureLoggedIn "/login"
 		server.authorization (clientID, redirectURI, done) ->
 			db.clients.findByClientID clientID, (err, client) ->
 				if err then return done err
@@ -59,7 +59,7 @@
 	]

 service.decision = [
-		ensure.ensureLoggedIn()
+		ensure.ensureLoggedIn "/login"
 		server.decision()
 	]


New deployment hooks directory

New deployment hooks directory

--- code/serv.coffee
+++ code/serv.coffee
@@ -144,7 +144,7 @@

 exports.unix_user_add = (box_name, callback) ->
   cmd = """
-        cd /root/deployment-hooks && . lib/chroot_user &&
+        cd /opt/deployment-hooks && . lib/chroot_user &&
         export HOOKS_HOME=$(pwd) &&
         create_user #{box_name} &&
         create_user_directories #{box_name} &&


Allow \n as newline separator on Win32 in addition to \r\n

This should help with Debug.Trace.trace somewhat

--- lib/ghci.coffee
+++ lib/ghci.coffee
@@ -39,7 +39,7 @@
         buffer = ''
         (data) ->
           output = data.toString('utf8')
-          [first, mid..., last] = output.split(EOL)
+          [first, mid..., last] = output.split(/\r?\n/)
           buffer += first
           if last? # it means there's at least one newline
             lines = [buffer, mid...]


Stop interval until removed.

Stop interval until removed.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -98,21 +98,21 @@

         msg.send "#{msg.envelope.user.name}, you have " + points[username] + " points!"

-    setInterval (->
-        robot.http("https://tmi.twitch.tv/group/user/masonest/chatters").get() (err, res, body) ->
-            chat = JSON.parse(body)
-            people = flatten([chat.chatters.moderators, chat.chatters.staff, chat.chatters.admins, chat.chatters.global_mods, chat.chatters.viewers])
-
-            for username in people
-                unless username = "awebot"
-                    points[username] ?= 0
-                    points[username] += 5
-                save(robot)
-
-                # Winners (and then recall) is equal to all users
-                # with points, and their points respectively.
-                robot.brain.set 'winners', points
-    ), 60000
+    # setInterval (->
+    #     robot.http("https://tmi.twitch.tv/group/user/masonest/chatters").get() (err, res, body) ->
+    #         chat = JSON.parse(body)
+    #         people = flatten([chat.chatters.moderators, chat.chatters.staff, chat.chatters.admins, chat.chatters.global_mods, chat.chatters.viewers])
+    #
+    #         for username in people
+    #             unless username = "awebot"
+    #                 points[username] ?= 0
+    #                 points[username] += 5
+    #             save(robot)
+    #
+    #             # Winners (and then recall) is equal to all users
+    #             # with points, and their points respectively.
+    #             robot.brain.set 'winners', points
+    # ), 60000

     # Get a list of stored people and points
     robot.respond /getset$/i, (msg) ->


webapp/courses: mismatching keys when checking payment config choices

--- src/smc-webapp/course/actions.coffee
+++ src/smc-webapp/course/actions.coffee
@@ -150,7 +150,7 @@
         store = @get_store()
         return if not store?
         settings = store.get('settings')
-        if settings.get('student') or settings.get('institute')
+        if settings.get('institute_pay') or settings.get('student_pay')
             # already done
             return
         @set_pay_choice('institute', false)


choosing a better world for S-dog

choosing a better world for S-dog

--- scripts/sachin.coffee
+++ scripts/sachin.coffee
@@ -82,8 +82,8 @@
   robot.respond /learn punjabi with Sdog/i, (msg)->
     msg.send msg.random punjabi

-  robot.respond /introduce Sachin/i, (msg)->
-    msg.send "Bloodies son of mother war. Breaker of swords. Maker of orphans. Sachin the Magnificent. "
+  robot.respond /announce Sachin/i, (msg)->
+    msg.send "Bloodies son of the mother war. Breaker of swords. Maker of orphans. Sachin the Magnificent. Beware of his treachery."

   robot.respond /sachin super duper bomb( (\d+))?/i, (msg)->
     count = msg.match[2] || 5


slight change to previous jupyter load improvements

--- src/smc-webapp/editor_jupyter.coffee
+++ src/smc-webapp/editor_jupyter.coffee
@@ -138,7 +138,7 @@
         # wait until connected -- iT is ***critical*** to wait until
         # the kernel is connected before doing anything else!
         start = new Date()
-        max_time_ms = 15*1000 # try for up to 15s
+        max_time_ms = 20*1000 # try for up to 20s
         f = () =>
             @frame ?= window.frames[@iframe_uuid]
             if not @frame
@@ -173,13 +173,11 @@
                         @dirty_interval = setInterval(@check_dirty, 250)
                         @nb.events.on('spec_changed.Kernel', => @nb.dirty = true)
                         @init_cursor()
-                    f = () =>
-                        @monkey_patch_frame()
-                        @disable_autosave()
-                        @state = 'ready'
-                        @emit('ready')
-                        cb()
-                    setTimeout(f, 1000)  # give it a second "for luck"
+                    @monkey_patch_frame()
+                    @disable_autosave()
+                    @state = 'ready'
+                    @emit('ready')
+                    cb()
                 else
                     # not yet connected, so try again shortly
                     setTimeout(f, 250)


Remove PromiseRenderer from the favourites button

--- app/collections/favorites-button.cjsx
+++ app/collections/favorites-button.cjsx
@@ -3,7 +3,6 @@
 getFavoritesName = require './get-favorites-name'
 alert = require '../lib/alert'
 SignInPrompt = require '../partials/sign-in-prompt'
-PromiseRenderer = require '../components/promise-renderer'

 module.exports = React.createClass
   displayName: 'CollectionFavoritesButton'
@@ -19,13 +18,11 @@
     user: null

   getInitialState: ->
-    favoritesPromise: null
-    favoritedPromise: null
-    favorites: {}
+    favorites: null
     favorited: false

   contextTypes:
-    geordi: React.PropTypes.object
+    geordi: React.PropTypes.object

   logSubjLike: (liked) ->
     @context.geordi?.logEvent
@@ -47,23 +44,25 @@
       favorites.get('subjects', id: @props.subject.id)
         .then ([subject]) -> subject?
     else
-      Promise.resolve(false)
+      false

   componentWillMount: ->
     # see if the subject is in the project's favorites collection
     # to see if it's favorites
-    favoritesPromise = @findFavoriteCollection()
-    favoritedPromise = favoritesPromise.then (favs) => @findSubjectInCollection(favs)
-
-    @setState {favoritesPromise, favoritedPromise}
+    @findFavoriteCollection()
+      .then (favorites) =>
+        @setState {favorites}
+        @findSubjectInCollection(favorites)
+      .then (favorited) =>
+        @setState {favorited}

   addSubjectTo: (collection) ->
+    @setState favorited: true
     collection.addLink('subjects', [@props.subject.id.toString()])
-      .then (collection) => @setState favoritedPromise: Promise.resolve(true)

   removeSubjectFrom: (collection) ->
+    @setState favorited: false
     collection.removeLink('subjects', [@props.subject.id.toString()])
-      .then (collection) => @setState favoritedPromise: Promise.resolve(false)

   createFavorites: ->
     display_name = getFavoritesName(@props.project)
@@ -75,37 +74,33 @@
     links.projects = [ project ] if project?
     collection = {favorite, display_name, links}

-    apiClient.type('collections').create(collection).save().then =>
-      @setState favoritedPromise: Promise.resolve(true)
+    @setState favorited: true
+    apiClient.type('collections').create(collection).save()

   toggleFavorite: ->
     if @props.user?
-      Promise.all([@state.favoritesPromise, @state.favoritedPromise])
-        .then ([favorites, favorited]) =>
-          if not favorites?
-            @createFavorites()
-            @logSubjLike 'favorite'
-          else if favorited
-            @removeSubjectFrom(favorites)
-            @logSubjLike 'unfavorite'
-          else
-            @addSubjectTo(favorites)
-            @logSubjLike 'favorite'
+      if not @state.favorites?
+        @createFavorites()
+        @logSubjLike 'favorite'
+      else if @state.favorited
+        @removeSubjectFrom(@state.favorites)
+        @logSubjLike 'unfavorite'
+      else
+        @addSubjectTo(@state.favorites)
+        @logSubjLike 'favorite'
     else
       @promptToSignIn()
       @logSubjLike 'favorite'

   render: ->
-    <PromiseRenderer promise={@state.favoritedPromise}>{(favorited) =>
-      <button
-        className="favorites-button #{@props.className ? ''}"
-        type="button"
-        title={if favorited then 'Unfavorite' else 'Favorite'}
-        onClick={@toggleFavorite}>
-        <i className="
-          fa fa-heart#{if favorited then '' else '-o'}
-          #{if favorited then 'favorited' else ''}
-          fa-fw
-        " />
-      </button>
-    }</PromiseRenderer>
+    <button
+      className="favorites-button #{@props.className ? ''}"
+      type="button"
+      title={if @state.favorited then 'Unfavorite' else 'Favorite'}
+      onClick={@toggleFavorite}>
+      <i className="
+        fa fa-heart#{if @state.favorited then '' else '-o'}
+        #{if @state.favorited then 'favorited' else ''}
+        fa-fw
+      " />
+    </button>


replaced

replaced

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -1,62 +0,0 @@
-# Description:
-#   Create new cards in Trello
-#
-# Dependencies:
-#   "node-trello": "latest"
-#
-# Configuration:
-#   HUBOT_TRELLO_KEY - Trello application key
-#   HUBOT_TRELLO_TOKEN - Trello API token
-#   HUBOT_TRELLO_LIST - The list ID that you'd like to create cards for
-#
-# Commands:
-#   hubot trello card <name> - Create a new Trello card
-#   hubot trello show - Show cards on list
-#
-# Notes:
-#   To get your key, go to: https://trello.com/1/appKey/generate
-#   To get your token, go to: https://trello.com/1/authorize?key=<<your key>>&name=Hubot+Trello&expiration=never&response_type=token&scope=read,write
-#   Figure out what board you want to use, grab it's id from the url (https://trello.com/board/<<board name>>/<<board id>>)
-#   To get your list ID, go to: https://trello.com/1/boards/<<board id>>/lists?key=<<your key>>&token=<<your token>>  "id" elements are the list ids.
-#
-# Author:
-#   carmstrong
-
-module.exports = (robot) ->
-  robot.respond /trello card (.*)/i, (msg) ->
-    cardName = msg.match[1]
-    if not cardName.length
-      msg.send "You must give the card a name"
-      return
-    if not process.env.HUBOT_TRELLO_KEY
-      msg.send "Error: Trello app key is not specified"
-    if not process.env.HUBOT_TRELLO_TOKEN
-      msg.send "Error: Trello token is not specified"
-    if not process.env.HUBOT_TRELLO_LIST
-      msg.send "Error: Trello list ID is not specified"
-    if not (process.env.HUBOT_TRELLO_KEY and process.env.HUBOT_TRELLO_TOKEN and process.env.HUBOT_TRELLO_LIST)
-      return
-    createCard msg, cardName
-
-  robot.respond /trello show/i, (msg) ->
-    showCards msg
-
-createCard = (msg, cardName) ->
-  Trello = require("node-trello")
-  t = new Trello(process.env.HUBOT_TRELLO_KEY, process.env.HUBOT_TRELLO_TOKEN)
-  t.post "/1/cards", {name: cardName, idList: process.env.HUBOT_TRELLO_LIST}, (err, data) ->
-    if err
-      msg.send "There was an error creating the card"
-      return
-    msg.send data.url
-
-showCards = (msg) ->
-  Trello = require("node-trello")
-  t = new Trello(process.env.HUBOT_TRELLO_KEY, process.env.HUBOT_TRELLO_TOKEN)
-  t.get "/1/lists/"+process.env.HUBOT_TRELLO_LIST, {cards: "open"}, (err, data) ->
-    if err
-      msg.send "There was an error showing the list."
-      return
-
-    msg.send "Cards in " + data.name + ":"
-    msg.send "- " + card.name for card in data.cards


Update pp talk

Update pp talk

--- scripts/banban.coffee
+++ scripts/banban.coffee
@@ -15,3 +15,6 @@

     robot.hear /boss/ig, (res) ->
       res.send "B0ss是他馬的爽兵!"
+
+	robot.hear /pp/i, (res) ->
+	  res.send "PP一進來怎麼就滿屋子菜味?"


yeeeah save last

yeeeah save last


updated tests

updated tests

--- test/index.coffee
+++ test/index.coffee
@@ -278,8 +278,10 @@
             url.should.be 'https://twitter.com/intent/tweet?text=Hello%20World'

           Clay 'client.share.any', [{text: 'Hello World'}], (err, res) ->
-            openCnt.should.be 1
-            done(err)
+            setTimeout ->
+              openCnt.should.be 1
+              done(err)
+            , 30

     describe 'register', ->
       before ->


#398 just reset-global-state is enough to refresh

--- lib/main.coffee
+++ lib/main.coffee
@@ -55,11 +55,13 @@
     @subscribe atom.workspace.onDidChangeActivePane ->
       workspaceClassList.remove('vim-mode-plus-pane-maximized', 'hide-tab-bar')

-    @subscribe settings.observe 'highlightSearch', (newValue) =>
+    @subscribe settings.observe 'highlightSearch', (newValue) ->
       if newValue
-        @refreshHighlightSearchForVisibleEditors()
+        # Re-setting value trigger highlightSearch refresh
+        value = globalState.get('highlightSearchPattern')
+        globalState.set('highlightSearchPattern', value)
       else
-        @clearHighlightSearchForEditors()
+        globalState.set('highlightSearchPattern', null)

   observeVimMode: (fn) ->
     fn() if atom.packages.isPackageActive('vim-mode')
@@ -80,15 +82,6 @@
     @vimStatesByEditor.forEach(fn)
     @onDidAddVimState(fn)

-  refreshHighlightSearchForVisibleEditors: ->
-    for editor in getVisibleEditors()
-      @getEditorState(editor).highlightSearch.refresh()
-
-  clearHighlightSearchForEditors: ->
-    for editor in atom.workspace.getTextEditors()
-      @getEditorState(editor).highlightSearch.clearMarkers()
-    globalState.set('highlightSearchPattern', null)
-
   clearRangeMarkerForEditors: ->
     for editor in atom.workspace.getTextEditors()
       @getEditorState(editor).clearRangeMarkers()
@@ -108,7 +101,7 @@
     @subscribe atom.commands.add 'atom-text-editor:not([mini])',
       # One time clearing highlightSearch. equivalent to `nohlsearch` in pure Vim.
       # Clear all editor's highlight so that we won't see remaining highlight on tab changed.
-      'vim-mode-plus:clear-highlight-search': => @clearHighlightSearchForEditors()
+      'vim-mode-plus:clear-highlight-search': -> globalState.set('highlightSearchPattern', null)
       'vim-mode-plus:toggle-highlight-search': -> settings.toggle('highlightSearch')
       'vim-mode-plus:clear-range-marker': => @clearRangeMarkerForEditors()


Fixed response object is not an array when fetching latest release only

--- lib/main.coffee
+++ lib/main.coffee
@@ -27,7 +27,7 @@

   # do the api call
   apiUrl = sprintf baseApiUrl, repo
-  if includePreReleases then apiUrl += '/latest'
+  if not includePreReleases then apiUrl += '/latest'
   got(apiUrl).then((response) ->
     # parse the response body into an object
     releases = JSON.parse response.body
@@ -45,11 +45,10 @@
           callback release, null
           break
     else
-      latest = releases[0]
-      tag = latest.tag_name.replace(/[^0-9$.,]/g, '')
+      tag = releases.tag_name.replace(/[^0-9$.,]/g, '')
       if semcmp(currentVersion, tag) is -1
         found = true
-        callback latest, null
+        callback releases, null

     callback(null, null) if not found
   ).catch((error) ->


code layout: order change

code layout: order change

--- lib/main.coffee
+++ lib/main.coffee
@@ -51,11 +51,59 @@
   subscribe: (arg) ->
     @subscriptions.add(arg)

-  getScopeNameAtCursor: (cursor) ->
-    scopeNames = (g.scopeName for g in atom.grammars.getGrammars())
-    scopes = cursor.getScopeDescriptor().getScopesArray().reverse()
-    return scope for scope in scopes when scope in scopeNames
+  getWord: (text, scopeName) ->
+    findWord = (text, setOfWords) ->
+      for words in setOfWords when (index = words.indexOf(text)) >= 0
+        return words[(index + 1) % words.length]

+    if (newText = findWord(text, @getUserWordGroup(scopeName)))?
+      return newText
+
+    if scopeName in settings.get('defaultWordGroupExcludeScope')
+      return null
+
+    if (newText = findWord(text, @getDefaultUserGroup(scopeName)))?
+      return newText
+
+  # Where: ['here', 'there', 'visit']
+  toggle: (where) ->
+    @editor = atom.workspace.getActiveTextEditor()
+    @editor.transact =>
+      @toggleWord(cursor, where) for cursor in @editor.getCursors()
+
+  toggleWord: (cursor, where) ->
+    pattern = /\b\w+\b/g
+    cursorPosition = cursor.getBufferPosition()
+    scanRange = @editor.bufferRangeForBufferRow(cursorPosition.row)
+    scopeNames = [@getScopeNameAtCursor(cursor), '*']
+
+    @editor.scanInBufferRange pattern, scanRange, ({range, replace, matchText, stop, match}) =>
+      return unless @isValidTarget(where, range, cursorPosition)
+      for scopeName in scopeNames when (newText = @getWord(matchText, scopeName))?
+        stop()
+        newRange = replace(newText)
+        newStart = newRange.start
+        @flashRange(newRange) if range.start.isGreaterThan(cursorPosition)
+        if (where in ['visit', 'here']) or newStart.isLessThan(cursorPosition)
+          cursor.setBufferPosition(newStart)
+        break
+
+  isValidTarget: (where, range, point) ->
+    if where is 'here'
+      range.containsPoint(point)
+    else
+      range.end.isGreaterThanOrEqual(point)
+
+  flashRange: (range) ->
+    marker = @editor.markBufferRange(range)
+    @editor.decorateMarker(marker, {type: 'highlight', class: 'toggle-flash'})
+    timeout = settings.get('flashDurationMilliSeconds')
+    setTimeout ->
+      marker.destroy()
+    , timeout
+
+  # Word group
+  # -------------------------
   getUserWordGroupPath: ->
     fs.normalize(settings.get('configPath'))

@@ -93,59 +141,12 @@
       CSON ?= require 'season'
       return CSON.readFileSync(filePath) or {}
     catch error
-      message = '[toggle] config file has error'
-      options =
-        detail: error.message
-      atom.notifications.addError message, options
+      atom.notifications.addError('[toggle] config file has error', detail: error.message)
     {}

-  getWord: (text, scopeName) ->
-    findWord = (text, setOfWords) ->
-      for words in setOfWords when (index = words.indexOf(text)) >= 0
-        return words[(index + 1) % words.length]
-
-    if (newText = findWord(text, @getUserWordGroup(scopeName)))?
-      return newText
-
-    if scopeName in settings.get('defaultWordGroupExcludeScope')
-      return null
-
-    if (newText = findWord(text, @getDefaultUserGroup(scopeName)))?
-      return newText
-
-  flashRange: (range) ->
-    marker = @editor.markBufferRange(range)
-    @editor.decorateMarker(marker, {type: 'highlight', class: 'toggle-flash'})
-    setTimeout ->
-      marker.destroy()
-    , settings.get('flashDurationMilliSeconds')
-
-  isValidTarget: (where, range, point) ->
-    if where is 'here'
-      range.containsPoint(point)
-    else
-      range.end.isGreaterThanOrEqual(point)
-
-  toggleWord: (cursor, where) ->
-    pattern = /\b\w+\b/g
-    cursorPosition = cursor.getBufferPosition()
-    scanRange = @editor.bufferRangeForBufferRow(cursorPosition.row)
-    scopeNames = [@getScopeNameAtCursor(cursor), '*']
-
-    @editor.scanInBufferRange pattern, scanRange, ({range, replace, matchText, stop, match}) =>
-      return unless @isValidTarget(where, range, cursorPosition)
-      for scopeName in scopeNames when (newText = @getWord(matchText, scopeName))?
-        stop()
-        newRange = replace(newText)
-        newStart = newRange.start
-        @flashRange(newRange) if range.start.isGreaterThan(cursorPosition)
-        if (where in ['visit', 'here']) or newStart.isLessThan(cursorPosition)
-          cursor.setBufferPosition(newStart)
-        break
-
-  # options
-  # - where: ['here', 'there', 'visit']
-  toggle: (where) ->
-    @editor = atom.workspace.getActiveTextEditor()
-    @editor.transact =>
-      @toggleWord(cursor, where) for cursor in @editor.getCursors()
+  # Utils
+  # -------------------------
+  getScopeNameAtCursor: (cursor) ->
+    scopeNames = (g.scopeName for g in atom.grammars.getGrammars())
+    scopes = cursor.getScopeDescriptor().getScopesArray().reverse()
+    return scope for scope in scopes when scope in scopeNames


Fix deprecation warning

Fix deprecation warning

--- lib/main.coffee
+++ lib/main.coffee
@@ -26,7 +26,7 @@
       @deprecationCopStatusBarView ?= new DeprecationCopStatusBarView()
       workspaceElement = atom.views.getView(atom.workspace)
       statusBar = workspaceElement.querySelector('.status-bar')
-      statusBar?.appendRight(@deprecationCopStatusBarView)
+      statusBar?.addRightTile(item: @deprecationCopStatusBarView)
       activatedDisposable.dispose()

     @commandSubscription = atom.commands.add 'atom-workspace', 'deprecation-cop:view', ->


Added currencyfield snippet

Added currencyfield snippet

--- completions.cson
+++ completions.cson
@@ -1644,6 +1644,17 @@
   )${0}
   """

+'CurrencyField':
+  'prefix': 'currencyfield'
+  'module': 'silverstripe/framework'
+  'scope': '.text.html.php'
+  'body': """
+  CurrencyField::create(
+  \t\'${1:name}\',
+  \t\'${2:${1:title}}\'
+  )${0}
+  """
+
 'NumericField_Readonly':
   'prefix': 'numericfield_readonly'
   'module': 'silverstripe/framework'


bugfix config

bugfix config

--- lib/mqtt.coffee
+++ lib/mqtt.coffee
@@ -9,4 +9,4 @@
   client.on 'close', -> console.log 'Connection closed'

   publish: (topic, data) ->
-    client.publish "#{config.mqtt.topicNs}#{topic}", JSON.stringify data
+    client.publish "#{mqttConfig.topicNs}#{topic}", JSON.stringify data


Improved Regex

Improved Regex

--- scripts/mumble.coffee
+++ scripts/mumble.coffee
@@ -82,12 +82,12 @@

   _message: (msg) ->
     commands = [
-      { regex: /!p(ause)?/, fn: () => @player.pause() },
-      { regex: /!r(esume)?/, fn: () => @player.resume() },
-      { regex: /!s(kip)?/, fn: () => @player.skip() },
-      { regex: /!a(dd)? ([^\s]+)/, fn: (match) => @player.add(match[2]) },
-      { regex: /!c(lear)?/, fn: () => @player.clear() },
-      { regex: /!vol(ume)? ([^\s]+)/, fn: (match) => @player.volume(match[2]) }
+      { regex: /^!p(ause)?$/, fn: () => @player.pause() },
+      { regex: /^!r(esume)?$/, fn: () => @player.resume() },
+      { regex: /^!s(kip)?$/, fn: () => @player.skip() },
+      { regex: /^!a(dd)? ([^\s]+)$/, fn: (match) => @player.add(match[2]) },
+      { regex: /^!c(lear)?$/, fn: () => @player.clear() },
+      { regex: /^!v(ol|olume)? ([^\s]+)$/, fn: (match) => @player.volume(match[2]) }
     ]

     c.fn(msg.match(c.regex)) for c in commands when c.regex.test(msg)


payment: expose logOrder method to client

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -22,9 +22,12 @@
           (signature Object, Function)
         canChangePlan     :
           (signature Object, Function)
+        logOrder          :
+          (signature Object, Function)

   { get, post } = require "./socialapi/requests"
+

   @subscribe = secure (client, data, callback)->
     requiredParams = [
@@ -85,6 +88,37 @@

     validateParams requiredParams, data, (err)->
       canChangePlan client, data.planTitle, callback
+
+
+  @logOrder = secure (client, raw, callback)->
+    requiredParams = [
+      "planTitle", "planAmount", "binNumber", "lastFour", "cardName"
+    ]
+
+    validateParams requiredParams, raw, (err)->
+      return callback err  if err
+
+      {planTitle, planAmount, binNumber, lastFour, cardName} = raw
+
+      data = {
+        "$type"               : "$create_order"
+        "$currency_code"      : "USD"
+        "$amount"             : planAmount*1000000
+        "$payment_methods"    : [
+          "$payment_type"     : "$credit_card"
+          "$payment_gateway"  : "$stripe"
+          "$card_bin"         : binNumber
+          "$card_last4"       : lastFour
+        ]
+        "$billing_address"    :
+          "$name"             : cardName
+        "$items"              : [
+          "$item_id"          : planTitle
+          "$price"            : planAmount*1000000
+        ]
+      }
+
+      logToSiftScience client, "create_order", data, callback

   validateParams = (requiredParams, data, callback)->
@@ -157,36 +191,44 @@
     JReferral.fetchEarnedSpace client, callback

+  fetchUserInfo = (client, callback)->
+    {sessionToken, connection : {delegate}} = client
+    delegate.fetchUser (err, user)->
+      return callback err  if err
+
+      {username, email} = user
+
+      callback null, {username, email, sessionToken}
+
   logTransaction = (client, raw, callback)->
-    {sessionToken, connection : {delegate}} = client
     {planTitle, planAmount, binNumber, lastFour, cardName, status} = raw

     return callback null  if planTitle is "free"

-    delegate.fetchUser (err, user)->
-      return callback err  if err
-
-      {username, email} = user
-
-      data = {
-        "$type"               : "$transaction"
-        "$transaction_status" : "$success"
-        "$currency_code"      : "USD"
-        "$user_id"            : username
-        "$user_email"         : email
-        "$session_id"         : sessionToken
-        "$amount"             : planAmount*1000000
-        "$payment_method"     :
-          "$payment_type"     : "$credit_card"
-          "$payment_gateway"  : "$stripe"
-          "$card_bin"         : binNumber
-          "$card_last4"       : lastFour
-        "$billing_address"    :
-          "$name"             : cardName
-      }
-
-      logToSiftScience "transaction", data, callback
-
-  logToSiftScience = (event, params, callback)->
+    data = {
+      "$type"               : "$transaction"
+      "$transaction_status" : "$success"
+      "$currency_code"      : "USD"
+      "$amount"             : planAmount*1000000
+      "$payment_method"     :
+        "$payment_type"     : "$credit_card"
+        "$payment_gateway"  : "$stripe"
+        "$card_bin"         : binNumber
+        "$card_last4"       : lastFour
+      "$billing_address"    :
+        "$name"             : cardName
+    }
+
+    logToSiftScience client, "transaction", data, callback
+
+  logToSiftScience = (client, event, data, callback)->
     siftScience = require('yield-siftscience') KONFIG.siftScience
-    siftScience.event[event] params, callback
+
+    fetchUserInfo client, (err, {username, email, sessionToken})->
+      return callback err   if err
+
+      data["$user_id"]    = username
+      data["$user_email"] = email
+      data["$session_id"] = sessionToken
+
+      siftScience.event[event] data, callback


JSession: activeSession: to fetch active session data

--- workers/social/lib/social/models/session.coffee
+++ workers/social/lib/social/models/session.coffee
@@ -42,8 +42,12 @@
       instance          : []
       static            : []
     sharedMethods       :
+      static            :
+        activeSession   :
+          (signature Function)
       instance          :
-        remove: (signature Function)
+        remove          :
+          (signature Function)

   do ->
     JAccount  = require './account'
@@ -55,6 +59,10 @@

         JSession.remove { username: oldUsername }, (err) ->
           console.error err  if err?
+
+
+  @activeSession = secure (client, callback) ->
+    @one { clientId: client.sessionToken }, callback

   # TODO not sure why we are creating session only for guest user


Social: remove dependency of RMQ from Tracker

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

 Analytics = require('analytics-node')
 analytics = new Analytics(KONFIG.segment)
-
-mqClient = null

 module.exports = class Tracker extends bongo.Base

@@ -91,8 +89,6 @@

     callback()

-  errMQClientNotSet = new Error 'Tracker: RabbitMQ client not set'
-
   @track = (username, event, options = {}, callback = -> ) ->

     return callback null  unless KONFIG.sendEventsToSegment
@@ -107,22 +103,8 @@
     event.from       or= defaultFromMail
     event.properties   = @addDefaults { options, username }

-    unless mqClient
-      console.error errMQClientNotSet
-      return callback errMQClientNotSet
-
-    sendMessage = ->
-      mqClient.exchange "#{exchangeName}", exchangeOpts, (exchange) ->
-        unless exchange
-          err = new Error "Tracker: Exchange not found to queue: #{exchangeName}"
-          console.error err
-          return callback err
-
-        exchange.publish '', event, { type: EVENT_TYPE }, (errored, err) ->
-          return callback err
-
-    if mqClient.readyEmitted then sendMessage()
-    else mqClient.on 'ready', -> sendMessage()
+    require("./socialapi/requests").publishMailEvent event, (err) =>
+      callback err # do not cause trailing parameters

   @page = (userId, name, category, properties) ->
@@ -162,6 +144,3 @@
     opts['env']      = KONFIG.environment
     opts['hostname'] = KONFIG.hostname
     opts
-
-
-  @setMqClient = (m) -> mqClient = m


snippets: update snippets

snippets: update snippets

--- snippets/language-coursescript.cson
+++ snippets/language-coursescript.cson
@@ -415,23 +415,34 @@
   'Speakinglink':
     'prefix': 'Speakingl'
     'body': """
-      [TYPE SL]
-      ID: $1
-      Audio: $2
-      Opts:
-        Text: $3
-        Text: $4
-        Text: $5
-        Text: $6
-        Text: $7
-        TR_text: $8
-    """
-    'RA':
+    [TYPE SL]
+    ID: $1
+    Audio: $2
+    Opts:
+      Text: $3
+      Text: $4
+      Text: $5
+      Text: $6
+      Text: $7
+      TR_text: $8
+    """
+  'RA':
       'prefix': 'RA'
       'body': """
-        [TYPE RA]
-        ID: $1
-        Pic: $2
-        Audio: $3
-        TR: $4
+      [TYPE RA]
+      ID: $1
+      Pic: $2
+      Audio: $3
+      TR: $4
       """
+  'note':
+      'prefix': 'note'
+      'body': """
+      \"""
+      note:
+      - title: $1
+        item:
+        - text: $2
+        - pic: $3
+      \"""
+      """


fix(snippets): update a2t2& p2t snippet

fix(snippets): update a2t2& p2t snippet

--- snippets/language-coursescript.cson
+++ snippets/language-coursescript.cson
@@ -570,6 +570,8 @@
       Opts:
       Text(checked=true): $3
       Text: $4
+      Text: $5
+      TR: $6
     """
   'DubShow':
     'prefix':'dubshow'
@@ -594,8 +596,7 @@
         Text: $4
         Text(checked=true): $5
         Text: $6
-        Text: $7
-      TR: $8
+      TR: $7
     """
   'A2P_MQSA':
     'prefix': 'mqsa'


add_error_hunting

--- snippets/language-coursescript.cson
+++ snippets/language-coursescript.cson
@@ -408,6 +408,14 @@
       Audio: $3
       Text: $4
     """
+  'Errorhunting':
+    'prefix': 'Errorhunting'
+    'body': """
+      [TYPE ErrorHunting]
+      ID: $1
+      Text: $2
+      TR: $3
+    """
   'Level':
     'prefix': "level"
     'body': """


Fixed single activity page's background

Fixed single activity page's background

--- server/lib/crawler/staticpages/activity.coffee
+++ server/lib/crawler/staticpages/activity.coffee
@@ -54,7 +54,7 @@
     <title>#{title} - Koding</title>
     #{getGraphMeta()}

-
+
       #{getDock()}


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/point.coffee
+++ test/point.coffee
@@ -12,7 +12,7 @@

     p2 = Point(p)

-    equal
+    equals p2.x

   test "#add", ->
     p1 = Point(5, 6)


payment: implement JPaymentPlan#removeByCode

--- workers/social/lib/social/models/payment/plan.coffee
+++ workers/social/lib/social/models/payment/plan.coffee
@@ -85,11 +85,16 @@
       @create client.context.group, formData, callback

   @removeByCode = (code, callback) ->
-    console.log { code }
-    callback message: 'needs to be implemented'
+    @one { code }, (err, plan) ->
+      return callback err  if err
+
+      unless plan?
+        return callback { message: 'Unrecognized plan code', code }
+
+      plan.remove callback

   @removeByCode$ = permit 'manage products',
-    success: (client, code, callback) -> @removeByCode formData, callback
+    success: (client, code, callback) -> @removeByCode code, callback

   @fetchAccountDetails = secure ({connection:{delegate}}, callback) ->
     console.error 'needs to be reimplemented'
@@ -107,6 +112,19 @@
     JPayment.invalidateCacheAndLoad this, selector, force, callback

   @fetchPlanByCode = (code, callback) -> @one { code }, callback
+
+  remove: (callback) ->
+    { code } = this
+    super (err) ->
+      if err
+        callback err
+      else if code?
+        recurly.deletePlan { code }, callback
+      else
+        callback null
+
+  remove$: permit 'manage products',
+    success: (client, callback) -> @remove callback

   fetchToken: secure (client, data, callback) ->
     JPaymentToken.createToken client, planCode: @code, callback


payment: represent product plans using relationships

--- workers/social/lib/social/models/payment/plan.coffee
+++ workers/social/lib/social/models/payment/plan.coffee
@@ -1,5 +1,6 @@
-jraphical = require 'jraphical'
-module.exports = class JPaymentPlan extends jraphical.Module
+{Module, Relationship} = require 'jraphical'
+
+module.exports = class JPaymentPlan extends Module

   force =
     forceRefresh  : yes
@@ -35,6 +36,8 @@
         'fetchToken'
         'subscribe'
         'modify'
+        'fetchProducts'
+        'updateProducts'
       ]
     schema          :
       planCode      :
@@ -68,6 +71,10 @@
         type        : Object
         default     : -> {}
       tags          : (require './schema').tags
+    relationships   :
+      product       :
+        targetType  : 'JPaymentProduct'
+        as          : 'plan product'

   # * It seems like we're stuffing some JSON into the description field
   #   on recurly.  I think that's a really bad idea, so let's store any
@@ -234,6 +241,45 @@
       JGroup.one _id: @product.category, (err, group)->
         callback err, unless err then group.slug

+  updateProducts: (quantities, callback) ->
+    JPaymentProduct = require './product'
+
+    planCodes = Object.keys quantities
+    productSelector = planCode: $in: planCodes
+
+    JPaymentProduct.some productSelector, { limit: 20 }, (err, products) =>
+      return callback err  if err
+
+      if products.length isnt planCodes.length
+        planCodesMap = planCodes.reduce( (acc, planCode) ->
+          acc[planCode] = yes
+          acc
+        , {})
+        products.forEach (product) ->
+          if planCodesMap.hasOwnProperty product.planCode
+            delete planCodesMap[product.planCode]
+        return callback {
+          message           : 'Unknown plan codes!'
+          unknownPlanCodes  : Object.keys planCodesMap
+        }
+
+      Relationship.remove {
+        sourceId    : @getId()
+        targetName  : 'JPaymentProduct'
+      }, (err) =>
+        return callback err  if err
+
+        queue = products.map (product) => =>
+          @addProduct product, (err) -> queue.fin err
+
+        dash queue, (err) =>
+          return callback err  if err
+          @modify { quantities }, (err) -> callback err
+
+  updateProducts$: permit 'manage products',
+    success: (client, quantities, callback) ->
+      @updateProducts quantities, callback
+
   @updateCache = (selector, callback)->
     # TODO: what on earth is the point of this? C
     JPayment.updateCache(


payment: revert removal of `JPaymentPlan#checkQuota` method

Required for subscription downgrade/upgrade procedure.

--- workers/social/lib/social/models/payment/plan.coffee
+++ workers/social/lib/social/models/payment/plan.coffee
@@ -10,6 +10,8 @@
   { v4: createId } = require 'node-uuid'

   {secure, dash, signature}        = require 'bongo'
+
+  {partition} = require 'bongo/lib/util'

   {permit}              = require '../group/permissionset'

@@ -46,6 +48,8 @@
           (signature Object, Object, Function)
         ]
         updateProducts:
+          (signature Object, Function)
+        checkQuota:
           (signature Object, Function)
         fetchCoupon:
           (signature String, Function)
@@ -253,5 +257,24 @@
       JGroup.one _id: @product.category, (err, group)->
         callback err, unless err then group.slug

+  checkQuota: (options, callback) ->
+    {usage, spend, couponCode, multiplyFactor} = options
+    multiplyFactor ?= 1
+
+    usages = for own planCode, quantity of spend
+      planSize    = @quantities[planCode] ? 0
+      usageAmount = usage[planCode] ? 0
+      spendAmount = (spend[planCode] ? 0) * multiplyFactor
+
+      total = planSize - usageAmount - spendAmount
+
+      { planCode, total }
+
+    [ok, over] = partition usages, ({ total }) -> total >= 0
+
+    if over.length > 0
+    then callback { message: 'quota exceeded', ok, over }
+    else callback null
+
   fetchCoupon: (type, callback) ->
     recurly.fetchCoupon @couponCodes[type], callback


added tests for setStartDate and setEndDate

--- test/specs.coffee
+++ test/specs.coffee
@@ -289,3 +289,39 @@
   it 'unsets value by passing null', ->
     @picker.setDate(null)
     expect(@input.val()).to.equal ''
+
+  it 'supports start date', ->
+    @picker.setDate(new Date(2002, 2, 15, 0, 0, 0, 0))
+    @picker.setStartDate(new Date(2002, 2, 14, 0, 0, 0, 0))
+    expect(@dateWidget.find('.datepicker-days .day:contains(13)').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-days .day:contains(14)').is '.disabled').to.be.false
+    expect(@dateWidget.find('.datepicker-days .prev').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-days .next').is '.disabled').to.be.false
+    @dateWidget.find('.datepicker-days .switch').click()
+    expect(@dateWidget.find('.datepicker-months .month:contains(Feb)').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-months .month:contains(Mar)').is '.disabled').to.be.false
+    expect(@dateWidget.find('.datepicker-months .prev').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-months .next').is '.disabled').to.be.false
+    @dateWidget.find('.datepicker-months .switch').click()
+    expect(@dateWidget.find('.datepicker-years .year:contains(2001)').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-years .year:contains(2002)').is '.disabled').to.be.false
+    expect(@dateWidget.find('.datepicker-years .prev').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-years .next').is '.disabled').to.be.false
+
+  it 'supports end date', ->
+    @picker.setDate(new Date(2002, 2, 15, 0, 0, 0, 0))
+    @picker.setEndDate(new Date(2002, 2, 16, 0, 0, 0, 0))
+    expect(@dateWidget.find('.datepicker-days .day:contains(17)').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-days .day:contains(16)').is '.disabled').to.be.false
+    expect(@dateWidget.find('.datepicker-days .next').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-days .prev').is '.disabled').to.be.false
+    @dateWidget.find('.datepicker-days .switch').click()
+    expect(@dateWidget.find('.datepicker-months .month:contains(Apr)').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-months .month:contains(Mar)').is '.disabled').to.be.false
+    expect(@dateWidget.find('.datepicker-months .next').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-months .prev').is '.disabled').to.be.false
+    @dateWidget.find('.datepicker-months .switch').click()
+    expect(@dateWidget.find('.datepicker-years .year:contains(2003)').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-years .year:contains(2002)').is '.disabled').to.be.false
+    expect(@dateWidget.find('.datepicker-years .next').is '.disabled').to.be.true
+    expect(@dateWidget.find('.datepicker-years .prev').is '.disabled').to.be.false


Fix ldap.  Checks server side changed after 0.35.0 (#155)


fix compressor

fix compressor


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

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

   ok obj.meter

-test "should set max<Attribute> if one doesn't exist", ->
+test "should set max<Attribute> if it isn't present in the in", ->
   obj = GameObject
     health: 150


query string in url is now working.

query string in url is now working.

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -43,6 +43,7 @@
       @eventUpdateFormValues()
       @eventSubmitForm()
       @eventGenerateOrderLink()
+      @eventFillCartFromQueryString()

   eventStorePageInit: ->
@@ -429,16 +430,35 @@
       $("#Nav-cart").click (event)=>
         event.preventDefault()
         link = $(event.target)
-        document.location = "?"+$.param(LocalStorage.dict())
-
-  # eventParseURL: ->
-  #   if $(@cartPage).length > 0
-  #     query = window.location.search.substring(1)
-  #     name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]")
-  #     regex = new RegExp("[\\?&]" + name + "=([^&#]*)")
-  #       results = regex.exec(location.search);
-  #   return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
-
+        data = []
+        for key,value of LocalStorage.dict()
+          orderKey = key
+          data.push({"name":"key","value":"#{orderKey}"})
+          for _k,v of value
+            name = "#{orderKey}"+"#{_k}"
+            data.push({"name":name,"value":v})
+        document.location = "?"+ $.param(data)
+
+  eventFillCartFromQueryString: ->
+    query = window.location.search.substring(1)
+    if query.length > 0
+      # cart?key=3grtmbgyuui8uxr&3grtmbgyuui8uxrorder=world_hs%2Ceye_30hz%2Clicense_academic&3grtmbgyuui8uxrqty=1&key=7ksn968mbj7cik9&7ksn968mbj7cik9order=world_hs%2Ceye_120hz_binocular%2Clicense_academic&7ksn968mbj7cik9qty=1&key=riv0r6jj5vu0udi&riv0r6jj5vu0udiorder=product_support_12&riv0r6jj5vu0udiqty=1
+      existingCartorderqty = LocalStorage.length()
+      LocalStorage.clear()
+      pairs = query.split('&')
+      while pairs.length > 0
+        key = pairs.shift().split("=").pop()
+        orderItems = decodeURIComponent(pairs.shift().split("=").pop()).split(',')
+        qty = parseInt(pairs.shift().split("=").pop())
+        item = {
+          "order" : orderItems
+          "qty"   : qty
+        }
+        LocalStorage.set(key, JSON.stringify(item))
+      if existingCartorderqty < 1
+        @eventRenderCart()
+      @eventUpdateCartNavCounter()
+

   _setOrderType: ->
     activeId = $("label[id^='OrderType-'][class~='Button--state-active']").attr('id').split('-').pop()


1.2.1 release

1.2.1 release

--- main.coffee
+++ main.coffee
@@ -4,32 +4,32 @@
   constraints: ($) ->
     if @options.constraints.verticalTop
       $ "// align #{@layers[0].name} and #{@layers[1].name} to top"
-      $ "view.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Top, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Top, multiplier: 1.0, constant: 0.0))"
+      $ "#{@options.superviewName}.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Top, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Top, multiplier: 1.0, constant: 0.0))"
       $.newline()

     if @options.constraints.verticalCenter
       $ "// center #{@layers[0].name} vertically with #{@layers[1].name}"
-      $ "view.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .CenterY, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .CenterY, multiplier: 1.0, constant: 0.0))"
+      $ "#{@options.superviewName}.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .CenterY, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .CenterY, multiplier: 1.0, constant: 0.0))"
       $.newline()

     if @options.constraints.verticalBottom
       $ "// align #{@layers[0].name} and #{@layers[1].name} to bottom"
-      $ "view.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Bottom, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Bottom, multiplier: 1.0, constant: 0.0))"
+      $ "#{@options.superviewName}.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Bottom, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Bottom, multiplier: 1.0, constant: 0.0))"
       $.newline()

     if @options.constraints.horizontalLeft
       $ "// align #{@layers[0].name} and #{@layers[1].name} to left"
-      $ "view.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Left, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Left, multiplier: 1.0, constant: 0.0))"
+      $ "#{@options.superviewName}.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Left, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Left, multiplier: 1.0, constant: 0.0))"
       $.newline()

     if @options.constraints.horizontalCenter
       $ "// center #{@layers[0].name} horizontally with #{@layers[1].name}"
-      $ "view.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .CenterX, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .CenterX, multiplier: 1.0, constant: 0.0))"
+      $ "#{@options.superviewName}.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .CenterX, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .CenterX, multiplier: 1.0, constant: 0.0))"
       $.newline()

     if @options.constraints.horizontalRight
       $ "// align #{@layers[0].name} and #{@layers[1].name} to right"
-      $ "view.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Right, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Right, multiplier: 1.0, constant: 0.0))"
+      $ "#{@options.superviewName}.addConstraint(NSLayoutConstraint(item: #{@layers[0].name}, attribute: .Right, relatedBy: .Equal, toItem: #{@layers[1].name}, attribute: .Right, multiplier: 1.0, constant: 0.0))"
       $.newline()

   render: ($) ->
@@ -37,12 +37,12 @@

     if @options.distance.x and @measures.distanceX >= 0
       $ "// views #{@layers[0].name}, #{@layers[1].name} horizontal distance"
-      $ "view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(\"H:[#{@layers[0].name}]-#{@measures.distanceX}-[#{@layers[1].name}]\", options: .DirectionLeadingToTrailing, metrics: nil, views: [\"#{@layers[0].name}\": #{@layers[1].name}]))"
+      $ "#{@options.superviewName}.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(\"H:[#{@layers[0].name}]-#{@measures.distanceX}-[#{@layers[1].name}]\", options: .DirectionLeadingToTrailing, metrics: nil, views: [\"#{@layers[0].name}\": #{@layers[1].name}]))"
       $.newline()

     if @options.distance.y and @measures.distanceY >= 0
       $ "// views #{@layers[0].name}, #{@layers[1].name} vertical distance"
-      $ "view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(\"V:[#{@layers[0].name}]-#{@measures.distanceY}-[#{@layers[1].name}]\", options: .DirectionLeadingToTrailing, metrics: nil, views: [\"#{@layers[0].name}\": #{@layers[1].name}]))"
+      $ "#{@options.superviewName}.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(\"V:[#{@layers[0].name}]-#{@measures.distanceY}-[#{@layers[1].name}]\", options: .DirectionLeadingToTrailing, metrics: nil, views: [\"#{@layers[0].name}\": #{@layers[1].name}]))"
       $.newline()


fix validation for division

fix validation for division

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -582,8 +582,8 @@
             $("input[id=#{stateId}]")
               .prop("placeholder", "state/province/division")
               .prop("required", true)
-              .prop("data-parsley-maxlength", "35")
-
+              .attr("data-parsley-maxlength", "35")
+              .attr("data-parsley-divisionvalidator","0")

           # check if country uses a postalCode and update for pattern
           if country.usesPostalCode
@@ -605,7 +605,10 @@
   divisionValidator: ->
     if $(@cartPage).length > 0
       window.Parsley.addValidator('divisionvalidator', ((value, requirement) ->
-        validity = value in USCADivisions[requirement]
+        if not requirement
+          validity = true
+        else
+          validity = value in USCADivisions[requirement]
         return validity
       )).addMessage 'en', 'divisionvalidator', 'Must be a valid division abbreviation.'


log out error on synctex

log out error on synctex

--- services/clsi/app/coffee/CompileManager.coffee
+++ services/clsi/app/coffee/CompileManager.coffee
@@ -20,7 +20,7 @@
 		compileDir = Path.join(Settings.path.compilesDir, request.project_id)

 		timer = new Metrics.Timer("write-to-disk")
-		logger.log project_id: request.project_id, "starting compile"
+		logger.log project_id: request.project_id, "starting doCompile"
 		ResourceWriter.syncResourcesToDisk request.project_id, request.resources, compileDir, (error) ->
 			if error?
 				logger.err err:error, project_id: request.project_id, "error writing resources to disk"
@@ -130,7 +130,9 @@
 		bin_path = Path.resolve(__dirname + "/../../bin/synctex")
 		seconds = 1000
 		child_process.execFile bin_path, args, timeout: 10 * seconds, (error, stdout, stderr) ->
-			return callback(error) if error?
+			if error?
+				logger.err err:error, args:args, "error running synctex"
+				return callback(error)
 			callback(null, stdout)

 	_parseSynctexFromCodeOutput: (output) ->


rollback

rollback

--- assets/scripts/controllers/task.coffee
+++ assets/scripts/controllers/task.coffee
@@ -190,7 +190,7 @@
         from: data.from
         active: true
         visible: false
-        isNew: true
+      jobInfo.flushDom data.from

   socket.on 'workerOut', (data) ->
     return unless jobInfo.workers[data.from]
@@ -201,9 +201,6 @@
     state: document.getElementsByClassName("tstate")[0]
     count: document.getElementsByClassName("tsourceNum")[0]
   setInterval (->
-    for w,x of jobInfo.workers when x.isNew
-      delete x.isNew
-      jobInfo.flushDom w
     nodeCnt = (w for w,x of jobInfo.workers when x.active).length
     headBar.count.innerHTML = nodeCnt
     activeCnt = (w for w,x of jobInfo.workers when x.active).length


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -99,6 +99,17 @@
   equals obj.I.age, 1, "should successfully update"

 test "use existing value of instance variable", ->
+  obj = GameObject()
+
+  obj.include(Cooldown)
+
+  obj.cooldown 'health',
+    target: 10
+
+  5.times ->
+    obj.update()
+
+  equals obj.I.health, 5

 test "initialize property to 0 if no current value", ->
   obj = GameObject()


tween with arbitrary easing function

tween with arbitrary easing function

--- test/drawable.coffee
+++ test/drawable.coffee
@@ -36,6 +36,8 @@

   transform = object.transform()

-  object = GameObject()
+  object = GameObject
+    scale: 2
+    sca

 module()


Replace common separators in site titles

--- vendor/assets/javascripts/modal.js.coffee
+++ vendor/assets/javascripts/modal.js.coffee
@@ -31,7 +31,9 @@
           return if $("form[data-path='#{path}']").length > 0 # already loaded

           # Extracts title from the page title itself.
-          title = $.trim html.match(/<title>((?:.|[\r\n])+)<\/title>/)[1] || 'Modal'
+          title = html.match(/<title>((?:.|[\r\n])+)<\/title>/)[1] || 'Modal'
+          title = title.replace(/(?:\||::|>).*$/, '') # replace common title separators like "Contact Us | My Site Name"
+          title = $.trim title
           # Content is the first form it finds.
           # Add separate pages to forms like <div modal-step>...</div>
           page = $(html)


Set slide duration to 10 if no duration is provided. Fix #17

--- routes/api.coffee
+++ routes/api.coffee
@@ -89,9 +89,7 @@

 router.post '/addSlide', upload.single('file'), (req, res) ->
   if !req.body.duration
-    fs.unlink req.file.path if req.file
-    req.flash 'error', 'No duration provided!'
-    return res.redirect '/admin'
+    req.body.duration = 10
   if !req.file
     req.flash 'error', 'No file uploaded!'
     return res.redirect '/admin'


Mise en place de l'authentification RF

Mise en place de l'authentification RF

--- postgreAPI/login.coffee
+++ postgreAPI/login.coffee
@@ -62,20 +62,25 @@
   req.session.destroy()
   res.sendStatus(200)

-###
+
 app.post '/loginRf', access.logged, (req, res) ->
-  User
-  .findOne(_id: req.session.user.id)
-  .exec()
+  db().then (connection) ->
+    #  mdp_super, , mdp AS pass_hash
+    connection.client.query """
+      SELECT DISTINCT *
+      FROM utilisateur
+      WHERE login = $1::text AND mdp_super = MD5($2::text)
+    """, [req.session.user.login, req.body.password]
   .then (user) ->
-    if user is null
+    if user.rowCount isnt 1
       res.status(404).send()
-    else if user.authenticateWithRole("rf", req.body.password)
+    else
       req.session.loginRf = true
       res.send true
-    else
-      res.status(404).send()
-  .then null, (err) ->
+  .catch (err) ->
+    console.error "err::", err
     res.status(500).send(err)
-###
+  .finally ->
+    @connection.done()
+
 module.exports = app


[master] Log

[master] Log

--- get_comment_count.coffee
+++ get_comment_count.coffee
@@ -70,7 +70,7 @@
               html = cherrio.load body
               [match] = /\d{4}-\d{2}-\d{2}/.exec html('.zwfbtime').text()
               createDate = moment match, 'YYYY-MM-DD'
-              debug "date change from #{lastEntryDate.format 'YYYY-MM-DD'} to #{createDate.format 'YYYY-MM-DD'}"
+              debug "date jumped from #{lastEntryDate.format 'YYYY-MM-DD'} to #{createDate.format 'YYYY-MM-DD'}"
               storePayload createDate
       , (err, lastEntryDate) ->
         throw err if err?


Welcome message: less is more

Welcome message: less is more

--- maze.coffee
+++ maze.coffee
@@ -482,7 +482,7 @@
 class LevelChooserUI extends MazeUI3D

   currentMessage: ->
-    'Welcome to the maze! Use the arrow keys to move around and choose a level'
+    'Welcome to the maze!<br>Use the arrow keys to move and choose a level.'

 root = exports ? this
 root.RandomMaze = RandomMaze


just some renaming

just some renaming

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -360,9 +360,9 @@
         @controls.addEventListener 'change', =>
             if @renderer_type is 'dynamic'
                 @rescale_objects()
-                if @_3dText?
+                if @_Text3d?
                     up = (new THREE.Vector3 0, 1, 0).applyQuaternion @camera.quaternion
-                    for mesh in @_3dText
+                    for mesh in @_Text3d
                         mesh.up = up.clone()
                         mesh.lookAt @camera.position
                 @renderer.render @scene, @camera
@@ -411,7 +411,7 @@
             @boundingBox.geometry.boundingBox = obj.geometry.boundingBox.clone()
         @boundingBox.update @boundingBox

-    add_text: (opts) ->
+    addText: (opts) ->
         opts = defaults opts,
             loc              : [0,0,0]
             text             : required
@@ -445,7 +445,6 @@
         # Make a material out of our texture.
         spriteMaterial = new THREE.SpriteMaterial
             map     : texture
-            opacity : opts.material.opacity

         # Make the sprite itself.  (A sprite is a 3d plane that always faces the camera.)
         sprite = new THREE.Sprite spriteMaterial
@@ -468,7 +467,7 @@
         @scene.add sprite
         return sprite

-    add_3dtext: (opts) ->
+    addText3d: (opts) ->
         opts = defaults opts,
             text        : required
             loc         : [0, 0, 0]
@@ -510,17 +509,17 @@

         if opts.rotation?
             mesh.rotation.set opts.rotation...
-        else if @_3dText?
-            @_3dText.push mesh
+        else if @_Text3d?
+            @_Text3d.push mesh
         else
-            @_3dText = [mesh]
+            @_Text3d = [mesh]

         if opts.in_frame
             @updateBoundingBox mesh
         @scene.add mesh
         return mesh

-    add_line: (opts) ->
+    addLine: (opts) ->
         opts = defaults opts,
             points     : required
             thickness  : 1
@@ -540,7 +539,7 @@
         @scene.add line
         return line

-    add_point: (opts) ->
+    addPoint: (opts) ->
         opts = defaults opts,
             loc  : [0,0,0]
             size : 5
@@ -577,7 +576,6 @@
                 texture.needsUpdate = true
                 spriteMaterial = new THREE.SpriteMaterial
                     map     : texture
-                    opacity : opts.material.opacity
                 particle = new THREE.Sprite spriteMaterial

                 position = @aspect_ratio_scale opts.loc...
@@ -607,7 +605,7 @@
         @scene.add particle
         return particle

-    add_index_face_set: (opts) ->
+    addIndexFaceSet: (opts) ->
         opts = defaults opts,
             vertices    : required
             faces       : required
@@ -666,31 +664,31 @@
         @scene.add mesh
         return mesh

-    add_group: (opts) ->
+    addGroup: (opts) ->
         opts = defaults opts,
             subobjs     : required

-        return opts.subobjs.map @add_obj
-
-    add_obj: (opts) ->
+        return opts.subobjs.map @addObj
+
+    addObj: (opts) ->
         opts = defaults opts, {type: required}, true

         switch opts.type
             when 'group'
                 delete opts.type
-                return @add_group opts
+                return @addGroup opts
             when 'text'
                 delete opts.type
-                return @add_text opts
+                return @addText opts
             when 'index_face_set'
                 delete opts.type
-                return @add_index_face_set opts
+                return @addIndexFaceSet opts
             when 'line'
                 delete opts.type
-                return @add_line opts
+                return @addLine opts
             when 'point'
                 delete opts.type
-                return @add_point opts
+                return @addPoint opts
             else
                 console.log "ERROR: bad object type #{opts.obj.type}"

@@ -756,7 +754,7 @@
                         loc2[offsetDirection[1]] -= offset*0.75
                     loc2 = [loc2.x, loc2.y, loc2.z]

-                    @add_line
+                    @addLine
                             points     : [loc, loc2]
                             thickness  : @frameOpts.thickness*4
                             in_frame   : false
@@ -764,7 +762,7 @@
                                     color   : frameColor
                                     opacity : 1

-                    text = @add_3dtext
+                    text = @addText3d
                             loc         : loc
                             text        : text
                             size        : textSize
@@ -915,7 +913,7 @@
         scene.opts.callback = (error, sceneobj) ->
             if not error
                 if scene.obj?
-                    sceneobj.add_obj scene.obj
+                    sceneobj.addObj scene.obj
                 sceneobj.finalize()
             opts.callback? error, sceneobj


CSRF token from init-data is preferred fixes #400

--- wzk/net/AuthMiddleware.coffee
+++ wzk/net/AuthMiddleware.coffee
@@ -31,7 +31,7 @@
     @param {Object} headers
   ###
   apply: (headers) ->
-    csrfs = (token for token in [@cookies.get(wzk.net.AuthMiddleware.COOKIES.CSRF), @csrf] when token?)
+    csrfs = (token for token in [@csrf, @cookies.get(wzk.net.AuthMiddleware.COOKIES.CSRF)] when token?)
     if not goog.array.isEmpty(csrfs)
       headers[wzk.net.AuthMiddleware.HEADER.CSRF] = csrfs[0]
     else


Upload return child

Upload return child

--- src/mecano.coffee
+++ src/mecano.coffee
@@ -1376,28 +1376,35 @@

   ###
   upload: (options, callback) ->
+    result = child mecano
+    finish = (err, uploaded) ->
+      callback err, uploaded if callback
+      result.end err, uploaded
     misc.options options, (err, options) ->
-      return callback err if err
+      return finish err if err
       uploaded = 0
       each( options )
       .on 'item', (options, next) ->
-        # Start real work
-        if options.binary
-          return options.ssh.sftp (err, sftp) ->
-            from = fs.createReadStream options.source
-            to = sftp.createWriteStream options.destination
-            from.pipe to
-            from.on 'error', (err) ->
-              next err
-            from.on 'end', ->
-              uploaded++
-              next()
-        options = misc.merge options, local_source: true
-        mecano.write options, (err, written) ->
-          uploaded++ if written is 1
-          next err
+        conditions.all options, next, ->
+          # Start real work
+          if options.binary
+            return options.ssh.sftp (err, sftp) ->
+              from = fs.createReadStream options.source#, encoding: 'binary'
+              to = sftp.createWriteStream options.destination#, encoding: 'binary'
+              l = 0
+              from.pipe to
+              from.on 'error', (err) ->
+                next err
+              from.on 'end', ->
+                uploaded++
+                next()
+          options = misc.merge options, local_source: true
+          mecano.write options, (err, written) ->
+            uploaded++ if written is 1
+            next err
       .on 'both', (err) ->
-        callback err, uploaded
+        finish err, uploaded
+    result
   ###

   `write(options, callback)`


Include escaped characters for single quote strings

--- grammars/powershell.cson
+++ grammars/powershell.cson
@@ -713,6 +713,9 @@
           {
             'include': '#constant_placeholder'
           }
+          {
+            'include': '#escaped_char'
+          }
         ]
       }
       {
@@ -732,6 +735,9 @@
           {
             'include': '#constant_placeholder'
           }
+          {
+            'include': '#escaped_char'
+          }
         ]
       }
       {
@@ -751,6 +757,9 @@
           {
             'include': '#constant_placeholder'
           }
+          {
+            'include': '#escaped_char'
+          }
         ]
       }
       {
@@ -770,6 +779,9 @@
           {
             'include': '#constant_placeholder'
           }
+          {
+            'include': '#escaped_char'
+          }
         ]
       }
     ]


Revert warn on model without default_view

--- bokehjs/src/coffee/common/embed.coffee
+++ bokehjs/src/coffee/common/embed.coffee
@@ -48,9 +48,6 @@
   # the views we create.
   views = {}
   render_model = (model) ->
-    if not model.default_view?
-      console.debug("Model #{model} does not have a default view and will not be rendered");
-      return
     view = _create_view(model)
     views[model.id] = view
     $(element).append(view.$el)


increase registration interval (and ttl)

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -77,7 +77,7 @@

 # How frequently to register with the database that this hub is up and running, and also report
 # number of connected clients
-REGISTER_INTERVAL_S = 30   # every 30 seconds
+REGISTER_INTERVAL_S = 45   # every 45 seconds

 # node.js -- builtin libraries
 net     = require('net')
@@ -3874,7 +3874,7 @@
         host    : program.host
         port    : program.port
         clients : number_of_clients()
-        ttl     : 2*REGISTER_INTERVAL_S
+        ttl     : 3*REGISTER_INTERVAL_S
         cb      : (err) ->
             if err
                 database_is_working = false
@@ -5969,7 +5969,7 @@
 connect_to_database = (opts) ->
     opts = defaults opts,
         error : 120
-        cb    : required
+        cb    : required
     if database? # already did this
         opts.cb(); return
     dbg = (m) -> winston.debug("connect_to_database: #{m}")


hub: an error message; a function call that shouldn't be needed

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -5585,7 +5585,7 @@
                 email_address : mesg.email_address
                 cb            : (error, not_available) ->
                     if error
-                        cb('other':"Unable to create account.  Please try later.")
+                        cb('other':"Unable to create account.  Please try later. -- #{misc.to_json(error)}")
                     else if not_available
                         cb(email_address:"This e-mail address is already taken.")
                     else
@@ -6462,7 +6462,7 @@
 #############################################

 program.usage('[start/stop/restart/status/nodaemon] [options]')
-    .option('--port <n>', 'port to listen on (default: 5000)', parseInt, 5000)
+    .option('--port <n>', 'port to listen on (default: 5000)', ((n)->parseInt(n)), 5000)
     .option('--proxy_port <n>', 'port that the proxy server listens on (default: 5001)', parseInt, 5001)
     .option('--log_level [level]', "log level (default: debug) useful options include INFO, WARNING and DEBUG", String, "debug")
     .option('--host [string]', 'host of interface to bind to (default: "127.0.0.1")', String, "127.0.0.1")


Fix opening hours logic.

Fix opening hours logic.

--- src/models.coffee
+++ src/models.coffee
@@ -371,7 +371,7 @@
         toJSON: (options) ->
             data = super()
             openingHours = _.filter @get('connections'), (c) ->
-                c.section == 'opening_hours' and p13n.getLanguage() of c.name
+                c.section_type == 'OPENING_HOURS' and p13n.getLanguage() of c.name
             lang = p13n.getLanguage()
             if openingHours.length > 0
                 data.opening_hours = _(openingHours)
@@ -379,7 +379,7 @@
                     .sortBy 'type'
                     .map (hours) =>
                         content: hours.name[lang]
-                        url: hours.www_url?[lang]
+                        url: hours.www?[lang]
                     .value()

             highlights = _.filter @get('connections'), (c) ->


Fix sorting edge case for missing unit location.

--- src/models.coffee
+++ src/models.coffee
@@ -66,12 +66,17 @@
             coords = @get('location')?.coordinates
             if coords?
                 @latLng = L.GeoJSON.coordsToLatLng coords
-            @latLng
+            else
+                null

         getDistanceToLastPosition: ->
             position = p13n.getLastPosition()
             if position?
-                position.getLatLng().distanceTo @getLatLng()
+                latLng = @getLatLng()
+                if latLng?
+                    position.getLatLng().distanceTo latLng
+                else
+                    null

     class SMModel extends Backbone.Model
         # FIXME/THINKME: Should we take care of translation only in


Promisify these jQuery calls

Promisify these jQuery calls

--- models/channel.coffee
+++ models/channel.coffee
@@ -1,3 +1,4 @@
+Promise = require 'bluebird-q'
 { extend } = require 'underscore'
 params = require 'query-params'
 { API_URL, APP_URL } = require('sharify').data
@@ -56,13 +57,13 @@
   generateShareLink: ->
     mediator.trigger 'sharelink:created'

-    $.post "#{API_URL}/channels/#{@slugOrId()}/share", (response) =>
+    Promise $.post "#{API_URL}/channels/#{@slugOrId()}/share", (response) =>
       @set 'share_link', response.share_link

   removeShareLink: ->
     mediator.trigger 'sharelink:removed'

-    $.ajax
+    Promise $.ajax
       type: "DELETE"
       url: "#{API_URL}/channels/#{@slugOrId()}/share"
       success: =>


fix(vertxEventBusProvider): make config functions chainable

solves #6

--- src/angular-vertxbus-adapter.coffee
+++ src/angular-vertxbus-adapter.coffee
@@ -42,31 +42,31 @@

   @usePrefix = (value = DEFAULT_OPTIONS.prefix) ->
     angularVertxbusOptions.prefix = value
-    return
+    return this

   @useUrlServer = (value = DEFAULT_OPTIONS.urlServer) ->
     angularVertxbusOptions.urlServer = value
-    return
+    return this

   @useUrlPath = (value = DEFAULT_OPTIONS.urlPath) ->
     angularVertxbusOptions.urlPath = value
-    return
+    return this

   @useReconnect = (value = DEFAULT_OPTIONS.reconnectEnabled) ->
     angularVertxbusOptions.reconnectEnabled = value
-    return
+    return this

   @useSockJsStateInterval = (value = DEFAULT_OPTIONS.sockjsStateInterval) ->
     angularVertxbusOptions.sockjsStateInterval = value
-    return
+    return this

   @useSockJsReconnectInterval = (value = DEFAULT_OPTIONS.sockjsReconnectInterval) ->
     angularVertxbusOptions.sockjsReconnectInterval = value
-    return
+    return this

   @useSockJsOptions = (value = DEFAULT_OPTIONS.sockjsOptions) ->
     angularVertxbusOptions.sockjsOptions = value
-    return
+    return this

   ###
     A stub representing the VertX Event Bus (core functionality)


Don't use TextEditorView

Don't use TextEditorView

--- spec/wrap-guide-spec.coffee
+++ spec/wrap-guide-spec.coffee
@@ -1,7 +1,7 @@
-{$, EditorView} = require 'atom'
+{$} = require 'atom'

 describe "WrapGuide", ->
-  [editorView, wrapGuide, workspaceElement] = []
+  [editor, wrapGuide, workspaceElement] = []

   beforeEach ->
     workspaceElement = atom.views.getView(atom.workspace)
@@ -21,7 +21,6 @@

     runs ->
       editor = atom.workspace.getActiveTextEditor()
-      editorView = atom.views.getView(editor).__spacePenView
       wrapGuide = workspaceElement.querySelector(".wrap-guide")

   describe ".activate", ->
@@ -33,7 +32,7 @@
       expect(workspaceElement.querySelectorAll(".underlayer > .wrap-guide").length).toBe 2

     it "positions the guide at the configured column", ->
-      width = editorView.charWidth * wrapGuide.getDefaultColumn()
+      width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn()
       expect(width).toBeGreaterThan(0)
       expect($(wrapGuide).position().left).toBe(width)
       expect(wrapGuide).toBeVisible()
@@ -69,7 +68,7 @@
       expect(initial).toBeGreaterThan(0)
       expect(wrapGuide).toBeVisible()

-      editorView.editor.setGrammar(atom.syntax.grammarForScopeName('text.plain.null-grammar'))
+      editor.setGrammar(atom.syntax.grammarForScopeName('text.plain.null-grammar'))
       expect($(wrapGuide).position().left).toBeGreaterThan(initial)
       expect(wrapGuide).toBeVisible()

@@ -77,14 +76,14 @@
     it "places the wrap guide at the custom column", ->
       atom.config.set('wrap-guide.columns', [{pattern: '\.js$', column: 20}])
       wrapGuide.updateGuide()
-      width = editorView.charWidth * 20
+      width = editor.getDefaultCharWidth() * 20
       expect(width).toBeGreaterThan(0)
       expect($(wrapGuide).position().left).toBe(width)

     it "uses the default column when no custom column matches the path", ->
       atom.config.set('wrap-guide.columns', [{pattern: '\.jsp$', column: '100'}])
       wrapGuide.updateGuide()
-      width = editorView.charWidth * wrapGuide.getDefaultColumn()
+      width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn()
       expect(width).toBeGreaterThan(0)
       expect($(wrapGuide).position().left).toBe(width)

@@ -100,14 +99,14 @@
     it "places the wrap guide at the custom column using scope name", ->
       atom.config.set('wrap-guide.columns', [{scope: 'source.js', column: 20}])
       wrapGuide.updateGuide()
-      width = editorView.charWidth * 20
+      width = editor.getDefaultCharWidth() * 20
       expect(width).toBeGreaterThan(0)
       expect($(wrapGuide).position().left).toBe(width)

     it "uses the default column when no scope name matches", ->
       atom.config.set('wrap-guide.columns', [{scope: 'source.gfm', column: '100'}])
       wrapGuide.updateGuide()
-      width = editorView.charWidth * wrapGuide.getDefaultColumn()
+      width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn()
       expect(width).toBeGreaterThan(0)
       expect($(wrapGuide).position().left).toBe(width)

@@ -115,11 +114,6 @@
       atom.config.set('wrap-guide.columns', [{pattern: '\.js$', column: 20},
                                              {scope: 'source.js', column: 30}])
       wrapGuide.updateGuide()
-      width = editorView.charWidth * 20
+      width = editor.getDefaultCharWidth() * 20
       expect(width).toBeGreaterThan(0)
       expect($(wrapGuide).position().left).toBe(width)
-
-  it "only attaches to editor views that are part of a pane", ->
-    editorView2 = new EditorView(mini: true)
-    editorView.overlayer.append(editorView2)
-    expect(editorView2.find('.wrap-guide').length).toBe 0


Correctly serialize binary objects in tests

--- test/dynamodb/document_client.coffee
+++ test/dynamodb/document_client.coffee
@@ -1,6 +1,7 @@
 helpers = require('../helpers')
 AWS = helpers.AWS
 Buffer = AWS.util.Buffer
+encode = AWS.util.base64.encode

 docClient = null

@@ -350,7 +351,7 @@
             bar: L: [
               {S: 'string'},
               {N: '2'},
-              {B: buffer}
+              {B: encode(buffer)}
               {BOOL: true}
             ]
       )
@@ -396,7 +397,7 @@
       wire = JSON.stringify(
         Item:
           foo:
-            'BS': [bar, baz, quux]
+            'BS': [encode(bar), encode(baz), encode(quux)]
       )
       output = Item:
         foo: set
@@ -496,13 +497,13 @@
                 {'S': 'alpha'},
                 {'S': 'beta'},
                 {'S': 'gamma'},
-                {'B': buffer},
+                {'B': encode(buffer)},
               ]},
               {L:[
                 {'N': '1'},
                 {'N': '2'},
                 {'N': '3'},
-                {'B': buffer},
+                {'B': encode(buffer)},
               ]},
               {L:[
                 {


that should probably say footer

that should probably say footer

--- resources/assets/coffee/_classes/sticky-footer.coffee
+++ resources/assets/coffee/_classes/sticky-footer.coffee
@@ -18,7 +18,7 @@

 # How to use:
 # 1. create a marker on when it should be fixed, with class including
-#    'js-sticky-header' and data attribute 'data-sticky-header-target'
+#    'js-sticky-footer' and data attribute 'data-sticky-footer-target'
 # 2. subscribe to 'stickyFooter' event
 # 3. in the function, check if second parameter (first one is unused event
 #    object) is the correct target