send more information about the dashboard to the agent (url + statusUrl)

--- server/cluster.coffee
+++ server/cluster.coffee
@@ -49,14 +49,18 @@
         dir: dir
         startScript: Scripts.bash.start app, version, instance, options, parameters
         stopScript: Scripts.bash.stop app, version, instance, options, parameters
+
         app:
           name: app
           version: version
           definition: YAML.safeLoad (findAppDef app, version).def
         instance:
           name: instance
-          options: options
-          #parameters: parameters we do not send params to the agent, this is a feature of the dashboard
+          options: _.extend({}, options, { project:project })
+          parameters: parameters
+        bigboat:
+          url: process.env.ROOT_URL
+          statusUrl: "#{process.env.ROOT_URL}/api/v1/state/#{instance}"

     console.log "Sending a POST request to '#{agentUrl}' to start '#{instance}'."

@@ -91,6 +95,17 @@
       data:
         dir: "#{Settings.get('project')}-#{instanceName}"

+        app:
+          name: instance.meta.appName
+          version: instance.meta.appVersion
+          definition: YAML.safeLoad (findAppDef instance.meta.appName, instance.meta.appVersion).def
+        instance:
+          name: instanceName
+          options: _.extend({}, { project: Settings.get('project') })
+        bigboat:
+          url: process.env.ROOT_URL
+          statusUrl: "#{process.env.ROOT_URL}/api/v1/state/#{instanceName}"
+
     if @userId
       user = Meteor.user()
     else


fix the request params

fix the request params

--- src/app/wap/script/mod/home/main.coffee
+++ src/app/wap/script/mod/home/main.coffee
@@ -17,7 +17,7 @@

 	loadWebsites: (page=1, pageSize=10)=>
 		app.ajax.get
-			url: '/api/v1/websites' + '?page=' + page + 'pageSize=' + pageSize;
+			url: '/api/v1/websites' + '?page=' + page + '&pageSize=' + pageSize;
 			success: (res)=>
 				console.log(res)
 				React.render(


Only send objects notifs to authenticated clients (2/2).

--- src/xo.coffee
+++ src/xo.coffee
@@ -226,7 +226,8 @@
             type: 'enter'
             items: $pluck entered, 'val'
           for id, connection of @connections
-            connection.notify 'all', enterParams
+            if connection.has('user_id')
+              connection.notify 'all', enterParams

         unless $isEmpty exited
           exitParams =


Getting Physichal

Getting Physichal

--- src/engine_physics.coffee
+++ src/engine_physics.coffee
@@ -9,5 +9,8 @@
     world.Step(1 / I.FPS, 10, 10)
     world.ClearForces()

+  self.bind "beforeAdd", (entityData) ->
+    entityData.world = world
+
   return {}


shut up, modes.coffee

shut up, modes.coffee

--- plugins/modes/modes.coffee
+++ plugins/modes/modes.coffee
@@ -13,20 +13,16 @@
   rb: 'ruby'

 modeForLanguage = (language) ->
-  console.log "modeForLanguage(#{language})"
   language = language.toLowerCase()
   modeName = modeMap[language] or language

-  console.log "modeName: #{modeName}"
   try
     require("ace/mode/#{modeName}").Mode
   catch e
     null

 setMode = ({filename}) ->
-  console.log "setMode(#{filename})"
   if mode = modeForLanguage _.last filename.split '.'
-    console.dir mode
     activeWindow.document.ace?.getSession().setMode new mode

 exports.init = ->


ivey just goes by ivey

ivey just goes by ivey

--- scripts/name-expander.coffee
+++ scripts/name-expander.coffee
@@ -4,7 +4,7 @@

 josiah = "Jkiehl"
 cliff = "cdickerson"
-ivey = "michael.ivey"
+ivey = "ivey"
 chong = "Chong Yan"
 jesse = "Jesse Howarth"
 kyle = "kallan"
@@ -18,7 +18,7 @@

 noc_members = ["Carter1", "Cames1", "jdowns", "jdowns1", "jtramullas", "nstuhring", "ctrytten1", "ctrytten", "akim", "nsanches", "jkim", "ebednik", "nhooper"]

-dert_mermbers = ["CDERCKERSERN", "MAHCHERLERVER", "JKERL", "CHIN YERN", "KERLERN", "ERNDRERW GERSERN"]
+dert_mermbers = ["CDERCKERSERN", "ERVER", "JKERL", "CHIN YERN", "KERLERN", "ERNDRERW GERSERN"]

 build_members = ["mvuong", "mstewart", "jromigh", "pmckeown", "pmckeown2", "pmckeown_home2", "zeffron"]


note default type secret_type

note default type secret_type

--- app/js/controllers/notes.coffee
+++ app/js/controllers/notes.coffee
@@ -10,7 +10,7 @@
         symbol: null
         title: null
         body: null
-        type: 'private_type'
+        type: 'secret_type'
     $scope.balances = {}

     tx_fee = null
@@ -77,7 +77,7 @@
     yesSend = ->
         form = @note_form
         message = JSON.stringify(title:$scope.note.title, body:$scope.note.body)
-        WalletAPI.note($scope.note.amount, $scope.note.symbol, $scope.note.from, message, $scope.note.type == 'private').then (response) =>
+        WalletAPI.note($scope.note.amount, $scope.note.symbol, $scope.note.from, message, $scope.note.type == 'secret_type').then (response) =>

             $scope.note_records.push
                 note:


generate long form summary for imgur links

--- scripts/preview/imgur.coffee
+++ scripts/preview/imgur.coffee
@@ -1,5 +1,19 @@
 class ImgurScraper extends Preview.BasicScraper
   Preview.register this
+
+  SNIPPET_TEMPLATE = '''
+

+

+        pretty small
+        (1337 bytes)
+        animated
+        image/png;
+        some views,
+        uploaded <time class="imgur-timestamp" pubdate>some time ago</time>.
+

+

+

+  '''

   @doesUriMatch: (uri) ->
     if uri.host is 'imgur.com' or uri.host is 'i.imgur.com'
@@ -21,7 +35,37 @@
           { image, links } = data.image
           @createPreview
             uri: links.imgur_page
-            title: image.title
+            title: image.title ? "Imgur image #{hash}"
+            snippet: @createSnippet(image)
             thumbnail: links.small_square
     else
       @pass()
+
+  createSnippet: (image) ->
+    snippet = $(SNIPPET_TEMPLATE)
+
+    # TODO parse the date. why can't everyone just do it the right way?
+    # console.log timestamp = Date.parse image.datetime
+    # $('.imgur-timestamp', snippet)
+    #   .attr(
+    #     title: timestamp.toLocaleString(),
+    #     datetime: timestamp.toISOString() )
+    #   .text(timestamp.toLocaleString())
+
+    $('.imgur-views', snippet).text(
+      switch image.views
+        when 0 then "no views"
+        when 1 then "one view"
+        else        "#{image.views} views"
+    )
+
+    $('.imgur-caption', snippet).text(image.caption) if image.caption?
+
+    # wtf, boolean strings?
+    $('.imgur-animated', snippet).remove() unless image.animated is "true"
+
+    $('.imgur-dimensions', snippet).text "#{image.width}\u00d7#{image.height}"
+    $('.imgur-size',       snippet).text "#{image.size} bytes"
+    $('.imgur-type',       snippet).text "#{image.type}"
+
+    snippet


Turbolinks 5 compatibility

Turbolinks 5 compatibility

--- lib/assets/javascripts/nested_form_fields.js.coffee
+++ lib/assets/javascripts/nested_form_fields.js.coffee
@@ -53,7 +53,7 @@
     $nested_fields_container.trigger("fields_removed.nested_form_fields",{object_class: object_class, delete_association_field_name: delete_association_field_name, removed_index: removed_index});
     false

-$(document).on "page:change", ->
+$(document).on "page:change turbolinks:load", ->
     nested_form_fields.bind_nested_forms_links()

 jQuery ->


test unicode symbols

test unicode symbols

--- test/slug.test.coffee
+++ test/slug.test.coffee
@@ -132,6 +132,22 @@
         for char, replacement of char_map
             [slug "foo #{char} bar baz"].should.eql ["foo-#{replacement}-bar-baz"]

+    it 'should replace unicode', ->
+        char_map = {
+            '☢':"radioactive",'☠':"skull-and-bones",'☤':"caduceus",
+            '☣':"biohazard",'☭':"hammer-and-sickle", '☯':"yin-yang",'☮':"peace",
+            '☏':"telephone",'☔':"umbrella-with-rain-drops",'☎':"telephone",
+            '☀':"sun-with-rays",'★':"star",'☂':"umbrella",'☃':"snowman",
+            '✈':"airplane",'✉':"envelope",'✊':"raised-fist"
+        }
+        for char, replacement of char_map
+            [slug "foo #{char} bar baz"].should.eql ["foo-#{replacement}-bar-baz"]
+
+    it 'should replace no unicode when disabled', ->
+        char_map = '😹☢☠☤☣☭☯☮☏☔☎☀★☂☃✈✉✊'.split ''
+        for char in char_map
+            [slug "foo #{char} bar baz", symbols:no].should.eql ["foo-bar-baz"]
+
     it 'should allow forcing lowercase slugs', ->
         [slug('FOO Bar baZ').toLowerCase()].should.eql ['foo-bar-baz']
         [slug('FOO Bar baZ', replacement:'_').toLowerCase()].should.eql ['foo_bar_baz']


small change to start_Process (support passing arrays as first param)

--- src/fluent-process.coffee
+++ src/fluent-process.coffee
@@ -2,8 +2,10 @@

 String::start_Process = (args...)->
   args ?= []
+  if args.first() instanceof Array
+    args = args.first()
   return child_process.spawn(@.str(),args)
-
+
 String::start_Process_Redirect_Console = (args...)->
   args ?= []
   childProcess = @.start_Process(args)


:bug: Cleanup properly on deactivate

:bug: Cleanup properly on deactivate

So panel is setup again on deactivate-activate

--- atom-main.coffee
+++ atom-main.coffee
@@ -92,3 +92,4 @@
   deactivate: ->
     @disposables.dispose()
     @panel?.destroy()
+    @panel = null


Get all the structure in place for ResultsModel

--- lib/project/search-results-model.coffee
+++ lib/project/search-results-model.coffee
@@ -3,12 +3,43 @@
 class SearchResultsModel
   _.extend @prototype, EventEmitter

-  constructor: ->
+  constructor: (state={}) ->
+    @useRegex = state.useRegex ? false
+    @inCurrentSelection = state.inCurrentSelection ? false
+
     @clear()
+    rootView.eachEditSession(@subscribeToEditSession)
+
+  subscribeToEditSession: (editSession) =>
+    resultsModel = this
+    editSession.on 'contents-modified', ->
+      return unless resultsModel.regex
+
+      matches = []
+      promise = @scan resultsModel.regex, (match) ->
+        matches.push(match)
+
+      promise.done = =>
+        resultsModel.setResult(@getPath(), matches)
+
+  serialize: ->
+    {@useRegex, @inCurrentSelection}
+
+  search: (pattern, paths)->
+    @regex = @getRegex(pattern)
+    project.scan @regex, {paths}, (result) =>
+      @setResult(result.filePath, result.matches)
+
+  toggleUseRegex: ->
+    @useRegex = not @useRegex
+
+  toggleInSelection: ->
+    @inSelection = not @inSelection

   clear: ->
+    @regex = null
     @results = {}
-    @trigger('results-cleared', filePath, matches)
+    @trigger('cleared', filePath, matches)

   getResult: (filePath) ->
     @results[filePath]
@@ -27,3 +58,12 @@
     if @results[filePath]
       delete @results[filePath]
       @trigger('result-removed', filePath)
+
+  getRegex: (pattern) ->
+    flags = 'g'
+    flags += 'i' unless @caseInsensitive
+
+    if @useRegex
+      new RegExp(pattern, flags)
+    else
+      new RegExp(_.escapeRegExp(pattern), flags)


Font to FiraCode for |> ligatures

Font to FiraCode for |> ligatures

--- atom/config.cson
+++ atom/config.cson
@@ -70,8 +70,8 @@
   "dash-ui":
     themeColor: "Orange"
   editor:
-    fontFamily: "Monoid HalfTight"
-    fontSize: 15
+    fontFamily: "Fira Code"
+    fontSize: 16
     invisibles: {}
     lineHeight: 1.3
     preferredLineLength: 90


Update `atom/config.cson`

Update `atom/config.cson`

--- atom/config.cson
+++ atom/config.cson
@@ -1,4 +1,20 @@
 "*":
+  "atom-beautify":
+    css:
+      beautify_on_save: true
+    handlebars:
+      beautify_on_save: true
+      default_beautifier: "Pretty Diff"
+    html:
+      beautify_on_save: true
+    js:
+      beautify_on_save: true
+    json:
+      beautify_on_save: true
+    sass:
+      beautify_on_save: true
+    scss:
+      beautify_on_save: true
   core:
     telemetryConsent: "no"
     themes: [
@@ -11,12 +27,16 @@
     fontFamily: "Menlo"
     fontSize: 14
     invisibles: {}
-    nonWordCharacters: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?s…"
+    nonWordCharacters: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?…"
     preferredLineLength: 120
     showIndentGuide: true
     softWrap: true
     softWrapAtPreferredLineLength: true
   "exception-reporting":
     userId: "884440fb-9e84-fcf9-8239-ecee423c921e"
+  "linter-eslint":
+    fixOnSave: true
+  "linter-ui-default":
+    panelHeight: 81
   welcome:
     showOnStartup: false


:atom: Update config.cson for Atom

:atom: Update config.cson for Atom

--- atom/config.cson
+++ atom/config.cson
@@ -5,6 +5,10 @@
     width: 120
   "activate-power-mode":
     autoToggle: false
+  "atom-ide-ui":
+    "atom-ide-datatip":
+      datatipDebounceDelay: 500
+    use: {}
   "autocomplete-emojis":
     enableUnicodeEmojis: false
   "autocomplete-plus":
@@ -18,6 +22,7 @@
         "god"
         "pp"
       ]
+    debugLSP: true
     destroyEmptyPanes: false
     disabledPackages: [
       "nvatom"
@@ -45,6 +50,7 @@
       "one-dark-syntax"
     ]
     titleBar: "custom"
+    uriHandlerRegistration: "always"
   editor:
     autoIndentOnPaste: false
     fontSize: 11
@@ -79,6 +85,7 @@
       "~/github"
       "~/src"
       "~/.dotfiles"
+      "~/Dropbox"
     ]
   "real-time": {}
   "spell-check":
