Get rid of latest symlink

Get rid of latest symlink

The PackageServer now automatically reads out the package
information from the newest available package. The symlink
was needed before, as the package server was not able to sort
the tar files properly.

--- app.coffee
+++ app.coffee
@@ -316,113 +316,105 @@
 					do fileCallback
 					return

-				latest = packageFolder + '/latest'
-				debug "Parsing #{latest}"
-				currentPackage = { }
+				currentPackage =
+					name: path.basename packageFolder
+					versions: { }

 				parsingFinished = ->
 					newPackageList[currentPackage.name] = currentPackage
 					fileCallback null
-
-				fs.exists latest, (latestExists) ->
-					# abort if `latest` is missing
-					unless latestExists
-						warn "#{latest} is missing"
-						fileCallback null
+
+				# read every file in the folder to find the available versions
+				fs.readdir packageFolder, (err, versions) ->
+					if err?
+						fileCallback err
 						return

-					# parse package.xml of `latest`
-					getPackageXml latest, (err, latestPackageXml) ->
-						if err?
-							fileCallback "Error parsing package.xml of #{latest}: #{err}"
-							return
-						debug "Finished parsing #{latest}"
-						currentPackage.name = latestPackageXml.package.$.name
-						# either latest does not belong here, or the folder is incorrectly named
-						unless currentPackage.name is path.basename packageFolder
-							fileCallback "package name does not match folder (#{currentPackage.name} isnt #{path.basename packageFolder})"
-							return
+					versions = versions.filter (versionFile) ->
+						if versionFile is 'latest'
+							debug "The latest symlink is obsolete now. The information are taken from the newest package automatically!"
+							false
+						else if (versionFile.substring 0, 1) is '.'
+							debug "Skipping dotfile #{packageFolder}/#{versionFile}"
+							false
+						else if /^([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)\.txt$/i.test versionFile
+							false
+						else unless /^([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)\.tar$/i.test versionFile
+							debug "Skipping #{packageFolder}/#{versionFile}, as it does not match a valid version number"
+							false
+						else
+							true
+
+					versions.sort (a, b) ->
+						if a is b
+							0
+						else if (createComparator "$v > #{b.replace(/\.tar$/, '')}")(a.replace(/\.tar$/, ''))
+							1
+						else
+							-1

-						# provide relevant information to the callback
-						currentPackage.packageinformation = latestPackageXml.package.packageinformation[0]
-						currentPackage.authorinformation = latestPackageXml.package.authorinformation[0]
-						currentPackage.versions = { }
-
-						# read every file in the folder to find the available versions
-						fs.readdir packageFolder, (err, versions) ->
+					async.eachSeries versions, (versionFile, versionsCallback) ->
+						versionFile = packageFolder + '/' + versionFile
+						debug "Parsing #{versionFile}"
+						fs.stat versionFile, (err, versionFileStat) ->
 							if err?
-								fileCallback err
+								versionsCallback versionFileStat
 								return

-							versions = versions.filter (versionFile) ->
-								if versionFile is 'latest'
-									false
-								else if (versionFile.substring 0, 1) is '.'
-									debug "Skipping dotfile #{packageFolder}/#{versionFile}"
-									false
-								else if /^([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)\.txt$/i.test versionFile
-									false
-								else unless /^([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)\.tar$/i.test versionFile
-									debug "Skipping #{packageFolder}/#{versionFile}, as it does not match a valid version number"
-									false
+							unless do versionFileStat.isFile
+								warn "#{versionFile} is not a file"
+								return
+
+							# parse package.xml
+							getPackageXml versionFile, (err, versionPackageXml) ->
+								if err?
+									versionsCallback err
+									return
+								name = versionPackageXml.package.$.name
+								# the tar file does not belong here
+								if name isnt path.basename packageFolder
+									fileCallback "package name does not match folder in #{versionFile} (#{name} != #{path.basename packageFolder})"
+									return
+
+								versionNumber = versionPackageXml.package.packageinformation[0].version[0]
+								# the tar file is incorrectly named -> abort
+								if (versionNumber.toLowerCase().replace (new RegExp ' ', 'g'), '_') isnt path.basename versionFile, '.tar'
+									fileCallback "version number does not match filename in  #{versionFile} (#{versionNumber.toLowerCase().replace new RegExp(' ', 'g'), '_'} != #{path.basename versionFile, '.tar'})"
+									return
+
+								# set {package,author}information to the ones of the last package found (the newest one)
+								currentPackage.packageinformation = versionPackageXml.package.packageinformation[0]
+								currentPackage.authorinformation = versionPackageXml.package.authorinformation[0]
+
+								currentVersion = { }
+								currentVersion.versionnumber = versionNumber
+								currentVersion.license = versionPackageXml.package.packageinformation[0].license?[0]
+								currentVersion.fromversions = (instruction.$.fromversion for instruction in versionPackageXml.package.instructions when instruction.$.fromversion?)
+								currentVersion.optionalpackages = versionPackageXml.package?.optionalpackages?[0]
+								currentVersion.requiredpackages = versionPackageXml.package?.requiredpackages?[0]
+								currentVersion.excludedpackages = versionPackageXml.package?.excludedpackages?[0]
+								currentVersion.timestamp = versionFileStat.mtime
+
+								finish = ->
+									currentPackage.versions[versionNumber] = currentVersion
+
+									debug "Finished parsing #{versionFile}"
+									versionsCallback null
+
+								if config.enableHash
+									hash = new crypto.Hash 'sha256'
+									fileStream = fs.createReadStream versionFile
+									fileStream.pipe hash
+									fileStream.on 'end', ->
+										currentVersion.hash = do hash.read
+										do finish
 								else
-									true
-							versions.sort (a, b) ->
-								return 0 if a is b
-								return 1 if (createComparator "$v > #{b.replace(/\.tar$/, '')}")(a.replace(/\.tar$/, ''))
-								return -1
-							async.eachSeries versions, (versionFile, versionsCallback) ->
-								versionFile = packageFolder + '/' + versionFile
-								debug "Parsing #{versionFile}"
-								fs.stat versionFile, (err, versionFileStat) ->
-									if err?
-										versionsCallback versionFileStat
-										return
-
-									unless do versionFileStat.isFile
-										warn "#{versionFile} is not a file"
-										return
-
-									# parse package.xml
-									getPackageXml versionFile, (err, versionPackageXml) ->
-										if err?
-											versionsCallback err
-											return
-										versionNumber = versionPackageXml.package.packageinformation[0].version[0]
-										# the tar file is incorrectly named -> abort
-										if (versionNumber.toLowerCase().replace (new RegExp ' ', 'g'), '_') isnt path.basename versionFile, '.tar'
-											fileCallback "version number does not match file (#{versionNumber.toLowerCase().replace new RegExp(' ', 'g'), '_'} != #{path.basename versionFile, '.tar'})"
-											return
-
-										currentVersion = { }
-										currentVersion.versionnumber = versionNumber
-										currentVersion.license = versionPackageXml.package.packageinformation[0].license?[0]
-										currentVersion.fromversions = (instruction.$.fromversion for instruction in versionPackageXml.package.instructions when instruction.$.fromversion?)
-										currentVersion.optionalpackages = versionPackageXml.package?.optionalpackages?[0]
-										currentVersion.requiredpackages = versionPackageXml.package?.requiredpackages?[0]
-										currentVersion.excludedpackages = versionPackageXml.package?.excludedpackages?[0]
-										currentVersion.timestamp = versionFileStat.mtime
-
-										finish = ->
-											currentPackage.versions[versionNumber] = currentVersion
-
-											debug "Finished parsing #{versionFile}"
-											versionsCallback null
-
-										if config.enableHash
-											hash = new crypto.Hash 'sha256'
-											fileStream = fs.createReadStream versionFile
-											fileStream.pipe hash
-											fileStream.on 'end', ->
-												currentVersion.hash = do hash.read
-												do finish
-										else
-											setImmediate finish
-							, (err) ->
-								if err?
-									fileCallback err
-								else
-									do parsingFinished
+									setImmediate finish
+					, (err) ->
+						if err?
+							fileCallback err
+						else
+							do parsingFinished
 		, (err) ->
 			if err?
 				error "Error reading package list: #{err}"


no message

no message

--- app.coffee
+++ app.coffee
@@ -23,7 +23,7 @@
       space_id: '7vfiut035ckb'
       content_types:
         topics:
-          id: '73GL8unsu4aocWYMsaoYcI'
+          id: '5rUTPHybXa8AMUSQYmMSCc'
           template: 'views/_topic.jade'
           write: 'data.json'
 ]


Changed copy to @zbloom's suggestion

Changed copy to @zbloom's suggestion

--- app.coffee
+++ app.coffee
@@ -19,7 +19,7 @@
   .option('-p, --port [80]', "Port to serve http requests from.  Comma seperate to bind onto multiple ports.", NumberList)
   .option('-s, --https-port [443]', "Port to serve https requests from (0 to disable).", NumberList)
   .option('-d, --debug', "Output route matching debug info.", Boolean)
-  .option('-r, --pass-redirects', "Push 3XXs to the browser (default, vee follows them)", Boolean)
+  .option('-r, --pass-redirects', "Pass 3XXs to the browser, rather than following them.", Boolean)
   .parse(process.argv)

 watch = (file) ->


[➠] Improved “roofify” aka update “pointer”.

--- vendor/assets/javascripts/jquery/plugins/roofify/roofy/section.js.coffee
+++ vendor/assets/javascripts/jquery/plugins/roofify/roofy/section.js.coffee
@@ -51,9 +51,7 @@
     return

   activate: ->
-    inactive = do @isInactive
-
-    if inactive
+    if do @inactive
       @section.velocity @options.activate.effect,
         easing: @options.activate.easing
         delay: @options.activate.delay
@@ -64,9 +62,7 @@
     return

   deactivate: ->
-    active = do @isActive
-
-    if active
+    if do @active
       @section.velocity @options.deactivate.effect,
         easing: @options.deactivate.easing
         delay: @options.deactivate.delay
@@ -110,16 +106,33 @@

     return

+  updatePointer: ->
+    if do @active
+      label = @pointer.data 'active-label'
+      title = @pointer.data 'active-title'
+
+      @pointer.addClass 'active'
+    else
+      label = @pointer.data 'label'
+      title = @pointer.data 'title'
+
+      @pointer.removeClass 'active'
+
+    @pointer.html label
+    @pointer.attr 'title', title
+
+    return
+
   states: ->
     Section.STATES

-  isActive: ->
+  active: ->
     @state == 'active'

-  isInactive: ->
+  inactive: ->
     @state == 'inactive'

-  isLocked: ->
+  locked: ->
     @state == 'locked'

   controls: ->
@@ -143,6 +156,8 @@

     do @update

+    do @updatePointer
+
     return

   beforeDeactivate: (section) =>
@@ -161,6 +176,8 @@

     do @update

+    do @updatePointer
+
     return

   onControlClick: (event) =>


ComputeProvider: slice arguments to copy them

--- workers/social/lib/social/models/computeproviders/computeprovider.coffee
+++ workers/social/lib/social/models/computeproviders/computeprovider.coffee
@@ -60,15 +60,13 @@
         return callback new KodingError \
           "Credential is required.", "MissingCredential"

-      _arguments = arguments
+      args = [ arguments... ]

       checkCredential credential, (err, cred)=>

         if err then return callback err
-
-        _arguments[1].credential = cred
-
-        fn.apply @, _arguments
+        args[1].credential = cred
+        fn.apply @, args

   @providers = PROVIDERS


Add ifEqual view helper

Add ifEqual view helper

--- SingularityUI/app/lib/view_helper.coffee
+++ SingularityUI/app/lib/view_helper.coffee
@@ -1,3 +1,4 @@
+Handlebars.registerHelper 'ifEqual', (v1, v2, options) -> if v1 is v2 then options.fn @ else options.inverse @
 Handlebars.registerHelper 'ifLT', (v1, v2, options) -> if v1 < v2 then options.fn @ else options.inverse @
 Handlebars.registerHelper 'ifGT', (v1, v2, options) -> if v1 > v2 then options.fn @ else options.inverse @


Client: Activity: Feed: Added fetchMesagesByQuery method to message actions

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -591,6 +591,26 @@
   { SET_CHANNEL_MESSAGES_SEARCH_QUERY } = actionTypes

   dispatch SET_CHANNEL_MESSAGES_SEARCH_QUERY, { query }
+
+
+fetchMessagesByQuery = (channelId, query, options={}) ->
+
+  {
+    LOAD_MESSAGES_SUCCESS
+    CHANNEL_MESSAGES_SEARCH_BEGIN
+    CHANNEL_MESSAGES_SEARCH_SUCCESS } = actionTypes
+
+  dispatch CHANNEL_MESSAGES_SEARCH_BEGIN, { channelId, query }
+
+  kd.singletons.search.searchChannel query, channelId, options
+    .then (messages) ->
+      kd.singletons.reactor.batch ->
+        for message in messages
+          dispatchLoadMessageSuccess channelId, message
+
+        dispatch CHANNEL_MESSAGES_SEARCH_SUCCESS, { channelId, messages }
+        dispatch LOAD_MESSAGES_SUCCESS, { channelId, messages }
+

 module.exports = {
   loadMessage
@@ -612,5 +632,6 @@
   removeLoaderMarker
   editEmbedPayloadByUrl
   disableEditedEmbedPayload
+  fetchMessagesByQuery
   setChannelMessagesSearchQuery
 }


Fix for IN USE label

Fix for IN USE label

--- client/app/lib/stacks/credentiallistitem.coffee
+++ client/app/lib/stacks/credentiallistitem.coffee
@@ -41,7 +41,7 @@
     { selectedCredentials } = delegate.getOptions()

     credentials = stackTemplate?.credentials ? {}
-    credentials = (credentials[val].first.identifier for val of credentials)
+    credentials = (credentials[val].first for val of credentials)
     credentials = credentials.concat (selectedCredentials or [])

     if identifier in credentials


Fix eval of template settings

Fix eval of template settings

--- templito-cli.coffee
+++ templito-cli.coffee
@@ -87,7 +87,7 @@

 if argv.template_settings?
   try
-    argv.template_settings = eval "#{argv.template_settings};"
+    argv.template_settings = eval("(#{argv.template_settings});")
   catch e
     console.error "The template-settings you passed in " +
                   "(#{argv.template_settings}) does not appear to be a " +


Fix throttled functions

Fix throttled functions

--- apps/artwork/client/deep_zoom.coffee
+++ apps/artwork/client/deep_zoom.coffee
@@ -14,8 +14,8 @@
     'click .dz-close': 'return'
     'click .dz-slider-minus': 'zoomOut'
     'click .dz-slider-plus': 'zoomIn'
-    'change .dz-slider-range': 'zoomTo'
-    'mousemove': 'detectActivity'
+    'change .dz-slider-range': -> @zoomTo arguments...
+    'mousemove': -> @detectActivity arguments...

   initialize: ({ @artwork, @image }) ->
     @zoomTo = _.throttle @_zoomTo, 50


Remove (overlooked) debugging code.

Remove (overlooked) debugging code.

--- content_scripts/ui_component.coffee
+++ content_scripts/ui_component.coffee
@@ -78,7 +78,7 @@
             highlight: false
             highlightOnlyIfNotTop: true

-      if windowIsFocused() and false
+      if windowIsFocused()
         # We already have the focus.
         refocusSourceFrame()
       else


Fix circulate require.

Fix circulate require.

--- browser/api/lib/app.coffee
+++ browser/api/lib/app.coffee
@@ -1,4 +1,3 @@
-BrowserWindow = require 'browser-window'
 EventEmitter = require('events').EventEmitter

 bindings = process.atomBinding 'app'


return assignment; collect assignment

return assignment; collect assignment

--- salvus/page/editor_course.cjsx
+++ salvus/page/editor_course.cjsx
@@ -287,7 +287,124 @@

         # Copy the files for the given assignment_id from the given student to the
         # corresponding collection folder.
-        copy_assignment_from_student: (assignment, student) =>
+        copy_assignment_from_student: (assignment, student, cb) =>
+            id = @set_activity(desc:"Copying assignment from a student")
+            error = (err) =>
+                @set_activity(id:id)
+                err="copy from student: #{err}"
+                @set_error(err)
+                cb?(err)
+            if not @_store_is_initialized()
+                return error("store not yet initialized")
+            if not student = store.get_student(student)
+                return error("no student")
+            if not assignment = store.get_assignment(assignment)
+                return error("no assignment")
+            student_name = store.get_student_name(student)
+            student_project_id = student.get('project_id')
+            if not student_project_id?
+                # nothing to do
+                @set_activity(id:id)
+                cb?()
+            else
+                @set_activity(id:id, desc:"Copying assignment from #{student_name}")
+                salvus_client.copy_path_between_projects
+                    src_project_id    : student_project_id
+                    src_path          : assignment.get('target_path')
+                    target_project_id : project_id
+                    target_path       : assignment.get('collect_path') + '/' + student.get('student_id')
+                    overwrite_newer   : assignment.get('collect_overwrite_newer')
+                    delete_missing    : assignment.get('collect_delete_missing')
+                    cb                : (err) =>
+                        @set_activity(id:id)
+                        where = {table:'assignments', assignment_id:assignment.get('assignment_id')}
+                        last_collect = syncdb.select_one(where:where).last_collect ? {}
+                        last_collect[student.get('student_id')] = {time: misc.mswalltime(), error:err}
+                        @_update(set:{last_collect:last_collect}, where:where)
+                        cb?(err)
+
+        # Copy the given assignment to all non-deleted students, doing 10 copies in parallel at once.
+        copy_assignment_from_all_students: (assignment, cb) =>
+            id = @set_activity(desc:"Copying assignment from all students")
+            error = (err) =>
+                @set_activity(id:id)
+                err="copy from student: #{err}"; @set_error(err); cb?(err)
+            if not @_store_is_initialized()
+                return error("store not yet initialized")
+            if not assignment = store.get_assignment(assignment)
+                return error("no assignment")
+            errors = ''
+            f = (student, cb) =>
+                @copy_assignment_from_student assignment, student, (err) =>
+                    if err
+                        errors += "\n #{err}"
+                    cb()
+            async.mapLimit store.get_student_ids(deleted:false), 10, f, (err) =>
+                if err
+                    return error(errors)
+                else
+                    @set_activity(id:id)
+                    cb?()
+
+        return_assignment_to_student: (assignment, student, cb) =>
+            id = @set_activity(desc:"Returning assignment to a student")
+            error = (err) =>
+                @set_activity(id:id)
+                err="return to student: #{err}"
+                @set_error(err)
+                cb?(err)
+            if not @_store_is_initialized()
+                return error("store not yet initialized")
+            if not student = store.get_student(student)
+                return error("no student")
+            if not assignment = store.get_assignment(assignment)
+                return error("no assignment")
+            student_name = store.get_student_name(student)
+            student_project_id = student.get('project_id')
+            if not student_project_id?
+                # nothing to do
+                @set_activity(id:id)
+                cb?()
+            else
+                @set_activity(id:id, desc:"Returning assignment to #{student_name}")
+                salvus_client.copy_path_between_projects
+                    src_project_id    : project_id
+                    src_path          : assignment.get('collect_path') + '/' + student.get('student_id')
+                    target_project_id : student_project_id
+                    target_path       : assignment.get('graded_path')
+                    overwrite_newer   : assignment.get('overwrite_newer')
+                    delete_missing    : assignment.get('delete_missing')
+                    cb                : (err) =>
+                        @set_activity(id:id)
+                        where = {table:'assignments', assignment_id:assignment.get('assignment_id')}
+                        last_return_graded = syncdb.select_one(where:where).last_return_graded ? {}
+                        last_return_graded[student.get('student_id')] = {time: misc.mswalltime(), error:err}
+                        @_update(set:{last_return_graded:last_return_graded}, where:where)
+                        cb(err)
+
+        # Copy the given assignment to all non-deleted students, doing 10 copies in parallel at once.
+        return_assignment_to_all_students: (assignment, cb) =>
+            id = @set_activity(desc:"Returning assignments to all students")
+            error = (err) =>
+                @set_activity(id:id)
+                err="return to student: #{err}"; @set_error(err); cb?(err)
+            if not @_store_is_initialized()
+                return error("store not yet initialized")
+            if not assignment = store.get_assignment(assignment)
+                return error("no assignment")
+            errors = ''
+            f = (student, cb) =>
+                @return_assignment_to_student assignment, student, (err) =>
+                    if err
+                        errors += "\n #{err}"
+                    cb()
+            async.mapLimit store.get_student_ids(deleted:false), 10, f, (err) =>
+                if err
+                    return error(errors)
+                else
+                    @set_activity(id:id)
+                    cb?()
+

         # Copy the files for the given assignment to the given student. If
         # the student project doesn't exist yet, it will be created.
@@ -325,7 +442,6 @@
                         target_path       : assignment.get('target_path')
                         overwrite_newer   : assignment.get('overwrite_newer')
                         delete_missing    : assignment.get('delete_missing')
-                        timeout           : assignment.get('timeout')
                         cb                : (err) =>
                             where = {table:'assignments', assignment_id:assignment.get('assignment_id')}
                             last_assignment = syncdb.select_one(where:where).last_assignment ? {}
@@ -695,13 +811,36 @@
     render: ->
         e.preventDefault(); @open_path()}>{@props.path}

-
 StudentList = rclass
     displayName : "CourseEditor-StudentList"
     propTypes:
         students   : rtypes.object.isRequired
         user_map   : rtypes.object.isRequired
         info       : rtypes.func.isRequired
+
+    render_event: (name, obj) ->
+        v = [{name+': '}]
+        if obj?
+            if obj.time?
+                v.push({new Date(obj.time).toLocaleString()})
+            if obj.error
+                v.push({obj.error})
+        else
+            v.push("(not yet)") # TODO: put a button on it
+        return v
+
+    render_student_info: (info) ->
+        <Row >
+            <Col md=4 key='last_assignment'>
+                {@render_event('Assigned', info.last_assignment)}
+            </Col>
+            <Col md=4 key='collect'>
+                {@render_event('Collected', info.collect)}
+            </Col>
+            <Col md=4 key='return_graded'>
+                {@render_event('Returned', info.return_graded)}
+            </Col>
+        </Row>

     render_students :->
         v = immutable_to_list(@props.students, 'student_id')
@@ -725,7 +864,7 @@
                     {x.name}
                 </Col>
                 <Col md=9>
-                    {misc.to_json(@props.info(x.student_id))}
+                    {@render_student_info(@props.info(x.student_id))}
                 </Col>
             </Row>
     render: ->
@@ -745,7 +884,9 @@
     render_students: ->
         students   = @props.students
         assignment = @props.assignment
-        last_assignment = assignment.get('last_assignment').toJS() ? {}
+        last_assignment    = assignment.get('last_assignment')?.toJS() ? {}
+        last_collect       = assignment.get('last_collect')?.toJS() ? {}
+        last_return_graded = assignment.get('last_return_graded')?.toJS() ? {}
         info = (student_id) ->
             # determine following info:
             #   - if (when) assignment assigned to this student
@@ -754,7 +895,9 @@
             #   - what the grade was
             #   - if the assignment was returned (and when)
             x =
-                last_assignment : last_assignment.get(student_id)
+                last_assignment    : last_assignment[student_id]
+                last_collect       : last_collect[student_id]
+                last_return_graded : last_return_graded[student_id]
             return x

         <StudentList info={info} students={@props.students} user_map={@props.user_map} />
@@ -823,10 +966,18 @@
             <Icon name="share-square-o" /> Assign to all...
         </Button>

+    collect_assignment: ->
+        # assign assignment to all (non-deleted) students
+        @props.flux.getActions(@props.name).copy_assignment_from_all_students(@props.assignment)
+
     render_collect_button: ->
         <Button onClick={@collect_assignment}>
             <Icon name="share-square-o" rotate="180" /> Collect from all...
         </Button>
+
+    return_assignment: ->
+        # assign assignment to all (non-deleted) students
+        @props.flux.getActions(@props.name).return_assignment_to_all_students(@props.assignment)

     render_return_button: ->
         <Button onClick={@return_assignment}>


fix in new / postfix code

fix in new / postfix code

--- salvus/page/project_files.cjsx
+++ salvus/page/project_files.cjsx
@@ -1131,7 +1131,7 @@
         url = document.URL
         url = url[0...url.indexOf('/projects/')]
         display_url = "#{url}/projects/#{@props.project_id}/files/#{misc.encode_path(single_item)}"
-        if @props.file_map[misc.path_split(single_item).tail].isdir
+        if @props.file_map[misc.path_split(single_item).tail]?.isdir
             display_url += '/'


Fix merge conflicts in es-ES.coffee

Fix merge conflicts in es-ES.coffee

--- app/locale/es-ES.coffee
+++ app/locale/es-ES.coffee
@@ -31,7 +31,7 @@
     contact: "Contacta"
     twitter_follow: "Síguenos en Twitter"
     teachers: "Profesores"
-#    careers: "Careers"
+    careers: "Carreras"

   modal:
     close: "Cerrar"
@@ -74,7 +74,7 @@
     anonymous: "Jugador Anonimo"
     level_difficulty: "Dificultad: "
     campaign_beginner: "Campaña de Principiante"
-    awaiting_levels_adventurer_prefix: "Liberamos cinco niveles cada semana." # {change}
+    awaiting_levels_adventurer_prefix: "Liberamos nuevos niveles cada semana."
     awaiting_levels_adventurer: "Regístrate como Aventurero"
     awaiting_levels_adventurer_suffix: "para ser el primero en jugar nuevos niveles."
     adjust_volume: "Ajustar volúmen"
@@ -84,12 +84,12 @@
     campaign_old_multiplayer_description: "Reliquias de una era más civilizada. Ninguna simulación es ejecutada para estas arenas multijugador antiguas y sin héroes."

   share_progress_modal:
-    blurb: "¡Estás teniendo un gran progreso! Cuéntale a alguien que tanto habeis aprendido con CodeCombat." # {change}
+    blurb: "¡Estás progresando bastante bien! Dile a tus padres cuánto has aprendido con CodeCombat."
     email_invalid: "La dirección de correo electrónico no es válida."
     form_blurb: "¡Introduzca su correo electrónico y nosotros les mostraremos!"
     form_label: "Correo Electrónico"
     placeholder: "correo electrónico"
-    title: "Excelente Trabajo Aprendiz"
+    title: "Excelente Trabajo, Aprendiz"

   login:
     sign_up: "Crear una cuenta"
@@ -335,17 +335,17 @@
     tip_javascript_java: "Java es a JavaScript lo que un automóvil es a un móvil. - Chris Heilmann"
     tip_move_forward: "Lo que sea que hagas, sigue hacia adelante. - Martin Luther King Jr."
     tip_google: "¿Teneis un problema que no podeis resolver? ¡Googleadlo!"
-#    tip_adding_evil: "Adding a pinch of evil."
+    tip_adding_evil: "Añadiendo una pizca de maldad."
 #    tip_hate_computers: "That's the thing about people who think they hate computers. What they really hate is lousy programmers. - Larry Niven"
     tip_open_source_contribute: "Puedes contribuir a mejorar CodeCombat!"
     tip_recurse: "Iterar es humano, recursar es divino. - L. Peter Deutsch"
-#    tip_free_your_mind: "You have to let it all go, Neo. Fear, doubt, and disbelief. Free your mind. - Morpheus"
+    tip_free_your_mind: "Tienes que dejarlo ir, Neo. Miedo, dudas e incredulidad. Libera tu mente. - Morpheus"
     tip_strong_opponents: "Incluso el más fuerte de los opositores oculta debilidad. - Itachi Uchiha"
-#    tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen."
+    tip_paper_and_pen: "Antes de empezar a programar, siempre puedes planificar con papel y lápiz."

   game_menu:
     inventory_tab: "Inventario"
-    save_load_tab: "Salvar/Cargar"
+    save_load_tab: "Guardar/Cargar"
     options_tab: "Opciones"
     guide_tab: "Guia"
     guide_video_tutorial: "Vídeo Tutorial"
@@ -362,17 +362,17 @@

   leaderboard:
     leaderboard: "Jefe de la liga"
-    view_other_solutions: "Ver Otras Soluciones" # {change}
+    view_other_solutions: "Ver clasificaciones"
     scores: "Puntuaciones"
-#    top_players: "Top Players by"
+    top_players: "Mejores jugadores de"
     day: "Hoy"
     week: "Esta semana"
-#    all: "All-Time"
+    all: "Siempre"
     time: "Tiempo"
     damage_taken: "Daño recibido"
     damage_dealt: "Daño causado"
-    difficulty: "Difficultad"
-    gold_collected: "Oro colectado"
+    difficulty: "Dificultad"
+    gold_collected: "Oro conseguido"

   inventory:
     choose_inventory: "Equipar Objetos"
@@ -397,76 +397,76 @@
     prompt_title: "Gemas no suficientes"
     prompt_body: "¿Quieres obtener más?"
     prompt_button: "Ingresa a la tienda"
-    recovered: "Las gemas compradas con anterioridad han sido recuperadas. Por favor, refresca la página."
-#    price: "x3500 / mo"
+    recovered: "Las gemas compradas con anterioridad han sido recuperadas. Por favor, actualiza la página."
+    price: "x3500 / mes"

   subscribe:
-#    comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"
-#    feature1: "110+ basic levels across 4 worlds"
-#    feature2: "10 powerful new heroes with unique skills!"
-#    feature3: "70+ bonus levels"
-#    feature4: "3500 bonus gems every month!"
+    comparison_blurb: "¡Mejora tus habilidades con una suscripción a CodeCombat!"
+    feature1: "Más de 110 niveles básicos pasando por 4 mundos"
+    feature2: "¡10 héroes nuevos con poderes únicos!"
+    feature3: "Más de 70 niveles extra"
+    feature4: "¡3500 gemas extra cada mes!"
     feature5: "Vídeo tutoriales"
-#    feature6: "Premium email support"
-#    feature7: "Private Clans"
+    feature6: "Soporte electrónico Premium"
+    feature7: "Clanes privados"
     free: "Gratis"
     month: "mes"
-#    must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
+    must_be_logged: "Primero debes iniciar sesión. Crea una cuenta o entra a tu cuenta desde el menú."
     subscribe_title: "Suscríbete"
-#    unsubscribe: "Unsubscribe"
-#    confirm_unsubscribe: "Confirm Unsubscribe"
-#    never_mind: "Never Mind, I Still Love You"
-#    thank_you_months_prefix: "Thank you for supporting us these last"
+    unsubscribe: "Dar de baja"
+    confirm_unsubscribe: "Confirmar baja"
+    never_mind: "No te preocupes, aún así te quiero"
+    thank_you_months_prefix: "Gracias por apoyarnos estos"
     thank_you_months_suffix: "meses."
-#    thank_you: "Thank you for supporting CodeCombat."
+    thank_you: "Gracias por apoyar a CodeCombat."
 #    sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
 #    unsubscribe_feedback_placeholder: "O, what have we done?"
-#    parent_button: "Ask your parent"
+    parent_button: "Pregunta a tus padres"
 #    parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
-#    parent_email_input_invalid: "Email address invalid."
-#    parent_email_input_label: "Parent email address"
+    parent_email_input_invalid: "Correo electrónico inválido."
+    parent_email_input_label: "Correo electrónico de los padres"
 #    parent_email_input_placeholder: "Enter parent email"
-#    parent_email_send: "Send Email"
-#    parent_email_sent: "Email sent!"
+    parent_email_send: "Enviar correo"
+    parent_email_sent: "¡Correo enviado!"
 #    parent_email_title: "What's your parent's email?"
     parents: "Para Padres"
-    parents_title: "Tus hijos aprenderan a programar." # {change}
-    parents_blurb1: "Con CodeCombat, tus hijos aprendes a desarrollar código real. Al inicio aprenden comandos simples, y avanzan a temas más avanzados."
-#    parents_blurb1a: "Computer programming is an essential skill that your child will undoubtedly use as an adult. By 2020, basic software skills will be needed by 77% of jobs, and software engineers are in high demand across the world. Did you know that Computer Science is the highest-paid university degree?"
-    parents_blurb2: "Por $9.99 USD/mes, tienen nuevos desafios cada semana y un correo personal con soporte de nuestros programadores profesionales." # {change}
+    parents_title: "Queridos padres: Vuestro hijo está aprendiendo a programar. ¿Le ayudáis para que continue?"
+    parents_blurb1: "Su hijo ha jugado __nLevels__ niveles y ha aprendido conceptos básicos de programación. Ayudadle a mejorar su eficiencia adquiriendo una suscripción para poder seguir programando."
+    parents_blurb1a: "La programación informática es una habilidad fundamental que su hijo usará indudablemente cuando sea un adulto. Aproximadamente en 2020, se necesitarán conceptos de programación en el 77% de los empleos, y los ingenieros programadores están muy demandados en todo el mundo. ¿Sabían que la Ingeniería Informática es la titulación que más dinero paga?"
+    parents_blurb2: "Por tan sólo $9.99 USD/mes, su hijo podrá afrontar nuevos retos cada semana, y recibirá soporte por correo electrónico de programadores profesionales."
     parents_blurb3: "Sin riesgo: 100% garantía de devoluación de dinero, desuscripción con un simple click."
-#    payment_methods: "Payment Methods"
-#    payment_methods_title: "Accepted Payment Methods"
-#    payment_methods_blurb1: "We currently accept credit cards and Alipay."
-#    payment_methods_blurb2: "If you require an alternate form of payment, please contact"
-#    sale_already_subscribed: "You're already subscribed!"
-#    sale_blurb1: "Save 35%"
-#    sale_blurb2: "off regular subscription price of $120 for a whole year!" # {changed}
-#    sale_button: "Sale!"
-#    sale_button_title: "Save 35% when you purchase a 1 year subscription"
-#    sale_click_here: "Click Here"
-#    sale_ends: "Ends"
-#    sale_extended: "*Existing subscriptions will be extended by 1 year."
-#    sale_feature_here: "Here's what you'll get:"
-#    sale_feature2: "Access to 9 powerful new heroes with unique skills!"
-#    sale_feature4: "42,000 bonus gems awarded immediately!"
-#    sale_continue: "Ready to continue adventuring?"
-#    sale_limited_time: "Limited time offer!"
-#    sale_new_heroes: "New heroes!"
-#    sale_title: "Back to School Sale"
-#    sale_view_button: "Buy 1 year subscription for"
+    payment_methods: "Métodos de pago"
+    payment_methods_title: "Métodos de pago permitidos"
+    payment_methods_blurb1: "Actualmente aceptamos pagos a través de tarjetas de crédito / débito y Alipay."
+    payment_methods_blurb2: "Si necesitas una forma alternativa de pago, puedes contactarnos"
+    sale_already_subscribed: "Ya estás suscrito!"
+    sale_blurb1: "¡Ahorra un 35%"
+    sale_blurb2: "de una suscripción normal de 120$ por un año entero!!"
+    sale_button: "Comprar!"
+    sale_button_title: "Ahorra un 35% si compras una suscripción de 1 año"
+    sale_click_here: "Haz click aquí"
+    sale_ends: "Finaliza"
+    sale_extended: "*Las suscripciones actuales se extenderán un año más."
+    sale_feature_here: "Esto es lo que tendrás:"
+    sale_feature2: "¡10 héroes nuevos con poderes únicos!"
+    sale_feature4: "¡42,000 gemas extra entregadas al momento!"
+    sale_continue: "¿Listo para continuar con la aventura?"
+    sale_limited_time: "¡Oferta de tiempo limitado!"
+    sale_new_heroes: "¡Nuevos héroes!"
+    sale_title: "Venta de Vuelta al Colegio"
+    sale_view_button: "Compra una suscripción de 1 año por"
     stripe_description: "Suscripción mensual"
-#    stripe_description_year_sale: "1 Year Subscription (35% discount)"
+    stripe_description_year_sale: "Suscripción de 1 año (35% descuento)"
     subscription_required_to_play: "Necesitas una suscripción para jugar este nivel."
 #    unlock_help_videos: "Subscribe to unlock all video tutorials."
 #    personal_sub: "Personal Subscription" # Accounts Subscription View below
-#    loading_info: "Loading subscription information..."
-#    managed_by: "Managed by"
-#    will_be_cancelled: "Will be cancelled on"
-#    currently_free: "You currently have a free subscription"
-#    currently_free_until: "You currently have a subscription until" # {changed}
-#    was_free_until: "You had a free subscription until"
-#    managed_subs: "Managed Subscriptions"
+    loading_info: "Cargando información de la suscripción..."
+    managed_by: "Administrado por"
+    will_be_cancelled: "Será cancelado el"
+    currently_free: "Actualmente tienes una suscripción gratuita"
+    currently_free_until: "Actualmente tienes una suscripción hasta el"
+    was_free_until: "Tenías una suscripción gratuita hasta el"
+    managed_subs: "Suscripciones administradas"
 #    managed_subs_desc: "Add subscriptions for other players (students, children, etc.)"
 #    managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
 #    group_discounts: "Group discounts"
@@ -477,13 +477,13 @@
 #    group_discounts_20: "20% off"
 #    group_discounts_12th: "Subscriptions 12+"
 #    group_discounts_40: "40% off"
-#    subscribing: "Subscribing..."
+    subscribing: "Suscribiendo..."
 #    recipient_emails_placeholder: "Enter email address to subscribe, one per line."
 #    subscribe_users: "Subscribe Users"
 #    users_subscribed: "Users subscribed:"
 #    no_users_subscribed: "No users subscribed, please double check your email addresses."
-#    current_recipients: "Current Recipients"
-#    unsubscribing: "Unsubscribing..."
+    current_recipients: "Últimos recibos"
+    unsubscribing: "Dando de baja..."
 #    subscribe_prepaid: "Click Subscribe to use prepaid code"
 #    using_prepaid: "Using prepaid code for monthly subscription"

@@ -491,7 +491,7 @@
     choose_hero: "Selecciona tu Heroe"
     programming_language: "Lenguaje de Programación"
     programming_language_description: "¿Qué lenguaje de programación deseas usar?"
-#    default: "Default"
+    default: "Por defecto"
     experimental: "Experimental"
     python_blurb: "Simple pero poderoso."
     javascript_blurb: "El lenguaje de la web."
@@ -504,8 +504,8 @@
     weapons: "Armas"
     weapons_warrior: "Espadas - Corto Alcance, Sin Magia"
     weapons_ranger: "Ballestas, Pistolas - Largo Alcance, Sin Magia"
-    weapons_wizard: "Varitas, Bastones - Largo Alcance, Magia"
-    attack: "Daño" # Can also translate as "Attack"
+    weapons_wizard: "Varitas, Bastones - Largo Alcance, Con Magia"
+    attack: "Daño" # Can also translate as "Atacar"
     health: "Salud"
     speed: "Velocidad"
     regeneration: "Regeneración"
@@ -528,11 +528,11 @@
   skill_docs:
 #    writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this
     read_only: "Solo lectura"
-#    action: "Action"
-#    spell: "Spell"
+    action: "Acción"
+    spell: "Hechizo"
     action_name: "nombres"
-#    action_cooldown: "Takes"
-#    action_specific_cooldown: "Cooldown"
+    action_cooldown: "Toma"
+    action_specific_cooldown: "Reposo"
     action_damage: "Daño"
     action_range: "Alcance"
     action_radius: "Radio"
@@ -546,7 +546,7 @@
     granted_by: "Otorgado por"

   save_load:
-    granularity_saved_games: "Salvado"
+    granularity_saved_games: "Guardado"
     granularity_change_history: "Historia"

   options:
@@ -586,17 +586,17 @@
     press_paragraph_1_link: "paquete de prensa"
     press_paragraph_1_suffix: ". Todos los logos y las imagenes pueden ser usados sin necesidad de contactarnos directamente."
     team: "Equipo"
-    george_title: "CEO" # {change}
+    george_title: "Co-Fundador"
     george_blurb: "Hombre de Negocios"
-    scott_title: "Programador" # {change}
+    scott_title: "Co-Fundador"
     scott_blurb: "Razonable"
-    nick_title: "Programador" # {change}
+    nick_title: "Co-Fundador"
     nick_blurb: "Guru Motivacional"
     michael_title: "Programador"
     michael_blurb: "Administrador de Sistemas"
-    matt_title: "Programador" # {change}
+    matt_title: "Co-Fundador"
     matt_blurb: "Ciclista"
-#    cat_title: "Chief Artisan"
+    cat_title: "Jefe Artesano"
 #    cat_blurb: "Airbender"
     josh_title: "Diseñador de Videojuegos"
 #    josh_blurb: "Floor Is Lava"
@@ -606,11 +606,11 @@
 #    retrostyle_blurb: "RetroStyle Games"

   teachers:
-#    title: "CodeCombat: Info for Teachers"
+    title: "CodeCombat: Información para profesores"
 #    intro_1: "CodeCombat is an online game that teaches programming. Students write code in real programming languages."
-#    intro_2: "No experience required!"
-#    free_title: "How much does it cost?"
-#    cost_china: "CodeCombat in China is free for the first five levels, after which it costs $9.99 USD per month for access to our other 180+ levels on our exclusive China servers."
+    intro_2: "¡No se necesita experiencia!"
+    free_title: "¿Cuanto cuesta?"
+    cost_china: "CodeCombat en China es gratuito durante los primeros 5 niveles, después tiene un coste de $9.99 USD por mes con acceso a nuestros más de 180+ niveles en nuestros servidores exclusivos de China."
 #    free_1: "There are 110+ FREE levels which cover every concept."
 #    free_2: "A monthly subscription provides access to video tutorials and extra practice levels."
 #    teacher_subs_title: "Teachers get free subscriptions!"
@@ -643,9 +643,9 @@
 #    material_1: "Approximately 25 hours of free content and an additional 15 hours of subscriber content."
 #    concepts_title: "What concepts are covered?"
 #    how_much_title: "How much does a monthly subscription cost?"
-#    how_much_1: "A"
+    how_much_1: "Una"
     how_much_2: "suscripción mensual"
-#    how_much_3: "costs $9.99, and can be cancelled anytime."
+    how_much_3: "tiene un coste de $9.99, y puede ser cancelada en cualquier momento."
 #    how_much_4: "Additionally, we provide discounts for larger groups:"
 #    how_much_5: "We accept discounted one-time purchases and yearly subscription purchases for groups, such as a class or school.  Please contact"
 #    how_much_6: "for more details."
@@ -874,14 +874,14 @@
     revert: "Revertir"
     revert_models: "Revertir Modelos"
     pick_a_terrain: "Escoge un Terreno"
-#    dungeon: "Dungeon"
+    dungeon: "Mazmorra"
     indoor: "Interior"
     desert: "Desierto"
     grassy: "Cubierto de hierba"
     mountain: "Mountaña"
     glacier: "Glaciar"
     small: "Pequeño"
-#    large: "Large"
+    large: "Grande"
     fork_title: "Bifurcar nueva versión"
     fork_creating: "Creando bifurcación..."
     generate_terrain: "Generar Terreno"
@@ -908,7 +908,7 @@
 #    config_thang: "Double click to configure a thang"
     delete: "Borrar"
     duplicate: "Duplicar"
-#    stop_duplicate: "Stop Duplicate"
+    stop_duplicate: "Detener duplicados"
     rotate: "Rotar"
     level_settings_title: "Ajustes"
     level_component_tab_title: "Componentes Actuales"
@@ -933,13 +933,13 @@
     new_level_title_login: "Inicia sesión para Crear un Nuevo Nivel"
     new_achievement_title: "Crea un nuevo Logro"
     new_achievement_title_login: "Inicia sesión para Crear un Nuevo Logro"
-#    new_poll_title: "Create a New Poll"
+    new_poll_title: "Crear una nueva Encuesta"
 #    new_poll_title_login: "Log In to Create a New Poll"
     article_search_title: "Buscar artículos aquí"
     thang_search_title: "Busca tipos de objetos aquí"
     level_search_title: "Buscar niveles aquí"
     achievement_search_title: "Buscar Logros"
-#    poll_search_title: "Search Polls"
+    poll_search_title: "Buscar encuestas"
     read_only_warning2: "Nota: no puedes guardar nada de lo que edites aqui porque no has iniciado sesión."
     no_achievements: "No se han añadido logros a este nivel."
 #    achievement_query_misc: "Key achievement off of miscellanea"
@@ -955,8 +955,8 @@
     edit_btn_preview: "Vista preliminar"
     edit_article_title: "Editar artículo"

-#  polls:
-#    priority: "Priority"
+  polls:
+    priority: "Prioridad"

   contribute:
     page_title: "Colaborar"
@@ -1083,12 +1083,12 @@
     tournament_blurb_blog: "en nuestro blog"
     rules: "Reglas"
     winners: "Ganadores"
-#    league: "League"
-#    red_ai: "Red AI" # "Red AI Wins", at end of multiplayer match playback
-#    blue_ai: "Blue AI"
-#    wins: "Wins" # At end of multiplayer match playback
-#    humans: "Red" # Ladder page display team name
-#    ogres: "Blue"
+    league: "Liga"
+    red_ai: "IA Roja" # "IA Roja Gana", at end of multiplayer match playback
+    blue_ai: "IA Azul"
+    wins: "Gana" # At end of multiplayer match playback
+    humans: "Roja" # Ladder page display team name
+    ogres: "Azul"

   user:
     stats: "Estadisticas"
@@ -1129,9 +1129,9 @@
     no_recent_games: "No he jugado juegos en las ultimas dos semanas."
     payments: "Pagos"
     purchased: "Adquirido"
-#    sale: "Sale"
-#    subscription: "Subscription"
-#    invoices: "Invoices"
+    sale: "Venta"
+    subscription: "Suscripción"
+    invoices: "Facturas"
     service_apple: "Apple"
     service_web: "Web"
     paid_on: "Pagado en"
@@ -1140,23 +1140,23 @@
     gems: "Joyas"
     active: "Activo"
     subscribed: "Suscrito"
-#    unsubscribed: "Unsubscribed"
+    unsubscribed: "Sin suscripción"
     active_until: "Activo Hasta"
-    cost: "Costo"
+    cost: "Coste"
     next_payment: "Siguiente Pago"
     card: "Tarjeta"
     status_unsubscribed_active: "No estás suscrito y no seras facturado, pero tu cuenta sigue activa por ahora."
     status_unsubscribed: "¡Obten acceso a nuevos niveles, heroes, artículos, y joyas adicionales con una suscripción a CodeCombat!"

-#  account_invoices:
-#    amount: "Amount in US dollars"
-#    declined: "Your card was declined"
-#    invalid_amount: "Please enter a US dollar amount."
-#    not_logged_in: "Log in or create an account to access invoices."
-#    pay: "Pay Invoice"
-#    purchasing: "Purchasing..."
-#    retrying: "Server error, retrying."
-#    success: "Successfully paid. Thanks!"
+  account_invoices:
+    amount: "Cantidad en dólares Americanos"
+    declined: "Tu tarjeta ha sido rechazada"
+    invalid_amount: "Introduce una cantidad en dólares."
+    not_logged_in: "Inicia sesión o crea una cuenta para acceder a los recibos."
+    pay: "Pagar recibo"
+    purchasing: "Comprando..."
+    retrying: "Error del servidor, reintentando."
+    success: "Pago realizado correctamente. ¡Muchas gracias!"

   loading_error:
     could_not_load: "Error al cargar desde el servidor."
@@ -1184,7 +1184,7 @@
     leaderboard: "Clasificación"
     user_schema: "Esquema de usuario"
     user_profile: "Perfil de usuario"
-#    patch: "Patch"
+    patch: "Parche"
     patches: "Parches"
     patched_model: "Documento Fuente"
     model: "Modelo"
@@ -1218,29 +1218,29 @@
     play_counts: "Contador de Juegos"
     feedback: "Apoyo"
     payment_info: "Información de Pago"
-#    campaigns: "Campaigns"
+    campaigns: "Campañas"
     poll: "Encuesta"
-#    user_polls_record: "Poll Voting History"
+    user_polls_record: "Historial de votaciones"

   concepts:
-#    advanced_strings: "Advanced Strings"
-#    algorithms: "Algorithms"
+    advanced_strings: "Cadenas de texto avanzadas"
+    algorithms: "Algoritmos"
     arguments: "Argumentos"
-#    arithmetic: "Arithmetic"
-#    arrays: "Arrays"
-#    basic_syntax: "Basic Syntax"
-#    boolean_logic: "Boolean Logic"
+    arithmetic: "Aritmética"
+    arrays: "Vectores"
+    basic_syntax: "Sintáxis básica"
+    boolean_logic: "Lógica booleanos"
 #    break_statements: "Break Statements"
-#    classes: "Classes"
+    classes: "Clases"
 #    continue_statements: "Continue Statements"
-#    for_loops: "For Loops"
+    for_loops: "Bucles For"
     functions: "Funciones"
-#    graphics: "Graphics"
+    graphics: "Gráficos"
 #    if_statements: "If Statements"
 #    input_handling: "Input Handling"
-#    math_operations: "Math Operations"
+    math_operations: "Operaciones matemáticas"
 #    object_literals: "Object Literals"
-#    parameters: "Parameters"
+    parameters: "Parámetros"
     strings: "Cadenas"
     variables: "Variables"
     vectors: "Vectores"
@@ -1250,7 +1250,7 @@
   delta:
     added: "Añadido"
     modified: "Modificado"
-#    not_modified: "Not Modified"
+    not_modified: "Sin Modificar"
     deleted: "Eliminado"
     moved_index: "Indice Movido"
     text_diff: "Diferencias"


`build/02-csv-ndjson.coffee`: small fixes

--- build/02-csv-ndjson.coffee
+++ build/02-csv-ndjson.coffee
@@ -12,7 +12,7 @@
 targetBase = path.join __dirname, '../data'

 parseAgency = (agency) ->
-	return agency.replace /[^\w]+$/, ''
+	return agency.replace /[^a-zA-Z0-9]+$/, ''

@@ -42,8 +42,6 @@
 			stationFromId:	parseInt data.from_stop_id
 			stationToId:	parseInt data.to_stop_id
 			time:			parseInt data.min_transfer_time
-			tripFromId:		parseInt data.from_trip_id
-			tripToId:		parseInt data.to_trip_id


Make computing the recency score of an item into RankingUtils.

--- background_scripts/completion.coffee
+++ background_scripts/completion.coffee
@@ -101,15 +101,8 @@
     onComplete(suggestions)

   computeRelevancy: (queryTerms, suggestion) ->
-    @oneMonthAgo ||= 1000 * 60 * 60 * 24 * 30
     historyEntry = suggestion.extraRelevancyData
-    recency = Date.now() - historyEntry.lastVisitTime
-    recencyDifference = Math.max(0, @oneMonthAgo - recency) / @oneMonthAgo
-
-    # recencyScore is between [0, 1]. It is 1 when recenyDifference is 0. This qudratic equation will
-    # incresingly discount older history entries.
-    recencyScore = recencyDifference * recencyDifference * recencyDifference
-
+    recencyScore = RankingUtils.recencyScore(historyEntry.lastVisitTime)
     wordRelevancy = RankingUtils.wordRelevancy(queryTerms, suggestion.url, suggestion.title)
     # Average out the word score and the recency. Recency has the ability to pull the score up, but not down.
     score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2
@@ -165,6 +158,20 @@
     titleScore = titleScore * RankingUtils.normalizeDifference(queryLength, title.length)
     (urlScore + titleScore) / 2

+  # Returns a score between [0, 1] which indicates how recent the given timestamp is. Items which are over
+  # a month old are counted as 0. This range is quadratic, so an item from one day ago has a much stronger
+  # score than an item from two days ago.
+  recencyScore: (lastAccessedTime) ->
+    @oneMonthAgo ||= 1000 * 60 * 60 * 24 * 30
+    recency = Date.now() - lastAccessedTime
+    recencyDifference = Math.max(0, @oneMonthAgo - recency) / @oneMonthAgo
+
+    # recencyScore is between [0, 1]. It is 1 when recenyDifference is 0. This quadratic equation will
+    # incresingly discount older history entries.
+    recencyScore = recencyDifference * recencyDifference * recencyDifference
+
+
+
   # Takes the difference of two numbers and returns a number between [0, 1] (the percentage difference).
   normalizeDifference: (a, b) ->
     max = Math.max(a, b)


Comment out unittest prototype for ts-task-edit because affected coverage.

--- src/app/pages/tasks/tsTaskEdit.directive.spec.coffee
+++ src/app/pages/tasks/tsTaskEdit.directive.spec.coffee
@@ -1,4 +1,5 @@
 # Disabled because unittests for directives is extra efforts at the point
+###
 xdescribe 'tsTaskEdit', () ->
   beforeEach(module('trackSeatsApp'))

@@ -28,3 +29,4 @@
     expect(html).toContain(@scope.task.departure_point)
     expect(html).toContain(@scope.task.destination_point)
     expect(html).toContain(@scope.task.train)
+###


Wait until all OPF files have finished loading

--- scripts/gh-book/content.coffee
+++ scripts/gh-book/content.coffee
@@ -24,6 +24,18 @@
     url: -> 'META-INF/container.xml'

     toJSON: -> model.toJSON() for model in @models
+
+    # Extend the `load()` to wait until all content is loaded
+    _loadComplex: (fetchPromise) ->
+      promise = new $.Deferred()
+      fetchPromise.done () =>
+        contentPromises = @map (model) => model.load()
+        $.when.apply($, contentPromises)
+        .fail(() => alert 'problem loading book (OPF)')
+        .done () =>
+          promise.resolve(@)
+      return promise
+
     parse: (json) ->
       # Github.read returns a `{sha: "1234", content: "<rootfiles>...</rootfiles>"}
       sha = json.sha


fixed draggable

fixed draggable

--- src/js/peteshow-view.coffee
+++ src/js/peteshow-view.coffee
@@ -74,7 +74,7 @@
   _handleDragMove: (e) =>
     if @dragging
       position = {}
-      position.x = e.pageX
+      position.x = e.pageX - @$peteshow.width()
       position.y = e.pageY
       @_positionWindow(position)


Delete EventHandler.coffee

Delete EventHandler.coffee

--- src/lib/EventHandler.coffee
+++ src/lib/EventHandler.coffee
@@ -1 +0,0 @@
-


slight adjustment to scroll top when using keyboard nav

--- src/main/coffee/main.coffee
+++ src/main/coffee/main.coffee
@@ -248,7 +248,7 @@
                 objSelf.scrollDisabled = false
                 $(window).scroll(objSelf._onScrollFn)
             $('html, body').animate({
-                scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - $("#ad-block").height() - $(".jumbotron").height()
+                scrollTop: newArticleOffset.top - $("#top-nav-bar").height()
             }, 500, "swing", () -> setTimeout(doneFn, 0))
             e.preventDefault()

@@ -312,7 +312,7 @@
                objSelf.scrollDisabled = false
                $(window).scroll(objSelf._onScrollFn)
            $('html, body').animate({
-                scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - $("#ad-block").height() - $(".jumbotron").height()
+                scrollTop: newArticleOffset.top - $("#top-nav-bar").height()
            }, 500, "swing", () -> setTimeout(doneFn, 0))

     _initializeKeyboardNavigation: ->


Added ability to use RequireJS. See docs.

--- lib/class-complete.coffee
+++ lib/class-complete.coffee
@@ -1,3 +1,4 @@
+
 module.exports =
     activate: ->
         # atom.workspaceView.command "class-complete:complete", => @complete()
@@ -10,30 +11,40 @@
         cursor = editor.cursors[0]
         text = editor.getSelections()[0].getText()

+        # Parse classdef to find out what needs to be generated
         classdef = module.exports.parseClassdef(text)

-        buffer += "#{classdef.fullname} = (function() {\n"
-
-        buffer += module.exports.indent(module.exports.generateMethod(classdef.name, null, classdef.parameters, "constructor", {
-            "before": "#{classdef.extends}.apply(this, arguments);" if classdef.extends
-        }), 1)
-
-        if classdef.extends
+        if classdef.type == "class"
+
+            # Start the class
+            buffer += "#{classdef.fullname} = (function() {\n"
+
+            # Indent and generate all methods needed
+            buffer += module.exports.indent(module.exports.generateMethod(classdef.name, null, classdef.parameters, "constructor", {
+                "before": "#{classdef.extends}.apply(this, arguments);" if classdef.extends
+            }), 1)
+
+            # If classdef extends another class, add appropriate code
+            if classdef.extends
+                buffer += "\n"
+                buffer += "\t#{classdef.name}.prototype = Object.create(#{classdef.extends}.prototype);\n"
+                buffer += "\t#{classdef.name}.prototype.constructor = #{classdef.name};\n"
+
+            if classdef.methods.length > 0
+                for method in [0..classdef.methods.length - 1]
+                    if method >= 0
+                        buffer += "\n"
+
+                    method = classdef.methods[method]
+                    buffer += module.exports.indent(module.exports.generateMethod(classdef.name, method.name, method.parameters, method.type), 1)
+
             buffer += "\n"
-            buffer += "\t#{classdef.name}.prototype = Object.create(#{classdef.extends}.prototype);\n"
-            buffer += "\t#{classdef.name}.prototype.constructor = #{classdef.name};\n"
-
-        if classdef.methods.length > 0
-            for method in [0..classdef.methods.length - 1]
-                if method >= 0
-                    buffer += "\n"
-
-                method = classdef.methods[method]
-                buffer += module.exports.indent(module.exports.generateMethod(classdef.name, method.name, method.parameters, method.type), 1)
-
-        buffer += "\n"
-        buffer += "\treturn #{classdef.name};"
-        buffer += "\n}());"
+            buffer += "\treturn #{classdef.name};"
+            buffer += "\n}());"
+
+        else if classdef.type == "require"
+                buffer += "var #{classdef.varName} = require(\"#{classdef.path}\");"
+                buffer += "\n"

         editor.insertText buffer

@@ -45,10 +56,28 @@
             name: null,
             parameters: [],
             methods: [],
-            extends: null
+            extends: null,
+            type: "class",
+            varName : null,
+            path: null
         }

-        def.fullname = string.split(":")[0]
+        fullname = string.split(":")[0]
+
+        def.fullname = fullname
+
+        if (fullname.toLowerCase() == "r")
+            varName = string.split(":")[1]
+            path = string.split(":")[2]
+            def.type = "require"
+            def.varName = varName
+            def.path = ((path != null) ? path : varName);
+            if typeof path != "undefined" && path.length > 0
+                def.path = path
+            else
+                def.path = varName
+
+            return def

         string = string.replace(/\s/g, "")
         parts = string.split(":")


:bug: Validate and convert Message trace

--- lib/editor-linter.coffee
+++ lib/editor-linter.coffee
@@ -78,5 +78,6 @@
       unless result.type
         throw new Error "Missing type field on Linter Response, Got: #{Object.keys(result)}"
       result.range = Range.fromObject result.range if result.range?
+      EditorLinter.validateResults(result.trace) if result.trace
     results
 module.exports = EditorLinter


Add ambiguous linting ability.

Add ambiguous linting ability.

Just add a wildcard to your scopes array.
Fixes #32.

--- lib/editor-linter.coffee
+++ lib/editor-linter.coffee
@@ -23,6 +23,7 @@
     @lint true unless onChange

     scopes = @editor.scopeDescriptorForBufferPosition(@editor.getCursorBufferPosition()).scopes
+    scopes.push '*' # Ensure that everything can be linted with a wildcard.
     promises = @lintResults onChange, scopes
     Promise.all(promises).then =>
       @progress onChange, false


slight cleanup

slight cleanup

--- lib/editor/result.coffee
+++ lib/editor/result.coffee
@@ -74,23 +74,13 @@
       class: 'ink-underlay'
       invalidate: 'never'
     @expDecoration = @editor.decorateMarker @expMarker, mark
-    setTimeout (=>
-      elRect = @editor.editorElement.getBoundingClientRect()
-      w = elRect.width + elRect.left - 40 -
-          @view.parentElement.getBoundingClientRect().left
-      if w < 100 then w = 100
-      @view.style.maxWidth = w + 'px'), 50
+    @updateWidth()

   collapseView: () ->
     @expanded = false
     @expMarker?.destroy()
     @decorateMarker()
-    setTimeout (=>
-      elRect = @editor.editorElement.getBoundingClientRect()
-      w = elRect.width + elRect.left - 40 -
-          @view.parentElement.getBoundingClientRect().left
-      if w < 100 then w = 100
-      @view.style.maxWidth = w + 'px'), 50
+    @updateWidth()

   createView: (opts) ->
     {content, fade, loading} = opts
@@ -139,20 +129,21 @@
       when 'inline' then mark.type = 'overlay'; mark.class = 'ink-overlay'
       when 'block' then mark.type = 'block'; mark.position = 'after'
     @decoration = @editor.decorateMarker @marker, mark
+    if @type == 'inline'
+      setTimeout (=> resizer.listenTo @editor.editorElement, => @updateWidth()), 50

   initMarker: ->
     @marker = @editor.markBufferRange @lineRange(@start, @end)
     @marker.result = @
     @decorateMarker()
     @disposables.add @marker.onDidChange (e) => @checkMarker e
-    if @type == 'inline'
-      resizer.listenTo @editor.editorElement, (el) =>
-        setTimeout (=>
-          elRect = el.getBoundingClientRect()
-          w = elRect.width + elRect.left - 40 -
-              @view.parentElement.getBoundingClientRect().left
-          if w < 100 then w = 100
-          @view.style.maxWidth = w + 'px'), 50
+
+  updateWidth: ->
+    elRect = @editor.editorElement.getBoundingClientRect()
+    w = elRect.width + elRect.left - 40 -
+        @view.parentElement.getBoundingClientRect().left
+    if w < 100 then w = 100
+    @view.style.maxWidth = w + 'px'

   toggleTree: ->
     trees.toggle $(@view).find('> .tree')


Only allow reloading of expanded directories

--- lib/directory-view.coffee
+++ lib/directory-view.coffee
@@ -56,7 +56,7 @@
       new FileView(entry)

   reload: ->
-    @directory.reload()
+    @directory.reload() if @isExpanded

   buildEntries: ->
     @entries.empty()


activity: add liked cssClass if a post is already liked

--- client/Social/Activity/views/likelink.coffee
+++ client/Social/Activity/views/likelink.coffee
@@ -9,6 +9,8 @@
     super options, data

     { @isInteracted } = data.interactions.like
+
+    @update()

     data
       .on 'LikeAdded',   @bound 'update'
@@ -53,21 +55,8 @@
     @setTemplate @pistachio()

     if @isInteracted
-      @trackLike()
-      @setClass 'liked'
-    else
-      @trackUnlike()
-      @unsetClass 'liked'
-
-
-  trackLike: ->
-
-    KD.mixpanel "Activity like, success"
-
-
-  trackUnlike: ->
-
-    KD.mixpanel "Activity unlike, success"
+    then @setClass 'liked'
+    else @unsetClass 'liked'

   showError: (err) ->


Emit "EditorIsReady" event.

Emit "EditorIsReady" event.

--- client/ide/workspace/panes/editorpane.coffee
+++ client/ide/workspace/panes/editorpane.coffee
@@ -23,6 +23,7 @@
     @aceView.ace.once 'ace.ready', =>
       @getEditor().setValue content, 1
       @ace.setReadOnly yes  if @getOptions().readOnly
+      @emit 'EditorIsReady'

   getEditor: ->
     return @aceView.ace.editor


Use subscribe for window focus listener

Use subscribe for window focus listener

--- src/packages/fuzzy-finder/src/fuzzy-finder.coffee
+++ src/packages/fuzzy-finder/src/fuzzy-finder.coffee
@@ -21,7 +21,7 @@

   initialize: (@rootView) ->
     super
-    $(window).on 'focus', => @projectPaths = null
+    @subscribe $(window), 'focus', => @projectPaths = null
     @observeConfig 'fuzzy-finder.ignoredNames', (ignoredNames) =>
       @projectPaths = null


Add snippets for commonly-inserted Node modules

--- user/snippets.cson
+++ user/snippets.cson
@@ -95,7 +95,20 @@
 	"Read Stdin":
 		prefix: "stdin"
 		body: 'process.stdin.setEncoding("UTF8");\nprocess.stdin.on("readable", () => {\n\tlet chunk = process.stdin.read();\n\tif(null !== chunk){\n\t\t$1\n\t}\n})\n'
-
+
+	# Module shortcuts for the impatient programmer
+	"Module - Filesystem":
+		prefix: "fs"
+		body: 'const fs = require("fs");$1'
+	"Module - Path":
+		prefix: "pat"
+		body: 'const path = require("path");$1'
+	"Module - Child Process":
+		prefix: "cp"
+		body: 'const childProcess = require("child_process");$1'
+	"Module - Print":
+		prefix: "print"
+		body: 'const print = require("print");$1'

 # CoffeeScript/CSON


COMMENT update

COMMENT update

--- source/NeptuneLib/Merge.coffee
+++ source/NeptuneLib/Merge.coffee
@@ -33,7 +33,7 @@
     result

   ###
-  Just like mergeInfo except only merge into the result object UNLESs result.hasOwnProperty
+  Just like mergeInfo except only merge into the result object UNLESS result.hasOwnProperty

   if
     mergeInfo a, b is just like merge a, b except it modifies and returns a instead of returning a new object


[grunt] fetch locales and base locale for i18n task from global data

--- tasks/grunt-nunjucks-2-html.coffee
+++ tasks/grunt-nunjucks-2-html.coffee
@@ -17,15 +17,14 @@
   marked       = require('marked')
   markdown     = require('nunjucks-markdown')

-  # Configuration
-  locales    = ['en-US', 'ru', 'zh-CN']
-  baseLocale = 'en-US'
+  locales    = grunt.template.process('<%= data.site.locales %>')
+  baseLocale = grunt.template.process('<%= data.site.baseLocales %>')
+
+  locale     = grunt.template.process('<%= data.site.lang %>')
+
+  buildDir   = grunt.template.process('<%= path.build.root %>')
+  layoutsDir = grunt.template.process('<%= path.source.layouts %>')
   localeSrc  = grunt.template.process('<%= path.source.locales %>')
-
-  locale       = grunt.template.process('<%= data.site.lang %>')
-
-  buildDir     = grunt.template.process('<%= path.build.root %>')
-  layoutsDir   = grunt.template.process('<%= path.source.layouts %>')

   # Helpers


Bug fix

Bug fix

--- lib/relations/one.coffee
+++ lib/relations/one.coffee
@@ -39,7 +39,7 @@
       Utils.updateModel(previous_related_model, value)

     # clear the reverse relation if it's loaded
-    else if value is null and @reverse_relation and (@reverse_relation.type is 'hasOne' or @reverse_relation.type is 'belongsTo')
+    else if (value is null) and @reverse_relation and (@reverse_relation.type is 'hasOne' or @reverse_relation.type is 'belongsTo')
       unless @embed or @reverse_relation.embed
         if model.isLoaded(@key)
           previous_related_model.set(@reverse_relation.key, null) if previous_related_model and previous_related_model.get(@reverse_relation.key)
@@ -50,8 +50,6 @@

   get: (model, key, callback) ->
     throw new Error "One::get: Unexpected key #{key}. Expecting: #{@key} or #{@ids_accessor}" unless (key is @key or key is @ids_accessor)
-
-    # console.log "GET #{key} is_loaded: #{model.isLoaded(@key)}" if @model_type.model_name is 'Owner'

     returnValue = =>
       return null unless related_model = model.attributes[@key]
@@ -144,7 +142,6 @@
       return callback(err) if err

       model.set(@key, related_model = if json then Utils.updateOrNew(json, @reverse_model_type) else null)
-
       model.setLoaded(@key, true)
       callback(null, related_model)

@@ -166,12 +163,12 @@

     model._orm_bindings = {}
     model._orm_bindings.change = (model) =>
+      related_model = model.get(@key)
+
       # update backlinks
       if previous_related_model = model.previous(@key)
         if @reverse_relation.remove then @reverse_relation.remove(previous_related_model, model) else previous_related_model.set(@reverse_relation.key, null)
-
-      # update backlinks
-      if related_model = model.get(@key)
+      if related_model
         if @reverse_relation.add then @reverse_relation.add(related_model, model) else related_model.set(@reverse_relation.key, model)

     model.on("change:#{@key}", model._orm_bindings.change)


I don't really like that idea

I don't really like that idea

--- test/getter_setter.coffee
+++ test/getter_setter.coffee
@@ -96,11 +96,3 @@
       #this is actually questionable, since there would be no way to "unbind"
       it "when passed with a function as the second parameter get should call it with the value on load and update"

-    describe ".changed()", ->
-      describe "when an attribute has been changed", ->
-        it "should return true when passed the changed attribute's name"
-        it "should return false when passed the changed attribute has not been changed"
-        it "should differentiate between remote and local changes"
-
-
-


Update server.coffee

Update server.coffee

--- lib/server/server.coffee
+++ lib/server/server.coffee
@@ -35,7 +35,7 @@
     atom.notifications.addInfo 'Jekyll server stopped'

   serverStarted: ->
-    atom.notifications.addSuccess 'Jekyll site avliable at http://localhost:' + atom.config.get('jekyll.serverPort')
+    atom.notifications.addSuccess 'Jekyll site available at http://localhost:' + atom.config.get('jekyll.serverPort')

     for editor in atom.workspace.getTextEditors()
       @didOpenFile(editor)


Fixes failing specs by tearing down stubs/benv

--- src/mobile/components/layout/test/layout.test.coffee
+++ src/mobile/components/layout/test/layout.test.coffee
@@ -155,7 +155,7 @@
     }
     @bootstrap()
     @Cookies.expire.callCount.should.equal 0
