rename $model to $data to reflect haojing view

--- test/Transpiler.php5.coffee
+++ test/Transpiler.php5.coffee
@@ -2,48 +2,48 @@

 exports.PHP5Transpiler =

-	block:
-
+	block:
+
 		'comment single line':
 			input: [
 				['comment', [1, 1], ['comment line']]
 			]
-
+
 			expect: [['echo \'\';']]

 		'comment multiple lines':
 			input: [
 				['comment', [1, 1], ['comment line 1', 'comment line 2']]
 			]
-
+
 			expect: [[
 				'echo \'\';'
 			]]
-
+
 		'doctype':
 			input: [[
 				'comment',
 				[1,1],
 				['html5']
 			]]
-
+
 			expect: [['echo \'\';']]

 		'suppress single line':
 			input: [
 				['suppress', [1, 1], ['suppress line']]
 			]
-
+
 			expect: [['// suppress line']]
-
+
 		'suppress multiple lines':
 			input: [
 				['suppress', [1, 1], ['suppress line 1', 'suppress line 2']]
 			]
-
+
 			expect: [[
 				'// suppress line 1',
 				'// suppress line 2',
@@ -53,11 +53,11 @@
 			input: [
 				['inject', [1, 1], 'inject line', null, []]
 			]
-
+
 			expect: [[
 				'inject line;'
 			]]
-
+
 		'inject line with block':
 			input: [
 				['inject', [1, 1], 'inject line', null, [
@@ -65,7 +65,7 @@
 					['comment', [3, 2], ['line 2']]
 				]]
 			]
-
+
 			expect: [[
 				'inject line',
 				'{',
@@ -80,9 +80,9 @@
 			input: [[
 				'binding', [1, 1], null, ['Symbol', 'x'], []
 			]]
-
-			expect: [['echo htmlspecialchars($model->x);']]
-
+
+			expect: [['echo htmlspecialchars($data->x);']]
+
 		'binding of a list':
 			input: [
 				['binding', [1, 1], null,
@@ -90,7 +90,7 @@
 					[]
 				]
 			]
-
+
 		'binding of a tuple':
 			input: [
 				['binding', [ 1, 1 ], null,
@@ -98,15 +98,15 @@
 					[]
 				]
 			]
-
+
 		'binding of a named tuple':
 			input: [
 				[
-					'binding',
-					[ 1, 1 ],
+					'binding',
+					[ 1, 1 ],
 					null,
 					[
-						'Tuple',
+						'Tuple',
 						[
 							[
 								'Mapping',
@@ -128,28 +128,28 @@
 			input: [
 				['text', [1, 1], undefined, ['Hello world!']]
 			]
-
+
 			expect: [['echo \'Hello world!\', "\\n";']]
-
+
 		'single quot text no escape/interpolation':
 			input: [
 				['text', [1, 1], undefined, ['Hello {user}!\\n']]
 			]
-
+
 			expect: [['echo \'Hello {user}!\\n\', "\\n";']]
-
+
 		'single quot multiple lines text ':
 			input: [
 				['text', [1, 1], undefined, ['Hello world!', 'foo bar baz', 'rawr rawr', 'super cool']]
 			]
-
+
 			expect:[[
 				'echo \'Hello world!\', "\\n";'
 				'echo \'foo bar baz\', "\\n";'
 				'echo \'rawr rawr\', "\\n";'
 				'echo \'super cool\', "\\n";'
 			]]
-
+
 		'single quot text with single quot':
 			input: [
 				['text', [1, 1], undefined, ["I'm ok!"]]
@@ -163,9 +163,9 @@
 					[['String', 'Hello world!', 'Hello world!']]
 				]]
 			]
-
+
 			expect: [['echo htmlspecialchars(\'Hello world!\');']]
-
+
 		'double quot text escape/interpolation':
 			input: [
 				['text', [1, 1], undefined, [
@@ -176,9 +176,9 @@
 					]
 				]]
 			]
-
-			expect: [['echo htmlspecialchars(\'Hello \'), htmlspecialchars($model->user), htmlspecialchars(\'!\n\');']]
-
+
+			expect: [['echo htmlspecialchars(\'Hello \'), htmlspecialchars($data->user), htmlspecialchars(\'!\n\');']]
+
 		'double quot multiple lines text with tag':
 			input: [
 				['text', [1, 1], ['Symbol', 'r'],
@@ -202,15 +202,15 @@
 					]
 				]
 			]
-
-			expect: [[
-				'if (($model->x > $model->y)) {',
+
+			expect: [[
+				'if (($data->x > $data->y)) {',
 				[[
-					'echo htmlspecialchars($model->x), htmlspecialchars(\' > \'), htmlspecialchars($model->y);'
+					'echo htmlspecialchars($data->x), htmlspecialchars(\' > \'), htmlspecialchars($data->y);'
 				]],
 				'}'
 			]]
-
+
 		'iterate values':
 			input: [
 				['instruction', [1, 1], 'for',
@@ -220,9 +220,9 @@
 					]]]
 				]
 			]
-
-			expect: [[
-				'foreach ($model->x as $v) {',
+
+			expect: [[
+				'foreach ($data->x as $v) {',
 				[[
 					'echo htmlspecialchars($v);'
 				]],
@@ -246,8 +246,8 @@
 				],
 				[
 					[
-						'text',
-						[2, 2],
+						'text',
+						[2, 2],
 						undefined,
 						[
 							[
@@ -262,7 +262,7 @@
 			]]

 			expect: [[
-				'foreach ($model->x as $key => $value) {',
+				'foreach ($data->x as $key => $value) {',
 				[[
 					'echo htmlspecialchars($key), htmlspecialchars(\' = \'), htmlspecialchars($value), htmlspecialchars(\'"\');'
 				]],
@@ -273,18 +273,18 @@
 			input: [
 				['element', [1, 1], ['div', ['test1'], undefined], undefined, []]
 			]
-
+
 			expect: [[
 				'echo \'<div class="test1"\';',
 				'',
 				'echo \'
\';'
 			]]
-
+
 		'element with binding':
 			input: [
 				['element', [1, 1], ['div', ['test1'], undefined], ['Symbol', 'x'], []]
 			]
-
+
 		'nested elements with binding':
 			input: [
 				['element', [1, 1], ['div', ['test1'], undefined], undefined, [
@@ -317,35 +317,35 @@
 					[[ 'echo \' charset="\';', 'echo htmlspecialchars(\'utf-8\');', 'echo \'"\';' ] ],
 				[]]
 			]
-
+
 		'element double quot with x':
 			input: [ [ 'element', [ 1, 1 ], [ 'meta', '', undefined ], undefined,
 					[ [ 'attribute', [ 1, 16 ], 'charset', '=',
-						[ 'Quasi', undefined, [ [ 'String', 'utf-8', 'utf-8' ], [ 'Symbol', 'x' ] ] ]
+						[ 'Quasi', undefined, [ [ 'String', 'utf-8', 'utf-8' ], [ 'Symbol', 'x' ] ] ]
 					] ]
 				]]
-
+
 			expect: [
 				[ 'echo \'<meta\';',
-					[[ 'echo \' charset="\';', 'echo htmlspecialchars(\'utf-8\'), htmlspecialchars($model->x);', 'echo \'"\';' ] ],
+					[[ 'echo \' charset="\';', 'echo htmlspecialchars(\'utf-8\'), htmlspecialchars($data->x);', 'echo \'"\';' ] ],
 				[]]
 			]

 	document:
-
+
 		'extend':
 			input: ['document', ['./test/test', 1, 1], '', undefined,
 				[['text', [1, 1], undefined, ['Big sheep testcase']]]
 			]
-
+
 			expect: '<?php\necho \'<!doctype html>\', "\\n";\n echo \'Big sheep testcase\', "\\n";\n?>'
-
+
 		'extend with before hook':
 			input: [ 'document', [ './test/test', 1, 1 ], '', undefined,
 				[
 					[ 'fragment', [ 2, 3 ], 'headBlock', 'before',
 						[[ 'element', [ 3, 9 ], [ 'style', '', undefined ], undefined,
-							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
+							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
 						]]
 					],
 					[ 'fragment', [ 1, 1 ], 'headBlock', undefined,
@@ -353,28 +353,27 @@
 					]
 				]
 			]
-
+
 			expect: '<?php\necho \'<!doctype html>\', "\\n";\n //  #headBlock\n   echo \'\';\n //  #headBlock\n   echo \'Big sheep testcase\', "\\n";\n?>'

-		'extend with mutiple hooks':
+		'extend with mutiple hooks':
 			input: [ 'document', [ './test/test', 1, 1 ], '', undefined,
 				[
 					[ 'fragment', [ 2, 3 ], 'headBlock', 'before',
 						[[ 'element', [ 3, 9 ], [ 'style', '', undefined ], undefined,
-							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
+							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
 						]]
 					],
 					[ 'fragment', [ 2, 3 ], 'headBlock', undefined,
 						[[ 'element', [ 3, 9 ], [ 'style', '', undefined ], undefined,
-							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
+							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
 						]]
 					],
 					[ 'fragment', [ 2, 3 ], 'headBlock', 'after',
 						[[ 'element', [ 3, 9 ], [ 'style', '', undefined ], undefined,
-							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
+							[[ 'attribute', [ 3, 10 ], 'src', '=', [ 'String', 'test.css' ]]]
 						]]
 					],
 				]
 			]
 			expect: '<?php\necho \'<!doctype html>\', "\\n";\n //  #headBlock\n   echo \'\';\n //  #headBlock\n   echo \'\';\n //  #headBlock\n   echo \'\';\n?>'
-


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -26,6 +26,8 @@
   indexSum = 0

   engine.each '.rad=Awesome', (obj, index) ->
+    ind
+
     if index is 0
       equals obj, obj1
     else if index is 1


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -23,7 +23,7 @@

   engine.each (obj, index) ->
     counter += 1
-    indexSum
+    indexSum +=

 test "#find", ->
   engine = Engine()


redraw recruit paths if year is changed

redraw recruit paths if year is changed

--- source/javascripts/county-map.coffee
+++ source/javascripts/county-map.coffee
@@ -1,4 +1,6 @@
 render = (event, env) ->
+  selectedSchool = null
+
   map = d3.select('#county-map').append('svg')
     .attr('width', env.width)
     .attr('height', env.height)
@@ -54,6 +56,9 @@
   #
   # Returns nothing
   drawRecruitPathsToSchool = (school) ->
+    school = school || selectedSchool
+    return if !school
+
     year = $('.js-year').val()
     schoolRecruits  = env.recruits.filter (r) ->
       fromSchool = r.institution in [school.team, school.alt]
@@ -99,6 +104,7 @@
       .attr('r', 3)

     recruitNodes.exit().remove()
+    selectedSchool = school

   # Set the fill domain based on the total number of recruits
   env.fill.domain [0.2, d3.max(env.counties.features, (d) -> d.properties.total)]
@@ -128,8 +134,7 @@
     .on('click', drawRecruitPathsToSchool)

   # Shade the county by the selected year
-  drawCountyAtYear = ->
-    year = $(this).val()
+  drawCountyAtYear = (year) ->
     year = if year then "total_#{year}" else 'total'
     numCounties = env.counties.features.length

@@ -143,6 +148,9 @@
         stars = d.properties[year] || 0
         if stars > 0 then env.fill(stars || 0) else '#333')

-  $('.js-year').on 'change', drawCountyAtYear
+  $('.js-year').on 'change', ->
+    year = $(this).val()
+    drawCountyAtYear year
+    drawRecruitPathsToSchool()

 $(document).on 'data.loaded', render


Updated phantom.coffee to match with current compiled version of phantom.js (close HTTP server on error)

--- phantom.coffee
+++ phantom.coffee
@@ -67,6 +67,7 @@
       ps.stderr.on 'data', options.onStderr || (data) -> module.exports.stderrHandler(data.toString('utf8'))

       ps.on 'error', (err) ->
+        httpServer.close()
         if err?.code is 'ENOENT'
           console.error "phantomjs-node: You don't have 'phantomjs' installed"
         else


Minor fixes

Minor fixes

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -310,8 +310,8 @@
     # in with the located and instantiated services. Please refer
     # to the implementation on how and what is being done exactly.
     setupScaffolding: ->
-        tag = nconf.get "NODE_ENV" or null
-        missing = "No NODE_ENV variable found"
+        missing = "no NODE_ENV variable found"
+        tag = nconf.get "NODE_ENV" or undefined
         assert not _.isEmpty(tag), missing
         @scope = scoping.Scope.lookupOrFail tag
         assert @scope.incorporate this


Agent pie: working on label collisions

Agent pie: working on label collisions

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -208,6 +208,9 @@
             d.y
         )([])

+        # Create an instance name variable that will be in scope below
+        myInstanceDataId = @instanceDataId
+
         #create an empty quadtree to hold label positions
         maxLabelWidth = 0
         maxLabelHeight = 0
@@ -271,7 +274,7 @@
                 return

             if conflicts.length
-                console.log @instanceDataId , ' ', d, ' conflicts with ', conflicts
+                console.log myInstanceDataId , ' ', d, ' conflicts with ', conflicts
                 rightEdge = d3.max(conflicts, (d2) ->
                     #`var maxLabelHeight`
                     #`var maxLabelWidth`
@@ -281,7 +284,7 @@
                 d.x = d.l + bbox.width / 2 + 5
                 d.r = d.l + bbox.width + 10
             else
-                console.log 'no conflicts for ', d, ' ', @instanceDataId
+                console.log 'no conflicts for ', d, ' ', myInstanceDataId

             ### add this label to the quadtree, so it will show up as a conflict
                for future labels.


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -99,7 +99,7 @@
         x = c[0]
         y = c[1]
         mx = 1.2
-        my = 1.5
+        my = 1.3
         h = Math.sqrt(x*x + y*y)
         r = labelRadius
         if x <= 0


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -62,7 +62,7 @@
     else
       arcs.append("svg:text")
         .attr('fill', "#fff")
-        .attr("text-anchor", "middle").attr("alignment-baseline", "middle").text(sum).attr('font-size', radiusi + 'px')
+        .attr("text-anchor", "middle").attr("alignment-baseline", "middle").attr("text-shadow", "2px 2px #222").text(sum).attr('font-size', radiusi + 'px')
       arcs.append("svg:text").attr("transform", (d, i) ->
         procent_val = Math.round(data[i].value/sum * 100)
         #d.innerRadius = (radiuso * (100-procent_val)/100) - radiuso/2 #45  #45=max text size/2


keyboard support for ⌹

keyboard support for ⌹

--- web-src/index.coffee
+++ web-src/index.coffee
@@ -167,7 +167,7 @@
       'alt-shift': [
         '⍣ {empty} {empty} {empty} {empty} {empty} {empty} {empty} ⍱ ⍲ ≡ {empty}'
         '{empty} ⌽ ⍷ {empty} ⍉ {empty} {empty} ⌷ ⍬ ⍟ {empty} {empty}'
-        '⊖ ⍨ {empty} {empty} ⍒ ⍋ {empty} {empty} ⍞ {enter}'
+        '⊖ ⍨ ⌹ {empty} ⍒ ⍋ {empty} {empty} ⍞ {enter}'
         '{shift} {empty} {empty} ⍝ {empty} ⍎ ⍕ {empty} « » {bksp}'
         '{alt} {space} {exec!!}'
       ]


Change `RELOAD` component to redirect when not in production (#5345)

--- app/router.cjsx
+++ app/router.cjsx
@@ -75,7 +75,10 @@
 # Usage: <Route path="path/to/location" component={RELOAD} />
 RELOAD = createReactClass
   componentDidMount: ->
-    window.location.reload()
+    if window.location.hostname is 'www.zooniverse.org'
+      window.location.reload()
+    else
+      window.location = @props.newUrl
   render: ->
     null

@@ -88,7 +91,7 @@
     <Route path="about" component={AboutPage} ignoreScrollBehavior>
       <IndexRoute component={AboutHome} />
       <Route path="team" component={TeamPage} />
-      <Route path="publications" component={PublicationsPage} />
+      <Route path="publications" component={() => <RELOAD newUrl='https://fe-content-pages.zooniverse.org/about/publications' />} />
       <Route path="acknowledgements" component={Acknowledgements} />
       <Route path="resources" component={Resources} />
       <Route path="contact" component={Contact} />


Fixed bug: There is nothing like console.notice.

--- app/assets/javascripts/app/lib/spine/spine.coffee
+++ app/assets/javascripts/app/lib/spine/spine.coffee
@@ -422,7 +422,7 @@
     if !@constructor.irecords[@id]
       model     = @constructor.className
       atts_json = JSON.stringify(atts)
-      console.notice "Can't refresh not exisint %s instance with ID %s and attributes %s. Possible race condition?", model, @id, atts_json
+      console.log "Can't refresh not exisint %s instance with ID %s and attributes %s. Possible race condition?", model, @id, atts_json
       # prevent exception
       return


fix errors scroll in atom form

fix errors scroll in atom form

--- app/cells/folio/console/form/errors/errors.coffee
+++ app/cells/folio/console/form/errors/errors.coffee
@@ -48,9 +48,7 @@
       else
         offset = $formGroup.offset().top

-      $scroll = $formGroup.closest('.f-c-with-aside__scroll')
-      if $scroll.length is 0
-        $scroll = $formGroup.closest('.f-c-layout-main-container')
+      $scroll = $formGroup.closest('.f-c-with-aside__scroll, .f-c-simple-form-with-atoms__scroll, .f-c-layout-main-container')

       $scroll.animate scrollTop: offset - $scroll.offset().top - 10, ->
         $formGroup.addClass('has-danger-blink')


fix initialization of playlists

fix initialization of playlists

--- core/plugmixer.coffee
+++ core/plugmixer.coffee
@@ -222,7 +222,11 @@
     initialized = false

     @initialize: ->
-      API.getPlaylists (_playlists) =>
+      playlists = API.getPlaylists().map (playlist) ->
+        return new Playlist(playlist)
+      activePlaylist = @getActivated()
+
+      API.getPlaylists (_playlists) => # Retrieve id as well.
         playlists = _playlists.map (playlist) ->
           return new Playlist(playlist)


[Java Play] Replaced URL in CS to reverse route.

--- java-play/todo/app/assets/javascripts/todo.coffee
+++ java-play/todo/app/assets/javascripts/todo.coffee
@@ -42,7 +42,7 @@
                             type: 'POST',
                             datatype : "json",
                             contentType: "application/json; charset=utf-8",
-                            url: "/todos/#{entityId}/update",
+                            url: @routes.Todo.update(entityId),
                             data: JSON.stringify(params),
                             success: (data) -> $(td).text child.val(),
                             error: (XMLHttpRequest, textStatus, errorThrown) -> $(td).text text


Formatting on multiple line substitution

--- annotran/static/scripts/annotator/plugin/substitution.coffee
+++ annotran/static/scripts/annotator/plugin/substitution.coffee
@@ -67,7 +67,7 @@
   createSubstitutionElement: (originalText, substituteText, ele) ->

     # we don't preserve full formatting but we can at least put some paragraph breaks in
-    originalText = originalText.replace(new RegExp('\n', 'g'), '
')
+    originalText = originalText.replace(new RegExp('\n', 'g'), '

')
     substituteText = substituteText.replace(new RegExp('\n', 'g'), '
')

     newEle = $('')


hide_cell for columns_hider hiddeb default

--- app/assets/javascripts/ab_admin/core/columns_hider.js.coffee
+++ app/assets/javascripts/ab_admin/core/columns_hider.js.coffee
@@ -2,12 +2,16 @@
   constructor: ->
     @store_key = 'cols'
     @data_el = $('#columns_hider_data')
+    @column_names = @columnNames()
+    @collection_name = $('html').attr('id').replace(/^controller_/, '')
     @data = @getData()
-    @collection_name = $('html').attr('id').replace(/^controller_/, '')
-    @column_names = @columnNames()
+    @initDefaults()
     @initButtons()
     @initHandlers()
     @refreshColumns()
+
+  initDefaults: ->
+    @data

   setData: ->
     res = {}
@@ -22,6 +26,10 @@
       data[k] = []
       _.each v.split('_'), (el) ->
         data[k].push to_i(el) if el
+    unless data[@collection_name]
+      data[@collection_name] = []
+      for col, i in @column_names
+        data[@collection_name].push(i) if col.disabled
     data

   fetchData: ->
@@ -36,15 +44,15 @@

   initButtons: ->
     @data_el.empty()
-    for name, i in @column_names
+    for col, i in @column_names
       css_class = if _.include(@data[@collection_name], i) then 'active' else ''
-      html = "<button class='btn btn-primary #{css_class}' data-val='#{i}'>#{name}</button>"
+      html = "<button class='btn btn-primary #{css_class}' data-val='#{i}'>#{col.name}</button>"
       @data_el.append(html)

   columnNames: ->
     _.map $('#list th'), (el) ->
       $el = $(el)
-      $.trim($el.text().replace(/[▼▲]/g, ''))
+      {disabled: $el.hasClass('hide_cell'), name: $.trim($el.text().replace(/[▼▲]/g, ''))}

   initHandlers: ->
     $('#submit_columns_hider').click =>


fix showAll in columns_hider.js

fix showAll in columns_hider.js

--- app/assets/javascripts/ab_admin/core/columns_hider.js.coffee
+++ app/assets/javascripts/ab_admin/core/columns_hider.js.coffee
@@ -67,7 +67,7 @@
     $("#list > thead > tr > th:nth-child(#{i}), #list > tbody > tr > td:nth-child(#{i})").hide()

   showAll: ->
-    $("#list > thead > tr > th, #list > thead > tr > td").show()
+    $("#list > thead > tr > th, #list > tbody > tr > td").show()

 $ ->
   window.columns_hider = new ColumnsHider()


Fixed chained dropdowns bug in cites_listings

--- app/assets/javascripts/admin/admin_in_place_editor.js.coffee
+++ app/assets/javascripts/admin/admin_in_place_editor.js.coffee
@@ -221,6 +221,10 @@
   init: () ->
     @initForm()
     $("[rel='tooltip']").tooltip()
+    $('#listing_change_event_id').change((e) ->
+      if $(this).val() == ""
+        $('#listing_change_hash_annotation_id').removeAttr("disabled")
+    )

   initForm: () ->
     super


Adding a bit of character

Adding a bit of character

--- app/assets/javascripts/backbone/views/channel_tabs.js.coffee
+++ app/assets/javascripts/backbone/views/channel_tabs.js.coffee
@@ -7,7 +7,7 @@
     @

   createChannel: (event)->
-    channelName = prompt("What's the channel name?", "New channel")
+    channelName = prompt("What's the channel name?", @randomChannelName())
     channelName = channelName.replace(/^\s+|\s+$/g, '')
     if channelName
       channel = new Kandan.Models.Channel({name: channelName})
@@ -22,3 +22,18 @@
     channelIndex = $(event.target).parents('li').prevAll().length
     Kandan.Helpers.Channels.deleteChannelByTabIndex(channelIndex) if channelIndex != 0
     return false
+
+  randomChannelName: () ->
+    names = [
+      "A Dark Place",
+      "Discotheque",
+      "Dungeon",
+      "Garden",
+      "Lobby",
+      "Office",
+      "Palace",
+      "Park",
+      "Studio",
+      "Temple",
+      "War Room",
+      "Zork"]


waterfall: smaller label font. Closes #898

With 11px size the labels were cut out.

--- app/assets/javascripts/lib/views/waterfall_chart_view.coffee
+++ app/assets/javascripts/lib/views/waterfall_chart_view.coffee
@@ -60,7 +60,7 @@
           tickOptions:
             angle: -90
             showGridline: false
-            fontSize: @defaults.font_size
+            fontSize: '10px'
         y2axis:
           rendererOptions:
             forceTickAt0: true # we always want a tick a 0


notifications are automatically read with each login/refresh bug is fixed

--- client/landing/app/MainApp/avatararea/avatarareapopup.coffee
+++ client/landing/app/MainApp/avatararea/avatarareapopup.coffee
@@ -4,7 +4,7 @@
     super

     @sidebar = @getDelegate()
-    @sidebar.on "NavigationPanelWillCollapse", => @hide()
+    @sidebar.on "NavigationPanelWillCollapse", => @hide() if @hasClass "active"

     @on 'ReceivedClickElsewhere', => @hide()


rename request function parameters

rename request function parameters

--- newebe/client/app/lib/request.coffee
+++ newebe/client/app/lib/request.coffee
@@ -17,20 +17,20 @@

 # Sends a get request with data as body
 # Expected callbacks: success and error
-exports.get = (url, callbacks) ->
-    exports.request "GET", url, null, callbacks
+exports.get = (url, callback) ->
+    exports.request "GET", url, null, callback

 # Sends a post request with data as body
 # Expected callbacks: success and error
-exports.post = (url, data, callbacks) ->
-    exports.request "POST", url, data, callbacks
+exports.post = (url, data, callback) ->
+    exports.request "POST", url, data, callback

 # Sends a put request with data as body
 # Expected callbacks: success and error
-exports.put = (url, data, callbacks) ->
-    exports.request "PUT", url, data, callbacks
+exports.put = (url, data, callback) ->
+    exports.request "PUT", url, data, callback

 # Sends a delete request with data as body
 # Expected callbacks: success and error
-exports.del = (url, callbacks) ->
-    exports.request "DELETE", url, null, callbacks
+exports.del = (url, callback) ->
+    exports.request "DELETE", url, null, callback


Convert channel_ids to actual ids

Convert channel_ids to actual ids


Scene: Fix unload_objects

Scene: Fix unload_objects

--- engine/scene.coffee
+++ engine/scene.coffee
@@ -330,19 +330,15 @@
                 for mat in ob.materials
                     for tex in mat.last_shader?.textures or []
                         used_textures.push tex
-        for ob in list
-            ob_data = ob.data
-            if ob_data?
-                ob_data.splice _,1 if (_ = ob_data.indexOf ob)!=-1
+        for ob in list when ob.type == 'MESH'
+            ob_data = ob.data?.remove ob
             if unload_textures
                 for mat in ob.materials
                     for tex in mat.last_shader?.textures or []
                         if tex not in used_textures
                             tex.unload()
             for lod_ob in ob.lod_objects or []
-                lod_ob_data = lod_ob.object.data
-                if lod_ob_data?
-                    lod_ob_data.splice _,1 if (_ = lod_ob_data.indexOf ob)!=-1
+                lod_ob.object.data?.remove lod_ob
                 # We're assuming lod objects have same materials
                 # NOTE: should we?
         return


Add bmp to img type file extensions

Add bmp to img type file extensions

--- app/user.coffee
+++ app/user.coffee
@@ -7,7 +7,7 @@
 defaults = _.partialRight _.assign, (value, other) ->
   if not _.isUndefined(value) then other else undefined
 types =
-  'img': ['.png', '.jpg', '.gif', '.jpeg']
+  'img': ['.png', '.jpg', '.gif', '.jpeg', '.bmp']
   'video': ['.mp4', '.webm', '.wmv']
 storage = require 'node-persist'
 storage.initSync()


smooth scroll to champion list

smooth scroll to champion list

--- ui/app/components/MasterTemplate.cjsx
+++ ui/app/components/MasterTemplate.cjsx
@@ -1,9 +1,15 @@
 React = require('react')
+$ = require('jquery')

 class MasterTemplate extends React.Component
   @defaultProps = {
     halfPage: false
   }
+
+  scrollToChampions: ->
+    $('html, body').animate({
+      scrollTop: $("#title").offset().top
+    }, 500);

   render: ->
     klass = if this.props.halfPage then 'halfpage' else 'topbar'
@@ -12,11 +18,11 @@

-
fortifica

+
fortifica

Item Build Path Generator for League of Legends

-              Champions
+              Champions
               Methodology
               About


Popping in outline for menu view tests

Popping in outline for menu view tests

--- ui/tests/views/components/menu.coffee
+++ ui/tests/views/components/menu.coffee
@@ -1 +1,35 @@
 require('../../../scripts/views/components/menu')
+
+describe('Menu View', ->
+    beforeEach(->
+
+    )
+
+    afterEach(->
+
+    )
+
+    it.skip('should open when the icon is clicked', ->
+
+    )
+
+    it.skip('should close when the overlay is clicked', ->
+
+    )
+
+    it.skip('should close when the icon is clicked again', ->
+
+    )
+
+    it.skip('should render a list of menu items, given a model', ->
+
+    )
+
+    it.skip('should show a title and icon for each menu item', ->
+
+    )
+
+    it.skip('should go to the related page when clicking a menu item', ->
+
+    )
+)


expand comment

expand comment

--- js/inner/crumb.coffee
+++ js/inner/crumb.coffee
@@ -44,7 +44,7 @@
   constructor: (@username, @project, @branch, options) ->
     super(options)

-    # FIXME: branch is a computed observable because of the BuildPage definition
+    # FIXME: branch is a computed observable because of the BuildPage definition, and the only computed observable in Crumb

   name: () =>
     if @branch()


fix scrolling to fragment on the knockout app

I'm not sure why we need to use setTimeout here, but when
we don't add it, we end up scrolling about 50 pixels too far.

--- js/outer/_page.coffee
+++ js/outer/_page.coffee
@@ -71,7 +71,11 @@
   scroll: (hash) =>
     if hash == '' or hash == '#' then hash = "body"
     if $(hash).offset()
-      $('html, body').animate({scrollTop: $(hash).offset().top}, 0)
+      # Not sure why, but this works. Maybe setTimeout gives the page time to render?
+      # Without the setTimeout, we end up scrolling past by 50px
+      setTimeout () ->
+        $main = $('.app-main')
+        $main.animate({scrollTop: ($(hash).offset().top + $main.scrollTop())}, 0)

   title: =>
     @_title


parse body

parse body

--- routes/todoist.coffee
+++ routes/todoist.coffee
@@ -33,10 +33,10 @@
     req.speaker "사용 방법:\n!할일 [추가|끝|삭제] [프로젝트 이름] [할일 이름]\n!할일 [프로젝트 이름]\n등록된 프로젝트: "

 get_projects = (req, callback)->
-  request "#{APIPATH}getProjects#{APITAIL}", (e, r) ->
+  request "#{APIPATH}getProjects#{APITAIL}", (e, r, b) ->
     if !e && r
       console.log r
-      callback req, JSON.parse r
+      callback req, JSON.parse b
     else
       req.speaker "에러가 발생하였습니다.. #{e}"


Code cleanup.

Code cleanup.

--- app/assets/javascripts/app/controllers/signup.js.coffee
+++ app/assets/javascripts/app/controllers/signup.js.coffee
@@ -1,5 +1,4 @@
 $ = jQuery.sub()
-User = App.User

 class Index extends App.Controller
   className: 'container signup'
@@ -21,12 +20,12 @@
   render: ->

     # set password as required
-    for item in User.configure_attributes
+    for item in App.User.configure_attributes
       if item.name is 'password'
         item.null = false

     @html App.view('signup')(
-      form: @formGen( model: User, required: 'signup' ),
+      form: @formGen( model: App.User, required: 'signup' ),
     )

   cancel: ->
@@ -38,7 +37,7 @@
     @params = @formParam(e.target)
     ###
     for num in [1..199]
-      user = new User
+      user = new App.User
       params.login = 'login_c' + num
       user.updateAttributes(params)
     return false
@@ -50,7 +49,7 @@

     @params.role_ids = [0]
     @log 'updateAttributes', @params
-    user = new User
+    user = new App.User
     user.load(@params)

     errors = user.validate()


Hide some course properties when empty

Hide some course properties when empty

--- app/assets/javascripts/components/overview/details.cjsx
+++ app/assets/javascripts/components/overview/details.cjsx
@@ -37,6 +37,11 @@
     online = <InlineUsers {...@props} users={@props.online} role={2} title='Online Volunteers' />
     campus = <InlineUsers {...@props} users={@props.campus} role={3} title='Campus Volunteers' />
     staff = <InlineUsers {...@props} users={@props.staff} role={4} title='Wiki Ed Staff' />
+    if @props.course.school
+      # FIXME: Convert lego to parameterized messages.
+      school =
{CourseUtils.i18n('school', @props.course.string_prefix)}: {@props.course.school}

+    if @props.course.term
+      term =
{CourseUtils.i18n('term', @props.course.string_prefix)}: {@props.course.term}

     if @props.course.passcode
       passcode = (
@@ -54,6 +59,58 @@
         </fieldset>
       )

+    if @props.course.expected_students
+      expected_students = (
+        <fieldset>
+          <TextInput
+            onChange={@updateDetails}
+            value={@props.course.expected_students}
+            value_key='expected_students'
+            editable={@props.editable}
+            type='number'
+            label={CourseUtils.i18n('expected_students', @props.course.string_prefix)}
+          />
+        </fieldset>
+      )
+
+    # TODO: course.timeline?
+    if @props.course.timeline_start || @props.course.timeline_end
+      timeline_start_props =
+        minDate: moment(@props.course.start)
+        maxDate: moment(@props.course.timeline_end).subtract(1, 'week')
+      timeline_end_props =
+        minDate: moment(@props.course.timeline_start).add(1, 'week')
+        maxDate: moment(@props.course.end)
+
+      timeline_start = (
+        <fieldset>
+          <TextInput
+            onChange={@updateDetails}
+            value={@props.course.timeline_start}
+            value_key='timeline_start'
+            editable={@props.editable}
+            type='date'
+            label={CourseUtils.i18n('assignment_start', @props.course.string_prefix)}
+            date_props={timeline_start_props}
+            required=true
+          />
+        </fieldset>
+      )
+      timeline_end = (
+        <fieldset>
+          <TextInput
+            onChange={@updateDetails}
+            value={@props.course.timeline_end}
+            value_key='timeline_end'
+            editable={@props.editable}
+            type='date'
+            label={CourseUtils.i18n('assignment_end', @props.course.string_prefix)}
+            date_props={timeline_end_props}
+            required=true
+          />
+        </fieldset>
+      )
+
     cohorts = if @props.cohorts.length > 0
       _.pluck(@props.cohorts, 'title').join(', ')
     else 'None'
@@ -61,13 +118,6 @@
     tags = if @props.tags.length > 0
       _.pluck(@props.tags, 'tag').join(', ').replace(/_/g, ' ')
     else 'None'
-
-    timeline_start_props =
-      minDate: moment(@props.course.start)
-      maxDate: moment(@props.course.timeline_end).subtract(1, 'week')
-    timeline_end_props =
-      minDate: moment(@props.course.timeline_start).add(1, 'week')
-      maxDate: moment(@props.course.end)

@@ -79,20 +129,11 @@
         {online}
         {campus}
         {staff}
-
{CourseUtils.i18n('school', @props.course.string_prefix)}: {@props.course.school}

-
{CourseUtils.i18n('term', @props.course.string_prefix)}: {@props.course.term}

+        {school}
+        {term}
         <form>
           {passcode}
-          <fieldset>
-            <TextInput
-              onChange={@updateDetails}
-              value={@props.course.expected_students}
-              value_key='expected_students'
-              editable={@props.editable}
-              type='number'
-              label={CourseUtils.i18n('expected_students', @props.course.string_prefix)}
-            />
-          </fieldset>
+          {expected_students}
           <fieldset>
             <TextInput
               onChange={@updateDetails}
@@ -117,30 +158,8 @@
               required=true
             />
           </fieldset>
-          <fieldset>
-            <TextInput
-              onChange={@updateDetails}
-              value={@props.course.timeline_start}
-              value_key='timeline_start'
-              editable={@props.editable}
-              type='date'
-              label={CourseUtils.i18n('assignment_start', @props.course.string_prefix)}
-              date_props={timeline_start_props}
-              required=true
-            />
-          </fieldset>
-          <fieldset>
-            <TextInput
-              onChange={@updateDetails}
-              value={@props.course.timeline_end}
-              value_key='timeline_end'
-              editable={@props.editable}
-              type='date'
-              label={CourseUtils.i18n('assignment_end', @props.course.string_prefix)}
-              date_props={timeline_end_props}
-              required=true
-            />
-          </fieldset>
+          {timeline_start}
+          {timeline_end}
         </form>

           Cohorts: {cohorts}


Fixed a bug in the angularjs service that finds feeds in a folder, which sometimes caused the wrong feeds to be returned.

The problem is that the $filter('filter') builtin angularjs method was being used incorrectly. The way it was used, it performed a subscring match between the id of the folder and the folder_id attribute of each feed; this meant that e.g. when searching for feeds in folder with id "4", feeds in folder with id "14" will be returned as well.

The solution is to use the builtin filter method correctly. Instead of passing an options object for the third argument, which performs a substring match, a function is passed which compares the folder.id and feed.folder_id attributes and only returns true if they are equal.

Note that the method compares ids, not objects themselves, and therefore works fine even after an AJAX reload of the feeds or folders lists, which discards all objects and inserts new ones in the lists.

--- app/assets/javascripts/ng-services/ng-FindSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-FindSvc.js.coffee
@@ -29,7 +29,8 @@
   #---------------------------------------------
   find_folder_feeds: (folder)->
     if folder != 'all'
-      return $filter('filter') $rootScope.feeds, {folder_id: folder.id}
+      return $filter('filter') $rootScope.feeds, (feed)->
+        return feed.folder_id == folder.id
     else
       return $rootScope.feeds
 ]


display warning message for pivot

display warning message for pivot

--- app/assets/javascripts/notebook/magic/pivotChart.coffee
+++ app/assets/javascripts/notebook/magic/pivotChart.coffee
@@ -70,13 +70,16 @@
       pivotOptions.onRefresh = refresh
       pivotOptions.rendererOptions = rendererOptions

+      p = $("
")
+      p.addClass("pivotChart").appendTo(container)
+
       plotThat = (data) =>
-        $(container).pivotUI(data, pivotOptions)
+        p.pivotUI(data, pivotOptions)

         toggleOptionsBtn = $("show/hide options")
         toggleOptionsBtn.click ->
-          $(container).find(".pvtUi").toggleClass("pivot-controls-hidden")
-        $(container).prepend(toggleOptionsBtn)
+          p.find(".pvtUi").toggleClass("pivot-controls-hidden")
+        p.prepend(toggleOptionsBtn)
         # collapse all pivotchart controls in report mode
         report_mode = $("body[data-presentation='report']").length > 0
         $(".pvtUi").addClass("pivot-controls-hidden") if report_mode


hotfix form validation

hotfix form validation

--- app/assets/javascripts/per_page/forum/topics/new.coffee
+++ app/assets/javascripts/per_page/forum/topics/new.coffee
@@ -1,8 +1,9 @@
 window.TopicsNew =
   init: ->
+    form = $('#new_frm_topic')
     text_editor = CKEDITOR.instances['frm_topic_posts_attributes_0_text']
     text_editor.on 'change', ->
       text_editor.updateElement()
-      $('form').formValidation 'revalidateField', 'frm_topic[posts_attributes][0][text]'
+      form.formValidation 'revalidateField', 'frm_topic[posts_attributes][0][text]'

 window.TopicsCreate = window.TopicsNew


rethink: better input checking

rethink: better input checking

--- salvus/rethink.coffee
+++ salvus/rethink.coffee
@@ -384,6 +384,7 @@
         opts = defaults opts,
             account_id : required
             cb         : required
+        if not @_validate_uuids(opts) then return
         @table('accounts').get(opts.account_id).delete().run(opts.cb)

     account_exists: (opts) =>
@@ -410,6 +411,7 @@
         opts = defaults opts,
             account_ids : required
             cb          : required # (err, mapping {account_id:{first_name:?, last_name:?}})
+        if not @_validate_uuids(opts) then return
         if opts.account_ids.length == 0 # easy special case -- don't waste time on a db query
             opts.cb(false, [])
             return
@@ -431,6 +433,7 @@
             use_cache    : true
             cache_time_s : 60*60        # one hour
             cb           : required     # cb(err, map from account_id to object (user name))
+        if not @_validate_uuids(opts) then return
         user_names = {}
         for account_id in opts.account_ids
             user_names[account_id] = false
@@ -581,6 +584,7 @@
                              'groups', 'passports',
                              'password_is_set'  # set in the answer to true or false, depending on whether a password is set at all.
                             ]
+        if not @_validate_uuids(opts) then return
         @_account(opts).pluck(opts.columns...).run(opts.cb)

     # check whether or not a user is banned
@@ -589,6 +593,7 @@
             email_address : undefined
             account_id    : undefined
             cb            : required    # cb(err, true if banned; false if not banned)
+        if not @_validate_uuids(opts) then return
         @_account(opts).pluck('banned').run (err, x) =>
             if err
                 opts.cb(err)
@@ -600,6 +605,7 @@
             account_id    : undefined
             email_address : undefined
             cb            : required
+        if not @_validate_uuids(opts) then return
         @_account(opts).update(banned:true).run(opts.cb)

@@ -648,6 +654,7 @@
         if opts.settings.email_address?
             email_address = opts.settings.email_address
             delete opts.settings.email_address
+        if not @_validate_uuids(opts) then return
         async.parallel([
             (cb) =>
                 # treat email separately, since email must be globally unique.
@@ -675,6 +682,7 @@
             value      : required
             ttl        : required
             cb         : required
+        if not @_validate_uuids(opts) then return
         @table('remember_me').insert(id:opts.hash, value:opts.value, expires:expire_time(opts.ttl), account_id:opts.account_id).run(opts.cb)

     # Invalidate all outstanding remember me cookies for the given account by
@@ -692,6 +700,7 @@
             password_hash          : required
             invalidate_remember_me : true
             cb                     : required
+        if not @_validate_uuids(opts) then return
         async.series([  # don't do in parallel -- don't kill remember_me if password failed!
             (cb) =>
                 @_account(opts).update(password_hash:opts.password_hash).run(cb)
@@ -710,6 +719,7 @@
             account_id    : required
             email_address : required
             cb            : required
+        if not @_validate_uuids(opts) then return
         @account_exists
             email_address : opts.email_address
             cb            : (err, exists) =>
@@ -729,7 +739,7 @@
             account_id : required
             filename   : required
             cb         : undefined
-
+        if not @_validate_uuids(opts) then return
         entry =
             project_id : opts.project_id
             account_id : opts.account_id
@@ -760,6 +770,7 @@
             title       : undefined
             description : undefined
             cb          : required    # cb(err, project_id)
+        if not @_validate_uuids(opts) then return
         project =
             title       : opts.title
             description : opts.description
@@ -778,6 +789,7 @@
             project_id  : required
             columns     : PROJECT_COLUMNS
             cb          : required
+        if not @_validate_uuids(opts) then return
         @db.table('projects').get(opts.project_id).pluck(opts.columns...).run(opts.cb)

     # TODO: api change -- now it's a map path--> description rather than a
@@ -786,6 +798,7 @@
         opts = defaults opts,
             project_id  : required
             cb          : required
+        if not @_validate_uuids(opts) then return
         @db.table('projects').get(opts.project_id).pluck('public_paths').run (err, x) =>
             opts.cb(err, x?.public_paths)   # map {path:description}

@@ -795,6 +808,7 @@
             path        : required
             description : required
             cb          : required
+        if not @_validate_uuids(opts) then return
         x = {}; x[opts.path] = opts.description
         @db.table('projects').get(opts.project_id).update(public_paths:x).run(opts.cb)

@@ -803,15 +817,21 @@
             project_id  : required
             path        : required
             cb          : required
+        if not @_validate_uuids(opts) then return
         x = {}; x[opts.path] = true
         db.table('projects').get(opts.project_id).replace(@r.row.without(public_paths:x)).run(opts.cb)

     _validate_uuids: (opts) =>
         for k, v of opts
             if k.slice(k.length-2) == 'id'
-                if not misc.is_valid_uuid_string(v)
+                if v? and not misc.is_valid_uuid_string(v)
                     opts.cb("invalid #{k} -- #{v}")
                     return false
+            if k.slice(k.length-3) == 'ids'
+                for w in v
+                    if not misc.is_valid_uuid_string(w)
+                        opts.cb("invalid uuid #{w} in #{k} -- #{misc.to_json(v)}")
+                        return false
         return true

     add_user_to_project: (opts) =>
@@ -833,14 +853,19 @@
             account_id : required
             group      : required  # see PROJECT_GROUPS above
             cb         : required  # cb(err)
-
+        if opts.group not in misc.PROJECT_GROUPS
+            opts.cb("unknown project group '#{opts.group}'"); return
+        if not @_validate_uuids(opts) then return
+        x = {}
+        x[opts.group] = @r.row(opts.group).default([]).setDifference([opts.account_id])
+        @table('projects').get(opts.project_id).update(x).run(opts.cb)

     get_project_users: (opts) =>
         opts = defaults opts,
             project_id : required
             groups     : PROJECT_GROUPS
             cb         : required    # cb(err, {group:[{account_id:?,first_name:?,last_name:?}], ...})
-
+        if not @_validate_uuids(opts) then return
         groups = undefined
         names  = undefined
         async.series([
@@ -886,6 +911,7 @@
             project_id : required
             size       : undefined
             cb         : undefined
+        if not @_validate_uuids(opts) then return

     recently_modified_projects: (opts) =>
         opts = defaults opts,
@@ -896,23 +922,27 @@
         opts = defaults opts,
             project_id  : required
             cb          : undefined
+        if not @_validate_uuids(opts) then return

     delete_project: (opts) =>
         opts = defaults opts,
             project_id  : required
             cb          : undefined
+        if not @_validate_uuids(opts) then return

     hide_project_from_user: (opts) =>
         opts = defaults opts,
             project_id : required
             account_id : required
             cb         : undefined
+        if not @_validate_uuids(opts) then return

     unhide_project_from_user: (opts) =>
         opts = defaults opts,
             project_id : required
             account_id : required
             cb         : undefined
+        if not @_validate_uuids(opts) then return

     # Make it so the user with given account id is listed as a(n invited) collaborator or viewer
@@ -936,6 +966,7 @@
         opts = defaults opts,
             project_id  : required
             cb          : required  # cb(err, is_public)
+        if not @_validate_uuids(opts) then return

     # cb(err, true if user is in one of the groups)
     user_is_in_project_group: (opts) =>
@@ -944,6 +975,7 @@
             account_id  : required
             groups      : required  # array of elts of PROJECT_GROUPS above
             cb          : required  # cb(err)
+        if not @_validate_uuids(opts) then return

     # all id's of projects having anything to do with the given account (ignores
     # hidden projects unless opts.hidden is true).
@@ -952,11 +984,13 @@
             account_id : required
             hidden     : false
             cb         : required      # opts.cb(err, [project_id, project_id, project_id, ...])
+        if not @_validate_uuids(opts) then return

     get_hidden_project_ids: (opts) =>
         opts = defaults opts,
             account_id : required
             cb         : required    # cb(err, mapping with keys the project_ids and values true)
+        if not @_validate_uuids(opts) then return

     # gets all projects that the given account_id is a user on (owner,
     # collaborator, or viewer); gets all data about them, not just id's
@@ -966,12 +1000,14 @@
             collabs_as_names : true       # replace all account_id's of project collabs with their user names.
             hidden           : false      # if true, get *ONLY* hidden projects; if false, don't include hidden projects
             cb               : required
+        if not @_validate_uuids(opts) then return

     get_projects_with_ids: (opts) =>
         opts = defaults opts,
             ids     : required   # an array of id's
             columns : PROJECT_COLUMNS
             cb      : required
+        if not @_validate_uuids(opts) then return

     get_project_titles: (opts) =>
         opts = defaults opts,
@@ -979,12 +1015,14 @@
             use_cache    : true
             cache_time_s : 60*60        # one hour
             cb           : required     # cb(err, map from project_id to string (project title))
+        if not @_validate_uuids(opts) then return

     # cb(err, array of account_id's of accounts in non-invited-only groups)
     get_account_ids_using_project: (opts) ->
         opts = defaults opts,
             project_id : required
             cb         : required
+        if not @_validate_uuids(opts) then return

     ###
     # STATS


updated fbsh

updated fbsh

--- bin/fbsh.coffee
+++ bin/fbsh.coffee
@@ -69,6 +69,12 @@
             fb.people().then(printUsers)
         when 'm'
             fb.milestones().then(printMilestones)
+        when 'search'
+            fb.search(args[2]).then(printCases)
+        when 'assign'
+            printFail(fb.assign(args[2], args[3]))
+        when 'done'
+            printFail(fb.done(args[2], args[3]))
         else
             fb.search()
               # TODO do not hardcode active status
@@ -77,16 +83,22 @@

 help = () ->
     console.log 'commands:'
-    console.log '  ls   - list active cases from current filter'
-    console.log '  ls f - list available filters'
-    console.log '  ls p - list available projects'
-    console.log '  ls m - list available milestones'
+    console.log '  ls                       - list active cases from current filter'
+    console.log '  ls f                     - list available filters'
+    console.log '  ls p                     - list available projects'
+    console.log '  ls m                     - list available milestones'
+    console.log '  ls u                     - list available users'
+    console.log '  search q                 - searches cases by specified q'
+    console.log '  assign <caseId> <userId> - assignes specified case to given user'
     done

 # utils
 isfn = (x) -> typeof x == 'function'
 toJson = (x) -> JSON.stringify(x, null, 2)
 printJson = (_) -> [].slice.call(arguments, 0).forEach((x) -> console.log(toJson(x)))
+printFail = (p) ->
+    p.fail (err) -> console.log err
+    return p

 unwrap = (x) ->
     if (x.id && x.name)


Add error handling to login.call

Add error handling to login.call

Because it should. And specifically so we can debug this test better,
which very very rarely fails:

  API
    Upgrading my account
      When Recurly can't find my account
        PUT /api/:user/subscription/change/large-ec2

--- test/integration/api.coffee
+++ test/integration/api.coffee
@@ -19,7 +19,11 @@
     uri: "#{helper.base_url}/logout"
     followRedirect: false
   , (err) =>
-    request.get @loginURL, =>
+    if err
+      done(err)
+    request.get @loginURL, (err) =>
+      if err
+        done(err)
       request.post
         uri: @loginURL
         form:


Fixed error in parser when no chords are given in a section

--- public/scripts/parser.coffee
+++ public/scripts/parser.coffee
@@ -120,7 +120,7 @@

   # If there is no above line of chords, use the section chords
   if not chords.length
-    chords = sectionChords
+    chords = sectionChords ? []

   # Otherwise, substitute * from chords where necessary
   else


Re-enabled top team graph interactivity.

--- web/coffee/progression_graphs.coffee
+++ web/coffee/progression_graphs.coffee
@@ -16,7 +16,7 @@
   },
   pointSize: 3,
   areaOpacity: 0.0,
-  enableInteractivity: false
+  enableInteractivity: true
 }

 teamGraphOptions = {


Remove cruft

Remove cruft

--- bokehjs/src/coffee/models/canvas/cartesian_frame.coffee
+++ bokehjs/src/coffee/models/canvas/cartesian_frame.coffee
@@ -41,26 +41,6 @@
       for name, extra_range of extra_ranges
         ranges[name] = extra_range
     return ranges
-
-  # _get_scales: (scale_type_name, ranges, frame_range) ->
-  #   scales = {}
-  #   for name, range of ranges
-  #     if range.type == "Range1d" or range.type == "DataRange1d"
-  #       if scale_type_name == "log"
-  #         scale_type = LogScale
-  #       else
-  #         scale_type = LinearScale
-  #       range.scale_hint = scale_type_name
-  #     else if range.type == "FactorRange"
-  #       scale_type = CategoricalScale
-  #     else
-  #       logger.warn("unknown range type for range '#{name}': #{range}")
-  #       return null
-  #     scales[name] = new scale_type({
-  #       source_range: range
-  #       target_range: frame_range
-  #     })
-  #   return scales

   _get_scales: (scale, ranges, frame_range) ->
     scales = {}


tweak params

tweak params

--- salvus/rethink.coffee
+++ salvus/rethink.coffee
@@ -131,8 +131,8 @@
     _init: (authKey) =>
         #discovery   : true  # this option conflicts with password auth -- https://github.com/neumino/rethinkdbdash/issues/133
         opts =
-            maxExponent : 4
-            timeout     : 10
+            maxExponent : 5
+            #timeout     : 10
             servers: ({host:h, authKey:authKey} for h in @_hosts)
         @r = rethinkdbdash(opts)
         @db = @r.db(@_database)


Add escape key support

Add escape key support

--- app/components/dropdown-form.cjsx
+++ app/components/dropdown-form.cjsx
@@ -1,4 +1,6 @@
 React = require 'react'
+
+ESC_KEY = 27

 DropdownForm = React.createClass
   displayName: 'DropdownForm'
@@ -18,9 +20,17 @@

   componentDidMount: ->
     @reposition()
+    addEventListener 'keydown', @handleGlobalKeyDown
+
+  componentWillUnmount: ->
+    removeEventListener 'keydown', @handleGlobalKeyDown

   reposition: ->
     console.log 'Will anchor to', @props.anchor
+
+  handleGlobalKeyDown: (e) ->
+    if e.which is ESC_KEY
+      @props.onCancel arguments...

   render: ->
     <div className="dropdown-form-underlay" style={@underlayStyle} onClick={@handleUnderlayClick}>


resolves #13

resolves #13

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -191,7 +191,7 @@
             # undo last mark
             @masterMarkingSurface.marks[@masterMarkingSurface.marks.length-1].destroy()
             # replace first element to reflect marks on marking-surface
-            @marks.shift()
+            # @marks.shift()
             # return
           else
             # new mark
@@ -257,6 +257,8 @@
     console.log 'Classifier: mark created' # STI

     # keep a copy of all marks in here
+    if @asteroidMarkedInFrame[ @currFrameIdx ]
+      @marks.pop()
     @marks.push mark
     # locate the surface this frame coresponds to


send users to asteroid mode if they click on channel cycler while cycling

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -71,6 +71,11 @@

     'click .marking-surface': ->
       @notify translate 'classifier.rightPanel.whatKindScreen' if @state is 'whatKind'
+
+    'click .channel-cycler': ->
+      if @cycling and !@tutorial.el.hasClass "open"
+        @setState 'asteroidTool'
+        @onClickCycleChannels()

     'click .finished-screen': ->
       if @state is 'whatKind' and @finishButton.prop("disabled") and !@nextSubjectButton.is(":visible")


try resolving the weird button behavior after tutorial

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -579,9 +579,10 @@
     @showSummaryScreen()

   onClickNextSubject: ->
+    @nextSubjectButton.disabled()
+    @hideMarkingButtons()
     @course.prompt_el.hide()
     @classifySummary.fadeOut(150)
-    @hideMarkingButtons()

     # # switch to verify mode
     # if @course.count % @verifyRate is 0
@@ -633,18 +634,17 @@
     # reveal ids
     @el.find('.star-id').fadeIn()

-    @commentsContainer.animate({ scrollTop: @commentsContainer.height()}, 1000)
-
-    if not User.current?
-      @el.find('.talk-pill-nologin').show()
-      @el.find('.talk-pill').hide()
-    else
-      @el.find('.talk-pill-nologin').hide()
-      @el.find('.talk-pill').show()
-
     if @classification.subject.id is 'TUTORIAL_SUBJECT'
+      console.log 'TUTORIAL SUBJECT'
       @onClickNextSubject()
     else
+      @commentsContainer.animate({ scrollTop: @commentsContainer.height()}, 1000)
+      if not User.current?
+        @el.find('.talk-pill-nologin').show()
+        @el.find('.talk-pill').hide()
+      else
+        @el.find('.talk-pill-nologin').hide()
+        @el.find('.talk-pill').show()
       @hideMarkingButtons()
       @nextSubjectButton.show()
       @setGuestObsContent()


ugh off by one error in funding form's only digits directive

--- app/directives/only-digits.coffee
+++ app/directives/only-digits.coffee
@@ -5,6 +5,6 @@
   restrict: 'A'
   link: (scope, el, attrs) ->
     el.bind 'keypress', (e) ->
-      acceptableKeyCodes = _.range(48,57).concat([0, 8])
+      acceptableKeyCodes = _.range(48,58).concat([0, 8])
       unless _.includes(acceptableKeyCodes, e.which)
         e.preventDefault()


Sometimes [K gets added in addition to \n, we need to make sure to remove the [K as well when replacing new lines with <br>

--- app/javascripts/cs_console.coffee
+++ app/javascripts/cs_console.coffee
@@ -325,7 +325,7 @@
     message = message.replace(/^\s/, '')

     message = "
#{message}"
-    message = message.replace(/\n/g, '
')
+    message = message.replace(/\[K\n|\n/g, '
')

     @addColors(message)


inform user if private key already belonged to account

--- app/js/controllers/account.coffee
+++ app/js/controllers/account.coffee
@@ -58,7 +58,10 @@
     $scope.import_key = ->
         WalletAPI.import_private_key($scope.private_key.value, $scope.account.name).then (response) ->
             $scope.private_key.value = ""
-            Growl.notice "", "Your private key was successfully imported."
+            if response == name
+                Growl.notice "", "Your private key was successfully imported."
+            else
+                Growl.notice "", "Private key already belongs to another account: \"" + response + "\"."
             Wallet.refresh_transactions_on_update()

     $scope.import_wallet = ->


Image preview fix

Image preview fix

--- app/scripts/lib/formatters.coffee
+++ app/scripts/lib/formatters.coffee
@@ -81,7 +81,7 @@
     ]

   IMG_PREVIEW_FORMATTER:
-    [/([^<]+)<\/a>/g
+    [/([^<]+)<\/a>/g
     , (_m, link, ext, alt) ->
       "

"
     ]


Now shuffling possibleActions from UltimateTicTacToe

- shuffling makes the game more insteresting and non-deterministic

--- app/scripts/minimax-worker.coffee
+++ app/scripts/minimax-worker.coffee
@@ -3,6 +3,26 @@

 player = null
 game = null
+
+# returns random j such that i ≤ j < n
+random = (i, n) -> Math.floor(Math.random()*(n-i)+i)
+
+# Fisher–Yates
+shuffle = (a) ->
+  n = a.length
+  return a if n is 0
+  for i in [0...n-1]
+    j = random i, n
+    [a[i],a[j]] = [a[j],a[i]]
+  a
+
+UltimateTicTacToe::possibleActions = ->
+  return @actions if @actions?
+  res = []
+  for [i, js] in @openPositions()
+    for j in js
+      res.push [i, j]
+  @actions = shuffle res

 self.onmessage = (e) ->
   switch e.data.command


lodasherize, add test to see if api host is set


anonymous: fallback to sign up in case of rejection

--- src/smc-webapp/client_browser.coffee
+++ src/smc-webapp/client_browser.coffee
@@ -326,7 +326,18 @@
         require('./alerts').alert_message(args...)

     do_anonymous_setup: () =>
-        await callback2(@create_account, {})
+        try
+            x = await callback2(@create_account, {})
+            if x?.event == 'account_creation_failed'
+                throw Error("failed")
+        catch err
+            # This happens if, e.g., a token is required,
+            # or maybe this ip is blocked. Falling back
+            # to normal sign up makes sense in this case.
+            i = window.location.href.indexOf('?')
+            if i != -1
+                window.history.pushState("", "", window.location.href.slice(0,i))
+            return
         if not @is_signed_in()
             await once(@, "signed_in")
         actions = @_redux.getActions('projects')


webapp/latex: some commands take longer and hence tell "exec" to not use post messages

--- src/smc-webapp/latex/document.coffee
+++ src/smc-webapp/latex/document.coffee
@@ -52,6 +52,7 @@
             timeout     : 30
             err_on_exit : false
             bash        : false
+            allow_post  : true
             cb          : required
         #console.log(opts.path)
         #console.log(opts.command + ' ' + opts.args.join(' '))
@@ -280,6 +281,7 @@
             command     : latex_cmd
             bash        : true
             timeout     : 45
+            allow_post  : false
             err_on_exit : false
             cb          : (err, output) =>
                 #if DEBUG then console.log("_run_latex done: output=", output, ", err=", err)
@@ -341,10 +343,11 @@
         if not target?
             target = @base_filename + '.sagetex.sage'
         @_exec
-            command : 'sage'
-            args    : [target]
-            timeout : 45
-            cb      : (err, output) =>
+            command     : 'sage'
+            args        : [target]
+            timeout     : 45
+            allow_post  : false
+            cb          : (err, output) =>
                 if err
                     cb?(err)
                 else
@@ -369,10 +372,11 @@
     _run_knitr: (cb) =>
         @_need_to_run ?= {}
         @_exec
-            command  : "echo 'require(knitr); opts_knit$set(concordance = TRUE); knit(\"#{@filename_rnw}\")' | R --no-save"
-            bash     : true
-            timeout  : 60
-            cb       : (err, output) =>
+            command     : "echo 'require(knitr); opts_knit$set(concordance = TRUE); knit(\"#{@filename_rnw}\")' | R --no-save"
+            bash        : true
+            timeout     : 60
+            allow_post  : false
+            cb          : (err, output) =>
                 if err
                     cb?(err)
                 else
@@ -568,11 +572,12 @@

                 #console.log('gs ' + args.join(" "))
                 @_exec
-                    command : 'gs'
-                    args    : args
+                    command     : 'gs'
+                    args        : args
                     err_on_exit : true
-                    timeout : 120
-                    cb      : (err, output) ->
+                    timeout     : 120
+                    allow_post  : false
+                    cb          : (err, output) ->
                         cb(err)

             # delete the copied PDF file, we no longer need it (might use up a lot of disk/memory space)


Remove console.log

Remove console.log

--- src/swagged-angular-resources.coffee
+++ src/swagged-angular-resources.coffee
@@ -31,7 +31,6 @@

 getResourceOperations = (apiDefinition) ->
   _.reduce(apiDefinition.paths, (memo, methods, path) ->
-    log path, methods
     _.each(methods, (operation, action) ->
       response = operation.responses["200"]
       if response and response.schema


set header

set header

--- scripts/server.coffee
+++ scripts/server.coffee
@@ -24,5 +24,6 @@
       robot.receive msg

   robot.router.get '/script.js', (req, res) ->
-    fs.readFile "#{__dirname}/../public/script.coffee", 'utf8', (err, data) ->
+    res.header "Content-Type", "text/javascript"
+    fs.readFile path.resolve(root, "public/script.coffee"), 'utf8', (err, data) ->
       res.send coffee.compile data


Init CODAP correctly when loading context

Fixes graph buttons not appearing

[#132771013]

--- src/code/models/codap-connect.coffee
+++ src/code/models/codap-connect.coffee
@@ -90,7 +90,7 @@
       resource: 'dataContext[Sage Simulation]'
     , (ret) =>
       if ret?.success
-        @initGameHandler
+        @initGameHandler ret
       else
         @codapPhone.call
           action: 'create',


Make web socket port dynamic

Make web socket port dynamic

The web socket will attempt to connect back to the service on the same port as the HTTP connection

--- oncue-service/app/assets/javascripts/entities/websocket.coffee
+++ oncue-service/app/assets/javascripts/entities/websocket.coffee
@@ -22,7 +22,7 @@
         delete @websocket.onclose
         delete @websocket.onerror
         delete @websocket.onmessage
-      @websocket = new WebSocket("ws://#{window.location.hostname}:9000/websocket")
+      @websocket = new WebSocket("ws://#{window.location.hostname}:#{window.location.port}/websocket")
       @websocket.onopen = @onOpen
       @websocket.onclose = @onClose
       @websocket.onerror = @onError


add missing TIMESLICE updates

add missing TIMESLICE updates

--- examples/audio/static/coffee/spectrogram.coffee
+++ examples/audio/static/coffee/spectrogram.coffee
@@ -47,7 +47,7 @@
     @power = [new Float32Array(NUM_SAMPLES)]
     @power_idx = [new Array(NUM_SAMPLES)]
     for i in [0..@power_idx[0].length-1]
-      @power_idx[0][i] = (i/(@power_idx[0].length-1))*TIMESLICE
+      @power_idx[0][i] = (i/(@power_idx[0].length-1))*window.TIMESLICE
     @power_source = Collections('ColumnDataSource').create(
       data:{power: @power, idx: @power_idx}
     )
@@ -292,7 +292,7 @@
   create_power: () ->
     plot_model = Collections('Plot').create()

-    xrange = Collections('Range1d').create({start: 0, end: TIMESLICE})
+    xrange = Collections('Range1d').create({start: 0, end: window.TIMESLICE})
     yrange = Collections('Range1d').create({start: -0.25, end: 0.25})

     xaxis = Collections('LinearAxis').create(


Missing colon in test

Missing colon in test


Fixed tests

Fixed tests

--- test/hubot-archive-today-test.coffee
+++ test/hubot-archive-today-test.coffee
@@ -13,7 +13,4 @@
     require('../src/hubot-archive-today')(@robot)

   it 'registers a respond listener', ->
-    expect(@robot.respond).to.have.been.calledWith(/hello/)
-
-  it 'registers a hear listener', ->
-    expect(@robot.hear).to.have.been.calledWith(/orly/)
+    expect(@robot.respond).to.have.been.calledWith(/archive (.+)/i)


Attempt object data retrieval.

Attempt object data retrieval.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -101,13 +101,21 @@
             chat = JSON.parse(body)
             people = flatten([chat.chatters.moderators, chat.chatters.staff, chat.chatters.admins, chat.chatters.global_mods, chat.chatters.viewers])

-            robot.brain.set 'people', people
+            robot.brain.set 'winners', people

             for username in people
                 points[username] ?= 0
                 points[username] += 5
                 save(robot)
     ), 60000
+
+    # Get a list of stored people and points
+    robot.respond /getset$/i, (msg) ->
+        if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
+
+            pointcount = robot.brain.get 'winners'
+            savescore = ["#{username} has #{attrs.points}" for username, attrs of pointcount]
+            msg.send "Top points: #{savescore}"

     # robot.respond /top (\d*)$/i, (msg) ->
     #     if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
@@ -124,56 +132,21 @@
     #
     #         msg.send "The top #{pointcount} users with the most points are: #{topscore}"

-    robot.respond /output$/i, (msg) ->
-        if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
-            points[username] ?= 0
-            peoplelist = robot.brain.get 'people'
-
-            # Returns active users only with their points
-            # Getting list from 'people' works but not robot.brain.data
-            score = ["#{username} has #{points[username]}" for username in peoplelist]
-
-            msg.send "Scores: #{score}"
-
-    # robot.respond /getset$/i, (msg) ->
+    # Manual data retrieval from object values
+    # robot.respond /getset1$/i, (msg) ->
     #     if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
     #
-    #         robot.brain.set 'coins', 10
-    #         coincount = robot.brain.get 'coins'
-    #         msg.send "Coints: #{coincount}"
-
-    robot.respond /getset1$/i, (msg) ->
-        if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
-
-            saved =
-                masonest:
-                    coins: 20
-                knexem:
-                    coins: 12
-                phillyf:
-                    coins: 1
-                superking:
-                    coins: 30
-
-            robot.brain.set 'winners', saved
-            coincount = robot.brain.get 'winners'
-            savescore = ["#{user} has #{attrs.coins}" for user, attrs of coincount]
-            msg.send "Coins: #{savescore}"
-
-    robot.respond /getset2$/i, (msg) ->
-        if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
-
-            saved2 =
-                masonest:
-                    coins: 20
-                knexem:
-                    coins: 12
-                phillyf:
-                    coins: 1
-                superking:
-                    coins: 30
-
-            robot.brain.set 'winners2', saved
-            coincount2 = robot.brain.get 'winners2'
-            savescore2 = ["#{user} has #{attrs.coins}" for user, attrs of saved2]
-            msg.send "Coins: #{savescore2}"
+    #         saved =
+    #             masonest:
+    #                 coins: 20
+    #             knexem:
+    #                 coins: 12
+    #             phillyf:
+    #                 coins: 1
+    #             superking:
+    #                 coins: 30
+    #
+    #         robot.brain.set 'winners', saved
+    #         coincount = robot.brain.get 'winners'
+    #         savescore = ["#{user} has #{attrs.coins}" for user, attrs of coincount]
+    #         msg.send "Coins: #{savescore}"


More inspection.

More inspection.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -123,6 +123,4 @@

             # Outputs an inspection of recall which comes out as:
             # { masonest: 3670, awebot: 3860, knexem: 3455 }
-            # msg.send "#{Util.inspect(recall)}"
-
-            msg.send "#{username}" for username in recall
+            msg.send "Inspection: #{Util.inspect(recall)}"


Increment points as normal.

Increment points as normal.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -103,8 +103,7 @@

             for username in people
                 points[username] ?= 0
-                robot.brain.set points[username] += 5
-                save(robot)
+                points[username] += 5
     ), 60000

     # robot.respond /top (\d*)$/i, (msg) ->


add ctrl-, binding for find symbols

add ctrl-, binding for find symbols

--- keymaps/omnisharp-atom.cson
+++ keymaps/omnisharp-atom.cson
@@ -34,6 +34,7 @@
   'ctrl-.': 'omnisharp-atom:get-code-actions'
   'alt-enter': 'omnisharp-atom:get-code-actions'
   'alt-shift-t': 'omnisharp-atom:find-symbols'
+  'ctrl-,': 'omnisharp-atom:find-symbols'

 '.platform-darwin atom-text-editor:not([mini])[data-grammar~="cs"], atom-text-editor:not([mini])[data-grammar~="csx"]':


Appnima HTTPS request

Appnima HTTPS request

--- lib/services/appnima.coffee
+++ lib/services/appnima.coffee
@@ -7,15 +7,15 @@
 ###
 "use strict"

-http    = require "http"
+https   = require "https"
 qs      = require "querystring"
 Hope    = require "hope"

 Appnima =
   key     : ""
-  protocol: "http"
+  protocol: "https"
   host    : "api.appnima.com"
-  port    : 80
+  port    : 443

   open: (connection) ->
     promise = new Hope.Promise()
@@ -68,7 +68,7 @@
       options.headers["Content-Type"] = "application/x-www-form-urlencoded"
       options.headers["Content-Length"] = body.length

-    client = http.request options, (response) =>
+    client = https.request options, (response) =>
       body = ""
       response.setEncoding "utf8"
       response.on "data", (chunk) -> body += chunk


Share the task between all SpellCheckViews.


fix job runner

fix job runner

--- server/api/job.coffee
+++ server/api/job.coffee
@@ -239,7 +239,7 @@
         proc.result.string = JSON.stringify proc.result[proc.function] # try saving as string then change it back
         delete proc.result[proc.function]
         job_result.insert proc
-        proc.result[proc.function] = JSON.parse(pros.result.string)
+        proc.result[proc.function] = JSON.parse(proc.result.string)
         delete proc.result.string
   job_processing.remove(proc._id) if not proc.limit or Date.now() >= proc.timeout
   if proc.repeat


fix type

fix type


Fix JSON formatting

Fix JSON formatting

--- Learn-server/index.coffee
+++ Learn-server/index.coffee
@@ -5,34 +5,48 @@
 app.get '/classes', (req, res) ->

   response = """{
-    "url": "/classes"
+    "url": "/classes",
     "classes": [
       {
-        "url": "/class/1"
-        "name": "Mobile and Autonomous Innovation"
+        "url": "/class/1",
+        "name": "Mobile and Autonomous Innovation",
         "lectures": [
           {
-            "url": "/class/1/lecture/1"
-            "name": "Lecture One"
-            "image": "/class/1/images/thumbnail"
+            "url": "/class/1/lecture/1",
+            "name": "Lecture One",
+            "image": "/class/1/lecture/1/images/thumbnail",
           },{
-            "url": "/class/1/lecture/2"
-            "name": "Lecture Two"
-            "image": "/class/1/images/thumbnail"
+            "url": "/class/1/lecture/2",
+            "name": "Lecture Two",
+            "image": "/class/1/lecture/2/images/thumbnail",
           }
         ]
       },{
-        "url": "/class/2"
-        "name": "Data Communications and Wireless Practical"
+        "url": "/class/2",
+        "name": "Data Communications and Wireless Practical",
         "lectures": [
           {
-            "url": "/class/2/lecture/1"
-            "name": "Lecture One"
-            "image": "/class/2/images/thumbnail"
+            "url": "/class/2/lecture/1",
+            "name": "Lecture One",
+            "image": "/class/2/lecture/1/images/thumbnail"
           },{
-            "url": "/class/2/lecture/2"
-            "name": "Lecture Two"
-            "image": "/class/2/images/thumbnail"
+            "url": "/class/2/lecture/2",
+            "name": "Lecture Two",
+            "image": "/class/2/lecture/2/images/thumbnail"
+          }
+        ]
+      },{
+        "url": "/class/3",
+        "name": "Information Architecture",
+        "lectures": [
+          {
+            "url": "/class/3/lecture/1",
+            "name": "Lecture One",
+            "image": "/class/3/lecture/1/images/thumbnail"
+          },{
+            "url": "/class/3/lecture/2",
+            "name": "Lecture Two",
+            "image": "/class/3/lecture/2/images/thumbnail"
           }
         ]
       }
@@ -41,7 +55,6 @@

   res.setHeader 'Content-Type', 'application/json'
   res.send response
-

 app.get '/class/:id', (req, res) ->

@@ -55,16 +68,20 @@
 app.get '/class/:classid/lecture/:lectureid', (req, res) ->

   response = """{
-    "url": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}"
-    "pages": [
-      {
-        "url": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/page/1"
-        "title": "Slide One"
-        "primary": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/images/page-1-primary"
-        "secondary": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/images/page-1-secondary"
-        "notes": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/page/1/collaboration"
-      }
-    ]
+    "lecture": {
+      "url": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}",
+      "name": "Lecture One",
+      "image": "/class/1/lecture/1/images/thumbnail",
+      "pages": [
+        {
+          "url": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/page/1",
+          "title": "Slide One",
+          "primary": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/images/page-1-primary",
+          "secondary": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/images/page-1-secondary",
+          "notes": "/class/#{req.route.params['classid']}/lecture/#{req.route.params['lectureid']}/page/1/collaboration"
+        }
+      ]
+    }
   }"""

   res.setHeader 'Content-Type', 'application/json'
@@ -94,8 +111,8 @@
   res.sendfile 'files/placeholder.png'

-
-app.listen 3000
-console.log 'Listening on port 3000'
+port = 80
+app.listen port
+console.log "Listening on port #{port}"


Création d'un utilisateur anonyme

Création d'un utilisateur anonyme

--- server/middles.coffee
+++ server/middles.coffee
@@ -22,6 +22,12 @@
       INSERT INTO ardoise ("id", "montant")
       VALUES (0, 0)
     """
+    .then =>
+      @connection.client.query """
+        INSERT INTO utilisateur ("id", "login", "ardoise_id", "nom", "prenom", "mail", "departement_id")
+        VALUES (0, 'anonyme', 0, 'nonyme', 'a', 'foyer@ens-lyon.fr.invalid', 1)
+      """
+
 .then ->
   @connection.client.query """
     SELECT *


Forgot to update jsdoc #360

Forgot to update jsdoc #360

--- src/goo/loaders/DynamicLoader.coffee
+++ src/goo/loaders/DynamicLoader.coffee
@@ -44,6 +44,7 @@
 	* @param {object} parameters
 	* @param {World} [parameters.world] The target World object.
 	* @param {string} [parameters.rootPath] The root path where to get resources.
+	* @param {boolean} [parameters.ajax] If true, load resources from the server if not found in the cache. Defaults to true.
 	*###
 	class DynamicLoader
 		_jsonTest = /\.(shader|script|entity|material|scene|mesh|texture)$/


Removed toJSON

Removed toJSON

--- core/app/backbone/models/user.coffee
+++ core/app/backbone/models/user.coffee
@@ -22,12 +22,6 @@

   feed_activities: ->
     @_feed_activities ?= new UserActivities null, user: @
-
-  toJSON: ->
-    username = @get('username')
-    _.extend super,
-      is_current_user: FactlinkApp.isCurrentUser(this)
-      link: @link()

   is_following_users: ->
     !@following.isEmpty()


Refactoring

Refactoring

--- client/Social/Activity/views/privatemessage/pane.coffee
+++ client/Social/Activity/views/privatemessage/pane.coffee
@@ -142,7 +142,7 @@
     return [prevSibling, nextSibling]

-  unsetPaddingIfNeed: ->
+  removePadding: ->

     listView     = @listController.getView()
     headerHeight = @heads?.getHeight() or 0
@@ -156,8 +156,7 @@

   messageExpanded: () ->

-    if @unsetPaddingIfNeed()
-      @scrollDown()
+    @scrollDown()  if @removePadding()

     @scrollView.wrapper.emit 'MutationHappened'


Activity: minor refac for edit last message

--- client/Social/Activity/views/privatemessage/pane.coffee
+++ client/Social/Activity/views/privatemessage/pane.coffee
@@ -51,13 +51,14 @@
   editLastMessage: ->

     items = @listController.getItemsOrdered().slice(0).reverse()
-    myId  = KD.whoami()._id
-
-    for item in items when item.data.account._id is myId
+    post  = item.getData()
+
+    for item in items when KD.isMyPost post

       item.showEditWidget()
-
       break
+
+    return post

   # override this so that it won't


[Logout] logout location.reload() done after cookie is just changed

--- client/app/MainApp/maincontroller/maincontroller.coffee
+++ client/app/MainApp/maincontroller/maincontroller.coffee
@@ -96,9 +96,10 @@
   doLogout:->
     KD.logout()
     KD.remote.api.JUser.logout (err, account, replacementToken)=>
-      $.cookie 'clientId', replacementToken if replacementToken
       @_logoutAnimation()
-      KD.utils.wait 1100, -> location.reload()
+      KD.utils.wait 1000, ->
+        $.cookie 'clientId', replacementToken  if replacementToken
+        location.reload()

   _logoutAnimation:->
     mainView      = KD.getSingleton("mainView")


Prevent form submit on enter in IE < 11.

--- community/browser/app/scripts/directives/onEnter.coffee
+++ community/browser/app/scripts/directives/onEnter.coffee
@@ -28,7 +28,8 @@
         elem.bind('keydown', (e)->
           code = e.which || e.keyCode
           return unless code is 13
-
+          e.preventDefault()
+
           if attr.onEnter is 'focus'
             element = document.getElementById(attr.onEnterTargetId)
             element.focus()


added init values to most methods

added init values to most methods

--- lib/coffee/helper.coffee
+++ lib/coffee/helper.coffee
@@ -10,6 +10,7 @@
   obj or= {}
   return _.has(obj, "route") and _.has(obj, "res") and _.has(obj, "next")

+
 item_by_keys = (obj, keyArr) ->
   item = obj
   for key in keyArr
@@ -17,14 +18,6 @@
     item = item[key]

   return item.toString()
-
-
-# Take an array of keys to retrieve an object
-arg_by_keys_wrapper = (keyArr) ->
-  throw "array is not supplied" unless keyArr
-  return (mod) ->
-    return (level, args) ->
-      return item_by_keys(args, keyArr)

 req_item_by_keys = (keyArr) ->
@@ -48,11 +41,17 @@
       return realTitle

-exports.session_user_id = (defArr = ["session", "user", "id"]) ->
+exports.session_user_id = ({init, defArr} = {}) ->
+  defArr ?= ["session", "user", "id"]
+  init   ?= null
   return req_item_by_keys(defArr)

-exports.session_user_email = (defArr = ["session", "user", "email"]) ->
-  return req_item_by_keys(defArr)
+
+exports.session_user_email = ({init, defArr} = {}) ->
+  defArr ?= ["session", "user", "email"]
+  init   ?= null
+  return req_item_by_keys(defArr) or init
+

 exports.line_number = (opts = {}) ->
   return (mod) ->
@@ -61,8 +60,8 @@
         throw new Error()
       catch e
         # now magic will happen: get line number from callstack
-        # console.error(e.stack) # TODO REMOVE
         return e.stack.split('\n')[4].split(':')[1];
+

 exports.module_title = (opts = {}) ->
   return (mod) ->
@@ -71,7 +70,7 @@
       #   return moduleTitle

       unless mod
-        return '<unknown>'
+        return opts.init or '<unknown>'

       unless mod.id
         return mod
@@ -94,7 +93,8 @@
 exports.single_line_message = (opts = {}) ->
   showHidden = opts.showHidden
   depth      = opts.depth
-  separator  = opts.separator or ""
+  separator  = opts.separator ? ""
+  init       = opts.init ? null
   return (mod) ->
     return (level, args) ->
       ret = []
@@ -106,13 +106,15 @@

       ret = ret.join(separator)
       # remove all '\n' but not '\\n' as that is an escaped '\n' and not something introduced by util.inspect
-      ret = ret.replace(removeReturnsRegex, '')
+      ret = ret.replace(removeReturnsRegex, '') or init
       return ret
+

 exports.message = (opts = {}) ->
   showHidden = opts.showHidden
   depth      = opts.depth
-  separator  = opts.separator or ""
+  separator  = opts.separator ? ""
+  init       = opts.init ? null
   return (mod) ->
     return (level, args) ->
       ret = []
@@ -122,8 +124,10 @@
         else
           ret.push(util.inspect(item, showHidden, depth))

-      ret = ret.join(separator)
+      # works because of "falsy" ''.
+      ret = ret.join(separator) or init
       return ret
+

 do_but_ignore_req_at_first = (fn) ->
@@ -140,10 +144,8 @@
           return fn_after_mod(level, args)

-
 exports.single_line_message_ignore_express_req_at_first = do_but_ignore_req_at_first(exports.single_line_message)
 exports.message_ignore_express_req_at_first             = do_but_ignore_req_at_first(exports.message)
-

 exports.string = (x) ->


:lipstick: Use existencial operator in test

--- lib/color-project.coffee
+++ lib/color-project.coffee
@@ -43,7 +43,7 @@
     new ColorContext(@getVariablesObject())

   getVariablesObject: ->
-    return {} unless @variables
+    return {} unless @variables?

     variablesObject = {}
     variablesObject[variable.name] = variable for variable in @variables


fixed not a funciont

fixed not a funciont

--- lib/commands/nojs.coffee
+++ lib/commands/nojs.coffee
@@ -71,7 +71,7 @@
     .then ->
       runCmd("git add . --all && git commit -m '#{options.message}'", execOptions)
     .then ->
-      runCommand("git checkout `git reflog HEAD | sed -n " +
+      runCmd("git checkout `git reflog HEAD | sed -n " +
         "'/checkout/ !d; s/.* \\(\\S*\\)$/\\1/;p' | sed '2 !d'`", execOptions)
     .then ->
       ui.write "should be good to push"


realm-collection view is not activated initially - fix "0 realms" bug

--- www/src/views/realm/collection.coffee
+++ www/src/views/realm/collection.coffee
@@ -5,7 +5,6 @@
 ], (RealmSmall, AnyCollection, html) ->
     class extends AnyCollection
         template: _.template(html)
-        activated: true
         generateItem: (model) ->
             new RealmSmall(model: model)


Fixed #395 unnecessary XHR produces by GridColumnsManager

--- wzk/ui/grid/GridColumnsManager.coffee
+++ wzk/ui/grid/GridColumnsManager.coffee
@@ -269,7 +269,6 @@
     @grid.setColumns(arrCols)
     @grid.rowBuilder.setColumns(arrCols)
     @showOrHideHeader()
-    @grid.refresh()

   ###*
     @protected


More debug output

More debug output

--- server/cron/submitSubmits.coffee
+++ server/cron/submitSubmits.coffee
@@ -57,6 +57,7 @@
     try
         testSystem = await getTestSystem("informatics")
         registeredUser = await RegisteredUser.findByKey(submit.user)
+        logger.info submit, registeredUser
         await testSystem.submitWithObject(registeredUser, submit.problem, {source: submit.sourceRaw, language: getLanguage(submit.language)})
         await Submit.remove({_id: submit._id})
         dirtyResults = {}


fix jawbone konnector

fix jawbone konnector

--- server/konnectors/jawbone.coffee
+++ server/konnectors/jawbone.coffee
@@ -99,7 +99,6 @@
                 if moves.length > 0
                     start = moment(moves[0].date)
                     year = start.format('YYYY-MM-DD').substring 0, 4
-                    console.log moves[0].date
                 else
                     start = moment '20110101', 'YYYYMMDD'
                     year = '2011'
@@ -252,10 +251,8 @@
         else
             callback()

-    async.eachSeries lines, (line, callback) ->
-        saveLine line, (err) ->
-            if err then callback err
-            else recSave()
+    async.eachSeries lines, (line, cb) ->
+        saveLine line, cb
     , (err) ->
         log.info 'CSV file imported.'
         callback err


bugfix

bugfix

--- server/mails/mail_handler.coffee
+++ server/mails/mail_handler.coffee
@@ -42,12 +42,12 @@

             else return cb null

-                mailOptions =
-                    to: guest.email
-                    subject: subject
-                    html: template
-                        event: event.toJSON()
-                        key: guest.key
+            mailOptions =
+                to: guest.email
+                subject: subject
+                html: template
+                    event: event.toJSON()
+                    key: guest.key

             CozyAdapter.sendMailFromUser mailOptions, (err) ->
                 if not err


/competitions/by-membership finds competitions by ownerId as well

--- resources/competitions.coffee
+++ resources/competitions.coffee
@@ -65,11 +65,12 @@
         console.log "[HTTP] found no competition memberships (userId=#{userId})"
         return []

-      membershipIds = memberships.map((membership) ->
+      ownerOrMatchesMembership = memberships.map((membership) ->
         _id: membership.competition_id
       )
-      anyCompetition = { $or: membershipIds }
-      dbQuery = @model.find(anyCompetition)
+      ownerOrMatchesMembership.push({ owner_id: userId })
+      condition = { $or: ownerOrMatchesMembership }
+      dbQuery = @model.find(condition)
       unless showClosed
         dbQuery.where('open').equals(true)
       Q(dbQuery.exec())


cleaned up

cleaned up

--- server/methods.coffee
+++ server/methods.coffee
@@ -30,8 +30,7 @@
     createCard = Async.wrap Stripe.customers, 'createCard'
     try
       card = createCard user.billing.customerId, card: card.id
-      Meteor.users.update _id: user._id,
-        $set: 'billing.cardId': card.id
+      user.update('billing.cardId': card.id)
     catch e
       console.error e
       throw new Meteor.Error 500, e.message
@@ -39,7 +38,7 @@
   #
   # Delete a card on customer and unset cardId
   #
-  deleteCard: (userId, card) ->
+  deleteCard: (userId, cardId) ->
     console.log 'Deleting card for', userId
     user = BillingUser.first(_id: userId)
     unless user then throw new Meteor.Error 404, "User not found.  Card cannot be deleted."
@@ -47,9 +46,8 @@
     Stripe = StripeAPI(Billing.settings.secretKey)
     deleteCard = Async.wrap Stripe.customers, 'deleteCard'
     try
-      card = deleteCard user.billing.customerId, card.id
-      Meteor.users.update _id: user._id,
-        $set: 'billing.cardId': null
+      card = deleteCard user.billing.customerId, cardId
+      user.update('billing.cardId': null)
     catch e
       console.error e
       throw new Meteor.Error 500, e.message
@@ -112,24 +110,6 @@

   #
-  # Restart a subscription that was previously canceled
-  #
-  restartSubscription: (userId, card) ->
-    console.log 'Restarting subscription for', userId
-    user = BillingUser.first(_id: userId)
-    if user then customerId = user.billing.customerId
-    unless user and customerId then new Meteor.Error 404, "User not found.  Subscription cannot be restarted."
-
-    Stripe = StripeAPI(Billing.settings.secretKey)
-    createCard = Async.wrap Stripe.customers 'createCard'
-    try
-      newCard = createCard customerId, card: card.id
-      user.update('billing.cardId': newCard.id)
-    catch e
-      console.error e
-      throw new Meteor.Error 500, e.message
-
-  #
   # Get past invoices
   #
   getInvoices: ->


Use capitalisation to differentiate pattern snippets

The CSON snippet added in a3e3f04 was too specific, even for authoring a
language grammar. All I really needed was something to insert a one-line
"patterns:" snippet. Which is usually followed-up with "inc" anyway.

--- .config/snippets.cson
+++ .config/snippets.cson
@@ -56,15 +56,15 @@

 # CoffeeScript/CSON
 ".source.coffee":
-	"Patterns":
+	"Patterns (Single-line)":
 		prefix: "pat"
+		body: "patterns: [{$1}]"
+	"Patterns (Multiline)":
+		prefix: "Pat"
 		body: "patterns: [{\n\t$1\n}]"
 	"Include":
 		prefix: "inc"
 		body: "include: \"#$1\""
-	"One-line pattern-inclusion":
-		prefix: "pic"
-		body: "patterns: [{include: \"#$1\"}]"
 	"Extended Expression":
 		prefix: "xe"
 		body: "'(?x)\n\t$1\n'"


Shortcut to color level pips based on completion rates

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -59,6 +59,8 @@
     'mouseleave .portals': 'onMouseLeavePortals'
     'mousemove .portals': 'onMouseMovePortals'
     'click .poll': 'showPoll'
+  shortcuts:
+    'shift+s': 'onShiftS'

   constructor: (options, @terrain) ->
     super options
@@ -273,6 +275,51 @@
     @applyCampaignStyles()
     @testParticles()

+  onShiftS: (e) ->
+    @generateCompletionRates() if @editorMode
+
+  generateCompletionRates: ->
+    return unless me.isAdmin()
+    startDay = utils.getUTCDay -14
+    startDayDashed = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}"
+    endDay = utils.getUTCDay -1
+    endDayDashed = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}"
+    for level in @campaign?.renderedLevels ? []
+      #$("div[data-level-slug=#{level.slug}] .level-kind").text("Loading...")
+      do (level) =>
+        console.log(level.slug)
+        request = @supermodel.addRequestResource 'level_completions', {
+          url: '/db/analytics_perday/-/level_completions'
+          data: {startDay: startDay, endDay: endDay, slug: level.slug}
+          method: 'POST'
+          success: (data) =>
+            return if @destroyed
+            started = 0
+            finished = 0
+            for day in data
+              started += day.started ? 0
+              finished += day.finished ? 0
+            if started is 0
+              ratio = 0
+            else
+              ratio = finished / started
+            rateDisplay = (ratio * 100).toFixed(2) + '%'
+            #$("div[data-level-slug=#{level.slug}] .level-kind").html(finished + " / " + started + "<br>" + rateDisplay)
+            if ratio <= 0.5
+              color = "rgb(255, 0, 0)"
+            else if ratio > 0.5 and ratio <= 0.85
+              offset = (ratio - 0.5) / 0.35
+              color = "rgb(255, #{Math.round(256 * offset)}, 0)"
+            else if ratio > 0.85 and ratio <= 0.95
+              offset = (ratio - 0.85) / 0.1
+              color = "rgb(#{Math.round(256 * (1-offset))}, 256, 0)"
+            else
+              color = "rgb(0, 256, 0)"
+            #$("div[data-level-slug=#{level.slug}] .level-kind").css("color", color)
+            $("div[data-level-slug=#{level.slug}]").css("background-color", color)
+        }, 0
+        request.load()
+
   afterInsert: ->
     super()
     if @getQueryVariable('signup') and not me.get('email')


use redirect method instead

use redirect method instead

--- apps/fair_info/client/index.coffee
+++ apps/fair_info/client/index.coffee
@@ -25,7 +25,6 @@
     initFairLayout
       model: new Profile sd.PROFILE
       fair: @fair
-    defaultNavLinkIsActive()

   visitors: ->
     view = new FairInfoVisitorsView
@@ -44,11 +43,6 @@
       model: @fair
       el: $('.fair-info2-events')

-  defaultNavLinkIsActive = ->
-    strLen = sd.CURRENT_PATH.length
-    if sd.CURRENT_PATH.substr((strLen - 5), strLen) == 'info2'
-      $('.fair-info2-nav a:first-child').addClass('is-active')
-
 module.exports.init = ->
   new FairInfoRouter
   Backbone.history.start({pushState: true})


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- source/javascripts/_test/string_extensions.coffee
+++ source/javascripts/_test/string_extensions.coffee
@@ -51,7 +51,7 @@
   equals "31.".toInt(), 31

-  equals "031.3".toInt(), 31
+  equals "09".toInt(), 31

 test "#underscore", ->
   equals "Pro-tip".underscore(), "pro_tip"


specs

specs

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -1077,6 +1077,48 @@

             expect(editor.getText()).toBe 'ok then a.someMethod()'

+      describe "when the alternate keyboard integration is used", ->
+        beforeEach ->
+          atom.config.set('autocomplete-plus.confirmCompletion', 'tab and enter')
+          atom.config.set('autocomplete-plus.alternateCompletion', true)
+
+        it 'inserts the word on tab and moves the cursor to the end of the word', ->
+          triggerAutocompletion(editor, false, 'a')
+
+          runs ->
+            key = atom.keymaps.constructor.buildKeydownEvent('tab', {target: document.activeElement})
+            atom.keymaps.handleKeyboardEvent(key)
+
+            expect(editor.getText()).toBe 'ok then ab'
+
+            bufferPosition = editor.getCursorBufferPosition()
+            expect(bufferPosition.row).toEqual(0)
+            expect(bufferPosition.column).toEqual(10)
+
+        it 'does not insert the word on enter', ->
+          triggerAutocompletion(editor, false, 'a')
+
+          runs ->
+            key = atom.keymaps.constructor.buildKeydownEvent('enter', {keyCode: 13, target: document.activeElement})
+            atom.keymaps.handleKeyboardEvent(key)
+            expect(editor.getText()).toBe 'ok then a\n'
+
+        it 'inserts the word on enter after the selection has been changed and moves the cursor to the end of the word', ->
+          triggerAutocompletion(editor, false, 'a')
+
+          runs ->
+            editorView = atom.views.getView(editor)
+            atom.commands.dispatch(editorView, 'core:move-down')
+            key = atom.keymaps.constructor.buildKeydownEvent('enter', {keyCode: 13, target: document.activeElement})
+            atom.keymaps.handleKeyboardEvent(key)
+
+            expect(editor.getText()).toBe 'ok then abc'
+
+            bufferPosition = editor.getCursorBufferPosition()
+            expect(bufferPosition.row).toEqual(0)
+            expect(bufferPosition.column).toEqual(11)
+
+
       describe 'when tab is used to accept suggestions', ->
         beforeEach ->
           atom.config.set('autocomplete-plus.confirmCompletion', 'tab')


Update test AMI

Update test AMI

--- scripts/test-instance/instance-ami.coffee
+++ scripts/test-instance/instance-ami.coffee
@@ -1,7 +1,8 @@
 module.exports  =
-  current: 'ami-9cea82fc'
+  current: 'ami-7592f915'

   # Ordered by creation date
   list: [
     'ami-9cea82fc'
+    'ami-7592f915'
   ]


code cleanup

code cleanup

--- src/extras/testing/controller_test_case.coffee
+++ src/extras/testing/controller_test_case.coffee
@@ -2,7 +2,6 @@
   @mixin Batman.ModelExpectations

   dispatch: (action, params = {}) ->
-    debugger
     @controllerClass ||= Batman.currentApp[@constructor.name.replace(/Test/,'')]
     if not @controllerClass
       throw new Error( "Couldn't deduce controller name" )


chore($tests): change message for environment method tests

--- specs/support/helpers/spec_helpers.coffee
+++ specs/support/helpers/spec_helpers.coffee
@@ -95,13 +95,13 @@

 exports.environmentMethod = (method) ->
   cannotBeCalledInsideIt: ->
-    runningSpecs('called inside it')
+    runningSpecs('call inside it')
     .shouldFailWith /called inside a it block/, ->
       describe 'foo', ->
         it -> spectacular.global[method]()

   cannotBeCalledOutsideIt: ->
-    runningSpecs('called outside it')
+    runningSpecs('call outside it')
     .shouldStopWith /called outside a it block/, ->
       describe 'foo', -> spectacular.global[method]()


fix chaptermarks not changing during playback

--- src/javascripts/extensions/chaptermarks.coffee
+++ src/javascripts/extensions/chaptermarks.coffee
@@ -42,8 +42,8 @@
     @options = _.extend(@defaultOptions, @app.extensionOptions.ChapterMarks)
     return if @options.disabled

-    @chaptermarks = @app.episode.chaptermarks
-    return unless @chaptermarks && @chaptermarks.length
+    @chapters = @app.episode.chaptermarks
+    return unless @chapters && @chapters.length

     @renderPanel()
     @renderButton()
@@ -61,13 +61,13 @@
   renderPanel: =>
     @panel = $(@panelHtml)
     @panel.hide()
-    marks = _.map(@chaptermarks, (item) =>
+    @chaptermarks = _.map(@chapters, (item) =>
       new ChapterMark(item, @click)
     )
-    marks = _.sortBy(marks, (mark) =>
+    @chaptermarks = _.sortBy(@chaptermarks, (mark) =>
       mark.data.startInSeconds
     )
-    _.map(marks, (mark) =>
+    _.map(@chaptermarks, (mark) =>
       @panel.find('ul').append(mark.render())
     )

@@ -76,12 +76,12 @@

   setActiveMark: () =>
     time = @app.player.currentTimeInSeconds
-    if time <= Utils.hhmmssToSeconds(@chaptermarks[0].start)
+    if time <= Utils.hhmmssToSeconds(@chaptermarks[0].data.start)
       @deactivateAll()
       @activateMark(@chaptermarks[0])
     else
       _(@chaptermarks).findLast (mark) =>
-        markTime = Utils.hhmmssToSeconds(mark.start)
+        markTime = Utils.hhmmssToSeconds(mark.data.start)
         return unless time >= markTime

         @deactivateAll()


Add string case to

Add string case to

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -714,6 +714,9 @@
 };

 var and_ = function (x, xs) {
+  if (typeof xs == 'string' || xs instanceof String) {
+    return x + xs;
+  } // cases for other sequences
   return {
     head: x,
     tail: xs