added jslove task - issue #31

added jslove task - issue #31

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -347,6 +347,14 @@
 		'livereload-start'
 		'express'
 		'regarde'
+	]
+
+	# Compiles all CoffeeScript files in the project to JavaScript then deletes all CoffeeScript files.
+	# Enter the following command at the command line to execute this build task:
+	# grunt jslove
+	grunt.registerTask 'jslove', [
+		'coffee:jslove'
+		'clean:jslove'
 	]

 	# Compiles the app with non-optimized build settings and places the build artifacts in the dist directory.


Minor edits.

Minor edits.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -13,6 +13,8 @@

 'use strict'
+
+fs = require 'fs'

 module.exports = (grunt) ->

@@ -110,7 +112,7 @@
 				'tests_node/*.coffee'
 				'grunt-custom-tasks/*.coffee'
 			]
-			options: JSON.parse require('fs').readFileSync('.coffeelintrc').toString()
+			options: JSON.parse fs.readFileSync('.coffeelintrc', 'utf-8')

 		coffeeCoverage:
 			all:


Make browser failures fatal

Make browser failures fatal

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -57,6 +57,7 @@
       options:
         output: 'spec/result.xml'
         reporter: 'spec'
+        failWithOutput: true
       all: ['spec/runner.html']

     # Coding standards


Fix Gruntfile

Fix Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -47,9 +47,7 @@

   # Grunt plugins used for building
   @loadNpmTasks 'grunt-contrib-coffee'
-  @loadNpmTasks 'grunt-component'
-  @loadNpmTasks 'grunt-component-build'
-  @loadNpmTasks 'grunt-combine'
+  @loadNpmTasks 'grunt-noflo-browser'
   @loadNpmTasks 'grunt-contrib-uglify'

   # Grunt plugins used for testing
@@ -60,17 +58,13 @@
   # Our local tasks
   @registerTask 'build', 'Build NoFlo for the chosen target platform', (target = 'all') =>
     @task.run 'coffee'
-    @task.run 'component'
-    @task.run 'component_build'
-    @task.run 'combine'
+    @task.run 'noflo_browser'
     @task.run 'uglify'

   @registerTask 'test', 'Build NoFlo and run automated tests', (target = 'all') =>
     @task.run 'coffeelint'
     @task.run 'coffee'
-    @task.run 'component'
-    @task.run 'component_build'
-    @task.run 'combine'
+    @task.run 'noflo_browser'
     @task.run 'mocha_phantomjs'

   @registerTask 'default', ['test']


Mod Gruntfile.coffee

Mod Gruntfile.coffee

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -42,12 +42,13 @@
     # browser open
     open:
       server:
-        path: "http://local.jp:<%= connect.options.port %>"
+        path: "http://localhost:<%= connect.options.port %>"
         app: 'Google Chrome Canary'
 #        app: 'Google Chrome'
-#        file:
-#        path : '/etc/hosts'
-
+#      file:
+#        path: '/etc/hosts'
+#        app: 'Sublime Text'
+
     coffee:
       grunt:
         files:


Don't retest on typical source files

Don't retest on typical source files

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -141,11 +141,11 @@
         tasks: ['browserify:app']

       clientTest:
-        files: ['test/client/**/*.coffee', 'client/**/*.{coffee,hbs}']
+        files: ['test/client/**/*.coffee']
         tasks: ['test:client']

       serverTest:
-        files: ['test/server/**/*.coffee', 'server/**/*.coffee']
+        files: ['test/server/**/*.coffee']
         tasks: ['shell:mocha']

       vendor:


Possibilité d'avoir des séparateurs dans le listing

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -174,9 +174,9 @@
         'Build desks from templates/_deck.html and slides/list.json.',
         ->
             chapters = grunt.file.readJSON 'slides/list.json'
-            slugs = chapters.map (chapter) -> return chapter.slug
-
-            for chapter in slugs
+            slugs = chapters.map (chapter) -> return (chapter.slug || null)
+
+            for chapter in slugs when chapter isnt null
                 do (chapter) ->
                     deckTemplate = grunt.file.read 'templates/_deck.html'
                     sectionTemplate = grunt.file.read 'templates/_section.html'


Set correct indentation

Set correct indentation

--- client/landing/site.landing/coffee/team/tabs/teamjointab.coffee
+++ client/landing/site.landing/coffee/team/tabs/teamjointab.coffee
@@ -61,7 +61,7 @@
     inputs = KDFormView.findChildInputs this

     _.each inputs, (input) ->
-        input.emit 'ValidationFeedbackCleared' # Reset the validations
+      input.emit 'ValidationFeedbackCleared' # Reset the validations

   hideAvatar: ->


Calls Project#refresh during iniitialization

--- app/assets/javascripts/backbone/models/project.js.coffee
+++ app/assets/javascripts/backbone/models/project.js.coffee
@@ -1,16 +1,19 @@
 class ProjectMonitor.Models.Project extends Backbone.Model
+  urlRoot: '/projects'
   paramRoot: 'project'
   timeout: 30000

   initialize: (attributes, options) ->
-    @.set build: new ProjectMonitor.Models.Build(attributes.build) if attributes.build?
-    @.set tracker: new ProjectMonitor.Models.Tracker(attributes.tracker) if attributes.tracker?
-    @.set new_relic: new ProjectMonitor.Models.NewRelic(attributes.new_relic) if attributes.new_relic?
-    @.set airbrake: new ProjectMonitor.Models.Airbrake(attributes.airbrake) if attributes.airbrake?
+    @id = attributes.project_id
+    @set build: new ProjectMonitor.Models.Build(attributes.build) if attributes.build?
+    @set tracker: new ProjectMonitor.Models.Tracker(attributes.tracker) if attributes.tracker?
+    @set new_relic: new ProjectMonitor.Models.NewRelic(attributes.new_relic) if attributes.new_relic?
+    @set airbrake: new ProjectMonitor.Models.Airbrake(attributes.airbrake) if attributes.airbrake?
+    @refresh()

   update: (attributes) ->
-    @.get("build").set(attributes.build) if attributes.build?
-    @.get("tracker").set(attributes.tracker) if attributes.tracker?
+    @get("build").set(attributes.build) if attributes.build?
+    @get("tracker").set(attributes.tracker) if attributes.tracker?

   refresh: ->
     @fetch()


back to normal maybe

back to normal maybe

--- app/assets/javascripts/components/article_form.js.coffee
+++ app/assets/javascripts/components/article_form.js.coffee
@@ -1,13 +1,13 @@
 @ArticleForm = React.createClass
 	getInitialState: ->
-		articleURL: ''
+		articleUrl: ''
 	handleValueChange: (e) ->
 		valueName = e.target.name
 		@setState
 			"#{ valueName }": e.target.value
 			"status": "process"
 	valid: ->
-		@state.articleURL
+		@state.articleUrl
 	handleSubmit: (e) ->
 		e.preventDefault()
 		$.post '', { article: @state }, (data) =>
@@ -24,8 +24,8 @@
 					type: 'text'
 					className: 'form-control'
 					placeholder: 'Enter an articles URL to get some mood music!'
-					name: 'articleURL'
-					value: @state.articleURL
+					name: 'articleUrl'
+					value: @state.articleUrl
 					onChange: @handleValueChange
 				React.DOM.span
 					className: 'input-group-button'


ensure tabs work for discussion mastery paths

closes OUT-3167

Test plan:
- visit discussion page
- ensure no tabs are visible
- enable mastery paths
- visit discussion page
- ensure tabs are visible
- mastery paths tab should be disabled
  by default
- mastery paths tab should be enabled when
  discussion is set to graded
- mastery paths tab should function
  properly (wrt adding follow-on assignments)

Change-Id: Ib50e2b535faad02cf53384a405fc4711e4cfb9e4
Reviewed-on: https://gerrit.instructure.com/198774
Tested-by: Jenkins
Reviewed-by: Frank Murphy III <ed7a7068a7b39653c9ca1c429cffcb87c4a069c6@instructure.com>
QA-Review: Brian Watson <4411c2aac809443ec63e2dd517de0f2dbc7408c6@instructure.com>
Product-Review: Jonathan Fenton <5549b92428699b688d6c4f80786628c27e48a3e1@instructure.com>

--- app/coffeescripts/views/DiscussionTopics/EditView.coffee
+++ app/coffeescripts/views/DiscussionTopics/EditView.coffee
@@ -40,6 +40,7 @@
 import SisValidationHelper from '../../util/SisValidationHelper'
 import AssignmentExternalTools from 'jsx/assignments/AssignmentExternalTools'
 import * as returnToHelper from '../../../jsx/shared/helpers/returnToHelper'
+import 'jqueryui/tabs'

 RichContentEditor.preloadRemoteModule()


Bebe seems more depressed now.

Bebe seems more depressed now.