-
+
   it 'saves an artwork', ->
     @getCookie.returns(
       JSON.stringify({
@@ -204,7 +204,13 @@
       @bootstrap.__set__('FlashMessage', @FlashMessage)
       @logoutEventHandler = @bootstrap.__get__('logoutEventHandler')
       done()
-
+
+  after ->
+    window.location.reload.restore()
+    $.ajax.restore()
+    benv.teardown()
+
+
   it 'logs out the user and tracks the logout event', ->
     @logoutEventHandler()
     $.ajax.args[0][0].url.should.be.eql '/users/sign_out'


cmd-enter to submit comment

cmd-enter to submit comment

Fixes #52

--- templates/embed/CommentForm.coffee
+++ templates/embed/CommentForm.coffee
@@ -21,9 +21,7 @@
     @element.addEventListener 'submit', (event) =>
       event.preventDefault()
       event.stopPropagation()
-      if @hasBody()
-        @thread.add(@body())
-        @element.querySelector('.body').value = ''
+      @saveComment()
     @element.querySelector('a.create').addEventListener 'click', (event) =>
       event.preventDefault()
       event.stopPropagation()
@@ -35,6 +33,9 @@
       event.preventDefault()
       event.stopPropagation()
       @hideCommentForm()
+    @element.querySelector('textarea').addEventListener 'keydown', (event) =>
+      if event.keyCode == 13 and event.metaKey
+        @saveComment()
     document.addEventListener 'hasLoggedIn', =>
       @showCommentForm()

@@ -51,3 +52,8 @@
   hideCommentForm: ->
     @element.querySelector('form').style.display = 'none'
     @element.querySelector('a').style.display = 'block'
+
+  saveComment: ->
+    if @hasBody()
+      @thread.add(@body())
+      @element.querySelector('.body').value = ''


Use correct uniqueId function

Use correct uniqueId function

--- src/spec/integration/integration-product.spec.coffee
+++ src/spec/integration/integration-product.spec.coffee
@@ -99,7 +99,7 @@

   it 'should add, update and delete tax category', (done) ->
     tax =
-      name: _.uniqueId 'myTax'
+      name: uniqueId 'myTax'
       rates: []

     # addition
@@ -120,7 +120,7 @@

     # change
       tax =
-        name: _.uniqueId 'myTax2'
+        name: uniqueId 'myTax2'
         rates: []
       @client.taxCategories.save(tax)
     .then (result) =>


fix overzealous find and replace

fix overzealous find and replace

--- spec/views/subject_spec.coffee
+++ spec/views/subject_spec.coffee
@@ -17,4 +17,4 @@

     it 'returns Subject for unknown type', ->
       model = new Backbone.Model()
-      expect(App.Views.Subject.for(model)).toBe(App.Views.Subject.Subject)
+      expect(App.Views.Subject.for(model)).toBe(App.Views.Subject)


updated base generator

updated base generator

--- lib/generators/rails_script/install/templates/base.js.coffee
+++ lib/generators/rails_script/install/templates/base.js.coffee
@@ -1,5 +1,5 @@
-window.App ||= {}
-class App.Base
+window.<%= RailsScript.app_namespace %> ||= {}
+class <%= RailsScript.app_namespace %>.Base

   constructor: ->
     if (window.jQuery) then @setClearEventHandlers() # clearing application event handlers only possible with jQuery


fix bug in clear event handler code

fix bug in clear event handler code

Events with blank namespace now cleared on page:change

--- lib/generators/rails_script/install/templates/base.js.coffee
+++ lib/generators/rails_script/install/templates/base.js.coffee
@@ -30,8 +30,9 @@
   loads, i.e. "$(window).on 'scroll.app', myHandler"
   ###
   setClearEventHandlers: ->
-    jQuery(document).on 'page:before-change', ->
+    $(document).on 'page:before-change', ->
       for element in [window, document]
-        for event, handlers in (jQuery._data(element, 'events') || {})
+        for event, handlers of ($._data(element, 'events') || {})
           for handler in handlers
-            if handler.namespace == '' then $(element).off event, handler.handler
+            if handler? && handler.namespace == ''
+              $(element).off event, handler.handler


Add deterministic group IDs

Add deterministic group IDs

--- test/util/fakeData.coffee
+++ test/util/fakeData.coffee
@@ -57,6 +57,25 @@
   ]
   statuses[@rand(0, statuses.length-1)]

+Generator.prototype.genGroups = (len) ->
+  groups = [
+    '52c672252dc5138712808e01'
+    '52c672252dc5138712808e02'
+    '52c672252dc5138712808e03'
+    '52c672252dc5138712808e04'
+    '52c672252dc5138712808e05'
+    '52c672252dc5138712808e06'
+    '52c672252dc5138712808e07'
+    '52c672252dc5138712808e08'
+    '52c672252dc5138712808e09'
+    '52c672252dc5138712808e10'
+  ]
+  res = []
+  while res.length < len
+    rand = @rand(0, groups.length-1)
+    res.push groups[rand] if groups[rand] not in res
+  res
+
 Generator.prototype.genTags = ->
   res = []
   tags =
@@ -97,7 +116,7 @@
     navn    : @checksum(Date.now())
     tags    : @genTags()
     privat  : @genPrivat()
-    grupper : @genId(@rand(0,3), true)
+    grupper : @genGroups(@rand(0,3))
     bilder  : @genId(@rand(0,20), true)

   delete doc[key] for key of doc when key in @exclude


_onDisconnect() test for Remote.Peer

_onDisconnect() test for Remote.Peer

--- src/tests/spec/public/models/remote.peer.spec.coffee
+++ src/tests/spec/public/models/remote.peer.spec.coffee
@@ -447,3 +447,34 @@
 				waitsFor(->
 						return success
 					, 1000)
+
+		describe 'when closing a channel', ->
+			it 'should trigger a channel.closed event with the event', ->
+				peer = new Peer(fakeController, '1', true, FakeRTCPeerConnection)
+				success = false
+
+				fakeEvent = {
+					a: 1
+					b: 2
+				}
+				peer.on('channel.closed', ( thePeer, event ) ->
+						expect(thePeer).toBe(peer)
+						expect(event).toBe(fakeEvent)
+						success = true
+					)
+
+				peer._onChannelClose(fakeEvent)
+				waitsFor(->
+						return success
+					, 1000)
+
+		describe 'when the channel disconnects', ->
+			it 'should clear the ping interval', ->
+				peer = new Peer(fakeController, '1', true, FakeRTCPeerConnection)
+				jasmine.Clock.useMock();
+				spyOn(peer, 'ping')
+				peer._onChannelOpen()
+				jasmine.Clock.tick(2501)
+				peer._onDisconnect()
+				jasmine.Clock.tick(2500 * 10)
+				expect(peer.ping.callCount).toBe(1)


:art: Dry up buildTokenizedLineForRow methods on TokenizedBuffer

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@gmail.com>

--- src/tokenized-buffer.coffee
+++ src/tokenized-buffer.coffee
@@ -302,15 +302,9 @@
     new TokenizedLine({tokens, tabLength, indentLevel, @invisibles, lineEnding})

   buildTokenizedLineForRow: (row, ruleStack) ->
-    line = @buffer.lineForRow(row)
-    lineEnding = @buffer.lineEndingForRow(row)
-    tabLength = @getTabLength()
-    indentLevel = @indentLevelForRow(row)
-    {tokens, ruleStack} = @grammar.tokenizeLine(line, ruleStack, row is 0)
-    new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel, @invisibles})
-
-  buildTokenizedLineForRowWithText: (row, line) ->
-    ruleStack = @stackForRow(row - 1)
+    @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack)
+
+  buildTokenizedLineForRowWithText: (row, line, ruleStack = @stackForRow(row - 1)) ->
     lineEnding = @buffer.lineEndingForRow(row)
     tabLength = @getTabLength()
     indentLevel = @indentLevelForRow(row)