@@ -94,8 +101,13 @@
     rightEditorColor: "green"
     syncHorizontalScroll: true
   "status-bar": {}
+  swackets:
+    colorBrackets: false
+    colorParens: true
   tabs:
     enableVcsColoring: true
+  teletype:
+    dev: {}
   welcome:
     showOnStartup: false
 ".git-commit.text":


Slowed down the Atom partial match timeout a bit for my own good.

--- atom/init.coffee
+++ atom/init.coffee
@@ -1,4 +1,4 @@
-atom.keymaps.partialMatchTimeout = 100
+atom.keymaps.partialMatchTimeout = 140
 atom.commands.dispatch(atom.views.getView(atom.workspace), 'atom-hide-tabs:toggle')

 atom.commands.add 'atom-text-editor', 'awe:cycle-pane', ->


IOServer: Fixed tombstoning.

IOServer: Fixed tombstoning.

--- src/ioserver/ioserver.coffee
+++ src/ioserver/ioserver.coffee
@@ -30,12 +30,15 @@
   finishReducer: (reducerid) ->
     @reducers[reducerid].finished = true
     @numReducers++
+    console.log(@numReducers + " out of " + @totalReducers)
     if (@numReducers == @totalReducers)
       jobMap = @combineToJobMap()
       str = @serializeJobMap(jobMap)
       @saveToFile(str)
       firebase_db.JOB_STATUS_REF.child(@job).child('output_url')
       .set(constants.OUTPUT_DIR + @job)
+      return true
+    return false

   combineToJobMap: () ->
     jobMap = {}
@@ -65,7 +68,9 @@
     fs.writeFileSync(constants.TOP_DIR + constants.OUTPUT_DIR + @job, str)

 test = (fb) ->
