Implement enter creates project. Only show hidden and deleted toggles if project list contains them.

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -583,6 +583,10 @@
                     description_text : ''
                     error            : ''

+    handle_keypress : (e) ->
+        if e.keyCode == 13 and @state.title_text != ''
+            @create_project()
+
     render_input_section : ->
         <Well style={backgroundColor: '#ffffff'}>
             <Row>
@@ -599,6 +603,7 @@
                         placeholder = 'Title (you can easily change this later)'
                         disabled    = {@state.state == 'saving'}
                         onChange    = {=>@setState(title_text:@refs.new_project_title.getValue())}
+                        onKeyDown   = {@handle_keypress}
                         autoFocus   />
                 </Col>

@@ -609,7 +614,8 @@
                         type        = 'text'
                         placeholder = 'No description'
                         disabled    = {@state.state == 'saving'}
-                        onChange    = {=>@setState(description_text:@refs.new_project_description.getValue())} />
+                        onChange    = {=>@setState(description_text:@refs.new_project_description.getValue())}
+                        onKeyDown   = {@handle_keypress} />
                 </Col>

                 <Col sm=2>
@@ -672,21 +678,37 @@
     displayName : 'ProjectsFilterButtons'

     propTypes :
-        hidden  : rtypes.bool.isRequired
-        deleted : rtypes.bool.isRequired
+        hidden              : rtypes.bool.isRequired
+        deleted             : rtypes.bool.isRequired
+        show_hidden_button  : rtypes.bool
+        show_deleted_button : rtypes.bool

     getDefaultProps : ->
         hidden  : false
         deleted : false
+        show_hidden_button : false
+        show_deleted_button : false
+
+    render_deleted_button : ->
+        if @props.show_deleted_button
+            return <Button onClick = {=>flux.getActions('projects').setTo(deleted: not @props.deleted)}>
+                    <Icon name={if @props.deleted then 'check-square-o' else 'square-o'} fixedWidth /> Deleted
+                </Button>
+        else
+            return null
+
+    render_hidden_button : ->
+        if @props.show_hidden_button
+            return <Button onClick = {=>flux.getActions('projects').setTo(hidden: not @props.hidden)}>
+                    <Icon name={if @props.hidden then 'check-square-o' else 'square-o'} fixedWidth /> Hidden
+                </Button>
+        else
+            return null

     render : ->
         <ButtonGroup>
-            <Button onClick = {=>flux.getActions('projects').setTo(deleted: not @props.deleted)}>
-                <Icon name={if @props.deleted then 'check-square-o' else 'square-o'} fixedWidth /> Deleted
-            </Button>
-            <Button onClick={=>flux.getActions('projects').setTo(hidden: not @props.hidden)}>
-                <Icon name={if @props.hidden then 'check-square-o' else 'square-o'} fixedWidth /> Hidden
-            </Button>
+            {@render_deleted_button()}
+            {@render_hidden_button()}
         </ButtonGroup>

 ProjectsSearch = rclass
@@ -1087,6 +1109,24 @@
         project = @visible_projects()[0]
         if project?
             open_project(project: project.project_id)
+    ###
+    # Consolidate the next two functions.
+    ###
+
+    # Returns true if this project has any hidden files
+    has_hidden_files : ->
+        for project in @project_list()
+            if project_is_in_filter(project, true, false)
+                return true
+        return false
+
+
+    # Returns true if this project has any deleted files
+    has_deleted_files : ->
+        for project in @project_list()
+            if project_is_in_filter(project, false, true)
+                return true
+        return false

     render : ->
         if not @props.project_map?
@@ -1103,7 +1143,9 @@
                     <Col sm=8>
                         <ProjectsFilterButtons
                             hidden  = {@props.hidden}
-                            deleted = {@props.deleted} />
+                            deleted = {@props.deleted}
+                            show_hidden_button = {@has_hidden_files() or @props.hidden}
+                            show_deleted_button = {@has_deleted_files() or @props.deleted}/>
                     </Col>
                 </Row>
                 <Row>


example: remove `postrender` (no longer supported)

--- examples/postrender.coffee
+++ examples/postrender.coffee
@@ -1,46 +0,0 @@
-require('./zappajs') ->
-
-  @locals.title = 'Post-rendering'
-  @locals.style = '''
-    #quotas div {border: 1px solid #999; background: #eee; padding: 10px; margin: 10px}
-    #quotas .highlighted {border: 3px solid #37697e; background: #d0deea}
-  '''
-
-  @get '/': ->
-    # @user = plan: 'staff'
-    @user = plan: 'basic'
-
-    @render 'index', {@user, postrender: 'plans'}
-
-  @postrender plans: ($) ->
-    $('.staff').remove() if @user.plan isnt 'staff'
-    $('div.' + @user.plan).addClass 'highlighted'
-
-  {h1,div,h2,p,button} = @teacup
-
-  @view index: ->
-    h1 'Quotas:'
-
-    div '#quotas', ->
-      div '.basic', ->
-        h2 'Basic'
-        p 'Disk: 1 GB'
-        p 'Bandwidth: 10 GB'
-        button class: 'staff', -> 'Change Quotas'
-
-      div '.silver', ->
-        h2 'Silver'
-        p 'Disk: 2 GB'
-        p 'RAM: 15 GB'
-        button class: 'staff', -> 'Change Quotas'
-
-      div '.golden', ->
-        h2 'Golden'
-        p 'Disk: 4 GB'
-        p 'RAM: 30 GB'
-        button class: 'staff', -> 'Change Quotas'
-
-      div '.staff', ->
-        h2 'Staff'
-        p 'Disk: 10 GB'
-        p 'RAM: 100 GB'


resolving issues with comments in the middle of unfinished single-line expressions.

--- lexer.coffee
+++ lexer.coffee
@@ -98,7 +98,7 @@
     tag: id.toUpperCase() if include(KEYWORDS, id) and
       not (include(ACCESSORS, @tag(0)) and not @prev().spaced)
     @identifier_error id  if include RESERVED, id
-    tag: 'LEADING_WHEN'   if tag is 'WHEN' and include BEFORE_WHEN, @tag()
+    tag: 'LEADING_WHEN'   if tag is 'WHEN' and include LINE_BREAK, @tag()
     @token(tag, id)
     @i += id.length
     true
@@ -171,9 +171,11 @@
   comment_token: ->
     return false unless comment: @match COMMENT, 1
     @line += (comment.match(MULTILINER) or []).length
-    lines: comment.replace(COMMENT_CLEANER, '').split(MULTILINER)
-    @token 'COMMENT', compact lines
-    @token 'TERMINATOR', "\n"
+    lines: compact comment.replace(COMMENT_CLEANER, '').split(MULTILINER)
+    i: @tokens.length - 1
+    if @unfinished()
+      i -= 1 while @tokens[i] and not include LINE_BREAK, @tokens[i][0]
+    @tokens.splice(i + 1, 0, ['COMMENT', lines, @line], ['TERMINATOR', '\n', @line])
     @i += comment.length
     true

@@ -194,9 +196,7 @@
     prev: @prev(2)
     size: indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length
     next_character: @chunk.match(MULTI_DENT)[4]
-    no_newlines: next_character is '.' or
-      (@value() and @value().match and @value().match(NO_NEWLINE) and
-      prev and (prev[0] isnt '.') and not @value().match(CODE))
+    no_newlines: next_character is '.' or @unfinished()
     if size is @indent
       return @suppress_newlines() if no_newlines
       return @newline_token(indent)
@@ -406,6 +406,12 @@
     return false unless m: @chunk.match(regex)
     if m then m[index] else false

+  # Are we in the midst of an unfinished expression?
+  unfinished: ->
+    prev: @prev(2)
+    @value() and @value().match and @value().match(NO_NEWLINE) and
+      prev and (prev[0] isnt '.') and not @value().match(CODE)
+
 # There are no exensions to the core lexer by default.
 Lexer.extensions: []

@@ -500,4 +506,4 @@
 # Tokens that, when immediately preceding a `WHEN`, indicate that the `WHEN`
 # occurs at the start of a line. We disambiguate these from trailing whens to
 # avoid an ambiguity in the grammar.
-BEFORE_WHEN: ['INDENT', 'OUTDENT', 'TERMINATOR']
+LINE_BREAK: ['INDENT', 'OUTDENT', 'TERMINATOR']


fixing heredoc indentation from herecomment tweaks

--- lexer.coffee
+++ lexer.coffee
@@ -295,7 +295,8 @@
   # erasing all external indentation on the left-hand side.
   sanitize_heredoc: (doc, options) ->
     while match: HEREDOC_INDENT.exec doc
-      indent: match[1] if not indent or match[1].length < indent.length
+      attempt: match[2] or match[3]
+      indent: attempt if not indent or attempt.length < indent.length
     doc: doc.replace(new RegExp("^" +indent, 'gm'), '')
     return doc if options.herecomment
     doc.replace(MULTILINER, "\\n")
@@ -503,7 +504,7 @@
 STRING_NEWLINES : /\n[ \t]*/g
 COMMENT_CLEANER : /(^[ \t]*#|\n[ \t]*$)/mg
 NO_NEWLINE      : /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/
-HEREDOC_INDENT  : /\n+([ \t]*)/g
+HEREDOC_INDENT  : /(\n+([ \t]*)|^([ \t]+))/g

 # Tokens which a regular expression will never immediately follow, but which
 # a division operator might.


postgres testing: changefeed on a single project

--- src/smc-hub/test/postgres-user-queries-changefeeds-2.coffee
+++ src/smc-hub/test/postgres-user-queries-changefeeds-2.coffee
@@ -141,3 +141,40 @@
             ], done)

+describe 'changefeed on a single project', ->
+    before(setup)
+    after(teardown)
+
+    it 'make 2 projects, feed on single, remove and add user', (done) ->
+        changefeed_id = misc.uuid()
+        accounts = projects = undefined
+        async.series([
+            (cb) ->
+                create_accounts 1, (err, x) -> accounts=x; cb(err)
+            (cb) ->
+                # make 2 projects; one will be comletely ignored
+                create_projects 2, accounts[0], (err, v) ->
+                    projects = v
+                    cb(err)
+            (cb) ->
+                db.user_query
+                    account_id : accounts[0]
+                    query      : {projects:[{project_id:projects[0], description:null}]}
+                    changes    : changefeed_id
+                    cb         : changefeed_series([
+                        (x, cb) ->
+                            expect(x.projects.length).toEqual(1)
+
+                            db.remove_user_from_project(project_id:projects[0], account_id:accounts[0], cb:cb)
+                        (x, cb) ->
+                            expect(x).toEqual({ action: 'delete', old_val: { project_id: projects[0]} })
+
+                            db.add_user_to_project(project_id:projects[0], account_id:accounts[0], cb:cb)
+                        (x, cb) ->
+                            expect(x).toEqual({ action: 'insert', new_val: { project_id: projects[0], description: "Description 0"} })
+                            cb()
+                    ], cb)
+        ], done)
+
+
+


style(comments): formatting

style(comments): formatting


clean

clean

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -1,11 +1,9 @@
 gulp       = require 'gulp'
-jshint     = require 'gulp-jshint'
 uglify     = require 'gulp-uglify'
 rename     = require 'gulp-rename'
 sourcemaps = require 'gulp-sourcemaps'
 gif        = require 'gulp-if'
 lr         = require 'gulp-livereload'
-watchify   = require 'watchify'
 coffeeify  = require 'gulp-coffeeify'
 autowatch  = require 'gulp-autowatch'


Fixed build output step when creating a distribution.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -326,8 +326,8 @@
 gulp.task 'create-distribution', [ 'clean-distribution', 'build' ], ->

   # Copy Build Output
-  gulp.src BUILD_OUTPUT
-    .pipe gulp.dest '#{DISTRIBUTION_OUTPUT}/#{pkg.version}'
+  gulp.src paths.build.base
+    .pipe gulp.dest("#{paths.distribution.base}/#{pkg.version}")

 # Default Gulp Task ------------------------------------------------------------


Update reload task

Update reload task

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -62,6 +62,9 @@
 gulp.task "build", ->
   runSequence "lint-scss", "swift", "uncss", "jekyll-build"

+gulp.task "rebuild", ->
+  runSequence "jekyll-build-local", "reload"
+
 gulp.task "clean",
   del.bind(null, ["_site"])

@@ -69,18 +72,17 @@
   gulp.watch "#{paths.sass}/**/*.scss", ["sass"]
   gulp.watch "#{paths.coffee}/**/*.coffee", ["coffee"]
   gulp.watch "#{paths.coffee}/vendor.js", ["vendor-js"]
-  gulp.watch paths.jekyllFiles, ["jekyll-reload"]
+  gulp.watch paths.jekyllFiles, ["rebuild"]

 gulp.task "jekyll-build-local", ->
   run "bundle exec jekyll build --config _config.yml,_config.serve.yml", silent: true
     .exec()