修复Tree 控件itemClick事件穿透的Bug

修复Tree 控件itemClick事件穿透的Bug

--- src/widget/list/tree.coffee
+++ src/widget/list/tree.coffee
@@ -120,15 +120,16 @@
 		.delegate(".tree.item", "click", (evt) =>
 			if @_autoExpand
 				itemDom = @_findItemDom(evt.currentTarget)
-				return unless itemDom
+				return false unless itemDom
 				node = cola.util.userData(itemDom, "item")
-				return unless node
+				return false unless node

 				if node.get("expanded")
 					@collapse(node)
 				else if node.get("hasChild") isnt false
 					@expand(node)
 				return false
+			return false
 		)

 		itemsScope = @_itemsScope


updated tests regarding phantomjs version used

--- tests/suites/casper/request.coffee
+++ tests/suites/casper/request.coffee
@@ -2,97 +2,102 @@

 #jshint strict:false

-SERVER = 'http://localhost:54321/'
-ORIGINAL_URL = "tests/site/index.html"
-CHANGED_URL = "tests/site/index.html?foo=bar"
+utils = require "utils"

-setToTrueOnResourceRequested = false
-setToTrueOnResourceReceived = false
-requestURLRequested = ''
-requestURLReceived = ''
+if utils.ltVersion(phantom.version, '1.9.0')
+  casper.test.skip(6, 'PhantomJS version <1.9.0 does not implement request.abort()')
+  casper.test.done()
+else
+  SERVER = 'http://localhost:54321/'
+  ORIGINAL_URL = "tests/site/index.html"
+  CHANGED_URL = "tests/site/index.html?foo=bar"