-  fb.push({name: "START_JOB", job: 7, numReducers: 1})
+  fb.push({name: "START_JOB", job: 7, numReducers: 3})
+  fb.push({name: "START_REDUCER_OUTPUT", job: 7, reducer: 3})
+  fb.push({name: "STOP_REDUCER_OUTPUT", job: 7, reducer: 3})
   fb.push({name: "START_REDUCER_OUTPUT", job: 7, reducer: 1})
   fb.push({
     name: "REDUCER_OUTPUT",
@@ -103,7 +108,6 @@

   fb.on("child_added", newMessage = (snapshot) ->
     message = snapshot.val()
-    console.log message
     switch message.name
       when "START_JOB" then (
         if (not (currentJobs[message.job] == -1))
@@ -121,8 +125,8 @@
       )
       when "STOP_REDUCER_OUTPUT" then (
         if (not((currentJobs[message.job] == null or currentJobs[message.job] == -1)))
-          currentJobs[message.job].finishReducer(message.reducer)
-          currentJobs[message.job] = -1
+          if (currentJobs[message.job].finishReducer(message.reducer))
+            currentJobs[message.job] = -1
       )
     fb.child(snapshot.name()).remove()
   )


Modified Dota history command to allow custom URL

--- steam.coffee
+++ steam.coffee
@@ -10,7 +10,7 @@
 # Commands:
 #   hubot steam id [me] <custom URL> - Returns the Steam ID for the user under http://steamcommunity.com/id/<custom URL>
 #   hubot steam status <Steam ID> - Returns <Steam ID> community status
-#   hubot dota history <Steam ID> - Returns metadata for the latest 5 game lobbies with <Steam ID>
+#   hubot dota history [-u] <Steam ID|custom URL> - Returns metadata for the latest 5 game lobbies with <Steam ID> or <custom URL> if the -u flag is set
 #   hubot dota match <match ID> [<Steam ID>] - Returns information about a particular <match ID>. Optionally, if <Steam ID> is included, its match information will also be returned
 #
 # Author:
@@ -38,6 +38,7 @@
     4: "Co-op with bots"
     5: "Team match"
     6: "Solo queue"
+    7: "N/A"
 towers = [
     "Ancient top"
     "Ancient bottom"
@@ -67,27 +68,42 @@
             msg.reply "#{msg.match[1]} belongs to #{player.personaname} who is currently #{status} and was last online #{lastOnline}."
         , 2

-    robot.respond /dota history (\d+)/i, (msg) ->
-        steam_request msg, "/IDOTA2Match_570/GetMatchHistory", { account_id: msg.match[1], matches_requested: 5 }, (object) ->
-            if object.result.status is 15
-                msg.reply "The Steam ID you have entered (\"#{msg.match[1]}\") does not exist or it does not have match history enabled."
-                return
-            else if object.result.num_results is 0
-                msg.reply "No game matches were found for the Steam ID: #{msg.match[1]}."
-                return
+    robot.respond /dota history (-u )?(.*)/i, (msg) ->
+        [command, flag, value] = msg.match

-            communityID = getCommunityID(msg.match[1])
+        params =
+            account_id: value
+            matches_requested: 5

-            for match in object.result.matches
-                hero = "N/A"
-                start = moment.unix(match.start_time).fromNow()
+        history = (params, type = 'Steam ID') ->
+            steam_request msg, "/IDOTA2Match_570/GetMatchHistory", params, (object) ->
+                if object.result.status is 15
+                    msg.reply "The #{type} you have entered (\"#{msg.match[1]}\") does not exist or it does not have match history enabled."
+                    return
+                else if object.result.num_results is 0
+                    msg.reply "No game matches were found for the #{type}: #{msg.match[1]}."
+                    return

-                for player in match.players
-                    if player.account_id is communityID
-                        hero = getHero(player.hero_id).localized_name
-                        break;
+                communityID = getCommunityID params.account_id

-                msg.send "Match ID: #{match.match_id} | Lobby: #{lobbies[match.lobby_type]} | Hero: #{hero} | #{start}"
+                for match in object.result.matches
+                    hero = "N/A"
+                    start = moment.unix(match.start_time).fromNow()
+
+                    for player in match.players
+                        if player.account_id is communityID
+                            hero = getHero(player.hero_id).localized_name
+                            break;
+
+                    msg.send "Match ID: #{match.match_id} | Lobby: #{lobbies[match.lobby_type]} | Hero: #{hero} | #{start}"
+
+        if flag is '-u '
+            getSteamID msg, value, (id) ->
+                return false if not id
+                params.account_id = id
+                history params, 'profile URL'
+        else
+            history params

     robot.respond /dota match (\d+)( \d*)?/i, (msg) ->
         steam_request msg, "/IDOTA2Match_570/GetMatchDetails", match_id: msg.match[1], (object) ->


sometimes no arguments are passed

sometimes no arguments are passed

Signed-off-by: Steven Hoffman <f5bcacb8ce1651b7d1e56f2693431f20e085fdeb@valenciamgmt.com>

--- vendor/assets/javascripts/fustrate/components/modal.coffee
+++ vendor/assets/javascripts/fustrate/components/modal.coffee
@@ -19,7 +19,7 @@
         display: 'none'
     _cachedHeight: undefined

-  constructor: ({ title, content, settings }) ->
+  constructor: ({ title, content, settings } = {}) ->
     @modal = @constructor.createModal()
     @settings = $.extend true, @constructor.settings, (settings ? {})
     @settings.previousModal = $()


Add "reviewed" prop to Sources on startup if prop does not exist

--- server/startup.coffee
+++ server/startup.coffee
@@ -3,6 +3,7 @@
 articleSchema = require '/imports/schemas/article.coffee'
 Articles = require '/imports/collections/articles.coffee'
 PromedPosts = require '/imports/collections/promedPosts.coffee'
+CuratorSources = require '/imports/collections/curatorSources'

 Meteor.startup ->
   incidents = Incidents.find().fetch()
@@ -12,6 +13,7 @@
     catch error
       console.log error
       console.log JSON.stringify(incident, 0, 2)
+
   for article in Articles.find().fetch()
     promedId = /promedmail\.org\/post\/(\d+)$/ig.exec(article.url)?[1]
     if promedId
@@ -33,3 +35,9 @@
         console.log JSON.stringify(article, 0, 2)
     else
       console.log "non-ProMED Article:", article.url
+
+  CuratorSources.update
+    reviewed:
+      $exists: false
+    {$set: reviewed: false}
+    {multi: true}


fix tests

fix tests

--- test/test_main.coffee
+++ test/test_main.coffee
@@ -91,10 +91,9 @@
 		run: (fn) ->
 			alight = buildAlight()
 			stat.bStarted++;
-			closed++;
 			scope = makeScope()
 			try
-			  fn scope, alight
+				fn scope, alight
 			catch e
 				err = e
 				if e.stack
@@ -110,11 +109,15 @@
 			alight = buildAlight()
 			alight.debug.useObserver = true
 			stat.bStarted++;
-			closed++;
 			scope = makeScope()
 			try
-			  fn scope, alight
+				fn scope, alight
 			catch e
-				err = if typeof(e) is 'string' then e else e.stack
+				err = e
+				if e.stack
+					err = e.stack
+				else if e.description
+					err = e.description
 				scope.error()
-				console.error err
+				console.error '!!', err
+	r


Deal with YAML parse errors

Deal with YAML parse errors

--- components/RunTests.coffee
+++ components/RunTests.coffee
@@ -36,7 +36,13 @@

     suites = []
     for s in specs
-      suites = suites.concat fbpSpec.testsuite.loadYAML s.code
+      continue unless s.code.length
+      try
+        suite = fbpSpec.testsuite.loadYAML s.code
+      catch e
+        console.log e
+        continue
+      suites = suites.concat suite
     unless suites.length
       do callback
       return


Applied patch from Petteri Salmi, which fixes chat panel bubble duplicates for the 1st item

--- js/chat/chat_panel/chat_panel.coffee
+++ js/chat/chat_panel/chat_panel.coffee
@@ -47,11 +47,11 @@
         _nextSame = ( _userId == _nextLine[0] )
       else
         _nextSame = false
-      _prevLine = @value.lines[i-1]
+      _prevLine = @value.lines[i-1] if i > 0
       if _prevLine?
         _prevSame = ( _userId == _prevLine[0] )
       else
-        _nextSame = false
+        _prevSame = false
       if _date?
         # _date = new Date(_date*1000)
         _prevDate = _date


Add test for put() method

Add test for put() method

--- test/util.spec.coffee
+++ test/util.spec.coffee
@@ -60,6 +60,14 @@
         ]
         assert.deepEqual util.pluck(array, 'prop'), [1,2], "Get prop property of objects"

+    test 'put', ->
+        arr = [
+            { id: 1 }
+            { id: 2 }
+        ]
+        util.put(arr, 'loco', true)
+        assert.deepEqual util.pluck(arr, 'loco'), [true, true]
+
     test 'prefix', ->
         assert.strictEqual util.prefix('document'), document, "find unprefixed object"
         context = { mozBacon: -> }


Fixed item click bug which sometimes happened. layerPos ist now correct in all cases.

--- src/jquery.rondell.coffee
+++ src/jquery.rondell.coffee
@@ -178,9 +178,8 @@
           when 'mouseout'
             item.object.removeClass('rondellItemHovered')
           when 'click'
-            if item.object.is(':visible') and not (@currentLayer is layerNum or item.hidden)
-              @shiftTo(layerNum)
-              e.preventDefault()
+            e.preventDefault() unless @currentLayer is layerNum
+            @shiftTo(layerNum) if item.object.is(':visible') and not item.hidden
       )

       @loadedItems += 1
@@ -427,11 +426,8 @@
       layerPos = layerNum

       # Find new layer position
-      if layerDist > @visibleItems and @repeating
-        if @currentLayer + @visibleItems > @maxItems
-          layerPos += @maxItems
-        else if @currentLayer - @visibleItems <= @maxItems
-          layerPos -= @maxItems
+      if layerDist > @visibleItems and layerDist > @maxItems / 2 and @repeating
+        if layerNum > @currentLayer then layerPos -= @maxItems else layerPos += @maxItems
         layerDist = Math.abs(layerPos - @currentLayer)

       # Get the absolute layer number difference


bugfix (Emulate) Fix emulate command not working on Linux.

--- src/steroids.coffee
+++ src/steroids.coffee
@@ -411,7 +411,7 @@
         console.log "see: steroids debug"

       when "emulate"
-        PortChecker = require "./steroids/Portchecker"
+        PortChecker = require "./steroids/PortChecker"
         connectServer = new PortChecker
           port: 4567


Factor common inlined images into re-used symbols (fix #42)

* Convert `<image>`s with `data:` URLs into `<use>`s of a `<symbol>` with
  `id` of the form `$image$filename$v0`.
* Images are identical if all attributes except `x`, `y`, `width`, and
  `height` are identical.  These four attributes can be overridden at the
  `<use>` level.  Thus different `image-rendering` will make different symbols.
* Move `<symbol>` wrapping (and `id` setting) into symbol instead of drawing
  (which makes sense, and allows other code to assume the root is <symbol>)
* Change `Symbol.id` from function to computed field, and add `setId` method
  for manually changing `id` (for custom escaping).
* Factor out `href` vs. `xlink:href` code into `getHref`

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -62,7 +62,10 @@
   else
     recurse = (node) ->
       if node.nodeType != node.ELEMENT_NODE or
-         node.tagName in ['defs', 'symbol', 'use']
+         node.nodeName in ['defs', 'use']
+        return null
+      # Ignore <symbol>s except the root <symbol> that we're bounding
+      if node.nodeName == 'symbol' and node != xml.documentElement
         return null
       switch node.tagName
         when 'rect', 'image'
@@ -111,7 +114,7 @@
           ymax = Math.max ...(viewBox[1]+viewBox[3] for viewBox in viewBoxes)
           [xmin, ymin, xmax - xmin, ymax - ymin]
     viewBox = recurse xml.documentElement
-    if Infinity in viewBox or -Infinity in viewBox
+    if not viewBox? or Infinity in viewBox or -Infinity in viewBox
       null
     else
       viewBox
@@ -128,6 +131,15 @@
     if style
       match = ///(?:^|;)\s*#{styleKey}\s*:\s*([^;\s][^;]*)///i.exec style
       match?[1]
+
+getHref = (node) ->
+  for key in ['xlink:href', 'href']
+    if href = node.getAttribute key
+      return
+        key: key
+        href: href
+  key: null
+  href: null

 zIndex = (node) ->
   ## Check whether DOM node has a specified z-index, defaulting to zero.
@@ -285,6 +297,23 @@
     # we will have such a tag in the top-level <svg>.
     @xml.documentElement.removeAttribute 'xmlns'

+    ## Wrap XML in <symbol> if not already.
+    symbol = @xml.createElementNS SVGNS, 'symbol'
+    symbol.setAttribute 'id', @id = escapeId @key
+    # Avoid a layer of indirection for <symbol>/<svg> at top level
+    if @xml.documentElement.nodeName in ['symbol', 'svg'] and
+       not @xml.documentElement.nextSibling?
+      for attribute in @xml.documentElement.attributes
+        unless attribute.name in ['version', 'id'] or attribute.name[...5] == 'xmlns'
+          symbol.setAttribute attribute.name, attribute.value
+      doc = @xml.documentElement
+      @xml.removeChild @xml.documentElement
+    else
+      doc = @xml
+    for child in (node for node in doc.childNodes)
+      symbol.appendChild child
+    @xml.appendChild symbol
+
     ## <image> processing
     domRecurse @xml.documentElement, (node) =>
       if node.nodeName == 'image'
@@ -305,9 +334,8 @@
           style += ';' if style
           node.setAttribute 'style', style + 'image-rendering:pixelated'
         ## Read file for width/height detection and/or inlining
-        for key in ['xlink:href', 'href']
-          if filename = node.getAttribute key
-            break
+        {href, key} = getHref node
+        filename = href
         filename = path.join @dirname, filename if @dirname? and filename
         if filename? and not /^data:|file:|[a-z]+:\/\//.test filename # skip URLs
           filedata = null
@@ -316,33 +344,41 @@
           catch e
             console.warn "Failed to read image '#{filename}': #{e}"
           ## Fill in width and height
-          unless window? or (node.hasAttribute('width') and node.hasAttribute('height'))
+          size = null
+          unless window?
             try
               size = require('image-size') filedata ? filename
             catch e
               console.warn "Failed to detect size of image '#{filename}': #{e}"
-              size = null
-            if size?
-              ## If one of width and height is set, scale to match.
-              if not isNaN width = parseFloat node.getAttribute 'width'
-                node.setAttribute 'height', size.height * (width / size.width)
-              else if not isNaN height = parseFloat node.getAttribute 'height'
-                node.setAttribute 'width', size.width * (height / size.height)
-              else
-                ## If neither width nor height are set, set both.
-                node.setAttribute 'width', size.width
-                node.setAttribute 'height', size.height
+          if size?
+            ## If one of width and height is set, scale to match.
+            if not isNaN width = parseFloat node.getAttribute 'width'
+              node.setAttribute 'height', size.height * (width / size.width)
+            else if not isNaN height = parseFloat node.getAttribute 'height'
+              node.setAttribute 'width', size.width * (height / size.height)
+            else
+              ## If neither width nor height are set, set both.
+              node.setAttribute 'width', size.width
+              node.setAttribute 'height', size.height
           ## Inline
           if filedata? and Drawing.inlineImages
             type = contentType[extensionOf filename]
             if type?
+              node.setAttribute "data-filename", filename
+              if size?
+                node.setAttribute "data-width", size.width
+                node.setAttribute "data-height", size.height
               node.setAttribute key,
                 "data:#{type};base64,#{filedata.toString 'base64'}"
         false
       else
         true

+    ## Set viewBox attribute if absent.
     @viewBox = svgBBox @xml
+    if @viewBox? and not @xml.documentElement.hasAttribute 'viewBox'
+      @xml.documentElement.setAttribute 'viewBox', @viewBox.join ' '
+
     # Overflow behavior
     overflow = attributeOrStyle @xml.documentElement, 'overflow'
     if @constructor.overflowDefault? and not overflow?
@@ -394,8 +430,9 @@
           false # don't recurse into <text>'s children
         else
           true
-  id: ->
-    escapeId @key
+  setId: (id) ->
+    @id = id # for <use>
+    @xml.documentElement.setAttribute 'id', id
   use: -> @  ## do nothing for static symbol
   usesContext: false

@@ -424,7 +461,7 @@
       version = @nversions++
       @versions[string] =
         Symbol.parse "#{@key}$v#{version}", result, @dirname
-      @versions[string].id = => "#{escapeId @key}$v#{version}"
+      @versions[string].setId "#{escapeId @key}$v#{version}"
     @versions[string]
   usesContext: true

@@ -437,7 +474,7 @@
     <path xmlns="http://www.w3.org/2000/svg" stroke="none" fill="red" d="M 200,100 100,200 0,100 100,0 200,100 z M 135.64709,74.70585 q 0,-13.52935 -10.00006,-22.52943 -9.99999,-8.99999 -24.35289,-8.99999 -17.29415,0 -30.117661,5.29409 L 69.05879,69.52938 q 9.764731,-6.23528 21.52944,-6.23528 8.82356,0 14.58824,4.82351 5.76469,4.82351 5.76469,12.70589 0,8.5883 -9.94117,21.70588 -9.94117,13.11766 -9.94117,26.76473 l 17.88236,0 q 0,-6.3529 6.9412,-14.9412 11.76471,-14.58816 12.82351,-16.35289 6.9412,-11.05887 6.9412,-23.29417 z m -22.00003,92.11771 0,-24.70585 -27.29412,0 0,24.70585 27.29412,0 z"/>
   </symbol>
 '''
-unrecognizedSymbol.id = -> '$UNRECOGNIZED$' # cannot be output of standard id()
+unrecognizedSymbol.setId '$UNRECOGNIZED$' # cannot be output of escapeId()

 class Input
   @encoding: 'utf8'
@@ -667,20 +704,49 @@
     ## Include all used symbols in SVG
     for key, symbol of symbolsByKey
       continue unless symbol?
-      svg.appendChild node = doc.createElementNS SVGNS, 'symbol'
-      node.setAttribute 'id', symbol.id()
-      if symbol.xml.documentElement.tagName in ['svg', 'symbol']
-        ## Remove a layer of indirection for <svg> and <symbol>
-        for attribute in symbol.xml.documentElement.attributes
-          unless attribute.name in ['version', 'id'] or attribute.name[...5] == 'xmlns'
-            node.setAttribute attribute.name, attribute.value
-        for child in symbol.xml.documentElement.childNodes
-          node.appendChild child.cloneNode true
-      else
-        node.appendChild symbol.xml.documentElement.cloneNode true
-      ## Set/overwrite any viewBox attribute with one from symbol.
-      if symbol.viewBox?
-        node.setAttribute 'viewBox', symbol.viewBox.join ' '
+      svg.appendChild symbol.xml.documentElement.cloneNode true
+    ## Factor out duplicate inline <image>s into separate <symbol>s.
+    inlineImages = {}
+    inlineImageVersions = {}
+    domRecurse svg, (node, parent) ->
+      return true unless node.nodeName == 'image'
+      {href} = getHref node
+      return true unless href?.startsWith 'data:'
+      # data-filename gets set to the original filename when inlining,
+      # which we use for key labels so isn't needed as an exposed attribute.
+      # Ditto for width and height of image.
+      filename = node.getAttribute('data-filename') ? ''
+      node.removeAttribute 'data-filename'
+      width = node.getAttribute 'data-width'
+      node.removeAttribute 'data-width'
+      height = node.getAttribute 'data-height'
+      node.removeAttribute 'data-height'
+      # Transfer x/y/width/height to <use> element, for more re-usability.
+      parent.replaceChild (use = doc.createElementNS SVGNS, 'use'), node
+      for attr in ['x', 'y', 'width', 'height']
+        use.setAttribute attr, node.getAttribute attr if node.hasAttribute attr
+        node.removeAttribute attr
+      # Memoize versions
+      attributes =
+        for attr in node.attributes
+          "#{attr.name}=#{attr.value}"
+      attributes.sort()
+      attributes = attributes.join ' '
+      if attributes not of inlineImages
+        inlineImageVersions[filename] ?= 0
+        version = inlineImageVersions[filename]++
+        inlineImages[attributes] = "$image$#{escapeId filename}$v#{version}"
+        svg.appendChild symbol = doc.createElementNS SVGNS, 'symbol'
+        symbol.setAttribute 'id', inlineImages[attributes]
+        # If we don't have width/height set from data-width/height fields,
+        # we take the first used width/height as the master height.
+        node.setAttribute 'width', width or use.getAttribute 'width'
+        node.setAttribute 'height', height or use.getAttribute 'height'
+        symbol.setAttribute 'viewBox', "0 0 #{width} #{height}"
+        symbol.appendChild node
+      use.setAttribute 'xlink:href', '#' + inlineImages[attributes]
+      keyBase = "$image$#{filename}"
+      false
     ## Lay out the symbols in the drawing via SVG <use>.
     viewBox = [0, 0, 0, 0]  ## initially x-min, y-min, x-max, y-max
     levels = {}
@@ -699,7 +765,7 @@
         continue unless symbol?
         levels[symbol.zIndex] ?= []
         levels[symbol.zIndex].push use = doc.createElementNS SVGNS, 'use'
-        use.setAttribute 'xlink:href', '#' + symbol.id()
+        use.setAttribute 'xlink:href', '#' + symbol.id
         use.setAttributeNS SVGNS, 'x', x
         use.setAttributeNS SVGNS, 'y', y
         scaleX = scaleY = 1


Fixes to implicit export default plugin

Fixes to implicit export default plugin

* Support arrow functions as a kind of function expression
* Turn off Babel caching (debugging pain!)
* Cleanup JS debugging code to not crash with CoffeeScript

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -38,8 +38,9 @@
         lastNode = last.node
         if types.isExpressionStatement(last) and (
           types.isObjectExpression(lastNode.expression) or
-          types.isFunctionExpression(lastNode.expression)
-          # not AssignmentExpression
+          types.isFunctionExpression(lastNode.expression) or
+          types.isArrowFunctionExpression(lastNode.expression)
+          # not AssignmentExpression or CallExpression
         )
           exportLast = types.exportDefaultDeclaration lastNode.expression
           exportLast.leadingComments = lastNode.leadingComments
@@ -82,7 +83,9 @@
     #inlineMap: true  # rely on Babel's source map
     transpile: babelConfig

-  require('@babel/register') babelConfig
+  ## Prevent Babel from caching its results, for changes to our plugins.
+  require('@babel/register') {...babelConfig, cache: false}
+
   CoffeeScript = require 'coffeescript'
   CoffeeScript.FILE_EXTENSIONS = ['.coffee', '.cjsx']
   CoffeeScript.register()
@@ -906,11 +909,12 @@
       ## Normally use `require` to load code as a real NodeJS module
       filename = path.resolve @filename
       ## Debug Babel output
-      #{code} = require('@babel/core').transform fs.readFileSync(filename), {
-      #  ...babelConfig
-      #  filename: @filename
-      #}
-      #console.log code
+      #if @constructor == JSMapping
+      #  {code} = require('@babel/core').transform fs.readFileSync(filename), {
+      #    ...babelConfig
+      #    filename: @filename
+      #  }
+      #  console.log code
       pirates?.settings = @settings
       @module = runWithMapping @, -> require filename
       super @module.default ? {}


Bindings should be last applied

Bindings should be last applied

--- src/template.coffee
+++ src/template.coffee
@@ -40,12 +40,12 @@
             @bindings.push binding

         applyContext: (context)->
+            for text in @text
+                text.applyContext context
+            for attr in @attrs
+                attr.applyContext context
             for binding in @bindings
                 binding.applyContext context
-            for attr in @attrs
-                attr.applyContext context
-            for text in @text
-                text.applyContext context
             return


payment: type => subscriptionType

payment: type => subscriptionType

--- workers/social/lib/social/models/payment/product.coffee
+++ workers/social/lib/social/models/payment/product.coffee
@@ -52,7 +52,7 @@

     JGroup = require '../group'

-    { type: subscriptionType, overageEnabled, title, description,
+    { subscriptionType, overageEnabled, title, description,
       feeAmount, feeUnit, feeInterval } = formData

     product = new this {


config: reworking executing generate.sh

config: reworking executing generate.sh

--- config/main.default.coffee
+++ config/main.default.coffee
@@ -66,22 +66,25 @@
   KONFIG.workers = require('./workers')(KONFIG, options, credentials)
   KONFIG.client.runtimeOptions = require('./generateRuntimeConfig')(KONFIG, credentials, options)

-  { spawn } = require 'child_process'
+  generateSh = "#{options.projectRoot}/config/generate.sh"

   # BUG(rjeczalik): The Configuration gets executed twice, once with uninitialized
   # options, which makes the following code execute generate.sh with
   # options.projectRoot equal to "/opt/koding/config". The todo here is to
   # fix it so it gets executed only once one remove the workaround.
-  generate = spawn '/bin/bash', ['-c', "test ! -d #{options.projectRoot}/config && exit 0 || cd #{options.projectRoot}/config && ./generate.sh #{KONFIG.kontrol.url}"]
-  generate.stdout.on 'data', (data) ->
-    console.error data.toString()
-  generate.on 'exit', (code) ->
-    if code != 0
-      console.log """
-        failed to run #{options.projectRoot}/config/generate.sh (#{code}), please
-        execute it manually and most likely install missing dependencies
-      """
-      process.exit 1
+  if fs.existsSync generateSh
+    { execFile } = require 'child_process'
+
+    execFile generateSh, ["#{KONFIG.kontrol.url}"], (err, stdout, stderr) ->
+      process.stderr.write stdout
+      process.stderr.write stderr
+
+      if err
+        console.log """
+          failed to run #{options.projectRoot}/config/generate.sh (error: #{err})
+          please execute it manually and most likely install missing dependencies
+        """
+        process.exit 1

   options.disabledWorkers = [
     "algoliaconnector"


HOTFIX: disabled chat because it overwrites global stuff which causes our search scripts to crash

--- app/assets/javascripts/application.coffee
+++ app/assets/javascripts/application.coffee
@@ -42,7 +42,7 @@
 # = require vendor/inviewport
 # = require vendor/ios-orientationchange-fix
 # = require vendor/image_path_helper
-# = require vendor/livechat
+# require vendor/livechat
 #
 # = require bootstrap-sprockets
 # = require algolia/algoliasearch.min


Hook up plus one button

Hook up plus one button

--- app/assets/javascripts/fancybox.js.coffee
+++ app/assets/javascripts/fancybox.js.coffee
@@ -1,11 +1,14 @@
 loadPlusOneButton = (fancybox) ->
   return unless window.signed_in
-  fancybox.title += plusOneButton()
+  id = fancybox.element.data('photo-id')
+  authenticityToken = $('#authenticity_token').val()
+  fancybox.title += plusOneButton(id, authenticityToken)

-plusOneButton = ->
-  "

-    <button>+1</button>
-
"
+plusOneButton = (id, authenticityToken) ->
+  "<form method='post' action='/photos/#{id}/plus_ones' data-remote class='pull-right'>
+    <input type='hidden' name='authenticity_token' value='#{authenticityToken}'></input>
+    <button type='submit'>+1</button>
+  </form>"

 loadMessagesContainer = ->
   "

"


add picture, fix caption to FB share

add picture, fix caption to FB share

--- app/assets/javascripts/preorder.js.coffee
+++ app/assets/javascripts/preorder.js.coffee
@@ -29,8 +29,9 @@
         display: "popup",
         redirect_uri: "http://www.flappytoy.com",
         link: "http://www.flappytoy.com",
-        caption: "FlappyToy - Play Flappy Bird with the wireless toy",
-        description: "Flappy is no ordinary plush toy.  He's wireless and controls the game when you lift him up!"
+        caption: "Play Flappy Bird with the wireless toy",
+        description: "Flappy is no ordinary plush toy.  He's wireless and controls the game when you lift him up!",
+        picture: "http://flappytoycom.s3.amazonaws.com/assets/video_placeholder.png"
       })

   bindGiftCheckbox: ->


add todo

add todo

--- app/assets/javascripts/static_page.coffee
+++ app/assets/javascripts/static_page.coffee
@@ -28,6 +28,7 @@
     }, 500, 'swing'

 $(window).on 'load', ->
+  # TODO: check size of display first
   $('.carousel').height($('.carousel').height()*0.70)
   # options for scrollfire
   sf_options = [


Use driver.run for replicas

Use driver.run for replicas

--- admin/static/coffee/tables/replicas.coffee
+++ admin/static/coffee/tables/replicas.coffee
@@ -26,7 +26,7 @@
             @listenTo @model, 'change:num_available_replicas', @render_status

             @progress_bar = new UIComponents.OperationProgressBar @template.status
-            @interval = null
+            @timer = null

             window.foo = @

@@ -105,20 +105,6 @@
         fetch_progress: =>
             #Keep ignore in window?
             #We also fetch shards
-            ignore = (shard) -> shard('role').ne('nothing')
-            query =
-                r.db(system_db).table('table_status').get(@model.get('uuid')).do( (table) ->
-                    r.branch(
-                        table.eq(null),
-                        null,
-                        table.merge(
-                            num_replicas: table("shards").concatMap( (shard) -> shard ).filter(ignore).count()
-                            num_available_replicas: table("shards").concatMap( (shard) -> shard ).filter(ignore).filter({state: "ready"}).count()
-                            num_shards: table("shards").count()
-                            num_available_shards: table("shards").concatMap( (shard) -> shard ).filter({role: "director", state: "ready"}).count()
-                        )
-                    )
-                )

             driver.run query, (error, result) =>
                 if error?
@@ -133,14 +119,37 @@
         render_status: =>
             #TODO Handle backfilling when available on the API
             if @model.get('num_available_replicas') < @model.get('num_replicas')
-                if not @interval?
-                    @interval = setInterval @fetch_progress, 1000
+                if not @timer?
+                    ignore = (shard) -> shard('role').ne('nothing')
+                    query =
+                        r.db(system_db).table('table_status').get(@model.get('uuid')).do( (table) ->
+                            r.branch(
+                                table.eq(null),
+                                null,
+                                table.merge(
+                                    num_replicas: table("shards").concatMap( (shard) -> shard ).filter(ignore).count()
+                                    num_available_replicas: table("shards").concatMap( (shard) -> shard ).filter(ignore).filter({state: "ready"}).count()
+                                    num_shards: table("shards").count()
+                                    num_available_shards: table("shards").concatMap( (shard) -> shard ).filter({role: "director", state: "ready"}).count()
+                                )
+                            )
+                        )
+
+                    @timer = driver.run query, 1000, (error, result) =>
+                        if error?
+                            # This can happen if the table is temporary unavailable. We log the error, and ignore it
+                            console.log "Nothing bad - Could not fetch replicas statuses"
+                            console.log error
+                        else
+                            @model.set result
+                            @render_status() # Force to refresh the progress bar
+
                 if @progress_bar.get_stage() is 'none'
                     @progress_bar.skip_to_processing() # if the stage is 'none', we skipt to processing
             else if @model.get('num_available_replicas') is @model.get('num_replicas')
-                if @interval?
-                    clearInterval @interval
-                    @interval = null
+                if @timer?
+                    driver.stop_timer @timer
+                    @timer = null

             progress_bar_info =
@@ -173,8 +182,8 @@
             return @

         remove: =>
-            if @interval
-                clearInterval @interval
-                @interval = null
+            if @timer
+                driver.stop_timer @timer
+                @timer = null
             @progress_bar.remove()
             super()


Fix the translation object's key

Fix the translation object's key

As pointed out by @splendido

--- t9n/tr.coffee
+++ t9n/tr.coffee
@@ -1,7 +1,7 @@
 # We need a dummy translation so that a text is found.

-en =
+tr =

   add: "ekle"
   and: "ve"


Change wording

Change wording

--- i18n/Steps.en.coffee
+++ i18n/Steps.en.coffee
@@ -2,7 +2,7 @@
   Steps:
     ExportBitlyDataToGoogleSpreadsheet:
       icon: "bolt"
-      default: "Enable automatic export of Bitly data to Google Drive"
+      default: "Activate daily export of Bitly links and referrers to Google Spreadsheet"
       progressBars:
         CreateGoogleSpreadsheet:
           waiting: "Will create a Google Spreadsheet for Bitly links"


cleanup js;

cleanup js;

--- app/public/javascripts/application.coffee
+++ app/public/javascripts/application.coffee
@@ -1,28 +1,21 @@
 class App
+  # App must be Singleton
   @instance = null
-  # App is Singleton
   @run: -> @instance || @instance = new App

   settings:
-    airlines_path: -> '/airlines'
-    airports_path: -> '/airports'
     search_path: -> '/search'
-    flight_search_path: (code) -> "/search/#{code}"

+  # initialization
   constructor: ->
-    @init_datepicker()
-    @init_form()
-
-  init_form: ->
     $('#search_form').submit @on_submit_form.bind(@)
-
-  init_datepicker: ->
     $('.date').datepicker
       format: 'yyyy-mm-dd'
       autoclose: true,
       toggleActive: true
       orientation: 'auto left'

+  # search form logic
   on_submit_form: ->
     departure = $('#departure').val()
     destination = $('#destination').val()
@@ -40,6 +33,7 @@
         that.message(response.responseJSON.message, 'alert')
     false

+  # display UI messages (errors etc)
   message: (msg, type = '')->
     @message_container ||= $('#message')
     return @message_container.html(msg) if type is ''


creat

creat

--- keymaps/my-r-e-p-l.cson
+++ keymaps/my-r-e-p-l.cson
@@ -11,4 +11,4 @@
   'ctrl-y': 'my-r-e-p-l:toggle',
   'ctrl-y ctrl-i': 'my-r-e-p-l:createFromFile',
   'ctrl-y ctrl-f': 'my-r-e-p-l:interprete',
-  'ctrl-y ctrl-s': 'my-r-e-p-l:toggle'
+  'ctrl-y ctrl-s': 'my-r-e-p-l:create'


Fixed issues with cookie forwarding to the server-side DOM

--- server/http/middleware/angular.coffee
+++ server/http/middleware/angular.coffee
@@ -29,20 +29,18 @@
 				domain: get: -> 'localhost'
 			)

-			# Set up a DOM starting with our entry point.
+			# Set up a DOM, forwarding our cookie and navigating to the entry
+			# point.
 			document = jsdom(
-				index
-				null
+				index, jsdom.defaultLevel
+
+				cookie: cookie
+				cookieDomain: 'localhost'
 				url: "http://localhost:#{http.port()}/shrub-entry-point"
 			)
-
-			# Hax for XMLHttpRequest
-			document.cookie = cookie
-			document._cookieDomain = 'localhost'
-
 			window = document.createWindow()

-			# Capture console logs.
+			# Capture "client" console logs.
 			window.console = logger

 			# Hack in WebSocket.
@@ -51,7 +49,8 @@
 			# Inject Angular services so we can get up in its guts.
 			shrub = window.shrub = {}
 			injected = [
-				'$location', '$rootScope', '$route', '$sniffer', 'forms', 'socket'
+				'$location', '$rootScope', '$route', '$sniffer'
+				'forms', 'socket'
 			]
 			invocation = injected.concat [
 				-> shrub[inject] = arguments[i] for inject, i in injected
@@ -135,7 +134,7 @@

 			# Give a few extra ms for form logic.
 			delay + WINDOW_RENDER_TIME
-
+
 	(req, res, next) ->
 		{path} = url.parse req.url

@@ -146,32 +145,73 @@
 		locals = res.locals
 		body = req.body

-		# Get a DOM window.
-		angularWindow(id, req.headers.cookie, locals).done (window) ->
-
-			shrub = window.shrub
-
-			routes = shrub.$route.routes
-			if routes[path]?
-
-				# Does this path redirect? Do an HTTP redirect.
-				if routes[path].redirectTo?
-					return res.redirect routes[path].redirectTo
-
+		# Uncomment to bypass the server-side Angular.
+#		return http.renderApp(locals).done (index) -> res.end index
+
+		extractCookie = ->
+
+			deferred = Q.defer()
+
+			# If the client is in sync, awesome!
+			if req.signedCookies[http.sessionKey()] is req.sessionID
+
+				deferred.resolve req.headers.cookie
+
+			# Otherwise, stimulate the session middleware to create the cookie.
 			else
-
-				# Is there no path entry? Check for a default.
-				if routes[null]?
-					return res.redirect routes[null].redirectTo
-
-			# Navigate the Angular system to the new path.
-			navigate(window, path, body).done (delay) ->
-
-				# I'm not sure how to synchronize this.
-				setTimeout(
-					->
+				res.emit 'header'
+
+				# Yank it out of the response headers and map it.
+				setCookie = res._headers['set-cookie']
+				cookieObject = {}
+				for kv in setCookie.split ';'
+					[key, value] = kv.split '='
+					cookieObject[key.trim()] = value
+
+				# Pull out junk that only makes sense en route to client.
+				delete cookieObject['Path']
+				delete cookieObject['HttpOnly']
+
+				# Rebuild the cookie string.
+				cookie = ''
+				for k, v of cookieObject
+					cookie += '; ' if cookie
+					cookie += k + '=' + v
+
+				# Commit the session before offering the cookie, otherwise it
+				# wouldn't actually be pointing at anything yet.
+				req.session.save -> deferred.resolve cookie
+
+			deferred.promise
+
+		extractCookie().then (cookie) ->
+
+			# Get a DOM window.
+			angularWindow(id, cookie, locals).done (window) ->
+
+				shrub = window.shrub
+
+				routes = shrub.$route.routes
+				if routes[path]?
+
+					# Does this path redirect? Do an HTTP redirect.
+					if routes[path].redirectTo?
+						return res.redirect routes[path].redirectTo
+
+				else
+
+					# Is there no path entry? Check for a default.
+					if routes[null]?
+						return res.redirect routes[null].redirectTo
+
+				# Navigate the Angular system to the new path.
+				navigate(window, path, body).done (delay) ->
+
+					# I'm not sure how to synchronize this.
+					setTimeout(

 						# Emit the HTML as it appears.
-						res.end window.document.innerHTML
-					delay
-				)
+						-> res.end window.document.innerHTML
+
+						delay
+					)


Track editor macro views.

Track editor macro views.

--- services/web/public/coffee/ide.coffee
+++ services/web/public/coffee/ide.coffee
@@ -42,7 +42,7 @@
 	ReferencesManager
 ) ->

-	App.controller "IdeController", ($scope, $timeout, ide, localStorage) ->
+	App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) ->
 		# Don't freak out if we're already in an apply callback
 		$scope.$originalApply = $scope.$apply
 		$scope.$apply = (fn = () ->) ->
@@ -69,6 +69,16 @@

 		$scope.chat = {}

+		# Tracking code.
+		$scope.$watch "ui.view", (newView, oldView) ->
+			event_tracking.send "ide-open-view-#{ newView }" if newView?
+
+		$scope.$watch "ui.chatOpen", (isOpen) ->
+			event_tracking.send "ide-open-chat" if isOpen
+
+		$scope.$watch "ui.leftMenuShown", (isOpen) ->
+			event_tracking.send "ide-open-left-menu" if isOpen
+		# End of tracking code.

 		window._ide = ide


Prevent scrolling of datepicker input

Prevent scrolling of datepicker input

The most annoying default of all times...

--- app/assets/javascripts/alchemy/alchemy.datepicker.js.coffee
+++ app/assets/javascripts/alchemy/alchemy.datepicker.js.coffee
@@ -15,6 +15,7 @@
       $datepicker_inputs.each ->
         type = $(this).data('datepicker-type')
         options =
+          scrollInput: false
           format: Alchemy.t("formats.#{type}")
           timepicker: /time/.test(type)
           datepicker: /date/.test(type)


Switch to Spree.ajax & success / error callbacks

--- backend/app/assets/javascripts/spree/backend/store_credits.js.coffee
+++ backend/app/assets/javascripts/spree/backend/store_credits.js.coffee
@@ -11,20 +11,21 @@

   $('.js-save-memo').on('click', (ev) ->
     ev.preventDefault()
-    $.ajax(
+    Spree.ajax(
       $('#sc_memo_edit_form').attr('url'), {
         data: $('#sc_memo_edit_form').serialize(),
         dataType: 'json',
         method: 'put',
-        complete: (xhr, status) =>
-          if xhr.status is 200
-            newValue = $('#sc_memo_edit_form').find("[name='store_credit[memo]']").val()
-            $('#memo-edit-row').data('original-value', newValue)
-            $('#memo-display-row').find('.js-memo-text').text(newValue)
-            hideEditMemoForm()
-            show_flash('success', xhr.responseJSON.message)
-          else if xhr.status is 400
-            show_flash('error', xhr.responseJSON.message)
+        success: ( (response) =>
+          newValue = $('#sc_memo_edit_form').find("[name='store_credit[memo]']").val()
+          $('#memo-edit-row').data('original-value', newValue)
+          $('#memo-display-row').find('.js-memo-text').text(newValue)
+          hideEditMemoForm()
+          show_flash('success', response.message)
+        ),
+        error: ( (xhr, status) =>
+          show_flash('error', xhr.responseJSON.message)
+        )
       }
     )
   )


resurrection.activity: set anchors in activity body

pass internal links directly to router
open external links in new tab

--- client/Social/Activity/ListItems/ActivityListItemStatusUpdate.coffee
+++ client/Social/Activity/ListItems/ActivityListItemStatusUpdate.coffee
@@ -37,11 +37,33 @@
     @setTemplate @pistachio()
     @template.update()

+    @setAnchors()
+
     @utils.defer =>
       predicate = @getData().link?.link_url? and @getData().link.link_url isnt ''
       if predicate
       then @embedBox.show()
       else @embedBox.hide()
+
+  setAnchors: ->
+    @$(".status-body a").each (index, element) ->
+      {location: {origin}} = window
+      href = element.getAttribute "href"
+      return  unless href
+
+      if href.substring(0, origin.length) is origin
+        element.setAttribute "href", "/#{href.substring origin.length + 1}"
+        element.classList.add "internal"
+      else
+        element.setAttribute "target", "_blank"
+
+  click: (event) ->
+    super event
+    {target} = event
+    if $(event.target).is ".status-body a.internal"
+      @utils.stopDOMEvent event
+      href = target.getAttribute "href"
+      KD.singleton("router").handleRoute href

   pistachio:->
     """


resurrection.activity: apply markdown to status update activity content

--- client/Social/Activity/ListItems/ActivityListItemStatusUpdate.coffee
+++ client/Social/Activity/ListItems/ActivityListItemStatusUpdate.coffee
@@ -56,7 +56,7 @@
     return  match[1].split /:/g  if match = str.match /^\|(.+)\|$/

   formatContent: (str = "")->
-    str = @utils.applyTextExpansions str, yes
+    str = @utils.applyMarkdown str
     str = @expandTokens str
     return  str


Admin: Integrations: Let router do the heavy lifting.

--- client/admin/lib/views/integrations/adminintegrationsetupview.coffee
+++ client/admin/lib/views/integrations/adminintegrationsetupview.coffee
@@ -31,7 +31,7 @@
     @cancelButton = new KDButtonView
       title    : 'Cancel'
       cssClass : 'solid red compact cancel'
-      callback : @bound 'destroy'
+      callback : -> kd.singletons.router.handleRoute '/Admin/Integrations'

   setIntegration: ->
@@ -43,26 +43,7 @@
     integrationHelpers.create options, (err, response) =>
       return console.warn "couldnt create integration", err  if err

-      integration = @getData()
-
-      data =
-        id              : response.id
-        integration     : @getData()
-        token           : response.token
-        integrationId   : response.integrationId
-        selectedChannel : response.channelId
-        channels        : integration.channels
-        webhookUrl      : "#{globals.config.integration.url}/#{integration.name}/#{response.token}"
-
-      if integration.settings?.events
-        events = try JSON.parse integration.settings.events
-        catch e then []
-        data.settings = {events}
-
-      delete integration.channels
-
-      @destroy()
-      @emit 'NewIntegrationAdded', data
+      kd.singletons.router.handleRoute "/Admin/Integrations/Configure/#{response.id}"

   pistachio: ->


Removed unneeded code Altered structure of return data to make it easier to work with

--- server/methods/canAccessResource.coffee
+++ server/methods/canAccessResource.coffee
@@ -47,28 +47,15 @@
 			if user?.profile?.access
 				userPermissions = new Jedis.AccessPermission user.profile.access
 				result = userPermissions.canAccessResource resourcePermissions
-				console.log user._id
-				console.log result
 				allowed = result.canAccess
 				failedPermIds = result.failIds

 			unless allowed
 				deniedUser = {}
-				deniedUser[user._id] = failedPermIds
+				deniedUser.user = user._id
+				deniedUser.failedPermIds = failedPermIds
 				deniedUsers.push deniedUser

-		console.log deniedUsers
-
-		###
-		deniedUsers = _.filter users, (user) ->
-			allowed = true
-			if user?.profile?.access
-				userPermissions = new Jedis.AccessPermission user.profile.access
-				allowed = userPermissions.canAccessResource(resourcePermissions).canAccess
-			# exclude users that can access the resource
-			return not allowed
-		###
 		canAccess = deniedUsers.length is 0
-
-		#return { canAccess: canAccess, deniedUsers : _.pluck deniedUsers, '_id' };
+
 		return { canAccess: canAccess, deniedUsers : deniedUsers }


fixing a 1 character typo

fixing a 1 character typo

--- src/beautifiers/vue-beautifier.coffee
+++ src/beautifiers/vue-beautifier.coffee
@@ -44,7 +44,7 @@
                   lang: "less"
                   mode: "beautify"
                 )
-                prettydiffoptions)
+                prettydiff(options)
               when undefined
                 require("js-beautify").css(text, options)
               else


Added sitemapindex support to sitemapgeneratorworker.

--- workers/sitemapgenerator/lib/sitemapgenerator/sitemapgeneratorworker.coffee
+++ workers/sitemapgenerator/lib/sitemapgenerator/sitemapgeneratorworker.coffee
@@ -10,29 +10,40 @@
 module.exports = class SitemapGeneratorWorker extends EventEmitter
   constructor: (@bongo, @options = {}) ->

-  generateSitemapString: (urls, isMain=false)->
-
-    # sitemap.XML beginning and ending parts
+  generateSitemapString: (urls)->
+    # sitemap.xml beginning and ending parts
     sitemap = '<?xml version="1.0" encoding="UTF-8"?>
-      <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
-      xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
+      <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+      xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
       xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">'
     sitemapFooter = '</urlset>'

     # while generating main sitemap, we don't need hashbang in the url.
     for url in urls
-      if isMain
-        sitemap += "<url><loc>#{@options.uri.address}/#{url}</loc></url>"
-      else
-        sitemap += "<url><loc>#{@options.uri.address}/#!/#{url}</loc></url>"
+      sitemap += "<url><loc>#{@options.uri.address}/#!/#{url}</loc></url>"
     sitemap += sitemapFooter
+    return sitemap
+
+  generateSitemapIndexString: (sitemapNames)->
+    # sitemapindex.xml beginning and ending parts
+    sitemap = '<?xml version="1.0" encoding="UTF-8"?>
+      <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
+    sitemapFooter = '</sitemapindex>'
+
+    # while generating main sitemap, we don't need hashbang in the url.
+    for sitemapURL in sitemapNames
+      url = "<sitemap><loc>#{@options.uri.address}/#{sitemapURL}</loc></sitemap>"
+      console.log url
+      sitemap += "<sitemap><loc>#{@options.uri.address}/#{sitemapURL}</loc></sitemap>"
+    sitemap += sitemapFooter
+    return sitemap

   generateSitemapName: (groupPageNumber, skip)->
     return  "sitemap_" + groupPageNumber + "_" + skip + "_" + (skip + NAMEPERPAGE) + ".xml"

   saveSitemap: (name, content)->
     {JSitemap} = @bongo.models
-    JSitemap.update {name}, $set : {content}, {upsert : yes}, (err)->
+    JSitemap.update {name}, $set : {content}, {upsert : yes}, (err)->
       console.log err if err

   generate:=>
@@ -41,10 +52,9 @@
     groupSelector = {
       privacy:'public'
     }
-
-    generateMainSitemap = (sitemapNames)=>
+    generateSitemapIndex = (sitemapNames)=>
       name = "sitemap.xml"
-      content = @generateSitemapString sitemapNames, true
+      content = @generateSitemapIndexString sitemapNames
       @saveSitemap name, content

     JGroup.count groupSelector, (err, groupCount)=>
@@ -72,7 +82,7 @@
             'slugs.group': { $in: publicGroups }
           }

-          # Empty the group, it'll be filled in next cycle.
+          # Empty the group, it'll be filled in next iteration.
           publicGroups = []

           JName.count selector, (err, count)=>
@@ -85,7 +95,7 @@
               }
               JName.some selector, option, (err, names)=>
                 if names
-                  urls = (name.name for name in names)
+                  urls = (name.name for name in names)
                   sitemapName =  @generateSitemapName groupPageNumber, skip
                   content = @generateSitemapString urls
                   @saveSitemap sitemapName, content
@@ -95,9 +105,9 @@
                 queue.next()
             queue.push => groupQueue.next()
             daisy queue
-

-      groupQueue.push => generateMainSitemap(groupQueue.sitemapNames)
+
+      groupQueue.push => generateSitemapIndex(groupQueue.sitemapNames)
       daisy groupQueue


Duplicate line/tabstop commands for Linux/Windows

--- keymap.cson
+++ keymap.cson
@@ -15,11 +15,8 @@

 "atom-text-editor":
 	"f5":             "lines:sort-basic"
-	"ctrl-u":         "lines:unique"
 	"alt-up":         "editor:move-line-up"
 	"alt-down":       "editor:move-line-down"
-	"ctrl-a":         "user:align-columns"
-	"ctrl-w":         "user:unfuck-tabstops"
 	"[":              "user:square-bracket"
 	"{":              "user:curly-bracket"

@@ -82,6 +79,9 @@
 	"ctrl-d":         "editor:delete-line"
 	"ctrl-alt-up":    "editor:duplicate-lines"
 	"ctrl-w":         "editor:close-unprompted"
+	"alt-u":          "lines:unique"
+	"alt-a":          "user:align-columns"
+	"alt-w":          "user:unfuck-tabstops"
 	"ctrl-0":         "user:zoom-text"
 	"shift-ctrl-e":   "user:specsaver"
 	"ctrl-r":         "unset!"
@@ -136,6 +136,9 @@
 	"cmd-d":          "editor:delete-line"
 	"cmd-alt-up":     "editor:duplicate-lines"
 	"cmd-w":          "editor:close-unprompted"
+	"ctrl-u":         "lines:unique"
+	"ctrl-a":         "user:align-columns"
+	"ctrl-w":         "user:unfuck-tabstops"
 	"cmd-0":          "user:zoom-text"
 	"shift-cmd-e":    "user:specsaver"
 	"cmd-r":          "unset!"


Decide where to fetch content from based on its version

--- src/scripts/models/contents/node.coffee
+++ src/scripts/models/contents/node.coffee
@@ -7,15 +7,18 @@
   settings = require('settings')
   require('backbone-associations')

-  SERVER = "#{location.protocol}//#{settings.cnxarchive.host}:#{settings.cnxarchive.port}"
+  ARCHIVE = "#{location.protocol}//#{settings.cnxarchive.host}:#{settings.cnxarchive.port}"
+  AUTHORING = "#{location.protocol}//#{settings.cnxauthoring.host}:#{settings.cnxauthoring.port}"

   return class Node extends Backbone.AssociatedModel
     # url: () -> "#{SERVER}/contents/#{@id}"
     url: () ->
-      if @isNew()
-        return "#{SERVER}/contents"
+      if @get('version') is 'draft'
+        server = AUTHORING
       else
-        return "#{SERVER}/contents/#{@id}.json" # FIX: Remove .json from URL
+        server = ARCHIVE
+
+      return "#{server}/contents/#{@id}"

     parse: (response, options) ->
       # Don't overwrite the title from the book's table of contents
@@ -30,8 +33,13 @@

       @set('downloads', 'loading')

+      if @get('version') is 'draft'
+        server = AUTHORING
+      else
+        server = ARCHIVE
+
       $.ajax
-        url: "#{SERVER}/extras/#{@id}"
+        url: "#{server}/extras/#{@id}"
         dataType: 'json'
       .done (response) =>
         @set('downloads', response.downloads)


Save iframe state every 5 seconds (check if state has been changed first)

[#89024392]

--- app/assets/javascripts/iframe-saver.coffee
+++ app/assets/javascripts/iframe-saver.coffee
@@ -1,18 +1,15 @@
-instances = []
-
 # IFrameSaver : Wrapper around IFramePhone to save & Load IFrame data
 # into interactive_run_state models in LARA.
 class IFrameSaver
-
-  @instances:      []  # save-on-change.coffee looks these up.
+  @instances: []  # save-on-change.coffee looks these up.

   # @param iframe         : an iframe to save data from
   # @param $data_div      : a qjuery element that includes data-* attributes which describe where we post back to
   # @param $delete_button : a qjuery element that is used to trigger data deletion on click
   constructor: (iframe, $data_div, $delete_button) ->
-    @put_url  = $data_div.data('puturl')  # put our data here.
-    @get_url  = $data_div.data('geturl')  # read our data from here.
-    @auth_provider = $data_div.data('authprovider')  # through which provider did the current user log in
+    @put_url = $data_div.data('puturl') # put our data here.
+    @get_url = $data_div.data('geturl') # read our data from here.
+    @auth_provider = $data_div.data('authprovider') # through which provider did the current user log in
     @user_email = $data_div.data('user-email')
     @logged_in = $data_div.data('loggedin') # true/false - is the current session associated with a user
     @learner_url = null
@@ -21,7 +18,7 @@
       @delete_data()
     @$delete_button.hide()
     @should_show_delete = null
-    @has_saved_state = false
+    @saved_state = null

     @save_indicator = SaveIndicator.instance()

@@ -33,7 +30,6 @@
         @load_interactive()

     phone_answered = () =>
-
       @iframePhone.addListener 'setLearnerUrl', (learner_url) =>
         @learner_url = learner_url
       @iframePhone.addListener 'interactiveState', (interactive_json) =>
@@ -41,35 +37,33 @@
           @save_to_server(interactive_json, @learner_url)
         else
           # wait a bit and try again:
-          window.setTimeout () =>
+          window.setTimeout( =>
             @save_to_server(interactive_json, @learner_url)
-          ,
-          500
+          , 500)
       @iframePhone.addListener 'getAuthInfo', =>
-        authInfo = { provider: @auth_provider, loggedIn: @logged_in}
+        authInfo = {provider: @auth_provider, loggedIn: @logged_in}
         if @user_email?
           authInfo.email = @user_email
         @iframePhone.post('authInfo', authInfo)
       @iframePhone.addListener 'extendedSupport', (opts)=>
         if opts.reset?
           @should_show_delete = opts.reset
-          if @has_saved_state
+          if @saved_state
             if @should_show_delete
               @$delete_button.show()
             else
               @$delete_button.hide()

       if @put_url
-        #Save interactive every 42 seconds just to be safe:
-        window.setInterval (()=> @save()), 42 * 1000
+        #Save interactive every 5 seconds just to be safe:
+        window.setInterval (()=> @save()), 5 * 1000
       @iframePhone.post('getExtendedSupport')
       @iframePhone.post('getLearnerUrl')
       # TODO: There used to be a callback for model_did_load
       # hopefully this will work
       model_did_load()

-    @iframePhone      = new iframePhone.ParentEndpoint(iframe, phone_answered)
-
+    @iframePhone = new iframePhone.ParentEndpoint(iframe, phone_answered)

   @default_success: ->
@@ -78,11 +72,11 @@
   error: (msg) ->
     @save_indicator.showSaveFailed(msg)

-  save: (success_callback=null) ->
+  save: (success_callback = null) ->
     @success_callback = success_callback

     # will call back into "@save_to_server)
-    @iframePhone.post({ type:'getInteractiveState' })
+    @iframePhone.post({type: 'getInteractiveState'})

   confirm_delete: (callback) ->
     if (window.confirm("Are you sure you want to restart your work in this model?"))
@@ -93,15 +87,20 @@
       window.location.reload()
     @confirm_delete () =>
       @learner_url = null
-      @save_to_server(null,"")
+      @save_to_server(null, "")

   save_to_server: (interactive_json, learner_url) ->
     return unless @put_url
+    # Do not send the same state to server over and over again.
+    return if JSON.stringify(interactive_json) == JSON.stringify(@saved_state)
+
     runSuccess = =>
+      @saved_state = interactive_json
       if @success_callback
         @success_callback()
       else
         @default_success
+
     if interactive_json is "nochange"
       runSuccess()
       return
@@ -119,8 +118,6 @@
       success: (response) =>
         runSuccess()
         @save_indicator.showSaved("Saved Interactive")
-
-
       error: (jqxhr, status, error) =>
         @error("couldn't save interactive")

@@ -132,13 +129,13 @@
         if response['raw_data']
           interactive = JSON.parse(response['raw_data'])
           if interactive
-            @has_saved_state = true
-            @iframePhone.post({ type:'loadInteractive', content:interactive  })
+            @saved_state = interactive
+            @iframePhone.post({type: 'loadInteractive', content: interactive})
             @$delete_button.show() if @should_show_delete == null or @should_show_delete
-
       error: (jqxhr, status, error) =>
         @error("couldn't load interactive")

+# Export constructor.
 window.IFrameSaver = IFrameSaver

 $(document).ready ->


minor change

minor change

--- app/assets/javascripts/item_sets.js.coffee
+++ app/assets/javascripts/item_sets.js.coffee
@@ -74,6 +74,7 @@

     exportObj.blocks.push(blockObj)

+  exportObj.map = 'any'
   exportObj

 itemSetNamespace.controller("itemSetsController",  ($scope, $http) ->


OX-120 debug

OX-120 debug

--- app/assets/javascripts/model/binary.coffee
+++ app/assets/javascripts/model/binary.coffee
@@ -206,6 +206,8 @@
           direction[2] / directionMax
         ]

+      console.log direction
+
       delta_x = delta_y = delta_z = 0
       direction_x = direction_y = direction_z = 0
       index = buckets.length


#258 added error message for missing datalayers; fixes #258

--- app/assets/javascripts/oxalis/model.coffee
+++ app/assets/javascripts/oxalis/model.coffee
@@ -62,11 +62,19 @@

       if tracing.error
         Toast.error(tracing.error)
-        {"error": true}
+        return {"error" : true}

       else unless tracing.content.dataSet
-        Toast.error("Selected dataset doesnt exist")
-        {"error": true}
+        Toast.error("Selected dataset doesn't exist")
+        return {"error" : true}
+
+      else unless tracing.content.dataSet.dataLayers
+        datasetName = tracing.content.dataSet.name
+        if datasetName
+          Toast.error("Please, double check if you have the dataset '#{datasetName}' imported.")
+        else
+          Toast.error("Please, make sure you have a dataset imported.")
+        return {"error" : true}

       else
         Request.send(


remove debug logging

remove debug logging

--- src/server/virtual_browser/index.coffee
+++ src/server/virtual_browser/index.coffee
@@ -511,13 +511,6 @@
             # Create an event we can dispatch on the server.
             serverEv = RPCMethods._createEvent(clientEv, @browser.window)

-            if serverEv.type is 'change'
-                logger("Dispatching #{serverEv.type}\t" +
-                            "[#{eventTypeToGroup[clientEv.type]}] on " +
-                            "#{clientEv.target.__nodeID} [#{clientEv.target.tagName}]")
-
-                logger("bubbling: #{clientEv.bubbles}")
-
             clientEv.target.dispatchEvent(serverEv)
             if @domChanged
                 @broadcastEvent('resumeRendering', id)
@@ -528,8 +521,6 @@
                 gc()
             @server.eventTracker.inc()

-            if serverEv.type is 'change'
-                logger("Finished processing event: #{serverEv.type}")

     # Takes a clientEv (an event generated on the client and sent over DNode)
     # and creates a corresponding event for the server's DOM.


Avoid rasce condition when adding principals

* Trying to add a principal without choosing one from the dropdown
  this leads to 1) one useless invalid request and 2) could lead to a
  race condition avoiding the field to get clearing after choosing one.

--- app/assets/rdb/PermissionComponents.coffee
+++ app/assets/rdb/PermissionComponents.coffee
@@ -93,18 +93,19 @@
     principal = @refs['principal'].value()
     role      = @refs['role'].value().value

-    p = new Promise (resolve, reject) =>
-      @props.collection.create {role: role, principal: principal},
-        success: resolve
-        error: reject
+    if principal
+      p = new Promise (resolve, reject) =>
+        @props.collection.create {role: role, principal: principal},
+          success: resolve
+          error: reject

-    p = p.then =>
-      @props.collection.fetch merge: true
+      p = p.then =>
+        @props.collection.fetch merge: true

-    @refs['indicator'].track p
+      @refs['indicator'].track p

-    @refs['principal'].clear()
-    @refs['principal'].focus()
+      @refs['principal'].clear()
+      @refs['principal'].focus()

   renderPrincipal: (principal) ->
     components = []


Fix broken require in the crop task (#5283)

--- app/classifier/tasks/crop/initializer.cjsx
+++ app/classifier/tasks/crop/initializer.cjsx
@@ -1,6 +1,6 @@
 React = require 'react'
 createReactClass = require 'create-react-class'
-Draggable = require '../../../lib/draggable'
+Draggable = require('../../../lib/draggable').default
 DragHandle = require '../../drawing-tools/drag-handle'

 module.exports = createReactClass


Create AttachmentsMixin Controller

Create AttachmentsMixin Controller

--- app/coffee/modules/controllerMixins.coffee
+++ app/coffee/modules/controllerMixins.coffee
@@ -27,6 +27,10 @@
 toString = @.taiga.toString

+#############################################################################
+## Page Mixin
+#############################################################################
+
 class PageMixin
     loadUsersAndRoles: ->
         promise = @q.all([
@@ -48,8 +52,13 @@

             return results

+taiga.PageMixin = PageMixin

-# This mixin requires @location and @scope
+
+#############################################################################
+## Filters Mixin
+#############################################################################
+# This mixin requires @location ($tgLocation) and @scope

 class FiltersMixin
     selectFilter: (name, value, load=false) ->
@@ -88,7 +97,31 @@
         location = if load then @location else @location.noreload(@scope)
         location.search(name, value)

+taiga.FiltersMixin = FiltersMixin

-taiga = @.taiga
-taiga.PageMixin = PageMixin
-taiga.FiltersMixin = FiltersMixin
+
+#############################################################################
+## Attachments Mixin
+#############################################################################
+# This mixin requires @rs ($tgResources), @scope and @log ($tgLog)
+# The mixin required @..attachmentsUrlName (p.e. 'issues/attachments',see resources.coffee)
+
+class AttachmentsMixin
+    loadAttachments: (objectId) ->
+        if not @.attachmentsUrlName
+            return @log.error "AttachmentsMixin: @.attachmentsUrlName is required"
+
+        @scope.attachmentsCount = 0
+        @scope.activeAttachmentsCount = 0
+        @scope.deprecatedAttachmentsCount = 0
+
+        return @rs.attachments.list(@.attachmentsUrlName, objectId).then (attachments) =>
+            @scope.attachments = _.sortBy(attachments, "order")
+            @scope.attachmentsCount = @scope.attachments.length
+            @scope.activeAttachments = _.filter(@scope.attachments, is_deprecated: false)
+            @scope.activeAttachmentsCount = @scope.activeAttachments.length
+            @scope.deprecatedAttachments = _.filter(@scope.attachments, is_deprecated: true)
+            @scope.deprecatedAttachmentsCount = @scope.deprecatedAttachments.length
+            return attachments
+
+taiga.AttachmentsMixin = AttachmentsMixin


Update translations.coffee

Update translations.coffee

--- addon/locales/zh-tw/translations.coffee
+++ addon/locales/zh-tw/translations.coffee
@@ -11,14 +11,14 @@
   "language": "繁體中文"
   "loading": "正在加載..."
   "loading_timeout": "需時太久了！點擊可以重新加載"
-  "goodcity.hk" : "GoodCity.HK"
+  "goodcity.hk" : "好人好市.HK"
   "skip_intro" : "跳過簡介"
   "next" : "下一步"
   "prev" : "上一步"
   "back" : "回到上一頁"
   "end" : "結束"
   "done": "完成"
-  "other": "Other"
+  "other": "其他"
   "error.message" : "出錯了，請見諒！"
   "images" : "圖片"
   "delete" : "刪除"
@@ -47,15 +47,15 @@
   "thank_you": "多謝"
   "go_to_top": "回到頁面頂端"
   "input_length_error_message" : "InputWithCounter doesn\'t work without a maxlength attribute'"
-  "loading_timeout_error": "Loading view timeout reached."
+  "loading_timeout_error": "讀取時限已過"
   "items_text": "項目"
-  "version": "Version"
-  "crossroads_foundation": "Crossroads Foundation"
+  "version": "版本"
+  "crossroads_foundation": "十字路會"

   "menu":
-    "terms": "Terms of Use"
-    "policy": "Privacy Policy"
-    "version_short": "ver."
+    "terms": "使用條款"
+    "policy": "私隱政策"
+    "version_short": "版本"

   "day":
     "today": "今天"
@@ -118,7 +118,7 @@
     "unread" : "({{unreadCount}}) 條未讀信息"
     "owner" : "你"
     "today_with_time": "今天, {{time}}"
-    "chat_note": "Chat about this item with our reviewers"
+    "chat_note": "與審查員討論這項物資狀況"

   "notifications":
     "view": "查看"
@@ -142,8 +142,8 @@
       "back": "回到上一頁"
       "cancel_booking": "取消預約"
       "confirm_ggv_cancel": "當GoGoVan確認預約取消，您就能取消預約"
-      "driver_mobile": "Driver's Mobile"
-      "ggv_office": "GoGoVan Office"
+      "driver_mobile": "司機電話號碼"
+      "ggv_office": "GoGoVan辦公室"

   "delivery_details":
     "name": "聯絡人姓名"
@@ -185,13 +185,13 @@
     "missing": "{{name}} 將此物的狀況改為'失蹤'"

   "offer_log":
-    "donor_called": "{{name}} called"
-    "call_accepted": "{{name}} answered call"
+    "donor_called": "{{name}} 曾經來電"
+    "call_accepted": "{{name}} 接通電話"

   "edit_images":
-    "upload": "Choose Image"
-    "camera": "Take Photo"
-    "cancel": "Cancel"
+    "upload": "選擇圖片"
+    "camera": "拍照"
+    "cancel": "取消"
     "add_photo" : "加入圖片"
     "delete_confirm" : "確定要刪除圖片嗎？"
     "cant_delete_last_image" : "最少需要一張圖片"
@@ -203,8 +203,8 @@
     "image_uploading": "圖片上載中"
     "remove_image": "移除圖片"
     "cancel_item": "取消項目"
-    "cannot_provide_photo": "Cannot provide photo"
-    "last_image_with_item": "This is the only image associated with this item"
+    "cannot_provide_photo": "未能提供照片"
+    "last_image_with_item": "此為這項物資的唯一圖片"
     "cancelling_item_will_cancel_offer" : "刪除最後一項物品即會刪除此捐獻及已預約的運輸。"

   "plan_delivery":
@@ -304,7 +304,7 @@
   "drop-off":
     "select_schedule": "選擇時間"
     "expect_delivery": "物資大約何時到達？"
-    "find_crossroads": "Find Crossroads on Google Maps"
+    "find_crossroads": "在Google地圖上尋找十字路會"
     "open_new_tab": "將在新視窗打開"
     "arrive_on_time": "煩請於我們的辦公時間內到達，以確保我們能順利接收您的物資。"
     "book_schedule": "預約時間"
@@ -315,9 +315,9 @@

   "offer":
     "disable": "關閉"
-    "closed_by": "Closed by {{lastName}} {{firstName}}"
-    "cancelled_by": "Cancelled by {{lastName}} {{firstName}}"
-    "received_by": "Received by {{lastName}} {{firstName}}"
+    "closed_by": "由 {{lastName}} {{firstName}} 關閉"
+    "cancelled_by": "由 {{lastName}} {{firstName}} 取消"
+    "received_by": "由 {{lastName}} {{firstName}} 接收"

   # Components
   "upload-images":
@@ -398,67 +398,67 @@
     "last_updated": "本聲明最後更新日期為2014年8月。"

   "terms":
-    "title": "Terms and Conditions"
-    "info": "GoodCity.hk is part of the Crossroads Foundation network. By using the GoodCity.hk website http://www.goodcity.hk (\"GoodCity Website\") you are agreeing to be bound by these terms of use between you and the Crossroads Foundation and its affiliates (\"Crossroads\", \"us\", \"we\" and \"our\")."
+    "title": "使用條款及細則"
+    "info": "好人好市.hk為十字路會聯網的一部分。使用好人好市.hk網頁http://www.goodcity.hk (\"好人好市網站\")，即表示閣下同意閣下與十字路會及其旗下所屬部門 (\"十字路會\", \"我們\" and \"我們的\")之間的使用條款。"

     "agreement":
-      "title": "Your Agreement"
-      "terms_of_use": "You agree to use the GoodCity Website in accordance with these terms of use."
-      "use_authority": "You agree to use the GoodCity Website only for your personal use. If you are using the GoodCity Website on behalf of a company, partnership, association, government or other organisation, you represent and warrant that you are authorised to do so and that you are authorised to bind your organisation to these terms of use."
-      "lawful_use": "You agree to use the GoodCity Website only for lawful purposes."
-      "security": "You agree not to take any action that might compromise the security of the GoodCity Website, render the GoodCity Website inaccessible to others or otherwise cause damage to the GoodCity Website or its content."
-      "access_attempt": "You agree not to attempt to access any parts of the GoodCity Website that are not intended for you;"
-      "website_rights": "You agree not to use the GoodCity Website in any manner that might interfere with the rights of third parties."
+      "title": "閣下的同意書"
+      "terms_of_use": "閣下同意跟從下列使用條款使用好人好市網站。"
+      "use_authority": "閣下同意僅以個人身份使用好人好市網站。假如閣下代表公司、合作夥伴、聯會、政府或其他機構使用好人好市網頁，閣下必須代表並保證已經得到貴機構之授權，並代表其遵從下列使用條款。"
+      "lawful_use": "閣下同意僅為合法目的使用好人好市網站。"
+      "security": "閣下同意不會採取任何可能損害好人好市網站保安之手段，導致他人無法讀取好人好市網站及其內容，並/或對之造成損害。"
+      "access_attempt": "閣下同意不會企圖讀取好人好市網站上，任何不對閣下開放的內容；"
+      "website_rights": "閣下同意不會以任何可能損害第三方權益的方式使用好人好市網站。"
     "changes":
-      "title": "Changes to these Terms of Use"
-      "info": "We may from time to time issue, revise or add specific instructions, policies and terms applicable to your use of the GoodCity Website and these instructions, policies and terms form part of these terms of use. Please read the GoodCity Website terms of use carefully and come back to review them from time to time. By continuing to use the GoodCity Website after any amendment to the terms of use you are agreeing to these terms of use as amended."
-      "contact_us": "If you have any questions about the GoodCity Website terms of use, please contact us at"
+      "title": "使用條款的更改"
+      "info": "我們或會間中發布、修正或增加有關閣下使用好人好市網站的特定指引、政策及條款，而這些指引、政策及條款為使用條款的一部份。請細閱好人好市網站上的使用條款，並間中重閱。使用條款經過修正後繼續使用好人好市網站，即表示閣下同意經改正後的條款。"
+      "contact_us": "如閣下對好人好市網站上的使用條款有任何疑問，請與我們聯絡"
     "account":
-      "title": "Account Details"
-      "info": "Your GoodCity account name remains our property and we can reclaim and reuse the account name once your account is terminated or deactivated for whatever reason by either you or us. You are responsible for safeguarding any passwords used to access your account and the GoodCity Website. You will promptly notify us at privacy@crossroads.org.hk if you believe that your password or account has been compromised."
+      "title": "帳戶資料"
+      "info": "閣下的帳戶名稱為我們的資產。不論任何原因或肇事者，一旦閣下的帳戶終止使用或遭暫停，我們能夠收回該帳戶名稱，並重新使用。閣下有責任保障登入帳戶及好人好市網站的密碼。假如閣下認為密碼或帳戶遭入侵，必須儘快電郵至privacy@crossroads.org.hk通知我們。"
     "privacy":
-      "title": "Privacy Policy"
-      "info": "Your privacy is very important to us. We have prepared a Personal Information Collection Statement (PICS) which outlines our policies and practices with respect to the personal data you may be asked to provide when registering to receive items through GoodCity.HK."
-      "pics": "The PICS is available to view"
-      "privacy_link": "The full Crossroads privacy policy is available to view"
-      "use": "These policies are incorporated into and form part of these GoodCity Website terms of use."
-      "here": "here"
+      "title": "私隱政策"
+      "info": "我們十分重視閣下的私隱。當閣下登記，經由好人好市接收物資時，我們或回查詢閣下的個人資料，我們已經準備收集個人資料聲名（PICS），詳列我們有關個人資料的私隱政策及執行細節。"
+      "pics": "閣下可以隨時翻閱PICS"
+      "privacy_link": "十字路會完整的私隱政策亦可供查詢"
+      "use": "這些政策為好人好市網站使用條款的一部分。"
+      "here": "這裏"
     "intellectual":
-      "title": "Intellectual Property"
-      "info": "All intellectual property rights in the GoodCity Website (including any future updates, upgrades and new versions) and any connected or related applications, systems and services shall continue to belong to us and our licensors. You have no right to use our intellectual property rights except to the extent expressly permitted by us. In particular, you have no right to use our trade marks or product names, logos, domain names or other distinctive brand features without our prior written consent."
-      "grant": "We grant you a personal, non-exclusive, non-sublicensable, non-transferrable, royalty-free and revocable licence to use the GoodCity Website only in accordance with these terms of use."
-      "unauthorised_access": "You may not copy, modify, reverse engineer or extract any information, source codes or underlying HTML from the GoodCity Website or any connected or related applications, systems or services, except to the extent that we may not prohibit you from doing so under applicable law or you have our prior written consent to do so. You will not use the GoodCity Website in conjunction with any application or other technology that attempts to do any of the foregoing or which otherwise causes the GoodCity Website to operate in an unauthorised manner."
+      "title": "知識產權"
+      "info": "好人好市網站包含的一切知識產權（包括將來一切的資料更新、系統更新及新版本）及任何相聯或相關的程式、系統及服務將一直屬於我們及我們的發牌人。除了在得到我們明確授權外，閣下並無權利使用我們的知識產權。特別指出，如事前未得到我們的書面同意，閣下並無權使用我們的商標或產品名稱，標誌，網域名稱或其他明顯的品牌細節。"
+      "grant": "僅在合乎使用條款的情況下，我們給予閣下使用好人好市網站的使用權利，該使用權利供閣下個人，但不限於閣下一人使用，授權不得二次授權，不得轉讓，並不需專利費用，我們保留撤回的權利。"
+      "unauthorised_access": "除了在現行法例導致我們或不會禁止閣下，或閣下事前得到書面同意的情況下，閣下不能從好人好市網站或任何相聯或相關程式、系統或服務中複製、更改、進行逆向工程或提取任何資料，程式源代碼或基礎HTML。在使用好人好市網站時，閣下不能同時使用任何可能導致或嘗試引致上列或其他令好人好市網站以未經授權的方式運行的程式或其他技術。"

     "content":
-      "title": "Your Content"
-      "info": "When you submit, upload, transmit or display any photographs, data, information, media or other content in connection with your use of the GoodCity Website (“Your Content”), you understand and agree that:"
-      "responsible": "You will continue to own and be responsible for Your Content."
-      "no_sell": "We will not sell Your Content to any third party."
-      "privacy_policy": "Our use of Your Content will be subject to our Privacy Policy."
-      "right_to_use": "You are giving us and our affiliate companies the right to use Your Content (with no fees or charges payable by us to you) to provide our services and for the purposes of promoting, developing and trying to improve our services, including new services that we may provide in the future."
-      "share": "We may share Your Content with third parties that we work with to help provide, promote, develop and improve our services, but these third parties (other than our affiliate companies) will not make any separate use of Your Content for their own purposes (i.e. for any purposes that are not related to our services) and"
-      "suggestions": "Any comments or suggestions you may provide regarding the GoodCity Website are entirely voluntary and we will be free to use such comments and suggestions in our discretion without any obligation to you."
+      "title": "閣下的內容"
+      "info": "當閣下使用好人好市網站時提交、上載、傳輸或展示任何照片、數據、資料、媒介或其他內容（閣下的內容），即表示閣下明白並同意："
+      "responsible": "閣下將繼續擁有閣下的內容，並對其負責。""
+      "no_sell": "我們不會將閣下的內容銷售給第三方。"
+      "privacy_policy": "我們將跟隨我們的私隱政策使用閣下的內容。"
+      "right_to_use": "閣下給予我們及與我們相關的公司使用閣下的內容的權利（我們不須向閣下支付任何費用），用以提供我們的服務，並進行宣傳、發展和改善我們的服務的目的，包括我們於將來新增的服務。"
+      "share": "我們或會向與我們合作的第三方分享閣下的內容，以提供、宣傳、發展及改善我們的服務，但該第三方（非與我們相關的公司）將不會以私人目的單獨使用閣下的內容（意指任何與我們的服務無關的目的），並且"
+      "suggestions": "閣下提供任何有關好人好事網站的意見或建議均屬自願性質，我們將能夠免費使用，而不須向閣下進行任何義務。"
     "warranty":
-      "title": "Warranty and Disclaimer"
-      "info": "We warrant that we will provide the GoodCity Website and the services relating to it using reasonable care and skill. However, apart from this warranty, we make no representation or warranty and give no undertaking in relation to our services, our software or any content submitted, transmitted or displayed by our services, including any representation, warranty or undertaking that the GoodCity Website or any software, content or information provided to you by it will be secure or error-free or that the GoodCity Website will be compatible with your device or any software operating on it or fit for a particular purpose. The GoodCity Website is provided on an “as is” and “as available” basis."
+      "title": "擔保及免責聲明"
+      "info": "我們保證為好人好市網站及其相關服務提供合理保養及技術。然而，除此擔保外，對於我們的服務、軟件或任何已提交、傳輸或展示的內容，我們並不表示或擔保以及承諾會完全安全或不出錯、或好人好市會和閣下使用的裝置或任何安裝在裝置上的軟件兼容，這包括我們服務中，任何在好人好市網站或其他任何軟件、內容或資料中的顯示、擔保或承諾。好人好市網站僅提供現有、可用的版本。"
     "liability":
-      "title": "Liability"
-      "info": "The total aggregate liability of us and our affiliated companies in connection with the GoodCity Website, arising out of any circumstances, shall be limited to USD100."
-      "exclude": "Nothing in these terms of use shall limit or exclude any liability for any loss arising from death or personal injury caused by negligence, for fraudulent misrepresentation or for any other liability, to the extent that any such liability cannot be limited or excluded by law."
-      "indemnify_us": "You agree to indemnify us and hold us harmless against any liability or loss incurred by us arising in connection with your breach of these terms of use."
+      "title": "責任"
+      "info": "無論因為任何情況，好人好市網站及與其相關的公司的整體責任僅限至總計100美元。"
+      "exclude": "使用條款中沒有任何情況可免除任何由疏忽導致的死亡或人身傷害所造成的一切損失，欺詐誤導或任何其他責任均不能受限於或免除法律責任。"
+      "indemnify_us": "假如閣下違反任何使用條款，即閣下同意對我們作出賠償，並不能追究我們因此而造成的任何責任或損失。"

     "policy_to_children":
-      "title": "Our Policy Towards Children"
-      "info": "The GoodCity Website is not intended for use by children. Unfortunately, we cannot always determine the age of users to our site. Please do not use the GoodCity Website if you are under 18 years of age."
+      "title": "對兒童政策"
+      "info": "好人好市網站的使用對象並不包括兒童。然而，我們並無法完全斷定使用者的年齡。假如閣下的年齡為18歲或以下，請勿使用好人好市網站。"

     "general":
-      "title": "General"
-      "info": "These GoodCity Website terms of use are the entire agreement between you and us in relation to the GoodCity Website. You agree that you will have no claim against us for any statement which is not explicitly set out in these terms of use. No delay in enforcing any provision of these terms of use shall be construed to be a waiver of any rights under that provision. Any rights and obligations under these terms of use which by their nature should survive, including but not limited to any obligations in relation to the liability of the respective parties, will remain in effect after termination or expiration of these terms of use."
-      "hk_law": "These terms of use and any dispute or claim arising out of or in connection with them shall be governed by the laws of the Hong Kong Special Administrative Region. All legal proceedings arising out or in connection with these terms of use shall be non-exclusively brought in the courts of the Hong Kong Special Administrative Region."
-      "language": "Where there is any inconsistency between the English and any other language versions of these terms of use, the English language version will prevail."
+      "title": "一般"
+      "info": "這些好人好市網站使用條款完全是閣下和我們之間有關好人好市網站的合約。閣下同意不會就任何並未在這些使用條款中明確列明的條例向我們追究。如使用條款的實施有所延誤，不能將之視為放棄規定下的任何權利。根據其原意，在使用條款遭終止或過期前，使用條款中的任何權利及義務均有效並應得以保存，包括但不限於任何對相關方面責任的義務。"
+      "hk_law": "使用條款及與其相關或衍生的任何紛爭或索償均受香港特別行政區法律的監管。使用條款衍生出的或相關的任何法律程序皆由，並不限於香港特別行政區法院處理。"
+      "language": "如使用條款的英文版本與其他語言版本有出入，皆以英文版本為凖。"

     "contact":
-      "title": "Contact Us"
-      "info": "If you have any questions or concerns regarding the GoodCity Website or these terms of use please contact us at"
+      "title": "聯絡我們"
+      "info": "假如閣下對好人好市網站或這些使用條款有任何問題或疑問，請聯絡我們"

 `export default I18nTranslationsZhTw`


allow filename overrides

allow filename overrides

--- lib/adapter_base.coffee
+++ lib/adapter_base.coffee
@@ -97,7 +97,7 @@
     opts = _.clone(opts, true)
     (new File(file))
       .read(encoding: 'utf8')
-      .then _.partialRight(@render, _.extend(opts, {filename: file})).bind(@)
+      .then _.partialRight(@render, _.extend({ filename: file }, opts)).bind(@)

   ###*
    * Compile a string to a function
@@ -118,7 +118,7 @@
   compileFile: (file, opts = {}) ->
     (new File(file))
       .read(encoding: 'utf8')
-      .then _.partialRight(@compile, _.extend(opts, {filename: file})).bind(@)
+      .then _.partialRight(@compile, _.extend({ filename: file }, opts)).bind(@)

   ###*
    * Compile a string to a client-side-ready function


Esconde aviso ao selecionar qualquer dimensao

--- src/coffee/portal-sku-selector.coffee
+++ src/coffee/portal-sku-selector.coffee
@@ -131,6 +131,8 @@
 		selectedSkuObj = selectedSku(skus, selectedDimensionsMap)
 		undefinedDimensions = findUndefinedDimensions(selectedDimensionsMap)

+		options.selectors.warning($template)
+
 		# Trigger event for interested scripts
 		if selectedSkuObj and undefinedDimensions.length is 0
 			$el.trigger 'skuSelected', [selectedSkuObj, dimensionName]


Retira "prefixo" de URL do buy button

Retira "prefixo" de URL do buy button

--- src/coffee/portal-sku-selector.coffee
+++ src/coffee/portal-sku-selector.coffee
@@ -393,7 +393,7 @@
 	$.get '/api/catalog_system/pub/products/variations/' + productId

 $.skuSelector.getAddUrlForSku = (sku, seller = 1, qty = 1, redirect = true) ->
-	window.location.protocol + '//' + window.location.host + "/checkout/cart/add?qty=#{qty}&seller=#{seller}&sku=#{sku}&redirect=#{redirect}"
+	"/checkout/cart/add?qty=#{qty}&seller=#{seller}&sku=#{sku}&redirect=#{redirect}"

 #


Set min width to 225

Set min width to 225

--- src/components/performance/index.cjsx
+++ src/components/performance/index.cjsx
@@ -39,9 +39,9 @@
     tableWidth: 0
     tableHeight: 0
     debounce: _.debounce(@sizeTable, 100)
-    colDefaultWidth: 300
-    colSetWidth: 300
-    colResizeWidth: 300
+    colDefaultWidth: 225
+    colSetWidth: 225
+    colResizeWidth: 225
     colResizeKey: 0

@@ -130,7 +130,7 @@
         label={heading.title}
         headerRenderer={-> customHeader}
         cellRenderer={-> @cellData}
-        width={200}
+        width={@state.colSetWidth}
         flexGrow={1}
         fixed={fixed}
         isResizable=false


fix bug with base64 data

fix bug with base64 data

--- app/templates/gulp/plugins/resource.coffee
+++ app/templates/gulp/plugins/resource.coffee
@@ -45,6 +45,7 @@

 analyse = (decl, vendor_folder, root, prefix)->
   urldata(decl.value).forEach (_item)->
+    return if _item.indexOf(";base64,iVBORw0") > -1
     item = _item.split("?")[0].split("#")[0]

     realpath = libpath.resolve root, item


Add counters to undesired keys

Add counters to undesired keys

--- src/controllers/BaseController.coffee
+++ src/controllers/BaseController.coffee
@@ -68,7 +68,7 @@
       @subscribe source, @process

   extractKeys: (datum) ->
-    undesiredKeys = ['id', 'cid', 'image', 'zooniverse_id', 'objID']
+    undesiredKeys = ['id', 'cid', 'image', 'zooniverse_id', 'objID', 'counters']
     for key, value of datum
       dataKey = key if typeof(value) != 'function'
       @keys.push dataKey unless dataKey in undesiredKeys


Fixing suggestions in data explorer

Fixing suggestions in data explorer

OTS by @vexocide

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1519,7 +1519,7 @@
         comma: /^(\s)*,(\s)*/
         semicolon: /^(\s)*;(\s)*/
         number: /^[0-9]+\.?[0-9]*/
-        inline_comment: /^(\s)*\/\/.*\n|$/
+        inline_comment: /^(\s)*\/\/.*(\n|$)/
         multiple_line_comment: /^(\s)*\/\*[^(\*\/)]*\*\//
         get_first_non_white_char: /\s*(\S+)/
         last_char_is_white: /.*(\s+)$/