We don't focus annotations automatically anymore.

--- client/publication.coffee
+++ client/publication.coffee
@@ -4,9 +4,6 @@
 currentPublication = null
 publicationHandle = null
 publicationCacheHandle = null
-
-# If set to an annotation id, focus on next render
-focusAnnotationId = null

 # We use our own reactive variable for publicationDOMReady and not Session to
 # make sure it is not preserved when site autoreloads (because of a code change).
@@ -84,8 +81,6 @@
     @_pagesDone = 0
     @_pages = []
     @_highlighter = new Highlighter @_$displayWrapper, true
-
-    focusAnnotationId = null

     PDFJS.getDocument(@url(), null, null, @_progressCallback).then (@_pdf) =>
       # Maybe this instance has been destroyed in meantime
@@ -259,7 +254,6 @@
     Notify.debug "Destroying publication #{ @_id }"

     currentPublication = null
-    focusAnnotationId = null

     pages = @_pages or []
     @_pages = null # To remove references to pdf.js elements to allow cleanup, and as soon as possible as this disables other callbacks
@@ -334,8 +328,6 @@
       Notify.error "Error rendering page #{ page.pdfPage.pageNumber }", args

   showTEI: =>
-    focusAnnotationId = null
-
     # To make sure we are starting with empty slate
     @_$displayWrapper.empty()
     publicationDOMReady.set false
@@ -844,8 +836,6 @@
       # TODO: Does Meteor triggers removal if insertion was unsuccessful, so that we do not have to do anything?
       return Notify.meteorError error, true if error

-      focusAnnotationId = annotationId
-
       Meteor.Router.toNew Meteor.Router.annotationPath Session.get('currentPublicationId'), Session.get('currentPublicationSlug'), annotationId

     return # Make sure CoffeeScript does not return anything
@@ -999,21 +989,6 @@
 Template.publicationAnnotations.destroyed = ->
   $(document).off '.publicationAnnotations'

-focusAnnotation = (body) ->
-  return unless body
-
-  if $(body).text().length > 0
-    currentPublication?._highlighter?._annotator?._deselectAllHighlights()
-
-    range = document.createRange()
-    selection = window.getSelection()
-    range.setStart body, 1
-    range.collapse true
-    selection.removeAllRanges()
-    selection.addRange range
-
-  body.focus()
-
 focusEditor = ($editor) ->
   currentPublication?._highlighter?._annotator?._deselectAllHighlights()
   $editor.focus()
@@ -1079,11 +1054,6 @@
     $('.viewer .display-wrapper .highlights-layer .highlights-layer-highlight').trigger 'annotationMouseleave', [@data._id]
     return # Make sure CoffeeScript does not return anything

-  if focusAnnotationId is @data._id
-    focusAnnotationId = null
-
-    focusAnnotation $(@findAll '.body[contenteditable=true]').get(0)
-
 Template.publicationAnnotationsItem.canModify = ->
   @hasMaintainerAccess Meteor.person()

@@ -1159,8 +1129,6 @@

         LocalAnnotation.documents.remove @_id

-        focusAnnotationId = annotationId
-
         Meteor.Router.toNew Meteor.Router.annotationPath Session.get('currentPublicationId'), Session.get('currentPublicationSlug'), annotationId
     else
       Meteor.call 'update-annotation-body', @_id, body, (error, count) =>
@@ -1171,8 +1139,6 @@
         LocalAnnotation.documents.update @_id,
           $unset:
             editing: ''
-
-        focusAnnotationId = @_id

         Meteor.Router.toNew Meteor.Router.annotationPath Session.get('currentPublicationId'), Session.get('currentPublicationSlug'), @_id


add check in apply

add check in apply

--- static/js/upload.coffee
+++ static/js/upload.coffee
@@ -39,7 +39,7 @@
 			if file.size > 10485760
 				return modalAlert('照片不符合要求', '照片太大了， 不要超过10MB哦')

-			if file.type? and file.type.indexOf('image') != -1
+			if file.name.substring(file.name.lastIndexOf('.')).toLowerCase() in ['jpg', 'jpeg', 'png', 'gif']
 				reader = new FileReader()
 				reader.onload = (event)->
 					$('#avatar > img.preview').attr('src', event.target.result)
@@ -52,8 +52,11 @@
 			file = this.files[0]
 			if file.size > 104857600
 				return modalAlert('报名表不符合要求', '报名表太大了， 不要超过100MB哦')
-			else
+
+			if file.name.substring(file.name.lastIndexOf('.')).toLowerCase() in ['pdf', 'zip', 'rar', '7z']
 				$('#apply > img.preview').attr('src', 'static/img/logo.png')
+			else
+				return modalAlert('报名表不符合要求', '报名表格式不对， 请上传PDF / ZIP / RAR / 7Z格式的报名表')

 	$('#submit').click ->
 		if uploaded


Add Worker

Add Worker

--- src/add/perform.coffee
+++ src/add/perform.coffee
@@ -65,6 +65,7 @@
   POL = directum.app.ReferencesFactory.ПОЛ.GetObjectById u.Analit
   POL.Дополнение3 = u.AD.cn
   POL.Save()
+  u.Kod = POL.Код

 # ПРС
 steps.push (u)->
@@ -77,3 +78,27 @@
   Prs.Строка2 = u.AD.mail        		 # Личный e-mail
   Prs.Save()
   u.PrsKod = Prs.Код
+
+# Временный код для поиска кода подразделения по id
+# SQL сервер убивает русские буквы :-)
+id2kod = (id)->
+  directum.app.ReferencesFactory.ПОД.GetObjectById(id).Код
+
+# РАБ
+steps.push (u)->
+  Wrk = directum.app.ReferencesFactory.РАБ.GetComponent()
+  Wrk.Open()
+  Wrk.Insert()
+  popup """
+  Wrk.Персона = #{u.PrsKod}
+  Wrk.Пользователь = #{u.Kod}
+  Wrk.Подразделение = #{id2kod u.Dept.Analit}/#{u.Dept.Analit}
+  """
+  Wrk.Персона = u.PrsKod
+  Wrk.Пользователь = u.Kod
+  Wrk.Подразделение = id2kod u.Dept.Analit
+  Wrk.Строка = u.AD.title                 # Должность
+  Wrk.Дополнение4 = u.AD.telephoneNumber
+  Wrk.Дополнение3 = u.AD.employeeID       # Табельный номер
+  Wrk.Дополнение = "#{u.AD.sn} #{u.AD.givenName} #{u.AD.middleName}"
+  Wrk.Save()


Improve browser support detection

Improve browser support detection

- copy implementation from Modernizr
- enable in Chrome iOS (was buggy two years ago but appears to now work)
- fix loading Turbolinks in an iframe in IE (reading history.state at eval time raises an exception)

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -604,20 +604,19 @@

   window.addEventListener 'popstate', onHistoryChange, false

-# Handle bug in Firefox 26/27 where history.state is initially undefined
-historyStateIsDefined =
-  window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
-
-browserSupportsPushState =
-  window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
-
-browserIsntBuggy =
-  !navigator.userAgent.match /CriOS\//
-
-requestMethodIsSafe =
-  popCookie('request_method') in ['GET','']
-
-browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
+browserSupportsPushState = window.history and 'pushState' of window.history
+
+# Copied from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
+ua = navigator.userAgent
+browserIsBuggy =
+  (ua.indexOf('Android 2.') != -1 or ua.indexOf('Android 4.0') != -1) and
+  ua.indexOf('Mobile Safari') != -1 and
+  ua.indexOf('Chrome') == -1 and
+  ua.indexOf('Windows Phone') == -1
+
+requestMethodIsSafe = popCookie('request_method') in ['GET','']
+
+browserSupportsTurbolinks = browserSupportsPushState and !browserIsBuggy and requestMethodIsSafe

 browserSupportsCustomEvents =
   document.addEventListener and document.createEvent


Tweak crashed dialog messaging

Tweak crashed dialog messaging

--- src/atom-window.coffee
+++ src/atom-window.coffee
@@ -54,8 +54,8 @@
       chosen = dialog.showMessageBox @browserWindow,
         type: 'warning'
         buttons: ['Close Window', 'Reload', 'Keep It Open']
-        message: 'Renderer is crashed'
-        detail: "The renderer process has crashed, a crash report would be generated and you can report it to Atom's github page"
+        message: 'The editor has crashed'
+        detail: 'Please report this issue to https://github.com/github/atom/issues'
       switch chosen
         when 0 then setImmediate => @browserWindow.destroy()
         when 1 then @browserWindow.restart()


Guard against false result

Guard against false result

--- src/atom-window.coffee
+++ src/atom-window.coffee
@@ -30,7 +30,7 @@
     loadSettings = _.extend({}, settings)
     loadSettings.windowState ?= ''
     loadSettings.initialPath = pathToOpen
-    if fs.statSyncNoException(pathToOpen).isFile()
+    if fs.statSyncNoException(pathToOpen).isFile?()
       loadSettings.initialPath = path.dirname(pathToOpen)

     @browserWindow.loadSettings = loadSettings
@@ -54,7 +54,7 @@
       false
     else if pathToCheck is initialPath
       true
-    else if fs.statSyncNoException(pathToCheck).isDirectory()
+    else if fs.statSyncNoException(pathToCheck).isDirectory?()
       false
     else if pathToCheck.indexOf(path.join(initialPath, path.sep)) is 0
       true


editor can set code

editor can set code

--- src/atom/editor.coffee
+++ src/atom/editor.coffee
@@ -116,6 +116,9 @@
   code: ->
     @ace.getSession().getValue()

+  setCode: (code) ->
+    @ace.getSession().setValue code
+
   removeTrailingWhitespace: ->
     return
     @ace.replaceAll "",


prevent subjects from loading in verify mode (keep like this for now)

--- app/controllers/verification.coffee
+++ app/controllers/verification.coffee
@@ -57,6 +57,7 @@
     @loadSubject()

   loadSubject: ->
+    return # KEEP WHILE VERIFICATION NOT IN USE
     # return unless window.app.stack.activePage.target.constructor.name is "Verification"

     # @dataIndex ||= 0


Add workflow ID to tutorial subject

Add workflow ID to tutorial subject

--- app/lib/get_tutorial_subject.coffee
+++ app/lib/get_tutorial_subject.coffee
@@ -4,6 +4,7 @@
   Subject.create
     id: '5077375154558fabd7000003'
     zooniverseId: 'TUTORIAL_SUBJECT'