-onResourceRequested = (casper, requestData, request) ->
-	if requestData.url == (SERVER + ORIGINAL_URL)
-		setToTrueOnResourceRequested = true
-		requestURLRequested = requestData.url
+  setToTrueOnResourceRequested = false
+  setToTrueOnResourceReceived = false
+  requestURLRequested = ''
+  requestURLReceived = ''

-onResourceRequestedWithAbort = (casper, requestData, request) ->
-	if requestData.url == (SERVER + ORIGINAL_URL)
-		request.abort()
+  onResourceRequested = (casper, requestData, request) ->
+    if requestData.url == (SERVER + ORIGINAL_URL)
+      setToTrueOnResourceRequested = true
+      requestURLRequested = requestData.url

-onResourceRequestedWithChangeURL = (casper, requestData, request) ->
-	if requestData.url == (SERVER + ORIGINAL_URL)
-		request.changeUrl(SERVER + CHANGED_URL)
+  onResourceRequestedWithAbort = (casper, requestData, request) ->
+    if requestData.url == (SERVER + ORIGINAL_URL)
+      request.abort()

-onResourceReceived = (casper, response) ->
-	if response.url == (SERVER + ORIGINAL_URL)
-		setToTrueOnResourceReceived = true
-		requestURLReceived = response.url
+  onResourceRequestedWithChangeURL = (casper, requestData, request) ->
+    if requestData.url == (SERVER + ORIGINAL_URL)
+      request.changeUrl(SERVER + CHANGED_URL)