-  browserSync.notify messages.jekyllBuild

 gulp.task "jekyll-build", ->
   run "bundle exec jekyll build", silent: true
     .exec()

-gulp.task "jekyll-reload", ["jekyll-build-local"], ->
+gulp.task "reload", ->
   browserSync.reload()

 gulp.task "doctor", ->


Replace landing

Replace landing

The landing reporter ate some log statements :-/

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -61,7 +61,7 @@
 gulp.task "test", ["build:debug", "build:test"], ->
 	return gulp
 		.src("test/phantomjs/index.html")
-		.pipe(phantomjs({reporter: "landing"}))
+		.pipe(phantomjs({reporter: "dot"}))

 gulp.task "watch", ["test"], ->
 	gulp.watch(["./*.coffee", "framer/**", "test/tests/**"], ["test"])
@@ -95,7 +95,7 @@
 	return gulp
 		.src("test/phantomjs/index.html")
 		.pipe(phantomjs(
-			reporter: "landing"
+			# reporter: "landing"
 			phantomjs:
 				hooks: "coverage-capture"
 		))


remove gulp file

remove gulp file

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -1,21 +0,0 @@
-gulp           = require 'gulp'
-rename         = require 'gulp-rename'
-
-gulp.task 'default', ->
-
-  gulp.src ["./client/**/*", "!node_modules/"], base : "./client/"
-    .pipe rename (path, file) ->
-      return unless path.extname is '.coffee'
-
-      # console.log "was: ", "#{path.basename}#{path.extname}"
-      contents = file.contents.toString()
-      n = (/class\s([A-Z]{1}(?:\w|\.)+)/.exec contents)?[0]
-
-      if n
-        name = n.replace(/\./, '').replace(/class\s/, '').toLowerCase()
-        path.basename = name
-
-      path
-
-
-    .pipe gulp.dest './client5'


correct regex. match full block

correct regex. match full block

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -95,7 +95,7 @@
         links += link.replace /^(\[.+?\]:\s*(?!\/\/)[-._a-z0-9\/]+)litcoffee/gmi, "$1html"
         links += "\n"
         return ''
