Search completion; and even more minor tweaks.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -175,8 +175,7 @@

     completion = suggestions[0].slice query.length, length

-    # Don't complete trailing whitespace; so, strip it.  Then, verify that the completion is still long
-    # enough.
+    # Don't complete trailing whitespace, strip it.  Then, verify that the completion is still long enough.
     completion = completion.replace /\s+$/, ""
     return unless 0 < completion.length

@@ -187,11 +186,9 @@
     @input.value = query + completion
     @input.setSelectionRange query.length, query.length + completion.length

-  #
   # Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their
-  # keypress.
-  # We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
-  #
+  # keypress.  We support the arrow keys and various other shortcuts for moving. This method hides that
+  # complexity.
   actionFromKeyEvent: (event) ->
     key = KeyboardUtils.getKeyChar(event)
     if (KeyboardUtils.isEscape(event))
@@ -245,7 +242,7 @@
         if @suppressedLeadingKeyword?
           # This is a custom search engine completion.  Because of the way we add and highlight the text
           # common to all completions in the input (selectCommonMatches), the text in the input might not
-          # correspond to any of the completions.  So we fire the query off to the background page and use the
+          # correspond to any of the completions.  So we fire off the query to the background page and use the
           # completion at the top of the list (which will be the right one).
           @update true, =>
             if @completions[0]
@@ -279,8 +276,9 @@
   onKeypress: (event) =>
     if @inputContainsASelectionRange()
       # As the user types characters which match a highlighted completion suggestion (in the text input), we
-      # suppress the keyboard event and "simulate" it by advancing the start of the highlighted selection.  We
-      # do this so that the selection doesn't flicker as the user types.
+      # suppress the keyboard event and "simulate" it by advancing the start of the highlighted selection (but
+      # only if the typed character matches).  This avoids flicker as the selection is first collapsed then
+      # replaced.
       if @input.value[@input.selectionStart][0].toLowerCase() == (String.fromCharCode event.charCode).toLowerCase()
         console.log "extend selection:", @getInputWithoutSelectionRange()
         @input.setSelectionRange @input.selectionStart + 1, @input.selectionEnd
@@ -293,7 +291,7 @@
   inputContainsASelectionRange: ->
     @input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd

-  # Return the text of the input, with any selected text renage removed.
+  # Return the text of the input, with any selected text removed.
   getInputWithoutSelectionRange: ->
     if @inputContainsASelectionRange()
       @input.value[0...@input.selectionStart] + @input.value[@input.selectionEnd..]
@@ -301,7 +299,7 @@
       @input.value

   # Return the background-page query corresponding to the current input state.  In other words, reinstate any