-onResourceReceivedWithChangeURL = (casper, response) ->
-	if response.url == (SERVER + CHANGED_URL)
-		requestURLReceived = response.url
+  onResourceReceived = (casper, response) ->
+    if response.url == (SERVER + ORIGINAL_URL)
+      setToTrueOnResourceReceived = true
+      requestURLReceived = response.url

-setUp = (test) ->
-	casper.options.onResourceRequested = onResourceRequested
-	casper.options.onResourceReceived = onResourceReceived
-	casper.start()
+  onResourceReceivedWithChangeURL = (casper, response) ->
+    if response.url == (SERVER + CHANGED_URL)
+      requestURLReceived = response.url

-setUpWithAbort = (test) ->
-	casper.options.onResourceRequested = onResourceRequestedWithAbort
-	casper.options.onResourceReceived = onResourceReceived
-	casper.start()
+  setUp = (test) ->
+    casper.options.onResourceRequested = onResourceRequested
+    casper.options.onResourceReceived = onResourceReceived
+    casper.start()

-setUpWithChangeURL = (test) ->
-	casper.options.onResourceRequested = onResourceRequestedWithChangeURL
-	casper.options.onResourceReceived = onResourceReceivedWithChangeURL
-	casper.start()
+  setUpWithAbort = (test) ->
+    casper.options.onResourceRequested = onResourceRequestedWithAbort
+    casper.options.onResourceReceived = onResourceReceived
+    casper.start()

