now lists plugins in system/plugins.json

--- server/express/lib/server.coffee
+++ server/express/lib/server.coffee
@@ -342,7 +342,7 @@
           res.send('Favicon Saved')
         )
       else
-        mkdirp(argv.status, 0o0777, ->
+        mkdirp(argv.status,   0777, ->
           fs.writeFile(favLoc, buf, (e) ->
             if e then throw e
             res.send('Favicon Saved')
@@ -360,6 +360,12 @@
   # Send an array of pages in the database via json
   app.get('/system/slugs.json', cors, (req, res) ->
     fs.readdir(argv.db, (err, files) ->
+      res.send(files)
+    )
+  )
+
+  app.get('/system/plugins.json', cors, (req, res) ->
+    fs.readdir(path.join(argv.c, 'plugins'), (err, files) ->
       res.send(files)
     )
   )


small refactoring

small refactoring

--- client/app/views/home_view.coffee
+++ client/app/views/home_view.coffee
@@ -12,6 +12,12 @@
     constructor: ->
         super()
         @apps = new AppCollection @
+
+    subscriptions:
+        "app:removed": "onAppRemoved"
+
+    onAppRemoved: (slug) =>
+        @buttons.find("##{slug}").remove()

     ### Functions ###


Add checkin announcement

Add checkin announcement

--- client/components/Checkins.coffee
+++ client/components/Checkins.coffee
@@ -60,6 +60,10 @@
             <p>
             Не забудьте с собой паспорт (если паспорта еще нет, то свидетельство о рождении) -- его могут спросить охранники на входе!
             </p>
+
+            <Alert bsStyle="danger">
+                Занятий 24 ноября и 1 декабря не будет. Ниже запись на 8 декабря.
+            </Alert>

             {
             if @state.result?.error


Update Checkins.coffee

Update Checkins.coffee

--- client/components/Checkins.coffee
+++ client/components/Checkins.coffee
@@ -10,7 +10,8 @@
 import callApi from '../lib/callApi'

-SESSION_TIMES = ["14:00", "15:30"]
+SESSION_TIMES = ["12:00", "13:00", "14:00", "15:00"]
+#SESSION_TIMES = ["14:00", "15:30"]
 #SESSION_TIMES = ["14:00"]

 export default class Checkins extends React.Component
@@ -33,7 +34,7 @@
         @props.myUser && @props.myUser.rating > 0

     render: () ->
-        wasme = [false, false]
+        wasme = [false, false, false, false]

Регистрация на занятие

@@ -59,6 +60,10 @@

             Не забудьте с собой паспорт (если паспорта еще нет, то свидетельство о рождении) -- его могут спросить охранники на входе!

+
+            <Alert bsStyle="danger">
+                13 октября будет нестандартное расписание занятий: вместо двух занятий с большим количеством мест записи будут 4 занятия с небольшим количеством мест записи.
+            </Alert>

             {
             if @state.result?.error
@@ -133,7 +138,7 @@

                     {
-                    if wasme[0] or wasme[1]
+                    if wasme[0] or wasme[1] or wasme[2] or wasme[3]
                         <Button bsStyle="info" onClick={@register(null, @props.myUser?._id)}>Отменить регистрацию</Button>
                     else if !@canRegister()
                         <Alert bsStyle="danger">


добавлена возможность подтвердить с помощью Enter

--- client/components/EditUser.coffee
+++ client/components/EditUser.coffee
@@ -30,6 +30,7 @@
                 value = {@props.value}
                 onChange = {@props.onChange}
                 onBlur = {@props.onBlur}
+                onKeyPress = {@props.onKeyPress}
             />
             {if !@props.hideErrors

@@ -51,6 +52,7 @@
         @handleNewPassTwoChange = @handleNewPassTwoChange.bind(this)
         @handlePasswordChange = @handlePasswordChange.bind(this)
         @submit = @submit.bind(this)
+        @handleKeyPressed = @handleKeyPressed.bind(this)

     startState: (props) ->
         return
@@ -66,6 +68,10 @@
             passError: false
             newName: ''
             unknownError: false
+
+    handleKeyPressed: (e)->
+        if e.key == "Enter"
+            @submit()

     updateInformatics: ()->
         await @setState informaticsLoading: true
@@ -144,6 +150,7 @@
                                 name = "newName"
                                 value = {@state.newName}
                                 onChange = {(@handleNewNameChange)}
+                                onKeyPress={@handleKeyPressed}
                             />

@@ -153,6 +160,7 @@
                                 name = "newLogin"
                                 value = {@state.cfLogin}
                                 onChange = {@handleCfChange}
+                                onKeyPress={@handleKeyPressed}
                             />

@@ -162,6 +170,7 @@
                                 name = "newClass"
                                 value = {@state.clas}
                                 onChange = {@handleClassChange}
+                                onKeyPress={@handleKeyPressed}
                             />

@@ -172,7 +181,7 @@
                                 value = {@state.informaticsPassword}
                                 onChange = {@handleInformaticsPasswordChange}
                                 onBlur = {@updateInformatics}
-                                errors = {[@state.informaticsError &&
Пароль не подходит к вашему аккаунту на informatics
]}
+                                errors = {[@state.informaticsError &&
Пароль не подходит к вашему аккаунту на informatics
]}
                             />

@@ -183,6 +192,7 @@
                                 value =  {@state.password}
                                 onChange = {@handlePasswordChange}
                                 errors = {[@state.passError && "Неправильный пароль"]}
+                                onKeyPress={@handleKeyPressed}
                             />

@@ -194,6 +204,7 @@
                                 onChange = {@handleNewPassOneChange}
                                 hideErrors = {true}
                                 errors = {[noMatch && "Пароли не совпадают", whitespace && "Пароль не может начинаться с пробела или заканчиваться на него"]}
+                                onKeyPress={@handleKeyPressed}
                             />

@@ -204,6 +215,7 @@
                                 value = {@state.newPassTwo}
                                 onChange = {@handleNewPassTwoChange}
                                 errors = {[noMatch && "Пароли не совпадают", whitespace && "Пароль не может начинаться с пробела или заканчиваться на него"]}
+                                onKeyPress={@handleKeyPressed}
                             />

                     {@state.unknownError &&
Неизвестная ошибка, проверьте подключение к интернету и перезагрузите страницу
}


Let codemirror properly handle events when the query is too big - #3043

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1190,7 +1190,8 @@

             # The expensive operations are coming. If the query is too long, we just don't parse the query
             if @codemirror.getValue().length > @max_size_query
-                return false
+                # Return true or false will break the event propagation
+                return undefined

             query_before_cursor = @codemirror.getRange {line: 0, ch: 0}, @codemirror.getCursor()
             query_after_cursor = @codemirror.getRange @codemirror.getCursor(), {line:@codemirror.lineCount()+1, ch: 0}


Add comment

Add comment

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1649,6 +1649,7 @@

             return result_query

+        # Split input in queries. We use semi colon, pay attention to string, brackets and comments
         separate_queries: (query) =>
             queries = []
             is_parsing_string = false


Fix problem with comment on last line in web console.

OTS by @vexocide

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


Added test collapseExpandSidebar

Added test collapseExpandSidebar

--- client/test/lib/ide/layout.coffee
+++ client/test/lib/ide/layout.coffee
@@ -107,3 +107,22 @@
       .end()

+  collapseExpandSidebar: (browser) ->
+
+    sidebarSelector         = '.kdview.with-sidebar .logo-wrapper'
+    collapseButton          = '.logo-wrapper .sidebar-close-handle'
+    collapsedWindowSelector = '.kdview.with-sidebar.desktop.collapsed'
+
+    user = helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
+
+    browser
+      .waitForElementVisible     sidebarSelector, 20000
+      .moveToElement             sidebarSelector, 5, 5
+      .waitForElementVisible     collapseButton, 20000
+      .click                     collapseButton
+      .waitForElementVisible     collapsedWindowSelector, 20000
+      .moveToElement             sidebarSelector, 5, 5
+      .waitForElementNotPresent  collapsedWindowSelector, 20000
+      .end()
+


Implement inline editing/saving of records in table view.

--- app/assets/javascripts/databases.js.coffee
+++ app/assets/javascripts/databases.js.coffee
@@ -49,10 +49,41 @@
     bindEventHandlers()

 columnsReordered = ->
-  console.log('Columns reordered')
+  console.log('Columns reordered') # TODO: Send updates to server, so column order is persisted.

 rowSelected = (row) ->
-  console.log('Row selected')
+  $row = $(row)
+  # TODO: We should make each input appropraite for the column it's in.
+  return if $row.has(':input[type=submit][value=Save]').length == 1 # The row is already in save mode.
+  $row.find('td').each (index) ->
+    $td = $(this)
+    text = $td.text()
+    $td.replaceWith("<input type='text' value='#{text}' />	")
+  $actions_th = $row.find('th.actions')
+  $actions_th.append('<input type="submit" value="Save" />')
+  $actions_th.find('input[value=Save]').on 'click', () ->
+    saveRow(row)
+  $row.find('td:first :input').focus()
+
+saveRow = (row) ->
+  console.log('Saving row:')
+  $row = $(row)
+  values = $row.find(':input:not(:input[type=submit][value=Save])').map(() -> $(this).val()).get()
+  console.log values
+  saveRecord($row.data('url'), $row.data('id'), values)
+  # TODO: We should do validations first.
+  # TODO: We shouldn't do this until the save has succeded.
+  $row.find('td').each (index) ->
+    $td = $(this)
+    text = $td.find(':input').val()
+    $td.replaceWith("#{text}	")
+  $row.find('th.actions input[value=Save]').remove()
+  $row.removeClass('selected')
+
+saveRecord = (url, record_id, values) ->
+  data = {_method: 'PUT', all_values: values}
+  # TODO: We should handle failures and timeouts in POSTing, and make our own "Saving..." indicator.
+  $.post(url, data, null, 'json')

 rowDeselected = (row) ->
   console.log('Row deselected')


Initialize sliders once.

Initialize sliders once.

--- app/assets/javascripts/galleries.js.coffee
+++ app/assets/javascripts/galleries.js.coffee
@@ -26,7 +26,9 @@
         gallery:
           ratings_enabled: e.target.checked

-  $('.slider').slider()
+  slider = $('.slider').first()
+  if !slider.prev().hasClass('slider')
+    slider.slider()

   $('#new_picture input[type="file"]').change (e) ->
     $('#new_picture button[type="submit"]').removeClass('disabled')


Remove "getExtendedSupport" from LARA Interactive API

[#155515297]
[#155514666]

--- app/assets/javascripts/iframe-saver.coffee
+++ app/assets/javascripts/iframe-saver.coffee
@@ -90,7 +90,6 @@
           ForwardBlocker.instance.enable_forward_navigation_for(@$iframe[0])
         else
           ForwardBlocker.instance.prevent_forward_navigation_for(@$iframe[0], opts.message)
-    @iframePhone.post('getExtendedSupport')

     if @learner_state_saving_enabled()
       @iframePhone.post('getLearnerUrl')


Derived fields now are inheritable, not shared amongst entire inheritance chain.

--- app/assets/javascripts/slickbone.js.coffee
+++ app/assets/javascripts/slickbone.js.coffee
@@ -40,21 +40,27 @@
 class SlickBone.Model extends Backbone.Model
   initialize: ->
     @bind 'change', => @deriveFields()
+
+  @_setUpDerivations: ->
+    @_derivations ||= []
+    if @_derivations == @__super__.constructor._derivations
+      @_derivations = _.clone(@__super__.constructor._derivations)

   @derivedField: (fieldName, derivationFunction) ->
-    @_derivations ||= []
+    @_setUpDerivations()
     @_derivations.push
       field: fieldName
       func:  derivationFunction

   @prependDerivedField: (fieldName, derivationFunction) ->
-    @_derivations ||= []
+    @_setUpDerivations()
     @_derivations.unshift
       field: fieldName
       func: derivationFunction

-  deriveFields: ->
+  deriveFields: ->
     for derivation in @constructor._derivations
       result = {}
       result[derivation.field] = derivation.func(@)
       @set(result, silent: true)
+


Fix to prevent loading a collection wihout an id

--- app/collections/zooniverse_subjects.coffee
+++ app/collections/zooniverse_subjects.coffee
@@ -46,6 +46,8 @@

   url: =>
     if @type is 'collection'
+      if @id is '' or @id is undefined
+        throw new Error('Must supply Collection id')
       @base(@id)
     else
       unless @user.current?


factored out _resolvePath

factored out _resolvePath

--- coffeescript/feedlyconsole.coffee
+++ coffeescript/feedlyconsole.coffee
@@ -206,43 +206,55 @@

   constructor: (@name, @path, @json_data) ->
     @children = null
-
-  @initRootNode = ->
-    unless @_ROOT_NODE?
-      @_NODES[@_ROOT_PATH] = @_ROOT_NODE = new RootFeedlyNode()
-
-  @getNode = (path, callback) ->
-    _console.debug "[feedlyconsole:FeedlyNode] looking for node at '%s'.", path
-    @initRootNode()
-    return callback @_ROOT_NODE unless path?
-    return callback @_NODES[path] if path of @_NODES
-
-    parts = getPathParts(path)
-    # If the first part of path parts isn't empty, the path is a
-    # relative path, which can be turned into an *absolutish* path
-    # by pre-pending the parts of the current pathnode.
-    parts = getPathParts(_self.pathhandler.current.path)
-    .concat(parts)  if parts[0] isnt ""
-    # At this point the path is *absolutish*, i.e. looks absolute, but
-    # all `.` and `..` mentions need to removed and resolved before it
-    # is truly absolute.
-    resolved = []
+    FeedlyNode._NODES[@path] = @
+
+  @_initRootNode: =>
+    @_ROOT_NODE ?= new RootFeedlyNode()
+
+  @_getPathParts = (path) ->
+    parts = path.split("/")
+    #remove empty trailing element
+    return parts.slice(0, parts.length - 1)  if parts[parts.length - 1] is ""
+    return parts
+
+  @_current = ->
+    return Josh.config.pathhandler.current
+
+  @_resolvePath: (path) =>
+    parts = @_getPathParts(path)
+
+    # non-empty item 0 indicates relative path
+    # when relative, prepend _current path
+    parts = @_getPathParts(@_current().path).concat(parts) if parts[0] isnt ""
+
+    # resolve `.` and `..`
+    resolved = ['']
     _.each parts, (x) ->
-      return  if x is "."
+      return if x is "."
       if x is ".."
         resolved.pop()
       else
         resolved.push x

-    absolute = resolved.join("/")
+    return resolved.join("/")
+
+  @getNode: (path, callback) =>
+    @_initRootNode()
+    _console.debug "[feedlyconsole:FeedlyNode] looking for node at '%s'.", path
+    return callback @_ROOT_NODE unless path?
+    absolute = @_resolvePath path
     _console.debug "[Josh.FeedlyConsole]path to fetch: " + absolute
-    # if get getDir returns false, use same node
-    return getDir(absolute, callback) or self.node
-
-  @getChildNodes = (node, callback) ->
+    return callback @_NODES[path] if path of @_NODES
+
+    #temporary hack
+    return callback @_ROOT_NODE
+
+
+  @getChildNodes: (node, callback) =>
+    @_initRootNode()
     node.getChildNodes callback

-  getChildNodes = (callback) ->
+  getChildNodes: (callback) ->
     # If the given node is a file node, no further work is required.
     if node.isFile
       _console.debug "[Josh.FeedlyConsole] it's a file, no children %O", node
@@ -260,15 +272,6 @@
       node.children = detailNode.children
       callback node.children

-  ###
-  #

-
-  # getDir
-  # ------
-
-  # This function function fetches the directory listing for a path
-  # on a given repo and branch.
-  ###
   getDir = (path, callback) ->
     node = undefined
     name = undefined
@@ -323,10 +326,7 @@

   # This function splits a path on `/` and removes any empty trailing element.
   ###
-  getPathParts = (path) ->
-    parts = path.split("/")
-    return parts.slice(0, parts.length - 1)  if parts[parts.length - 1] is ""
-    parts
+

   #

@@ -376,13 +376,14 @@
       FeedlyNode._ROOT_PATH,
       { cmds: FeedlyNode._ROOT_COMMANDS }

-
   getChildNodes: (callback) ->
-    unless @children
-      @children = (new FeedlyNode(name,
-        [@path, name].join('/'),
-        null) for name in FeedlyNode._ROOT_COMMANDS)
+    @children ?= (new FeedlyNode(name,
+      [@path, name].join('/'),
+      null) for name in FeedlyNode._ROOT_COMMANDS)
     return callback @children
+
+  toString: ->
+    return "#{@name} '#{@path}'"

 #//////////////////////////////////////////////////////////
 # based on josh.js:gh-pages githubconsole


fix cursor

fix cursor

--- views/double-tabareas.cjsx
+++ views/double-tabareas.cjsx
@@ -82,7 +82,7 @@
       detail:
         visible: false
     window.dispatchEvent event
-    @handleSelectLeft 0
+    @handleSelectLeft 1
   handleCtrlOrCmdTabKeyDown: ->
     @handleSelect [(@state.key[0] + 1) % 1, @state.key[1]]
   handleCtrlOrCmdNumberKeyDown: (num) ->


fix double check for all issues

fix double check for all issues

--- app/assets/javascripts/issues.js.coffee
+++ app/assets/javascripts/issues.js.coffee
@@ -38,7 +38,7 @@

   initChecks: ->
     $(".check_all_issues").click ->
-      $(".selected_issue").attr "checked", @checked
+      $(".selected_issue").prop("checked", @checked)
       Issues.checkChanged()

     $(".selected_issue").bind "change", Issues.checkChanged


Remove extraneous debugging lines from JS code.

--- app/assets/javascripts/places.js.coffee
+++ app/assets/javascripts/places.js.coffee
@@ -19,7 +19,6 @@
   # In Place form
   $('#update_external_id').click (e) ->
     e.preventDefault()
-    console.debug 'Clicked!'
     placeService = new google.maps.places.PlacesService($('#google-places-attribution')[0])
     request =
       name: $('#place_name').val()
@@ -27,7 +26,6 @@
       radius: 500
       types: ['bar']
       rankBy: google.maps.places.RankBy.PROMINENCE
-    console.debug request

     placeService.nearbySearch(request, (results, status) =>
       if status == google.maps.places.PlacesServiceStatus.OK


add style from bootstrap

add style from bootstrap

--- app/assets/javascripts/tables.js.coffee
+++ app/assets/javascripts/tables.js.coffee
@@ -3,5 +3,6 @@
 # You can use CoffeeScript in this file: http://coffeescript.org/
 jQuery ->
 	$('#data-table').dataTable
+    sDom: "<'row'<'col-xs-6'T><'col-xs-6'f>r>t<'row'<'col-xs-6'i><'col-xs-6'p>>"
     oLanguage:
       sUrl: "datatables.spanish.txt"


Switch to a TurboLinks-ready initializer

--- app/assets/javascripts/browse_everything/behavior.js.coffee
+++ app/assets/javascripts/browse_everything/behavior.js.coffee
@@ -1,4 +1,4 @@
-$(document).ready ->
+ready = ->
   context = {}
   active = null
   dialog = $('div#browse-everything')
@@ -107,3 +107,6 @@
       else
         window.setTimeout check_func, 1000
     check_func()
+
+$(document).ready(ready)
+$(document).on('page:load', ready)


JSSegmentedControl update

JSSegmentedControl update

--- JSKit/04_JSSegmentedControl.coffee
+++ JSKit/04_JSSegmentedControl.coffee
@@ -12,21 +12,19 @@

 	setValue:(@_selectedSegmentIndex)->
 		if ($(@_viewSelector+"_radio").length)
-			$("input:radio[name='"+@_objectID+"_radio'][value='"+@_selectedSegmentIndex+"']").attr("checked", "checked")
-			$(@_viewSelector+"_radio").buttonset('refresh')
+			if (@_selectedSegmentIndex < 0)
+				@addSegmentTag()
+			else
+				$("input:radio[name='"+@_objectID+"_radio'][value='"+@_selectedSegmentIndex+"']").attr("checked", "checked")
+				$(@_viewSelector+"_radio").buttonset('refresh')

 	setTextSize:(@_textSize)->

-	viewDidAppear:->
-		super()
-		if (!@_dataarray?)
-			return
-
+	addSegmentTag:->
 		tag = '
'
 		for i in [0...@_dataarray.length]
 			tag += '<input type="radio" id="'+@_objectID+'_radio_'+i+'" name="'+@_objectID+'_radio" value="'+i+'" />
'+@_dataarray[i]+'
'
 		tag += '
'
-
 		if ($(@_viewSelector+"_radio").length)
 			$(@_viewSelector+"_radio").remove()
 		$(@_viewSelector).append(tag)
@@ -39,4 +37,10 @@
 				@_selectedSegmentIndex = select
 				if (@action?)
 					@action(@_self)
+
+	viewDidAppear:->
+		super()
+		if (!@_dataarray?)
+			return
+		@addSegmentTag()
 		@setValue(@_selectedSegmentIndex)


Add specs for desc sort order.

Add specs for desc sort order.

--- spec/javascripts/application/seeds_store_spec.js.coffee
+++ spec/javascripts/application/seeds_store_spec.js.coffee
@@ -29,3 +29,14 @@
         it 'beyond scope', (done) ->
           expect(subject.readFromAll(sortBy, sortOrderAsc, 3, 2)).eventually
             .eql([{name: 4}]).notify(done)
+      describe 'desc', ->
+        sortOrderAsc = false
+        it 'from the beginning', (done) ->
+          expect(subject.readFromAll(sortBy, sortOrderAsc, 0, 1)).eventually
+            .eql([{name: 4}]).notify(done)
+        it 'in the middle', (done) ->
+          expect(subject.readFromAll(sortBy, sortOrderAsc, 1, 2)).eventually
+            .eql([{name: 3}, {name: 2}]).notify(done)
+        it 'beyond scope', (done) ->
+          expect(subject.readFromAll(sortBy, sortOrderAsc, 3, 2)).eventually
+            .eql([{name: 1}]).notify(done)


Fix some copy markdown bugs

Fix some copy markdown bugs

Type names were not in sync meaning that Header and Bullet items were
not converting to markdown.

--- lib/foldingtext-to-markdown.coffee
+++ lib/foldingtext-to-markdown.coffee
@@ -1,5 +1,5 @@
 commonmark = require 'commonmark'
-toMarkdown = require('to-markdown').toMarkdown
+toMarkdown = require('to-markdown')

 ### Rewrite all of this to first generate CommonMark AST and then generate
 HTML or Markdown from that.
@@ -45,7 +45,7 @@
     context =
       listIndex: 0
       listLevel: 0
-      headingLevel: 0
+      headerLevel: 0
     for each in outline.root.children
       @visiItem each, context, results
     results.join '\n\n'
@@ -77,14 +77,14 @@
   @visitBLOCKQUOTE: (item, context) ->
     indent(context.listLevel) + '> ' + toMarkdown item.bodyHTML

-  @visitHEADING: (item, context) ->
+  @visitHEADER: (item, context) ->
     context.listIndex = 0
     context.listLevel = 0
-    context.headingLevel++
-    repeat('#', context.headingLevel) + ' ' + toMarkdown item.bodyHTML
+    context.headerLevel++
+    repeat('#', context.headerLevel) + ' ' + toMarkdown item.bodyHTML

-  @didVisitHEADING: (item, context) ->
-    context.headingLevel--
+  @didVisitHEADER: (item, context) ->
+    context.headerLevel--

   @visitORDERED: (item, context) ->
@@ -96,13 +96,13 @@
     if context.listLevel > 0
       context.listLevel--

-  @visitUNORDERED: (item, context) ->
+  @visitBULLET: (item, context) ->
     context.listLevel++
     indent(context.listLevel - 1) + '- ' + toMarkdown item.bodyHTML

-  @didVisitUNORDERED: (item, context) ->
+  @didVisitBULLET: (item, context) ->
     if context.listLevel > 0
       context.listLevel--

 module.exports = FTToMarkdown.outlineToMarkdown.bind(FTToMarkdown)
-###+###


Cleanup IntroCtrl.Coffee

Cleanup IntroCtrl.Coffee

--- app/assets/javascripts/IntroCtrl.coffee
+++ app/assets/javascripts/IntroCtrl.coffee
@@ -2,7 +2,6 @@
 class IntroCtrl
   constructor: (@$log, @$scope, @UserControlService) ->
     @$log.debug "IntroCtrl.constructor()"
-    @foo = @UserControlService
     intros =
       home: [
           element: ".add-meeting"
@@ -42,7 +41,7 @@

   showIntroFunc: ->
     @UserControlService.getActualUser().then((user) =>
-      console.log("foooo:" + user.showIntro)
+      @$log.debug("User (#{user.email}) showIntro: #{user.showIntro}")
       @$scope.showIntro = user.showIntro
     , (error) =>
       @$log.error "Unable to get actual User: #{error}"


fix filters update url

fix filters update url

--- app/coffee/modules/base/location.coffee
+++ app/coffee/modules/base/location.coffee
@@ -30,7 +30,7 @@
         return $location

     $location.isInCurrentRouteParams = (name, value) ->
-        params = _.merge($route.current.params, $location.search())
+        params = $location.search() || {}

         return params[name] == value


Fix initialZoom

Fix initialZoom

--- app/scripts/alchemy/core/startGraph.coffee
+++ app/scripts/alchemy/core/startGraph.coffee
@@ -108,9 +108,9 @@
             alchemy[alchemy.conf.afterLoad] = true

     if alchemy.conf.initialScale isnt alchemy.defaults.initialScale
-        alchemy.interactions.zoom.scale(alchemy.conf.initialScale)
+        alchemy.interactions.zoom().scale(alchemy.conf.initialScale)
         return

     if alchemy.conf.initialTranslate isnt alchemy.defaults.initialTranslate
-        alchemy.interactions.zoom.translate(alchemy.conf.initialTranslate)
+        alchemy.interactions.zoom().translate(alchemy.conf.initialTranslate)
         return


JForeignAuth: code cleanup for create operations

--- workers/social/lib/social/models/foreignauth.coffee
+++ workers/social/lib/social/models/foreignauth.coffee
@@ -51,30 +51,34 @@
     { sessionToken, group, username } = options

     JSession = require './session'
-    JSession.fetchOAuthInfo sessionToken, (err, foreignAuthInfo) =>
+    JSession.fetchOAuthInfo sessionToken, (err, foreignData) =>
       return callback err   if err
-      return callback null  unless foreignAuthInfo
-      return callback null  unless foreignAuthInfo.session
+      return callback null  unless foreignData
+      return callback null  unless foreignData.session

-      @create foreignAuthInfo, username, (err) ->
+      { session } = foreignData
+
+      @create { foreignData, group, username }, (err) ->
         return callback err  if err

-        JSession.clearOauthInfo foreignAuthInfo.session, (err) ->
+        JSession.clearOauthInfo session, (err) ->
           return callback err  if err

           response = {}
-          { session: { returnUrl } } = foreignAuthInfo
+          { returnUrl } = session
           response.returnUrl = returnUrl  if returnUrl

           return callback null, response

-  @create = ({ foreignAuth, foreignAuthType }, username, callback) ->
+  @create = ({ foreignData, group, username }, callback) ->

-    foreignAuth   = new JForeignAuth {
-      provider    : foreignAuthType
-      foreignId   : foreignAuth.foreignId
-      foreignData : foreignAuth
+    { foreignAuthType } = foreignData
+    foreignData = foreignData.foreignAuth[foreignAuthType]
+    foreignAuth = new JForeignAuth {
+      provider  : foreignAuthType
+      foreignId : foreignData.foreignId
+      foreignData
       username
       group
     }


Social: fix not sending member change event

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -655,12 +655,7 @@
             as          : role
           ).save (err) ->
             return callback err  if err
-            JAccount.one { _id: targetId }, (err, account) ->
-              return callback err  if err
-
-              contents = { role, group: client.context.group, adminNick: client.connection.delegate.profile.nickname }
-              account.sendNotification 'MembershipRoleChanged', contents  if account
-              queue.next()
+            queue.next()

         # remove existing ones
         queue = queue.concat [
@@ -672,10 +667,22 @@
             else
               queue.next()
           ->
+            notifyAccountOnRoleChange client, targetId, roles, queue.next
+          ->
             callback null
         ]

         daisy queue
+
+  notifyAccountOnRoleChange = (client, id, roles, callback) ->
+    JAccount.one { _id: id }, (err, account) ->
+      return callback err  if err or not account
+
+      role = if roles?.length > 0 then roles[0] else 'member'
+      contents = { role, group: client.context.group, adminNick: client.connection.delegate.profile.nickname }
+      account.sendNotification 'MembershipRoleChanged', contents
+      callback null
+

   addDefaultRoles:(callback) ->
     group = this


Social: added function for checking if given email is in allowed domains of group

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -595,6 +595,16 @@
           group.addRole role, queue.fin.bind queue
         dash queue, callback

+  # isInAllowedDomain checks if given email's domain is in allowed domains
+  isInAllowedDomain:(email, callback)->
+    return no  unless @allowedDomains?.length > 0
+
+    # even if incoming email doesnt have a @ in it, whole string will be taken
+    # into consideration as domain name
+    domain = email.substring email.indexOf("@")+1 # get the part after @
+
+    return domain in @allowedDomains
+
   updatePermissions: permit 'grant permissions',
     success:(client, permissions, callback=->)->
       @fetchPermissionSet (err, permissionSet)=>


work in progress…  100% CPU bug… working on it

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -26,10 +26,13 @@
   @trait __dirname, '../../traits/taggable'
   @trait __dirname, '../../traits/protected'
   @trait __dirname, '../../traits/joinable'
+  @trait __dirname, '../../traits/slugifiable'

   @share()

   @set
+    slugifyFrom     : 'title'
+    slugTemplate    : '/#{slug}'
     feedable        : no
     memberRoles     : ['admin','moderator','member','guest']
     permissions     :
@@ -170,59 +173,67 @@
             @one {_id: targetId}, callback

   @create = do->
+
     save_ =(label, model, queue, callback)->
       model.save (err)->
         if err then callback err
         else
           console.log "#{label} is saved"
           queue.next()
+
     create = secure (client, formData, callback)->
       JPermissionSet = require './permissionset'
       JMembershipPolicy = require './membershippolicy'
       JName = require '../name'
       {delegate} = client.connection
-      JName.claim formData.slug, [formData.slug], 'JGroup', 'slug', (err)=>
-        if err then callback err
-        else
-          group                 = new this formData
-          permissionSet         = new JPermissionSet
-          defaultPermissionSet  = new JPermissionSet
-          queue = [
-            -> save_ 'group', group, queue, callback
-            -> group.addMember delegate, (err)->
-                if err then callback err
-                else
-                  console.log 'member is added'
-                  queue.next()
-            -> group.addAdmin delegate, (err)->
-                if err then callback err
-                else
-                  console.log 'admin is added'
-                  queue.next()
-            -> save_ 'permission set', permissionSet, queue, callback
-            -> save_ 'default permission set', defaultPermissionSet, queue,
-                      callback
-            -> group.addPermissionSet permissionSet, (err)->
-                if err then callback err
-                else
-                  console.log 'permissionSet is added'
-                  queue.next()
-            -> group.addDefaultPermissionSet defaultPermissionSet, (err)->
-                if err then callback err
-                else
-                  console.log 'permissionSet is added'
-                  queue.next()
-            -> group.addDefaultRoles (err)->
-                if err then callback err
-                else
-                  console.log 'roles are added'
-                  queue.next()
-          ]
-          if 'private' is group.privacy
-            queue.push -> group.createMembershipPolicy -> queue.next()
-          queue.push -> callback null, group
-
-          daisy queue
+      group                 = new this formData
+      permissionSet         = new JPermissionSet
+      defaultPermissionSet  = new JPermissionSet
+      queue = [
+        # -> group.createSlug (err, slug)->
+        #   if err then callback err
+        #   else unless slug?
+        #     callback new KodingError "Couldn't claim the slug!"
+        #   else
+        #     console.log "created a slug #{slug}"
+        #     group.slug  = slug.slug
+        #     group.slug_ = slug.slug
+        #     queue.next()
+        # -> save_ 'group', group, queue, callback
+        # -> group.addMember delegate, (err)->
+        #     if err then callback err
+        #     else
+        #       console.log 'member is added'
+        #       queue.next()
+        # -> group.addAdmin delegate, (err)->
+        #     if err then callback err
+        #     else
+        #       console.log 'admin is added'
+        #       queue.next()
+        # -> save_ 'permission set', permissionSet, queue, callback
+        # -> save_ 'default permission set', defaultPermissionSet, queue,
+        #           callback
+        # -> group.addPermissionSet permissionSet, (err)->
+        #     if err then callback err
+        #     else
+        #       console.log 'permissionSet is added'
+        #       queue.next()
+        # -> group.addDefaultPermissionSet defaultPermissionSet, (err)->
+        #     if err then callback err
+        #     else
+        #       console.log 'permissionSet is added'
+        #       queue.next()
+        # -> group.addDefaultRoles (err)->
+        #     if err then callback err
+        #     else
+        #       console.log 'roles are added'
+        #       queue.next()
+      ]
+      if 'private' is group.privacy
+        queue.push -> group.createMembershipPolicy -> queue.next()
+      queue.push -> callback null, group
+
+      daisy queue

   @findSuggestions = (client, seed, options, callback)->
     {limit, blacklist, skip}  = options
@@ -304,7 +315,7 @@
         {delegate}            = client.connection
         permissionSet         = null
         defaultPermissionSet  = null
-        queue = [
+        daisy queue = [
           => @fetchPermissionSet (err, model)->
               if err then callback err
               else
@@ -327,7 +338,6 @@
               defaultPermissions  : defaultPermissionSet.permissions
             }
         ]
-        daisy queue

   fetchMyRoles: secure (client, callback)->
     {delegate} = client.connection


gaq: WIP replace debug ga for testing events

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -53,17 +53,19 @@
     <script src='/a/js/koding.#{KONFIG.version}.js'></script>
     <script>KD.prefetchedFeeds=#{encodedFeed};</script>

+    <script type="text/javascript">
+      var _gaq = _gaq || [];
+      _gaq.push(['_setAccount', 'UA-6520910-21']);
+      _gaq.push(['_setDomainName', 'none']);

-    <!-- GOOGLE ANALYTICS -->
-    <script>
-      var _gaq = _gaq || [];
-      _gaq.push(['_setAccount', 'UA-6520910-8']);
       _gaq.push(['_trackPageview']);
+
       (function() {
         var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
-        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/u/ga_debug.js';
         var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
       })();
+
     </script>

     <!-- ROLLBAR -->


analytics: revert accidentally committed code

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -39,9 +39,11 @@

     """
     <script type="text/javascript">
+      if (location.host === "koding.com") {
         !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";
-          analytics.load("kb2hfdgf20");
+          analytics.load("#{segment}");
         }}();
+      };
     </script>

     <script>


Execute template to set HTML

Execute template to set HTML

--- app/templates/src/coffee/views/home.coffee
+++ app/templates/src/coffee/views/home.coffee
@@ -12,6 +12,6 @@

 		render: ->
 			rtpl = _.template Templates.Home
-			@$el.html rtpl
+			@$el.html rtpl()

 			@


thingy: Experimental support for definition changes.  Will now detect missing and extra subkeys and "reconcile" dictionary and definitions.

--- client/thingy.coffee
+++ client/thingy.coffee
@@ -83,6 +83,23 @@

   null

+# Send create message for a subkey.
+#
+send_create_subkey = ( thingy, subkey ) ->
+  typename  = thingy.typename()
+  id        = thingy.id()
+  type_data = @_.per_type[ typename ]
+
+  attr_keys = type_data.subkeys[ subkey ]
+  key   = subkey_key( id, subkey )
+  value = only_keys( thingy.get( attr_keys... ), attr_keys )
+  if subkey == '_'
+    value =
+      typename: typename
+      attrs: value
+  @_.dictionary.create( @_.domain, key, value )
+
+
 # Send create message for a thingy.
 #
 # @param [thingy] thingy Thingy to send create for.
@@ -94,13 +111,7 @@

   @_.dictionary.batch =>
     for subkey, attr_keys of type_data.subkeys
-      key   = subkey_key( id, subkey )
-      value = only_keys( thingy.get( attr_keys... ), attr_keys )
-      if subkey == '_'
-        value =
-          typename: typename
-          attrs: value
-      @_.dictionary.create( @_.domain, key, value )
+      @_.send_create_subkey( thingy, subkey )
     # %create tells others to create the thingy.
     @_.dictionary.create( @_.domain, '%create', id )

@@ -108,19 +119,42 @@

 # Construct a thingy.
 #
-# @param [String] typename   Name of thingy type.
-# @param [String] id         ID.
-# @param [Object] attrs      Initial attributes.
-# @param [Object] local_data Local data, if any.
+# @param [String]        typename   Name of thingy type.
+# @param [String]        id         ID.
+# @param [Object]        attrs      Initial attributes.
+# @param [Object]        local_data Local data, if any.
+# @param [Array<String>] subkeys    Which subkeys were received. null
+#   for local creation.
 # @return [thingy] New thingy.
 # @throw [String] If `typename` isn't known.
 # @throw [String] If `id` already exists.
-make_thingy = ( typename, id, attrs, local_data ) ->
+make_thingy = ( typename, id, attrs, local_data, subkeys = null ) ->
   if ! typename?
     @_.on_error( "Missing typename for thingy: #{id}" )
   @_.make_guard = true
   thingy = new Heron.Thingy( this, typename, id, attrs, local_data )
   @_.make_guard = false
+
+  # Subkey reconciliation.  Need to create any subkeys in definition but not
+  # subkeys and remove any in subkeys but not definition.
+  if subkeys?
+    real_subkeys = {}
+    real_subkeys[ subkey ] = true for subkey of @_.per_type[ typename ].subkeys
+    to_delete = []
+    for subkey in subkeys
+      if real_subkeys[ subkey ]
+        delete real_subkeys[ subkey ]
+      else
+        to_delete.push( subkey )
+    to_push = Heron.Util.keys( real_subkeys )
+
+    @_.dictionary.batch( =>
+      for subkey in to_delete
+        @_.dictionary.delete( @domain(), subkey )
+      for subkey in to_push
+        @_.send_create_subkey( thingy, subkey )
+    )
+
   thingy

 # Create all partials.
@@ -128,8 +162,9 @@
 # @return [null] null
 flush_partials = ->
   for id, info of @_.partials
-    @_.make_thingy( info.typename, id, info.attrs, false )
+    @_.make_thingy( info.typename, id, info.attrs, false, @_.partials_subkeys[ id ] )
   @_.partials = {}
+  @_.partials_subkeys = {}
   null

 # Receiver class.
@@ -177,15 +212,22 @@
           @_.on_error( "Missing everything for thingy: #{id}" )
         else
           if @_.ready
-            @_.make_thingy( info.typename, id, info.attrs, false )
+            @_.make_thingy( info.typename, id, info.attrs, false, @_.partials_subkeys[ id ] )
           else
             @_.partials_to_create[ id ] = true
         delete @_.partials[ id ]
+        delete @_.partials_subkeys[ id ]
     else if key[0] != '_'
       # Extract id and subkey from key.
       [ id, subkey ] = split_key( key )
+      if @_.per_thingy[ id ]?
+        # Ignore creates for already existant thingies.  Might be part of
+        # subkey reconciliation.
+        return
+
       attrs = value
       @_.partials[ id ] ?= {}
+      (@_.partials_subkeys[ id ] ?= []).push( subkey )
       if subkey == '_'
         if ! value.typename?
           @_.on_error( "Cannot understand basekey #{key}.  No typename." )
@@ -265,6 +307,9 @@

       # Else Discard.
       return null
+
+    # If not base key, ignore.
+    return null if subkey != '_'

     thingy_data.delegate.remove( thingy_data.thingy, false )
     delete @_.per_thingy[ id ]
@@ -617,6 +662,13 @@
       # Map of id to typename and attrs.
       partials: {}

+      # This stores the subkeys of each partial.  It is used to reconcile
+      # dictionary subkeys with definitions subkeys when the definitions
+      # change.
+      #
+      # Map of id to array of subkeys.
+      partials_subkeys: {}
+
       # When thingyverse is not ready, this acts as a set of which partials
       # are ready to be created.  When thingyverse is ready, this is not
       # used.
@@ -627,8 +679,10 @@
         send_update_subkey.call( this, thingy, subkey )
       send_create: ( thingy ) =>
         send_create.call( this, thingy )
-      make_thingy: ( typename, id, domain, attrs, local_data ) =>
-        make_thingy.call( this, typename, id, domain, attrs, local_data )
+      send_create_subkey: ( thingy, subkey ) =>
+        send_create_subkey.call( this, thingy, subkey )
+      make_thingy: ( typename, id, domain, attrs, local_data, subkeys ) =>
+        make_thingy.call( this, typename, id, domain, attrs, local_data, subkeys )
       flush_partials: =>
         flush_partials.call( this )

@@ -775,8 +829,9 @@
       info = @_.partials[ id ]
       if ! info?
         throw "Insanity error: Partial to create but no info!"
-      @_.make_thingy( info.typename, id, info.attrs, false )
+      @_.make_thingy( info.typename, id, info.attrs, false, @_.partials_subkeys[ id ] )
       delete @_.partials[ id ]
+      delete @_.partials_subkeys[ id ]
       delete @_.partials_to_create[ id ]

     this


mount/unmount with blackout

mount/unmount with blackout

--- resources/assets/lib/modal.coffee
+++ resources/assets/lib/modal.coffee
@@ -38,12 +38,15 @@
     $(document).on 'turbolinks:before-cache.modal', () =>
       roots[0].removeChild @portal

+    Blackout.toggle(@props.visible, 0.5)
+

   componentDidUpdate: (prevProps) =>
     Blackout.toggle(@props.visible, 0.5) unless prevProps.visible == @props.visible

   componentWillUnmount: =>
+    Blackout.toggle(false)
     document.removeEventListener 'keydown', @handleEsc
     $(document).off '.modal'


fix(HomeCtrl): refresh the scrollspy on remove, not only on upload/download

--- app/scripts/controllers/HomeCtrl.coffee
+++ app/scripts/controllers/HomeCtrl.coffee
@@ -35,6 +35,17 @@
 				callback()
 			, throttledApply

+		$scope.removeLocal = ( item, callback = -> ) ->
+			audio.removeLocal item, $scope.localPath, $scope.localPath, ->
+				$scope.$apply()
+				$( "body" ).scrollspy "refresh"
+				callback()
+		$scope.removeLocal = ( item, callback = -> ) ->
+			audio.removeRemote item, $scope.localPath, ->
+				$scope.$apply()
+				$( "body" ).scrollspy "refresh"
+				callback()
+
 		$scope.isSyncing = no
 		$scope.syncDown = ( callback = -> ) ->
 			unless $scope.isSyncing
@@ -51,7 +62,7 @@
 				removeLocalRecursive = ->
 					next = audio.getFirst "localShouldRemove"
 					if next?
-						audio.removeLocal next, $scope.localPath, removeLocalRecursive
+						$scope.removeLocal next, removeLocalRecursive
 					else
 						downloadRecursive()

@@ -73,7 +84,7 @@
 				removeRemoteRecursive = ->
 					next = audio.getFirst "remoteShouldRemove"
 					if next?
-						$scope.upload next, removeRemoteRecursive
+						$scope.removeRemote next, removeRemoteRecursive
 					else
 						uploadRecursive()


office badges

office badges

--- assets/javascripts/views/interactives/spending_badge.coffee
+++ assets/javascripts/views/interactives/spending_badge.coffee
@@ -29,7 +29,7 @@
   render: ->
     """

-
+

#{this.model.get('name')}

$#{POV.Utilities.formatDollars(this.model.get('total_spent') || 0)}


Keyboard extends Sprite

Keyboard extends Sprite

This way, a keyboard is just another Turtle that you can move around
on the screen just like any other turtle, for fun and nice visual
effects.

--- site/top/lib/keyboard.coffee
+++ site/top/lib/keyboard.coffee
@@ -30,24 +30,43 @@

 ###

-class Keyboard
+class Keyboard extends Sprite
   constructor: (options = {}) ->
-    defwidth = if options.kh? then (options.kh / 5) else 10
+    defwidth = if options.kh?
+        (options.kh / 5)
+      else
+        10
     @kw = options.kw ? defwidth
     @kh = options.kh ? @kw * 5
     @bkw = options.bkw ? @kw * 3 / 5
     @bkh = options.bkh ? @kh * 3 / 5
     @lowest = options.lowest ? 21
     @highest = options.highest ? 108
-    @x = options.x ? 0
-    @y = options.y ? 0
-    @center = floor((@lowest + @highest) / 2)
+    @horiz = @calcHoriz()
     @kt = new Turtle
     @kt.ht()
     @kt.speed Infinity
-    @pt = new Turtle
-    @pt.ht()
+    @pt = this
+    # Render the keyboard as its own sprite
+    # that can be moved around.
+    super
+      width: @horiz.right - @horiz.left + 2
+      height: @kh + 2
+    @kt.drawon this
     do @drawall
+
+  calcHoriz: () ->
+    leftmost = Keyboard.wcp(@lowest)
+    rightmost = Keyboard.wcp(@highest) + 1
+    # if the highest key is a black key
+    # then go right one more space
+    if (Keyboard.keyshape(@highest) >= 8)
+      rightmost += 1
+    return {
+      left: leftmost * @kw
+      right: rightmost * @kw
+      mid: (leftmost + rightmost) * @kw / 2
+    }

   # white-key position: gives a number
   # that increases by 1 for every white
@@ -70,8 +89,9 @@
     if (args.length <= 0 or
         not $.isPlainObject(args[0]))
       args.unshift({})
-    args[0].callback = @makecallback()
-    @pt.play.apply(@pt, args)
+    args[0].callback = @makecallback(
+      args[0].callback)
+    Sprite.prototype.play.apply(@pt, args)

   draw: (n, fillcolor) ->
     if not (@lowest <= n <= @highest)
@@ -91,7 +111,7 @@
     for n in [@lowest..@highest]
       @draw(n)

-  makecallback: ->
+  makecallback: (cb2) ->
     pressed = []
     convertfreq = (f) ->
       round(69 + ln(f / 440) * 12 / ln(2))
@@ -104,14 +124,17 @@
         midikey = convertfreq f
         pressed.push midikey
         @draw midikey, Keyboard.keycolor midikey
+      if cb2
+        cb2(d)
     return callback

   keyoutline: (kt, n) ->
     # TODO: replace this with high-performance
     # canvas drawing code.
-    startx = @kw *
-      (Keyboard.wcp(n) - Keyboard.wcp(@center))
+    startx = @kw * Keyboard.wcp(n) - @horiz.mid
     starty = -@kh / 2
+    kt.jumpto this
+    kt.turnto this.direction()
     # top width of the c key
     ckw = (3 * @kw - 2 * @bkw) / 3
     # top width of the f key
@@ -134,7 +157,7 @@
         startx += 2 * fkw + @bkw - @kw
       when 12
         startx += 3 * fkw + 2 * @bkw - 2 * @kw
-    kt.jumpto startx, starty
+    kt.jump startx, starty
     switch ks
       when 1
         kt.fd @kh
@@ -237,5 +260,5 @@
         kt.bk @bkh
         kt.slide -@bkw

+# Export the Keyboard class as a global.
 window.Keyboard = Keyboard
-


Remove debug msg.

Remove debug msg.

--- app/client/js/storage.coffee
+++ app/client/js/storage.coffee
@@ -26,8 +26,8 @@

     @pendingStores = new goog.structs.Set
     # NOTE(steida): It saves traffic and prevents race conditions for
-    # localStorage store event. Not sure why, probably because get and set
-    # methods are not called in one transaction.
+    # localStorage store event, probably because get and set methods are not
+    # called in transaction.
     @savePendingStoresThrottled = new goog.async.Throttle @savePendingStores,
       Storage.THROTTLE_MS, @

@@ -65,8 +65,6 @@
     @param {app.Store} store
   ###
   onStoreChange: (store) ->
-    if goog.DEBUG
-      console.log 'app.Storage onStoreChange called'
     @pendingStores.add store
     @savePendingStoresThrottled.fire()
     @notify()


:shirt:Fix courses view linter warning

:shirt:Fix courses view linter warning

warn: Linting of app/views/courses/mock1/CoursesView.coffee: warn:
ensure_comprehensions at line 72.

May be a linter bug, have filed an issue here:
https://github.com/clutchski/coffeelint/issues/482

--- app/views/courses/mock1/CoursesView.coffee
+++ app/views/courses/mock1/CoursesView.coffee
@@ -69,7 +69,8 @@
     $('#continueModal').modal('hide')
     courseID = $(e.target).data('course-id')
     instanceName = $('.select-session').val()
-    instanceID = index for val, index in @instances when val.name is instanceName
+    for val, index in @instances when val.name is instanceName
+      instanceID = index
     viewClass = require 'views/courses/mock1/CourseDetailsView'
     viewArgs = [{}, courseID, instanceID]
     navigationEvent = route: "/courses/mock1/#{courseID}", viewClass: viewClass, viewArgs: viewArgs


Fixed translation, I think

Fixed translation, I think

--- app/views/play/modal/SubscribeModal.coffee
+++ app/views/play/modal/SubscribeModal.coffee
@@ -50,7 +50,7 @@
     @playSound 'menu-button-click'
     application.tracker?.trackEvent 'Started subscription purchase', {}
     stripeHandler.open({
-      description: $.i18n.t 'subscribe.stripe_description'
+      description: $.i18n.t('subscribe.stripe_description')
       amount: @product.amount
     })


bump version to 1.1-pre

bump version to 1.1-pre

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -20,7 +20,7 @@
   require.registerExtension '.coffee', (content) -> compile content

 # The current CoffeeScript version number.
-exports.VERSION = '1.0.1-pre'
+exports.VERSION = '1.1-pre'

 # Words that cannot be used as identifiers in CoffeeScript code
 exports.RESERVED = RESERVED


CoffeeScript 1.8.0

CoffeeScript 1.8.0

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -12,7 +12,7 @@
 SourceMap     = require './sourcemap'

 # The current CoffeeScript version number.
-exports.VERSION = '1.7.1'
+exports.VERSION = '1.8.0'

 exports.FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']


improved comment (minor)

improved comment (minor)

--- coffee/velbus.coffee
+++ coffee/velbus.coffee
@@ -66,7 +66,7 @@
       return

   write_to_serial: (data, repeated = false) =>
-    # make sure there is at least 30 ms between each two requests
+    # make sure there is at least 30 ms between each two packets sent
     if (wait = (Date.now() - @last_send - 30)) < 0
       setTimeout (
         =>


Block translations to en-US or en-GB fields (#638)

--- app/views/i18n/I18NEditModelView.coffee
+++ app/views/i18n/I18NEditModelView.coffee
@@ -168,6 +168,11 @@
       return UNSAVED_CHANGES_MESSAGE

   onSubmitPatch: (e) ->
+    # Added due to high volume of translations getting set on english fields.
+    if @selectedLanguage in ['en-US', 'en-GB', 'en']
+      alert("Blocked change to #{@selectedLanguage} field. Please check your language setting.")
+      return
+
     delta = modelDeltas.getDeltaWith(@originalModel, @model)
     flattened = deltasLib.flattenDelta(delta)
     collection = _.string.underscored @model.constructor.className


Let's just show 200 leaderboard entries for now until we have paging.

--- app/views/play/ladder/ladder_tab.coffee
+++ app/views/play/ladder/ladder_tab.coffee
@@ -55,7 +55,8 @@
 class LeaderboardData
   constructor: (@level, @team, @session) ->
     _.extend @, Backbone.Events
-    @topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: if @session then 10 else 20})
+    limit = 200 # if @session then 10 else 20  # We need to figure out paging.
+    @topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: limit})
     @topPlayers.fetch()
     @topPlayers.comparator = (model) ->
       return -model.get('totalScore')


[ES] added elastic search configuration schema

--- config/config.schema.coffee
+++ config/config.schema.coffee
@@ -113,6 +113,11 @@
       watch               : 1
       cronSchedule        : 1
       usageLimitInMinutes : 1
+    elasticSearch         :
+      host                : 1
+      port                : 1
+      enabled             : 1
+      queue               : 1
     emailConfirmationCheckerWorker :
       enabled             : 1
       login               : 1


Remove a console.log

Remove a console.log

--- admin/static/coffee/sidebar.coffee
+++ admin/static/coffee/sidebar.coffee
@@ -182,7 +182,6 @@
             'click a[rel=log_details]': 'show_popover'

         show_popover: (event) =>
-            console.log 'click on log'
             event.preventDefault()
             @.$(event.currentTarget).popover('show')
             $popover = $('.popover')


Remove -> Delete comment language (#6770)

* allow members to export polls and discussions

* use 'delete' rather than remove on delete comment

--- vue/src/shared/services/comment_service.coffee
+++ vue/src/shared/services/comment_service.coffee
@@ -75,7 +75,7 @@

     discard_comment:
       icon: 'mdi-delete'
-      name: 'common.action.remove'
+      name: 'common.action.delete'
       canPerform: -> AbilityService.canDiscardComment(comment)
       perform: ->
         openModal
@@ -84,10 +84,10 @@
             confirm:
               submit: -> comment.discard()
               text:
-                title: 'discard_comment_dialog.title'
-                helptext: 'discard_comment_dialog.question'
-                confirm: 'discard_comment_dialog.title'
-                flash: 'comment_form.messages.removed'
+                title: 'delete_comment_dialog.title'
+                helptext: 'delete_comment_dialog.question'
+                confirm: 'delete_comment_dialog.confirm'
+                flash: 'comment_form.messages.destroyed'

     undiscard_comment:
       icon: 'mdi-delete-restore'
@@ -95,17 +95,17 @@
       canPerform: -> AbilityService.canUndiscardComment(comment)
       perform: -> comment.undiscard()

-    delete_comment:
-      icon: 'mdi-delete'
-      canPerform: -> AbilityService.canDeleteComment(comment)
-      perform: ->
-        openModal
-          component: 'ConfirmModal',
-          props:
-            confirm:
-              submit: -> comment.destroy()
-              text:
-                title: 'delete_comment_dialog.title'
-                helptext: 'delete_comment_dialog.question'
-                confirm: 'delete_comment_dialog.confirm'
-                flash: 'comment_form.messages.destroyed'
+    # delete_comment:
+    #   icon: 'mdi-delete'
+    #   canPerform: -> AbilityService.canDeleteComment(comment)
+    #   perform: ->
+    #     openModal
+    #       component: 'ConfirmModal',
+    #       props:
+    #         confirm:
+    #           submit: -> comment.destroy()
+    #           text:
+    #             title: 'delete_comment_dialog.title'
+    #             helptext: 'delete_comment_dialog.question'
+    #             confirm: 'delete_comment_dialog.confirm'
+    #             flash: 'comment_form.messages.destroyed'


Lower precision to boost FPS

Lower precision to boost FPS

--- coffee/scene.coffee
+++ coffee/scene.coffee
@@ -24,7 +24,7 @@
       up: false
       down: false

-    @renderer = new THREE.WebGLRenderer()
+    @renderer = new THREE.WebGLRenderer(precision: 'lowp')
     @renderer.setSize(window.innerWidth, window.innerHeight)
     @renderer.setClearColor(0x7EC0EE, 1)


Moved atom.loadPackage to a spec-only helper function

--- spec/command-palette-spec.coffee
+++ spec/command-palette-spec.coffee
@@ -8,7 +8,7 @@

   beforeEach ->
     rootView = new RootView(require.resolve('fixtures/sample.js'))
-    atom.loadPackage("command-palette")
+    window.loadPackage("command-palette")
     rootView.attachToDom().focus()
     rootView.trigger 'command-palette:toggle'
     palette = rootView.find('.command-palette').view()


selector: don't emit an invalid CSS selector

--- client/Framework/components/inputs/KDInputRadioGroup.coffee
+++ client/Framework/components/inputs/KDInputRadioGroup.coffee
@@ -51,7 +51,7 @@
     # @getDomElement().find("input[value='#{value}']").parent().addClass('checked')
     @getDomElement().find("input[value='#{value}']").attr "checked","checked"
     @getDomElement().find(".kd-radio-holder").removeClass 'active'
-    @getDomElement().find(".kd-radio-holder.#{value}").addClass 'active'
+    @getDomElement().find(".kd-radio-holder.#{value}").addClass 'active'  if value

   getInputElements:->
     @getDomElement().find('input')


activity: focus and resize comment edit input after view element added to DOM

--- client/Social/Activity/views/comments/newcommentform.coffee
+++ client/Social/Activity/views/comments/newcommentform.coffee
@@ -93,7 +93,12 @@

     @commentInput.setValue Encoder.htmlDecode data.body
     @commentInput.on "EscapePerformed", @bound "cancel"
-    @commentInput.setFocus()

   cancel: ->
     @getDelegate().emit "CommentUpdateCancelled"
+
+  viewAppended: ->
+    super
+    KD.utils.defer =>
+      @commentInput.setFocus()
+      @commentInput.resize()


Teamwork: Bot messages are now just between user and bot, they don't broadcasting anymore.

--- client/landing/CollaborativeWorkspace/panes/chatpane.coffee
+++ client/landing/CollaborativeWorkspace/panes/chatpane.coffee
@@ -19,6 +19,10 @@
     @on "NewChatItemCreated", =>
       @checkEmbeddableContent()

+    @on "NewChatItemPosted", =>
+      @input.setValue ""
+      @input.setFocus()
+
   bindRemoteEvents: ->
     @chatRef.on "child_added", (snapshot) =>
       unless @isVisible() or @getOptions().floating
@@ -34,12 +38,14 @@
     @messages     = new KDView cssClass : "messages"
     @input        = new KDHitEnterInputView
       placeholder : "Type your message and hit enter"
-      callback    : =>
-        @sendMessage
-          message: @input.getValue()
-          by     : KD.nick()
-        @input.setValue ""
-        @input.setFocus()
+      callback    : @bound "createMessage"
+
+  createMessage: ->
+    @sendMessage
+      message: @input.getValue()
+      by     : KD.nick()
+
+    @emit "NewChatItemPosted"

   createDock: ->
     @dock        = new KDView


landing: added default value for recoverform

--- client/landing/site.landing/coffee/login/recoverform.coffee
+++ client/landing/site.landing/coffee/login/recoverform.coffee
@@ -1,4 +1,5 @@
-kd = require 'kd.js'
+kd                  = require 'kd.js'
+utils               = require './../core/utils'
 LoginViewInlineForm = require './loginviewinlineform'
 LoginInputView      = require './logininputview'

@@ -7,11 +8,16 @@
   constructor:->

     super
+
+    { invitation, signup } = utils.getTeamData()
+    email = signup?.email or invitation?.email
+
     @usernameOrEmail = new LoginInputView
       inputOptions    :
         name          : "email"
         placeholder   : "Email address"
         testPath      : "recover-password-input"
+        defaultValue  : email
         validate      :
           container   : this
           rules       :


Add spec for re-registering schema after unloading package

--- spec/package-manager-spec.coffee
+++ spec/package-manager-spec.coffee
@@ -154,7 +154,6 @@

     it "registers the config schema in the package's metadata, if present", ->
       pack = atom.packages.loadPackage("package-with-json-config-schema")
-
       expect(atom.config.getSchema('package-with-json-config-schema')).toEqual {
         type: 'object'
         properties: {
@@ -164,6 +163,18 @@
       }

       expect(pack.mainModule).toBeNull()
+
+      atom.packages.unloadPackage('package-with-json-config-schema')
+      atom.config.clear()
+
+      pack = atom.packages.loadPackage("package-with-json-config-schema")
+      expect(atom.config.getSchema('package-with-json-config-schema')).toEqual {
+        type: 'object'
+        properties: {
+          a: {type: 'number', default: 5}
+          b: {type: 'string', default: 'five'}
+        }
+      }

     describe "when a package does not have deserializers, view providers or a config schema in its package.json", ->
       beforeEach ->


Fix spec

Fix spec

--- spec/panel-container-spec.coffee
+++ spec/panel-container-spec.coffee
@@ -3,7 +3,7 @@
 PanelContainer = require '../src/panel-container'

 describe "PanelContainer", ->
-  [container] = []
+  [container, viewRegistry] = []

   class TestPanelItem
     constructior: ->


Remove the artificial delay on login

Remove the artificial delay on login

--- server/packages/user/login.coffee
+++ server/packages/user/login.coffee
@@ -27,40 +27,19 @@
 				deferred = Promise.defer()
 				passport.authenticate('local', deferred.callback) req, res, fn

-				Promise.settle([
+				# Log the user in (if it exists), and redact it for the
+				# response.
+				deferred.promise.bind({}).spread((@user, info) ->
+					throw errors.instantiate 'login' unless @user

-					# Log the user in (if it exists), and redact it for the
-					# response.
-					deferred.promise.bind({}).spread((@user, info) ->
-						throw errors.instantiate 'login' unless @user
-
-						(Promise.promisify req.login, req) @user
-
-					).then -> @user.redactFor @user
+					Promise.promisify(req.login, req) @user

-					# If no user is found, the rejection will happen a lot
-					# sooner than if one is; password hashing must be done in
-					# the latter case. We'll create an artificial floor of 1
-					# second to make it harder for an attacker to tell if a
-					# valid username was used.
-					# `TODO`: Remove this. We'll eventually disallow
-					# registration conflicts.
-					new Promise (resolve, reject) -> setTimeout resolve, 1000
-
-				]).then ([userPromiseInspector]) ->
-
-					if userPromiseInspector.isFulfilled()
-						userPromiseInspector.value()
-					else
-						throw userPromiseInspector.error()
+				).then -> @user.redactFor @user
+
+# } Using nodeify here crashes the app. It may be a bluebird bug.

-# TODO: Using nodeify here crashes the app. It may be a bluebird bug.
-
-#		loginPromise.nodeify fn
-		loginPromise.then(
-			(result) -> fn null, result
-			(error) -> fn error
-		)
+		loginPromise.then((user) -> fn null, user
+		).catch fn

 # ## Implements hook `transmittableError`
 exports.$transmittableError = clientModule.$transmittableError


More spaced image for Makrdow upload

More spaced image for Makrdow upload

--- controls/Markdown.jsx.coffee
+++ controls/Markdown.jsx.coffee
@@ -41,7 +41,8 @@
       node.style.height = "#{node.scrollHeight}px"

   appendMarkdown: (markdown) ->
-    @props.onDataChanged(@props.dataKey, @props.data + "\n\n" + markdown + "\n\n")
+    value = (@props.data ? "") + "\n\n" + markdown + "\n\n"
+    @props.onDataChanged(@props.dataKey, value)
     if @refs.textarea
       @refs.textarea.getDOMNode().focus()


Disable clients midi signal

Disable clients midi signal

--- jarkle/client/webgl_synth.coffee
+++ jarkle/client/webgl_synth.coffee
@@ -4,6 +4,7 @@

 class @WebGlSynth
   constructor: (@schema, el, noteMap, @pubSub) ->
+    Session.set 'infoMessage', null
     window.AudioContext = window.AudioContext or window.webkitAudioContext
     @synth = new Synth(new AudioContext(), noteMap, pubSub, @schema)
     @webGLVis = new WebGLVisualisation(el, window.innerWidth,
@@ -40,6 +41,11 @@
         Session.set 'infoMessage', null
         @pubSub.trigger CURRENT_PLAYER, nextPlayer
       when 'E'
+        Session.set 'infoMessage', null
+        @currentPlayerId = 'only masters'
+        @pubSub.trigger CURRENT_PLAYER,
+          _id: @currentPlayerId
+      when 'F'
         @synth.stopAll()
         @webGLVis.stopAll()


:bug:Show practice course progress dot as yellow if incomplete

--- app/lib/coursesHelper.coffee
+++ app/lib/coursesHelper.coffee
@@ -179,7 +179,7 @@
               courseProgress[userID].started ||= false unless isOptional #no-op
               courseProgress[userID].completed = false unless isOptional
               courseProgress[levelID].started ||= false #no-op
-              courseProgress[levelID].completed = false unless isOptional
+              courseProgress[levelID].completed = false
               courseProgress[levelID][userID].started = false
               courseProgress[levelID][userID].completed = false


Clear selection every time you change of folder.

--- src/FileManager/Selection.coffee
+++ src/FileManager/Selection.coffee
@@ -1,6 +1,6 @@
 angular.module('fileManager').
-factory('Selection', ()->
-  return class Selection
+factory('Selection', ($rootScope)->
+  class Selection
     @_files: {}
     @number: 0

@@ -62,4 +62,9 @@
         if file.isFolder()
           return true
       return false
+
+  $rootScope.$on('$stateChangeSuccess', ->
+    Selection.clear()
+  )
+  return Selection
 )


:new: [linter-behavior-spec] Add markers behavior spec

--- spec/linter-behavior-spec.coffee
+++ spec/linter-behavior-spec.coffee
@@ -6,6 +6,11 @@
     event = document.createEvent('HTMLEvents');
     event.initEvent(name, true, false);
     el.dispatchEvent(event);
+
+  getLinter = ->
+    return {grammarScopes: ['*'], lintOnFly: false, modifiesBuffer: false, scope: 'project', lint: -> }
+  getMessage = (type, filePath) ->
+    return {type, text: "Some Message", filePath, range: [[0, 0], [1,1]]}

   beforeEach ->
@@ -29,3 +34,13 @@
       expect(linter.views.panel.getVisibility()).toBe(false)
       trigger(bottomContainer.getTab('File'), 'click')
       expect(linter.views.panel.getVisibility()).toBe(true)
+
+  describe 'Markers', ->
+    it 'automatically marks files when they are opened if they have any markers', ->
+      provider = getLinter()
+      messages = [getMessage('Error', '/etc/passwd')]
+      linter.setMessages(provider, messages)
+      waitsForPromise ->
+        atom.workspace.open('/etc/paswd').then ->
+          activeEditor = atom.workspace.getActiveTextEditor()
+          expect(activeEditor.getMarkers().length).toBe(1)


Fix merge problems (wrong names or classes)

--- app/src/dongle/dongle.coffee
+++ app/src/dongle/dongle.coffee
@@ -279,7 +279,7 @@
       d.promise

   lock: () ->
-    if @_currentState isnt ledger.wallet.States.BLANK and @_currentState isnt ledger.wallet.States.FROZEN and @_currentState?
+    if @state isnt ledger.dongle.States.BLANK and @state?
       @_setState(States.LOCKED)

   ###


manyboxplots2: Remove trailing whitespace

--- manyboxplots2/manyboxplots2.coffee
+++ manyboxplots2/manyboxplots2.coffee
@@ -24,7 +24,7 @@
       topylim[1] = x if x > topylim[1]
   topylim[0] = Math.floor(topylim[0])
   topylim[1] = Math.ceil(topylim[1])
-
+
   # y-axis limits for bottom figure
   botylim = [0, data.counts[0][1]]
   for i of data.counts
@@ -391,7 +391,7 @@
       text += ", "
     text += "#{q*100}"
   text += " percentiles for each of #{data.ind.length} distributions.\n"
-
+
   d3.select("div#legend").append("p")
     .text(text)


Add load_models to bulk_save's dependency list

--- bokehjs/src/coffee/common/bulk_save.coffee
+++ bokehjs/src/coffee/common/bulk_save.coffee
@@ -4,7 +4,8 @@
   "jquery",
   "require",
   "./base",
-], (_, $, require, base) ->
+  "./load_models",
+], (_, $, require, base, load_models) ->

   bulk_save = (models) ->
     ##FIXME:hack


Annotate ._isResourceRelated() as private.

--- src/OVirtResponseHydrator.coffee
+++ src/OVirtResponseHydrator.coffee
@@ -268,6 +268,8 @@
   #
   # @return [Boolean] whether specified subject is a resource or resource link
   #
+  # @private
+  #
   _isResourceRelated: (subject) ->
     return no unless @isLink subject
     attributes = @_getAttributes subject


Tolerate missing etherpad api key for tests

--- config/config.coffee
+++ config/config.coffee
@@ -6,8 +6,12 @@
 fs = require 'fs'
 SECRET = fs.readFileSync(__dirname + "/secrets/SECRET.txt", 'utf-8').trim()
 API_KEY = fs.readFileSync(__dirname + "/secrets/API_KEY.txt", 'utf-8').trim()
-ETHERPAD_API_KEY = fs.readFileSync(
-  __dirname + "/../vendor/etherpad-lite/APIKEY.txt", 'utf-8').trim()
+try
+  ETHERPAD_API_KEY = fs.readFileSync(
+    __dirname + "/../vendor/etherpad-lite/APIKEY.txt", 'utf-8').trim()
+catch e
+  console.error "Etherpad API key not found.  Etherpad will not be available."
+  ETHERPAD_API_KEY = ""
 domains = require './domains'
 email_config = require './email'
 base_url = domains.front_end_url


Refactoring and docs

Refactoring and docs

--- app/assets/javascripts/ajax.js.coffee
+++ app/assets/javascripts/ajax.js.coffee
@@ -3,6 +3,10 @@
 # Helper functions ###########################
 element_exists = (pattern) ->
   $(pattern).length > 0
+
+debug_json = (json) ->
+  console.debug(JSON.stringify(json, undefined, 2))
+

 # Return the element representing the given project
@@ -27,7 +31,7 @@
       window.location = $(@).parent().data('url')

-# Toggle the spinning / non-spinning state of the project
+# Set the spinning / non-spinning state of the project
 # refresh button
 set_icon = (project_id, is_working) ->
   button = $("#test-button-#{project_id}")
@@ -46,18 +50,19 @@
   if percent < 15  # Even if it's zero, we want to see some
     percent = 15   # indication of activity

-  bar.css('width', "#{percent}%")
-  if percent == 100
+  bar.css('width', "#{percent}%")  # Set the actual state
+  if percent == 100                # Make its appearance not jarring
     container.fadeOut(1300)
   else
     container.fadeIn(1000)

+# True if the project has been updated on the server, and the
+# on-screen representation is out of date.
 project_is_updated = (p_id, tested_at) ->
-  server_time = tested_at
-  project     = project_container(p_id)
-  return if ! project
-  project.data('tested-at') < server_time
+  project = project_container(p_id)
+  return false if ! project
+  project.data('tested-at') < tested_at

 update_project_tile = (p_id) ->
@@ -86,20 +91,20 @@
       $("#project-page-#{p_id}").replaceWith(html)
       # Re-configure javascript events
       $("#project-page-#{p_id} abbr.timeago").timeago();
-      add_click_action_to_test_button()
+      add_click_to_test_buttons()
       )

 do_poll = ->
+  # The page controls polling by setting or not setting
+  # the data-api-query value.
   query  = $('#api-query').data('api-query')
   prefix = $('#path-prefix').data('path-prefix')

-  # The page controls polling by setting or not setting
-  # the api-query value.
   if query
     $.post(prefix + '/ajax/' + query)
       .done( (data) ->
-        console.debug(JSON.stringify(data, undefined, 2))
+        debug_json(data)

         # Update activity indicators
         unless $("#server-status").hasClass('fa-signal')
@@ -132,7 +137,7 @@
     delete window.think200_is_polling

-add_click_action_to_test_button = ->
+add_click_to_test_buttons = ->
   $('.test-button').click (e) ->
     e.stopPropagation()
     $('body').focus()
@@ -146,7 +151,7 @@

 ready = ->
     add_click_to_project_tiles()
-    add_click_action_to_test_button()
+    add_click_to_test_buttons()

     $('.project-tile').hover ->
       $(@).toggleClass( 'project-tile-active' )


fixing comment bug

fixing comment bug

--- app/assets/javascripts/patient.coffee
+++ app/assets/javascripts/patient.coffee
@@ -74,9 +74,11 @@
     for vital in @json['vital_signs']
       new hQuery.Result vital

+  ###*
   @returns {Array} A list of {@link Immunization} objects
   ###
+  ###*
   immunization: ->
     for immunization in @json['immunization']
       new hQuery.immunization
-  ###*
+  ###


select seller form fields on focus

select seller form fields on focus

--- app/assets/javascripts/sellers.coffee
+++ app/assets/javascripts/sellers.coffee
@@ -8,6 +8,13 @@
     number = parseInt(text)
     window.seller_list[number] && window.seller_list[number][0]

+SellersController = Paloma.controller "Sellers"
+SellersController.prototype.new =
+SellersController.prototype.edit = ->
+  jQuery ->
+    $(".field input").each ->
+      $(this).focus(-> $(this).select())
+    $("#seller_activities_attributes_0_actual_count").focus()

 jQuery ->
   window.seller_list = $("#seller_list").data("list")


beacon_map load after window load - fix load syntax

--- app/assets/javascripts/actions/beacons_map.js.coffee
+++ app/assets/javascripts/actions/beacons_map.js.coffee
@@ -16,7 +16,7 @@
       size: 'small'
   if $("#beacons-map").length > 0
     console.log $("#beacons-map").length
-    map = new MicelloMap('rrVDMQe2qjBRuJP5dXv59kAeK77SD7','beacons-map')
+    #map = new MicelloMap('rrVDMQe2qjBRuJP5dXv59kAeK77SD7','beacons-map')

     paramsMapper = new ParamsMapper('.map-view')


use scrollTop solution for both chrome & firefox

--- app/assets/javascripts/wall.js.coffee
+++ app/assets/javascripts/wall.js.coffee
@@ -45,7 +45,7 @@

   scrollDown: ->
     notes = $('ul.notes')
-    $('body').scrollTop(notes.height())
+    $('body, html').scrollTop(notes.height())

   initForm: ->
     form = $('.wall-note-form')


Update old weapon to enable new score system

--- app/scripts/components/weapons/OldWeaponLaser.coffee
+++ app/scripts/components/weapons/OldWeaponLaser.coffee
@@ -57,10 +57,10 @@
         damage: 100
         speed: @ship._forcedSpeed.x + settings.speed
         direction: 0
-      .bind 'HitTarget', =>
+      .bind 'HitTarget', (target) =>
         @addXP(1)
-        @ship.trigger('BulletHit')
-      .bind 'DestroyTarget', =>
+        @ship.trigger('BulletHit', target)
+      .bind 'DestroyTarget', (target) =>
         @addXP(5)
-        @ship.trigger('BulletDestroyedTarget')
+        @ship.trigger('BulletDestroyedTarget', target)


Update test to reflect new soft wrap indent clipping behavior

Signed-off-by: Nathan Sobo <2e8aa918660411855c6d44d5bb2da677aa033255@github.com>

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -299,7 +299,7 @@

         it "positions the cursor at the buffer position that corresponds to the given screen position", ->
           editor.setCursorScreenPosition([9, 0])
-          expect(editor.getCursorBufferPosition()).toEqual [8, 10]
+          expect(editor.getCursorBufferPosition()).toEqual [8, 11]

     describe ".moveUp()", ->
       it "moves the cursor up", ->


TerminalSession: webtermKillSession is added to new terminal kite

--- client/Main/kite/kites/terminalkite.coffee
+++ client/Main/kite/kites/terminalkite.coffee
@@ -1,7 +1,8 @@
 class KodingKite_TerminalKite extends KodingKite_VmKite

   @constructors['terminal'] = this
-
+
   @createApiMapping
     webtermGetSessions: 'webterm.getSessions'
     webtermConnect    : 'webterm.connect'
+    webtermKillSession: 'webterm.killSession'


Team parser: Refactor, no more dependency.

--- client/app/js/concerns/team_parsing.coffee
+++ client/app/js/concerns/team_parsing.coffee
@@ -1,78 +1,60 @@
-# Ported from Pokemon Showdown:
-# https://github.com/Zarel/Pokemon-Showdown-Client/blob/8beb563042cac1acb64ec88c552ef21fb28f0f39/js/client-teambuilder.js#L1236-L1352
-
 HiddenPower = (if module? then require('../../../../shared/hidden_power') else window.HiddenPower ?= {})

 @PokeBattle ?= {}
 @PokeBattle.parseTeam = (teamString) ->
   text = teamString.split('\n')
   team = []
+  pokemonRegex = /^(.*?)\s*(\(M\)|\(F\)|)?(?:\s*@\s*(.*))?$/
+  pokemon = null
   for line in text
-    line = line.replace(/^\s+/, '').replace(/\s+$/, '')
-    if line.length == 0 || line == '---'
-      if pokemon then team.push(pokemon)
+    line = line.trim()
+    if line.length == 0
       pokemon = null
     else if !pokemon
-      [ pokemonLine, item ] = line.split(' @ ')
-      possibleGender = pokemonLine[-4...]
-      if possibleGender == ' (M)'
-        gender = 'M'
-        pokemonLine = pokemonLine[...-4]
-      else if possibleGender == ' (F)'
-        gender = 'F'
-        pokemonLine = pokemonLine[...-4]
-      else
-        gender = null
+      [ all, pokemonLine, gender, item ] = line.match(pokemonRegex)
       pokemon = {}
+      team.push(pokemon)
+
       # Ignore nicknames for now
-      if pokemonLine.match(/.*?\s*\((.*)\)/)
-        pokemonLine = RegExp.$1
-      pokemon.name = pokemonLine
-      pokemon.gender = gender  if gender
+      pokemonLine    = RegExp.$1  if pokemonLine.match(/.*?\s*\((.*)\)/)
+      pokemon.name   = pokemonLine.trim()
+      pokemon.gender = gender[1]  if gender  # (M) and (F)
       pokemon.item   = item    if item
-    else if startsWith(line, "Trait: ")
-      pokemon.ability = without(line, "Trait: ")
-    else if line == 'Shiny: Yes'
+    else if line.match(/^(?:Trait|Ability):\s+(.*)$/i)
+      pokemon.ability = RegExp.$1
+    else if line.match(/^Level:\s+(.*)$/i)
+      pokemon.level = Number(RegExp.$1) || 100
+    else if line.match(/^Happiness:\s+(.*)$/i)
+      pokemon.happiness = Number(RegExp.$1) || 0
+    else if line.match(/^Shiny: Yes$/i)
       pokemon.shiny = true
-    else if startsWith(line, "Level: ")
-      pokemon.level = Number(without(line, "Level: ")) || 100
-    else if startsWith(line, 'Happiness: ')
-      pokemon.happiness = Number(without(line, "Happiness: ")) || 0
-    else if startsWith(line, 'Ability: ')
-      pokemon.ability = without(line, "Ability: ")
-    else if startsWith(line, 'EVs: ')
-      evs = without(line, 'EVs: ').split(/\//g)
+    else if line.match(/^EVs: (.*)$/i)
+      evs = RegExp.$1.split(/\//g)
       pokemon.evs = {}
       for ev in evs
-        ev = ev.replace(/^\s+/, '').replace(/\s+$/, '')
+        ev = ev.trim()
         [ numberString, rawStat ] = ev.split(/\s+/)
         pokemon.evs[statsHash[rawStat]] = Number(numberString) || 0
-    else if startsWith(line, 'IVs')
-      ivs = without(line, 'IVs: ').split(/\//g)
+    else if line.match(/^IVs: (.*)$/i)
+      ivs = RegExp.$1.split(/\//g)
       pokemon.ivs = {}
       for iv in ivs
-        iv = iv.replace(/^\s+/, '').replace(/\s+$/, '')
+        iv = iv.trim()
         [ numberString, rawStat ] = iv.split(/\s+/)
         pokemon.ivs[statsHash[rawStat]] = Number(numberString) || 0
-    else if line.match(/^([A-Za-z]+) (N|n)ature/)
+    else if line.match(/^([A-Za-z]+) nature/i)
       pokemon.nature = RegExp.$1
-    else if startsWith(line, '-') || startsWith(line, '~')
-      moveName = line.substr(1).replace(/^\s+/, '')
-      if startsWith(moveName, 'Hidden Power [')
+    else if line.match(/^[\-\~]\s*(.*)/)
+      moveName = RegExp.$1
+      if /Hidden Power \[/.test(moveName)
         if !pokemon.ivs
-          moveName.match(/Hidden Power \[\s*(.*)\s*\]/)
+          moveName.match(/Hidden Power \[\s*(.*)\s*\]/i)
           hiddenPowerType = RegExp.$1.toLowerCase()
           pokemon.ivs = HiddenPower.BW.ivs[hiddenPowerType] || {}
         moveName = 'Hidden Power'
       pokemon.moves ?= []
       pokemon.moves.push(moveName)
   return team
-
-startsWith = (string, suffix) ->
-  string[...suffix.length] == suffix
-
-without = (string, suffix) ->
-  string[suffix.length...]

 statsHash =
   'HP'   : 'hp'


Fix budget in administrations chart

Fix budget in administrations chart

We were using cents instead of Euros

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -72,7 +72,7 @@
     item.items = item.values.map (value) ->
       value.total = 0
       value.values.forEach (d) ->
-        value.total += +d.importe
+        value.total += Math.floor(+d.importe*0.01)
       return {
         name: value.key
         x0:   total
@@ -99,8 +99,8 @@
   keys = []

   data.forEach (item) ->
-    amount = Math.ceil(+item.importe_grupo*0.01)
-    amountUTE = Math.ceil(+item.importe_utes*0.01)
+    amount = Math.floor(+item.importe_grupo*0.01)
+    amountUTE = Math.floor(+item.importe_utes*0.01)
     total = amount + amountUTE
     keys.push
       key: item.grupo


Dblclick on user opens his profile

Dblclick on user opens his profile

--- client/components/tableRow/tableRow.coffee
+++ client/components/tableRow/tableRow.coffee
@@ -2,7 +2,7 @@
     name: ->
         user = Users.findById(@result.user)
         user.name
-
+
     active: ->
         if Session.get("activeUser") == @result.user
             "active"
@@ -51,3 +51,9 @@
 Template.tableRow.events
     'click .userName': (e,t) ->
         Session.set("activeUser", t.data.result.user)
+
+    'dblclick .userName': (e,t) ->
+        url = "http://informatics.mccme.ru/moodle/user/view.php?id=" + t.data.result.user
+        window.open(url, '_blank')
+
+


render favorites button unless workflow configuration states otherwise

--- app/components/subject-viewer.cjsx
+++ app/components/subject-viewer.cjsx
@@ -117,7 +117,8 @@
             <button type="button" aria-label="Metadata" title="Metadata" className="metadata-toggle" onClick={@showMetadata}></button>}
           {if @props.subject? and @props.user? and @props.project?

-              <FavoritesButton project={@props.project} subject={@props.subject} user={@props.user} />
+              {unless @props.workflow.configuration.disable_favorites
+                <FavoritesButton project={@props.project} subject={@props.subject} user={@props.user} />}
               <CollectionsManagerIcon project={@props.project} subject={@props.subject} user={@props.user} />
             }
           {if type is 'image' and @props.linkToFullImage


fixed a bug when loading existing results

--- app/helper/executableHelper.coffee
+++ app/helper/executableHelper.coffee
@@ -151,6 +151,7 @@
     async.waterfall [
       (callback) ->
         #STEP 1
+        #TODO check if the method actually requires images
         if (req.body.images?  and req.body.images[0].type is 'collection')
           preprocessCollection(collection, req, serviceInfo, parameterHelper,executionType, callback)
         else
@@ -260,7 +261,10 @@
     collection.name = req.body.images[0].value
     folder = nconf.get('paths:imageRootPath') + path.sep + collection.name
     collection.inputParameters = _.clone(req.body.inputs)
-    collection.inputHighlighters = _.clone(req.body.inputs.highlighter)
+    if req.body.inputs.highlighter?
+      collection.inputHighlighters = _.clone(req.body.inputs.highlighter)
+    else
+      collection.inputHighlighters = {}
     #collection.parameters = parameterHelper.matchParams(req.body.inputs, req.body.inputs.highlighter.segments,serviceInfo.parameters,folder,folder, "", req)
     if(ResultHelper.checkCollectionResultAvailable(collection))
       collection.result = ResultHelper.loadResult(collection)


Clean up copyright statement

Clean up copyright statement

--- app/js/loader/loader_common.coffee
+++ app/js/loader/loader_common.coffee
@@ -1,7 +1,6 @@
 ###@license
-<%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= pkg.homepage ? " * " + pkg.homepage : "" %>
-Date: <%= grunt.template.today("m/d/yyyy") %>
-Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>
+Factlink JavaScript library - https://factlink.com
+Copyright (c) 2014 Factlink Team

 Copyright for the annotator subcomponent:


Workaround jquery bug

Workaround jquery bug

--- app/js/util/factlink_button.coffee
+++ app/js/util/factlink_button.coffee
@@ -15,4 +15,5 @@
     $(buttonSel)
       .attr('title', 'Select any statement in this article and click this button to add your opinion.')
       .length > 0
-  setTitle() || $ setTitle
+  setTitle() || $.ready setTitle
+  # we can't use shorthand $ but need $.ready due to jquery 1.7.2 bug.


lot of comments for blog

lot of comments for blog

--- app/scripts/services/socket.coffee
+++ app/scripts/services/socket.coffee
@@ -1,26 +1,39 @@
 "use strict"
-angular.module("radioApp").service "Socket", ($rootScope) ->
+angular.module("radioApp").factory "Socket", ($rootScope) ->
+  # io is a global object available if you included socket.io script:
   socket = io.connect()
-  io.currSocket = socket.socket
-  returnValue = ($scope)->
-    $scope._subscriptions?= []
+  ###
+    $scope - scope of a controller
+    global - boolean indicating whether a change applies only
+             to a local scope or to a $rootScope
+             (in case perfomansc is an issues)
+  ###
+  resultObject = ($scope, global = true)->
+    # create an array of listeners if there is none (notice '?' operator):
+    $scope._listeners?= []
+    # if controller's scope is destroyed then $destroy event is fired
+    # on this event we should get rid of all listeners on this scope:
     $scope.$on '$destroy', (event)->
-      for sub in $scope._subscriptions
-        socket.removeListener(sub.eventName, sub.callback)
-      $scope._subscriptions.length=0
+      for lis in $scope._listeners
+        socket.removeListener(lis.eventName, lis.callback)
+      $scope._listeners.length=0

+    # return familiar to us socket.io object that can listen to and emit events:
     return  {
-      on: (eventName, cb) ->
-        callback = ->
+      on: (eventName, callback) ->
+        ngCallback = ()->
           args = arguments
-          $rootScope.$apply ->
-            cb.apply socket, args
-          $rootScope.$apply()
-        $scope._subscriptions.push {
+          scopeOfChange = if global then $rootScope else $scope
+          # trigger angular $digest cycle on selected scope:
+          scopeOfChange.$apply ->
+            callback.apply socket, args # apply function to original object
+        # save listener to a list on current scope so we can remove it later:
+        $scope._listeners.push {
           eventName
-          callback
+          ngCallback
         }
-        socket.on eventName, callback
+        # pass our own callback that wraps the one passed by a user
+        socket.on eventName, ngCallback

       emit: (eventName, data, callback) ->
         socket.emit eventName, data, ->
@@ -28,9 +41,8 @@
           $rootScope.$apply ->
             callback.apply socket, args  if callback
     }
-  returnValue.reconnect = ()->
-    io.currSocket.disconnect()
-    io.currSocket.connect()
-  return returnValue
-
-
+  # sometimes I find reconnect to be usefull:
+  resultObject.reconnect = ()->
+    socket.socket.disconnect()
+    socket.socket.connect()
+  return resultObject


add next dashboard name; add arrow; add click function; commonize id attributes

--- lib/dashing-contrib/assets/widgets/switcher/switcher.coffee
+++ lib/dashing-contrib/assets/widgets/switcher/switcher.coffee
@@ -28,14 +28,14 @@
     @curName = pathParts[pathParts.length - 1]
     @curPos = @dashboardNames.indexOf(@curName)

-    # instantiate switcher controls for countdown and manual switching
-    @switcherControls = new SwitcherControls(interval, @dashboardNames, @curName)
-    @switcherControls.start() if @switcherControls.present()
-
     # If not found, default to first
     if @curPos == -1
       @curPos = 0
       @curName = @dashboardNames[@curPos]
+
+    # instantiate switcher controls for countdown and manual switching
+    @switcherControls = new SwitcherControls(interval, @dashboardNames[@curPos + 1] || @dashboardNames[0])
+    @switcherControls.start() if @switcherControls.present()

     # Start loop
     @handle = setTimeout(() ->
@@ -62,7 +62,6 @@
 class WidgetSwitcher
   constructor: (@elements) ->
     @$elements = $(elements)
-    @incrementTime = 1000 # refresh every 1000 milliseconds

   start: (interval=5000) ->
     self = @
@@ -93,12 +92,12 @@
 #   - show the name of the next dashboard
 #   - add controls for manually cycling through dashboards
 class SwitcherControls
-  constructor: (interval=60000, dashboardNames, currentDashboardName) ->
+  constructor: (interval=60000, nextDashboardName) ->
     @currentTime = parseInt(interval, 10)
     @interval = parseInt(interval, 10)
     @$elements = $('#switcher-controls')
-    @currentDashboardName = currentDashboardName
-    @dashboardNames = dashboardNames
+    @nextDashboardName = nextDashboardName
+    @incrementTime = 1000 # refresh every 1000 milliseconds

   present: () ->
     @$elements.length
@@ -108,16 +107,20 @@
     @$timer = $.timer(@updateTimer, @incrementTime, true)

   addElements: () ->
-    @$countdown = $("00:00")
-    @$elements.append(@countdown)
+    @$nextDashboardNameContainer = $("")
+    @$countdown = $("")
+    manualSwitcher = $(" ￫")
+    @$elements.append(@$nextDashboardNameContainer).append(@$countdown).append(manualSwitcher).click( () =>
+      location.href = "/#{@nextDashboardName}"
+    )

   formatTime: (time) ->
     time = time / 10;
-    min = parseInt(time / 6000, 10),
+    min = parseInt(time / 6000, 10)
     sec = parseInt(time / 100, 10) - (min * 60)
-    "#{(if min > 0 then pad(min, 2) else "00")}:#{pad(sec, 2)}"
+    "#{(if min > 0 then @pad(min, 2) else "00")}:#{@pad(sec, 2)}"

-  pad: -> (number, length) {
+  pad: (number, length) =>
     str = "#{number}"
     while str.length < length
       str = "0#{str}"
@@ -132,9 +135,11 @@
     # Stop and reset timer
     @$timer.stop().once()

-  updateTimer: () ->
+  updateTimer: () =>
+    # Update dashboard name
+    @$nextDashboardNameContainer.text("Next dashboard: #{@nextDashboardName} in ")
     # Output timer position
-    timeString = formatTime(currentTime)
+    timeString = @formatTime(@currentTime)
     @$countdown.html(timeString)

     # If timer is complete, trigger alert
@@ -145,7 +150,9 @@

     # Increment timer position
     @currentTime -= @incrementTime
-    if (@currentTime < 0) @currentTime = 0
+    if @currentTime < 0
+      @currentTime = 0
+

 # Dashboard loaded and ready
 Dashing.on 'ready', ->


more prominent add new idea button

more prominent add new idea button


dont disable for practice that errors

dont disable for practice that errors

--- tutor/src/components/performance-forecast/practice.cjsx
+++ tutor/src/components/performance-forecast/practice.cjsx
@@ -22,7 +22,7 @@

   isDisabled: ->
     {page_ids, courseId} = @props
-    _.isEmpty(page_ids) or CoursePracticeStore.isDisabled(courseId, {page_ids})
+    _.isEmpty(page_ids) # or CoursePracticeStore.isDisabled(courseId, {page_ids})

   render: ->
     isDisabled = @isDisabled()


Remove unneeded trailing unless

Remove unneeded trailing unless

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -41,7 +41,7 @@
       else
         # The editor this preview was created for has been closed so close
         # this preview since a preview cannot be rendered without an editor
-        @parents('.pane').view()?.destroyItem(this) unless @editor?
+        @parents('.pane').view()?.destroyItem(this)

     if atom.workspace?
       resolve()


specs

specs

--- components/artwork_filter/test/view.coffee
+++ components/artwork_filter/test/view.coffee
@@ -25,7 +25,7 @@
   beforeEach ->
     sinon.stub Backbone, 'sync'
     @model = new Artist fabricate 'artist', id: 'foo-bar'
-    @view = new @ArtworkFilterView model: @model
+    @view = new @ArtworkFilterView model: @model, mode: 'grid'

   afterEach ->
     Backbone.sync.restore()
@@ -55,6 +55,9 @@
     it 'removes itself if the initial filter state errors', ->
       Backbone.sync.args[0][2].error {}
       @view.remove.called.should.be.true
+
+    it 'starts in grid mode', ->
+      @view.viewMode.get('mode').should.equal 'grid'

   describe '#renderFilter', ->
     beforeEach ->
@@ -174,6 +177,18 @@
       @view.filter.selected.attributes.should.eql {}
       _.last(Backbone.sync.args)[2].data.should.not.containEql price_range: '-1:1000000000000'

+  describe '#changeViewMode', ->
+    before ->
+      sinon.spy @ArtworkFilterView::, 'renderColumns'
+
+    it 'sets the view mode when the toggle is clicked', ->
+      @view.$('.icon-list').click()
+      @view.viewMode.get 'mode', 'list'
+
+    it 're-renders the artworks when the view mode is changed', ->
+      @view.viewMode.set 'mode', 'list'
+      @view.renderColumns.called.should.be.true
+
   describe '#setButtonState', ->
     beforeEach ->
       @columnLength = 0


Refactored form validation

Refactored form validation

--- src/app/assets/scripts/ps.coffee
+++ src/app/assets/scripts/ps.coffee
@@ -25,6 +25,10 @@
       @$header.find("#showDone").addClass('doneNotShown')
       @$header.find("#showDone").removeClass('doneShown')

+class ValidatableView extends Backbone.View
+  valid: () ->
+    @$form.validate(@validationRules)
+    @$form.valid()

 class ApplicationModel extends Backbone.Model

@@ -42,9 +46,10 @@
   model: EntryGroup
   url: window.urls.entryGroups

-class EditEntryForm
+class EditEntryForm extends ValidatableView
   $el: null
   $shownInsteadOf: null
+  $form: null
   controller: null
   modelBeingEdited: null
   isActive: false
@@ -63,6 +68,7 @@
   constructor: (controller) ->
     @controller = controller
     @$el = $('.editEntryForm')
+    @$form = $('.editEntryForm')
     @$el.detach()

   edit: (el) ->
@@ -87,7 +93,7 @@

   onOkEditEntry: (e) ->
     e.preventDefault()
-    if !@validate() then return
+    if !@valid() then return
     content = @$el.find('#entryContent').val()
     @modelBeingEdited.set('content', content)
     stateId = if @$el.find('#completed').prop('checked') then @controller.constants.closedStateId else @controller.constants.openedStateId
@@ -100,11 +106,7 @@
     e.preventDefault()
     @hide()

-  validate: () ->
-    @$el.validate(@validationRules)
-    @$el.valid()
-
-class EntryListView extends Backbone.View
+class EntryListView extends ValidatableView
   template: _.template($("#entryListTemplate").html())
   entries: null
   controller: null
@@ -170,10 +172,6 @@
     $editedEl = $(e.target).closest('.entryRow')
     window.app.views.editEntryForm.edit($editedEl)

-  valid: () ->
-    @$form.validate(@validationRules)
-    @$form.valid()
-
 class EntryGroupListView extends Backbone.View
   template: _.template($("#entryGroupListTemplate").html())
   entryGroups: null
@@ -215,9 +213,10 @@
     @controller.views.editGroupForm.edit(group)
     e.preventDefault()

-class EditGroupForm extends Backbone.View
+class EditGroupForm extends ValidatableView
   el: '#editGroupForm'
   $input: null
+  $form: null
   controller: null
   group: null

@@ -237,6 +236,7 @@
     @$el.find('#closePopup').click($.proxy(@onCancel, @))
     @$el.find('#okEditGroup').click($.proxy(@onOk, @))
     @$el.find('#cancelEditGroup').click($.proxy(@onCancel, @))
+    @$form = @$el.find('form')
     @$input = @$el.find('#groupName')

   edit: (group) ->
@@ -263,10 +263,6 @@
     @$el.hide()
     e.preventDefault()

-  valid: () ->
-    @$el.find('form').validate(@validationRules)
-    @$el.find('form').valid()
-

 class Application extends Backbone.Router
   constants:


small debug on deletion

small debug on deletion

--- app/views/CNeditor/CNeditor.coffee
+++ app/views/CNeditor/CNeditor.coffee
@@ -1037,7 +1037,7 @@
         # Perform deletion on selection and adapt remaining parts consequently.
         @_adaptEndLineType startLine, endLine # adapt end line type if needed.
         @_deleteSelectedLines range
-        @_addMissingFragment startLine, endOfLineFragment.childNodes[0]
+        @_addMissingFragment startLine, endOfLineFragment
         @_removeEndLine startLine, endLine
         @_setCaret(startContainer, startOffset) if replaceCaret
         @_adaptDepth startLine, startLineDepth, endLineDepth, deltaDepth
@@ -1091,7 +1091,8 @@
     # deletion.
     # if startFrag et myEndLine are SPAN and they both have the same class
     # then we concatenate both
-    _addMissingFragment: (startLine, startFrag) ->
+    _addMissingFragment: (startLine, endOfLineFragment) ->
+        startFrag = endOfLineFragment.childNodes[0]
         if startLine.line$[0].lastChild.nodeName == 'BR'
             startLine.line$[0].removeChild(startLine.line$[0].lastChild)
         endLine = startLine.line$[0].lastElementChild


:bug:Fix campaign editor next level arrows

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -371,8 +371,8 @@
     for level in orderedLevels
       # Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level.
       level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
-      break if foundNext = findNextLevel(level.nextLevels, true) # Check practice levels first
-      break if foundNext = findNextLevel(level.nextLevels, false)
+      foundNext = findNextLevel(level.nextLevels, true) unless foundNext # Check practice levels first
+      foundNext = findNextLevel(level.nextLevels, false) unless foundNext

     if not foundNext and orderedLevels[0] and not orderedLevels[0].locked and @levelStatusMap[orderedLevels[0].slug] isnt 'complete'
       orderedLevels[0].next = true


Move active back down after clear!

Move active back down after clear!

--- lib/project/results-model.coffee
+++ lib/project/results-model.coffee
@@ -52,8 +52,6 @@
     @emit('replacement-state-cleared', @getResultsSummary())

   search: (pattern, searchPaths, replacementPattern, {onlyRunIfChanged, keepReplacementState}={}) ->
-    @active = true
-
     return Q() if onlyRunIfChanged and pattern? and searchPaths? and pattern == @pattern and _.isEqual(searchPaths, @searchedPaths)

     if keepReplacementState
@@ -61,6 +59,7 @@
     else
       @clear()

+    @active = true
     @regex = @getRegex(pattern)
     @pattern = pattern
     @searchedPaths = searchPaths