+    workflowId: '5077375154558fabd7000002'

     location: standard: [
       'images/tutorial-subject/PICT0500.JPG'


Fixed parsing for ThangTypes that don't have lib.properties.

--- app/lib/sprites/SpriteParser.coffee
+++ app/lib/sprites/SpriteParser.coffee
@@ -26,8 +26,8 @@
   parse: (source) ->
     # Grab the library properties' width/height so we can subtract half of each from frame bounds
     properties = source.match(/.*lib\.properties = \{\n.*?width: (\d+),\n.*?height: (\d+)/im)
-    @width = parseInt(properties[1] ? "0", 10)
-    @height = parseInt(properties[2] ? "0", 10)
+    @width = parseInt(properties?[1] ? "0", 10)
+    @height = parseInt(properties?[2] ? "0", 10)

     options = {loc: false, range: true}
     ast = esprima.parse source, options


btn with plain class

btn with plain class

--- src/buttons/index.cjsx
+++ src/buttons/index.cjsx
@@ -153,9 +153,10 @@
   getDefaultProps: ->
     section: 'Reading'
   render: ->
-    {section} = @props
+    {section, className} = @props
+    classes = classnames 'btn-plain', className

-    <BookButton {...@props}>
+    <BookButton {...@props} className={classes}>
       <i className='fa fa-caret-left'></i>
       Return to {section}
     </BookButton>


Comment out watch

Comment out watch

--- app/scripts/natdashboardctrl.coffee
+++ app/scripts/natdashboardctrl.coffee
@@ -207,8 +207,10 @@

     # FIXME: for some reason this watch never gets triggered beyond first load...
     # resorting to ugly click event workaround
-    $scope.$watch "statusChoice", (oldval, newval) ->
-      console.log(oldval + "->" + newval)
+    # Note: using ngChange only paritally solves this. If you click
+    # between radio buttons too quickly it stops working all together
+    #$scope.$watch "statusChoice", (oldval, newval) ->
+    #  console.log(oldval + "->" + newval)

     $scope.selectStatusClicked = (event) ->
       status = event.target.attributes.value.value


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera_fade.coffee
+++ src/camera_fade.coffee
@@ -67,5 +67,5 @@
   fadeOut: (options={}) ->
     {alpha, color, duration} = Object.reverseMerge(options, fadeOutDefaults)

-    configureFade(duration, color, 1)
+    configureFade(duration, color, alpha)


Comments added, describing old functionality Nesting, args_wash

--- cakes.coffee
+++ cakes.coffee
@@ -49,8 +49,8 @@

 exports.Feature = createFeature(['label', 'whitespace', 'style'])

-args_wash = (args)->
-	title = ''  # allow blank  labels
+args_wash = (args)-  # allow blank  labels
+	title = ''
 	cb = undefined

 	if typeof args[0] == 'string' or typeof args[1] == 'function'
@@ -89,7 +89,7 @@
 			else
 				mocha[type] label, cb

-		if options.padding
+		if options.padding  # NESTING Support
 			mocha.describe '◦'[options.padding_color], ->
 				fn()
 		else


Code cleaned up

Code cleaned up

--- cakes.coffee
+++ cakes.coffee
@@ -180,84 +180,5 @@

 exports.System = createSystem(['style'])

-
-###
-gwt = (label, args, options)->
-	[title, cb] = args_wash args
-
-	options = _.extend
-		padding:true
-		pending:true
-	, options
-
-	dic 'it', label, [title,cb], options
-
-args_wash = (args)-  # allow blank  labels
-	title = ''
-	cb = undefined
-
-	if typeof args[0] == 'string' or typeof args[1] == 'function'
-		title = args[0]
-		cb = args[1]
-	else if typeof args[0] == 'function'
-		cb = args[0]
-
-	return [title, cb]
-
-dic = (type, label, args, options={})->  # Dictate to describe() or it()
-
-	options = _.extend
-		padding:false
-		padding_color:'green'
-	, options
-
-	if type in ['describe', 'it']
-
-		[title, cb] = args_wash args
-
-		if args.length == 1 and typeof args[0] == 'function'  # allow blank label
-			label = ''
-		else
-			label = label.replace('%s', title)
-
-		if options.pending
-			if not cb or cb.toString() == (->).toString()  # If Blank
-				label = '◊ '+_.clean(label.stripColors)+' (pending)'
-				cb = null
-
-
-		fn = ->
-			if type is 'it' and not cb
-				mocha[type] label
-			else
-				mocha[type] label, cb
-
-		if options.padding  # NESTING Support
-			mocha.describe '◦'[options.padding_color], ->
-				fn()
-		else
-			fn()
-
-
-exports.Given = ->
-	gwt "Given:".yellow+" %s", arguments
-
-exports.When = ->
-	gwt " When:".yellow+" %s", arguments
-
-exports.Then = ->
-	gwt " Then:".yellow+" %s", arguments
-
-
-exports.And = ->
-	gwt "  And".grey+"  %s", arguments, padding_color:'black'
-
-exports.But = ->
-	gwt "  But".grey+"  %s", arguments, padding_color:'black'
-
-exports.Describe = exports.Spec = ->  # describe() start of spec file
-	dic 'describe', '=== %s ==='.blue, arguments
-###
-
 # Add function names to global scope.
 (global[name] = func for name, func of module.exports)


removed app-control from status checker

removed app-control from status checker

--- server/jobs.coffee
+++ server/jobs.coffee
@@ -5,16 +5,6 @@
   job = new Job Jobs, 'serviceCheck',
     name: 'ETCD'
     url: Settings.etcd()
-  job.repeat
-    repeats: Jobs.forever
-    wait: 1000 * 60
-  job.retry
-    wait: 1000 * 60
-  job.save()
-
-  job = new Job Jobs, 'serviceCheck',
-    name: 'App Control'
-    url: 'http://iqtservices.isd.org:8080/'
   job.repeat
     repeats: Jobs.forever
     wait: 1000 * 60


Implement the updated participant def tests

--- spec/coordinator.coffee
+++ spec/coordinator.coffee
@@ -65,7 +65,7 @@
             else
               return done new Error "Unexpected event #{event}"
           coordinator.on 'participant', eventListener
-
+
           # send two times
           client.registerParticipant definition, (err) ->
             return done err if err
@@ -96,8 +96,44 @@
         # it 'should not be a new FBP node' # XXX: should be tested in ./protocol.coffee instead?

       describe 'discovery message changes component data', ->
-        it 'should send component-changed'
-        it 'should not send new participant'
+        added = []
+        updated = []
+        definition =
+          id: 'multiple-discovery-11'
+          role: 'multiple-discovery'
+          component: 'MultipleDiscovery'
+          inports: [
+            id: 'in'
+            type: 'string'
+            queue: 'foo'
+          ]
+          outports: []
+        eventListener = null
+        before (done) ->
+          eventListener = (event, def) ->
+            console.log event, def
+            if event == 'added'
+              added.push common.clone(def)
+            else if event == 'updated'
+              updated.push common.clone(def)
+              return done()
+            else
+              return done new Error "Unexpected event #{event}"
+          coordinator.on 'participant', eventListener
+
+          # send new definition
+          client.registerParticipant definition, (err) ->
+            done err if err
+
+        after (done) ->
+          coordinator.removeListener 'participant', eventListener
+          return done null
+        it 'should send component-changed', ->
+          chai.expect(updated).to.have.length 1
+          d = updated[0]
+          chai.expect(d.id).to.equal definition.id
+        it 'should not send new participant', ->
+          chai.expect(added).to.have.length 0

       describe 'receiving discovery message for pending connection', ->
         it 'should setup the connection'


Add jasmine.unspy

Add jasmine.unspy

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -63,6 +63,10 @@
     @append obj.inspect()
   else
     emitObject.call(this, obj)
+
+jasmine.unspy = (object, methodName) ->
+  throw new Error("Not a spy") unless object[methodName].originalValue?
+  object[methodName] = object[methodName].originalValue

 window.keyIdentifierForKey = (key) ->
   if key.length > 1 # named key


Make keydownEvent helper method more realistic and add some :lipstick:

The name of keyIdentifier should be captialized

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -3,13 +3,16 @@
 beforeEach ->
   document.querySelector('#jasmine-content').innerHTML = ""

-exports.keydownEvent = (keyIdentifier, {ctrl, shift, alt, cmd, which, target}={}) ->
+exports.keydownEvent = (key, {ctrl, shift, alt, cmd, which, target}={}) ->
   event = document.createEvent('KeyboardEvent')
   bubbles = true
   cancelable = true
   view = null
-  keyIdentifier = keyIdentifier.toUpperCase() if /^[a-z]$/.test(keyIdentifier) and shift
-  keyIdentifier = "U+#{keyIdentifier.charCodeAt(0).toString(16)}" if keyIdentifier.length is 1
+  key = key.toUpperCase() if /^[a-z]$/.test(key) and shift
+  if key.length is 1
+    keyIdentifier = "U+#{key.charCodeAt(0).toString(16)}"
+  else
+    keyIdentifier = key[0].toUpperCase() + key[1..]
   location = KeyboardEvent.DOM_KEY_LOCATION_STANDARD
   event.initKeyboardEvent('keydown', bubbles, cancelable, view,  keyIdentifier, location, ctrl, alt, shift, cmd)
   Object.defineProperty(event, 'target', get: -> target) if target?


pricing: "Complete your registration" button

--- client/Pricing/AppView.coffee
+++ client/Pricing/AppView.coffee
@@ -26,6 +26,8 @@
       return  unless workflow.active
       @breadcrumb.selectItem workflow.active.getOption 'name'

+    workflow.once "PasswordRecoveryToken", (@recoveryToken) =>
+
   hideBreadcrumb:->
     @breadcrumb.hide()
     document.body.classList.remove 'flow'
@@ -74,9 +76,8 @@
     @breadcrumb.selectItem 'thanks'

     subtitle =
-      if createAccount
-      then "Please check your email to complete your registration."
-      else "Now it’s time, time to start Koding!"
+      if loggedIn then "Now it’s time, time to start Koding!"
+      else ""

     @addSubView @thankYou = new KDCustomHTMLView
       cssClass : "pricing-final"
@@ -93,16 +94,14 @@
         title    : "Go to your environment"
         callback : ->
           KD.singleton("router").handleRoute "/Environments"
+    else if createAccount
+      @thankYou.addSubView @getCompleteYourRegistrationButton()

   showGroupCreated: (group, subscription) ->
-
+    {createAccount, loggedIn} = @formData
     @breadcrumb.selectItem 'thanks'

     planCodes = Object.keys subscription.quantities
-    subtitle =
-      if @formData.createAccount
-      then "Please check your email to complete your registration."
-      else ""

     @addSubView @thankYou = new KDCustomHTMLView
       cssClass : "pricing-final"
@@ -110,15 +109,23 @@
         """

#{group.title} has been successfully created

-
#{subtitle}

-        """
-
-    if @formData.loggedIn
+        """
+
+    if loggedIn
       @thankYou.addSubView new KDButtonView
         style    : "solid green"
         title    : "Go to Group"
         callback : ->
           window.open "#{window.location.origin}/#{group.slug}", "_blank"
+    else if createAccount
+      @thankYou.addSubView @getCompleteYourRegistrationButton()
+
+  getCompleteYourRegistrationButton: ->
+    return new KDButtonView
+      style    : "solid green"
+      title    : "Complete your registration"
+      callback : =>
+        window.location.href = "/Register/#{encodeURIComponent @recoveryToken}"  if @recoveryToken

   addGroupForm: ->
     @groupForm = @createGroupForm()
@@ -174,7 +181,7 @@
           name              : "groupSlug"
           validate          :
             rules           :
-              minLength     : 4
+              minLength     : 3

         Visibility          :
           itemClass         : KDSelectBox


handle loadpackage errors

handle loadpackage errors

--- connector-manager.coffee
+++ connector-manager.coffee
@@ -46,8 +46,9 @@

   isUpdateAvailable: (callback=->) =>
     @loadPackageJSON (error, packageJSON) =>
+      return callback false if error?
       request.get "https://registry.npmjs.org/#{@connector}", json: true, (error, response, body) =>
-        callback false if error?
+        return callback false if error?
         callback cmp(body?['dist-tags']?.latest, packageJSON.version) > 0

 module.exports = ConnectorManager


example appdef

example appdef

--- client/views/apps/apps.coffee
+++ client/views/apps/apps.coffee
@@ -1,7 +1,7 @@
 appDefTemplate =
   name: 'appName'
   version: 'appVersion'
-  def: 'name: appName \nversion:appVersion'
+  def: 'name: appName\nversion: appVersion\n\nservice1:\n\timage: imagename:version'

 appSearch = ->
   filterObj = {}


Possible carousel fix?

Possible carousel fix?

--- client/coffeescript/templates/post_wrapper.coffee
+++ client/coffeescript/templates/post_wrapper.coffee
@@ -107,12 +107,12 @@
         cur = Posts.findOne( _id: cur.parent_id )
         ancestors.push(cur.parent_id)
       if parent._id in ancestors && not Session.equals('carousel_handle',null)
-        Meteor.clearInterval(Session.get('carousel_handle'))
         Template.post_wrapper.start_carousel(@)
   }

   start_carousel: (parent_post) ->
     Session.set('carousel_parent', parent_post)
+    Meteor.clearInterval(Session.get('carousel_handle'))
     carousel_handle = Meteor.setInterval( ->
       parent = Session.get('carousel_parent')
       cur_reply = Session.get("reply_#{parent._id}")


Remove ci.log from package directories

Remove ci.log from package directories

--- tasks/spec-task.coffee
+++ tasks/spec-task.coffee
@@ -39,8 +39,8 @@
       grunt.verbose.writeln "Launching #{path.basename(packagePath)} specs."
       spawn options, (error, results, code) ->
         if process.platform is 'win32'
-          process.stdout.write(fs.readFileSync('ci.log'))
-          fs.unlinkSync('ci.log')
+          process.stdout.write(fs.readFileSync(path.join(packagePath, 'ci.log')))
+          fs.unlinkSync(path.join(packagePath, 'ci.log'))

         failedPackages.push path.basename(packagePath) if error
         callback()


Remove synced store debug logs

Remove synced store debug logs

--- app/src/storage/synced_store.coffee
+++ app/src/storage/synced_store.coffee
@@ -54,7 +54,6 @@
   # @param [Object] items Items to store
   # @param [Function] cb A callback invoked once the insertion is done
   set: (items, cb) ->
-    l "SET", _.clone(items)
     return cb?() unless items?
     @_changes.push {type: OperationTypes.SET, key: key, value: value} for key, value of items
     this.debounced_push()
@@ -104,9 +103,7 @@
     p = @client.get_settings_md5().then (md5) =>
       return yes if @_lastMd5 is md5
       @client.get_settings().then (data) =>
-        l 'Received', data
         data = @_decrypt(data)
-        l 'Decrypted', data
         @_merge(data).then =>
           @emit 'pulled'
           @_setLastMd5(md5)
@@ -114,7 +111,6 @@
       if e.status is 404
         throw Errors.NoRemoteData
       throw Errors.NetworkError
-    l 'Resolve', @_deferredPull
     @_deferredPull.resolve(p)
     @_deferredPull.promise.fin => @_deferredPull = null
     p
@@ -176,7 +172,6 @@
         checkData['__hashes'] = _(checkData['__hashes']).without(checkData['__hashes'][0])
         checkData = _(checkData).omit('__hashes') if checkData['__hashes'].length is 0
         [lastCommitHash, __] = @_computeCommit(checkData, @_changes)
-        l _.clone(data), lastCommitHash
         throw Errors.NoChanges if lastCommitHash is data['__hashes'][0]
       # Create commit hash
       [commitHash, pushedData] = @_computeCommit(data, @_changes)
@@ -226,7 +221,6 @@
   _getAllData: ->
     d = ledger.defer()
     @_super().keys (keys) =>
-      l keys
       @_super().get keys, (data) =>
         d.resolve(data)
     d.promise


Fix child element click bug with base view `routeLink`

--- SingularityUI/app/views/view.coffee
+++ SingularityUI/app/views/view.coffee
@@ -10,6 +10,9 @@

     routeLink: (e) =>
         $link = $(e.target)
+
+        $parentLink = $link.parents('a[href]')
+        $link = $parentLink if $parentLink.length

         url = $link.attr('href')


Clean up

Clean up

--- components/inquiry_questionnaire/analytics.coffee
+++ components/inquiry_questionnaire/analytics.coffee
@@ -7,17 +7,13 @@
     { collectorProfile } = user.related()
     { userInterests } = collectorProfile.related()

-    # Export a hash of objects to listen to
-    # + pass into event handlers
-    {
-      modal: modal.view
-      user: user
-      artwork: artwork
-      inquiry: inquiry
-      collectorProfile: collectorProfile
-      userInterests: userInterests
-      state: state
-    }
+    modal: modal.view
+    user: user
+    artwork: artwork
+    inquiry: inquiry
+    collectorProfile: collectorProfile
+    userInterests: userInterests
+    state: state

   attach: (modal) ->
     context = @context modal


updated expression ref to actually look for cached library context values

--- Src/coffeescript/cql-execution/src/elm/reusable.coffee
+++ Src/coffeescript/cql-execution/src/elm/reusable.coffee
@@ -18,8 +18,8 @@
     @name = json.name
     @library = json.libraryName
   exec: (ctx) ->
-    value = if @library then ctx.get(@library)?.get(@name) else ctx.get(@name)
     ctx = if @library then ctx.getLibraryContext(@library) else ctx
+    value = ctx.get(@name)
     if value instanceof Expression
       value = value.exec(ctx)
     value


render the stage

render the stage

--- client/client.coffee
+++ client/client.coffee
@@ -44,4 +44,3 @@

     nextAnimationFrame: ->
         @renderer.render @stage
-


avoid cache missing where require the same file with different name.

e.g.

# file lib/test.js
require('lib/foo')
require('./foo')

'lib/foo' module function should only be executed once.

--- lib/assets/javascripts/stitch_rails.js.coffee
+++ lib/assets/javascripts/stitch_rails.js.coffee
@@ -4,22 +4,20 @@
 cache = {}

 require = (name, root) ->
-  if module = cache[name]
-    return module
+  return cache[name] if name of cache

   path = expand(root, name)
-  if fn = modules[path] || modules[path = expand(path, './index')]
-    module = id: name, exports: {}
-    try
-      cache[name] = module.exports
-      fn(module.exports, ((name) -> require(name, dirname(path))), module)
-      cache[name] = module.exports
-    catch err
-      delete cache[name];
-      throw err
-  else
-    throw "module #{name} not found"
+  if fn = modules[path]
+    dir = dirname(path)
+  else if fn = modules[path + '/index']
+    dir = path

+  return throw "module #{name} not found" unless fn
+
+  return cache[path] if path of cache
+  module = id: name, exports: {}
+  fn(module.exports, ((name) -> require(name, dir)), module)
+  cache[path] = module.exports

 expand = (root, name) ->
   results = []


fix docs

fix docs

--- lib/assets/javascripts/unpoly/modal.js.coffee
+++ lib/assets/javascripts/unpoly/modal.js.coffee
@@ -87,7 +87,7 @@
     A string containing the HTML structure of the modal.
     You can supply an alternative template string, but make sure that it
     defines tag with the classes `up-modal`, `up-modal-dialog` and  `up-modal-content`.
-yy
+
     You can also supply a function that returns a HTML string.
     The function will be called with the modal options (merged from these defaults
     and any per-open overrides) whenever a modal opens.


added check to ensure no failure if .data() is undefined

--- _source/client/templates/components/forms/input.coffee
+++ _source/client/templates/components/forms/input.coffee
@@ -32,7 +32,7 @@

     self = @

-    if self.data().preFill
+    if self.data()?.preFill
       self.value.set self.data().preFill


Fixed wrong comment

Fixed wrong comment

--- client/import.coffee
+++ client/import.coffee
@@ -1,7 +1,7 @@
 # Local (client-only) document of importing files
 class ImportingFile extends Document
   # name: user's file name
-  # preprocessingProgress: progress of reading from file, in %
+  # preprocessingProgress: progress of file preprocessing, in %
   # uploadProgress: progress of uploading file, in %
   # status: current status or error message
   # finished: true when importing has finished


Don't set button small when it's not small

--- core/app/backbone/views/action_button_view.coffee
+++ core/app/backbone/views/action_button_view.coffee
@@ -19,7 +19,7 @@
   constructor: (options={}) ->
     @model = new ActionButtonState

-    @className += ' button-small button'
+    @className += ' button'
     @className += ' button-small' if options.mini

     super


Adding more snippets

Adding more snippets

--- snippets/wait.cson
+++ snippets/wait.cson
@@ -95,4 +95,10 @@
     'body': 'wait "$1" seconds for the for the page to contain the text "$2"'
   'wait61':
       'prefix': 'wait "time" seconds for the for the page to contain the text alias "text"'
-      'body': 'wait "$1" seconds for the for the page to contain the text alias "$2"'+      'body': 'wait "$1" seconds for the for the page to contain the text alias "$2"'
+  'wait62':
+    'prefix': 'wait "time" seconds for the for the page to contain the regex "text"'
+    'body': 'wait "$1" seconds for the for the page to contain the regex "$2"'
+  'wait63':
+      'prefix': 'wait "time" seconds for the for the page to contain the regex alias "text"'
+      'body': 'wait "$1" seconds for the for the page to contain the regex alias "$2"'


added test case for numbers that look like years


Consistent naming, I guess.

Consistent naming, I guess.

--- app/assets/javascripts/roofpig/CubeAnimation.js.coffee
+++ app/assets/javascripts/roofpig/CubeAnimation.js.coffee
@@ -52,17 +52,17 @@
     if @id == 1
       EventHandlers.set_focus(this)

-    @changers = {}
+    @world_changers = {}
     this.animate(true)

   animate: (first_time = false) ->  # called for each redraw
     now = (new Date()).getTime()

-    for own category, changer of @changers
+    for own category, changer of @world_changers
       if changer
         changer.update(now)
-        if changer.finished() then @changers[category] = null
+        if changer.finished() then @world_changers[category] = null
         any_change = true

     if any_change || first_time
@@ -71,8 +71,8 @@
     requestAnimationFrame => this.animate() # request next frame

   add_changer: (category, changer) ->
-    if @changers[category] then @changers[category].finish()
-    @changers[category] = changer
+    if @world_changers[category] then @world_changers[category].finish()
+    @world_changers[category] = changer

   button_click: (name, shift) ->
     switch name


remove updateStreams action

remove updateStreams action

--- app/assets/javascripts/services/event_stream.js.coffee
+++ app/assets/javascripts/services/event_stream.js.coffee
@@ -20,7 +20,7 @@
       if msg = @messageQueue.popObject()
         msg.parsedData = JSON.parse(msg.data)
         if @shouldProcessMessage(msg)
-          description = "Event Stream (#{msg.type}): #{msg.parsedData.subscription_name} -> #{msg.parsedData.action}"
+          description = "Event Stream (#{msg.type}): #{msg.parsedData.subscription_name}"
           Tahi.utils.debug(description, msg)
           @msgResponse(msg.parsedData)
     Ember.run.later(@, 'processMessages', [], interval)
@@ -107,6 +107,3 @@
           record = @store.getById(type, id)
         if record
           record.unloadRecord()
-
-    updateStreams: ->
-      @resetChannels()


Apparently false != true

Apparently false != true

--- app/js/jail_iframe/util/annotated_site_receiver.coffee
+++ app/js/jail_iframe/util/annotated_site_receiver.coffee
@@ -21,7 +21,7 @@
     FactlinkJailRoot.$modalFrame.removeClass 'factlink-control-visible'
     setTimeout( ->
       return unless modalOpen
-      modalOpen = true
+      modalOpen = false
       FactlinkJailRoot.trigger 'modalClosed'
     , 300)


FIX: Remove scope from showTaskView function

--- src/app/projects/partials/directives/student-project-directive.coffee
+++ src/app/projects/partials/directives/student-project-directive.coffee
@@ -18,7 +18,7 @@

     refreshCharts = Visualisation.refreshAll

-    $scope.showTaskView = (task) ->
+    showTaskView = (task) ->
       if not (task or $scope.project.selectedTask)
         task = _.find _.sortBy($scope.project.tasks, 'seq'), (t) -> $scope.taskDefinition(t).target_grade <= $scope.project.target_grade
       else if not task and $scope.project.selectedTask


Safety

Safety

--- components/Store.coffee
+++ components/Store.coffee
@@ -22,7 +22,7 @@
     forwardGroups: false
     async: true
   , (data, groups, out, callback) ->
-    if typeof data is 'object' and data.payload and data.action
+    if data and typeof data is 'object' and data.payload and data.action
       # New-style action object
       if data.state
         # Keep track of last state


protect a few attributes on teams

protect a few attributes on teams

--- controllers/teams.coffee
+++ controllers/teams.coffee
@@ -78,6 +78,8 @@

 # update
 app.put '/teams/:id', [m.loadTeam, m.ensureAccess], (req, res, next) ->
+  unless req.user.admin
+    delete req.body[attr] for attr in ['slug', 'code', 'search']
   _.extend req.team, req.body
   req.team.save (err) ->
     return next err if err and err.name != 'ValidationError'


Add comment to explain nodeType

Add comment to explain nodeType

--- src/mentions-kinder.coffee
+++ src/mentions-kinder.coffee
@@ -109,7 +109,7 @@
   getSerializedText: ->
     textNodes = []
     for node in @$editable[0].childNodes
-      if node.nodeType == 3
+      if node.nodeType == 3 # nodeType 3 is a text node
         textNodes.push node.data
       else if serializedMention = $(node).data('serializedMention')
         textNodes.push serializedMention


Add new child

Add new child

--- src/config/grid.coffee
+++ src/config/grid.coffee
@@ -21,5 +21,8 @@
           path  += "M#{n},1 V#{size - 1} M1,#{n} H#{size - 1} "
         path
     }
+    {
+      _LABEL: "sprites"
+    }
   ]
 }


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/collidable.coffee
+++ test/collidable.coffee
@@ -10,12 +10,12 @@
   .include "Collidable"

   rightObject = GameObject
-    includedModules: [
     x: 60
     y: 50
     width: 20
     height: 20
     velocity: Point(0, 0)
+  .include "Collidable"

   Collidable.separate(leftObject, rightObject)

@@ -33,6 +33,7 @@
     height: 20
     mass: 5
     velocity: Point(1, 0)
+

   rightObject = GameObject
     includedModules: ["Collidable"]


Remove fs require in config.coffee

Remove fs require in config.coffee

--- src/core/config.coffee
+++ src/core/config.coffee
@@ -1,4 +1,3 @@
-fs = require 'fs'
 async = require 'async'

 {readTOML, fileExists} = require './utils'


avoid duplicate password reset form submissions

stop the one time token being used multiple times and provide better user messaging around the sequence of events when resetting the password.

--- app/pages/reset-password.cjsx
+++ app/pages/reset-password.cjsx
@@ -70,29 +70,32 @@
   render: ->

       {if @props.query?.reset_password_token?
-        <form onSubmit={@handleResetSubmit}>
-
Go ahead and enter a new password, then you can get back to doing some science.

+        if @state.resetSuccess
+
You have successfully reset your password, please login to get started.

+        else
+          <form onSubmit={@handleResetSubmit}>
+
Go ahead and enter a new password, then you can get back to doing some research.

-

-            New password:

-            <input ref="password" type="password" className="standard-input" size="20" />
-

+

+              New password:

+              <input ref="password" type="password" className="standard-input" size="20" />
+

-

-            Again, to confirm:

-            <input ref="confirmation" type="password" className="standard-input" size="20" />
-

+

+              Again, to confirm:

+              <input ref="confirmation" type="password" className="standard-input" size="20" />
+

-

-            <button type="submit" className="standard-button">Submit</button>{' '}
-            {if @state.inProgress
-
-            else if @state.resetSuccess
-
-            else if @state.resetError?
-              {@state.resetError.toString()}}
-

-        </form>
+

+              <button type="submit" className="standard-button" disabled={@state.resetError || @state.resetSuccess}>Submit</button>{' '}
+              {if @state.inProgress
+
+              else if @state.resetSuccess
+
+              else if @state.resetError?
+                Something went wrong, please try and reset your password via email again. {@state.resetError.toString()}}
+

+          </form>

       else
         <form onSubmit={@handleEmailSubmit}>
@@ -102,10 +105,7 @@
             <input ref="email" type="email" required onChange={@handleEmailChange} className="standard-input" defaultValue={@props.query?.email} size="50" />

-            {if @state.emailIsValid
-              <button type="submit" className="standard-button">Submit</button>
-            else
-              <button type="submit" className="standard-button" disabled>Submit</button>}
+            <button type="submit" className="standard-button" disabled={!@state.emailIsValid}>Submit</button>

             {' '}


End anagram/example.coffee with a newline.

--- assignments/coffeescript/anagram/example.coffee
+++ assignments/coffeescript/anagram/example.coffee
@@ -8,4 +8,4 @@
 standard_form = (word) ->
   word.toLowerCase().split("").sort().join()

-module.exports = Anagram+module.exports = Anagram


fix multiple running builds

fix multiple running builds

--- test/configSpec.coffee
+++ test/configSpec.coffee
@@ -39,9 +39,12 @@
       fs.writeFileSync(dynamicYaml,
         fs.readFileSync(orgYaml))

+      doneCalled = false
       config = new Config(dynamicYaml, (config)=>
         expect( config.changed ).to.be.true
-        done()
+        unless doneCalled
+          doneCalled = true
+          done()
       )

       fs.writeFileSync(dynamicYaml,


Update default Atom config

Update default Atom config

--- config/atom/config.cson
+++ config/atom/config.cson
@@ -4,8 +4,6 @@
     createFileInstantly: true
     fuzzyMatch: true
   "atom-alignment": {}
-  "atom-css-comb": {}
-  "atom-csscomb": {}
   "atom-fuzzy-grep":
     inputThrottle: 200
   "atom-terminal-panel":
@@ -13,15 +11,38 @@
     statusBarText: "%(dynamic) %(username) [%(hours):%(minutes):%(seconds)]"
   "atom-ternjs":
     displayAboveSnippets: true
-    useSnippets: true
-    useSnippetsAndFunction: true
+    excludeLowerPriorityProviders: true
+    inlineFnCompletionDocumentation: true
+    sort: false
+    ternServerDependencyBudget: 30000
+  "autocomplete-flow":
+    pathToFlowExecutable: "c:/flow/flow"
+  "autocomplete-plus": {}
   blame:
     defaultWidth: 280
   core:
+    customFileTypes:
+      "source.ini": [
+        ".buckconfig"
+        ".hgrc"
+        ".flowconfig"
+      ]
+      "source.json": [
+        ".arcconfig"
+        "BUCK.autodeps"
+      ]
+      "source.python": [
+        "BUCK"
+      ]
     disabledPackages: [
+      "linter-csslint"
       "autocomplete-paths"
-      "react"
-      "linter-csslint"
+      "node-debugger"
+      "emmet"
+      "atom-jade"
+      "atom-terminal-panel"
+      "git-diff-details"
+      "turbo-javascript"
     ]
     ignoredNames: [
       ".git"
@@ -39,6 +60,7 @@
       "git-blame"
     ]
     projectHome: "D:\\repo"
+    telemetryConsent: "limited"
     themes: [
       "one-dark-ui"
       "base16-tomorrow-dark-theme"
@@ -56,14 +78,27 @@
     promptWhenFileHasChangedOnDisk: false
   "git-blame":
     columnWidth: 270
+  "git-diff-details":
+    enableSyntaxHighlighting: true
   "git-plus": {}
   "highlight-selected":
     timeout: 100
+  "indentation-indicator":
+    indicatorPosition: "right"
+    spaceAfterColon: true
   "js-hyperclick": {}
+  "line-ending-selector":
+    defaultLineEnding: "LF"
   linter:
     errorPanelHeight: 26
   "linter-eslint":
     disableWhenNoEslintrcFileInPath: true
+  "linter-flow":
+    executablePath: "c:/flow/flow"
+  "linter-flow-plus":
+    pathToFlowExecutable: "c:/flow/flow"
+    testRuleTwo: true
+  "linter-shellcheck": {}
   "merge-conflicts": {}
   minimap:
     plugins:
@@ -71,21 +106,14 @@
       "find-and-replaceDecorationsZIndex": 0
       "git-diff": true
       "git-diffDecorationsZIndex": 0
+      selection: true
+      selectionDecorationsZIndex: 0
       "split-diff": true
       "split-diffDecorationsZIndex": 0
   "mocha-test-runner":
     nodeBinaryPath: "C:\\nodejs\\node.exe"
-  nuclide:
-    remoteProjectsConfig: []
-  "nuclide-file-tree": {}
-  "nuclide-health":
-    showCpu: true
-    showMemory: true
   pigments: {}
   react: {}
-  "save-session":
-    dataSaveFolder: "C:\\Users\\edloidas\\.atom\\packages\\save-session\\projects"
-    skipSavePrompt: false
   "smart-tab-name": {}
   "split-diff":
     diffWords: true


CREATE-628 Add 'is-active' class when button is active

--- src/modules/toolbar.coffee
+++ src/modules/toolbar.coffee
@@ -78,6 +78,7 @@
       @triggering = false
     else
       $input.toggleClass('ql-active', value or false)
+      $input.toggleClass('is-active', value or false)

   updateActive: (range, formats = null) ->
     range or= @quill.getSelection()


Refactored to its own method

Refactored to its own method

--- desktop/components/json_page/client/editor.coffee
+++ desktop/components/json_page/client/editor.coffee
@@ -84,15 +84,29 @@
     # Remove some other features
     $(@disable.join ',').remove()
     $('input, textarea').each initImageUpload
+
     $('.hulk-array').each ->
       $(this).children('.hulk-array-element').each ->
         attachRemove $(this)
+
       $(this).append $button = $ "<button>Add</button>"
+
       $button.click ->
         attachRemove $el = $(this).prev().clone()
         $(this).before $el

-    # Add sorting arrows
+    @addSortingArrows()
+    this
+
+  #
+  # Adds the ability to sort array items with arrows.
+  # Unfortunately Hulk.js doesn't provide this functionality or the ability to
+  # re-render based on data manipulations so we have to jump through hoops to
+  # associate the DOM to `@data` and update the ordering for both.
+  # We could porbably avoid this classic problem with a React based solution:
+  # https://github.com/arqex/react-json
+  #
+  addSortingArrows: ->
     $('.hulk-array-element').prepend "

▲

@@ -137,5 +151,3 @@
       goUpALevelFrom $(e.target)
     $('.json-page-array-header-up').click moveArrayItem('up')
     $('.json-page-array-header-down').click moveArrayItem('down')
-
-    this


Use proper accessor (html/text), for #895

--- src/resources/forms/orbeon/builder/resources/cell/label-editor.coffee
+++ src/resources/forms/orbeon/builder/resources/cell/label-editor.coffee
@@ -5,6 +5,11 @@
     currentLabel = null
     isLabelHtml = -> currentControl.is('.fb-label-is-html')
     setLabelHtml = (isHtml) -> currentControl.toggleClass('.fb-label-is-html', isHtml)
+    labelValue = (value) ->
+        valueAccessor = if isLabelHtml() then currentLabel.html else currentLabel.text
+        valueAccessor = _.bind(valueAccessor, currentLabel)
+        # Don't pass value if undefined, as an undefined parameter is not the same to jQuery as no parameter
+        if value? then valueAccessor(value) else valueAccessor()

     # Called when users press enter or tab out
     endEdit = ->
@@ -21,8 +26,8 @@
             labelEditor().checkbox.tooltip('destroy')
             labelEditor().container.hide()
             # Update values in the DOM, without waiting for the server to send us the value
-            currentLabel.html(newLabel)
             setLabelHtml(isChecked)
+            labelValue(newLabel)

     # Heuristic to close the editor based on click and focus events
     clickOrFocus = ({target}) ->
@@ -71,7 +76,7 @@
         labelEditor().container.show()
         labelEditor().container.offset(currentLabel.offset())
         labelEditor().textfield.outerWidth(currentLabel.outerWidth() - labelEditor().checkbox.outerWidth(true))
-        labelEditor().textfield.val(currentLabel.html()).focus()
+        labelEditor().textfield.val(labelValue()).focus()
         labelEditor().checkbox.prop('checked', isLabelHtml())

     $('.fb-main').on('click', '.xforms-label', startEdit)


Exception on click on styled label, fixes #892 - handle click inside label: `currentTarget` or check parent is label - read and set content of label with `html()` instead of `text()`

--- src/resources/forms/orbeon/builder/resources/cell/label-editor.coffee
+++ src/resources/forms/orbeon/builder/resources/cell/label-editor.coffee
@@ -21,14 +21,18 @@
             labelEditor().checkbox.tooltip('destroy')
             labelEditor().container.hide()
             # Update values in the DOM, without waiting for the server to send us the value
-            currentLabel.text(newLabel)
+            currentLabel.html(newLabel)
             setLabelHtml(isChecked)

     # Heuristic to close the editor based on click and focus events
     clickOrFocus = ({target}) ->
         target = $(target)
-        eventOnEditor = labelEditor().textfield.is(target) or labelEditor().checkbox.is(target)
-        eventOnControlLabel = target.is('.xforms-label') && target.parents('.fb-main').is('*')
+        eventOnEditor = labelEditor().textfield.is(target) || labelEditor().checkbox.is(target)
+        eventOnControlLabel =
+            # Click on label or element inside label
+            (target.is('.xforms-label') || target.parents('.xforms-label').is('*')) &&
+            # Only interested in labels in the "editor" portion of FB
+            target.parents('.fb-main').is('*')
         endEdit() unless eventOnEditor or eventOnControlLabel

     # Returns a <div> which contains the text field and checkbox
@@ -47,8 +51,8 @@
         editor

     # Show editor on click on label
-    startEdit = ({target}) ->
-        currentLabel = $(target)
+    startEdit = ({currentTarget}) ->
+        currentLabel = $(currentTarget)
         # Find control for this label
         th = currentLabel.parents('th')
         currentControl =
@@ -56,9 +60,9 @@
                 # Case of a repeat
                 trWithControls = th.parents('table').find('tbody tr.fb-grid-tr').first()
                 tdWithControl = trWithControls.children(':nth-child(' + (th.index() + 1) + ')')
-                tdWithControl.find('.xforms-control, .xbl-component').first()
+                tdWithControl.find('.xforms-control, .xbl-component')
             else
-                currentLabel.parent('.xforms-control, .xbl-component')
+                currentLabel.parents('.xforms-control, .xbl-component').first()
         # Remove `for` so browser doesn't set the focus to the control on click
         currentLabel.removeAttr('for')
         # Setup tooltip for editor (don't do this once for all, as the language can change)
@@ -67,7 +71,7 @@
         labelEditor().container.show()
         labelEditor().container.offset(currentLabel.offset())
         labelEditor().textfield.outerWidth(currentLabel.outerWidth() - labelEditor().checkbox.outerWidth(true))
-        labelEditor().textfield.val(currentLabel.text()).focus()
+        labelEditor().textfield.val(currentLabel.html()).focus()
         labelEditor().checkbox.prop('checked', isLabelHtml())

     $('.fb-main').on('click', '.xforms-label', startEdit)


Fix to make some functions in object available from other functions

--- src/resources/forms/orbeon/builder/resources/grid/placeholders.coffee
+++ src/resources/forms/orbeon/builder/resources/grid/placeholders.coffee
@@ -146,14 +146,14 @@
                 $ selectors

         conditions:
-            isEmpty: (element) -> (f$.text editableInitialValue element) == ''
-            isNonEmpty: (element) -> not conditions.isEmpty element
-            pointerInsideCell: do ->
+            isEmpty: isEmpty = (element) -> (f$.text editableInitialValue element) == ''
+            isNonEmpty: (element) -> not isEmpty element
+            pointerInsideCell: pointerInsideCell = do ->
                 currentCell = null
                 Builder.mouseEntersGridTdEvent.subscribe ({gridTd}) -> currentCell = gridTd
                 Builder.mouseExitsGridTdEvent.subscribe () -> currentCell = null
                 (element) -> currentCell? and f$.is '*', f$.closest currentCell, element
-            pointerOutsideCell: (element) -> not conditions.pointerInsideCell element
+            pointerOutsideCell: (element) -> not pointerInsideCell element
             isNextSeqNo: (element) ->
                 storeSeqNo = parseInt (f$.data 'seqNo', element)
                 (currentSeqNo element) == storeSeqNo + 1


test redirect

test redirect

--- test/components/student-dashboard.spec.coffee
+++ test/components/student-dashboard.spec.coffee
@@ -7,11 +7,11 @@
 ReactTestUtils = React.addons.TestUtils
 {routerStub}   = require './helpers/utilities'
 {sinon}        = require './helpers/component-testing'
-WindowHelpers  = require '../../src/helpers/window'

 {StudentDashboardShell} = require '../../src/components/student-dashboard'
 {StudentDashboardStore, StudentDashboardActions} = require '../../src/flux/student-dashboard'
 {CourseListingActions, CourseListingStore} = require '../../src/flux/course-listing'
+CountdownRedirect = require '../../src/components/countdown-redirect'

 COURSE = require '../../api/user/courses/1.json'
 {CourseActions, CourseStore} = require '../../src/flux/course'
@@ -99,13 +99,13 @@
       expect(classes)
         .to.have.deep.equal([['workable'], ['workable'], [], [], [], []])

-  it 'redirects when a CC course', ->
-    sinon.stub(WindowHelpers, 'replaceBrowserLocation')
+  it 'displays redirect when a CC course', ->
     @course = _.clone(STUDENT_COURSE_ONE_MODEL)
     @course.is_concept_coach = true
     @course.webview_url = 'http://test.com/cc'
     CourseListingActions.loaded([@course])
-
-    renderDashBoard(STUDENT_COURSE_ONE_MODEL.id).then (state) =>
-      expect(WindowHelpers.replaceBrowserLocation.calledWith(@course.webview_url)).to.be.true
-      WindowHelpers.replaceBrowserLocation.restore()
+    routerStub.goTo("/courses/#{STUDENT_COURSE_ONE_MODEL.id}/list").then (result) ->
+      expect(
+        ReactTestUtils.findRenderedComponentWithType(result.component, CountdownRedirect)
+      ).to.exist
+      expect(result.div.textContent).to.include('redirected to tyour Concept Coach textbook')


updating spec to be meaningful

updating spec to be meaningful

--- test/components/task-plan/builder.spec.coffee
+++ test/components/task-plan/builder.spec.coffee
@@ -6,6 +6,7 @@
 {ExtendBasePlan, PlanRenderHelper} = require '../helpers/task-plan'

 yesterday = (new Date(Date.now() - 1000 * 3600 * 24)).toString()
+tomorrow = (new Date(Date.now() + 1000 * 3600 * 24)).toString()

 NEW_READING = ExtendBasePlan({id: "_CREATING_1", settings: {page_ids: []}})
 PUBLISHED_MODEL = ExtendBasePlan({
@@ -49,7 +50,6 @@
   it 'does not load a default due at for all periods', ->
     helper(NEW_READING).then ({dom, element}) ->
       element.setIndividualPeriods()
-      expect(dom.querySelector('#hide-periods-radio').checked).to.be.false
       element.setAllPeriods()
-      expect(dom.querySelector('#hide-periods-radio').checked).to.be.true
-
+      dueAt = TaskPlanStore.getDueAt(NEW_READING.id)
+      expect(dueAt).to.be.falsy


tests: fix destroy hanging for recent node versions

--- test/api.destroy.coffee
+++ test/api.destroy.coffee
@@ -51,7 +51,8 @@
     parser.on 'error', (err) ->
       err.message.should.eql 'Catch me'
       parser._readableState.destroyed.should.be.true()
-      next() if /^v(14)\./.test process.version
+      version = parseInt /^v(\d+)/.exec(process.version)[1], 10
+      next() if version >= 14
     parser.on 'end', ->
       next()
     generate length: 2, seed: 1, columns: 2, fixed_size: true


Fix SDK stylesheet importing.

Fix SDK stylesheet importing.

--- compiler/compileAssets.coffee
+++ compiler/compileAssets.coffee
@@ -40,7 +40,7 @@
         file: source_path
         includePaths: [
             project_directory
-            path.join(project_directory, 'node_modules', 'marquee-static-sdk', 'stylesheets')
+            path.join(project_directory, 'node_modules', 'proof-sdk', 'stylesheets')
         ]
         , (err, compiled) ->
             throw err if err


Fix progress update on Download jobs

Fix progress update on Download jobs

--- test/lib/Task/ActivityTask/Download/FreshdeskDownloadUsersSpec.coffee
+++ test/lib/Task/ActivityTask/Download/FreshdeskDownloadUsersSpec.coffee
@@ -43,7 +43,7 @@
             params: {}
       , input
     ,
-      {}
+      activityId: "FreshdeskDownloadUsers"
     ,
       in: new stream.PassThrough({objectMode: true})
       out: new stream.PassThrough({objectMode: true})
@@ -67,9 +67,7 @@
         Commands.insert
           _id: task.commandId
           progressBars: [
-            activityId: "FreshdeskReadUsers", isStarted: true, isFinished: false
-          ,
-            activityId: "FreshdeskSaveUsers", isStarted: true, isFinished: false
+            activityId: "FreshdeskDownloadUsers", isStarted: true, isFinished: false
           ]
       ]

@@ -93,10 +91,7 @@
         .then ->
           Commands.findOne(task.commandId)
           .then (command) ->
-            command.progressBars[0].total.should.be.equal(0)
-            command.progressBars[0].current.should.be.equal(934)
-            command.progressBars[1].total.should.be.equal(0)
-            command.progressBars[1].current.should.be.equal(934)
+            command.progressBars[0].should.be.deep.equal activityId: "FreshdeskDownloadUsers", total: 0, current: 934, isStarted: true, isFinished: false
         .then resolve
         .catch reject
         .finally recordingDone


Cleaned up tests.

Cleaned up tests.

--- test/config-test.coffee
+++ test/config-test.coffee
@@ -25,15 +25,6 @@
       topic: ->
         new config.Config

-      'when we do c.open()':
-        topic: (config) ->
-          config.open()
-
-        'returns an object': (config) ->
-          assert.equal _.isObject(config), true
-
-        'returns an empty file': (config) ->
-          assert.equal _.size(config), 0

       'and we do c.getHostname(http://example.com)':
         topic: (config) ->
@@ -69,7 +60,6 @@

       'when we do c.addPlugin(spell-check)':
         topic: (config) ->
-          config.open()
           config.addHostname 'http://example.com'
           config.addPlugin 'http://example.com', 'spell-check', {}

@@ -122,4 +112,4 @@
       'returns false': (config) ->
         assert.equal config, false

-  .export(module)
+  .export(module)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/core_object.coffee
+++ test/core_object.coffee
@@ -50,6 +50,7 @@

     test: true

+  o = Core

 test "#include multiple", ->
   o = Core


Parseint for data-step in datetimepicker

--- vendor/assets/javascripts/simple_form_extension/datetimepicker.coffee
+++ vendor/assets/javascripts/simple_form_extension/datetimepicker.coffee
@@ -14,7 +14,7 @@
     @$input.datetimepicker(
       lang: @locale
       format: @$input.data('format')
-      step: @$input.data('step')
+      step: parseInt @$input.data('step')
       dayOfWeekStart: @$input.data('week-start-day')
     )

@@ -26,7 +26,7 @@
     @$input.datetimepicker(
       lang: @locale
       timepicker: false
-      step: @$input.data('step')
+      step: parseInt @$input.data('step')
       format: @$input.data('format')
       dayOfWeekStart: @$input.data('week-start-day')
     )
@@ -36,7 +36,7 @@
     @$input.datetimepicker(
       lang: @locale
       datepicker: false,
-      step: @$input.data('step')
+      step: parseInt @$input.data('step')
       format: @$input.data('format')
     )


added HAVING tests

added HAVING tests

--- test/select.test.coffee
+++ test/select.test.coffee
@@ -119,6 +119,18 @@
               toParam: ->
                 assert.same @inst.toParam(), {
                   text: 'SELECT DISTINCT field1 AS "fa1", field2 FROM table, table2 `alias2` WHERE (a = (SELECT field1 FROM table1 WHERE (field2 = ?)) AND (b = ? OR c = ?)) GROUP BY field, field2'
+                  values: [10, 2, 3]
+                }
+
+            '>> having(squel.expr().and(a = ?, QueryBuilder).and_begin().or(b = ?, 2).or(c = ?, 3).end())':
+              beforeEach: ->
+                subQuery = squel.select().field('field1').from('table1').having('field2 = ?', 10)
+                @inst.having(squel.expr().and("a = ?", subQuery).and_begin().or("b = ?", 2).or("c = ?", 3).end())
+              toString: ->
+                assert.same @inst.toString(), 'SELECT DISTINCT field1 AS "fa1", field2 FROM table, table2 `alias2` GROUP BY field, field2 HAVING (a = (SELECT field1 FROM table1 HAVING (field2 = 10)) AND (b = 2 OR c = 3))'
+              toParam: ->
+                assert.same @inst.toParam(), {
+                  text: 'SELECT DISTINCT field1 AS "fa1", field2 FROM table, table2 `alias2` GROUP BY field, field2 HAVING (a = (SELECT field1 FROM table1 HAVING (field2 = ?)) AND (b = ? OR c = ?))'
                   values: [10, 2, 3]
                 }


More error messages in spec helper

More error messages in spec helper

--- test/spec_helper.coffee
+++ test/spec_helper.coffee
@@ -49,7 +49,7 @@
     actual = @obj.textContent.trim().replace(/\s/g, ' ')
     @assert actual is text, "expected #{actual} to be #{text}"
   chai.Assertion::attribute = (name) ->
-    @assert @obj.hasAttribute(name)
+    @assert @obj.hasAttribute(name), "expected #{@obj} to have attribute #{name}"
   chai.Assertion::triggerEvent = (event, options={}) ->
     args = null
     count = 0
@@ -61,7 +61,7 @@
     @obj()
     event.unbind fun

-    @assert count is 1, "event #{event.name} was triggered #{count} times, expected 1"
+    @assert count is 1, "event #{event.name} was triggered #{count} times, expected 1", "event #{event.name} should not have been triggered"
     if options.with
       @assert compareArrays(args, options.with), "event arguments #{args} do not match expected arguments #{options.with}"
   chai.Assertion::become = (value, done) ->


Adding argument indentation test.

Adding argument indentation test.

--- test/test_indent.coffee
+++ test/test_indent.coffee
@@ -133,4 +133,16 @@
             # FIXME: uncomment failing test
             #assert.isEmpty(errors)

+    'Arbitrarily indented arguments' :
+
+        topic : """
+            myReallyLongFunction withLots,
+                                 ofArguments,
+                                 everywhere
+            """
+
+        'are permitted' : (source) ->
+            errors = coffeelint.lint(source)
+            assert.isEmpty(errors)
+
 }).export(module)


[Editor] Fixed up syncing. Sync object parent changes.

--- assets/js/view_models/editor.coffee
+++ assets/js/view_models/editor.coffee
@@ -79,25 +79,11 @@

   # triggered by clicking a tab's close button
   closeTab: (tab, event) =>
-    remove = (restore = false) =>
-      tab.restore() if restore
-      tabIndex = @tabs.indexOf(tab)
-      selectOther = tab.active()
-      @tabs.remove(tab)
-      if selectOther
-        otherTab = @tabs()[tabIndex]
-        if not otherTab? and tabIndex > 0
-          otherTab = @tabs()[tabIndex-1]
-        @selectTab(otherTab) if otherTab?
-
-      if @tabs().length == 0
-        @selectedTab null
-
     if tab.dirty()
       bootbox.confirm "Are you sure you want to close this #{tab.type}?  Your unsaved changes will be lost.", (close) =>
-        remove(true) if close
+        @removeTab(tab) if close
     else
-      remove()
+      @removeTab tab

     event.stopPropagation()

@@ -156,6 +142,12 @@
     @socket.emit 'get_tree', null, (tree) =>
       @objects tree.map (o) => new TreeNode o, @

+  clearEditor: ->
+    @objects []
+    @tabs []
+    @selectedObject null
+    @selectedTab null
+
   findInTree: (id) ->
     find = (id, o) ->
       if o.id == id
@@ -173,6 +165,49 @@

     found

+  removeFromTree: (id) ->
+    removeChild = (id, p) =>
+      [match] = (child for child in p.children() when child.id is id)
+      if match?
+        p.children.remove match
+        match
+      else
+        removed = null
+        for child in p.children()
+          removed = removeChild(id, child)
+          break if removed?
+        removed
+
+    removed = null
+    for o in @objects()
+      removed = removeChild id, o
+      break if removed?
+
+    removed
+
+  insertIntoTree: (obj) ->
+    insertChild = (o, p) =>
+      if p.id == o.parent_id
+        if o instanceof TreeNode
+          p.children.push o
+        else
+          p.children.push new TreeNode o, @, p.level+1
+        p.children.sort (l, r) -> l.id - r.id
+        true
+      else
+        added = false
+        for child in p.children()
+          added = insertChild(o, child)
+          break if added
+        added
+
+    added = false
+    for o in @objects()
+      added = insertChild obj, o
+      break if added
+
+    added
+
   findInTabs: (id) ->
     os = @tabs().map (tab) ->
       switch tab.type
@@ -184,6 +219,33 @@
     [match] = os.filter (o) -> o.id is id

     match || null
+
+  removeFromTabs: (id) ->
+    tabsToRemove = []
+    for tab in @tabs()
+      switch tab.type
+        when 'property'
+          o = tab.property.object
+        when 'verb'
+          o = tab.verb.object
+      if o.id is id
+        tabsToRemove.push tab
+
+    @removeTab tab for tab in tabsToRemove
+
+  removeTab: (tab) ->
+    tab.restore()
+    tabIndex = @tabs.indexOf(tab)
+    selectOther = tab.active()
+    @tabs.remove(tab)
+    if selectOther
+      otherTab = @tabs()[tabIndex]
+      if not otherTab? and tabIndex > 0
+        otherTab = @tabs()[tabIndex-1]
+      @selectTab(otherTab) if otherTab?
+
+    if @tabs().length == 0
+      @selectedTab null

   #################
   # Context Menus #
@@ -225,39 +287,18 @@

   new_object: (obj) =>
     obj.children = []
-
-    insertChild = (o, p) =>
-      if p.id == o.parent_id
-        p.children.push new TreeNode o, @, p.level+1
-        return true
-      else
-        added = false
-        for child in p.children()
-          added = insertChild(o, child)
-          break if added
-        return added
-
-    for o in @objects()
-      break if insertChild obj, o
+    @insertIntoTree obj

   rm_object: (id) =>
-    removeChild = (id, p) =>
-      [match] = (child for child in p.children() when child.id is id)
-      if match?
-        p.children.remove match
-        return true
-      else
-        removed = false
-        for child in p.children()
-          removed = removeChild(id, child)
-          break if removed
-        return removed
-
-    for o in @objects()
-      break if removeChild id, o
+    @removeFromTree id
+    @removeFromTabs id
+    if @selectedObject().id is id
+      @selectedObject null

   object_parent_changed: (spec) =>
-    console.log 'TODO: object parent changed:', spec
+    o = @removeFromTree spec.id
+    o.parent_id = spec.parent_id
+    @insertIntoTree o

   object_name_changed: (spec) =>
     o = @findInTree spec.id
@@ -271,26 +312,31 @@
   #############################

   connect: =>
-    console.log 'Connected!'
+    # console.log 'Connected!'
     @loadBrowser()

   connecting: =>
-    console.log 'Connecting...'
+    # console.log 'Connecting...'

   disconnect: =>
-    console.log 'Disconnected from server.'
+    # console.log 'Disconnected from server.'
+    bootbox.alert 'Disconnected from server.'
+    @clearEditor()

   connect_failed: =>
-    console.log 'Connection to server failed.'
+    # console.log 'Connection to server failed.'
+    bootbox.alert 'Connection to server failed.'

   error: =>
-    console.log 'An unknown error occurred.'
+    # console.log 'An unknown error occurred.'
+    bootbox.alert 'An unknown error occurred.'

   reconnect_failed: =>
-    console.log 'Unable to reconnect to server.'
+    # console.log 'Unable to reconnect to server.'
+    bootbox.alert 'Unable to reconnect to server.'

   reconnect: =>
     # console.log 'Reconnected!'

   reconnecting: =>
-    console.log 'Attempting to reconnect...'+    # console.log 'Attempting to reconnect...'


Registered traphicam.com.  Cleaned up camera parsing file comments and arrangement.

--- prod/src/coffee-script/node/camparser.coffee
+++ prod/src/coffee-script/node/camparser.coffee
@@ -8,38 +8,50 @@

 parser = new xml2js.Parser()

+# Read and parse camera KML.
+fs.readFile 'data/raw/trafficcameraswithfeed.kml', (err, data) ->
+    if err then console.log err else parser.parseString data
+
+# Parse camera KML into MongoDB.
 parser.addListener 'end', (result) ->
-    # Define the mongoose schema
+    # Define the mongoose schema.
     Camera = new mongoose.Schema
         name: String
         url: String
         loc:
             x: Number
             y: Number
+
+    # Geospatial indexing!
     Camera.index loc: '2d'

-    # Create the mongoose model
+    # Create the mongoose model.
     mongoose.model 'Camera', Camera
     CameraModel = mongoose.model 'Camera'

-    # Connect to mongodb
+    # Connect to mongodb.
     mongoose.connect 'mongodb://localhost/cams'

-    # Remove all previously saved documents
+    # Remove all previously saved documents.
     aCamera = new CameraModel()
     aCamera.collection.drop()

-    # Parse each Placemark into a mongoose model and save each
+    # Parse each Placemark into a mongoose model and save each.
     _(result.Document.Folder.Placemark).each (placemark, i) ->
         # Watch for the last camera since Model.save() is asynchronous and the
         # database connection should be closed after the last successful save.
-        if i + 1 is result.Document.Folder.Placemark.length
-            isLast = true
+        if i + 1 is result.Document.Folder.Placemark.length then isLast = true
         else isLast = false

+        # Parse the camera URL out of the CDATA description soup.
+        # TODO: Add some error handling around this, it's pretty brittle.
         url = URL.parse placemark.description.split('"')[3]
+
+        # Coordinates are specified (lat, lng, alt) in the KML.  We only care
+        # about two-dimenional coordinates.
         coords = placemark.Point.coordinates.split ','

+        # Create a new camera!
         aCamera = new CameraModel
             name: placemark.name
             url: url.href
@@ -61,6 +73,3 @@
                 CameraModel.count {}, (err, count) ->
                     console.log "Saved #{count} cameras successfully."
                     mongoose.disconnect()
-
-fs.readFile 'data/raw/trafficcameraswithfeed.kml', (err, data) ->
-    if err then console.log err else parser.parseString data


Inline extension check to prevent library require

--- lib/archive-editor.coffee
+++ lib/archive-editor.coffee
@@ -1,9 +1,17 @@
 path = require 'path'

-archive = require 'ls-archive'
 {File} = require 'pathwatcher'
 fs = require 'fs-plus'
 Serializable = require 'serializable'
+
+isPathSupported = (filePath) ->
+  switch path.extname(filePath)
+    when '.epub', '.jar', '.love', '.tar', '.tgz', '.war', '.zip'
+      return true
+    when '.gz'
+      return path.extname(path.basename(filePath, '.gz')) is '.tar'
+    else
+      return false

 module.exports=
 class ArchiveEditor extends Serializable
@@ -13,7 +21,7 @@
     atom.workspace.addOpener (filePath='') ->
       # Check that the file path exists before opening in case something like
       # an http: URI is being opened.
-      if archive.isPathSupported(filePath) and fs.isFileSync(filePath)
+      if isPathSupported(filePath) and fs.isFileSync(filePath)
         new ArchiveEditor(path: filePath)

   constructor: ({path}) ->


trying to avoid angular warning

trying to avoid angular warning

--- fme-guidance-image.coffee
+++ fme-guidance-image.coffee
@@ -12,5 +12,6 @@
       scope.displayModal = () ->
         angular.element('#guidance-image-modal').find('#full-size-image').attr('src', scope.large_image)
         angular.element('#guidance-image-modal').modal('show')
+        return


Update server.coffee

Update server.coffee

Retire le point-virgule

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -17,7 +17,7 @@
 						j++
 				i++
 		if(game.win)
-			alert('You win !');
+			alert 'You win !'
 	lastState = game
 	serverStats.update()
 	#console.log game


Fix port name

Fix port name

--- components/ToFullscale.coffee
+++ components/ToFullscale.coffee
@@ -48,7 +48,7 @@

   noflo.helpers.WirePattern c,
     in: 'url'
-    out: 'out'
+    out: 'url'
     async: true
     forwardGroups: true
   , (url, groups, out, callback) ->


Add type information test cases

Add type information test cases

--- spec/InPort.coffee
+++ spec/InPort.coffee
@@ -53,3 +53,36 @@
         chai.expect(data).toEqual received.shift()
         done() if received.length is 0
       s.send 'some-iip'
+
+  describe 'with options stored in port', ->
+    it 'should store all provided options in port, whether we expect it or not', ->
+      options =
+        datatype: 'string'
+        type: 'http://schema.org/Person'
+        description: 'Person'
+        required: true
+        weNeverExpectThis: 'butWeStoreItAnyway'
+      p = new inport options
+      for name, option of options
+        chai.expect(p.options[name]).toEqual option
+
+  describe 'with data type information', ->
+    right = 'all string number int object array'.split ' '
+    wrong = 'not valie data types'.split ' '
+    f = (datatype) ->
+      new inport
+        datatype: datatype
+    right.forEach (r) ->
+      it "should accept a '#{r}' data type", =>
+        chai.expect(-> f r).to.not.throw()
+    wrong.forEach (w) ->
+      it "should NOT accept a '#{w}' data type", =>
+        chai.expect(-> f w).to.throw()
+
+  describe 'with TYPE (i.e. ontology) information', ->
+    f = (type) ->
+      new inport
+        type: type
+    it 'should be a URL', ->
+      chai.expect(-> f 'http://schema.org/Person').to.not.throw()
+      chai.expect(-> f 'not-a-url').to.throw()


Fixed test for Firefox

Fixed test for Firefox

--- spec/engine.coffee
+++ spec/engine.coffee
@@ -209,17 +209,17 @@
         it 'before solving', ->
           text1 = container.getElementsByTagName('h1')[0]
           text2 = container.getElementsByTagName('h1')[1]
-          assert text1.style['line-height'] is "12px"
-          assert text2.style['line-height'] is "12px"
-          assert text1.style['font-size'] is "12px"
-          assert text2.style['font-size'] is "12px"
+          assert text1.style['lineHeight'] is "12px"
+          assert text2.style['lineHeight'] is "12px"
+          assert text1.style['fontSize'] is "12px"
+          assert text2.style['fontSize'] is "12px"
         it 'after solving', (done) ->
           onSolved = (e) ->
             values = e.detail.values
-            assert text1.style['line-height'] is "42px"
-            assert text2.style['line-height'] is "42px"
-            assert text1.style['font-size'] is "42px"
-            assert text2.style['font-size'] is "42px"
+            assert text1.style['lineHeight'] is "42px"
+            assert text2.style['lineHeight'] is "42px"
+            assert text1.style['fontSize'] is "42px"
+            assert text2.style['fontSize'] is "42px"
             container.removeEventListener 'solved', onSolved
             done()
           container.addEventListener 'solved', onSolved


Refactor database._selectOneRow

Refactor database._selectOneRow

--- src/db/database.coffee
+++ src/db/database.coffee
@@ -29,11 +29,18 @@
         opt = {
             rowShape: rowShape
             onAllRows: (rows) ->
+                if rows.length == 0
+                    if !allowEmpty
+                        e  = "Expected query #{query} to return 1 row, " +
+                             "but it returned 0 rows."
+                        return cb(e)
+                    else
+                        return cb(null, null)
+
                 if (rows.length != 1)
                     e = "Expected query #{query} to return 1 row, " +
                         "but it returned #{rows.length} rows."
-                    cb(e) if rows.length > 1 || !allowEmpty
-                    cb(null, null)
+                    return cb(e)

                 v = (if rowShape == 'array' then rows[0][0] else rows[0])
                 cb(null, v)


Rename timer counter

Rename timer counter

--- src/6.timer.coffee
+++ src/6.timer.coffee
@@ -7,7 +7,7 @@
 class Date.Timer
     @_endOfTime: new Date("9999-12-30T00:00:00Z")
     constructor: (@targetTime = Date.Timer._endOfTime) ->
-        @_counterValue = 0
+        @_counter = 0
         @_internalTimer = null
         @_running = false
         @allowsEqual = true
@@ -15,17 +15,17 @@
         @onArrive = eventField()
     run: ->
         if @_running then return
-        @_counterValue = 0
+        @_counter = 0
         @_internalTimer = setInterval(=>
             nowTime = new Date()
             if (if @allowsEqual then nowTime >= @targetTime else nowTime > @targetTime)
-                @_counterValue++
+                @_counter++
                 lastTargetTime = @targetTime
                 @targetTime = Date.Timer._endOfTime
                 @onArrive.fire(
                     idealTime: lastTargetTime
                     nowTime: nowTime
-                    index: @_counterValue - 1
+                    index: @_counter - 1
                 )
         , @precision)
         @_running = true
@@ -36,8 +36,8 @@
         @_running = false
         @
     getRunning: -> @_running
-    resetCounter: -> @_counterValue = 0
-    getCounterValue: -> @_counterValue
+    resetCounter: -> @_counter = 0
+    getCounter: -> @_counter
 class Date.IntervalTimer extends Date.Timer
     constructor: (@interval = 1000, @startTime = new Date(), @endTime) ->
         super()


Add comment

Add comment

--- src/6.timer.coffee
+++ src/6.timer.coffee
@@ -114,6 +114,7 @@
                 catch
                     Date.Observer._error
             if newValue == undefined then newValue = Date.Observer._error
+            # Must use `Object.is`, otherwise if NaN then the events will be fired endlessly.
             if @_oldValue == undefined or not Object.is(newValue, @_oldValue)
                 @onUpdate.fire(value: newValue)
                 if @_oldValue != undefined


generateConfig()

generateConfig()

--- natal.coffee
+++ natal.coffee
@@ -102,6 +102,16 @@
              and is needed by the React Native packager.
              "
     cb()
+
+
+generateConfig = (name) ->
+  log 'Creating Natal config'
+  config =
+    name:   name
+    device: getUuidForDevice 'iPhone 6s'
+
+  writeConfig config
+  config

 writeConfig = (config) ->
@@ -299,14 +309,8 @@
         ]
       ]

-    log 'Creating Natal config'
     process.chdir '../..'
-    config =
-      name:   projName
-      device: pluckUuid getDeviceList().find (line) -> /iPhone 6/.test line
-
-    writeConfig config
-    launch config
+    launch generateConfig projName

     log ''
     log 'To get started with your new app, first cd into its directory:', 'yellow'


ActivitySidebar: Disabled legacy machine/workspace related code blocks.

--- client/app/lib/activity/sidebar/activitysidebar.coffee
+++ client/app/lib/activity/sidebar/activitysidebar.coffee
@@ -95,11 +95,10 @@
       # .on 'ChannelUpdateHappened',     @bound 'channelUpdateHappened'

     computeController
-      .on 'MachineDataModified',       @bound 'updateMachineTree'
+      .on 'MachineDataModified',       @bound 'updateMachines'
       .on 'RenderMachines',            @bound 'updateMachines'
       .on 'MachineBeingDestroyed',     @bound 'invalidateWorkspaces'

-    # @on 'MoreWorkspaceModalRequested', @bound 'handleMoreWorkspacesClick'
     @on 'ReloadMessagesRequested',     @bound 'handleReloadMessages'

     environmentDataProvider.revive()
@@ -116,9 +115,8 @@

     if isChannelCollaborative channel
       if channelMessage.payload['system-message'] in ['start', 'stop']
-        kd.warn 'acet, handle here... [messageAddedToChannel]'
-        # @fetchMachines => @fetchWorkspaces =>
-        #   @setWorkspaceUnreadCount channel, unreadCount
+        @fetchMachines =>
+          @setWorkspaceUnreadCount channel, unreadCount

     switch update.channel.typeConstant
       when 'pinnedactivity' then @replyAdded update
@@ -256,10 +254,9 @@
       index = 0  if isPrivateMessage

       if isChannelCollaborative channel
-        # @fetchMachines => @fetchWorkspaces =>
-        #   @setWorkspaceUnreadCount channel, unreadCount
         @fetchMachines (data) =>
           @sharedMachinesList.updateList data.shared.concat data.collaboration
+          @setWorkspaceUnreadCount channel, unreadCount
       else
         item = @addItem channel, index
         @setUnreadCount item, channel, unreadCount
@@ -275,6 +272,7 @@

     @sharedMachinesList.removeWorkspaceByChannelId id

+    # acet - refactor required.
     # if @workspaceItemChannelMap[id]
     #   @fetchMachines => @fetchWorkspaces()

@@ -459,8 +457,7 @@
     @addFollowedTopics()
     @addConversations()

-    kd.getSingleton 'computeController'
-      .ready @lazyBound 'fetchWorkspaces', @lazyBound 'addMessages'
+    kd.getSingleton('computeController').ready @lazyBound 'addMessages'

   initiateFakeCounter: ->
@@ -478,75 +475,75 @@

   # workspacesFetched  = no
-  fetchingWorkspaces = no
-
-  fetchWorkspaces: do (callbackQueue = []) -> (callback = kd.noop) ->
-
-    activitySidebar = this
-
-    # return callback null, globals.userWorkspaces  if workspacesFetched
-    return callbackQueue.push callback       if fetchingWorkspaces
-
-    fetchingWorkspaces = yes
-
-    # put first callback to queue as well.
-    callbackQueue.push callback
-
-    remote.api.JWorkspace.fetchByMachines()
-
-      .then (workspaces) =>
-        fetchingWorkspaces = no
-
-        {socialapi} = kd.singletons
-
-        otherMachineUIds = []
-        myMachineUIds    = []
-
-        globals.userMachines.forEach (m) ->
-          if m.isMine() or m.isPermanent()
-          then myMachineUIds.push m.uid
-          else otherMachineUIds.push m.uid
-
-        otherWorkspaces  = workspaces.filter (ws) -> return ws.channelId and ws.machineUId in otherMachineUIds
-        myWorkspaces     = workspaces.filter (ws) -> return ws.machineUId in myMachineUIds
-
-        myChannels = []
-        queue      = []
-        otherWorkspaces.forEach (ws) ->
-          queue.push ->
-            socialapi.channel.byId id : ws.channelId, (err, channel) ->
-              myChannels.push channel.id  if channel
-              queue.fin()
-
-        sinkrow.dash queue, =>
-          workspacesIHaveAccess = otherWorkspaces.filter (ws) -> ws.channelId in myChannels
-          userWorkspaces        = myWorkspaces.concat workspacesIHaveAccess
-
-          globals.userMachines.forEach (machine) =>
-            return  unless machine.isMine()
-
-            for workspace in userWorkspaces \
-              when workspace and workspace.slug is 'my-workspace' \
-              and workspace.machineUId is machine.uid
-                return
-
-            userWorkspaces.push @getDummyWorkspace machine
-
-          globals.userWorkspaces  = userWorkspaces
-          # workspacesFetched     = yes
-          activitySidebar.updateMachineTree()
-
-          callbackQueue.forEach (fn) -> fn null, userWorkspaces
-          callbackQueue = []
-
-      .error (rest...) ->
-        fetchingWorkspaces = no
-        callbackQueue.forEach (fn) -> fn rest...
-        callbackQueue = []
-
-
-
-  listMachines: (machines) ->
+  # fetchingWorkspaces = no
+
+  # fetchWorkspaces: do (callbackQueue = []) -> (callback = kd.noop) ->
+
+  #   activitySidebar = this
+
+  #   # return callback null, globals.userWorkspaces  if workspacesFetched
+  #   return callbackQueue.push callback       if fetchingWorkspaces
+
+  #   fetchingWorkspaces = yes
+
+  #   # put first callback to queue as well.
+  #   callbackQueue.push callback
+
+  #   remote.api.JWorkspace.fetchByMachines()
+
+  #     .then (workspaces) =>
+  #       fetchingWorkspaces = no
+
+  #       {socialapi} = kd.singletons
+
+  #       otherMachineUIds = []
+  #       myMachineUIds    = []
+
+  #       globals.userMachines.forEach (m) ->
+  #         if m.isMine() or m.isPermanent()
+  #         then myMachineUIds.push m.uid
+  #         else otherMachineUIds.push m.uid
+
+  #       otherWorkspaces  = workspaces.filter (ws) -> return ws.channelId and ws.machineUId in otherMachineUIds
+  #       myWorkspaces     = workspaces.filter (ws) -> return ws.machineUId in myMachineUIds
+
+  #       myChannels = []
+  #       queue      = []
+  #       otherWorkspaces.forEach (ws) ->
+  #         queue.push ->
+  #           socialapi.channel.byId id : ws.channelId, (err, channel) ->
+  #             myChannels.push channel.id  if channel
+  #             queue.fin()
+
+  #       sinkrow.dash queue, =>
+  #         workspacesIHaveAccess = otherWorkspaces.filter (ws) -> ws.channelId in myChannels
+  #         userWorkspaces        = myWorkspaces.concat workspacesIHaveAccess
+
+  #         globals.userMachines.forEach (machine) =>
+  #           return  unless machine.isMine()
+
+  #           for workspace in userWorkspaces \
+  #             when workspace and workspace.slug is 'my-workspace' \
+  #             and workspace.machineUId is machine.uid
+  #               return
+
+  #           userWorkspaces.push @getDummyWorkspace machine
+
+  #         globals.userWorkspaces  = userWorkspaces
+  #         # workspacesFetched     = yes
+  #         activitySidebar.updateMachineTree()
+
+  #         callbackQueue.forEach (fn) -> fn null, userWorkspaces
+  #         callbackQueue = []
+
+  #     .error (rest...) ->
+  #       fetchingWorkspaces = no
+  #       callbackQueue.forEach (fn) -> fn rest...
+  #       callbackQueue = []
+
+
+
+  # listMachines: (machines) ->

     # treeData = []
     # nickname = KD.nick()
@@ -611,7 +608,7 @@
     # @emit 'MachinesListed'

-  getDummyWorkspace: (machine) ->
+  # getDummyWorkspace: (machine) ->

     # new KD.remote.api.JWorkspace
     #   _id          : "#{machine.getId()}-my-workspace"
@@ -624,7 +621,7 @@
     #   name         : 'My Workspace'

-  sortWorkspaces: (workspaces) ->
+  # sortWorkspaces: (workspaces) ->

     # workspaces.sort (a, b) ->
     #   switch
@@ -635,7 +632,7 @@
     #     else 0

-  mapWorkspaceWithChannel: (data, node) ->
+  # mapWorkspaceWithChannel: (data, node) ->

     # return  unless data.data?.channelId?

@@ -952,7 +949,9 @@
       @sections.messages.hide()

-  handleReloadMessages: -> @fetchWorkspaces => @sections.messages.reload()
+  handleReloadMessages: ->
+
+    environmentDataProvider.fetch => @sections.messages.reload()

   machinesListed = no
@@ -1064,7 +1063,7 @@
   #         @emit 'WorkspaceCreated', workspace

-  updateMachineTree: ->
+  # updateMachineTree: ->

   updateMachines: (callback = noop) ->
@@ -1088,10 +1087,10 @@

       return kd.warn err  if err?

-      globals.userWorkspaces =
-        ws for ws in globals.userWorkspaces when ws.machineUId isnt machine.uid
-
-      @updateMachineTree()
+      # globals.userWorkspaces =
+      #   ws for ws in globals.userWorkspaces when ws.machineUId isnt machine.uid
+
+      # @updateMachineTree()

   removeMachineNode: (machine) ->
@@ -1114,11 +1113,14 @@

   newWorkspaceCreated: (workspace) ->

-    matches = globals.userWorkspaces.filter (w) ->
-      w.machineUId is workspace.machineUId and \
-      w.slug is workspace.slug
-
-    return  if matches.length
-
-    globals.userWorkspaces.push workspace
-    @updateMachineTree()
+    @updateMachines()
+
+
+    # matches = globals.userWorkspaces.filter (w) ->
+    #   w.machineUId is workspace.machineUId and \
+    #   w.slug is workspace.slug
+
+    # return  if matches.length
+
+    # globals.userWorkspaces.push workspace
+    # @updateMachineTree()


Check the dir exists before try to compile, if not exists just refresh the apps.

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -154,11 +154,6 @@
     appPath = getAppPath manifest
     indexJsPath = "#{appPath}/index.js"
     @kiteController.run "cat #{escapeFilePath indexJsPath}", (err, response)=>
-      if err
-        new KDNotificationView
-          title    : "App list is out-dated, refreshing apps..."
-          duration : 2000
-        @refreshApps noop
       callback err, response

   # #
@@ -471,7 +466,14 @@
     unless @constructor.manifests[name]
       @fetchApps (err, apps)=> kallback apps[name]
     else
-      kallback @constructor.manifests[name]
+      @kiteController.run "stat #{getAppPath @constructor.manifests[name]}", (err)=>
+        if err
+          new KDNotificationView
+            title    : "App list is out-dated, refreshing apps..."
+            duration : 2000
+          @refreshApps noop
+        else
+          kallback @constructor.manifests[name]

   installApp:(app, version='latest', callback)->


Apps: Added screenshots to raw manifest file.

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -757,6 +757,7 @@
         type        : "tab"
       icns          :
         "128"       : "./resources/icon.128.png"
+      screenshots   : []
       menu          : []
       fileTypes     : []


Moved comment

Moved comment

--- core/app/backbone/contrib/default_click_handler.coffee
+++ core/app/backbone/contrib/default_click_handler.coffee
@@ -6,10 +6,9 @@

   # Return if a modifier key is pressed or when Backbone has not properly been initialized
   # Make sure we return "true" so other functions can determine what happened
-  # Note that the capitalization in Backbone.[H]istory is intentional
   if e.metaKey or e.ctrlKey or e.altKey
     window.open routeTo, "_blank"
-  else if not Backbone.History.started
+  else if not Backbone.History.started # Capitalization in Backbone.[H]istory is intentional
     window.open routeTo, FactlinkApp.linkTarget
   else
     Backbone.View::navigateTo routeTo


WidgetController: Pass widget's view id to evaled script instead of appending a new view into that.

--- client/Main/widgetcontroller.coffee
+++ client/Main/widgetcontroller.coffee
@@ -56,7 +56,8 @@
           warn "#{key} widget failed to load"

   evalJS: (view, js) ->
-    view.addSubView eval Encoder.htmlDecode js
+    jsCode = "viewId = '#{view.getId()}'; #{js}"
+    eval Encoder.htmlDecode jsCode

   appendCSS: (css, key) ->
     domId         = "#{key}WidgetStyle"


terminal: "WebTerm.Terminal#paste" event handler

--- client/Terminal/src/Terminal.coffee
+++ client/Terminal/src/Terminal.coffee
@@ -26,11 +26,12 @@
       tagName   : 'input'
       attributes: type: 'text'
       cssClass  : 'offscreen'
-      bind      : 'keydown keyup keypress'
+      bind      : 'keydown keyup keypress paste'
       keydown   : @bound 'keyDown'
       keypress  : @bound 'keyPress'
       keyup     : @bound 'keyUp'
     @keyInput.appendToDomBody()
+    @keyInput.on "paste", @bound "paste"

     containerView.on 'KDObjectWillBeDestroyed', @keyInput.bound 'destroy'

@@ -78,6 +79,8 @@
         @setKeyFocus() unless event.ctrlKey or event.metaKey
         @utils.defer =>
           @mousedownHappened = false
+
+    @outputbox.on "paste", @bound "paste"

     @outputbox.on "drop", (event) =>
       @server.input event.originalEvent.dataTransfer.getData "text/plain"
@@ -276,3 +279,8 @@
       hex = "0" + hex if hex.length is 1
       '\\x' + hex
     '"' + escaped.replace('"', '\\"') + '"'
+
+  paste: (event) =>
+    KD.utils.stopDOMEvent event
+    @server.input event.originalEvent.clipboardData.getData "text/plain"
+    @setKeyFocus()


PR #2590 - Fixes #2589 Detail mode in ticket article view is incorrectly triggered when single-click de-selecting a text selection

--- app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee
@@ -348,6 +348,11 @@
     # by adding a delay to the toggle
     delay = 300

+    article = $(e.target).closest('.ticket-article-item')
+    if @elementContainsSelection(article.get(0))
+      @stopPropagation(e)
+      return false
+
     if @lastClick and +new Date - @lastClick < delay
       clearTimeout(@toggleMetaTimeout)
     else


Ensure getChildren() always returns the same collection

--- scripts/models/content/inherits/container.coffee
+++ scripts/models/content/inherits/container.coffee
@@ -127,7 +127,12 @@
       # Look up each entry in Contents
       contentsModels = _.map contents, (item) =>
         @_ALL_CONTENT_HACK.get({id: item.id})
-      json.contents = new Container(contentsModels)
+
+      if @getChildren()
+        @getChildren().reset(contentsModels)
+        delete json.contents
+      else
+        json.contents = new Container(contentsModels)

       return json


Display history in reverse order

Display history in reverse order

--- src/historystep.coffee
+++ src/historystep.coffee
@@ -14,7 +14,7 @@
         object = prevElement
       else
         object = $('<li>')
-        $('#history').append(object)
+        $('#history').prepend(object)
         prevElementCount = 0
       prevElementCount += 1
       object.text "#{prevElementCount} x #{type} "


kite.who fix

kite.who fix

--- node_modules_koding/kite-amqp/lib/kite-amqp/index.coffee
+++ node_modules_koding/kite-amqp/lib/kite-amqp/index.coffee
@@ -16,19 +16,17 @@
         cb false, @getChannelNameForKite()

       'kite.join': (opts, cb)->
-        console.log "========>>>>>>"
-        console.log "opts", opts
-
         channelName = opts.args[0]
-        kiteName = opts.args[1]
+        kiteName    = opts.args[1]
         if not @kiteList[channelName]
           @kiteList[channelName] = []
         if kiteName not in @kiteList[channelName]
           @kiteList[channelName].push kiteName
         console.log "clients", @kiteList
-        cb false, true
-
-      'kite.consume': ()->
+        if cb
+          cb false, true
+
+      'kite.consume': (opts, cb)->
         # TODO:
         @mergenData.lpop @getChannelNameForGenericName(), (err, val)->
           console.log "!!!!!!!!!!!!! lpop lpop >>>>>>", val
@@ -45,14 +43,13 @@
     "#{@publicKey}-kite-all"

   getGroupName:(groupName)->
-    "#{@publicKey}-kite-kite-#{@publicKey}-Deployer"
+    "#{@publicKey}-kite-kite-#{@publicKey}-#{groupName}"

   receivedCommand: (from, cmdid, cmd, args)->
-    #console.log "received command", from, cmd, args
-
     if cmd.indexOf('reply-') > -1
       cmdid = cmd.split('reply-')[1]
-      @waitingReplies[cmdid]?.call(@, {args:args, from:from})
+      console.log ">>>> reply cmd", cmd, args
+      @waitingReplies[cmdid]?.call @, false, {args:args, from:from}
       return

     fn = @commands[cmd]
@@ -111,11 +108,6 @@
     @mergen.subscribe @getChannelNameForKite()

     @mergen.on "message", (channel, message)=>
-
-      # console.log "received =================="
-      # console.log message
-      # console.log "// received ==============="
-
       message = JSON.parse message
       @receivedCommand message.from, message.id, message.cmd, message.args

@@ -138,8 +130,11 @@

     @mergenData.hset "#{@publicKey}-kites", "#{@serviceGenericName}-#{@serviceUniqueName}", data

-    @sendCommand @getChannelNameForGenericName(), "kite.who", [], (reply)->
-      @commands['kite.join'].call(@, @getChannelNameForGenericName(), reply)
+    @sendCommand @getChannelNameForGenericName(), "kite.who", [], (err, reply)->
+      console.log "=========11111111================================="
+      console.log err, reply
+      console.log "=========// 11111111================================="
+      @commands['kite.join'].call(@, {args:[@getChannelNameForGenericName(),reply.from], from:reply.from})

     @sendCommand @getChannelNameForGenericName(), "kite.join", [@getChannelNameForGenericName(), @getChannelNameForKite()]


Fixes #1188, scope for self-referencing functions.

--- nodes.coffee
+++ nodes.coffee
@@ -907,18 +907,18 @@
       return @compileSplice       o if @variable.isSplice()
       return @compileConditional  o if @context in ['||=', '&&=', '?=']
     name = @variable.compile o, LEVEL_LIST
-    if @value instanceof Code and match = @METHOD_DEF.exec name
-      @value.name  = match[2]
-      @value.klass = match[1] if match[1]
-    val = @value.compile o, LEVEL_LIST
-    return "#{name}: #{val}" if @context is 'object'
-    unless @variable.isAssignable()
+    unless @context or @variable.isAssignable()
       throw SyntaxError "\"#{ @variable.compile o }\" cannot be assigned."
     unless @context or isValue and (@variable.namespaced or @variable.hasProperties())
       if @param
         o.scope.add name, 'var'
       else
         o.scope.find name
+    if @value instanceof Code and match = @METHOD_DEF.exec name
+      @value.name  = match[2]
+      @value.klass = match[1] if match[1]
+    val = @value.compile o, LEVEL_LIST
+    return "#{name}: #{val}" if @context is 'object'
     val = name + " #{ @context or '=' } " + val
     if o.level <= LEVEL_LIST then val else "(#{val})"


fixes #1911: remove spacing around initial skipped comments/JS

--- nodes.coffee
+++ nodes.coffee
@@ -261,8 +261,9 @@
       break unless exp instanceof Comment or exp instanceof Literal
     o = merge(o, level: LEVEL_TOP)
     if i
-      rest = @expressions.splice i, @expressions.length
-      code = @compileNode(o)
+      rest = @expressions.splice i, 9e9
+      [spaced, @spaced] = [@spaced, no]
+      [code  , @spaced] = [(@compileNode o), spaced]
       @expressions = rest
     post = @compileNode o
     {scope} = o


bugfix for confirmWeek > sendDeclined if shift wasn't found

--- server/methods/mail/sendMail/sendDeclined.coffee
+++ server/methods/mail/sendMail/sendDeclined.coffee
@@ -1,36 +1,39 @@
 Meteor.methods
+
 	sendDeclined: (shiftId, teamId, userId) ->
 		shift = Shifts.findOne shiftId
-		project = Projects.findOne shift.projectId, fields: name: 1, email: 1
-		user = Meteor.users.findOne userId, fields: profile: 1

-		check { userId: Meteor.userId(), projectId: shift.projectId }, isShiftAdmin
+		if shift?
+			project = Projects.findOne shift.projectId, fields: name: 1, email: 1
+			user = Meteor.users.findOne userId, fields: profile: 1

-		if user? and shift?
-			thisMoment = moment(shift.date, 'YYYYDDDD')
-			thisMoment.locale(user.profile.language)
-			date = thisMoment.format('dddd, DD.MM.YYYY')
-			time = moment(shift.start, 'Hmm').format('HH:mm') + ' - ' + moment(shift.end, 'Hmm').format('HH:mm')
-			name = user.profile.firstname + ' ' + user.profile.lastname
+			check { userId: Meteor.userId(), projectId: shift.projectId }, isShiftAdmin

-		Meteor.call 'sendMail',
-			recipient: user.profile.email
-			sender: project.name
-			from: project.email
-			subject: TAPi18n.__('mail.declined.subject', '', user.profile.language)
-			template: 'declined'
-			language: user.profile.language
-			data:
-				project: project.name
-				name: name
-				datetime: TAPi18n.__('mail.declined.datetime', {date: date, time: time}, user.profile.language)
-				content: getMailTexts 'declined', user.profile.language
-		, (err, res) ->
-			if err
-				console.log 'sendMail failed: ' + err
-			else
-				teamNr = shift.teams.map((e) -> e._id).indexOf(teamId)
-				declinedNr = shift.teams[teamNr].declined.map((e) -> e._id).indexOf(userId)
-				set = {}
-				set['teams.' + teamNr + '.declined.' + declinedNr + '.informed'] = true
-				Shifts.update shift._id, $set: set
+			if user?
+				thisMoment = moment(shift.date, 'YYYYDDDD')
+				thisMoment.locale(user.profile.language)
+				date = thisMoment.format('dddd, DD.MM.YYYY')
+				time = moment(shift.start, 'Hmm').format('HH:mm') + ' - ' + moment(shift.end, 'Hmm').format('HH:mm')
+				name = user.profile.firstname + ' ' + user.profile.lastname
+
+			Meteor.call 'sendMail',
+				recipient: user.profile.email
+				sender: project.name
+				from: project.email
+				subject: TAPi18n.__('mail.declined.subject', '', user.profile.language)
+				template: 'declined'
+				language: user.profile.language
+				data:
+					project: project.name
+					name: name
+					datetime: TAPi18n.__('mail.declined.datetime', {date: date, time: time}, user.profile.language)
+					content: getMailTexts 'declined', user.profile.language
+			, (err, res) ->
+				if err
+					console.log 'sendMail failed: ' + err
+				else
+					teamNr = shift.teams.map((e) -> e._id).indexOf(teamId)
+					declinedNr = shift.teams[teamNr].declined.map((e) -> e._id).indexOf(userId)
+					set = {}
+					set['teams.' + teamNr + '.declined.' + declinedNr + '.informed'] = true
+					Shifts.update shift._id, $set: set