-tearDown = (test) ->
-	setToTrueOnResourceRequested = false
-	setToTrueOnResourceReceived = false
-	casper.options.onResourceRequested = null
-	casper.options.onResourceReceived = null
+  setUpWithChangeURL = (test) ->
+    casper.options.onResourceRequested = onResourceRequestedWithChangeURL
+    casper.options.onResourceReceived = onResourceReceivedWithChangeURL
+    casper.start()
+
+  tearDown = (test) ->
+    setToTrueOnResourceRequested = false
+    setToTrueOnResourceReceived = false
+    casper.options.onResourceRequested = null
+    casper.options.onResourceReceived = null

-casper.test.begin "onResourceRequested tests without abort/override", 4,
-	setUp: setUp
-	tearDown: tearDown
-	test: (test) ->
-		casper.open(ORIGINAL_URL).then ->
+  casper.test.begin "onResourceRequested tests without abort/override", 4,
+    setUp: setUp
+    tearDown: tearDown
+    test: (test) ->
+      casper.open(ORIGINAL_URL).then ->

-		casper.wait 3000, ->
-			test.assertEquals setToTrueOnResourceRequested, true, "Casper.options.onResourceRequested called successfully"
-			test.assertEquals requestURLRequested, SERVER+ORIGINAL_URL, "request url successfully recorded"
-			test.assertEquals setToTrueOnResourceReceived, true, "Casper.options.onResourceReceived called successfully"
-			test.assertEquals requestURLReceived, SERVER+ORIGINAL_URL, "response url successfully recorded"
+      casper.wait 200, ->
+        test.assertEquals setToTrueOnResourceRequested, true, "Casper.options.onResourceRequested called successfully"
+        test.assertEquals requestURLRequested, SERVER+ORIGINAL_URL, "request url successfully recorded"
+        test.assertEquals setToTrueOnResourceReceived, true, "Casper.options.onResourceReceived called successfully"
+        test.assertEquals requestURLReceived, SERVER+ORIGINAL_URL, "response url successfully recorded"