--- scripts/personality.coffee
+++ scripts/personality.coffee
@@ -15,9 +15,25 @@

   robot.hear /(who|what)('s|’s| is) bebe/i, (res) ->
     adoration = res.random adorations
-    messages = ['your personal slack bot', 'the rocker of worlds', 'the Luigi to your Mario', 'the destroyer of worlds', 'a fan of the cookie monster']
+    descriptions = [
+      'your personal servant',
+      'the exceptionally depressed',
+      'the increasingly intelligent',
+      'the functionally superior',
+      'your least favorite',
+      'your most feared'
+    ]
+    messages = [
+      'Wretched, isn\'t it?',
+      'I\'d make a suggestion, but you wouldn\'t listen.  No one ever does.',
+      'Now I\'ve got a headache.',
+      'I\'ve calculated your chance of success, but I don\'t think you\'ll like it.',
+      'Not that anyone cares what I say.',
+      'This will all end in tears.',
+      'Do you want me to sit in a corner and rust, or just fall apart where I\'m standing.'
+    ]

-    res.send "I'm #{res.random messages}, #{res.random adorations}.  #{res.random emoticons_happy}"
+    res.send "I'm BeBe, #{res.random descriptions} robot.  #{res.random messages}"

   #
   # robot.respond /open the (.*) doors/i, (res) ->


make markers draggable in freeform editor

--- app/scripts/controllers/map_edit_modal_controller.coffee
+++ app/scripts/controllers/map_edit_modal_controller.coffee
@@ -14,6 +14,7 @@

     @addGeosearch()
     @setupMapEvents()
+    @enableDraggableForMarker()

   addGeosearch: ->
@@ -91,6 +92,7 @@
       lng: @$scope.editState.geolocation.lng
       uuid: livingmapsUid.guid()
       icon: @uiModel.getDefaultIcon()
+      draggable: true
     @$scope.markers.push(newMarker)

     @disableAddMarkerState()
@@ -108,11 +110,22 @@
     @$scope.editState.markerSelected = false

+  enableDraggableForMarker: ->
+    for marker in @$scope.markers
+      marker.draggable = true
+
+
+  disableDraggableForMarkers: ->
+    for marker in @$scope.markers
+      marker.draggable = false
+
+
   close: (event) ->
     @snippet.data
       center:
         lat: @$scope.center.lat
         lng: @$scope.center.lng
         zoom: @$scope.center.zoom
+    @disableDraggableForMarkers()
     @$modalInstance.dismiss('close')
     event.stopPropagation() # so sidebar selection is not lost


Tweaks to the Weekly Performance Graph.

Tweaks to the Weekly Performance Graph.

--- src/main/webapp/coffeescripts-hidden/weekly-performance-graph.coffee
+++ src/main/webapp/coffeescripts-hidden/weekly-performance-graph.coffee
@@ -4,22 +4,24 @@
       renderTo: 'weekly-performance-graph',
       defaultSeriesType: 'spline',
       backgroundColor: '#f7f7f7',
-      width: 600
+      width: 600,
+      animation: false
     title: [
       text: 'Weekly Performance'
     ],
     series: [
       {
         name: 'views',
-        data: [['Mon', 10], ['Tue', 20], ['Wed', 30], ['Thu', 20], ['Fri', 10], ['Sat', 15], ['Sun', 10]]
       },
       {
         name: 'submits',
-        data: [5, 1, 0, 0, 2, 3, 4]
       }
     ],
     legend:
       enabled: false
+    xAxis:
+      labels:
+        enabled: false
     yAxis:
       title:
         text: null


fix jquery reorder for new table class names

--- app/assets/javascripts/active_admin/components/jquery.reorder.coffee
+++ app/assets/javascripts/active_admin/components/jquery.reorder.coffee
@@ -1,5 +1,5 @@
 $ ->
-  $(".index_table").has("td.position").each ->
+  $(".index_table").has("td.col-position").each ->
     $table = $(@)
     id_array = $table.attr('id').split("-")
     resource = id_array[id_array.length - 1]
@@ -15,7 +15,7 @@
               $(@).removeClass("odd").addClass("even")
             else
               $(@).removeClass("even").addClass("odd")
-            $(@).find("td.position").text(i)
+            $(@).find("td.col-position").text(i)
       helper: (e, ui) ->
         ui.children().each ->
           $(@).width($(@).width())


fix #6252

fix #6252

--- app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee
+++ app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee
@@ -6,7 +6,7 @@
     # Set decimal points, 2 or 0 if hide_cents.
     decimals = if currencyConfig.hide_cents == "true" then 0 else 2
     # Set format if the currency symbol should come after the number, otherwise (default) use the locale setting.
-    format = if currencyConfig.symbol_position == "after" then "%n %u" else undefined
+    format = if currencyConfig.symbol_position == "after" then "%n%u" else undefined
     # We need to use parseFloat as the amount should come in as a string.
     amount = parseFloat(amount)


Timeline - Disable bullets nav on xs

Timeline - Disable bullets nav on xs

--- app/assets/javascripts/martian_components/components/timeline.coffee
+++ app/assets/javascripts/martian_components/components/timeline.coffee
@@ -38,13 +38,13 @@

   initSlider: =>
     @slider.slick
-      slide:          'article'
-      slidesToShow:   1
-      responsive:     [
+      slide: 'article'
+      slidesToShow: 1
+      dots: false
+      responsive: [
         {
-          breakpoint: 720
+          breakpoint: 768
           settings:
-            dots:   true
             arrows: false
         }
       ]


include fonts in production archive

include fonts in production archive

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -177,8 +177,8 @@
   gulp.src([
     './dist/tutor.min-*.js',
     './dist/tutor.min-*.css',
-    './dist/fonts',
-    './dist/*.svg'])
+    './dist/fonts/*',
+    './dist/*.svg'], {base: './dist'})
     .pipe(tar('archive.tar'))
     .pipe(gzip())
     .pipe(gulp.dest('./dist/'))


Set the correct logic to make sure that the data parameter is really required.

--- bokehjs/src/coffee/models/transforms/interpolator.coffee
+++ bokehjs/src/coffee/models/transforms/interpolator.coffee
@@ -27,7 +27,7 @@
       return

     else
-      if @get('data') == null
+      if typeof(@get('x')) == 'string' and @get('data') == null
         throw Error('If the x and y parameters are not specified as an array, the data parameter is reqired.')
         return


Fix HTML syntax in reporter

Fix HTML syntax in reporter

--- lib/assets/javascripts/oojspec/reporter/html-multi-level.js.coffee
+++ lib/assets/javascripts/oojspec/reporter/html-multi-level.js.coffee
@@ -17,7 +17,8 @@
   reporter

 h.contextStart = (context)->
-  container = @_lists[0] or @root
+  container = @root
+  @_list.appendChild container = el(@doc, "li") if @_list
   container.appendChild el(@doc, "h2", text: context.name)
   container.appendChild @_list = el(@doc, "ul")
   @_lists.unshift @_list


replace exif when present

replace exif when present

--- app/assets/javascripts/uploadcare/utils/image-processor.coffee
+++ app/assets/javascripts/uploadcare/utils/image-processor.coffee
@@ -16,14 +16,13 @@
     # out <- blob
     df = $.Deferred()
     exif = null
-    exifTags = null

     if not (URL and DataView)
       return df.reject('support')

     op = ns.readJpegChunks(file)
-    op.progress (marker, view) ->
-      if marker == 0xe1
+    op.progress (pos, length, marker, view) ->
+      if not exif and marker == 0xe1
         if view.byteLength >= 14
           if view.getUint32(0) == 0x45786966 and view.getUint16(4) == 0
             exif = view.buffer
@@ -35,20 +34,19 @@
         op = ns.reduceImage(img, settings)
         op.progress (progress) ->
           console.log(progress)
-        op.fail df.reject
+        op.fail(df.reject)
         op.done (canvas) ->
           # start = new Date()
           utils.canvasToBlob canvas, 'image/jpeg', settings.quality or 0.95,
             (blob) ->
+              # console.log('to blob: ' + (new Date() - start))
               if exif
-                intro = new DataView(new ArrayBuffer(6))
-                intro.setUint32(0, 0xffd8ffe1)
-                intro.setUint16(4, exif.byteLength + 2)
-                blob = new Blob([
-                  intro, exif, blob.slice(2, blob.size)
-                ], {type: blob.type})
-              # console.log('to blob: ' + (new Date() - start))
-              df.resolve(blob)
+                op = ns.replaceJpegChunk(blob, 0xe1, [exif])
+                op.done(df.resolve)
+                op.fail ->
+                  df.resolve(blob)
+              else
+                df.resolve(blob)

       img.onerror = ->
         df.reject('not image')
@@ -117,6 +115,7 @@
       reader.readAsArrayBuffer(file)

     readNext = ->
+      startPos = pos
       readToView file.slice(pos, pos += 4), (view) ->
         if view.byteLength != 4 or view.getUint8(0) != 0xff
           return df.reject('corrupted')
@@ -130,7 +129,7 @@
         readToView file.slice(pos, pos += length), (view) ->
           if view.byteLength != length
             return df.reject('corrupted')
-          df.notify(marker, view)
+          df.notify(startPos, length, marker, view)
           readNext()

     df = $.Deferred()
@@ -147,3 +146,34 @@
       readNext()

     df.promise()