-  # custom search engine keyword which is currently stripped from the input.
+  # search engine keyword which is currently stripped from the input, and strip any selection.
   getInputValueAsQuery: ->
     (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @getInputWithoutSelectionRange()

@@ -337,15 +335,15 @@
     1 < queryTerms.length and queryTerms[0] in @keywords

   update: (updateSynchronously = false, callback = null) =>
-    # If the query text becomes a custom search, then we need to force a synchronous update (so that the
-    # interface is snappy).
+    # If the query text becomes a custom search (the user enters a search keyword), then we need to force a
+    # synchronous update (so that state is updated immediately).
     updateSynchronously ||= @isCustomSearch() and not @suppressedLeadingKeyword?
     if updateSynchronously
       @clearUpdateTimer()
       @updateCompletions callback
     else if not @updateTimer?
       # Update asynchronously for better user experience and to take some load off the CPU (not every
-      # keystroke will cause a dedicated update)
+      # keystroke will cause a dedicated update).
       @updateTimer = Utils.setTimeout @refreshInterval, =>
         @updateTimer = null
         @updateCompletions callback
@@ -376,14 +374,10 @@
 class BackgroundCompleter
   debug: true

-  # name is background-page completer to connect to: "omni", "tabs", or "bookmarks".
+  # The "name" is the background-page completer to connect to: "omni", "tabs", or "bookmarks".
   constructor: (@name) ->
     @port = chrome.runtime.connect name: "completions"
     @messageId = null
-    # @keywords and @cache are both reset in @reset().
-    # We only cache for the duration of a single vomnibar activation.
-    @keywords = []
-    @cache = {}
     @reset()

     @port.onMessage.addListener (msg) =>
@@ -393,7 +387,7 @@
           @lastUI.setKeywords @keywords
         when "completions"
           # The result objects coming from the background page will be of the form:
-          #   { html: "", type: "", url: "" }
+          #   { html: "", type: "", url: "", ... }
           # Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description.
           for result in msg.results
             result.performAction =
@@ -402,8 +396,8 @@
               else
                 @completionActions.navigateToUrl.curry result.url

-          # Cache the result -- if we have been told it's ok to do so (it could be that more results will be
-          # posted shortly).  We cache the result even if it arrives late.
+          # Cache the results, but only if we have been told it's ok to do so (it could be that more results
+          # will be posted shortly).  We cache the results even if they arrive late.
           if msg.mayCacheResults
             console.log "cache set:", "-#{msg.cacheKey}-" if @debug
             @cache[msg.cacheKey] = msg
@@ -416,7 +410,7 @@
   filter: (query, @mostRecentCallback) ->
     queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length
     cacheKey = queryTerms.join " "
-    cacheKey += " " if 0 < queryTerms.length and queryTerms[0] in @keywords and /\s$/.test query
+    cacheKey += " " if queryTerms.length == 1 and queryTerms[0] in @keywords and /\s$/.test query

     if cacheKey of @cache
       console.log "cache hit:", "-#{cacheKey}-" if @debug
@@ -432,17 +426,15 @@
         cacheKey: cacheKey

   reset: ->
-    @keywords = []
-    @cache = {}
+    [ @keywords, @cache ] = [ [], {} ]

   refresh: (@lastUI) ->
     @reset()
-    # Inform the background completer that we have a new vomnibar activation.
     @port.postMessage name: @name, handler: "refresh"

   cancel: ->
     # Inform the background completer that it may (should it choose to do so) abandon any pending query
-    # (because the user is typing, and there'll be another query along soon).
+    # (because the user is typing, and there will be another query along soon).
     @port.postMessage name: @name, handler: "cancel"

   # These are the actions we can perform when the user selects a result.


修改一处 bug

修改一处 bug

--- tools/render.coffee
+++ tools/render.coffee
@@ -13,7 +13,7 @@
 all = undefined

 #递归创建页面方法
-forPage = (callback) ->
+forPage = () ->
   views = path.join(__dirname, '../views')
   dirList = fs.readdirSync views
   dirList.forEach (item) ->
@@ -21,8 +21,7 @@
       if item.indexOf('.jade') isnt -1
         pages[item.split('.')[0]] = item
     return
-  callback() if callback
-  console.log pages
+  compileRenderService pages

 # res发送html字符串
@@ -30,7 +29,7 @@
   res.end html

 # 通过map实现每个页面渲染func的生成
-compileRenderService = ->
+compileRenderService = (pages)->
   done = 0
   all = 0
   for name of pages
@@ -51,9 +50,8 @@
         sendPage res, html
         return
       done++
-      console.log '\tre compile jade views' if done >= all
+      console.log '\n* re compile jade views' if done >= all

-console.log '\tre compile jade views'
-forPage compileRenderService()
+forPage()

 module.exports = render


fix(app generator): fix i18n support

fix(app generator): fix i18n support

--- generators/app/templates/src/_router.coffee
+++ generators/app/templates/src/_router.coffee
@@ -3,17 +3,28 @@
         module.exports = factory(
             require( 'backbone' )
             require( 'jquery' )
-            require( 'madlib-console' )
+            require( 'madlib-console' )<% if ( i18n ) { %>
+            require( 'madlib-locale' )<% } %>
+
             require( './views/index.coffee' )
         )
     else if typeof define is 'function' and define.amd
         define( [
             'backbone'
             'jquery'
-            'madlib-console'
+            'madlib-console'<% if ( i18n ) { %>
+            'madlib-locale'<% } %>
+
             './views/index.coffee'
         ], factory )
-)( ( Backbone, $, console, Views... ) ->
+)((
+    Backbone
+    $
+    console<% if ( i18n ) { %>
+    localeManager<% } %>
+
+    Views...
+) ->

     class AppRouter extends Backbone.Router


switch on zIndex, hone pixelOffset

switch on zIndex, hone pixelOffset

--- app/assets/javascripts/google_map/marker_builder.js.coffee
+++ app/assets/javascripts/google_map/marker_builder.js.coffee
@@ -30,5 +30,8 @@
     infowindow

   infobox: (boxText)->
+    offset = switch @serviceObject.zIndex
+      when 1 then new google.maps.Size(-151, 10)
+      when -1 then new google.maps.Size(-151, 4)
     content: boxText
-    pixelOffset: new google.maps.Size(-150, 10)
+    pixelOffset: offset


Compatibility with IE

Compatibility with IE

--- app/assets/javascripts/lib/mobile/autocomplete_2.js.coffee
+++ app/assets/javascripts/lib/mobile/autocomplete_2.js.coffee
@@ -66,9 +66,9 @@
     _init: (args) ->
       @config = @_updateConfig args
       @el = document.getElementById @config.id
+      @xhr = @_setupXHR()
       @_addEventHandlers()
       @results = @_buildResults()
-      @xhr = @_setupXHR()

     _updateConfig: (args) ->
       newConfig = {}
@@ -76,16 +76,23 @@
       newConfig[key] = value for own key, value of args
       newConfig

+    _on: (elt, evt, callback) ->
+      if window.addEventListener
+        elt.addEventListener evt, callback, false
+      else if window.attachEvent
+        elt.attachEvent "on#{evt}", callback
+
     _addEventHandlers: ->
-      @el.addEventListener 'input', (e) =>
+      @_on(@el, 'input', (e) =>
         @_searchFor e.currentTarget.value
-
-      @el.addEventListener 'keypress', (e) =>
+      )
+      @_on(@el, 'keydown', (e) =>
         @_keypressHandler e
+      )

     _setupXHR: ->
       xhr = new XMLHttpRequest()
-      xhr.addEventListener 'readystatechange', =>
+      xhr.onreadystatechange = =>
         if @xhr.readyState == 4
           if @xhr.status == 200
             @_populateResults JSON.parse(@xhr.responseText), @currentSearch
@@ -96,18 +103,17 @@
       @_addClass results, @config.resultsClass
       @_addClass results, "#{@config.resultsClass}--#{@config.classModifier}" if @config.classModifier

-      results.addEventListener 'click', (e) =>
+      @_on(results, 'click', (e) =>
         @_resultsClick e
-      , false
-
-      results.addEventListener 'mouseover', (e) =>
+      , false)
+
+      @_on(results, 'mouseover', (e) =>
         @_resultsMouseOver e.target
-      , false
-
-      results.addEventListener 'mouseout', (e) =>
+      , false)
+
+      @_on(results, 'mouseout', (e) =>
         @_resultsMouseOut e.target
-      , false
-
+      , false)
       results

     # event handlers


Removed ScriptComponentHandler, we leave that for later #184

--- src/goo/loaders/handlers/ScriptComponentHandler.coffee
+++ src/goo/loaders/handlers/ScriptComponentHandler.coffee
@@ -1 +0,0 @@
-S


SocialWorker: Tests: use availability argument in tests

--- workers/social/lib/social/models/group/index.test.coffee
+++ workers/social/lib/social/models/group/index.test.coffee
@@ -447,15 +447,14 @@
           ->
             # expecting isApiTokenEnabled field to be empty before request
             expect(group.isApiTokenEnabled).to.be.empty
-            options = { isApiTokenEnabled : true }
-            group.setApiTokenAvailability$ client, options, (err) ->
+            group.setApiTokenAvailability$ client, false, (err) ->
               expect(err?.message).to.not.exist
               queue.next()

           ->
             JGroup.one { slug : group.slug }, (err, group_) ->
               expect(err).to.not.exist
-              expect(group_.isApiTokenEnabled).to.be.truthy
+              expect(group_.isApiTokenEnabled).to.be.falsy
               queue.next()

           -> done()
@@ -472,8 +471,8 @@
       options = { createGroup : yes, context : { group : generateRandomString() } }
       withConvertedUser options, ({ group }) ->

-        group.setApiTokenAvailability {}, (err) ->
-          expect(err?.message).to.be.equal 'isApiTokenEnabled is a required field'
+        group.setApiTokenAvailability null, (err) ->
+          expect(err?.message).to.be.equal 'availability must be set'
           done()

@@ -487,8 +486,7 @@
           ->
             # expecting isApiTokenEnabled field to be empty before request
             expect(group.isApiTokenEnabled).to.be.empty
-            options = { isApiTokenEnabled : true }
-            group.setApiTokenAvailability options, (err) ->
+            group.setApiTokenAvailability true, (err) ->
               expect(err).to.not.exist
               queue.next()


Removed orphan file

Removed orphan file

--- packages/rocketchat-highlight/highlight.coffee
+++ packages/rocketchat-highlight/highlight.coffee
@@ -1,65 +0,0 @@
-###
-# Highlight is a named function that will highlight ``` messages
-# @param {Object} message - The message object
-###
-
-class Highlight
-
-	constructor: (message) ->
-
-		if s.trim message.html
-			message.tokens ?= []
-
-			# Count occurencies of ```
-			count = (message.html.match(/```/g) || []).length
-
-			if count
-
-				# Check if we need to add a final ```
-				if (count % 2 > 0)
-					message.html = message.html + "\n```"
-					message.msg = message.msg + "\n```"
-
-				# Separate text in code blocks and non code blocks
-				msgParts = message.html.split(/^\s*(```(?:[a-zA-Z]+)?(?:(?:.|\n)*?)```)(?:\n)?$/gm)
-
-				for part, index in msgParts
-					# Verify if this part is code
-					codeMatch = part.match(/^```(\w*[\n\ ]?)([\s\S]*?)```+?$/)
-					if codeMatch?
-						# Process highlight if this part is code
-						singleLine = codeMatch[0].indexOf('\n') is -1
-
-						if singleLine
-							lang = ''
-							code = _.unescapeHTML codeMatch[1] + codeMatch[2]
-						else
-							lang = codeMatch[1]
-							code = _.unescapeHTML codeMatch[2]
-
-						if s.trim(lang) is ''
-							lang = ''
-
-						if s.trim(lang) not in hljs.listLanguages()
-							result = hljs.highlightAuto (lang + code)
-						else
-							result = hljs.highlight s.trim(lang), code
-
-						token = "=&=#{Random.id()}=&="
-
-						message.tokens.push
-							highlight: true
-							token: token
-							text: "
```<br></span>" + result.value + "<span class='copyonly'><br>```
"
-
-						msgParts[index] = token
-					else
-						msgParts[index] = part
-
-				# Re-mount message
-				message.html = msgParts.join('')
-
-		return message
-
-RocketChat.callbacks.add 'renderMessage', Highlight, RocketChat.callbacks.priority.HIGH, 'highlight'
-RocketChat.Highlight = true


fix #15.

fix #15.

--- grails-app/assets/javascripts/chat.coffee
+++ grails-app/assets/javascripts/chat.coffee
@@ -21,7 +21,7 @@

     $('#area00').append """

-

+

             #{selectedDate}
@@ -39,6 +39,7 @@

     stompClient.send "/app/log", {}, JSON.stringify(message)
     $('#chatMessage').focus()
+    $('#area00').scrollTop(($("#area00")[0].scrollHeight))

 # update Date, Time
@@ -166,7 +167,7 @@
     else if msg.status is 'closeTime'
         selectedDate = $('#datetimepickerInline').data('DateTimePicker').date().format('YYYY-MM-DD')
         $('#area00').append """
-

+

                 #{selectedDate}


fix(tests): Add bigger timeout when deleting old productTypes during tests

--- src/spec/integration/product-type-importer.spec.coffee
+++ src/spec/integration/product-type-importer.spec.coffee
@@ -8,7 +8,7 @@
 errMissingCredentials = 'Missing configuration in env variable named SPHERE_PROJECT_KEY'

 argv =
-  projectKey: process.env.SPHERE_PROJECT_KEY
+  projectKey: process.env.SPHERE_PROJECT_KEY || "producttype-json-generator-tests"
   logSilent: true

 testProductType = {
@@ -50,6 +50,7 @@
   importer = null
   sphereClient = null

+  console.log "Using project %s for testing", argv.projectKey
   before ->
     expect(argv.projectKey).to.be.a 'string', errMissingCredentials

@@ -71,17 +72,21 @@
       sphereClient = new SphereClient options

   beforeEach ->
+    # increase timeout so we will have time to delete all previous product types
+    this.timeout 1000000
+
     sphereClient.productTypes
+    .perPage(50)
     .process (res) ->
       console.log "Deleting old product types", res.body.results.length
       Promise.map res.body.results, (productType) ->
         sphereClient.productTypes.byId(productType.id)
         .delete(productType.version)
-      , concurrency: 5
-      .then ->
-        console.log "Product types were deleted"
-      .catch (err) ->
-        console.error "There was an error while deleting product types", err
+      , concurrency: 10
+    .then ->
+      console.log "Product types were deleted"
+    .catch (err) ->
+      console.error "There was an error while deleting product types", err

   it 'should import product type', ->
     sphereClient.productTypes.fetch()


Updating port resource snippet

Updating port resource snippet

--- snippets/port.cson
+++ snippets/port.cson
@@ -1,4 +1,4 @@
 '.source.inspec':
   'Describe port':
     'prefix': 'port'
-    'body': "# http://inspec.io/docs/reference/resources/port/\ndescribe port(${1:number}) do\n  it { should${2:_not} be_listening }\n  its('protocols') { should{3:_not} include('${4:protocol}') }${5}\nend"
+    'body': "# http://inspec.io/docs/reference/resources/port/\ndescribe port(${1:number}) do\n  it { should${2:_not} be_listening }\n  its('protocols') { should${3:_not} include('${4:protocol}') }${5}\nend"


rules field private

rules field private

--- server/collection-security.coffee
+++ server/collection-security.coffee
@@ -28,7 +28,7 @@

   constructor: (@collection) ->
     @id = @collection._name
-    @rules = {}
+    @_rules = {}

     @collection.allow
       insert: @getSecFunction 'allow', 'insert'
@@ -48,7 +48,7 @@
       fields.push key for key, value of doc if fields.length is 0

       values = []
-      for field, fieldRule of self.rules when field in fields
+      for field, fieldRule of self._rules when field in fields
         if typeof fieldRule[scope] is 'object'
           rule = fieldRule[scope][type]
         else
@@ -66,7 +66,7 @@
     (selector, options) ->
       options = options or {}
       options.fields = options.fields or {}
-      for field, fieldRule of self.rules when fieldRule['visible']?
+      for field, fieldRule of self._rules when fieldRule['visible']?
         rule = fieldRule['visible']
         if typeof rule is 'function'
           value = rule.apply this, arguments
@@ -87,7 +87,7 @@

   attachRules: (rules) ->
-    @rules = _.extend @rules, rules
+    @_rules = _.extend @_rules, rules

   attachCRUD: (rules) ->
     prepared = {}


Updated news

Updated news

--- server/materials/data/news.coffee
+++ server/materials/data/news.coffee
@@ -3,9 +3,8 @@

 export default allNews = () ->
     return news([
-        newsItem("Зимние «сборы» (для региона и не только)", String.raw"""
-
Я хочу в зимние каникулы (ориентировочно с 5 по 10-12 января) провести дистанционные «сборы», в первую очередь в качестве подготовки к региональному этапу
-            (для тех, кто туда прошел), но и вообще для всех желающих, у кого уровень 2 и выше. Если вы хотите участвовать в этих сборах, напишите мне.

+        newsItem("13 января занятие в лицее 40 будет", String.raw"""
+
13 января занятие в лицее 40 будет, и скорее всего занятия будут и дальше по средам, пока все классы учатся очно.

         """),
         newsItem("Про областную (региональную) олимпиаду", String.raw"""

Добавлена информация про областную (региональную) олимпиаду.


Remove debug line.

Remove debug line.

--- services/web/public/coffee/ide.coffee
+++ services/web/public/coffee/ide.coffee
@@ -193,7 +193,6 @@
 				chromeVersion? &&
 				(chromeVersion == 61 || chromeVersion == 62)
 			)
-			console.log chromeVersion, browserIsChrome61or62
 			if browserIsChrome61or62
 				document.styleSheets[0].insertRule(".ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; }", 1)
 		catch err


Support custom message

Support custom message

--- i18n/Steps.en.coffee
+++ i18n/Steps.en.coffee
@@ -18,7 +18,9 @@
           waiting: "Will load Bitly links after finishing preceding tasks"
           connecting: "Connecting to Bitly API"
           loading_simple: "Loading Bitly links, {{current}} so far"
+          loading_simple_message: "Loading Bitly links, {{current}} so far ({{message}})"
           loading_complex: "Loading Bitly links, {{current}} of {{total}} so far"
+          loading_complex_message: "Loading Bitly links, {{current}} of {{total}} so far ({{message}})"
           loaded_simple: "Loaded Bitly links, {{current}} total"
           loaded_complex: "Loaded Bitly links, {{current}} of {{total}} total"
           skipped: "Skipped loading Bitly links"


Response fixes

Response fixes

-echo back the new factiod when its created
-don't respond to the sender on a tell command

--- src/scripts/factoid.coffee
+++ src/scripts/factoid.coffee
@@ -15,7 +15,7 @@
   add: (key, val) ->
     @cache[key] = val
     @robot.brain.data.factoids = @cache
-    "OK."
+    "OK. #{key} is #{val} "

   get: (key) ->
     @cache[key] or "No factoid for #{key}"
@@ -39,4 +39,7 @@
   factoids = new Factoids robot

   robot.hear /^~(.+)/i, (msg) ->
-    msg.reply factoids.handleFactoid msg.message.text
+    if match = (/^~tell (.+) about (.+)/i) or (/^~~(.+) (.+)/)
+      msg.send factoids.handleFactoid msg.message.text
+    else
+      msg.reply factoids.handleFactoid msg.message.text


fix user data removal due to premature load

--- core/plugmixer.coffee
+++ core/plugmixer.coffee
@@ -25,7 +25,6 @@
     console.log 'Plugmixer.initialize'
     Listener.initializeWindowMessage()
     User.initialize()
-    Listener.initializeAPI()

     if TRACKING_CODE?
       ga 'create', TRACKING_CODE, 'auto', name: 'plugmixer'
@@ -52,6 +51,7 @@
       @lastPlayedIn = response.lastPlayedIn || @lastPlayedIn

       Selections.initialize()
+      Listener.initializeAPI()

     @save: ->
       data = {}
@@ -128,6 +128,7 @@
       @save()

     @changedTo: (newRoom) ->
+      return if !@id?
       @id = newRoom.id
       Storage.load 'room', idToUse(User.lastPlayedIn)

@@ -156,7 +157,6 @@
           switch data.type
             when 'user' # Should only happen once.
               User.update data.response
-              Room.initialize()
             when 'room' then Room.update data.response # Should only happen once.
             when 'selections' then Selections.update data.response

@@ -177,6 +177,8 @@

       API.on API.PLAYLIST_ACTIVATE, (playlist) ->
         API.chatLog "Next playing from #{playlist.name}"
+
+      Room.initialize()

   ###


rm this mismerged old-style http call

rm this mismerged old-style http call

--- src/scripts/wordnik.coffee
+++ src/scripts/wordnik.coffee
@@ -48,9 +48,6 @@
     word = msg.match[1]

     fetch_wordnik_resource(msg, word, '', {includeSuggestions: 'true'}) (err, res, body) ->
-      #msg.http("http://api.wordnik.com/v4/word.json/#{escape(word)}?includeSuggestions=true")
-      #  .header('api_key', process.env.WORDNIK_API_KEY)
-      #  .get() (err, res, body) ->
       wordinfo = JSON.parse(body)
       if wordinfo.canonicalForm
         msg.send "\"#{word}\" is a word."


Registration: when user log-in re-draw filetree

--- client/finder/filetree/controllers/findercontroller.coffee
+++ client/finder/filetree/controllers/findercontroller.coffee
@@ -28,6 +28,11 @@
     @appStorage = KD.getSingleton('appStorageController').storage 'Finder', '1.1.1'

     @watchers = {}
+
+    # this is here for when user login/register after opening Ace App
+    # to refresh filetree
+    mainController = KD.getSingleton("mainController")
+    mainController.on "accountChanged.to.loggedIn", @bound 'reset'

     if options.useStorage
       @appStorage.ready =>


Set image properties when inserting an image.

--- app/assets/javascripts/cmsimple/panels/images/list.coffee
+++ app/assets/javascripts/cmsimple/panels/images/list.coffee
@@ -26,7 +26,13 @@

   triggerImage: (e)->
     img = $(e.target)
-    @trigger 'image:selected', src: img.data('full-url'), 'image-id': img.data('id')
+    image = CMSimple.Image.find(img.data('id'))
+    @trigger 'image:selected',
+        src: img.data('full-url'),
+        alt: image.title,
+        width: image.width,
+        height: image.height,
+        'image-id': image.id

   deleteImage: (e)->
     e.preventDefault()


#1397 Test Cube.get[OrCreate]Bucket()

#1397 Test Cube.get[OrCreate]Bucket()

--- app/assets/javascripts/test/model/binary/cube.spec.coffee
+++ app/assets/javascripts/test/model/binary/cube.spec.coffee
@@ -30,6 +30,30 @@
     cube.initializeWithQueues(pullQueue, pushQueue)

+  describe "GetBucket", ->
+
+    it "should return a NullBucket on getBucket()", ->
+
+      bucket = cube.getBucket([0, 0, 0, 0])
+      expect(bucket.isNullBucket).toBe(true)
+      expect(cube.bucketCount).toBe(0)
+
+    it "should create a new bucket on getOrCreateBucket()", ->
+
+      expect(cube.bucketCount).toBe(0)
+
+      bucket = cube.getOrCreateBucket([0, 0, 0, 0])
+      expect(bucket.isNullBucket).toBe(undefined)
+      expect(cube.bucketCount).toBe(1)
+
+    it "should only create one bucket on getOrCreateBucket()", ->
+
+      bucket1 = cube.getOrCreateBucket([0, 0, 0, 0])
+      bucket2 = cube.getOrCreateBucket([0, 0, 0, 0])
+      expect(bucket1).toBe(bucket2)
+      expect(cube.bucketCount).toBe(1)
+
+
   describe "Volume Annotation Handling", ->

     describe "Voxel Labeling", ->
@@ -144,39 +168,39 @@
         expect(cube.getDataValue([0, 0, 0], mapping)).toBe(1)
         expect(cube.getDataValue([1, 1, 1], mapping)).toBe(43)

-    describe "Garbage Collection", ->
-
-      beforeEach ->
-
-        Cube::MAXIMUM_BUCKET_COUNT = 3
-
-      it "should only keep 3 buckets", ->
-
-        cube.getOrCreateBucket([0, 0, 0, 0])
-        cube.getOrCreateBucket([1, 1, 1, 0])
-        cube.getOrCreateBucket([2, 2, 2, 0])
-        cube.getOrCreateBucket([3, 3, 3, 0])
-
-        expect(cube.bucketCount).toBe(3)
-
-      it "should not collect buckets with shouldCollect() == false", ->
-
-        b1 = cube.getOrCreateBucket([0, 0, 0, 0])
-        b1.pull()
-        b2 = cube.getOrCreateBucket([1, 1, 1, 0])
-        b3 = cube.getOrCreateBucket([2, 2, 2, 0])
-        b4 = cube.getOrCreateBucket([3, 3, 3, 0])
-
-        expect(b1.shouldCollect()).toBe(false)
-
-        addresses = cube.buckets.map((b) -> b.zoomedAddress)
-        expect(addresses).toEqual([[0, 0, 0, 0], [3, 3, 3, 0], [2, 2, 2, 0]])
-
-      it "should throw an exception if no bucket is collectable", ->
-
-        cube.getOrCreateBucket([0, 0, 0, 0]).pull()
-        cube.getOrCreateBucket([1, 1, 1, 0]).pull()
-        cube.getOrCreateBucket([2, 2, 2, 0]).pull()
-
-        expect(-> cube.getOrCreateBucket([3, 3, 3, 0])).toThrow()
-
+  describe "Garbage Collection", ->
+
+    beforeEach ->
+
+      Cube::MAXIMUM_BUCKET_COUNT = 3
+
+    it "should only keep 3 buckets", ->
+
+      cube.getOrCreateBucket([0, 0, 0, 0])
+      cube.getOrCreateBucket([1, 1, 1, 0])
+      cube.getOrCreateBucket([2, 2, 2, 0])
+      cube.getOrCreateBucket([3, 3, 3, 0])
+
+      expect(cube.bucketCount).toBe(3)
+
+    it "should not collect buckets with shouldCollect() == false", ->
+
+      b1 = cube.getOrCreateBucket([0, 0, 0, 0])
+      b1.pull()
+      b2 = cube.getOrCreateBucket([1, 1, 1, 0])
+      b3 = cube.getOrCreateBucket([2, 2, 2, 0])
+      b4 = cube.getOrCreateBucket([3, 3, 3, 0])
+
+      expect(b1.shouldCollect()).toBe(false)
+
+      addresses = cube.buckets.map((b) -> b.zoomedAddress)
+      expect(addresses).toEqual([[0, 0, 0, 0], [3, 3, 3, 0], [2, 2, 2, 0]])
+
+    it "should throw an exception if no bucket is collectable", ->
+
+      cube.getOrCreateBucket([0, 0, 0, 0]).pull()
+      cube.getOrCreateBucket([1, 1, 1, 0]).pull()
+      cube.getOrCreateBucket([2, 2, 2, 0]).pull()
+
+      expect(-> cube.getOrCreateBucket([3, 3, 3, 0])).toThrow()
+


do not show dialog after direct file selection if `preview-step` turned off

--- app/assets/javascripts/uploadcare/widget/widget.js.coffee
+++ app/assets/javascripts/uploadcare/widget/widget.js.coffee
@@ -130,7 +130,10 @@

     __openDialogWithFile: (type, data) =>
       file = uploadcare.fileFrom(type, data, @settings)
-      uploadcare.openDialog(file, @settings).done(@__setFile)
+      if @settings.previewStep
+        uploadcare.openDialog(file, @settings).done(@__setFile)
+      else
+        @__setFile file

     openDialog: (tab) ->
       uploadcare.openDialog(@currentFile, tab, @settings)


Add new controller to application.js so it's loaded / compiled by sprockets.

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -18,6 +18,7 @@
   'controllers/search_controller'
   'controllers/session_controller'
   'controllers/user_controller'
+  'controllers/about_controller'
 ], ($, _bootstrap, JoatuApplication, routes, I18n) ->
   'use strict'


Fix underlying issue with emoji reactions

Issue was: blank space was rendering
as a element in an array of authors.
Element was being used by `join`.
Original fix !2450 was trying to remove the space after it happened.
This checks properly for it and only moves forward if it does not exist.
Also removes "me" upon unchecking emoji.

--- app/assets/javascripts/awards_handler.coffee
+++ app/assets/javascripts/awards_handler.coffee
@@ -44,8 +44,7 @@
   decrementCounter: (emoji) ->
     counter = @findEmojiIcon(emoji).siblings(".counter")
     emojiIcon = counter.parent()
-
-    if parseInt(counter.text()) > 1
+    if parseInt(counter.text()) > 0
       counter.text(parseInt(counter.text()) - 1)
       emojiIcon.removeClass("active")
       @removeMeFromAuthorList(emoji)
@@ -60,16 +59,19 @@
   removeMeFromAuthorList: (emoji) ->
     award_block = @findEmojiIcon(emoji).parent()
     authors = award_block.attr("data-original-title").split(", ")
-    authors = _.without(authors, "me").join(", ")
-    award_block.attr("title", authors)
+    if authors.indexOf("me") != -1
+      authors.splice(authors.indexOf("me"),1)
+    award_block.closest(".award").attr("data-original-title", authors.join(", "))
     @resetTooltip(award_block)

   addMeToAuthorList: (emoji) ->
     award_block = @findEmojiIcon(emoji).parent()
-    authors = award_block.attr("data-original-title").trim().split(", ")
+    origTitle = award_block.attr("data-original-title").trim()
+    authors = []
+    if origTitle
+      authors = origTitle.split(', ')
     if authors.indexOf("me") == -1
       authors.push("me")
-    console.log('authors');
     award_block.attr("title", authors.join(", "))
     @resetTooltip(award_block)


Validating terms acceptance

Validating terms acceptance

--- app/assets/javascripts/initiatives.js.coffee
+++ app/assets/javascripts/initiatives.js.coffee
@@ -8,59 +8,66 @@
     form = $('#pay_form')
     submit = form.find('[type=submit]')
     status = form.find('.status')
+    terms = form.find('#terms')
     status.removeClass 'success'
     status.removeClass 'failure'
-    status.html("
Enviando dados de pagamento para o Moip...

")
-    token = form.data('token')
-    plan_code = form.data('plan')
-    submit.hide()
-    status.show()
-    if MoipAssinaturas?
-      moip = new MoipAssinaturas(token)
-      moip.callback (response) ->
-        status.find('h4').html("#{response.message} (Moip)")
-        unless response.has_errors()
-          unless billing_info_ok
-            billing_info_ok = true
-            subscription = new Subscription()
-            subscription.with_code(form.data('subscription'))
-            subscription.with_customer(customer)
-            subscription.with_plan_code(plan_code)
-            moip.subscribe(subscription)
+    status.find('ul').html('')
+    unless terms.is(':checked')
+      status.addClass 'failure'
+      status.html("
Você precisa aceitar os termos de uso para continuar.
")
+      status.show()
+    else
+      status.html("
Enviando dados de pagamento para o Moip...

")
+      token = form.data('token')
+      plan_code = form.data('plan')
+      submit.hide()
+      status.show()
+      if MoipAssinaturas?
+        moip = new MoipAssinaturas(token)
+        moip.callback (response) ->
+          status.find('h4').html("#{response.message} (Moip)")
+          unless response.has_errors()
+            unless billing_info_ok
+              billing_info_ok = true
+              subscription = new Subscription()
+              subscription.with_code(form.data('subscription'))
+              subscription.with_customer(customer)
+              subscription.with_plan_code(plan_code)
+              moip.subscribe(subscription)
+            else
+              next_invoice = "#{response.next_invoice_date.day}/#{response.next_invoice_date.month}/#{response.next_invoice_date.year}"
+              $.ajax
+                url: form.data('activate'),
+                type: 'PUT',
+                dataType: 'json',
+                success: (response) ->
+                  status.addClass 'success'
+                  status.find('ul').append("
Seu código de assinatura é #{form.data('subscription')}
")
+                  status.find('ul').append("
Seu pagamento ainda será processado pelo Moip
")
+                  status.find('ul').append("
Se você quiser suspender seu apoio, basta acessar o menu Unlocks apoiados a qualquer momento.
")
+                  status.find('ul').append("
Sua próxima cobrança será realizada em #{next_invoice}.
")
+                  form.find('input, label').hide()
+                error: (response) ->
+                  status.find('h4').html("Não foi possível ativar sua assinatura")
+                  status.addClass 'failure'
+                  for error in response.responseJSON.errors
+                    status.find('ul').append("
#{error}
")
+                  submit.show()
           else
-            next_invoice = "#{response.next_invoice_date.day}/#{response.next_invoice_date.month}/#{response.next_invoice_date.year}"
-            $.ajax
-              url: form.data('activate'),
-              type: 'PUT',
-              dataType: 'json',
-              success: (response) ->
-                status.addClass 'success'
-                status.find('ul').append("
Seu código de assinatura é #{form.data('subscription')}
")
-                status.find('ul').append("
Seu pagamento ainda será processado pelo Moip
")
-                status.find('ul').append("
Se você quiser suspender seu apoio, basta acessar o menu Unlocks apoiados a qualquer momento.
")
-                status.find('ul').append("
Sua próxima cobrança será realizada em #{next_invoice}.
")
-                form.find('input, label').hide()
-              error: (response) ->
-                status.find('h4').html("Não foi possível ativar sua assinatura")
-                status.addClass 'failure'
-                for error in response.responseJSON.errors
-                  status.find('ul').append("
#{error}
")
-                submit.show()
-        else
-          status.addClass 'failure'
-          for error in response.errors
-            status.find('ul').append("
#{error.description}
")
-          submit.show()
-      billing_info =
-        fullname: $("#holder_name").val(),
-        expiration_month: $("#expiration_month").val(),
-        expiration_year: $("#expiration_year").val(),
-        credit_card_number: $("#number").val()
-      customer = new Customer()
-      customer.code = form.data('customer')
-      customer.billing_info = new BillingInfo(billing_info)
-      moip.update_credit_card(customer)
-    else
-      status.addClass 'failure'
-      status.find('h4').html("Erro ao carregar o Moip Assinaturas. Por favor, recarregue a página e tente novamente.")
-      submit.show()
+            status.addClass 'failure'
+            for error in response.errors
+              status.find('ul').append("
#{error.description}
")
+            submit.show()
+        billing_info =
+          fullname: $("#holder_name").val(),
+          expiration_month: $("#expiration_month").val(),
+          expiration_year: $("#expiration_year").val(),
+          credit_card_number: $("#number").val()
+        customer = new Customer()
+        customer.code = form.data('customer')
+        customer.billing_info = new BillingInfo(billing_info)
+        moip.update_credit_card(customer)
+      else
+        status.addClass 'failure'
+        status.find('h4').html("Erro ao carregar o Moip Assinaturas. Por favor, recarregue a página e tente novamente.")
+        submit.show()


fix media track/caption XSS vulnerability, fixes CNVS-17659

ensure locale and other fields are properly sanitized when building
<track /> tags

test plan: see repro steps on CNVS-17659

Change-Id: I8016d05ce12854166ed467f06a2769c5f1bfa979
Reviewed-on: https://gerrit.instructure.com/46288
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Cody Cutrer <c4fe2b1d90ef8f2548c8aeabfa633434316f0305@instructure.com>
QA-Review: Caleb Guanzon <54fa1f30d451e4f3c341a8f3c47c9215fd0be9c6@instructure.com>
Product-Review: Jon Jensen <44f878afe53efc66b76772bd845eb65944ed8232@instructure.com>

--- app/coffeescripts/jquery/mediaComment.coffee
+++ app/coffeescripts/jquery/mediaComment.coffee
@@ -6,7 +6,8 @@
   'vendor/mediaelement-and-player'
   'jquery'
   'compiled/util/kalturaAnalytics'
-], (I18n, _, pubsub, mejs, $, kalturaAnalytics) ->
+  'str/htmlEscape'
+], (I18n, _, pubsub, mejs, $, kalturaAnalytics, htmlEscape) ->

   VIDEO_WIDTH = 550
   VIDEO_HEIGHT = 448
@@ -47,11 +48,11 @@
       # this 'when ...' is because right now in canvas, none of the mp3 urls actually work.
       # see: CNVS-12998
       sources = for source in data.media_sources when source.content_type isnt 'audio/mp3'
-        "<source type='#{source.content_type}' src='#{source.url}' title='#{source.width}x#{source.height} #{Math.floor(source.bitrate / 1024)} kbps' />"
+        "<source type='#{htmlEscape source.content_type}' src='#{htmlEscape source.url}' title='#{htmlEscape source.width}x#{htmlEscape source.height} #{htmlEscape(Math.floor(source.bitrate / 1024))} kbps' />"

       tracks = _.map data.media_tracks, (track) ->
         languageName = mejs.language.codes[track.locale] || track.locale
-        "<track kind='#{track.kind}' label='#{languageName}' src='#{track.url}' srclang='#{track.locale}' />"
+        "<track kind='#{htmlEscape track.kind}' label='#{htmlEscape languageName}' src='#{htmlEscape track.url}' srclang='#{htmlEscape track.locale}' />"

       types = _.map data.media_sources, (source) -> source.content_type
       dfd.resolve {sources, tracks, types, can_add_captions: data.can_add_captions}
@@ -62,7 +63,8 @@
     st_tags = sourcesAndTracks.sources.concat(sourcesAndTracks.tracks).join('')

     getElement = (tagType) ->
-      $("<#{tagType} #{if mediaType is 'video' then "width='#{width}' height='#{height}'" else ''} controls>#{st_tags}</#{tagType}>")
+      html = "<#{tagType} #{if mediaType is 'video' then "width='#{width}' height='#{height}'" else ''} controls>#{st_tags}</#{tagType}>"
+      $(html)

     willPlayAudioInFlash = ->
       opts = $.extend({mode: 'auto'}, mejs.MediaElementDefaults, mejs.MepDefaults,mediaPlayerOptions)


document a potentially bad design choice in the mark_file client function

--- src/smc-util/client.coffee
+++ src/smc-util/client.coffee
@@ -1587,6 +1587,8 @@
         opts.client = @
         return new syncstring.SyncObject(opts)

+    # If called on the fronted, will make the given file with the given action.
+    # Does nothing on the backend.
     mark_file: (opts) =>
         opts = defaults opts,
             project_id : required


Fix promise cycle errors

Fix promise cycle errors

--- app/scripts/lib/script_modules/entity.coffee
+++ app/scripts/lib/script_modules/entity.coffee
@@ -38,11 +38,11 @@
       @entity.unbind(eventName, eventHandler)
       p = (sequence) =>
         @alternatePath = null
-        WhenJS(sequenceFunction.apply(this, args)(sequence))
+        WhenJS(sequenceFunction.apply(this, args)(sequence)).catch =>
+          @alternatePath
+
       @currentSequence = sequence = Math.random()
       @alternatePath = p(sequence)
-        .catch =>
-          @alternatePath
     @entity.bind(eventName, eventHandler)

   # remove an entity from the current gameplay. This means it cannot shoot


world stores primitives faithfully

world stores primitives faithfully

--- src/test/test_world.coffee
+++ src/test/test_world.coffee
@@ -12,6 +12,7 @@
     t.equal result, value

   test 'world stores primitives faithfully', (t) ->
-    t.ok world
-    test_store(t, "one", 2)
+    test_store(t, "number", 2)
+    test_store(t, "string", "okay")
+    test_store(t, "array", [1, "b"])
     t.end()


Naively assuming that Hybi-10 is the same as Hybi-07

--- src/trans-websocket.coffee
+++ src/trans-websocket.coffee
@@ -21,7 +21,8 @@
         location += '://' + req.headers.host + req.url

-        if req.headers['sec-websocket-version'] is '8'
+        ver = req.headers['sec-websocket-version']
+        if  ver is '8' or ver is '7'
             new websocket_hybi10.WebHandshake8(req, connection, head or '', origin, location)
         else
             new websocket_hixie76.WebHandshakeHixie76(req, connection, head or '', origin, location)


Fix typo, tray => Tray

Fix typo, tray => Tray

--- atom/browser/api/lib/exports/electron.coffee
+++ atom/browser/api/lib/exports/electron.coffee
@@ -45,7 +45,7 @@
   screen:
     enumerable: true
     get: -> require '../screen'
-  tray:
+  Tray:
     enumerable: true
     get: -> require '../tray'
   # The internal modules, invisible unless you know their names.


Fix CommandRegistry::findCommands spec

Fix CommandRegistry::findCommands spec

Don’t assert against commands registered on the window with jQuery

--- spec/command-registry-spec.coffee
+++ spec/command-registry-spec.coffee
@@ -119,7 +119,7 @@
       registry.add '.grandchild', 'namespace:command-3', ->
       registry.add '.grandchild.no-match', 'namespace:command-4', ->

-      expect(registry.findCommands(target: grandchild)).toEqual [
+      expect(registry.findCommands(target: grandchild)[0..2]).toEqual [
         {name: 'namespace:command-3', displayName: 'Namespace: Command 3'}
         {name: 'namespace:command-2', displayName: 'Namespace: Command 2'}
         {name: 'namespace:command-1', displayName: 'Namespace: Command 1'}


Add spec for grammmars config setting

Add spec for grammmars config setting

--- spec/markdown-preview-spec.coffee
+++ spec/markdown-preview-spec.coffee
@@ -229,3 +229,17 @@

       waitsFor ->
         MarkdownPreviewView::renderMarkdown.callCount > 0
+
+  describe "when the editor's grammar it not enabled for preview", ->
+    it "does not open the markdown preview", ->
+      atom.config.set('markdown-preview.grammars', [])
+
+      atom.workspaceView.attachToDom()
+
+      waitsForPromise ->
+        atom.workspaceView.open("subdir/file.markdown")
+
+      runs ->
+        spyOn(atom.workspace, 'open').andCallThrough()
+        atom.workspaceView.getActiveView().trigger 'markdown-preview:show'
+        expect(atom.workspace.open).not.toHaveBeenCalled()


torch turn on and off specs

torch turn on and off specs

--- specsApp/app/models/runner.coffee
+++ specsApp/app/models/runner.coffee
@@ -74,6 +74,28 @@

       runs ->
         expect( toggled ).toBeTruthy()
+
+    it "should turn on", ->
+      turnedOn = false
+
+      steroids.device.torch.turnOn {},
+        onSuccess: -> turnedOn = true
+
+      waitsFor -> turnedOn
+
+      runs ->
+        expect( turnedOn ).toBeTruthy()
+
+    it "should turn off", ->
+      turnedOff = false
+
+      steroids.device.torch.turnOff {},
+        onSuccess: -> turnedOff = true
+
+      waitsFor -> turnedOff
+
+      runs ->
+        expect( turnedOff ).toBeTruthy()

 describe "steroids.layers", ->


whitespace change to trigger travis?

whitespace change to trigger travis?

--- ansir.coffee
+++ ansir.coffee
@@ -61,3 +61,4 @@
     )

   commander.parse(process.argv)
+


Swipe functionality working.

Swipe functionality working.

--- examples/views/story/default.coffee
+++ examples/views/story/default.coffee
@@ -89,7 +89,7 @@

 								#swipe integration
 								coffeescript ->
-									$(".gallery").wipetouch
+									$(".slide").wipetouch
 									  wipeLeft: (result) ->
 									    jQuery(".next").click()


loading_message

--- nacl/js/aroma.coffee
+++ nacl/js/aroma.coffee
@@ -66,6 +66,10 @@
     prefetch: (msg, callback) ->
       [_, files] = msg
       count = files.length
+
+      msg = "Prefetching #{count} file"
+      msg += "s" if count != 1
+      @show_loading msg

       for tuple in files
         [loader_type, file] = tuple
@@ -82,6 +86,7 @@

     require: (msg, callback) ->
       [_, module] = msg
+      @show_loading "Loading module `#{module}`"
       @file_loader.get_module module, (code) ->
         if code?
           callback ["success", code]
@@ -90,6 +95,7 @@

     image: (msg, callback) ->
       [_, path] = msg
+      @show_loading "Loading `#{path}`"
       @file_loader.get_file path, "image", (img) ->
         if img?
           [img, w, h] = img
@@ -99,6 +105,7 @@

     audio: (msg, callback) ->
       [_, path, type] = msg
+      @show_loading "Loading `#{path}`"

       file_type = switch type
         when "static" then "sound"
@@ -123,6 +130,7 @@
     request: (msg) -> # forwards to async handler
       [_, id, data] = msg
       @dispatch @async_handlers, data[0], data, (res) =>
+        @hide_loading()
         @post_message ["response", id, res]

     audio: (msg) ->
@@ -141,13 +149,28 @@
     @file_loader = new Aroma.FileLoader this
     @audio = new Aroma.Audio

+    @loading_elm = @container.querySelector ".loading_message"
+
     listen @container, "load", =>
       log "Loaded module"
+      @module = @container.querySelectorAll("embed")[0]
       @fire "loaded"
-      @module = @container.querySelectorAll("embed")[0]

     listen @container, "message", (e) =>
       @handle_message e
+
+  show_loading: (msg="Loading") ->
+    return unless @loading_elm
+
+    @loading_elm.innerHTML = msg
+    unless @loading_elm.className.match /visible/
+      @loading_elm.className += " visible"
+
+  hide_loading: ->
+    return unless @loading_elm
+
+    @loading_elm.className =
+      @loading_elm.className.replace /visible/g, ""

   # reset any shared state
   reset: ->


fix(archive): Remove optimizations that were breaking archive...

--- internal_packages/thread-list/lib/thread-list-store.coffee
+++ internal_packages/thread-list/lib/thread-list-store.coffee
@@ -98,7 +98,6 @@
     keyboardId = FocusedContentStore.keyboardCursorId('thread')

     for thread in selected
-      continue if thread.hasTagId('archive')
       task = new AddRemoveTagsTask(thread, ['archive'], ['inbox'])
       Actions.queueTask(task)
       if thread.id is focusedId
@@ -119,8 +118,7 @@
     focused = FocusedContentStore.focused('thread')
     explicitOffset = if offset is "auto" then false else true

-    return unless focused and not focused.hasTagId('archive')
-
+    return unless focused

     # Determine the current index
     index = @_view.indexOfId(focused.id)


server request now needs and instance of OAuth1

--- plugins/server.request/server_request.coffee
+++ plugins/server.request/server_request.coffee
@@ -17,9 +17,10 @@
 async = require 'async'
 qs = require 'querystring'
 Url = require 'url'
+OAuth1 = require '../../lib/oauth1'

 oauth =
-	oauth1: require '../../lib/oauth1'
+	oauth1: new OAuth1
 	oauth2: require '../../lib/oauth2'

 exports.setup = (callback) ->


Fix copy/paste breakage.

Fix copy/paste breakage.

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -835,7 +835,7 @@
                     "Ctrl-Space"   : "indentAuto"
                     "Tab"          : (editor)   => @press_tab_key(editor)
                     "Esc"          : (editor) => @interrupt_key()
-                    "Ctrl-C"       : (editor) => @interrupt_key()
+                    #"Ctrl-C"       : (editor) => @interrupt_key()  # this breaks copy on windows/linux!

         @codemirror = make_editor(elt[0])

@@ -1052,7 +1052,7 @@

 codemirror_session_editor = (editor, filename, extra_opts) ->
     ext = filename_extension(filename)
-
+
     E = new CodeMirrorEditor(editor, filename, "", extra_opts)
     # Enhance the editor with synchronized session capabilities.
     opts =


make public md/rst/etc still be visible

make public md/rst/etc still be visible

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -127,28 +127,28 @@
 #file_associations['tex'] =  # TODO: only for TESTING!!!
 #    editor : 'html-md'
 #    icon   : 'fa-file-code-o'
-#    opts   : {indent_unit:4, tab_size:4}
+#    opts   : {indent_unit:4, tab_size:4, mode:'stex2'}

 file_associations['html'] =
     editor : 'html-md'
     icon   : 'fa-file-code-o'
-    opts   : {indent_unit:4, tab_size:4}
+    opts   : {indent_unit:4, tab_size:4, mode:'htmlmixed2'}

 file_associations['md'] =
     editor : 'html-md'
     icon   : 'fa-file-code-o'
-    opts   : {indent_unit:4, tab_size:4}
+    opts   : {indent_unit:4, tab_size:4, mode:'gfm2'}

 file_associations['rst'] =
     editor : 'html-md'
     icon   : 'fa-file-code-o'
-    opts   : {indent_unit:4, tab_size:4}
+    opts   : {indent_unit:4, tab_size:4, mode:'rst'}

 file_associations['mediawiki'] = file_associations['wiki'] =
     editor : 'html-md'
     icon   : 'fa-file-code-o'
-    opts   : {indent_unit:4, tab_size:4}
+    opts   : {indent_unit:4, tab_size:4, mode:'mediawiki'}

 file_associations['sass'] =
     editor : 'codemirror'
@@ -803,7 +803,10 @@
             when 'latex'
                 editor = new LatexEditor(@, filename, content, extra_opts)
             when 'html-md'
-                editor = new HTML_MD_Editor(@, filename, content, extra_opts)
+                if extra_opts.public_access
+                    editor = new CodeMirrorEditor(@, filename, opts.content, extra_opts)
+                else
+                    editor = new HTML_MD_Editor(@, filename, content, extra_opts)
             when 'history'
                 editor = new HistoryEditor(@, filename, content, extra_opts)
             when 'pdf'


do the codemirror refresh in the next render loop -- it sometimes doesn't work doing it in the same loop; also remove some spurious refresh calls.

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -1550,13 +1550,11 @@
             @syncdoc.sync()

         @element.show()
-        @codemirror.refresh()

         if @opts.style_active_line
             @_style_active_line($(@codemirror.getWrapperElement()).css('background-color'))

         if @_split_view
-            @codemirror1.refresh()
             $(@codemirror1.getWrapperElement()).show()
         else
             $(@codemirror1.getWrapperElement()).hide()
@@ -1601,7 +1599,7 @@
             cm_wrapper.css
                 height : ht
                 width  : width
-            cm.refresh()
+            setTimeout((()=>cm.refresh()), 0)

         if chat
             chat_elt = @element.find(".salvus-editor-codemirror-chat")


KDView: unsetKeyView fixed

KDView: unsetKeyView fixed

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -622,7 +622,7 @@
   mouseWheel:(event)-> yes

   mouseDown:(event)->
-    (KD.getSingleton "windowController").unsetKeyView
+    KD.singleton("windowController").unsetKeyView()
     yes

   paste:(event)->      yes


Add update method to tipsy to update title if tooltip is already visible, and call this update after updateTooltip@KDView

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -764,6 +764,7 @@
     o.selector or= null
     o.title    or= ""
     @$(o.selector)[0].setAttribute "original-title", o.title
+    @$(o.selector).tipsy "update"

   hideTooltip:(o = {})->
     o.selector or= null


filter commenter relation from activity ticker

--- workers/social/lib/social/models/activityticker.coffee
+++ workers/social/lib/social/models/activityticker.coffee
@@ -78,6 +78,8 @@
         timestamp : {"$lt" : new Date(from)}
         data      :
           group   : groupSlug
+        as        :
+          $ne     : "commenter"

       relOptions  =    # do not fetch more than 15 at once
         limit     : 5  # Math.min options.limit ? 15, 15


套餐相关选项

套餐相关选项

--- core/config.coffee
+++ core/config.coffee
@@ -14,6 +14,14 @@
       'linux', 'nodejs', 'php', 'python', 'rootpanel'
     ]

+  plans:
+    all:
+      service: ['memcached', 'mongodb', 'mysql', 'nginx', 'phpfpm', 'pptp', 'shadowsocks', 'ssh']
+      resources:
+        storage: 520
+        transfer: 39
+        memory: 27
+
   db:
     type: 'mongo'
     server: 'localhost'


Fix check for delete status code

Fix check for delete status code

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -506,7 +506,7 @@
       .delete() (err, res, body) ->
         json_body = null
         switch res.statusCode
-          when 204 || 200
+          when 204, 200
             value = true
           else
             console.log res.statusCode


fix: when testing combinations, explicitly return `null` when none is found

--- src/css-selector-generator.coffee
+++ src/css-selector-generator.coffee
@@ -142,6 +142,8 @@
       for item in (items.map (item) -> tag + item)
         return item if @testUniqueness element, item

+    return null
+

   getUniqueSelector: (element) ->
     selectors = @getAllSelectors element


don't crash when receiving no messages

don't crash when receiving no messages

--- src/adapter.coffee
+++ src/adapter.coffee
@@ -53,8 +53,9 @@
           @robot.logger.debug "Polling datafeed #{response.id}"
           @symphony.readDatafeed(response.id)
             .then (response) =>
-              @robot.logger.debug "Received #{response.length} datafeed messages"
-              @_receiveMessage msg for msg in response when msg.v2messageType = 'V2Message'
+              if response?
+                @robot.logger.debug "Received #{response.length} datafeed messages"
+                @_receiveMessage msg for msg in response when msg.v2messageType = 'V2Message'
               @emit 'poll', response.id
             .fail (err) =>
               @robot.emit 'error', new Error("Unable to read datafeed #{response.id}: #{err}")


Setup .apmrc from run()

Setup .apmrc from run()

--- src/apm-cli.coffee
+++ src/apm-cli.coffee
@@ -28,7 +28,6 @@
     fs.writeFileSync(rcPath, "cache = #{cachePath}\n")

 setupTempDirectory()
-setupApmRcFile()

 commandClasses = [
   require './clean'
@@ -147,6 +146,7 @@

 module.exports =
   run: (args, callback) ->
+    setupApmRcFile()
     options = parseOptions(args)

     unless options.argv.color


Fix covers sizing

Fix covers sizing

--- scripts/controllers/FrequencySelector.coffee
+++ scripts/controllers/FrequencySelector.coffee
@@ -80,14 +80,14 @@
 	onRangeChange: =>
 		@el.attr 'data-source', @range.source

-		highHeight = ((1 - @range.high) * 100) + '%'
-		lowHeight = (@range.low * 100) + '%'
+		highHeight = (1 - @range.high) * 100
+		lowHeight = @range.low * 100

-		@highCover.css 'height', highHeight
-		@lowCover.css 'height', lowHeight
+		@highCover.css 'height', highHeight + '%'
+		@lowCover.css 'height', lowHeight + '%'
 		@timesContainer.css
-			top: highHeight
-			bottom: lowHeight
+			top: highHeight + '%'
+			bottom: lowHeight + '%'

 		workflowTarget = if highHeight > lowHeight then @highCover else @lowCover
 		@workflowContainer.appendTo workflowTarget


FileUploadButton: Fixed "disabled" state, don't open File Picker.

The problem was that Button did not preventDefault when disabled.

--- src/elements/Button/Button.coffee
+++ src/elements/Button/Button.coffee
@@ -278,6 +278,7 @@
 					return

 				ev.stopPropagation()
+				ev.preventDefault()
 				@onClickAction(ev)
 				return


tiere aus tierschutz chemnitz dazu

tiere aus tierschutz chemnitz dazu

--- API/src/scraper.coffee
+++ API/src/scraper.coffee
@@ -3,6 +3,7 @@
 express = require 'express'
 NodeCache = require 'node-cache'
 fs = require 'fs'
+_ = require 'underscore'

 app = express()
 cache = new NodeCache()
@@ -54,37 +55,87 @@
                 urls.push detail_url
             f urls

+get_tierschutz = (url) ->
+    new Promise (f, r) ->
+        request url, (err, response, body) ->
+            if err
+                r err
+            $ = cheerio.load body
+            content = $('table')
+            name = content.find('.Stil2').text().replace /"/g, ''
+            img = content.find('img').attr('src')
+            pic = tierschutz_base_url + content.find('img').attr('src')
+            id = img.split '.', 1
+            id = id[0].split '/'
+            content.find('.Stil2').remove()
+            content.find('p').first().remove()
+            content.find('p').last().remove()
+            content.find('p').each ->
+                elem = $(this)
+                elem.remove() if elem.text().trim().split(/\s+/).length < 10
+                elem.remove() if /^-/.test elem.text().trim()
+            details =
+                id: id[-1..][0]
+                pic: pic
+                name: name
+                link: url
+                desc: content.find('.Stil1')
+                    .text().replace(/\n/g, '')
+                    .replace(/\r/g, '')
+                    .replace(/\t/g, '')
+                    .trim()
+            f details
+
 get_tierschutzUrls = (url) ->
     new Promise (f, r) ->
         request url, (err, response, body) ->
             if err
                 console.error err
                 r err
-                return
             urls = []
             $ = cheerio.load body
-            $('td', 'table', '#center').each ->
+            $('td', '#center').each ->
                 elem = $(this)
-                if elem.attr 'colspan'
-                    return
-                else
-                    detail_url = tierschutz_base_url + elem.find('a').attr('href')
-                    urls.push detail_url
-                    return
+                if elem.attr 'colspan' is undefined
+                    href = elem.find('a').attr('href')
+                    if href isnt undefined and /^vermittlung/.test href
+                        detail_url = tierschutz_base_url + href
+                        urls.push detail_url
             f urls

+get_tierfreundedata = ->
+    new Promise (f, r) ->
+        get_tierfreundeUrls tierfreunde_url
+            .then (urls) ->
+                p = []
+                for url in urls
+                    p.push get_tierfreunde url
+                Promise.all p
+            .then (values) ->
+                f values
+            .catch (err) ->
+                r err
+
+get_tierschutzdata = ->
+    new Promise (f, r) ->
+        get_tierschutzUrls tierschutz_url
+            .then (urls) ->
+                p = []
+                for url in urls
+                    p.push get_tierschutz url
+                Promise.all p
+            .then (values) ->
+                f values
+            .catch (err) ->
+                r err
+
 get_data = ->
-    new Promise (f, r) ->
+    new Promise (f ,r) ->
         values = cache.get('tiere')
         if not values
-            console.log "no cache"
-            get_tierfreundeUrls tierfreunde_url
-                .then (urls) ->
-                    p = []
-                    for url in urls
-                        p.push get_tierfreunde url
-                    Promise.all p
-                .then (values) ->
+            Promise.all [get_tierschutzdata(), get_tierfreundedata()]
+                .then (list_of_values) ->
+                    values = _.union.apply null, list_of_values
                     cache.set('tiere', values, 60*60*24)
                     f values
                 .catch (err) ->
@@ -118,14 +169,13 @@
             fs.writeFileSync filename, JSON.stringify postedPets
             f notPostedPet

-
-get_tierschutzUrls(tierschutz_url).then (values) ->
-    console.log values
 ###
+get_data().then (values) ->
+    console.log values.length
 get_notPostedPet().then (pet) ->
     console.log pet
 ###
-###
+
 # Return all pets
 app.get '/', (req, res) ->
     get_data()


Fixed url detection of no log of ui log request.

--- app/assets/javascripts/app/lib/app_init/track.js.coffee
+++ app/assets/javascripts/app/lib/app_init/track.js.coffee
@@ -62,7 +62,7 @@
     $(document).bind( 'ajaxComplete', ( e, request, settings ) =>

       # do not log ui requests
-      if settings.url && settings.url.substr(0,3) isnt '/ui'
+      if settings.url && settings.url.substr(settings.url-3,3) isnt '/ui'
         level = 'notice'
         responseText = ''
         if request.status >= 400


Moved to new log server.

Moved to new log server.

--- app/assets/javascripts/app/lib/app_init/track.js.coffee
+++ app/assets/javascripts/app/lib/app_init/track.js.coffee
@@ -25,8 +25,7 @@
     @browser = App.Browser.detection()
     @data    = []
 #    @url     = 'http://localhost:3005/api/v1/ui'
-#    @url     = 'https://log.znuny.com/api/ui'
-    @url     = 'https://portal.znuny.com/api/v1/ui'
+    @url     = 'https://log.zammad.com/api/v1/ui'

     @log( 'start', 'notice', {} )


Added validation requirements to Address model

--- app/assets/javascripts/sprangular/models/address.coffee
+++ app/assets/javascripts/sprangular/models/address.coffee
@@ -1,6 +1,15 @@
 'use strict'

 class Sprangular.Address
+  Validity.define @,
+    firstname: 'required'
+    lastname: 'required'
+    address1: 'required'
+    city: 'required'
+    stateId: 'required'
+    countryId: ['required', {greaterThan: 0}]
+    zipcode: 'required'
+
   init: ->
     @stateId = @state_id
     @countryId = @country_id


brought EntityConstructorMap into AutoMapper for canHandle matching

--- assets/scripts/coffee/scribe/factory/auto_mapper.coffee
+++ assets/scripts/coffee/scribe/factory/auto_mapper.coffee
@@ -1,3 +1,5 @@
+EntityConstructorMap = require 'scribe/repositories/entity_constructor_map'
+
 #
 # @author - Tim Shelburne (tim@musiconelive.com)
 #
@@ -8,7 +10,7 @@

   constructor: (@entityClass, @referenceBuilder=null)->

-  canHandle: (entityClass)-> entityClass is @entityClass.name
+  canHandle: (entityClass)-> EntityConstructorMap.match(entityClass, @entityClass)

   handle: (config)->
     entity = map.call @, config


Clean DOM when publication is destroyed. Fixes #176

--- client/publication.coffee
+++ client/publication.coffee
@@ -100,6 +100,8 @@
     for page in pages
       page.page.destroy()
     @_pdf.destroy() if @_pdf
+
+    $('#viewer .display-wrapper').empty()

   renderPage: (page) =>
     return if page.rendering


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/bounded.coffee
+++ src/bounded.coffee
@@ -46,14 +46,21 @@
   position method.

-  player = GameObject
-    x: 50
-    y: 40
-
-  player.include(Bounded)
-
-  player.position()
-  # => {x: 50, y: 40}
+    player = GameObject
+    x: 50
+    y: 50
+    width: 10
+    height: 10
+
+  player.include Bounded
+
+  enemy = GameObject
+    x: 110
+    y: 120
+    width: 7
+    height: 20
+
+  equals player.distance(enemy), Point.distance(player.position(), enemy.position())

   @name distance


Task label changes Closes #44 Closes #43

--- app/config.coffee
+++ app/config.coffee
@@ -15,13 +15,10 @@
       label: 'Environmental Health'
       description: 'Monitoring environmental health and sanitation was a major part of the Medical Officer’s role. They reported on contaminated water, noxious fumes and smells and sewerage and drainage. They monitored the health effects of seasonal changes in temperature and smog levels. They also directed the clean-up of areas that were a potential health hazard, removing piles of rubbish, stopping effluent run-off.'
       tools: [{
-        label: 'Environmental problem'
+        label: 'Pollution, sanitation and waste problems'
         value: 'issue'
         required: true
         subtasks: [{
-          label: 'Related health issue or hazard'
-          value: 'hazard'
-        },{
           label: 'People affected'
           value: 'people'
         },{
@@ -115,9 +112,6 @@
         value: 'issue'
         required: true
         subtasks: [{
-          label: 'Related health issue or hazard'
-          value: 'hazard'
-        },{
           label: 'People affected'
           value: 'people'
         },{


Use installUpdate

Use installUpdate

--- src/browser/auto-updater-win32.coffee
+++ src/browser/auto-updater-win32.coffee
@@ -13,7 +13,7 @@
       shellAutoUpdater.quitAndInstall()
       return

-    SquirrelUpdate.spawn ['--update', @updateUrl], (error) =>
+    @installUpdate (error) ->
       return if error?

       SquirrelUpdate.spawn ['--processStart', 'atom.exe'], ->


shortcuts: truncate description text

shortcuts: truncate description text

--- client/account/lib/views/accounteditshortcutsrow.coffee
+++ client/account/lib/views/accounteditshortcutsrow.coffee
@@ -4,8 +4,14 @@
 EventType = require './accounteditshortcutseventtype'
 globals = require 'globals'

-# Determines class name to set for duplicate/colliding items.
+# Class name to set for duplicate/colliding items.
 DUP_CLASS_NAME = 'collides'
+
+# The maximum description string length.
+DESCRIPTION_TRUNC_LEN = 30
+
+# The separator pattern to truncate to.
+DESCRIPTION_TRUNC_SEP = ' '

 # On Mac we display corresponding unicode chars for the following keys.
 MAC_UNICODE =
@@ -60,9 +66,14 @@

     if @model.enabled then toggle.setClass 'enabled'

+    descriptionText = _.escape @model.description
+
     description = new kd.View
       cssClass : 'col'
-      partial  : _.escape @model.description
+      partial  : _.trunc descriptionText, separator: DESCRIPTION_TRUNC_SEP, length: DESCRIPTION_TRUNC_LEN
+
+    if descriptionText.length > DESCRIPTION_TRUNC_LEN
+      description.domElement.attr 'title', descriptionText

     binding = new kd.View
       cssClass : 'col'


proxy static institutions URLs in dev env

--- services/web/config/settings.defaults.coffee
+++ services/web/config/settings.defaults.coffee
@@ -14,6 +14,11 @@
 httpAuthUsers[httpAuthUser] = httpAuthPass

 sessionSecret = "secret-please-change"
+
+v1Api =
+	url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
+	user: 'overleaf'
+	pass: 'password'

 module.exports = settings =

@@ -157,9 +162,9 @@
 		thirdpartyreferences:
 			url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046"
 		v1:
-			url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
-			user: 'overleaf'
-			pass: 'password'
+			url: v1Api.url
+			user: v1Api.user
+			pass: v1Api.pass

 	templates:
 		user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
@@ -420,7 +425,9 @@
 	redirects:
 		"/templates/index": "/templates/"

-	proxyUrls: {}
+	proxyUrls:
+		'/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' }
+		'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }

 	reloadModuleViewsOnEachRequest: true


Shorten text for link to Illustris API

Shorten text for link to Illustris API

--- app/lib/en.coffee
+++ app/lib/en.coffee
@@ -1143,7 +1143,7 @@
     sfr: 'Star formation rate'
     skyserver_link: 'View on SkyServer'
     legacyserver_link: 'View on Sky Viewer'
-    illustris_link: 'View full data from the Illustris Project'
+    illustris_link: 'View data from Illustris Project'
     explorer_link: 'Illustris Explorer'


pricing: added missing headerview

pricing: added missing headerview

--- client/landing/site.landing/coffee/pricing/AppView.coffee
+++ client/landing/site.landing/coffee/pricing/AppView.coffee
@@ -2,6 +2,7 @@
 FooterView       = require '../home/footerview'
 CustomLinkView   = require '../core/customlinkview'
 PricingPlansView = require './plans'
+MainHeaderView   = require '../core/mainheaderview'

 module.exports = class PricingAppView extends JView

@@ -35,6 +36,8 @@

   initViews: ->
+
+    @mainHeader = new MainHeaderView

     @header = new KDCustomHTMLView
       partial   : "Our pricing, your terms"
@@ -164,6 +167,7 @@

   pistachio: ->
     """
+      {{> @mainHeader}}
       {{> @header}}
       {{> @headerDescription}}
       {{> @intervalToggle}}


simplified creation of channel list in profilecontroller

--- core/app/backbone/controllers/profile_controller.coffee
+++ core/app/backbone/controllers/profile_controller.coffee
@@ -58,8 +58,7 @@
     app.leftTopRegion.show(userLargeView);

   setChannelListing: (username) ->
+    changed = window.Channels.setUsernameAndRefresh(username)
     channelCollectionView = new ChannelsView(collection: window.Channels)
-    changed = window.Channels.setUsername(username)
-    window.Channels.setupReloading(changed)
-    window.Channels.unsetActiveChannel()
+    channelCollectionView.setActiveChannel(`undefined`)
     app.leftMiddleRegion.show(channelCollectionView)


log the case where there are no entries in the document history

--- services/track-changes/app/coffee/DocArchiveManager.coffee
+++ services/track-changes/app/coffee/DocArchiveManager.coffee
@@ -28,6 +28,7 @@
 		MongoManager.getDocChangesCount doc_id, (error, count) ->
 			return callback(error) if error?
 			if count == 0
+				logger.log {project_id, doc_id}, "document history is empty, not archiving"
 				return callback()
 			else
 				MongoManager.getLastCompressedUpdate doc_id, (error, update) ->


project name should be clickable href in invite email

--- services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -92,7 +92,7 @@
 	layout: NotificationEmailLayout
 	type:"notification"
 	compiledTemplate: _.template """
-
Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>' with you

+
Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>' with you

 			<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">


Prevent messing up other specs by setting url to /

--- test/api/index.spec.coffee
+++ test/api/index.spec.coffee
@@ -17,16 +17,17 @@
     require.cache[require.resolve('api/loader')].exports = {loader: REAL_LOADER, isPending}
     # force require to re-parse the api file now that the stub's removed
     delete require.cache[require.resolve('api')]
+    @api.initialize('/')

   it 'only calls loader a single time', ->
-    @api.initialize('/test/url')
+    @api.initialize('/')
     expect(@loader.callCount).equal(1)
-    @api.initialize('/test/url')
+    @api.initialize('/')
     expect(@loader.callCount).equal(1)

   it 'can be re-initialized after destroy', ->
-    @api.initialize('/test/url')
+    @api.initialize('/')
     expect(@loader.callCount).equal(1)
     @api.destroy()
-    @api.initialize('/test/url')
+    @api.initialize('/')
     expect(@loader.callCount).equal(2)


prevent unnecessary renders

prevent unnecessary renders

--- @client/point.coffee
+++ @client/point.coffee
@@ -492,7 +492,7 @@
   #     discussion & click a new point below it
   ensureDiscussionIsInViewPort : ->
     is_selected = get_selected_point() == @props.point
-    if @local.is_selected != is_selected
+    if !!@local.is_selected != !!is_selected
       if is_selected

         i = setInterval =>


refactor asana task info script, clean up a little

--- scripts/asana-info.coffee
+++ scripts/asana-info.coffee
@@ -1,7 +1,7 @@
 # Description:
 #   Listens for Asana URLs and provides info about the task.
 #   The message is a markdown formatted string.
-#   The format looks something like this:
+#   The format looks like this:
 #
 #   > [ ] Task Title (Assignee)
 #   > Lorem ipsum dolor sit amet, I am the description …
@@ -13,13 +13,19 @@
 #   HUBOT_ASANA_API_KEY - find this in Account Settings -> API
 #
 # Commands:
-#   Simply post an Asana URL that points to a task, eg.
-#   https://app.asana.com/0/12345678901234/12345678901234
+#   post Asana task URL - get info about task
 #
 # Author:
 #   mxlje

-api_key = process.env.HUBOT_ASANA_API_KEY
+asana_api_key = process.env.HUBOT_ASANA_API_KEY
+
+# template for response
+response = (task) ->
+  """
+  > **#{task.status} #{task.title} (#{task.assignee})**
+  > #{task.notes}
+  """

 module.exports = (robot) ->
   robot.hear /app\.asana\.com\/\d{1}\/\d+\/(\d+)/i, (msg) ->
@@ -27,7 +33,7 @@
     # extract task ID from URL
     task_id = msg.match[1]
     req_uri = "https://app.asana.com/api/1.0/tasks/#{task_id}"
-    auth    = 'Basic ' + new Buffer("#{api_key}:").toString('base64')
+    auth    = 'Basic ' + new Buffer("#{asana_api_key}:").toString('base64')

     robot.http(req_uri)
       .header("Authorization", auth)
@@ -45,18 +51,18 @@
         # parse the response body
         data = JSON.parse(body).data

-        # prepare some data points based on their existence
-        completed = if data.completed then "[✓]" else "[ ]"
-        assignee  = if data.assignee then "#{data.assignee.name}" else "unassigned"
-
-        response = "> #{completed} **#{data.name}** (#{assignee})"
+        # extract and reformat data from response
+        task =
+          title:    data.name
+          status:   if data.completed then "[✓]" else "[ ]"
+          assignee: if data.assignee then "#{data.assignee.name}" else "unassigned"

         # clip notes at first paragraph
         if data.notes.length > 0
           split = data.notes.split("\n")
-          notes = if split.length > 1 then "#{split[0]} …" else data.notes
-
-          response = response + "\n> #{notes}"
-
+          task.notes = if split.length > 1 then "#{split[0]} …" else data.notes
+        else
+          task.notes = "(no description)"
+
         # send the finished markdown down the pipe
-        msg.send response
+        msg.send response(task)


remove error fn

remove error fn

--- app/builtins.coffee
+++ app/builtins.coffee
@@ -139,6 +139,7 @@
   component_fn 'dir', (ctx, object) ->
     ObjectBrowserComponent {object}

+
   component_fn 'md', 'Renders Markdown', (ctx, string, opts) ->
     Markdown.MarkdownComponent value: string, opts: opts

@@ -154,6 +155,7 @@

   component_fn 'html', 'Adds some HTML', (ctx, string) ->
     HtmlComponent value: string
+

   ErrorComponent = React.createClass
     displayName: 'ErrorComponent'
@@ -169,8 +171,6 @@
         # TODO handle exceptions better
       React.DOM.pre {className: 'error'}, message

-  component_fn 'error', 'Shows a preformatted error message', (ctx, message) ->
-    ErrorComponent {message}

   component_fn 'example', 'Makes a clickable code example', (ctx, value, opts) ->
     ExampleComponent value: value, run: opts?.run ? true


Remove stupid comment

Remove stupid comment

--- assets/user.coffee
+++ assets/user.coffee
@@ -1,5 +1,2 @@
-#
-# The javscript asset package for the user app.
-#
 $ ->
   require("../apps/user/client.coffee").init()


Add a bomb

Add a bomb

--- app/js/world.coffee
+++ app/js/world.coffee
@@ -270,14 +270,10 @@
     super e
     if e.keyCode is 32 # space
       v = Vec.scale @ship.orientation, 5
-      @addParticle new Particle
-        lifespan: 1
-        position: @ship.tip()
-        velocity: Vec.add @ship.velocity, v
-        size: 2
-        color: "#4FA"
-        fade: true
-        collides: true
+      @fireMissile @ship.tip(), Vec.add @ship.velocity, v
+    if e.keyCode is 88 # x
+      v = Vec.scale @ship.orientation, 3
+      @fireMissile @ship.tip(), Vec.add(@ship.velocity, v), 5, true

   collisions: (contacts) ->
     bumped = []
@@ -298,6 +294,13 @@
       continue if body.ship
       contact.particle.alive = false
       continue if contact.body.deleted
+
+      if contact.particle.spawnMore
+        num = Utils.randomInt(10, 25)
+        for i in [0..num]
+          direction = Rotation.fromAngle Utils.random() * Math.PI * 2
+          velocity = Vec.scale direction, 2.5 + Utils.random() * 2.5
+          @fireMissile contact.particle.position, velocity

       @explosionAt particle.position
       @removeBody contact.body
@@ -321,7 +324,7 @@
           velocity = Vec.add shard.velocity, Vec.scale inward, Utils.random()

           @addParticle new Particle
-            lifespan: 2
+            lifespan: 1 + Utils.random()
             size: 2
             position: shard.position
             velocity: velocity
@@ -345,3 +348,15 @@
         damping: 0.05
         color: color
         fade: true
+
+  fireMissile: (position, velocity, size = 2, spawnMore = false) ->
+    missile = new Particle
+      lifespan: 1
+      position: position
+      velocity: velocity
+      size: size
+      color: "#4FA"
+      fade: true
+      collides: true
+    missile.spawnMore = spawnMore
+    @addParticle missile


Don't update or handle events when paused

--- app/js/world.coffee
+++ app/js/world.coffee
@@ -278,6 +278,7 @@
     @createStarfield()

   keydown: (e) ->
+    return if @paused
     super e

     return if @ship.dead
@@ -297,6 +298,7 @@
       @explodeShip()

   mousedown: (e) =>
+    return if @paused
     point = [e.offsetX / @scale, @sizeY - e.offsetY / @scale]
     for body in @quadtree.atPoint point
       if Geometry.pointInsidePolygon point, body.vertices()
@@ -308,6 +310,7 @@

   update: (dt) ->
     super dt
+    return if @paused
     @updateStarfield @cameraDelta if Vec.magnitudeSquared(@cameraDelta) > 0
     @updateDamage()


Document method wrapping for non-GET/POST request

--- lib/assets/javascripts/unpoly/link.coffee
+++ lib/assets/javascripts/unpoly/link.coffee
@@ -801,9 +801,12 @@
   @param [up-method='get']
     The HTTP method to use for the request.

-    Common values are `get`, `post`, `put`, `patch` and `delete`.  `The value is case insensitive.
+    Common values are `get`, `post`, `put`, `patch` and `delete`. The value is case insensitive.

     The HTTP method may also be passed as an `[data-method]` attribute.
+
+    By default, methods other than `get` or `post` will be converted into a `post` request, and carry
+    their original method as a configurable [`_method` parameter](/up.protocol.config#config.methodParam).

   @param [up-params]
     A JSON object with additional [parameters](/up.Params) that should be sent as the request's


Add test for handling esa.webhook and emitting slack.attachment

--- test/esa-slack-test.coffee
+++ test/esa-slack-test.coffee
@@ -108,3 +108,88 @@
         expect(content.text).to.equal @comment.body_md
         expect(content.author_name).to.equal @comment.created_by.screen_name
         expect(content.author_icon).to.equal @comment.created_by.icon
+
+    context 'emit esa.webhook event', ->
+      buildWebhookArgs = (fixture) ->
+        webhook = JSON.parse(fs.readFileSync "#{__dirname}/fixtures/webhook_#{fixture}.json")
+        data =
+          team: webhook.team
+          user: webhook.user
+          post: webhook.post
+          comment: webhook.comment || null
+        return [webhook.kind, data]
+
+      context 'with post_create event', ->
+        beforeEach (done)->
+          [kind, @data] = buildWebhookArgs('post_create')
+          room.robot.emit 'esa.webhook', kind, @data
+          setTimeout done, 200
+
+        it 'emit slack.attachment with data', ->
+          content = emitted_data.content
+          expect(content.pretext).to.equal 'New post created'
+          expect(content.author_name).to.equal @data.user.screen_name
+          expect(content.author_icon).to.equal @data.user.icon.url
+          expect(content.title).to.equal @data.post.name
+          expect(content.title_link).to.equal @data.post.url
+
+          expect(content.text).to.equal @data.post.message
+
+      context 'with post_update event', ->
+        beforeEach (done)->
+          [kind, @data] = buildWebhookArgs('post_update')
+          room.robot.emit 'esa.webhook', kind, @data
+          setTimeout done, 200
+
+        it 'emit slack.attachment with data', ->
+          content = emitted_data.content
+          expect(content.pretext).to.equal 'The post updated'
+          expect(content.author_name).to.equal @data.user.screen_name
+          expect(content.author_icon).to.equal @data.user.icon.url
+          expect(content.title).to.equal @data.post.name
+          expect(content.title_link).to.equal @data.post.url
+
+          expect(content.text).to.equal @data.post.message
+
+      context 'with post_archive event', ->
+        beforeEach (done)->
+          [kind, @data] = buildWebhookArgs('post_archive')
+          room.robot.emit 'esa.webhook', kind, @data
+          setTimeout done, 200
+
+        it 'emit slack.attachment with data', ->
+          content = emitted_data.content
+          expect(content.pretext).to.equal 'The post archived'
+          expect(content.author_name).to.equal @data.user.screen_name
+          expect(content.author_icon).to.equal @data.user.icon.url
+          expect(content.title).to.equal @data.post.name
+          expect(content.title_link).to.equal @data.post.url
+
+          expect(content.text).to.be.undefined
+
+      context 'with comment_create event', ->
+        beforeEach (done)->
+          [kind, @data] = buildWebhookArgs('comment_create')
+          room.robot.emit 'esa.webhook', kind, @data
+          setTimeout done, 200
+
+        it 'emit slack.attachment with data', ->
+          content = emitted_data.content
+          expect(content.pretext).to.equal 'The comment posted'
+          expect(content.author_name).to.equal @data.user.screen_name
+          expect(content.author_icon).to.equal @data.user.icon.url
+          expect(content.title).to.equal @data.post.name
+          expect(content.title_link).to.equal @data.post.url
+
+          expect(content.text).to.equal @data.comment.body_md
+
+      context 'with member_join event', ->
+        beforeEach (done)->
+          [kind, @data] = buildWebhookArgs('member_join')
+          room.robot.emit 'esa.webhook', kind, @data
+          setTimeout done, 200
+
+        it 'emit slack.attachment with data', ->
+          content = emitted_data.content
+          expect(content.pretext).to.equal 'New member joined'
+          expect(content.text).to.equal @data.user.screen_name


Removed unused function

Removed unused function

--- create_liar.coffee
+++ create_liar.coffee
@@ -38,11 +38,6 @@
     injectLies return_lie.value, return_lie.on_value
   return_lie.value

-
-filter = (lies, function_name, args_obj) ->
-  lies = filter_on_function(lies, function_name)
-  filter_on_args(lies, args_obj)
-
 filter_on_function = (lies, function_name) ->
   lie for lie in lies when lie.function_name is function_name

@@ -50,11 +45,6 @@
   clean_args = remove_functions(args_obj)
   matches_args = (lie) -> arrays_equal lie.arguments ? [], clean_args
   lie for lie in lies when matches_args(lie)
-
-
-
-
-

 run_callback = (lie, arguments_obj) ->
   callback_arguments = callback_arguments_array lie


Add options for saml firstName and lastName fields.

--- server-ce/settings.coffee
+++ server-ce/settings.coffee
@@ -437,7 +437,9 @@
 	settings.externalAuth = true
 	settings.saml =
 		identityServiceName: process.env["SHARELATEX_SAML_IDENTITY_SERVICE_NAME"]
-		emailFieldName: process.env["SHARELATEX_SAML_EMAIL_FIELD_NAME"]
+		emailField: process.env["SHARELATEX_SAML_EMAIL_FIELD"] || process.env["SHARELATEX_SAML_EMAIL_FIELD_NAME"]
+		firstNameField: process.env["SHARELATEX_SAML_FIRST_NAME_FIELD"]
+		lastNameField:  process.env["SHARELATEX_SAML_LAST_NAME_FIELD"]
 		server:
 			# strings
 			entryPoint: process.env["SHARELATEX_SAML_ENTRYPOINT"]


Remove the starttls option

Remove the starttls option

--- server-ce/settings.coffee
+++ server-ce/settings.coffee
@@ -506,7 +506,6 @@
 		lastNameAtt: process.env["SHARELATEX_LDAP_"]
 		updateUserDetailsOnLogin: process.env["SHARELATEX_LDAP_UPDATE_USER_DETAILS_ON_LOGIN"] == 'true'
 		placeholder: process.env["SHARELATEX_LDAP_"]
- 		starttls: process.env["SHARELATEX_LDAP_TLS"] == 'true'

 	if process.env["SHARELATEX_LDAP_TLS_OPTS_CA_PATH"]
 		try


Update license inquiry call task name

Update license inquiry call task name

--- server/lib/closeIO.coffee
+++ server/lib/closeIO.coffee
@@ -115,7 +115,7 @@
         _type: "lead"
         lead_id: leadID
         assigned_to: userID
-        text: "Call #{teacherEmail}"
+        text: "Call license inquiry #{teacherEmail}"
         is_complete: false
       options =
         uri: "https://#{apiKey}:X@app.close.io/api/v1/task/"


:bug:Fix license inquiry licensesRequest parsing

--- server/lib/closeIO.coffee
+++ server/lib/closeIO.coffee
@@ -98,6 +98,7 @@

   processLicenseRequest: (teacherEmail, userID, leadID, licensesRequested, amount, done) ->
     # Update lead with licenses requested
+    licensesRequested = parseInt(licensesRequested)
     putData = 'custom.licensesRequested': licensesRequested
     options =
       uri: "https://#{apiKey}:X@app.close.io/api/v1/lead/#{leadID}/"
@@ -135,7 +136,7 @@
           date_won: dateWon.toISOString().substring(0, 10)
           lead_id: leadID
           status: 'Active'
-          value: parseInt(licensesRequested) * amount
+          value: licensesRequested * amount
           value_period: "annual"
         options =
           uri: "https://#{apiKey}:X@app.close.io/api/v1/opportunity/"


Moved jQuery selector to ui object

Moved jQuery selector to ui object

--- core/app/backbone/views/evidence/vote_up_down_view.coffee
+++ core/app/backbone/views/evidence/vote_up_down_view.coffee
@@ -8,6 +8,10 @@
     "click .js-up":   "_on_up_vote"
     "click .js-down": "_on_down_vote"

+  ui:
+    upButton: '.js-up'
+    downButton: '.js-down'
+
   initialize: ->
     @listenTo @model, "change", @_updateButtons

@@ -15,8 +19,8 @@
     @_updateButtons()

   _updateButtons: ->
-    @$('a.js-up').toggleClass 'active', @model.get('current_user_opinion') == 'believes'
-    @$('a.js-down').toggleClass 'active', @model.get('current_user_opinion') == 'disbelieves'
+    @ui.upButton.toggleClass 'active', @model.get('current_user_opinion') == 'believes'
+    @ui.downButton.toggleClass 'active', @model.get('current_user_opinion') == 'disbelieves'

   _on_up_vote: ->
     mp_track "Factlink: Upvote evidence click"


Better warnings about unknown user

Better warnings about unknown user

--- server/models/user.coffee
+++ server/models/user.coffee
@@ -93,6 +93,9 @@
     logger.info "Updating user", userId
     await updateResults(userId, dirtyResults)
     u = await User.findById(userId)
+    if not u
+        logger.warn "Unknown user ", userId
+        return
     await u.updateChocos()
     await u.updateRatingEtc()
     await u.updateLevel()


FIX: Get the teaching period if it is not in loadedPeriods

--- src/app/api/models/teaching-period.coffee
+++ src/app/api/models/teaching-period.coffee
@@ -17,7 +17,7 @@

   TeachingPeriod = {
     rollover: rollover
-
+
     # Get a teaching period with an id, assumes that teaching periods are loaded...
     getTeachingPeriod: (id) ->
       _.find data.loadedPeriods, (tp) -> tp.id == id
@@ -53,9 +53,14 @@
     get: (id, onSuccess, onFailure) ->
       resource.get( {id: id}
         (success) ->
+          result = _.find data.loadedPeriods, (tp) -> tp.id == id
           # Fetched teaching period details... update teaching period in loadedPeriods
-          result = _.find data.loadedPeriods, (tp) -> tp.id == id
-          _.extend result, success
+          if result?
+            _.extend result, success
+          # If teaching period is not in loadedPeriods
+          else
+            loadedPeriods << success
+            result = success
           onSuccess(result)
         (error) ->
           onFailure error


Create hasFocus method

Create hasFocus method

--- src/extensions/tree-view/tree-view.coffee
+++ src/extensions/tree-view/tree-view.coffee
@@ -55,7 +55,7 @@
     @on 'tree-view:add', => @add()
     @on 'tree-view:remove', => @removeSelectedEntry()
     @on 'tree-view:directory-modified', =>
-      if @is(':focus')
+      if @hasFocus()
         @selectEntryForPath(@selectedPath) if @selectedPath
       else
         @selectActiveFile()
@@ -74,7 +74,7 @@
   serialize: ->
     directoryExpansionStates: @root?.serializeEntryExpansionStates()
     selectedPath: @selectedEntry()?.getPath()
-    hasFocus: @is(':focus')
+    hasFocus: @hasFocus()
     attached: @hasParent()
     scrollTop: @scrollTop()

@@ -82,7 +82,7 @@
     @root?.unwatchEntries()

   toggle: ->
-    if @is(':focus')
+    if @hasFocus()
       @detach()
       @rootView.focus()
     else
@@ -95,6 +95,9 @@
   detach: ->
     @scrollTopAfterAttach = @scrollTop()
     super
+
+  hasFocus: ->
+    @is(':focus')

   entryClicked: (e) ->
     entry = $(e.currentTarget).view()


github token message style

github token message style

--- app/github.coffee
+++ app/github.coffee
@@ -126,7 +126,7 @@
           "Please set a GitHub access token for #{@props.site.api_base_url}: "
           React.DOM.input ref: 'input'
           React.DOM.button {onClick: @handle_set}, 'Set'
-        React.DOM.p null,
+        React.DOM.p {className: 'tip'},
           'lead.js requires an access token to use the GitHub API. You can create a '
           React.DOM.a {href: 'https://github.com/blog/1509-personal-api-tokens', target: '_blank'}, 'personal access token'
           # TODO just link to the settings