-		casper.run ->
-			test.done()
+      casper.run ->
+        test.done()

-casper.test.begin "onResourceRequested tests with request.abort()", 1,
-	setUp: setUpWithAbort
-	tearDown: tearDown
-	test: (test) ->
-		casper.open(ORIGINAL_URL).then ->
+  casper.test.begin "onResourceRequested tests with request.abort()", 1,
+    setUp: setUpWithAbort
+    tearDown: tearDown
+    test: (test) ->
+      casper.open(ORIGINAL_URL).then ->

-		casper.wait 3000, ->
-			test.assertNotEquals setToTrueOnResourceReceived, true, "Casper.options.onResourceReceived correctly never called"
+      casper.wait 200, ->
+        test.assertNotEquals setToTrueOnResourceReceived, true, "Casper.options.onResourceReceived correctly never called"

-		casper.run ->
-			test.done()
+      casper.run ->
+        test.done()

-casper.test.begin "onResourceRequested tests with request.changeUrl()", 1,
-	setUp: setUpWithChangeURL
-	tearDown: tearDown
-	test: (test) ->
-		casper.open(ORIGINAL_URL).then ->
+  casper.test.begin "onResourceRequested tests with request.changeUrl()", 1,
+    setUp: setUpWithChangeURL
+    tearDown: tearDown
+    test: (test) ->
+      casper.open(ORIGINAL_URL).then ->