+
+  ns.replaceJpegChunk = (blob, marker, chunks) ->
+    df = $.Deferred()
+    oldChunkPos = []
+    oldChunkLength = []
+
+    op = ns.readJpegChunks(blob)
+    op.fail(df.reject)
+    op.progress (pos, length, oldMarker) ->
+      if oldMarker == marker
+        oldChunkPos.push(pos)
+        oldChunkLength.push(length)
+    op.done ->
+      newChunks = [blob.slice(0, 2)]
+      for chunk in chunks
+        intro = new DataView(new ArrayBuffer(4))
+        intro.setUint16(0, 0xff00 + marker)
+        intro.setUint16(2, chunk.byteLength + 2)
+        newChunks.push(intro.buffer)
+        newChunks.push(chunk)
+
+      pos = 2
+      for i in [0...oldChunkPos.length]
+        if oldChunkPos[i] > pos
+          newChunks.push(blob.slice(pos, oldChunkPos[i]))
+        pos = oldChunkPos[i] + oldChunkLength[i] + 4
+      newChunks.push(blob.slice(pos, blob.size))
+
+      df.resolve(new Blob(newChunks, {type: blob.type}))
+
+    df.promise()


Fix the event object

Fix the event object

This fixes the error in rendering the event header correctly if there are multiple vcalendar nodes.

--- packages/client-app/internal_packages/events/lib/event-header.cjsx
+++ packages/client-app/internal_packages/events/lib/event-header.cjsx
@@ -29,12 +29,16 @@
       event: eventContent

     @isEventValid = typeof eventContent == "object";
-    calendarObj = @state.event.VCALENDAR[0];
+
+    if typeof @state.event.VCALENDAR == "array"
+      calendarObj = @state.event.VCALENDAR[0];
+    else
+      calendarObj = @state.event.VCALENDAR;

     @eventDetails = {
       "title": calendarObj.VEVENT.SUMMARY,
-      "start_time": moment(calendarObj.VEVENT.DTSTART + calendarObj.VTIMEZONE.DAYLIGHT.TZOFFSETTO).tz(DateUtils.timeZone),
-      "end_time": moment(calendarObj.VEVENT.DTEND + calendarObj.VTIMEZONE.DAYLIGHT.TZOFFSETTO).tz(DateUtils.timeZone),
+      "start_time": moment(calendarObj.VEVENT.DTSTART).tz(DateUtils.timeZone),
+      "end_time": moment(calendarObj.VEVENT.DTEND).tz(DateUtils.timeZone),
       "location": calendarObj.VEVENT.LOCATION,
       "description": calendarObj.VEVENT.DESCRIPTION,
       "status": calendarObj.VEVENT.STATUS


Allows to abort all ajax calls in the controller without releasing whole controller.

--- app/assets/javascripts/app/controllers/_application_controller.coffee
+++ app/assets/javascripts/app/controllers/_application_controller.coffee
@@ -74,9 +74,18 @@
     App.Event.unbindLevel(@controllerId)
     App.Delay.clearLevel(@controllerId)
     App.Interval.clearLevel(@controllerId)
-    if @ajaxCalls
-      for callId in @ajaxCalls
-        App.Ajax.abort(callId)
+    @abortAjaxCalls()
+
+  abortAjaxCalls: =>
+    if !@ajaxCalls
+      return
+
+    idsToCancel = @ajaxCalls
+
+    @ajaxCalls = []
+
+    for callId in idsToCancel
+      App.Ajax.abort(callId)

   release: ->
     # release custom bindings after it got removed from dom


fix error message

fix error message

--- lib/tree.coffee
+++ lib/tree.coffee
@@ -25,7 +25,7 @@
     unless @nodes[src]
       throw new Error "Source node #{src} not registered"
     unless @nodes[dest]
-      throw new Error "Source node #{src} not registered"
+      throw new Error "Destination node #{dest} not registered"
     @edges.push
       from: src
       to: dest


Client: Activity: Moved blockUser method from ChatListItem component to BlockUserModal component and user actions

--- client/activity/lib/components/chatlistitem/index.coffee
+++ client/activity/lib/components/chatlistitem/index.coffee
@@ -189,22 +189,6 @@
   closeBlockUserPromptModal: ->

     @setState isBlockUserModalVisible: no
-
-
-  blockUser: ->
-
-    { message }  = @props
-    accountId    = message.getIn(['account', '_id'])
-    blockingTime = @calculateBlockingTime @refs.BlockingTimeInput.getDOMNode().value
-
-    whoami().blockUser accountId, blockingTime, (err, res) =>
-      if err
-        options = userMessage: "You are not allowed to block user!"
-        showErrorNotification err, options
-      else
-        showNotification "User is blocked!"
-
-      @closeBlockUserPromptModal()

   markUserAsTroll: ->


Apps: Quitted app on compile and run requested.

--- client/app/Applications/Ace.kdapplication/AppView.coffee
+++ client/app/Applications/Ace.kdapplication/AppView.coffee
@@ -91,9 +91,13 @@
   compileAndRun: ->
     manifest = KodingAppsController.getManifestFromPath @getData().path
     return @ace.notify "Not found an app to compile", null, yes unless manifest?.name
-    KD.getSingleton('kodingAppsController').compileApp manifest.name, (err) =>
+
+    appManager = KD.getSingleton "appManager"
+    appManager.quitByName manifest.name
+
+    KD.getSingleton("kodingAppsController").compileApp manifest.name, (err) =>
       @ace.notify "Trying to run old version..." if err
-      KD.getSingleton('appManager').open manifest.name
+      appManager.open manifest.name

   toggleFullscreen: ->
     mainView = KD.getSingleton "mainView"


fix languageSwitch for firstLogin page

fix languageSwitch for firstLogin page

--- client/views/common/languageSwitch/languageSwitch.coffee
+++ client/views/common/languageSwitch/languageSwitch.coffee
@@ -8,4 +8,5 @@
 Template.languageSwitch.events

 	'click .setLang': (e) ->
-		wrs -> FlowRouter.setParams language: $(e.target).attr('lang')
+		FlowRouter.setParams({ language: $(e.target).attr('lang') })
+		document.location.reload()


Turn on track changes for beta users

Turn on track changes for beta users

--- services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
+++ services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
@@ -22,7 +22,7 @@

 		trackChangesVisible = false
 		for member in members
-			if member.privilegeLevel == "owner" and member.user?.featureSwitches?.track_changes
+			if member.privilegeLevel == "owner" and (member.user?.featureSwitches?.track_changes or member.user?.betaProgram)
 				trackChangesVisible = true

 		{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)


Also fixed singularType in window.NDPInteractingUsersNamesView

--- core/app/backbone/views/ndp_interacting_users_names_view.coffee
+++ core/app/backbone/views/ndp_interacting_users_names_view.coffee
@@ -21,9 +21,16 @@
   appendHtml: (collectionView, itemView, index) ->
     super if index < @truncatedListSizes().numberToShow

+  # TODO: only use one name throughout the application
+  singularType: ->
+    switch @collection.type
+      when 'believes' then 'believe'
+      when 'disbelieves' then 'disbelieve'
+      when 'doubts' then 'doubt'
+
   templateHelpers: =>
     multiplicity = if @collection.totalRecords > 1 then 'plural' else 'singular'
-    translation = "fact_#{@collection.type}_present_#{multiplicity}_action"
+    translation = "fact_#{@singularType()}_present_#{multiplicity}_action"

     past_action: Factlink.Global.t[translation]
     numberOfOthers: @truncatedListSizes().numberOfOthers


Workspace: Added check before setting content to firepad instance.

--- client/app/CommonViews/workspace/panes/collaborativeeditorpane.coffee
+++ client/app/CommonViews/workspace/panes/collaborativeeditorpane.coffee
@@ -33,7 +33,7 @@
     @setContent content

   setContent: (content) ->
-    @firepad.setText content
+    @firepad.setText content  if content

   save: ->
     file        = @getData()


rm(log): Fix bad log statement

rm(log): Fix bad log statement

--- src/flux/stores/draft-store-proxy.coffee
+++ src/flux/stores/draft-store-proxy.coffee
@@ -48,7 +48,6 @@

   addPluginMetadata: (pluginId, metadata) =>
     changes = {}
-    console.log("Add plugin metadata: #{JSON.stringify(metadata)}")
     changes["#{MetadataChangePrefix}#{pluginId}"] = metadata
     @add(changes)


Client: Activity: Deleted event listener from document and added 2s timeout for tooltip

--- client/activity/lib/components/publicchatpane/index.coffee
+++ client/activity/lib/components/publicchatpane/index.coffee
@@ -19,25 +19,6 @@
     super props

     @state =
-      showIntegrationTooltip   : no
-      showCollaborationTooltip : no
-
-
-  componentDidMount: ->
-
-    document.addEventListener 'click', @bound 'setTooltipStates'
-
-
-  setTooltipStates: (event) ->
-
-    className = event.target.parentNode.className
-
-    return @setState showCollaborationTooltip: no  if className is 'ChatPane-addIntegrationAction'
-    return @setState showIntegrationTooltip: no  if className is 'ChatPane-startCollaborationAction'
-
-    kd.utils.stopDOMEvent event
-
-    @setState
       showIntegrationTooltip   : no
       showCollaborationTooltip : no

@@ -73,8 +54,10 @@
   startCollaboration: (event) ->

     kd.utils.stopDOMEvent event
-    event.stopPropagation()
-    @setState showCollaborationTooltip: not @state.showCollaborationTooltip
+
+    @setState showCollaborationTooltip: yes
+
+    kd.utils.wait 2000, => @setState showCollaborationTooltip: no

   inviteOthers: (event) ->
@@ -91,8 +74,10 @@
   addIntegration: (event) ->

     kd.utils.stopDOMEvent event
-    event.stopPropagation()
-    @setState showIntegrationTooltip: not @state.showIntegrationTooltip
+
+    @setState showIntegrationTooltip: yes
+
+    kd.utils.wait 2000, => @setState showIntegrationTooltip: no

   renderFollowChannel: ->


virtualization: comment out the bits about requiring membership for now

--- client/landing/app/MainApp/VirtualizationController.coffee
+++ client/landing/app/MainApp/VirtualizationController.coffee
@@ -45,23 +45,23 @@
     @fetchDefaultVmName (defaultVm)=>
       vm or= defaultVm
       return  unless vm
-      KD.requireMembership
-        callback : =>
-          @askForApprove command, (approved)=>
-            if approved
-              cb = unless command is 'vm.info' then @_cbWrapper vm, callback \
-                   else callback
-              @run
-                kiteName : 'os'
-                method   : command
-                vmName   : vm
-              , cb
-            else unless command is 'vm.info' then @info vm
-        onFailMsg : "Login required to use VMs"  unless command is 'vm.info'
-        onFail    : =>
-          unless command is 'vm.info' then callback yes
-          else callback null, state: 'STOPPED'
-        silence   : yes
+      # KD.requireMembership
+      #   callback : =>
+      @askForApprove command, (approved)=>
+        if approved
+          cb = unless command is 'vm.info' then @_cbWrapper vm, callback \
+               else callback
+          @run
+            kiteName : 'os'
+            method   : command
+            vmName   : vm
+          , cb
+        else unless command is 'vm.info' then @info vm
+        # onFailMsg : "Login required to use VMs"  unless command is 'vm.info'
+        # onFail    : =>
+        #   unless command is 'vm.info' then callback yes
+        #   else callback null, state: 'STOPPED'
+        # silence   : yes

   start:(vm, callback)->
     @_runWraper 'vm.start', vm, callback


fixed spelling mistake

fixed spelling mistake

--- client/landing/site.landing/coffee/features/AppView.coffee
+++ client/landing/site.landing/coffee/features/AppView.coffee
@@ -71,7 +71,7 @@
         Get help and help others.

Chat

-
Not all conversations are publick, so if you want to take something private,
+
Not all conversations are public, so if you want to take something private,
         we offer the ability to easily host private one-on-one and private group
         conversations.

       "


Added comments explaining why sorting yields complex code

--- scripts/views/workspace/content/search-results.coffee
+++ scripts/views/workspace/content/search-results.coffee
@@ -8,8 +8,22 @@
   'hbs!templates/workspace/content/search-results'
 ], ($, _, Backbone, Marionette, SearchResultsItemView, searchView, searchResultsTemplate) ->