-      if links then file.contents = new Buffer data.replace /(^(#{1,6}| {4}).+?$)/gmi, "$1\n#{links}\n"
+      if links then file.contents = new Buffer data.replace /(^(#{1,6}|(( {4}.*\n*)* {4})).*?$)/gmi, "$1\n#{links}\n"
       callback null, file
     .pipe $.docco()
     .pipe $.gulp.dest _.doc


add output of test result code.

add output of test result code.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -36,12 +36,14 @@
   cp.execFile(
     'node',
     ['node_modules/mocha/bin/mocha','-R','html-cov','-u','bdd','--compilers','coffee:coffee-script','--require','chai',path.resolve(__dirname,'./test/')],
-  {},
-  (error, stdout, stderr) ->
-    console.log 'Saving coverage results...'
-    fs.createWriteStream('./lib-cov/coverage.html').write(stdout)
-    openCoverageDocumentInBrowser()
-    process.env.CODE_COVERAGE = codecoverage || 'false'
+    {},
+    (error, stdout, stderr) ->
+      console.log 'Saving coverage results...'
+      fs.createWriteStream('./lib-cov/coverage.html').write(stdout)
+      openCoverageDocumentInBrowser()
+      process.env.CODE_COVERAGE = codecoverage || 'false'
+      console.warn '\r\nComplete' #console.warn will flush the output
+      process.exit error?.code? or 0
   )

 openCoverageDocumentInBrowser = ->


Using gruntfile instead of cakefile

Using gruntfile instead of cakefile

--- gruntfile.coffee
+++ gruntfile.coffee
@@ -1,2 +1,56 @@
-module.exports = ->
-  +module.exports = (grunt) ->
+
+  sourceOrigin = 'src/**/*.coffee'
+
+  grunt.initConfig
+    pkg: grunt.file.readJSON 'package.json'
+    coffeelint:
+      source: ['src/**/*.coffee']
+    coffee:
+      compile:
+        expand: true
+        cwd: 'src'
+        src: '**/*.coffee'
+        dest: 'js/'
+        ext: '.js'
+    concat:
+      dist:
+        src: ['js/**/*.js']
+        dest: 'dist/<%= pkg.name %>.js'
+    uglify:
+      options:
+        banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("dd-mm-yyyy") %> */\n'
+        report: 'gzip'
+      dist:
+        files:
+          'dist/<%= pkg.name %>.min.js': 'dist/<%= pkg.name %>.js'
+    dependo:
+      targetPath: 'js'
+      outputPath: './doc/dependencies'
+      format: 'amd'
+
+
+  grunt.loadNpmTasks 'grunt-coffeelint'
+  grunt.loadNpmTasks 'grunt-contrib-concat'
+  grunt.loadNpmTasks 'grunt-contrib-uglify'
+  grunt.loadNpmTasks 'grunt-contrib-coffee'
+  grunt.loadNpmTasks 'grunt-contrib-watch'
+  grunt.loadNpmTasks 'grunt-dependo'
+
+  grunt.registerTask 'codo', 'Generate Codo documentation', ->
+    done = @async()
+
+    {exec} = require 'child_process'
+    DOCGEN = 'codo'
+    DOCOUTPUT = './doc/api'
+
+    exec "#{DOCGEN} -o #{DOCOUTPUT} ./src/**/*.coffee", (err, stdout, stderr) ->
+      if err
+        return grunt.log.writeln stderr
+
+      grunt.log.writeln stdout
+      done()
+
+  grunt.registerTask 'test', 'Lints and unit tests', ['coffeelint']
+  grunt.registerTask 'doc', 'Generated documentation', ['dependo', 'codo']
+  grunt.registerTask 'default', 'Default task', ['test', 'coffee', 'concat', 'uglify', 'doc']


修正Table的远端排序不可用的BUG

修正Table的远端排序不可用的BUG

--- src/widget/list/table.coffee
+++ src/widget/list/table.coffee
@@ -397,14 +397,14 @@
 				if collection instanceof cola.EntityList
 					invoker = collection._providerInvoker
 					if invoker
-						parameter = invoker.invokerOptions.data
+						parameter = invoker.ajaxService.get("parameter")
 						if not parameter
 							invoker.invokerOptions.data = parameter = {}
 						else if typeof parameter isnt "object" or parameter instanceof cola.EntityList or parameter instanceof Date
 							throw new cola.Exception("Can not set sort parameter automatically.")
 						else if parameter instanceof cola.Entity
 							parameter = parameter.toJSON()
-						parameter.sort = criteria
+						parameter[cola.setting("defaultSortParameter") or sort] = criteria

 						cola.util.flush(collection)
 			else


 Merge branch 'master.activity.comment' into 'master.activity'

activity: keep the focus on comment input element

--- client/Social/Activity/views/comments/inputform.coffee
+++ client/Social/Activity/views/comments/inputform.coffee
@@ -35,7 +35,7 @@

       @input.setValue ''
       @input.resize()
-      @input.setBlur()
+      @input.setFocus()

     KD.requireMembership
       callback  : kallback


AdminModal: Code refactor.

AdminModal: Code refactor.

--- client/admin/lib/views/groupgeneralsettingsview.coffee
+++ client/admin/lib/views/groupgeneralsettingsview.coffee
@@ -14,57 +14,6 @@
 showError          = require 'app/util/showError'
 Encoder            = require 'htmlencode'
 geoPattern         = require 'geopattern'
-
-
-createSection = (options = {}) ->
-
-  { name, title, description } = options
-
-  section = new KDCustomHTMLView
-    tagName  : 'section'
-    cssClass : kd.utils.curry 'AppModal-section', name
-
-  section.addSubView desc = new KDCustomHTMLView
-    tagName  : 'p'
-    cssClass : 'AppModal-sectionDescription'
-    partial  : description
-
-  return section
-
-
-addInput = (form, options) ->
-
-  { name, label, description, itemClass, nextElement } = options
-
-  itemClass  or= KDInputView
-  form.inputs ?= {}
-
-  form.addSubView field = new KDCustomHTMLView tagName : 'fieldset'
-
-  if label
-    field.addSubView labelView = new KDCustomHTMLView
-      tagName : 'label'
-      for     : name
-      partial : label
-    options.label = labelView
-
-  field.addSubView form.inputs[name] = input = new itemClass options
-  field.addSubView new KDCustomHTMLView tagName : 'p', partial : description  if description
-
-  field.addSubView nextElement  if nextElement and nextElement instanceof KDView
-
-  return input
-
-addButton = (form, options) ->
-
-  { title, callback } = options
-
-  form.addSubView new KDButtonView {
-    title
-    callback
-    type     : 'submit'
-    cssClass : 'solid medium green'
-  }

 module.exports = class GroupGeneralSettingsView extends KDView
@@ -87,18 +36,18 @@
     group = @getData()
     url   = if group.slug is 'koding' then '' else "#{group.slug}."

-    @addSubView section = createSection name: 'general-settings'
+    @addSubView section = @createSection name: 'general-settings'

     section.addSubView form = @generalSettingsForm = new KDFormView

-    addInput form,
+    @addInput form,
       label        : 'Name'
       description  : 'Your team name is displayed in menus and emails. It usually is (or includes) the name of your company.'
       name         : 'title'
       defaultValue : Encoder.htmlDecode group.title ? ''
       placeholder  : 'Please enter a title here'

-    addInput form,
+    @addInput form,
       label        : 'URL'
       description  : 'Changing your team URL is currently not supported, if, for any reason, you must change this please send us an email at support@koding.com.'
       name         : 'url'
@@ -106,7 +55,7 @@
       defaultValue : Encoder.htmlDecode group.slug ? ''
       placeholder  : 'Please enter a title here'

-    addInput form,
+    @addInput form,
       label        : 'Default Channels'
       description  : 'Your new members will automatically join to <b>#general</b> channel. Here you can specify more channels for new team members to join automatically.'
       name         : 'channels'
@@ -117,14 +66,16 @@
         tagName    : 'span'
         partial    : 'Please add channel names separated by commas.'

-    addButton form,
+    form.addSubView new KDButtonView
       title    : 'Save Changes'
+      type     : 'submit'
+      cssClass : 'solid medium green'
       callback : @bound 'update'

   createAvatarUploadForm: ->

-    @addSubView section = createSection
+    @addSubView section = @createSection
       name : 'avatar-upload'

     section.addSubView @avatar = new KDCustomHTMLView
@@ -143,7 +94,7 @@
     jGroup   = @getData()
     logo     = jGroup.customize?.logo

-    if logo
+    if logo is 12
       avatarEl.style.backgroundImage = "url(#{logo})"
     else
       pattern = geoPattern.generate jGroup.title, generator: 'plusSigns'
@@ -154,13 +105,13 @@

   createDeletionForm: ->

-    @addSubView section = createSection
+    @addSubView section = @createSection
       name        : 'deletion'
       description : 'If Koding is no use to your team anymore, you can delete your team page here.'

     section.addSubView form = new KDFormView

-    addInput form,
+    @addInput form,
       itemClass    : KDButtonView
       cssClass     : 'solid medium red'
       description  : 'Note: Don\'t delete your team if you just want to change your team\'s name or URL. You also might want to export your data before deleting your team.'
@@ -198,3 +149,43 @@
         kd.warn err

       new KDNotificationView title: message, duration: 5000
+
+
+  createSection: (options = {}) ->
+
+    { name, title, description } = options
+
+    section = new KDCustomHTMLView
+      tagName  : 'section'
+      cssClass : kd.utils.curry 'AppModal-section', name
+
+    section.addSubView desc = new KDCustomHTMLView
+      tagName  : 'p'
+      cssClass : 'AppModal-sectionDescription'
+      partial  : description
+
+    return section
+
+
+  addInput: (form, options) ->
+
+    { name, label, description, itemClass, nextElement } = options
+
+    itemClass  or= KDInputView
+    form.inputs ?= {}
+
+    form.addSubView field = new KDCustomHTMLView tagName : 'fieldset'
+
+    if label
+      field.addSubView labelView = new KDCustomHTMLView
+        tagName : 'label'
+        for     : name
+        partial : label
+      options.label = labelView
+
+    field.addSubView form.inputs[name] = input = new itemClass options
+    field.addSubView new KDCustomHTMLView tagName : 'p', partial : description  if description
+
+    field.addSubView nextElement  if nextElement and nextElement instanceof KDView
+
+    return input


Parse correctly new format from grep (with -A and -B)

--- client/ide/views/contentsearch/idecontentsearch.coffee
+++ client/ide/views/contentsearch/idecontentsearch.coffee
@@ -101,6 +101,10 @@
     str.replace(/([\\"'`$\s\(\)<>])/g, "\\$1");

   formatOutput: (output, callback = noop) ->
+    # Regexes
+    MAIN_LINE = /^:?([\s\S]+):(\d+):([\s\S]*)$/
+    CONTEXT_LINE = /^([\s\S]+)\-(\d+)\-([\s\S]*)$/
+
     lines      = output.split '\n'
     formatted  = {}
     stats      = {
@@ -108,26 +112,32 @@
       numberOfSearchedFiles: 0
     }

-    for line in lines
-      parts = line.split ":"
-      if parts.length < 3
-       continue
+    formatted = lines
+    .map (line) ->
+      # Remove erronous whitespace
+      return line.trimLeft()
+    .filter (line) ->
+      # Skip lines that aren't one of these
+      return MAIN_LINE.test(line) or CONTEXT_LINE.test(line)
+    .map (line) ->
+      # Get the matches
+      return line.match(MAIN_LINE) or line.match(CONTEXT_LINE)
+    .reduce( (accu, matches) ->
+      # Extract matches
+      [fileName, lineNumber, line] = [matches[1], parseInt(matches[2], 10), matches[3]]

-      # Parse path
-      fileName = parts.shift()
-      fileName = fileName.trim()
+      # new filename found
+      unless accu[fileName]
+        accu[fileName] = []

-      # Parse line number
-      lineNumber = parseInt parts.shift()
+      # Add line to list of lines found for this filename
+      accu[fileName].push { lineNumber, line, occurence: MAIN_LINE.test(matches[0]) }

-      # Parse line
-      line = parts.join ''
+      # Increment matches
+      stats.numberOfMatches += 1

-      unless formatted[fileName] # new filename found
-        formatted[fileName] = [] # create an empty object for filename and continue to loop
-
-      formatted[fileName].push { lineNumber, line, occurence: yes}
-      stats.numberOfMatches += 1
+      return accu
+    , {})

     stats.numberOfSearchedFiles = Object.keys(formatted).length


Default account to props

Default account to props

--- client/skr/screens/gl-transactions/GlTransactions.cjsx
+++ client/skr/screens/gl-transactions/GlTransactions.cjsx
@@ -36,7 +36,7 @@
     modelForAccess: 'gl-transaction'

     dataObjects:
-        transactions: -> new Transactions
+        transactions: -> new Transactions(account: @props.account)

     render: ->
         <LC.ScreenWrapper flexVertical identifier="gl-transactions">


Fix building flat array of nodes

Fix building flat array of nodes

--- src/trix/observers/mutation_observer.coffee
+++ src/trix/observers/mutation_observer.coffee
@@ -93,9 +93,11 @@
     mutation for mutation in @mutations when mutation.type is type

   getRemovedTextNodes: ->
+    nodes = []
     for mutation in @getMutationsByType("childList")
       for node in mutation.removedNodes when node.nodeType is Node.TEXT_NODE
-        node
+        nodes.push(node)
+    nodes

   stringDifference = (a, b) ->
     leftIndex = 0


"Just a module" action tests for module first and passes if not present.

--- lib/actions/just_a_module.coffee
+++ lib/actions/just_a_module.coffee
@@ -1,3 +1,10 @@
-# assume that the path is that of a module and load and run that module
+isModule = require('../utilities').isModule
+
+# invoke the module at the given path if it is present
 module.exports= justAModuleAction= (pass,req,res,next,malifi,meta) ->
-    require(req.malifi.path.full)(req,res,next,malifi,meta)
+  path = req.malifi.path.full
+  isModule path, (found)->
+    if found
+      require(path)(req,res,next,malifi,meta)
+    else
+      pass()


Load craftingrecipes as a plugin and re-enable workbench recipe

The 'craftingrecipes' plugin now stores the recipe list and thesaurus,
which workbench and inventory-dialog can access for crafting.

Requires:
deathcap/craftingrecipes@fb82e001042bd43d76f06290c107ce31b604cc5d
deathcap/voxel-inventory-dialog@3881bee45484dd5466fc75bb297929d577490d95
deathcap/voxel-workbench@aec3a53d45062156302768ed56b90aa34e32aabb

--- index.coffee
+++ index.coffee
@@ -111,30 +111,11 @@
   }

-  # recipes TODO: move to registry?
-  #registry.recipes = RecipeLocator
-  #registry.thesaurus = CraftingThesaurus
-  CraftingThesaurus.registerName 'wood.log', new ItemPile('logOak')
-  CraftingThesaurus.registerName 'wood.log', new ItemPile('logBirch')
-  CraftingThesaurus.registerName 'wood.plank', new ItemPile('plankOak')
-  CraftingThesaurus.registerName 'tree.leaves', new ItemPile('leavesOak')
-  RecipeLocator.register new AmorphousRecipe(['wood.log'], new ItemPile('plankOak', 2))
-  RecipeLocator.register new AmorphousRecipe(['wood.plank', 'wood.plank'], new ItemPile('stick', 4))
-
-  RecipeLocator.register new PositionalRecipe([
-    ['wood.plank', 'wood.plank', 'wood.plank'],
-    [undefined, 'stick', undefined],
-    [undefined, 'stick', undefined]], new ItemPile('pickaxeWood', 1))
-
-  # temporary recipe
-  RecipeLocator.register new PositionalRecipe([
-    ['tree.leaves', 'tree.leaves', 'tree.leaves'],
-    [undefined, 'stick', undefined],
-    [undefined, 'stick', undefined]], new ItemPile('pickaxeDiamond', 1))
+  plugins.preload '!craftingrecipes', {}

   playerInventory = new Inventory(10, 5)

-  plugins.preload 'workbench', {playerInventory:playerInventory, registerRecipe:false}  # TODO: re-enable recipes
+  plugins.preload 'workbench', {playerInventory:playerInventory}

   plugins.preload 'land', {populateTrees: true}

@@ -230,6 +211,27 @@

   plugins.loadOrderly()
   ## plugins are loaded from here on out ##
+
+  # recipes
+  recipes = plugins.all['!craftingrecipes']
+  recipes.thesaurus.registerName 'wood.log', new ItemPile('logOak')
+  recipes.thesaurus.registerName 'wood.log', new ItemPile('logBirch')
+  recipes.thesaurus.registerName 'wood.plank', new ItemPile('plankOak')
+  recipes.thesaurus.registerName 'tree.leaves', new ItemPile('leavesOak')
+
+  recipes.register new AmorphousRecipe(['wood.log'], new ItemPile('plankOak', 2))
+  recipes.register new AmorphousRecipe(['wood.plank', 'wood.plank'], new ItemPile('stick', 4))
+
+  recipes.register new PositionalRecipe([
+    ['wood.plank', 'wood.plank', 'wood.plank'],
+    [undefined, 'stick', undefined],
+    [undefined, 'stick', undefined]], new ItemPile('pickaxeWood', 1))
+
+  # temporary recipe
+  recipes.register new PositionalRecipe([
+    ['tree.leaves', 'tree.leaves', 'tree.leaves'],
+    [undefined, 'stick', undefined],
+    [undefined, 'stick', undefined]], new ItemPile('pickaxeDiamond', 1))

   avatar = plugins.all.player


output oauth codes before sending

output oauth codes before sending

--- index.coffee
+++ index.coffee
@@ -118,6 +118,8 @@
  		app_secret: configs.clef.app_secret
  		code: code

+ 	console.log form
+
  	request.post
  		url: 'https://clef.io/api/v1/authorize'
  		form: form


Add leaves to hotbar textures, and expand to 10 slots ('0' key = 10th)

--- index.coffee
+++ index.coffee
@@ -26,6 +26,7 @@
       'ore_coal',
       'brick_red',
       'obsidian',
+      'leaves_oak',
       'glass',
       ]
     texturePath: '/ProgrammerArt/images/' # subproject with textures
@@ -99,8 +100,10 @@
       avatar.playerSkin.head.visible = show

       avatar.pov(game.pov)
-    else if '0'.charCodeAt(0) < ev.keyCode <= '9'.charCodeAt(0)
+    else if '0'.charCodeAt(0) <= ev.keyCode <= '9'.charCodeAt(0)
       slot = ev.keyCode - '0'.charCodeAt(0)
+      if slot == 0
+        slot = 10
       console.log "switching to slot #{slot}"

       game.currentMaterial = slot


Remove old entry point

Remove old entry point

--- index.coffee
+++ index.coffee
@@ -1,3 +0,0 @@
-module.exports =
-  auth: require './app/api/auth'
-  api: require './app/api/client'


Added "once" function

Added "once" function

--- index.coffee
+++ index.coffee
@@ -156,3 +156,14 @@
 @deepCopy = deepCopy

+###
+  @once
+###
+
+once = (fn)->
+  return (args...)->
+    if fn
+      fn.apply this, args
+      fn = null
+
+@once = once


add comment

add comment

--- client/driver_lines.coffee
+++ client/driver_lines.coffee
@@ -136,7 +136,7 @@
 Template.driver_lines.events
   "click .insert": (event, template) ->
     event.preventDefault()
-    Session.set "typed_name",null
+    Session.set "typed_name",null # FIXME: use ReactiveVar not to spread to other browser windows?
     window.dialog_template = bootbox.dialog
       message: window.renderTmp(Template.AddDriverLineDialog)
       buttons:


Fixing coffeescript syntax error.

Fixing coffeescript syntax error.

Not sure when this was introduced, but this is a syntax error in at least the latest version of coffeescript.

--- coffee/vex.dialog.coffee
+++ coffee/vex.dialog.coffee
@@ -126,10 +126,12 @@
         $buttons = $('<div class="vex-dialog-buttons" />')

         $.each buttons, (index, button) ->
-            $buttons.append $("""<input type="#{button.type}" />""")
+            $button = $("""<input type="#{button.type}" />""")
                 .val(button.text)
                 .addClass(button.className + ' vex-dialog-button ' + (if index is 0 then 'vex-first ' else '') + (if index is buttons.length - 1 then 'vex-last ' else ''))
                 .bind('click.vex', (e) -> button.click($(@).parents(".#{vex.baseClassNames.content}"), e) if button.click)
+
+            $button.appendTo $buttons

         return $buttons

@@ -144,4 +146,4 @@
     module.exports = vexDialogFactory require('jquery'), require('vex')
 else
     # Global
-    window.vex.dialog = vexDialogFactory window.jQuery, window.vex+    window.vex.dialog = vexDialogFactory window.jQuery, window.vex


Adds PartnerShows as an Item type

Adds PartnerShows as an Item type

--- collections/items.coffee
+++ collections/items.coffee
@@ -30,6 +30,9 @@
       when 'Gene'
         Gene = require '../models/gene.coffee'
         new Gene attrs, options
+      when 'PartnerShow'
+        PartnerShow = require '../models/partner_show.coffee'
+        new PartnerShow attrs, options
       else
         new Item attrs, options


Fix bugs

Fix bugs

--- hammurabi.coffee
+++ hammurabi.coffee
@@ -24,7 +24,7 @@

 too_many_people_died = (game)-> game.starved / (game.population + game.starved - game.newcomers) >= 0.45

-valid_seed = (game, tick_data)-> 0 <= tick_data.seed and tick_data.seed <= _.min[game.acres, 10 * game.population, game.bushels]
+valid_seed = (game, tick_data)-> 0 <= tick_data.seed and tick_data.seed <= _.min([game.acres, 10 * game.population, game.bushels])
 valid_feed = (game, tick_data)-> 0 <= tick_data.feed and tick_data.feed <= game.bushels
 valid_acres = (game, tick_data)-> -game.acres <= tick_data.acres and tick_data.acres <= game.acres

@@ -43,7 +43,7 @@
   game.population = game.population - game.starved + game.newcomers;
   game.harvest = _.random(1, 8)
   game.bushels = game.internal_bushels + game.harvest * tick_data.seed
-  game.rats = if has_rat_problems(game) Math.max(0, Math.floor(_.random(1, 3) / 10) * game.bushels)) else 0
+  game.rats = if has_rat_problems(game) then Math.max(0, Math.floor(_.random(1, 3) / 10) * game.bushels) else 0
   game.bushels = game.bushels - game.rats
   game.internal_bushels = game.bushels
   game.price = _.random(1, 10) + 16
@@ -52,8 +52,8 @@
   game.ended = game.year is 10

   game.perfomance = {
-    avg_starved = game.total_starved / game.year
-    acre_person = game.acres / game.population
+    avg_starved : game.total_starved / game.year
+    acre_person : game.acres / game.population
   }
   return context


Remove temp tracking

Remove temp tracking

--- index.coffee
+++ index.coffee
@@ -3,8 +3,6 @@
 path = require 'path'
 temp = require 'temp'
 _ = require 'underscore'
-
-temp.track()

 module.exports = (grunt) ->
   spawn = (options, callback) ->


fix the bug where on first registration nothing happens :(

--- index.coffee
+++ index.coffee
@@ -241,7 +241,7 @@
 												#New user created!  Woohoo!
 												req.session.logged_in_at = Date.now()
 												req.session.user = newUser
-												v0.responses.respond res
+												v0.responses.respond res, "<script type='text/javascript'>parent.postMessage({auth: true}, '*');</script>"
 									else
 										#user already exists.  Let's use that.
 										req.session.logged_in_at = Date.now()


pass visited sitemaps and errors back from done CB

--- index.coffee
+++ index.coffee
@@ -34,6 +34,8 @@
 			inLoc = node.name is 'loc'
 			isURLSet = true if node.name is 'urlset'
 			isSitemapIndex = true if node.name is 'sitemapindex'
+		parserStream.on 'error', (err) ->
+			done err
 		parserStream.on 'text', (text) =>
 			if inLoc
 				if isURLSet
@@ -44,7 +46,7 @@
 					else
 						@sitemap_cb text
 		parserStream.on 'end', () =>
-			done()
+			done null, @visited_sitemaps

 		@_download url, parserStream


export i18n and moment

export i18n and moment

--- index.coffee
+++ index.coffee
@@ -132,19 +132,30 @@
     else
       throw new Error 'Unknown Engine'

+  buildT = (i18n_options) ->
+    return  (name, payload) ->
+      result = translate name, i18n_options.language
+
+      if _.isObject payload
+        for k, v of payload
+          result = result.replace "__#{k}__", v
+
+      return result
+
+  buildM = (i18n_options) ->
+    return ->
+      return moment.apply(@, arguments).locale(i18n_options.language).tz(i18n_options.timezone)
+
   return {
+    i18n: (i18n_options) ->
+      buildT i18n_options
+
+    moment: (i18n_options) ->
+      buildM i18n_options
+
     sendMail: (template_name, to_address, view_data, i18n_options, callback) ->
-      t = (name, payload) ->
-        result = translate name, i18n_options.language
-
-        if _.isObject payload
-          for k, v of payload
-            result = result.replace "__#{k}__", v
-
-        return result
-
-      m = ->
-        return moment.apply(@, arguments).locale(i18n_options.language).tz(i18n_options.timezone)
+      t = buildT i18n_options
+      m = buildM i18n_options

       {engine, file_path, file_name} = getTemplateInfo template_name


minor typos

minor typos

--- fizzylogo_ListClass.coffee
+++ fizzylogo_ListClass.coffee
@@ -253,7 +253,7 @@
           # now actually send the message to the receiver. Note that
           # only part of the message might be consumed, in which case
           # we'll have to find the result from what we can consume and then
-          # sent the remaining part to such reult. This is why
+          # send the remaining part to such result. This is why
           # we have to keep iterating until the whole message is consumed

           if (receiver.flClass == FLNumber or receiver.flClass == FLString or receiver.flClass == FLBoolean) and restOfMessage.isEmpty()


insert file contents earlier so raw stretches get properly handled

--- src/macro-escaper.coffee
+++ src/macro-escaper.coffee
@@ -260,11 +260,11 @@
   # debug '©II6XI', rpr text
   [ R, discard_count, ] = @escape.truncate_text_at_end_command_macro S, text
   whisper "detected <<!end>> macro; discarding approx. #{discard_count} characters" if discard_count > 0
+  R = @escape.insert_macros             S, R
   R = @escape.escape_chrs               S, R
   R = @escape.html_comments             S, R
   # R = @escape.sensitive_ws              S, R
   R = @escape.bracketed_raw_macros      S, R
-  R = @escape.insert_macros             S, R
   R = @escape.action_macros             S, R
   R = @escape.region_macros             S, R
   R = @escape.comma_macros              S, R


removed file removed from includes

removed file removed from includes

--- client/includes.coffee
+++ client/includes.coffee
@@ -389,10 +389,6 @@
   "app/Applications/Ace.kdapplication/acesettingsview.coffee",
   "app/Applications/Ace.kdapplication/acesettings.coffee",
   "app/Applications/Ace.kdapplication/acefindandreplaceview.coffee",
-
-  # groups
-  "app/Applications/Groups.kdapplication/groupdata.coffee"
-  "app/Applications/Groups.kdapplication/AppController.coffee",

   # termlib shell
   #  'app/Applications/Shell.kdapplication/AppRequirements.coffee',
@@ -497,6 +493,8 @@
   # GROUPS

   # groups controllers
+  "app/Applications/Groups.kdapplication/groupdata.coffee"
+  "app/Applications/Groups.kdapplication/AppController.coffee",
   "app/Applications/Groups.kdapplication/controllers/invitationrequestlistcontroller.coffee",

   # groups views
@@ -528,7 +526,6 @@

   # app
   "app/Applications/Groups.kdapplication/AppView.coffee",
-  "app/Applications/Groups.kdapplication/ContentDisplays/controller.coffee",

   # APPS VIEWS
   "app/Applications/Apps.kdapplication/AppView.coffee",


Add lock for preventing concurrent tasks loading

--- static/scripts/models.coffee
+++ static/scripts/models.coffee
@@ -11,12 +11,18 @@
                 @items = []
                 @canLoad = true
                 @offset = 0
+                @loadLock = false

             load: (callback=->@) ->
                 if @canLoad
-                    $http.get(@getUrl()).success (data) =>
-                        @onLoaded(data, callback)
-                    @offset += @limit
+                    if @loadLock
+                        setTimeout 100, => @load(callback)
+                    else
+                        @loadLock = true
+                        $http.get(@getUrl()).success (data) =>
+                            @onLoaded(data, callback)
+                            @loadLock = false
+                        @offset += @limit

             getUrl: ->
                 base = [


Style: use CoffeeScript multi line string


editor needs termStart and termEnd

editor needs termStart and termEnd

--- tutor/specs/components/task-plan/mini-editor/editor.spec.cjsx
+++ tutor/specs/components/task-plan/mini-editor/editor.spec.cjsx
@@ -1,8 +1,11 @@
 {React, _} = require '../../helpers/component-testing'
+
+moment = require 'moment'

 MiniEditor = require '../../../../src/components/task-plan/mini-editor/editor'
 {CourseActions} = require '../../../../src/flux/course'
 {TaskPlanActions, TaskPlanStore} = require '../../../../src/flux/task-plan'
+{TimeStore} = require '../../../../src/flux/time'

 COURSE  = require '../../../../api/courses/1.json'
 COURSE_ID = '1'
@@ -19,6 +22,13 @@
   save: wrapper.find('.-save')
   cancel: wrapper.find('.btn.cancel')

+fakeTerm = ->
+  now = moment(TimeStore.getNow())
+  start = now.clone().add(1, 'year').startOf('year')
+  end = start.clone().add(6, 'months')
+
+  {start, end}
+
 describe 'TaskPlan MiniEditor wrapper', ->

   beforeEach ->
@@ -31,10 +41,16 @@
     CourseActions.loaded(COURSE, COURSE_ID)

     TaskPlanActions.loaded(PLAN, PLAN.id)
+
+    term = fakeTerm()
+
     @props =
       id: PLAN.id
       courseId: COURSE_ID
       onHide: sinon.spy()
+      termStart: term.start
+      termEnd: term.end
+      handleError: sinon.spy()

   afterEach ->


Players can see longer than they can reach - bug fix

--- Content/MAZE/src/game/game.coffee
+++ Content/MAZE/src/game/game.coffee
@@ -239,7 +239,7 @@
         , 7000)

     requestPlayerInitialPosition: (player) ->
-        positions = ([position.x, position.y] for position in @mazeModel.positionsReachableByPlayer(player))
+        positions = ([position.x, position.y] for position in @mazeModel.positionsReachableFromPosition(player.position, player.reachDistance + 2))
         @client.reportBackWhenBrickMovedToPosition([player.position.x, player.position.y], positions, id=player.index)

     requestPlayerPosition: (player) ->


Fix refresh open tabs and state.

Fix refresh open tabs and state.

Fixes refresh of state bug introduced in
046650ba22ac12ebb18d1eb109fe694016eb77c0.

--- background_scripts/main.coffee
+++ background_scripts/main.coffee
@@ -393,10 +393,9 @@
     allFrames: true
     code: Settings.get("userDefinedLinkHintCss")
     runAt: "document_start"
-  chrome.tabs.insertCSS tabId, cssConf, ->
-    if not chrome.runtime.lastError
-      updateOpenTabs(tab)
-      updateActiveState(tabId)
+  chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError
+  updateOpenTabs(tab)
+  updateActiveState(tabId)

 chrome.tabs.onAttached.addListener (tabId, attachedInfo) ->
   # We should update all the tabs in the old window and the new window.


ContentSearch: Code refactor for long options in constructor.

--- client/ide/lib/views/contentsearch/idecontentsearch.coffee
+++ client/ide/lib/views/contentsearch/idecontentsearch.coffee
@@ -10,6 +10,174 @@
 module.exports = class IDEContentSearch extends kd.ModalViewWithForms

   constructor: (options = {}, data) ->
+
+    @setFormOptions options, data
+
+    super options, data
+
+
+  handleKeyDown: (e) ->
+
+    code = e.which or e.keyCode
+    key  = keycode code
+
+    @destroy()  if key is 'esc'
+
+
+  search: ->
+
+    @warningView.hide()
+
+    searchText = Encoder.XSSEncode @findInput.getValue()
+
+    return @searchButton.hideLoader()  if searchText is ''
+
+    @searchText     = searchText
+    @rootPath       = Encoder.XSSEncode @whereInput.getValue()
+    isCaseSensitive = @caseToggle.getValue()
+    isWholeWord     = @wholeWordToggle.getValue()
+    isRegExp        = @regExpToggle.getValue()
+    exts            = editorSettings.getAllExts()
+    include         = "\\*{#{exts.join ','}}"
+    exclureDirs     = Object.keys editorSettings.ignoreDirectories
+    exclureDirs     = " --exclude-dir=#{exclureDirs.join ' --exclude-dir='}"
+    searchText      = @searchText
+
+    unless isRegExp
+      splitText     = searchText.split '\\n'
+      splitText     = splitText.map @grepEscapeRegExp
+      searchText    = splitText.join '\\n'
+
+    searchText      = searchText.replace (new RegExp "\\\'", 'g'), "'\\''"
+    searchText      = searchText.replace /-/g, '\\-'
+
+    flags = [
+      '-s'                           # Silent mode
+      '-r'                           # Recursively search subdirectories listed.
+      '-n'                           # Each output line is preceded by its relative line number in the file
+      '-A 3'                         # Print num lines of trailing context after each match.
+      '-B 3'                         # Print num lines of trailing context before each match.
+      '-i'                           # Match case insensitively
+      '-w'                           # Only match whole words
+      '--color=never'                # Disable color output to get plain text
+      '--binary-files=without-match' # Do not search binary files
+    ]
+
+    flags.splice flags.indexOf('-i'), 1  if isCaseSensitive
+    flags.splice flags.indexOf('-w'), 1  unless isWholeWord
+
+    command = "grep #{flags.join ' '} #{exclureDirs} --include=#{include} '#{searchText}' \"#{@escapeShell @rootPath}\""
+
+    { machine } = @getData()
+    machine.getBaseKite().exec({ command })
+      .then  (res) => @formatOutput res.stdout, @bound 'createResultsView'
+      .catch (err) =>
+        @showWarning 'Something went wrong, please try again.'
+        kd.warn err
+
+
+  escapeRegExp: (str) -> str.replace /([.*+?\^${}()|\[\]\/\\])/g, '\\$1'
+
+  escapeShell: (str) -> str.replace /([\\"'`$\s\(\)<>])/g, '\\$1'
+
+  grepEscapeRegExp: (str) -> str.replace /[[\]{}()*+?.,\\^$|#\s"']/g, '\\$&'
+
+
+  formatOutput: (output, callback = kd.noop) ->
+
+    return @showWarning 'Something went wrong, please try again.', yes  if output.stderr
+
+    # Regexes
+    mainLineRegex           = /^:?([\s\S]+):(\d+):([\s\S]*)$/
+    contextLineRegex        = /^([\s\S]+)\-(\d+)\-([\s\S]*)$/
+    lines                   = output.split '\n'
+    formatted               = {}
+    stats                   =
+      numberOfMatches       : 0
+      numberOfSearchedFiles : 0
+
+    formatted = lines
+    .map (line) -> # Remove erroneous whitespace
+      return line.trimLeft()
+    .filter (line) -> # Skip lines that aren't one of these
+      return mainLineRegex.test(line) or contextLineRegex.test(line)
+    .map (line) ->
+      # Get the matches
+      return line.match(mainLineRegex) or line.match(contextLineRegex)
+    .reduce( (accu, matches) ->
+      # Extract matches
+      [fileName, lineNumber, line] = [matches[1], parseInt(matches[2], 10), matches[3]]
+
+      # new filename found
+      unless accu[fileName]
+        accu[fileName] = []
+
+      # Add line to list of lines found for this filename
+      accu[fileName].push { lineNumber, line, occurence: mainLineRegex.test(matches[0]) }
+
+      # Increment matches
+      stats.numberOfMatches += 1
+
+      return accu
+    , {})
+
+    stats.numberOfSearchedFiles = Object.keys(formatted).length
+
+    # No results
+    if stats.numberOfMatches is 0
+      return @showWarning 'No results found, please refine your search.'
+
+    # Send results
+    callback formatted, stats
+
+
+  createResultsView: (result, stats) ->
+
+    { searchText }  = this
+    isCaseSensitive = @caseToggle.getValue()
+    { machine }     = @getData()
+    resultsView     = new IDEContentSearchResultView { result, stats, searchText, isCaseSensitive, machine }
+
+    @emit 'ViewNeedsToBeShown', resultsView
+    @destroy()
+
+
+  showWarning: (text, isError) ->
+
+    view = @warningView
+
+    view.unsetClass 'error'
+    view.setClass   'error'  if isError
+    view.updatePartial text
+    view.show()
+    @searchButton.hideLoader()
+
+
+  viewAppended: ->
+
+    super
+
+    @addSubView new kd.CustomHTMLView cssClass: 'icon'
+
+    searchForm        = @modalTabs.forms.Search
+    { @warningView  } = searchForm.fields
+    { @searchButton } = searchForm.buttons
+    { @findInput,  @whereInput } = searchForm.inputs
+    { @caseToggle, @regExpToggle, @wholeWordToggle } = searchForm.inputs
+
+    @findInput.setFocus()
+
+    for name, view of searchForm.fields
+      do (name, view) ->
+        if name in [ 'caseToggle', 'regExpToggle', 'wholeWordToggle' ]
+          [ label, wrapper ] = view.getSubViews()
+          [ checkbox ]       = wrapper.getSubViews()
+
+          label.on 'click', =>
+            if checkbox.getValue() then checkbox.setValue 0 else checkbox.setValue 1
+
+
+  setFormOptions: (options, data) ->

     { rootPath } = data.workspace

@@ -58,166 +226,3 @@
             warningView     :
               itemClass     : kd.View
               cssClass      : 'hidden notification'
-
-    super options, data
-
-
-  handleKeyDown: (e) ->
-
-    code = e.which or e.keyCode
-    key  = keycode code
-
-    @destroy()  if key is 'esc'
-
-
-  search: ->
-
-    @warningView.hide()
-
-    searchText = Encoder.XSSEncode @findInput.getValue()
-
-    return @searchButton.hideLoader()  if searchText is ''
-
-    @searchText     = searchText
-    @rootPath       = Encoder.XSSEncode @whereInput.getValue()
-    isCaseSensitive = @caseToggle.getValue()
-    isWholeWord     = @wholeWordToggle.getValue()
-    isRegExp        = @regExpToggle.getValue()
-    exts            = editorSettings.getAllExts()
-    include         = "\\*{#{exts.join ','}}"
-    exclureDirs     = Object.keys editorSettings.ignoreDirectories
-    exclureDirs     = " --exclude-dir=#{exclureDirs.join ' --exclude-dir='}"
-    searchText      = @searchText
-
-    unless isRegExp
-      splitText     = searchText.split '\\n'
-      splitText     = splitText.map @grepEscapeRegExp
-      searchText    = splitText.join '\\n'
-
-    searchText      = searchText.replace (new RegExp "\\\'", 'g'), "'\\''"
-    searchText      = searchText.replace /-/g, '\\-'
-
-    flags = [
-      '-s'                           # Silent mode
-      '-r'                           # Recursively search subdirectories listed.
-      '-n'                           # Each output line is preceded by its relative line number in the file
-      '-A 3'                         # Print num lines of trailing context after each match.
-      '-B 3'                         # Print num lines of trailing context before each match.
-      '-i'                           # Match case insensitively
-      '-w'                           # Only match whole words
-      '--color=never'                # Disable color output to get plain text
-      '--binary-files=without-match' # Do not search binary files
-    ]
-
-    flags.splice flags.indexOf('-i'), 1  if isCaseSensitive
-    flags.splice flags.indexOf('-w'), 1  unless isWholeWord
-
-    command = "grep #{flags.join ' '} #{exclureDirs} --include=#{include} '#{searchText}' \"#{@escapeShell @rootPath}\""
-
-    { machine } = @getData()
-    machine.getBaseKite().exec({ command })
-      .then  (res) => @formatOutput res.stdout, @bound 'createResultsView'
-      .catch (err) =>
-        @showWarning 'Something went wrong, please try again.'
-        kd.warn err
-
-
-  escapeRegExp: (str) -> str.replace /([.*+?\^${}()|\[\]\/\\])/g, '\\$1'
-
-  escapeShell: (str) -> str.replace /([\\"'`$\s\(\)<>])/g, '\\$1'
-
-  grepEscapeRegExp: (str) -> str.replace /[[\]{}()*+?.,\\^$|#\s"']/g, '\\$&'
-
-
-  formatOutput: (output, callback = kd.noop) ->
-
-    return @showWarning 'Something went wrong, please try again.', yes  if output.stderr
-
-    # Regexes
-    mainLineRegex           = /^:?([\s\S]+):(\d+):([\s\S]*)$/
-    contextLineRegex        = /^([\s\S]+)\-(\d+)\-([\s\S]*)$/
-    lines                   = output.split '\n'
-    formatted               = {}
-    stats                   =
-      numberOfMatches       : 0
-      numberOfSearchedFiles : 0
-
-    formatted = lines
-    .map (line) -> # Remove erroneous whitespace
-      return line.trimLeft()
-    .filter (line) -> # Skip lines that aren't one of these
-      return mainLineRegex.test(line) or contextLineRegex.test(line)
-    .map (line) ->
-      # Get the matches
-      return line.match(mainLineRegex) or line.match(contextLineRegex)
-    .reduce( (accu, matches) ->
-      # Extract matches
-      [fileName, lineNumber, line] = [matches[1], parseInt(matches[2], 10), matches[3]]
-
-      # new filename found
-      unless accu[fileName]
-        accu[fileName] = []
-
-      # Add line to list of lines found for this filename
-      accu[fileName].push { lineNumber, line, occurence: mainLineRegex.test(matches[0]) }
-
-      # Increment matches
-      stats.numberOfMatches += 1
-
-      return accu
-    , {})
-
-    stats.numberOfSearchedFiles = Object.keys(formatted).length
-
-    # No results
-    if stats.numberOfMatches is 0
-      return @showWarning 'No results found, please refine your search.'
-
-    # Send results
-    callback formatted, stats
-
-
-  createResultsView: (result, stats) ->
-
-    { searchText }  = this
-    isCaseSensitive = @caseToggle.getValue()
-    { machine }     = @getData()
-    resultsView     = new IDEContentSearchResultView { result, stats, searchText, isCaseSensitive, machine }
-
-    @emit 'ViewNeedsToBeShown', resultsView
-    @destroy()
-
-
-  showWarning: (text, isError) ->
-
-    view = @warningView
-
-    view.unsetClass 'error'
-    view.setClass   'error'  if isError
-    view.updatePartial text
-    view.show()
-    @searchButton.hideLoader()
-
-
-  viewAppended: ->
-
-    super
-
-    @addSubView new kd.CustomHTMLView cssClass: 'icon'
-
-    searchForm        = @modalTabs.forms.Search
-    { @warningView  } = searchForm.fields
-    { @searchButton } = searchForm.buttons
-    { @findInput,  @whereInput } = searchForm.inputs
-    { @caseToggle, @regExpToggle, @wholeWordToggle } = searchForm.inputs
-
-    @findInput.setFocus()
-
-    for name, view of searchForm.fields
-      do (name, view) ->
-        if name in [ 'caseToggle', 'regExpToggle', 'wholeWordToggle' ]
-          [ label, wrapper ] = view.getSubViews()
-          [ checkbox ]       = wrapper.getSubViews()
-
-          label.on 'click', =>
-            if checkbox.getValue() then checkbox.setValue 0 else checkbox.setValue 1


Koding group allows creation of groups

Koding group allows creation of groups

--- client/landing/app/MainApp/VirtualizationController.coffee
+++ client/landing/app/MainApp/VirtualizationController.coffee
@@ -58,9 +58,6 @@
   createGroupVM:(type='personal', callback)->
     defaultVMOptions = {cpu : 1, disk : 1, ram : 1}
     group = KD.singletons.groupsController.getCurrentGroup()
-
-    if group.slug is 'koding'
-      return callback "Koding group does not support to create additional VMs"

     group.fetchBundle (err, bundle)->
       switch type


Fix twitter deconnect

Fix twitter deconnect

--- core/app/assets/javascripts/jquery_ujs_override.coffee
+++ core/app/assets/javascripts/jquery_ujs_override.coffee
@@ -6,7 +6,7 @@
     $('<meta name="csrf-token">').appendTo(document.head)
     $('<meta name="csrf-param">').appendTo(document.head)
   $('meta[name=csrf-token]').attr('content', safeLocalStorage.getItem('factlink_csrf_token'))
-  $('meta[name=csrf-param]').attr('content', safeLocalStorage.getItem('factlink_csrf_token'))
+  $('meta[name=csrf-param]').attr('content', safeLocalStorage.getItem('factlink_csrf_param'))

 updateCsrfTagsBeforeExecution = (func) -> ->
   updateRailsCsrfMetaTags()


Add a go function to directly go to a particular slide.

--- coffee/presentation.coffee
+++ coffee/presentation.coffee
@@ -42,16 +42,24 @@

   prev: =>
     $prev = @$current.prev('section')
-    if $prev.length
-      @y -= $prev.outerHeight()
-      $('.presentation').css('transform', "translateY(-#{@y}px)")
-      @$current = $prev
+    @go($prev)

   next: =>
     $next = @$current.next('section')
-    if $next.length
-      @y += @$current.outerHeight()
-      $('.presentation').css('transform', "translateY(-#{@y}px)")
-      @$current = $next
+    @go($next)

-presentation = new Presentation()
+  go: (slide = 1) ->
+    $section = if (slide instanceof $) then slide else $("##{slide}")
+    if $section.length is 0
+      $section = $('section').slice(parseInt(slide) - 1).first()
+
+    if $section.length
+      y = $section.prevAll().map(->
+        $(@).outerHeight()
+      ).get().reduce((memo, height) ->
+        memo + height
+      , 0)
+      $('.presentation').css('transform', "translateY(-#{y}px)")
+      @$current = $section
+
+window.presentation = new Presentation()


no negative decimal precision

no negative decimal precision

--- coffee/src/humanize.coffee
+++ coffee/src/humanize.coffee
@@ -76,6 +76,7 @@

 # converts an integer into its most compact representation
 @Humanize.compactInteger = (input, decimals=0) ->
+    decimals = Math.max decimals, 0
     number               = parseInt input, 10
     signString           = if number < 0 then "-" else ""
     unsignedNumber       = Math.abs number


update rundeck plugin

update rundeck plugin

--- hubot/scripts/slack_rundeck.coffee
+++ hubot/scripts/slack_rundeck.coffee
@@ -93,7 +93,7 @@
       else
         @logger.debug body
         parser.parseString body, (e, result) ->
-          cb JSON.stringify(result)
+          cb result

 class Job
   constructor: (data) ->
@@ -189,7 +189,8 @@
     if robot.auth.hasRole(msg.envelope.user, rundeck.adminRole)
       rundeck.jobs().run name, null, (job, results) ->
         if job
-          msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
+          #msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
+          msg.send "Running job #{name}: #{results}"
         else
           msg.send "Could not execute Rundeck job \"#{name}\"."
     else


Add short hand for api.user collection

Add short hand for api.user collection

--- coffee/db/mongo.coffee
+++ coffee/db/mongo.coffee
@@ -9,7 +9,7 @@
     throw err if err
     @db = database

-    for col in ['arrangementer', 'bilder', 'grupper', 'områder', 'turer', 'steder']
+    for col in ['arrangementer', 'bilder', 'grupper', 'områder', 'turer', 'steder', 'api.users']
       @[col] = @db.collection col

     @emit 'ready'


Add support for quick Redis cache lookup

--- coffee/document.coffee
+++ coffee/document.coffee
@@ -7,16 +7,31 @@
   return res.json 400, error: 'Invalid ObjectID' if not /^[a-f0-9]{24}$/.test id
   req.id = new ObjectID id
   return next() if req.method is 'OPTIONS'
-  cb = (err, docs) ->
-    return res.json 404, error: 'Document Not Found' if docs.length is 0
-    req.etag = crypto.createHash('md5').update(docs[0]._id + docs[0].endret).digest("hex")
+
+  result = (err, doc) ->
+    if not doc or doc.status is 'Slettet'
+      return res.json 404, error: 'Document Not Found'
+    req.etag = crypto.createHash('md5').update(req.id + doc.endret).digest("hex")
     return res.status(304).end() if req.get('if-none-match') is req.etag
     res.set 'ETag', req.etag
-    res.set 'Last-Modified', new Date(docs[0].endret).getTime()
-    cb = err = docs = null
+    res.set 'Last-Modified', new Date(doc.endret).toUTCString()
+    cb = err = doc = null
     next()

-  req.col.find({_id: req.id}, {endret: true}, {limit: 1}).toArray cb
+  key = "#{req.type}:#{req.id}"
+  req.cache.hgetall key, (err, data) ->
+    if data and typeof req.query.nocache is 'undefined'
+      res.set 'X-Cache-Hit', 'true'
+      return result(null, data)
+
+    req.col.find({_id: req.id}, {endret: true, status: true}, {limit: 1}).toArray (err, docs) ->
+      endret = docs[0]?.endret or new Date().toISOString()
+      status = docs[0]?.status or 'Slettet'
+
+      req.cache.hmset key, "endret", endret, "status", status, (err, data) ->
+        result null,
+          endret: endret
+          status: status

 exports.options = (req, res, next) ->
   res.setHeader 'Access-Control-Allow-Methods', 'GET, PUT, PATCH, DELETE'


Add tagged_callback so only the last query's results will be displayed

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -612,11 +612,17 @@
                 @hide_suggestion()
             return @

-
-        callback_render: (data) =>
-            execution_time = new Date() - @start_time
-            @.$('.loading_query_img').css 'display', 'none'
-            @data_container.render(@query, data, execution_time)
+        create_tagged_callback: =>
+            id = Math.random()
+            @last_id = id
+            console.log id
+            tagged_callback = (data) =>
+                if id is @last_id
+                    execution_time = new Date() - @start_time
+                    @.$('.loading_query_img').css 'display', 'none'
+                    @data_container.render(@query, data, execution_time)
+
+            return tagged_callback

         execute_query: =>
             window.result = {}
@@ -642,9 +648,10 @@

             @.$('.loading_query_img').css 'display', 'block'

-            full_query = @query + '.run(this.callback_render)'
-            @start_time = new Date()
+            full_query = @query + '.run(tagged_callback)'
             try
+                tagged_callback = @create_tagged_callback()
+                @start_time = new Date()
                 eval(full_query)
             catch err
                 @.$('.loading_query_img').css 'display', 'none'


Don't use regex to remove comments... Related to #1212

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1354,7 +1354,6 @@
                             element.type = 'string'
                             start = i
                         continue
-
                     result_inline_comment = @regex.inline_comment.exec query.slice i
                     if result_inline_comment?
                         to_skip = result_inline_comment[0].length-1
@@ -2300,9 +2299,7 @@

             @raw_query = @codemirror.getValue()

-            @query = @raw_query.replace(/(\s)*\/\/.*\n/g, '\n')
-            @query = @query.replace(/(\s)*\/\*[^]*\*\//g, '')
-            @query = @replace_new_lines_in_query @query # Save it because we'll use it in @callback_multilples_queries
+            @query = @clean_query @raw_query # Save it because we'll use it in @callback_multilples_queries

             # Execute the query
             try
@@ -2336,6 +2333,7 @@
                 full_query += @queries[@index]

                 try
+                    console.log full_query
                     rdb_query = @evaluate(full_query)
                 catch err
                     @.$('.loading_query_img').hide()
@@ -2511,7 +2509,8 @@
         #   world')
         # becomes
         #   r.expr('hello\nworld')
-        replace_new_lines_in_query: (query) ->
+        #  We also remove comments from the query
+        clean_query: (query) ->
             is_parsing_string = false
             start = 0

@@ -2519,6 +2518,7 @@
             for char, i in query
                 if to_skip > 0
                     to_skip--
+                    start++
                     continue

                 if is_parsing_string is true
@@ -2537,11 +2537,15 @@

                     result_inline_comment = @regex.inline_comment.exec query.slice i
                     if result_inline_comment?
-                        to_skip = result_inline_comment[0].length-1
+                        result_query += query.slice(start, i).replace(/\n/g, '\\\\n')
+                        start = i
+                        to_skip = result_inline_comment[0].length
                         continue
                     result_multiple_line_comment = @regex.multiple_line_comment.exec query.slice i
                     if result_multiple_line_comment?
-                        to_skip = result_multiple_line_comment[0].length-1
+                        result_query += query.slice(start, i).replace(/\n/g, '\\\\n')
+                        start = i
+                        to_skip = result_multiple_line_comment[0].length
                         continue
             if is_parsing_string
                 result_query += query.slice(start, i).replace(/\n/g, '\\\\n')


More play turn benchmarks

More play turn benchmarks

--- test/perf.coffee
+++ test/perf.coffee
@@ -54,3 +54,10 @@
     playTurn = (state) -> state.play agent.nextAction state

     run 'minimax depth 2 play turn from initial conditions', -> playTurn new ConnectFour
+
+    state = new ConnectFour
+    state = playTurn state for i in [0...10]
+    run 'minimax depth 2 play turn after 10 turns', -> playTurn state
+
+    state = playTurn state for i in [0...10]
+    run 'minimax depth 2 play turn after 20 turns', -> playTurn state


Revert "Add categories."

Revert "Add categories."

This reverts commit d4bdb686f0cfc4c982ee014a8ca96f17b166791f.

--- app/assets/javascripts/application/seeds_store.js.coffee
+++ app/assets/javascripts/application/seeds_store.js.coffee
@@ -57,7 +57,7 @@
   countPods: () ->
     @db.count('pod')
   updatePods: (pods) ->
-    #@db.put('pod', pods)
+    @db.put('pod', pods)
   findCategories: () ->
     iterator = new ydn.db.ValueIterator("category")
     @db.values iterator, 1000
@@ -65,5 +65,5 @@
     keyRange = ydn.db.KeyRange.only(name)
     @db.values 'category', keyRange
   updateCategories: (categories) ->
-    #@db.clear('category')
-    #@db.put('category', categories)
+    @db.clear('category')
+    @db.put('category', categories)


EDSC-326: re-focusing on non-selected granule will select that granule

--- app/assets/javascripts/models/ui/granules_list.js.coffee
+++ app/assets/javascripts/models/ui/granules_list.js.coffee
@@ -21,6 +21,8 @@
       $granuleList.on 'blur', (e) =>
         @_hasFocus = false
       $granuleList.on 'focus.panel-list-item', (e) =>
+        # We want click behavior when we have focus, but not when the focus came from the
+        # click's mousedown.  Ugh.
         setTimeout((=> @_hasFocus = true), 500)

       map.focusDataset(@dataset)
@@ -97,9 +99,9 @@
       granule == @stickied()

     toggleStickyFocus: (granule, e) =>
-      return if @stickied() && !@_hasFocus
+      return if @isStickied(granule) && !@_hasFocus
       return true if $(e?.target).closest('a').length > 0
-      granule = null if @stickied() == granule
+      granule = null if @isStickied(granule)
       @_map.fire 'edsc.stickygranule', granule: granule

   exports = GranulesList


use location for autocomplete

use location for autocomplete

--- app/assets/javascripts/modules/query_autocomplete.coffee
+++ app/assets/javascripts/modules/query_autocomplete.coffee
@@ -39,7 +39,6 @@
 generateSource = ->
   Clarat.ttAdapter
     hitsPerPage: 5
-    aroundLatLng: Clarat.currentGeolocation # TODO: use entered location
     aroundRadius: 999999999
     aroundPrecision: 500

@@ -50,11 +49,12 @@
 Clarat.ttTotalResults = undefined
 Clarat.ttAdapter = (params) ->
   (query, cb) ->
+    params['aroundLatLng'] = Clarat.currentGeolocation #<- difference to Algolia
     Clarat.index.search(
       query,
       (success, content) ->
         if success
-          Clarat.ttTotalResults = content.nbHits # <-only difference to Algolia
+          Clarat.ttTotalResults = content.nbHits # <- difference to Algolia
           cb(content.hits)
       , params
     )


Fix scroll logic.

Fix scroll logic.

--- app/assets/javascripts/web/views/messages_view.js.coffee
+++ app/assets/javascripts/web/views/messages_view.js.coffee
@@ -15,10 +15,11 @@
   didResize: (textarea) -> @organizeLayers()

   didScroll: (view,e) ->
-    distance = @scrollBody()[0].scrollHeight - @scrollBody().outerHeight()
-    top      = @scrollBody().scrollTop()
+    distance = (
+      @scrollBody()[0].scrollHeight - (@scrollBody().outerHeight() + @scrollOffset)
+    ) - @scrollBody().scrollTop()

-    @set('desiredScroll', (distance - top)) unless @isScrolledToTop()
+    @set('desiredScroll', distance) unless @isScrolledToTop()
     @get('controller').send('loadMore') if @isScrolledToTop()

   didInsertElement: ->


add spin off check

add spin off check

--- library/exposure/policies.coffee
+++ library/exposure/policies.coffee
@@ -80,11 +80,13 @@
     policy: (privilege, parameters..., callback) ->
         noCallback = "please supply a receiver callback"
         noPrivilege = "please supply a privilege string"
+        barebones = "this is not isolated or spinned off"
         assert _.isFunction(callback or 0), noCallback
         assert _.isString(privilege or 0), noPrivilege
         assert _.isArray(parameters), "signature error"
         assert _.isObject envelope = Object.create this
         assert qualifiers = @entityQualifiers? envelope
+        assert _.isObject(this.__origin or 0), barebones
         g = (z) -> z.inspector.apply envelope, parameters
         cmp = (sample) -> sample.toString() in qualifiers
         matches = (z, cb) -> envelope.decision = cb; g(z)


Ability to use validation outside of req

--- library/membrane/validate.coffee
+++ library/membrane/validate.coffee
@@ -39,6 +39,47 @@

 {Primitive} = require "./primitive"
 {Barebones} = require "./skeleton"
+
+# A base class for all the validation contexts. Basically the conext
+# encapsulates the necessary internal details as well as provides a
+# set of validators to be used. So in order to create custom ones, you
+# will need to create a descendant of this class and set is as context.
+# The context setting happens on the servies level with the directive.
+module.exports.Context = class Context extends Primitive
+
+    # Create a new instance of the validation context with the given
+    # value set as the subject for validation. Each validator will
+    # refer to this value to perform their validation logic on it.
+    # If the message arg is passed in then it will be used by force.
+    constructor: (@value, @message) ->
+
+    # Chain in a new validator to the context. Validator is a method
+    # that will be called within this context that should look up the
+    # value and see if it fits the validator logic and report either
+    # an error, via standard async callback or pass on to the next one.
+    chain: (validator) ->
+        noFunction = "a #{validator} is not a function"
+        wrongParams = "a validator should have 1 argument"
+        assert _.isFunction(validator), noFunction
+        assert validator.length is 1, wrongParams
+        @emit "chain", validator, @validators
+        (@validators ?= []).push validator
+
+    # Run the entire stack of chained validators. Once done, callback
+    # will be called with an error parameter passed in. If any of the
+    # validators in the chain has failed, then this parameter will have
+    # an error object set in with an appropriate message about mistake.
+    run: (callback) ->
+        validators = @validators ?= []
+        invalid = "inconsistent validators"
+        assert _.isArray validators, invalid
+        async.series validators, (error) =>
+            failed = _.isObject error
+            custom = _.isString @message
+            matches = failed and custom
+            error.message = @message if matches
+            @emit "run", error
+            callback error

 # This is an ABC service intended to be used only as a compund. It
 # provides a complete validation solution for the framework. The
@@ -84,7 +125,7 @@
         return @fail response, params: map

     # Given the request with possible validation contexts appended
-    # run all the validator contexsts in parallel and wait for the
+    # run all the validator contexts in parallel and wait for the
     # completion. If no validation mistakes found, run continuation.
     # If some mistakes are found, however, run `@renderValidation`.
     validateAndContinue: (request, response, continuation) ->
@@ -99,13 +140,34 @@
         transformed =  _.map _.values(vcontexts), transformer
         transformed = _.object _.keys(vcontexts), transformed
         async.parallel transformed, (error, results) =>
-            assert not error, "internval valdation error"
+            assert not error, "internal valdation error"
             errors = _.any _.values(results), _.isObject
             hasRender = _.isFunction @renderValidation
             assert hasRender, "no method to render validation"
             params = [results, request, response, continuation]
             return @renderValidation params... if errors
             return continuation.bind(this)()
+
+    # Given the request with possible validation contexts appended
+    # run all the validator contexts in parallel and wait for the
+    # completion. Once the validation has been completed, call the
+    # continuation routine and pass the validation results to it.
+    withValidation: (storage, continuation) ->
+        notStorage = "a #{storage} is not a storage"
+        notContinuation = "a #{continuation} is not function"
+        transformer = (o) -> (c) -> o.run (e) -> c null, e
+        assert _.isFunction(continuation), notContinuation
+        assert _.isObject(storage), notStorage
+        vcontexts = storage.vcontexts or {}
+        transformed =  _.map _.values(vcontexts), transformer
+        transformed = _.object _.keys(vcontexts), transformed
+        async.parallel transformed, (error, results) =>
+            assert not error, "internal valdation error"
+            errors = _.any _.values(results), _.isObject
+            results = _.transform results, (acc, val, key) ->
+                messaged = _.isString val?.message
+                acc[key] = val.message if messaged
+            return continuation.bind(this) errors, results

     # Create a validation context for the parameter designated by
     # the `name` and add it to the current request. If the `message`
@@ -120,43 +182,15 @@
         created = new context value, message
         vcontexts[name] = created; created

-# A base class for all the validation contexts. Basically the conext
-# encapsulates the necessary internal details as well as provides a
-# set of validators to be used. So in order to create custom ones, you
-# will need to create a descendant of this class and set is as context.
-# The context setting happens on the servies level with the directive.
-module.exports.Context = class Context extends Primitive
-
-    # Create a new instance of the validation context with the given
-    # value set as the subject for validation. Each validator will
-    # refer to this value to perform their validation logic on it.
-    # If the message arg is passed in then it will be used by force.
-    constructor: (@value, @message) ->
-
-    # Run the entire stack of chained validators. Once done, callback
-    # will be called with an error parameter passed in. If any of the
-    # validators in the chain has failed, then this parameter will have
-    # an error object set in with an appropriate message about mistake.
-    run: (callback) ->
-        validators = @validators ?= []
-        invalid = "inconsistent validators"
-        assert _.isArray validators, invalid
-        async.series validators, (error) =>
-            failed = _.isObject error
-            custom = _.isString @message
-            matches = failed and custom
-            error.message = @message if matches
-            @emit "run", error
-            callback error
-
-    # Chain in a new validator to the context. Validator is a method
-    # that will be called within this context that should look up the
-    # value and see if it fits the validator logic and report either
-    # an error, via standard async callback or pass on to the next one.
-    chain: (validator) ->
-        noFunction = "a #{validator} is not a function"
-        wrongParams = "a validator should have 1 argument"
-        assert _.isFunction(validator), noFunction
-        assert validator.length is 1, wrongParams
-        @emit "chain", validator, @validators
-        (@validators ?= []).push validator
+    # Create a validation context for the parameter designated by
+    # the `name` and add it to the supplied storage. If the `message`
+    # is supplied then it will be forced as an error messages. Use
+    # this method to automatically obtain contex for the parameter.
+    value: (storage, name, message) ->
+        context = @constructor.validationContext?()
+        context = Context unless _.isObject context
+        assert storage; value = storage[name]
+        vcontexts = (storage.vcontexts ?= {})
+        return obtain if obtain = vcontexts[name]
+        created = new context value, message
+        vcontexts[name] = created; created


Add comma to replaced characters in urn_to_id

--- src/js/cts-cite-driver.coffee
+++ src/js/cts-cite-driver.coffee
@@ -15,7 +15,7 @@
   cite_collection_editor_url: "http://cite-harpokration.appspot.com/editor"

 urn_to_id = (urn) ->
-  urn.replace(/[:.'-]/g,'_')
+  urn.replace(/[:.,'-]/g,'_')

 urn_to_head = (urn) ->
   urn.replace(/^.*:/,'').replace(/_/g,' ')


Add an OPTIMIZE comment

Add an OPTIMIZE comment

--- lib/lint-view.coffee
+++ lib/lint-view.coffee
@@ -93,6 +93,7 @@

     currentCursorPosition = @editor.getCursor().getBufferPosition()

+    # OPTIMIZE: Consider using binary search.
     neighborViolation = _[enumerationMethod] @lastViolations, (violation) ->
       violation.bufferRange.start[comparingMethod](currentCursorPosition)


Fix generated css

Fix generated css

--- lib/ltconsole.coffee
+++ lib/ltconsole.coffee
@@ -39,7 +39,7 @@

   # style for actual message
   sheet.insertRule(
-    ".latextools-console-message { font-family: #{font}; font-size: #{fontSize};",
+    ".latextools-console-message { font-family: #{font}; font-size: #{fontSize}; }",
     index
   )


FIXES #309: Optional parens and interpolation.

--- lexer.coffee
+++ lexer.coffee
@@ -56,7 +56,6 @@
       @chunk: @code.slice @i
       @extract_next_token()
     @close_indentation()
-    return @tokens if o.rewrite is off
     (new Rewriter()).rewrite @tokens

   # At every position, run through this list of attempted matches,
@@ -366,7 +365,8 @@
           tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
           inner: expr.substring(2, expr.length - 1)
           if inner.length
-            nested: lexer.tokenize "($inner)", {rewrite: no, line: @line}
+            nested: lexer.tokenize "($inner)", {line: @line}
+            (nested[index][0]: ')') for value, index in nested when value[0] is 'CALL_END'
             nested.pop()
             tokens.push ['TOKENS', nested]
           else


Fix line numbers when first line is indented.

* Offset @chunkLine for inserted line break.
* Avoid line break insertion for blank lines.

--- lexer.coffee
+++ lexer.coffee
@@ -35,7 +35,6 @@
   # unless explicitly asked not to.
   tokenize: (code, opts = {}) ->
     @literate = opts.literate  # Are we lexing literate CoffeeScript?
-    code      = @clean code    # The stripped, cleaned original source code.
     @indent   = 0              # The current indentation level.
     @indebt   = 0              # The over-indentation at the current level.
     @outdebt  = 0              # The under-outdentation at the current level.
@@ -47,6 +46,7 @@
         opts.line or 0         # The start line for the current @chunk.
     @chunkColumn =
         opts.column or 0       # The start column of the current @chunk.
+    code = @clean code         # The stripped, cleaned original source code.

     # At every position, run through this list of attempted matches,
     # short-circuiting if any of them succeed. Their order determines precedence:
@@ -80,8 +80,10 @@
   # by removing all lines that aren't indented by at least four spaces or a tab.
   clean: (code) ->
     code = code.slice(1) if code.charCodeAt(0) is BOM
-    code = "\n#{code}" if WHITESPACE.test code
     code = code.replace(/\r/g, '').replace TRAILING_SPACES, ''
+    if WHITESPACE.test code
+        code = "\n#{code}"
+        @chunkLine--
     if @literate
       lines = for line in code.split('\n')
         if match = LITERATE.exec line


fix double click to open conversation in old conversations

fixes CNVS-9217, CNVS-9790

test plan:
 - navigate to old conversations.
 - double click on a conversation and make sure it opens
   correctly.

Change-Id: Id5578d343532fabd45106c16517f35744900c6d5
Reviewed-on: https://gerrit.instructure.com/27071
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Braden Anderson <a6cc2cef7d25f27847876bdb81651755fcebacf0@instructure.com>
QA-Review: Steven Shepherd <e794458448443812fb2685675aab3bbd1a3891bc@instructure.com>
Product-Review: Jon Willesen <eb5ce5e6a76c243326fecfa94534101600f87bd9@instructure.com>

--- app/coffeescripts/conversations/ConversationsList.coffee
+++ app/coffeescripts/conversations/ConversationsList.coffee
@@ -75,7 +75,7 @@
     clicked: (e) =>
       # ignore clicks that come from the gear menu
       unless $(e.target).closest('.admin-links').length
-        @select $(e.currentTarget).data('id')
+        @select $(e.currentTarget).attr('data-id')

     lastMessageKey: ->
       if @scope is 'sent'


Restore the colour and habitat filters

Restore the colour and habitat filters

--- workflows/field.coffee
+++ workflows/field.coffee
@@ -18,6 +18,8 @@
       type: OrchidFilterTask.type
       question: 'Can you identify the orchid by comparing with plants from the following grid?'
       filters: [
+        require './filter-colours'
+        require './filter-habitats'
       ]
       choices: require './orchid-types'
       next: 'marking'


hub/proxy: be explicit about the type of error

--- src/smc-hub/proxy.coffee
+++ src/smc-hub/proxy.coffee
@@ -75,7 +75,7 @@
             # status code 4xx to indicate this is a client problem and not 5xx, a server problem
             # 426 means "upgrade required"
             res.writeHead(426, {'Content-Type':'text/html'})
-            res.end("RELOAD COCALC TAB OR RESTART BROWSER -- version=#{version} < required_version=#{min_version}")
+            res.end("426 (UPGRADE REQUIRED): reload CoCalc tab or restart your browser -- version=#{version} < required_version=#{min_version}")
         return true
     else
         return false


made test script DRYer and removed some whitespace/rendundant comments

--- test/test.coffee
+++ test/test.coffee
@@ -14,10 +14,12 @@
 # can't test watch because the process hangs - the internals of
 # the watch command are tested below in the compiler section though

+files_exist = (test_path, files) ->
+  for file in files
+    fs.existsSync(path.join(test_path, file)).should.be.ok
+
 describe 'command', ->
-
   describe 'compile', -> # ----------------------------------------------------------------
-
     before (done) ->
       run "cd \"#{basic_root}\"; ../../../bin/roots compile", done

@@ -34,53 +36,57 @@
       shell.rm '-rf', path.join(basic_root, 'public')

   describe 'new', -> # --------------------------------------------------------------------
-
     test_path = path.join(root, 'testproj')

     it 'should use the default template if no flags present', (done) ->
       run "cd \"#{root}\"; ../bin/roots new testproj", ->
-        fs.existsSync(test_path).should.be.ok
-        fs.existsSync(path.join(test_path, 'app.coffee')).should.be.ok
-        fs.existsSync(path.join(test_path, 'readme.md')).should.be.ok
-        fs.existsSync(path.join(test_path, 'views')).should.be.ok
-        fs.existsSync(path.join(test_path, 'views/index.jade')).should.be.ok
-        fs.existsSync(path.join(test_path, 'views/layout.jade')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/favicon.ico')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/css')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/css/_settings.styl')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/css/master.styl')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/js')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/js/main.coffee')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/js/_helper.coffee')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/js/require.js')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/img')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/img/noise.png')).should.be.ok
+        files_exist(test_path,[
+          '/'
+          'app.coffee'
+          'readme.md'
+          'views'
+          'views/index.jade'
+          'views/layout.jade'
+          'assets'
+          'assets/favicon.ico'
+          'assets/css'
+          'assets/css/_settings.styl'
+          'assets/css/master.styl'
+          'assets/js'
+          'assets/js/main.coffee'
+          'assets/js/_helper.coffee'
+          'assets/js/require.js'
+          'assets/img'
+          'assets/img/noise.png'
+        ])
         shell.rm '-rf', path.join(root, 'testproj')
         done()

     it 'should use express template if the --express flag is present', (done) ->
       run "cd \"#{root}\"; ../bin/roots new testproj --express", ->
-        fs.existsSync(test_path).should.be.ok
-        fs.existsSync(path.join(test_path, 'app.js')).should.be.ok
-        fs.existsSync(path.join(test_path, 'routes')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets')).should.be.ok
-        fs.existsSync(path.join(test_path, 'views')).should.be.ok
-        fs.existsSync(path.join(test_path, 'public')).should.be.ok
+        files_exist(test_path,[
+          '/'
+          'app.js'
+          'routes'
+          'assets'
+          'views'
+          'public'
+        ])
         shell.rm '-rf', path.join(root, 'testproj')
         done()

     it 'should use basic template if the --basic flag is present', (done) ->
       run "cd \"#{root}\"; ../bin/roots new testproj --basic", ->
-        fs.existsSync(test_path).should.be.ok
-        fs.existsSync(path.join(test_path, 'views/index.html')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/js/main.js')).should.be.ok
-        fs.existsSync(path.join(test_path, 'assets/css/example.css')).should.be.ok
+        files_exist(test_path,[
+          '/'
+          'views/index.html'
+          'assets/js/main.js'
+          'assets/css/example.css'
+        ])
         shell.rm '-rf', path.join(root, 'testproj')
         done()

   describe 'plugin', -> # -----------------------------------------------------------------
-
     it 'should create a template inside /plugins on \'generate\'', (done) ->
       run "cd \"#{basic_root}\"; ../../../bin/roots plugin generate", ->
         fs.existsSync(path.join(basic_root, 'plugins/template.coffee')).should.be.ok
@@ -94,7 +100,6 @@
         done()

   describe 'version', -> # ----------------------------------------------------------------
-
     it 'should output the correct version number for roots', (done) ->
       version = JSON.parse(fs.readFileSync('package.json')).version
       run './bin/roots version', (err,out) ->
@@ -102,18 +107,12 @@
         done()

   describe 'js', -> # ---------------------------------------------------------------------
-
     it 'should expose bower\'s interface', (done) ->
       run "cd \"#{basic_root}\"; ../../../bin/roots js", (err,out, stdout) ->
         out.should.match /bower/
         done()

-#
-# compiler
-#
-
 describe 'compiler', ->
-
   compiler = null

   before ->
@@ -125,7 +124,6 @@
     compiler.finish()

 describe 'jade', ->
-
   jade_path = path.join root, 'sandbox/jade'
   jade_path_2 = path.join root, 'sandbox/no-layout'

@@ -142,7 +140,6 @@
       done()

 describe 'ejs', ->
-
   ejs_path = path.join root, 'sandbox/ejs'

   it 'should compile ejs', (done) ->
@@ -152,7 +149,6 @@
       done()

 describe 'coffeescript', ->
-
   coffeescript_path = path.join root, 'sandbox/coffeescript'
   coffeescript_path_2 = path.join root, 'sandbox/coffee-basic'

@@ -174,7 +170,6 @@
       done()

 describe 'stylus', ->
-
   stylus_path = path.join root, 'sandbox/stylus'

   before (done) ->
@@ -192,7 +187,6 @@
     shell.rm '-rf', path.join(stylus_path, 'public')

 describe 'static files', ->
-
   static_path = path.join root, 'sandbox/static'

   before (done) ->
@@ -206,7 +200,6 @@
     shell.rm '-rf', path.join(static_path, 'public')

 describe 'errors', ->
-
   errors_path = path.join root, 'sandbox/errors'

   it 'notifies you if theres an error', (done) ->
@@ -215,7 +208,6 @@
       done()

 describe 'dynamic content', ->
-
   dynamic_path = path.join root, 'sandbox/dynamic'

   before (done) ->
@@ -229,9 +221,7 @@
     content.should.match(/This is my first blog post/)
     shell.rm '-rf', path.join(dynamic_path, 'public')

-
 describe 'precompiled templates', ->
-
   precompile_path = path.join root, 'sandbox/precompile'

   before (done) ->
@@ -245,7 +235,6 @@
     shell.rm '-rf', path.join(precompile_path, 'public')

 describe 'multipass compiles', ->
-
   multipass_path = path.join root, 'sandbox/multipass'

   before (done) ->
@@ -257,10 +246,6 @@
     content = fs.readFileSync path.join(multipass_path, 'public/index.html'), 'utf8'
     content.should.match(/blarg world/)
     shell.rm '-rf', path.join(multipass_path, 'public')
-
-#
-# deploy
-#

 describe 'deploy', ->
   deployer = null


Add test case

Add test case

--- test/test.coffee
+++ test/test.coffee
@@ -21,6 +21,11 @@
       expect -> p.parse('(').to.throw()

   describe 'evaluator', ->
+    it 'should treat nil as empty list', ->
+      expect(Lisp.eval("(car nil)").body).to.equal("nil")
+      expect(Lisp.eval("(cdr nil)").body).to.equal("nil")
+      expect(Lisp.eval("(cons 10 nil)").body).to.equal("(10)")
+
     describe 'special forms', ->
       it 'should return first value', ->
         expect(Lisp.eval("""


add dependency tests

add dependency tests

--- test/test.coffee
+++ test/test.coffee
@@ -1,4 +1,11 @@
 expect = chai.expect
+
+describe 'Dependencies', ->
+  it 'has loDash', ->
+    expect(typeof _).to.not.eq 'undefined'
+
+  it 'has jQuery', ->
+    expect($).to.eq jQuery

 describe 'Layout', ->
   layout   = SwanKiosk.Layout


More strict test

More strict test

--- src/tools/remotes.coffee
+++ src/tools/remotes.coffee
@@ -31,4 +31,4 @@
   src: line

 msi = (line)->
-  ~line.files.indexOf 'msi'
+  ~line.files.indexOf '-msi'


Detach instead of hide to discard DOM elements.

(I think.)

--- lib/side-view.coffee
+++ lib/side-view.coffee
@@ -21,7 +21,7 @@
     @side.conflict.on 'conflict:resolved', =>
       @deleteMarker @side.refBannerMarker
       @deleteMarker @side.marker unless @side.wasChosen()
-      @hide()
+      @detach()

     @side.marker.on 'changed', (event) =>
       marker = @side.marker


simulator: Fix duplicate registarations of response handler

--- lib/simulator.coffee
+++ lib/simulator.coffee
@@ -66,10 +66,6 @@
             console.log 'FBP MICROFLO SEND:', response if util.debug_protocol
             @emit 'message', response

-    handleMessage: (msg) ->
-        console.log 'FBP MICROFLO RECV:', msg if util.debug_protocol
-        runtime.handleMessage this, msg
-
         @device.on 'response', =>
             args = []
             i = 0
@@ -78,6 +74,10 @@
                 i++
             runtime.deviceResponseToFbpProtocol @, @conn.send, args
             # Assumes comm is open
+
+    handleMessage: (msg) ->
+        console.log 'FBP MICROFLO RECV:', msg if util.debug_protocol
+        runtime.handleMessage this, msg

     uploadGraph: (graph, callback) ->
         @graph = graph


Fixed _checkCollection method

Fixed _checkCollection method

--- lib/somewhere.coffee
+++ lib/somewhere.coffee
@@ -19,7 +19,7 @@
     fs.writeFileSync @databasePath, JSON.stringify @database if @databasePath

   save: (collection, data) ->
-    _checkCollection collection
+    _checkCollection @database, collection
     data.id = do _uuid
     @database[collection].push data
     do @write
@@ -54,7 +54,7 @@
     v = if c is 'x' then r else r & 7 | 8
     v.toString 16

-_checkCollection = (collection) ->
+_checkCollection = (database, collection) ->
   return if database[collection]
   database[collection] = []


Fixed bug with SerialHelper

Fixed bug with SerialHelper

--- lib/spark-dev.coffee
+++ lib/spark-dev.coffee
@@ -325,6 +325,7 @@
     if !port
       @choosePort('spark-dev:identify-core')
     else
+      @SerialHelper ?= require './utils/serial-helper'
       promise = @SerialHelper.askForCoreID port
       promise.done (coreID) =>
         @IdentifyCoreView ?= require './views/identify-core-view'


JTag: add one more signature for JTag.one method

--- workers/social/lib/social/models/tag.coffee
+++ workers/social/lib/social/models/tag.coffee
@@ -72,8 +72,10 @@
         delete:
           (signature Function)
       static        :
-        one:
-          (signature Object, Function)
+        one: [
+          (signature Object, Function)
+          (signature Object, Object, Function)
+        ]
         on:
           (signature String, Function)
         some:


Revert "EXTREMELY important bug fix"

Revert "EXTREMELY important bug fix"

This reverts commit bf937921ac8f042476aac6a9f17640971ecb94be.

--- app/assets/javascripts/common.js.coffee
+++ app/assets/javascripts/common.js.coffee
@@ -60,7 +60,5 @@
   new Konami( () ->
     audio = new Audio('/jingle.mp3')
     audio.play()
-    styles = document.styleSheets[document.styleSheets.length-1]
-    styles.insertRule('* { font-family: "Comic Sans MS" !important; }',styles.cssRules.length)
   )
 )


cameCasing

cameCasing

--- app/assets/javascripts/deploy.js.coffee
+++ app/assets/javascripts/deploy.js.coffee
@@ -17,7 +17,7 @@
     @$code = $body.find('code')
     @$body = $body
     @$window = $(window)
-    @initial_scroll = true
+    @initialScroll = true

   poll: =>
     jQuery.ajax @pollUrl,
@@ -44,8 +44,8 @@
       @$window.scrollTop(@codeBottomPosition() - @$window.height() + 50)

   isScrolledToBottom: ->
-    if @initial_scroll
-      @initial_scroll = (window.scrollY == 0)
+    if @initialScroll
+      @initialScroll = (window.scrollY == 0)
       true
     else
       @viewportBottomPosition() >= @codeBottomPosition() - STICKY_SCROLL_TOLERENCE and \


Chicken feet are drawing correctly

Chicken feet are drawing correctly

--- app/assets/javascripts/graphs.js.coffee
+++ app/assets/javascripts/graphs.js.coffee
@@ -84,13 +84,41 @@
     drawRelationshipsFromEntity(e)

 drawRelationship = (relationship, entity1, entity2) ->
+
+  endpoints = relationshipEndpoints(entity1, entity2)
+  p1 = endpoints[0]
+  p2 = endpoints[1]
+
+  chickenfoot = {}
+  chickenfoot["left"  ] = "m   0  15 l -40 -15 l  40 -15 "
+  chickenfoot["right" ] = "m   0 -15 l  40  15 l -40  15 "
+  chickenfoot["top"   ] = "m -15   0 l  15 -40 l  15  40 "
+  chickenfoot["bottom"] = "m  15   0 l -15  40 l -15 -40 "
+
+  offset = {}
+  offset["left"  ] = {dx:-50, dy:  0}
+  offset["right" ] = {dx: 50, dy:  0}
+  offset["top"   ] = {dx:  0, dy:-50}
+  offset["bottom"] = {dx:  0, dy: 50}
+
+  o1 = offset[p1.side]
+  o2 = offset[p2.side]
+
+  svg_path = "M #{p1.x} #{p1.y} " + chickenfoot[p1.side] +
+             "M #{p2.x} #{p2.y} " + chickenfoot[p2.side] +
+             "M #{p1.x} #{p1.y} " +
+             "L #{p1.x + o1.dx} #{p1.y + o1.dy} " +
+             "L #{p2.x + o2.dx} #{p2.y + o2.dy} " +
+             "L #{p2.x} #{p2.y} "
+
+  $('#relationship' + relationship).attr('d', svg_path)
+
+relationshipEndpoints = (entity1, entity2) ->
   p1 = {}
   p2 = {}

   center1 = entityCoordinates(entity1, 0.5, 0.5)
   center2 = entityCoordinates(entity2, 0.5, 0.5)
-
-  SYMBOL_SIZE = 40

   if vertical(center1, center2)
     if center1.y < center2.y
@@ -120,7 +148,7 @@
     p1.y = interpolate(center1.x, center1.y, center2.x, center2.y, p1.x)
     p2.y = interpolate(center1.x, center1.y, center2.x, center2.y, p2.x)

-  $('#relationship' + relationship).attr('d', "M #{p1.x} #{p1.y} L #{p2.x} #{p2.y}")
+  return [p1,p2]

 vertical = (coord1, coord2) ->
   return Math.abs(coord1.y - coord2.y) > Math.abs(coord1.x - coord2.x)


Clean-up layout.js.coffee

Clean-up layout.js.coffee

--- app/assets/javascripts/layout.js.coffee
+++ app/assets/javascripts/layout.js.coffee
@@ -1,29 +1,11 @@
-f = (window, document) ->
-  layout   = document.getElementById('layout')
-  menu     = document.getElementById('menu')
-  menuLink = document.getElementById('menuLink')
+$(document).ready(() ->
+  $('#menuLink').click((e) ->
+    active = 'active'

-  toggleClass = (element, className) ->
-    classes = element.className.split(/\s+/)
-    length = classes.length
-    for i in [0..length]
-      if classes[i] == className
-        classes.splice(i, 1)
-        break
-    # The className is not found
-    if length == classes.length
-        classes.push(className)
-    element.className = classes.join(' ')
-
-  menuLink.onclick = (e) ->
-      active = 'active'
-
-      e.preventDefault()
-      toggleClass(layout, active)
-      toggleClass(menu, active)
-      toggleClass(menuLink, active)
-
-$(document).ready(() =>
-  f(this, this.document)
-  $('pre code').each((i, e) => hljs.highlightBlock(e))
+    e.preventDefault()
+    $('#layout').toggleClass(active)
+    $('#menu').toggleClass(active)
+    $('#menuLink').toggleClass(active)
+  )
+  $('pre code').each((i, e) -> hljs.highlightBlock(e))
 )


don't nest the routes

don't nest the routes

--- app/assets/javascripts/router.js.coffee
+++ app/assets/javascripts/router.js.coffee
@@ -1,3 +1,3 @@
 Fortchan12.Router.map ()->
-  @resource 'topics', path: '/', ()->
-    @resource 'topic', path: ':topic_id'
+  @resource 'topics', path: '/'
+  @resource 'topic', path: ':topic_id'