-		casper.wait 3000, ->
-			test.assertEquals requestURLReceived, SERVER+CHANGED_URL, "response url successfully changed"
+      casper.wait 200, ->
+        test.assertEquals requestURLReceived, SERVER+CHANGED_URL, "response url successfully changed"

-		casper.run ->
-			test.done()
-
+      casper.run ->
+        test.done()


Allow to create loggers with namespace.

Allow to create loggers with namespace.

--- src/coffee/kopi/logging.coffee
+++ src/coffee/kopi/logging.coffee
@@ -6,6 +6,9 @@
   .define (exports, array, object, settings, exceptions) ->

     class LoggerError extends exceptions.ValueError
+
+    # Store all loggers with their names
+    loggers = {}

     ###
     日志
@@ -19,10 +22,11 @@
       accumulators = {}
       # @type {Hash}    日志级别
       levels =
-        debug:  0
-        info:   1
-        warn:   2
-        error:  3
+        log:    0
+        debug:  1
+        info:   2
+        warn:   3
+        error:  4
       # @type {Console} 日志对象
       console = window.console

@@ -54,6 +58,7 @@
         throw new LoggerError("Logger must have a name") unless name
         this._name = name
         this._disabled = false
+        loggers[name] = this

       name: -> this._name

@@ -83,12 +88,12 @@
           if options.accumulate
             accumulators[key].push(time)
             message += " total #{array.sum(accumulators[key])}ms. average #{array.average(accumulators[key])}ms."
-          send("debug", message)
+          send(this._name, "debug", message)
           timers[key] = null

         else
           # start timer
-          send("debug", "#{name} started.")
+          send(this._name, "debug", "#{name} started.")
           timers[key] = new Date()
           accumulators[key] or= [] if options.accumulate
         this
@@ -102,10 +107,14 @@
           this
       defineMethod(levels) for level of levels

+    # Default logger
     logger = new Logger("kopi")
     exports.time = logger.time
     exports.debug = logger.debug
     exports.info = logger.info
     exports.warn = logger.warn
     exports.error = logger.error
-    exports.logger = (name) -> new Logger(name)
+
+    # Factory method for loggers
+    exports.logger = (name) ->
+      if name then loggers[name] or new Logger(name) else logger


added a connect method to caboose-model

added a connect method to caboose-model

--- caboose-model/index.coffee
+++ caboose-model/index.coffee
@@ -17,6 +17,19 @@
     #       logger.error err.stack
     #     process.exit(1)
     @
+
+  # This is to be used to ensure a connection is made
+  # Potentially the connection will not work and connect
+  # could be called multiple times until it works
+  connect: (callback) ->
+    return callback(null, caboose_model.connection) if caboose_model.connection?
+    return callback(new Error('No configuration found for caboose-model')) unless caboose_model.config?
+
+    conn = new caboose_model.Connection()
+    conn.open caboose_model.config, (err, c) ->
+      return callback(err) if err?
+      caboose_model.connection = c
+      callback(null, c)

   'caboose-plugin': {
     install: (util, logger) ->
@@ -65,3 +78,4 @@
 caboose_model.Model = require './lib/model'
 caboose_model.Query = require './lib/query'
 caboose_model.Promise = Caboose.exports.promise
+caboose_model.mongodb = require 'mongodb'


Another conversion to function

Another conversion to function

--- resources/assets/coffee/react/_components/play-detail.coffee
+++ resources/assets/coffee/react/_components/play-detail.coffee
@@ -19,69 +19,65 @@
 {a, div, img, small, span} = React.DOM
 el = React.createElement

-@PlayDetail = React.createClass
-  mixins: [React.addons.PureRenderMixin]
+@PlayDetail = ({score, shown}) ->
+  modsText =
+    if score.mods.length
+      " +#{(mod.shortName for mod in score.mods).join(',')} "
+    else
+      ' '
+  topClasses = 'detail-row'
+  topClasses += ' hidden' unless shown

-  render: ->
-    score = @props.score
-    modsText =
-      if score.mods.length
-        " +#{(mod.shortName for mod in score.mods).join(',')} "
-      else
-        ' '
-    topClasses = 'detail-row'
-    topClasses += ' hidden' unless @props.shown
+  div
+    className: topClasses
+    div
+      className: 'detail-row__icon'
+      div className: "badge-rank badge-rank--full badge-rank--#{score.rank}"

-    div
-      className: topClasses
+    div className: 'detail-row__detail',
       div
-        className: 'detail-row__icon'
-        div className: "badge-rank badge-rank--full badge-rank--#{score.rank}"
-
-      div className: 'detail-row__detail',
+        className: 'detail-row__detail-column detail-row__detail-column--full'
         div
-          className: 'detail-row__detail-column detail-row__detail-column--full'
+          className: 'detail-row__detail-row detail-row__detail-row--main'
+          a
+            href: score.beatmap.url
+            className: 'detail-row__text-score detail-row__text-score--title'
+            title: "#{score.beatmapset.artist} - #{score.beatmapset.title} "
+            "#{score.beatmapset.title} [#{score.beatmap.version}]"
+            ' '
+            small
+              className: 'detail-row__text-score detail-row__text-score--artist'
+              score.beatmapset.artist
+        div
+          className: 'detail-row__detail-row detail-row__detail-row--bottom'
+          span
+            className: 'detail-row__text-score detail-row__text-score--time'
+            dangerouslySetInnerHTML:
+              __html: osu.timeago score.created_at
+      div
+        className: 'detail-row__detail-column detail-row__detail-column--score-data'
+        div
+          className: 'detail-row__score-data detail-row__score-data--mods'
+          el Mods, mods: score.mods
+        div
+          className: 'detail-row__score-data detail-row__score-data--main'
           div
             className: 'detail-row__detail-row detail-row__detail-row--main'
-            a
-              href: score.beatmap.url
-              className: 'detail-row__text-score detail-row__text-score--title'
-              title: "#{score.beatmapset.artist} - #{score.beatmapset.title} "
-              "#{score.beatmapset.title} [#{score.beatmap.version}]"
-              ' '
-              small
-                className: 'detail-row__text-score detail-row__text-score--artist'
-                score.beatmapset.artist
+            span
+              className: 'detail-row__text-score detail-row__text-score--pp'
+              if score.pp
+                osu.trans('users.show.extra.top_ranks.pp', amount: Math.round(score.pp))
+              else
+                score.score.toLocaleString()
           div
-            className: 'detail-row__detail-row detail-row__detail-row--bottom'
-            span
-              className: 'detail-row__text-score detail-row__text-score--time'
-              dangerouslySetInnerHTML:
-                __html: osu.timeago score.created_at
-        div
-          className: 'detail-row__detail-column detail-row__detail-column--score-data'
-          div
-            className: 'detail-row__score-data detail-row__score-data--mods'
-            el Mods, mods: score.mods
-          div
-            className: 'detail-row__score-data detail-row__score-data--main'
-            div
-              className: 'detail-row__detail-row detail-row__detail-row--main'
-              span
-                className: 'detail-row__text-score detail-row__text-score--pp'
-                if score.pp
-                  osu.trans('users.show.extra.top_ranks.pp', amount: Math.round(score.pp))
-                else
-                  score.score.toLocaleString()
-            div
-              className: 'detail-row__score-details'
-              if score.weight
-                div
-                  className: 'detail-row__text-score'
-                  osu.trans 'users.show.extra.top_ranks.weighted_pp',
-                    percentage: "#{Math.round(score.weight.percentage)}%"
-                    pp: osu.trans('users.show.extra.top_ranks.pp', amount: Math.round(score.weight.pp))
+            className: 'detail-row__score-details'
+            if score.weight
               div
                 className: 'detail-row__text-score'
-                osu.trans 'users.show.extra.historical.recent_plays.accuracy',
-                  percentage: "#{(score.accuracy * 100).toFixed(2)}%"
+                osu.trans 'users.show.extra.top_ranks.weighted_pp',
+                  percentage: "#{Math.round(score.weight.percentage)}%"
+                  pp: osu.trans('users.show.extra.top_ranks.pp', amount: Math.round(score.weight.pp))
+            div
+              className: 'detail-row__text-score'
+              osu.trans 'users.show.extra.historical.recent_plays.accuracy',
+                percentage: "#{(score.accuracy * 100).toFixed(2)}%"


add better comments about how event dispatching works

--- dom.coffee
+++ dom.coffee
@@ -37,6 +37,7 @@
 		@cancelable = true
 		@timeStamp = 0
 	stopPropagation: () ->
+	preventDefault: () ->
 	initEvent: (type, canBubble, cancelable) ->
 		@type = type
 		@bubbles = canBubble
@@ -130,13 +131,16 @@
 			chain.unshift chain[0].parentNode
 		chain.unshift @ownerDocument
 		# CAPTURING_PHASE
+		# the capturing phase proceeds from the top of the DOM downward
+		# firing on all ancestral parents of the target node
 		for ancestor in chain
 			evt.currentTarget = ancestor
 			list = ancestor.listeners[true][evt.type]
 			handler(evt) for handler in list if list
 			break if stopped

-		# AT_TARGET
+		# AT_TARGET phase
+		# once the CAPTURE_PHASE is complete, we fire all handlers on the target node
 		evt.eventPhase = Event.AT_TARGET
 		evt.currentTarget = evt.target
 		# fire both capturing and non-capturing handlers
@@ -146,11 +150,21 @@
 		handler(evt) for handler in list if list

 		# BUBBLING_PHASE
+		# after the target itself has been triggered
+		# fire along the ancestral chain in reverse (nearest first, up to the root)
 		if evt.bubbles
 			for ancestor in chain.reverse()
 				evt.currentTarget = ancestor
 				list = ancestor.listeners[false][evt.type]
 				handler(evt) for handler in list if list
+
+		# DEFAULT_PHASE
+		# normally in a browser, there would be some default behavior for an event
+		# for example, a 'click' that bubbled into a link would cause a page request
+		# Event.preventDefault() exists to stop this, but since we have no default actions
+		# there is nothing to prevent and calls to preventDefault() is ignored,
+		# only stopPropagation() has any effect, and only in the CAPTURE_PHASE
+
 	cloneNode: (deep = false) ->
 		ret = new Node(@nodeName, @nodeValue, @nodeType, @ownerDocument)
 		for a of @_private
@@ -233,7 +247,6 @@
 		@childNodes.splice(i, 1, newNode)
 		oldNode
 	toString: (pretty=false,deep=true,indentLevel=0) ->
-		# console.log("Node::toString[#{@nodeName}] from #{@nodeValue}")
 		if pretty
 			indent = repeat("  ", indentLevel)
 			newline = "\n"


Add getLoadSettings to atom metadata section

--- src/atom.coffee
+++ src/atom.coffee
@@ -233,6 +233,12 @@
   # Returns the absolute path to `~/.atom`.
   getConfigDirPath: ->
     @constructor.getConfigDirPath()
+
+  # Public: Get the load settings for the current window.
+  #
+  # Returns an {Object} containing all the load setting key/value pairs.
+  getLoadSettings: ->
+    @constructor.getLoadSettings()

   ###
   Section: Managing The Atom Window
@@ -401,12 +407,6 @@

-  # Public: Get the load settings for the current window.
-  #
-  # Returns an {Object} containing all the load setting key/value pairs.
-  getLoadSettings: ->
-    @constructor.getLoadSettings()
-
   deserializeProject: ->
     Project = require './project'


InspectorFrontendAPI changed to DevToolsAPI

--- src/atom.coffee
+++ src/atom.coffee
@@ -226,7 +226,7 @@

       if openDevTools
         @openDevTools()
-        @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
+        @executeJavaScriptInDevTools('DevToolsAPI.showConsole()')

       @emit 'uncaught-error', arguments...
       @emitter.emit 'did-throw-error', {message, url, line, column, originalError}