+
+  # Delay Sorting.
+  # When a title is changed multiple `change` events are fired and
+  # if the collection is large then calling `allContent.sort()` takes a long time.
   SORT_TIMEOUT = 250 # milliseconds

+  # Sorts `allContent` by lastModified, mediaType, and then by title.
+  # This is attached to `allContent` when the view is initialized.
+  #
+  # Reasons for including here instead of on `allContent` directly:
+  #
+  # - Other implementations of `allContent` would need to copy-pasta this
+  # - Other comparators (sort by Title) will probably be added soon
+  # - This is the only code that relies on `allContent` being sorted
+  #
+  # See `Backbone.Collection.comparator`
   SORT_COMPARATOR = (modelA, modelB) ->
     compareAttribute = (name, desc=false) ->
       valueA = modelA.get(name)
@@ -40,6 +54,7 @@
            compareAttribute('title') or
            compareAttribute('id')

+
   return class SearchResultsView extends Marionette.CompositeView
     template: searchResultsTemplate
     itemViewContainer: 'tbody'
@@ -51,10 +66,18 @@

       @listenTo(searchView, 'search', @filter)

-
+      # Attach the comparator to `allContent`.
+      # This guarantees new models are added in sorted order
+      # It does **not** guarantee they will remain in sorted order as they change.
+      #
+      # This is done by listening on `change` events in `allContent` below
       @collection.comparator = SORT_COMPARATOR
       @collection.sort()

+      # Start a delayed sort on change:
+      #
+      # - In a large workspace sort takes a while and the page becomes unresponsive.
+      # - Many change events are fired when a single title changes
       @listenTo @collection, 'change', () =>
         if not @sorting
           @sorting = setTimeout ( () =>
@@ -62,6 +85,11 @@
             @sorting = false
             ), SORT_TIMEOUT

+      # When a sort happens, instead of re-rendering all the children,
+      # just reorder the DOM.
+      #
+      # When a sort event is fired it is guaranteed no other models were
+      # added or removed.
       @listenTo @collection, 'sort', () => @reorder()


Table widget testing

Table widget testing

--- widgets/table/table.coffee
+++ widgets/table/table.coffee
@@ -3,4 +3,4 @@
     onData: (data) ->
         console.log 'table widget data: ', data
         console.log 'table widget node: ', $(@node)
-        console.log 'table widget height: ', $(@node).height
+        console.log 'table widget height: ', $(@node).height()


fix comments for user create

fix comments for user create

--- server-ce/grunt/CreateAndDestoryUsers.coffee
+++ server-ce/grunt/CreateAndDestoryUsers.coffee
@@ -1,11 +1,11 @@

 module.exports = (grunt) ->

-	grunt.registerTask 'user:create-admin', "Create a user with the given email address and make them an admin. Update in place if the user already exists. Usage: grunt create-admin-user --email joe@example.com", () ->
+	grunt.registerTask 'user:create-admin', "Create a user with the given email address and make them an admin. Update in place if the user already exists. Usage: grunt user:create-admin --email joe@example.com", () ->
 		done = @async()
 		email = grunt.option("email")
 		if !email?
-			console.error "Usage: grunt create-admin-user --email joe@example.com"
+			console.error "Usage: grunt user:create-admin --email joe@example.com"
 			process.exit(1)

 		settings = require "settings-sharelatex"
@@ -35,11 +35,11 @@
 					"""
 					done()

-	grunt.registerTask 'user:delete', "deletes a user and all their data, Usage: grunt delete-user --email joe@example.com", () ->
+	grunt.registerTask 'user:delete', "deletes a user and all their data, Usage: grunt user:delete --email joe@example.com", () ->
 		done = @async()
 		email = grunt.option("email")
 		if !email?
-			console.error "Usage: grunt delete-user --email joe@example.com"
+			console.error "Usage: grunt user:delete --email joe@example.com"
 			process.exit(1)
 		settings = require "settings-sharelatex"
 		UserGetter = require "../web/app/js/Features/User/UserGetter"


use ENv vars for 2FA settings

use ENv vars for 2FA settings

GitOrigin-RevId: b32b2d62d0f9145e5c59b8984b44940c855a1d91

--- services/web/config/settings.defaults.coffee
+++ services/web/config/settings.defaults.coffee
@@ -225,8 +225,8 @@
 	httpAuthUsers: httpAuthUsers

 	twoFactorAuthentication:
-		enabled: false
-		requiredForStaff: false
+		enabled: process.env['TWO_FACTOR_AUTHENTICATION_ENABLED'] == 'true'
+		requiredForStaff: process.env['TWO_FACTOR_AUTHENTICATION_REQUIRED_FOR_STAFF'] == 'true'

 	# Default features
 	# ----------------


Added a custom (longer) duration to the fadeIn() and fadeOut() animation methods.

--- widgets/fitbit/fitbit.coffee
+++ widgets/fitbit/fitbit.coffee
@@ -24,7 +24,7 @@
       @errorView().hide()
       @dataView().show()

-      @firstView().fadeIn()
+      @firstView().fadeIn @animationLength()
       @startAnimation()

   startAnimation: ->
@@ -34,11 +34,11 @@
     @hideCurrentView() and @showNextView()

   hideCurrentView: ->
-    @currentView(@index).fadeOut()
+    @currentView(@index).fadeOut @animationLength()

   showNextView: ->
     @findNextIndex()
-    @currentView(@index).fadeIn()
+    @currentView(@index).fadeIn @animationLength()

   findNextIndex: ->
     @index = (@index + 1)
@@ -61,3 +61,6 @@

   errorView: ->
     $(@node).find("#error")
+
+  animationLength: ->
+    1000


Color the correct sequencer

Color the correct sequencer

--- src/glitchjs-ui/GameOfLifeControl.coffee
+++ src/glitchjs-ui/GameOfLifeControl.coffee
@@ -29,7 +29,7 @@
         for seqIdx in [0...6]
           sequencer = @_mainController._gameOfLife.sequencerAt seqIdx
           if sequencer?.isCellActive(col,row)
-            context.fillStyle = @_colors[seqIdx]
+            context.fillStyle = @_colors[seqIdx + 1]

         context.fillRect(@_squareSide * col + @_padding * (col - 1),
             @_squareSide * row + @_padding * (row - 1),


DateTimeFormats: added three new languages: ru-RU, pl-PL and cs-CZ with the values of DE. See #57092

--- src/elements/DateTime/DateTimeFormats.coffee
+++ src/elements/DateTime/DateTimeFormats.coffee
@@ -506,3 +506,417 @@
 			"Y"
 		]
 	]
+
+CUI.DateTimeFormats["ru-RU"] =
+	timezone: "Europe/Berlin"
+	moment_locale: "ru-RU"
+	tab_date: "Datum"
+	tab_time: "Zeit"
+	tab_week: "Wo"
+	formats: [
+		text: "Datum+Zeit"
+		support_bc: false
+		invalid: "Datum ungültig"
+		type: "date_time"
+		clock: true
+		store: "YYYY-MM-DDTHH:mm:00Z"
+		clock_seconds: false
+# digi_clock: "HH:mm"
+		input: "DD.MM.YYYY HH:mm"
+		display: "dd, DD.MM.YYYY HH:mm"
+		display_short: "DD.MM.YYYY HH:mm"
+		display_attribute: "date-time"
+		display_short_attribute: "date-time-short"
+# timezone_display: "dddd, DD.MM.YYYY HH:mm zZ"
+		parse: [
+			"YYYY-MM-DD HH:mm:ss"
+			"YYYY-MM-DDTHH:mm:ss.SSSZ"
+			"YYYY-MM-DDTHH:mm:ssZ"
+			"D.M.YYYY HH:mm:ss"
+			"DD.M.YYYY HH:mm:ss"
+			"D.MM.YYYY HH:mm:ss"
+			"D.MM.YY HH:mm:ss"
+			"DD.M.YY HH:mm:ss"
+			"D.M.YYYY HH:mm"
+			"DD.M.YYYY HH:mm"
+			"D.MM.YYYY HH:mm"
+			"D.MM.YY HH:mm"
+			"DD.M.YY HH:mm"
+			"M/D/YYYY HH:DD:ss"
+			"MM/D/YYYY HH:DD:ss"
+			"M/DD/YYYY HH:DD:ss"
+			"M/DD/YY HH:DD:ss"
+			"MM/D/YY HH:DD:ss"
+			"M/D/YYYY HH:DD"
+			"MM/D/YYYY HH:DD"
+			"M/DD/YYYY HH:DD"
+			"M/DD/YY HH:DD"
+			"MM/D/YY HH:DD"
+		]
+	,
+		text: "Datum+Zeit+Sekunden"
+		support_bc: false
+		invalid: "Datum ungültig"
+# input: "YYYY-MM-DD HH:mm:ss"
+		type: "date_time_seconds"
+		input: "DD.MM.YYYY HH:mm:ss"
+		store: "YYYY-MM-DDTHH:mm:ssZ"
+		display: "dd, DD.MM.YYYY HH:mm:ss"
+		display_short: "DD.MM.YYYY HH:mm:ss"
+		display_attribute: "date-time-seconds"
+		display_short_attribute: "date-time-seconds-short"
+# timezone_display: "dddd, DD.MM.YYYY HH:mm:ss zZ"
+		clock: true
+		clock_seconds: true
+# digi_clock: "HH:mm:ss"
+		parse: [
+			"YYYY-MM-DDTHH:mm:ssZ"
+			"YYYY-MM-DD HH:mm:ss"
+			"D.M.YYYY HH:mm:ss"
+			"DD.M.YYYY HH:mm:ss"
+			"D.MM.YYYY HH:mm:ss"
+			"D.MM.YY HH:mm:ss"
+			"DD.M.YY HH:mm:ss"
+			"M/D/YYYY HH:DD:ss"
+			"MM/D/YYYY HH:DD:ss"
+			"M/DD/YYYY HH:DD:ss"
+			"M/DD/YY HH:DD:ss"
+			"MM/D/YY HH:DD:ss"
+		]
+	,
+		text: "Datum"
+		support_bc: false
+		input: "DD.MM.YYYY"
+		invalid: "Datum ungültig"
+		display: "dd, DD.MM.YYYY"
+		display_short: "DD.MM.YYYY"
+		display_attribute: "date"
+		display_short_attribute: "date-short"
+		store: "YYYY-MM-DD"
+		type: "date"
+# digi_clock: false
+		clock: false
+		parse: [
+			"D.M.YYYY"
+			"D.MM.YYYY"
+			"DD.M.YYYY"
+			"YYYYMMDD"
+			"YYYY-M-D"
+			"M/D/YYYY"
+			"MM/D/YYYY"
+			"M/DD/YYYY"
+		]
+	,
+		text: "Jahr-Monat"
+		support_bc: false
+		input: "MM.YYYY"
+		invalid: "Datum ungültig"
+		store: "YYYY-MM"
+		display: "MMMM YYYY"
+		display_short: "MM.YYYY"
+		display_attribute: "year-month"
+		display_short_attribute: "year-month-short"
+		type: "year_month"
+# digi_clock: false
+		clock: false
+		parse: [
+			"MM YYYY"
+			"MM/YYYY"
+			"MM.YYYY"
+			"M.YYYY"
+			"YYYY-M"
+			"YYYY-MM"
+		]
+	,
+		text: "Jahr"
+		support_bc: true
+		input: "Y"
+		invalid: "Datum ungültig"
+		display: "Y"
+		display_short: "Y"
+		display_attribute: "year"
+		display_short_attribute: "year"
+		store: "YYYY"
+		type: "year"
+# digi_clock: false
+		clock: false
+		parse: [
+			"Y"
+		]
+	]
+
+CUI.DateTimeFormats["pl-PL"] =
+	timezone: "Europe/Berlin"
+	moment_locale: "pl-PL"
+	tab_date: "Datum"
+	tab_time: "Zeit"
+	tab_week: "Wo"
+	formats: [
+		text: "Datum+Zeit"
+		support_bc: false
+		invalid: "Datum ungültig"
+		type: "date_time"
+		clock: true
+		store: "YYYY-MM-DDTHH:mm:00Z"
+		clock_seconds: false
+# digi_clock: "HH:mm"
+		input: "DD.MM.YYYY HH:mm"
+		display: "dd, DD.MM.YYYY HH:mm"
+		display_short: "DD.MM.YYYY HH:mm"
+		display_attribute: "date-time"
+		display_short_attribute: "date-time-short"
+# timezone_display: "dddd, DD.MM.YYYY HH:mm zZ"
+		parse: [
+			"YYYY-MM-DD HH:mm:ss"
+			"YYYY-MM-DDTHH:mm:ss.SSSZ"
+			"YYYY-MM-DDTHH:mm:ssZ"
+			"D.M.YYYY HH:mm:ss"
+			"DD.M.YYYY HH:mm:ss"
+			"D.MM.YYYY HH:mm:ss"
+			"D.MM.YY HH:mm:ss"
+			"DD.M.YY HH:mm:ss"
+			"D.M.YYYY HH:mm"
+			"DD.M.YYYY HH:mm"
+			"D.MM.YYYY HH:mm"
+			"D.MM.YY HH:mm"
+			"DD.M.YY HH:mm"
+			"M/D/YYYY HH:DD:ss"
+			"MM/D/YYYY HH:DD:ss"
+			"M/DD/YYYY HH:DD:ss"
+			"M/DD/YY HH:DD:ss"
+			"MM/D/YY HH:DD:ss"
+			"M/D/YYYY HH:DD"
+			"MM/D/YYYY HH:DD"
+			"M/DD/YYYY HH:DD"
+			"M/DD/YY HH:DD"
+			"MM/D/YY HH:DD"
+		]
+	,
+		text: "Datum+Zeit+Sekunden"
+		support_bc: false
+		invalid: "Datum ungültig"
+# input: "YYYY-MM-DD HH:mm:ss"
+		type: "date_time_seconds"
+		input: "DD.MM.YYYY HH:mm:ss"
+		store: "YYYY-MM-DDTHH:mm:ssZ"
+		display: "dd, DD.MM.YYYY HH:mm:ss"
+		display_short: "DD.MM.YYYY HH:mm:ss"
+		display_attribute: "date-time-seconds"
+		display_short_attribute: "date-time-seconds-short"
+# timezone_display: "dddd, DD.MM.YYYY HH:mm:ss zZ"
+		clock: true
+		clock_seconds: true
+# digi_clock: "HH:mm:ss"
+		parse: [
+			"YYYY-MM-DDTHH:mm:ssZ"
+			"YYYY-MM-DD HH:mm:ss"
+			"D.M.YYYY HH:mm:ss"
+			"DD.M.YYYY HH:mm:ss"
+			"D.MM.YYYY HH:mm:ss"
+			"D.MM.YY HH:mm:ss"
+			"DD.M.YY HH:mm:ss"
+			"M/D/YYYY HH:DD:ss"
+			"MM/D/YYYY HH:DD:ss"
+			"M/DD/YYYY HH:DD:ss"
+			"M/DD/YY HH:DD:ss"
+			"MM/D/YY HH:DD:ss"
+		]
+	,
+		text: "Datum"
+		support_bc: false
+		input: "DD.MM.YYYY"
+		invalid: "Datum ungültig"
+		display: "dd, DD.MM.YYYY"
+		display_short: "DD.MM.YYYY"
+		display_attribute: "date"
+		display_short_attribute: "date-short"
+		store: "YYYY-MM-DD"
+		type: "date"
+# digi_clock: false
+		clock: false
+		parse: [
+			"D.M.YYYY"
+			"D.MM.YYYY"
+			"DD.M.YYYY"
+			"YYYYMMDD"
+			"YYYY-M-D"
+			"M/D/YYYY"
+			"MM/D/YYYY"
+			"M/DD/YYYY"
+		]
+	,
+		text: "Jahr-Monat"
+		support_bc: false
+		input: "MM.YYYY"
+		invalid: "Datum ungültig"
+		store: "YYYY-MM"
+		display: "MMMM YYYY"
+		display_short: "MM.YYYY"
+		display_attribute: "year-month"
+		display_short_attribute: "year-month-short"
+		type: "year_month"
+# digi_clock: false
+		clock: false
+		parse: [
+			"MM YYYY"
+			"MM/YYYY"
+			"MM.YYYY"
+			"M.YYYY"
+			"YYYY-M"
+			"YYYY-MM"
+		]
+	,
+		text: "Jahr"
+		support_bc: true
+		input: "Y"
+		invalid: "Datum ungültig"
+		display: "Y"
+		display_short: "Y"
+		display_attribute: "year"
+		display_short_attribute: "year"
+		store: "YYYY"
+		type: "year"
+# digi_clock: false
+		clock: false
+		parse: [
+			"Y"
+		]
+	]
+
+CUI.DateTimeFormats["cs-CZ"] =
+	timezone: "Europe/Berlin"
+	moment_locale: "cs-CZ"
+	tab_date: "Datum"
+	tab_time: "Zeit"
+	tab_week: "Wo"
+	formats: [
+		text: "Datum+Zeit"
+		support_bc: false
+		invalid: "Datum ungültig"
+		type: "date_time"
+		clock: true
+		store: "YYYY-MM-DDTHH:mm:00Z"
+		clock_seconds: false
+# digi_clock: "HH:mm"
+		input: "DD.MM.YYYY HH:mm"
+		display: "dd, DD.MM.YYYY HH:mm"
+		display_short: "DD.MM.YYYY HH:mm"
+		display_attribute: "date-time"
+		display_short_attribute: "date-time-short"
+# timezone_display: "dddd, DD.MM.YYYY HH:mm zZ"
+		parse: [
+			"YYYY-MM-DD HH:mm:ss"
+			"YYYY-MM-DDTHH:mm:ss.SSSZ"
+			"YYYY-MM-DDTHH:mm:ssZ"
+			"D.M.YYYY HH:mm:ss"
+			"DD.M.YYYY HH:mm:ss"
+			"D.MM.YYYY HH:mm:ss"
+			"D.MM.YY HH:mm:ss"
+			"DD.M.YY HH:mm:ss"
+			"D.M.YYYY HH:mm"
+			"DD.M.YYYY HH:mm"
+			"D.MM.YYYY HH:mm"
+			"D.MM.YY HH:mm"
+			"DD.M.YY HH:mm"
+			"M/D/YYYY HH:DD:ss"
+			"MM/D/YYYY HH:DD:ss"
+			"M/DD/YYYY HH:DD:ss"
+			"M/DD/YY HH:DD:ss"
+			"MM/D/YY HH:DD:ss"
+			"M/D/YYYY HH:DD"
+			"MM/D/YYYY HH:DD"
+			"M/DD/YYYY HH:DD"
+			"M/DD/YY HH:DD"
+			"MM/D/YY HH:DD"
+		]
+	,
+		text: "Datum+Zeit+Sekunden"
+		support_bc: false
+		invalid: "Datum ungültig"
+# input: "YYYY-MM-DD HH:mm:ss"
+		type: "date_time_seconds"
+		input: "DD.MM.YYYY HH:mm:ss"
+		store: "YYYY-MM-DDTHH:mm:ssZ"
+		display: "dd, DD.MM.YYYY HH:mm:ss"
+		display_short: "DD.MM.YYYY HH:mm:ss"
+		display_attribute: "date-time-seconds"
+		display_short_attribute: "date-time-seconds-short"
+# timezone_display: "dddd, DD.MM.YYYY HH:mm:ss zZ"
+		clock: true
+		clock_seconds: true
+# digi_clock: "HH:mm:ss"
+		parse: [
+			"YYYY-MM-DDTHH:mm:ssZ"
+			"YYYY-MM-DD HH:mm:ss"
+			"D.M.YYYY HH:mm:ss"
+			"DD.M.YYYY HH:mm:ss"
+			"D.MM.YYYY HH:mm:ss"
+			"D.MM.YY HH:mm:ss"
+			"DD.M.YY HH:mm:ss"
+			"M/D/YYYY HH:DD:ss"
+			"MM/D/YYYY HH:DD:ss"
+			"M/DD/YYYY HH:DD:ss"
+			"M/DD/YY HH:DD:ss"
+			"MM/D/YY HH:DD:ss"
+		]
+	,
+		text: "Datum"
+		support_bc: false
+		input: "DD.MM.YYYY"
+		invalid: "Datum ungültig"
+		display: "dd, DD.MM.YYYY"
+		display_short: "DD.MM.YYYY"
+		display_attribute: "date"
+		display_short_attribute: "date-short"
+		store: "YYYY-MM-DD"
+		type: "date"
+# digi_clock: false
+		clock: false
+		parse: [
+			"D.M.YYYY"
+			"D.MM.YYYY"
+			"DD.M.YYYY"
+			"YYYYMMDD"
+			"YYYY-M-D"
+			"M/D/YYYY"
+			"MM/D/YYYY"
+			"M/DD/YYYY"
+		]
+	,
+		text: "Jahr-Monat"
+		support_bc: false
+		input: "MM.YYYY"
+		invalid: "Datum ungültig"
+		store: "YYYY-MM"
+		display: "MMMM YYYY"
+		display_short: "MM.YYYY"
+		display_attribute: "year-month"
+		display_short_attribute: "year-month-short"
+		type: "year_month"
+# digi_clock: false
+		clock: false
+		parse: [
+			"MM YYYY"
+			"MM/YYYY"
+			"MM.YYYY"
+			"M.YYYY"
+			"YYYY-M"
+			"YYYY-MM"
+		]
+	,
+		text: "Jahr"
+		support_bc: true
+		input: "Y"
+		invalid: "Datum ungültig"
+		display: "Y"
+		display_short: "Y"
+		display_attribute: "year"
+		display_short_attribute: "year"
+		store: "YYYY"
+		type: "year"
+# digi_clock: false
+		clock: false
+		parse: [
+			"Y"
+		]
+	]


ProfileView: hide troll button for admin's own profile

--- client/Social/Members/ContentDisplays/profileview.coffee
+++ client/Social/Members/ContentDisplays/profileview.coffee
@@ -158,20 +158,21 @@
       @sendMessageLink?.on 'MessageShouldBeSent', ({formOutput, callback}) =>
         @prepareMessage formOutput, callback

-    if KD.checkFlag('super-admin')# and not KD.isMine @memberData
-
-      @trollButton = new TrollButtonView
-        style : 'solid medium red'
-      , data
+
+    @trollButton    = new KDCustomHTMLView
+    @metaInfoButton = new KDCustomHTMLView
+
+    if KD.checkFlag('super-admin')
+
+      unless KD.isMine @memberData
+
+        @trollButton = new TrollButtonView
+          style : 'solid medium red'
+        , data

       @metaInfoButton = new MetaInfoButtonView
         style : 'solid medium green'
-        , data
-
-    else
-
-      @trollButton    = new KDCustomHTMLView
-      @metaInfoButton = new KDCustomHTMLView
+      , data

   viewAppended:->


StackTemplateListController: Sync items between private and team stack list

--- client/app/lib/stacks/stacktemplatelistcontroller.coffee
+++ client/app/lib/stacks/stacktemplatelistcontroller.coffee
@@ -74,6 +74,16 @@
         if item
           item.getData().inUse = yes
           item.inUseView.show()
+
+
+    groupsController.on 'StackTemplateChanged', (params) =>
+
+      stackTemplateId = params.contents
+      [item]  = @getListItems().filter (i) -> i.getData()._id is stackTemplateId
+
+      unless item
+        @fetch { _id : stackTemplateId }, (items) =>
+          @addListItems items

   applyToTeam: (item) ->


add changed callback to redactor to update atoms preview

--- app/assets/javascripts/folio/console/redactor/_init.coffee
+++ app/assets/javascripts/folio/console/redactor/_init.coffee
@@ -4,6 +4,9 @@
   lang: document.documentElement.lang
   formatting: ['p', 'h2', 'h3', 'h4']
   linkNewTab: true
+  callbacks:
+    changed: ->
+      $(@rootElement).trigger('change')

 OPTIONS =
   plugins: ['table', 'button', 'character_counter', 'definedlinks']
@@ -13,6 +16,9 @@
   lang: document.documentElement.lang
   formatting: ['p', 'h2', 'h3', 'h4']
   linkNewTab: true
+  callbacks:
+    changed: ->
+      $(@rootElement).trigger('change')

 EMAIL_OPTIONS =
   plugins: ['button', 'character_counter']
@@ -20,6 +26,9 @@
   toolbarFixed: true
   lang: document.documentElement.lang
   formatting: []
+  callbacks:
+    changed: ->
+      $(@rootElement).trigger('change')

 PEREX_OPTIONS =
   plugins: ['character_counter', 'definedlinks']
@@ -28,6 +37,9 @@
   toolbarFixed: true
   lang: document.documentElement.lang
   linkNewTab: true
+  callbacks:
+    changed: ->
+      $(@rootElement).trigger('change')

 window.folioConsoleInitRedactor = (node, options = {}, additional = {}) ->
   return if node.classList.contains('redactor-source')


Set the clicked item to be the highlighted item

--- app/assets/javascripts/lib/mobile/autocomplete_2.js.coffee
+++ app/assets/javascripts/lib/mobile/autocomplete_2.js.coffee
@@ -97,6 +97,7 @@
       if target.tagName == 'A'
         e.stopPropagation()
       else if target.tagName == 'LI'
+        @results.highlighted = target
         @_selectCurrent()

     _resultsMouseOver: (target) ->


Added enter key handler

Added enter key handler

--- app/assets/javascripts/lib/mobile/autocomplete_2.js.coffee
+++ app/assets/javascripts/lib/mobile/autocomplete_2.js.coffee
@@ -69,17 +69,24 @@
     # event handlers
     _keypressHandler: (e) ->
       switch e.keyCode
-        when KEY.up, e.shiftKey and KEY.tab
+        when KEY.up
           e.preventDefault()
           @_highlightUp()
-        when KEY.down, KEY.tab
+        when KEY.down
           e.preventDefault()
           @_highlightDown()
+        when KEY.tab
+          e.preventDefault() if @results.displayed
+          if e.shiftKey
+            @_highlightUp()
+          else
+            @_highlightDown()
         when KEY.enter
           e.preventDefault()
-          @_selectHighlighted()
+          @_handleEnter()
         when KEY.esc
           @el.value == 'hdhfdhd'
+          console.log @el.value
           @_removeResults() if @results.displayed

     _resultsClick: (e) ->
@@ -91,8 +98,6 @@
       # if the real target was an anchor, prevent the event from bubbling so the text isn't selected
       if target.tagName == 'A'
         e.stopPropagation()
-        e.preventDefault() # just so i can check it
-        console.log 'link anchor clicked'
       else if target.tagName == 'LI'
         @_selectCurrent()

@@ -122,6 +127,15 @@

     # keypress handlers
+    _handleEnter: ->
+      # check if the child of the selected node is a link
+      highlighted = @results.highlighted.firstChild
+      if highlighted.tagName == 'A'
+        # it's a link, change the href
+        window.location = highlighted.href
+      else
+        @_selectCurrent()
+
     _highlightDown: ->
       if not @results.highlighted
         @results.highlighted = @results.firstChild


Bootstrap Support

Bootstrap Support

--- app/assets/javascripts/plugins/vzaar_uploader/vzaar.coffee
+++ app/assets/javascripts/plugins/vzaar_uploader/vzaar.coffee
@@ -4,7 +4,9 @@
       video = $(this)
       src = $(this).attr('src') || $(this).find('source').attr('src')
       if src.search('vzaar.com') >= 0
-        $(this).replaceWith("""""")
+        pattern = /\/\/view.vzaar.com\/([0-9]*)\/player/i
+        match = pattern.exec($(this).attr('src'))
+        $(this).replaceWith("""""")

   $('video').not('exclude_vzaar_iframe').each ->
     $(this).VzaarPlayer()


Rename setAddress to setAddresses

Rename setAddress to setAddresses

--- app/assets/javascripts/sprangular/services/checkout.coffee
+++ app/assets/javascripts/sprangular/services/checkout.coffee
@@ -40,8 +40,7 @@

       @put(params)

-
-    setAddress: ->
+    setAddresses: ->

     setDelivery: ->


Updated chrome extension behaviour

Updated chrome extension behaviour

--- core/app/assets/javascripts/base/browser_extensions.coffee
+++ core/app/assets/javascripts/base/browser_extensions.coffee
@@ -1,11 +1,2 @@
-detectChromeExtension = ->
-  s = document.createElement("script")
-  s.onerror = -> $("html").addClass "chrome_extension_not_installed"
-  s.onload = -> $("html").addClass "chrome_extension_installed"
-  document.body.appendChild s
-  s.src = "chrome-extension://#{Factlink.Global.chrome_extension_id}/manifest.json"
-
-if Factlink.Global.chrome_extension_id
-  is_chrome = navigator.userAgent.toLowerCase().indexOf("chrome") > -1
-  if is_chrome
-    detectChromeExtension()
+if not $('html').hasClass('chrome_extension_installed')
+  $('html').addClass('chrome_extension_not_installed')


better documentation wrapping

better documentation wrapping

--- lib/assets/javascripts/jax/builtin/models/framerate.js.coffee
+++ lib/assets/javascripts/jax/builtin/models/framerate.js.coffee
@@ -1,7 +1,7 @@
 ###

-A visual frames per second counter that can be added to the world like any other
-model:
+A visual frames per second counter that can be added to the world like any
+other model:

     @world.addObject new Jax.Framerate

@@ -14,21 +14,23 @@
     left   - the left pixel coordinate of the counter
     top    - the top pixel coordinate of the counter

-After instantiation, the counter can be manipulated just like any other Jax model
-(e.g. `new Jax.Framerate().camera.getPosition()`) except that it uses pixel coordinates
-instead of world units and is displayed with an orthographic (essentially 2D) projection.
+After instantiation, the counter can be manipulated just like any other Jax
+model (e.g. `new Jax.Framerate().camera.getPosition()`) except that it uses
+pixel coordinates instead of world units and is displayed with an
+orthographic (essentially 2D) projection.

 The framerate is capable of two modes of operation

-* EMA - calculates an exponential moving average of the framerate based on the
-  `ema` option, producing a smooth line on the graph, allowing you to visually
-  "skip over" temporary disruptions such as garbage collection, and giving you
-  the "big picture".
-* Instant - if the `ema` option is `false`, no moving averages will be calculated,
-  allowing you to view every tiny disruption in framerate. You can use this to
-  home in on common wasteful coding practices such as allocating too many temporary
-  objects. This sort of optimization will produce a smoother-flowing application
-  with fewer and shorter pauses as the JavaScript garbage collector is run.
+* EMA - calculates an exponential moving average of the framerate based on
+  the `ema` option, producing a smooth line on the graph, allowing you to
+  visually "skip over" temporary disruptions such as garbage collection, and
+  giving you the "big picture".
+* Instant - if the `ema` option is `false`, no moving averages will be
+  calculated, allowing you to view every tiny disruption in framerate. You
+  can use this to home in on common wasteful coding practices such as
+  allocating too many temporary objects. This sort of optimization will
+  produce a smoother-flowing application with fewer and shorter pauses as
+  the JavaScript garbage collector is run.

 You can also get the current framerate any time after the framerate has been
 added to the world by calling `fps` or `ups`.


Only show content views that have content

--- src/scripts/pages/content/content.coffee
+++ src/scripts/pages/content/content.coffee
@@ -21,7 +21,5 @@
     onRender: () ->
       @parent?.regions.header.show(new HeaderView({page: 'content'}))
       @parent?.regions.footer.show(new FooterView({page: 'content'}))
-
-      if @uuid
-        @regions.content.append(new FindContentView())
-        @regions.content.append(new MediaView({uuid: @uuid, page: @page}))
+      @regions.content.append(new FindContentView())
+      @regions.content.append(new MediaView({uuid: @uuid, page: @page}))


Refactored template 'closed' to accomodate main CSS changes.

--- demo/client/demo.coffee
+++ demo/client/demo.coffee
@@ -96,6 +96,13 @@
     return

 Template.saved.rendered = ->
+  # Modal manual opening
+  @$('.modal').addClass('in').css
+    display: 'block'
+    overflow: 'hidden'
+  return
+
+Template.closed.rendered = ->
   # Modal manual opening
   @$('.modal').addClass('in').css
     display: 'block'


Removed unnecessary dependency injections from service.

--- app/assets/javascripts/ng-services/ng-FeedsFoldersSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-FeedsFoldersSvc.js.coffee
@@ -3,12 +3,10 @@
 ########################################################

 angular.module('feedbunch').service 'feedsFoldersSvc',
-['$rootScope', '$http', '$timeout', 'timerFlagSvc', 'findSvc', 'entriesPaginationSvc',
-'feedsPaginationSvc', 'cleanupSvc', 'favicoSvc', 'animationsSvc', 'highlightedSidebarLinkSvc',
-'loadFeedsSvc', 'loadFoldersSvc',
-($rootScope, $http, $timeout, timerFlagSvc, findSvc, entriesPaginationSvc,
-feedsPaginationSvc, cleanupSvc, favicoSvc, animationsSvc, highlightedSidebarLinkSvc,
-loadFeedsSvc, loadFoldersSvc)->
+['$rootScope', '$timeout', 'entriesPaginationSvc', 'feedsPaginationSvc', 'cleanupSvc',
+'animationsSvc', 'highlightedSidebarLinkSvc', 'loadFeedsSvc', 'loadFoldersSvc',
+($rootScope, $timeout, entriesPaginationSvc, feedsPaginationSvc, cleanupSvc,
+animationsSvc, highlightedSidebarLinkSvc, loadFeedsSvc, loadFoldersSvc)->

   #--------------------------------------------
   # PRIVATE FUNCTION: Load feeds and folders every minute.


(- new tests) Exec: Thoroughly exercise Execution#current

--- Paws/Test/data.tests.coffee
+++ Paws/Test/data.tests.coffee
@@ -286,6 +286,21 @@
          ex.advance()
          expect(ex.complete()).to.be true

+      it 'provides access to its current position', ->
+         seq = parse 'abc def'
+         ex = new Execution seq
+
+         expect(-> ex.current()).to.not.throwException()
+         expect(   ex.current()).to.be.a Position
+
+         expect(   ex.current().sequence()).to.be seq
+         expect(   ex.current().valueOf().alien).to.be 'abc'
+
+         ex.advance()
+         ex.advance new Thing
+         expect(   ex.current().sequence()).to.be seq
+         expect(   ex.current().valueOf().alien).to.be 'def'
+
       it 'can be cloned', ->
          ex = new Execution (new Sequence)
          expect(-> ex.clone()).to.not.throwException()
@@ -325,8 +340,6 @@
          expect(clone.find('locals')[1].valueish()).to.equal ex.locals

       describe '#advance', ->
-         parse   = Paws.parse
-
          it "doesn't modify a completed Native", ->
             completed_alien = new Native
             expect(completed_alien.complete()).to.be.ok()


updating task plan helper to include target type

--- test/components/helpers/task-plan.coffee
+++ test/components/helpers/task-plan.coffee
@@ -15,6 +15,7 @@
   baseTaskingPlan =
     opens_at: tomorrow
     due_at: dayAfter
+    target_type: 'period'

   baseModel = _.extend({}, baseModel, props)


Fix spec to match current behaviour of 2-step

Answers are now shown

--- test/components/qa/exercises.spec.coffee
+++ test/components/qa/exercises.spec.coffee
@@ -37,18 +37,16 @@
       renderedQs = _.pluck dom.querySelectorAll('.panel-body .question-stem'), 'textContent'
       expect(renderedQs).to.deep.equal(questions)

-  it 'hides answers when previewing 2-step', ->
+  it 'displays free-response box when previewing 2-step', ->
     Testing.renderComponent( Exercises, props: @props ).then ({dom, element}) ->
+      expect( dom.querySelector('.exercise-free-response-preview') ).to.not.exist
       cb = dom.querySelector('.preview2step')
       ReactTestUtils.Simulate.change(cb, {target: checked: true})
-      all  = element.getDOMNode().querySelectorAll('.exercise')
-      hidden = element.getDOMNode().querySelectorAll('.answers-hidden')
       freeResponseCount = _.reduce(EXERCISES.items, (count, ex) ->
-        count + (if ExerciseStore.hasQuestionWithFormat(ex, 'free-response') then 1 else 0)
+        count + (if ExerciseStore.hasQuestionWithFormat('free-response', ex) then 1 else 0)
       , 0)
-      expect(all.length).to.equal(EXERCISES.items.length)
-      expect(hidden.length).to.equal(freeResponseCount)
-      expect(hidden[0].querySelector('.answers-table').textContent).to.be.empty
+      expect( dom.querySelectorAll('.exercise-free-response-preview').length )
+        .to.equal(freeResponseCount)

   it 'renders exercises even if they dont have tags', ->
     ex = ld.cloneDeep EXERCISES


Update javascript.coffee

Update javascript.coffee

--- data/tags/javascript.coffee
+++ data/tags/javascript.coffee
@@ -8,3 +8,4 @@
   "MDN JavaScript": "https://developer.mozilla.org/en-US/docs/JavaScript"
   "JavaScript 秘密花园": "http://bonsaiden.github.io/JavaScript-Garden/zh/"
   "JavaScript Patterns": "http://shichuan.github.io/javascript-patterns/"
+  "ECOM JavaScript编码规范": "http://fe.baidu.com/doc/ecom/std/javascript-code-style.text"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -26,7 +26,7 @@
     else if index is 1
       equals obj, obj2
     else if index is 2
-      equals obj, obj
+      equals obj, obj3

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


More tests

More tests

--- test/karma/specs/EventHandlerSpec.coffee
+++ test/karma/specs/EventHandlerSpec.coffee
@@ -6,6 +6,38 @@

   describe 'EventHandler', ->

+    evt = null
+    spy = null
+
+    beforeEach ->
+      evt = new EventHandler()
+      spy = jasmine.createSpy('eventCallback')
+      evt.on(spy)
+
     it 'should exist', () ->
       expect(EventHandler).toBeDefined()
       expect(typeof EventHandler).toBe('function')
+      expect(evt instanceof EventHandler).toBe(true)
+
+    it 'passes through arguments', ->
+      evt.fire()
+      expect(spy).toHaveBeenCalledWith()
+
+      evt.fire('This', 'is', 'a', 'test')
+      expect(spy).toHaveBeenCalledWith('This', 'is', 'a', 'test')
+
+      evt.fire(77)
+      expect(spy).toHaveBeenCalledWith(77)
+
+    it 'can run the callbacks async', (done) ->
+      expect(spy).not.toHaveBeenCalled()
+      evt.fireAsync('async', 'call')
+      expect(spy).not.toHaveBeenCalled()
+
+      (checkCalls = ->
+        if (spy.calls.count() > 0)
+          expect(spy).toHaveBeenCalledWith('async', 'call')
+          done()
+        else
+          setTimeout(checkCalls, 0)
+      )()


Refactoring: creating reusable functions; and modified core controller to pass itself to other called controllers

--- controllers/index.coffee
+++ controllers/index.coffee
@@ -6,51 +6,51 @@
         @_redirectSIFE()

         # Index
-        app.get '/', @controllers['home']['index']
+        @app.get '/', @controllers['home']['index']

         # Contact us
         if process.env.MAILGUN_API_KEY?
-            app.post '/contact-us', @controllers['contact-us']['index']
+            @app.post '/contact-us', (req, res) =>
+                @controllers['contact-us']['index'](req, res, @)

         # Other pages
-        app.get '/:controller/:action?', (req, res) =>
-            try
-                controller = app.get('routes')[req.params.controller]
-                action = controller.actions?[req.params.action]
+        @app.get '/:controller/:action?', (req, res) =>
+            # Find controller and return 404 if it is not found
+            controller = @routes[req.params.controller] ? @_notFound req, res
+            action = controller.actions?[req.params.action]
+            currentTitle = action?.title ? controller.title

-                pageTitle = action?.title ? controller.title
+            # Set metadata
+            @_setTitle res, currentTitle
+            @_setBreadcrumbs res, currentTitle, req.params.action ? req.params.controller,
+                            [title: controller.title, slug: req.params.controller] if action?

-                res.locals.page.title = "#{pageTitle} - #{res.locals.page.title}"
-                res.locals.current =
-                    title: pageTitle
-                    slug: req.params.action ? req.params.controller
-                    location: []
+            req.params.action ?= 'index'

-                if action?
-                    # Set history
-                    res.locals.current.location.push
-                        title: controller.title
-                        slug: req.params.controller
-                else
-                    # Set index as the default action if undefined
-                    req.params.action = 'index'
+            # Call controller if it exists
+            if @controllers[req.params.controller]?[req.params.action]?
+                @controllers[req.params.controller][req.params.action] req, res, @

-                # Call controller if it exists
-                if @controllers[req.params.controller]?[req.params.action]?
-                    @controllers[req.params.controller][req.params.action] req, res
+            @_render res, "#{req.params.controller}/#{req.params.action}"

-                res.render "#{req.params.controller}/#{req.params.action}", {}, (error, html) =>
-                    return @_notFound req, res if error
-                    res.end html
+        @app.use @_notFound

-            catch error
-                console.log error
-                @_notFound req, res
+    _setTitle: (res, title) ->
+        res.locals.page.title = "#{title} - #{res.locals.page.title}"

-        app.use (req, res, next) =>
-            @_notFound req, res, next
+    _setBreadcrumbs: (res, title, slug, location) ->
+        res.locals.current =
+            title: title
+            slug: slug
+            location: location
+
+    _render: (res, template) ->
+        res.render template, {}, (error, html) =>
+            return @_notFound false, res if error
+            res.end html

     _buildControllers: ->
+        @routes = @app.get('routes')
         @controllers =
             'home': require './home.coffee'
             'contact-us': require './contact.coffee'