utils: introduces getMainDomain util

utils: introduces getMainDomain util

--- client/landing/site.landing/coffee/core/utils.coffee
+++ client/landing/site.landing/coffee/core/utils.coffee
@@ -44,6 +44,16 @@

     match = location.pathname.match /\/R\/(.*)/
     return referrer  if match and referrer = match[1]
+
+
+  getMainDomain: ->
+
+    { hostname, port } = location
+    domain = if prefix = hostname.split('.').shift() in ['dev', 'sandbox', 'latest']
+    then hostname
+    else hostname.split('.').slice(1).join('.')
+
+    return "#{domain}#{if port then ':'+port else ''}"

   getGroupNameFromLocation: ->


Device - testing uppercase

Device - testing uppercase

--- client/mobile/app/coffee/modules/device/model.coffee
+++ client/mobile/app/coffee/modules/device/model.coffee
@@ -31,7 +31,17 @@
     @writeFast([2,0,0,0,0,0,0])

   writeKeydown: (char) =>
-    @writeFast([2,0,charMap[char],0,0,0,0])
+
+    # Uppercase testing
+    @shift ||= 0
+    if char == char.toUpperCase()
+      # shift = 225 # LShift
+      @shift = 57 # Caps Lock
+    else
+      @shift = 0
+
+    char = char.toLowerCase()
+    @writeFast([2,@shift,charMap[char],0,0,0,0])

   clickMouseLeft: =>
     @writeFast([1,0,0,1])
@@ -42,7 +52,6 @@
     @writeFast([1,0,0,0])

   writeChar: (char) =>
-    char = char.toLowerCase() # TODO - lowercase only
     @writeKeydown(char)
     @writeKeyup()


Issue #10: add the round function to the Atom plugin

--- tools/atom-plugin/language-micro/grammars/micro.cson
+++ tools/atom-plugin/language-micro/grammars/micro.cson
@@ -47,7 +47,7 @@
 	}
 	{
 		'comment': 'Built-in functions (it should be described before functions)'
-		'match': '\\b(nil|num|type|arity|floor|ceil|trunc|sin|cos|tn|arcsin|arccos|arctn|angle|pow|sqrt|exp|ln|lg|abs|random|head|tail|if|str|in|out|outln|err|errln|exit)\\b'
+		'match': '\\b(nil|num|type|arity|floor|ceil|trunc|round|sin|cos|tn|arcsin|arccos|arctn|angle|pow|sqrt|exp|ln|lg|abs|random|head|tail|if|str|in|out|outln|err|errln|exit)\\b'
 		'name': 'variable.function.micro'
 	}
 	{


add issue links to github-issues

add issue links to github-issues

--- src/scripts/github-issues.coffee
+++ src/scripts/github-issues.coffee
@@ -27,5 +27,5 @@
           for issue in issues
             labels = ("##{label.name}" for label in issue.labels).join(" ")
             assignee = if issue.assignee then " (#{issue.assignee.login})" else ""
-            msg.send "[#{issue.number}] #{issue.title} #{labels}#{assignee}"
+            msg.send "[#{issue.number}] #{issue.title} #{labels}#{assignee} = #{issue.html_url}"


add i option to regex

add i option to regex

--- src/scripts/heroku-status.coffee
+++ src/scripts/heroku-status.coffee
@@ -3,7 +3,7 @@
 # heroku status - Returns the current Heroku status for app operations and tools

 module.exports = (robot) ->
-  robot.respond /heroku status/, (msg) ->
+  robot.respond /heroku status/i, (msg) ->
     msg.http("https://status.heroku.com/status.json")
       .get() (err, res, body) ->
         try


Fix typo in environment variable

Fix typo in environment variable

--- src/scripts/tweet-content.coffee
+++ src/scripts/tweet-content.coffee
@@ -8,7 +8,7 @@
 #   HUBOT_TWITTER_CONSUMER_KEY
 #   HUBOT_TWITTER_CONSUMER_SECRET
 #   HUBOT_TWITTER_ACCESS_TOKEN_KEY
-#   HUBOT_TWITTER_ACCES_TOKEN_SECRET
+#   HUBOT_TWITTER_ACCESS_TOKEN_SECRET
 #
 # Dependencies:
 #  "ntwitter" : "0.2.10",


students --> people

students --> people

--- src/smc-util/upgrade-spec.coffee
+++ src/smc-util/upgrade-spec.coffee
@@ -274,7 +274,7 @@

 subscription.large_course =
     icon  : 'battery-full'
-    desc : 'Basic Large Course\n(250 students)'
+    desc : 'Basic Large Course\n(250 people)'
     statement : 'COCALC BASIC LG'
     price :
         week   : 199
@@ -293,7 +293,7 @@

 subscription.large_course2 =
     icon  : 'battery-full'
-    desc : 'Standard Large Course\n(250 students)'
+    desc : 'Standard Large Course\n(250 people)'
     statement : 'COCALC LG'
     price :
         week   : 399
@@ -312,7 +312,7 @@

 subscription.medium_course =
     icon  : 'battery-three-quarters'
-    desc  : 'Basic Medium Course\n(70 students)'
+    desc  : 'Basic Medium Course\n(70 people)'
     statement : 'COCALC BASIC MD'
     price :
         week   : 79
@@ -330,7 +330,7 @@

 subscription.medium_course2 =
     icon  : 'battery-three-quarters'
-    desc  : 'Standard Medium Course\n(70 students)'
+    desc  : 'Standard Medium Course\n(70 people)'
     statement : 'COCALC MD'
     price :
         week   : 159
@@ -349,7 +349,7 @@

 subscription.xsmall_course =
     icon  : 'battery-empty'
-    desc  : 'Basic Extra Small Course\n(10 students)'
+    desc  : 'Basic Extra Small Course\n(10 people)'
     statement : 'COCALC BASIC XS'
     price :
         week   : 19
@@ -367,7 +367,7 @@

 subscription.xsmall_course2 =
     icon  : 'battery-empty'
-    desc  : 'Standard Extra Small Course\n(10 students)'
+    desc  : 'Standard Extra Small Course\n(10 people)'
     statement : 'COCALC XS'
     price :
         week   :  39
@@ -386,7 +386,7 @@

 subscription.small_course =
     icon  : 'battery-quarter'
-    desc  : 'Basic Small Course\n(25 students)'
+    desc  : 'Basic Small Course\n(25 people)'
     statement : 'COCALC BASIC SM'
     price :
         week   :  39
@@ -404,7 +404,7 @@

 subscription.small_course2 =
     icon  : 'battery-quarter'
-    desc  : 'Standard Small Course\n(25 students)'
+    desc  : 'Standard Small Course\n(25 people)'
     statement : 'COCALC SM'
     price :
         week   : 79
@@ -427,7 +427,7 @@

 subscription.xsmall_basic_course =
     icon  : 'battery-empty'
-    desc  : 'Basic Extra Small Course\n(10 students)'
+    desc  : 'Basic Extra Small Course\n(10 people)'
     statement : 'COCALC BASIC XS'
     price :
         week   :  29
@@ -445,7 +445,7 @@

 subscription.small_basic_course =
     icon  : 'battery-quarter'
-    desc  : 'Basic Small Course\n(25 students)'
+    desc  : 'Basic Small Course\n(25 people)'
     statement : 'COCALC BASIC SM'
     price :
         week   : 59
@@ -463,7 +463,7 @@

 subscription.medium_basic_course =
     icon  : 'battery-three-quarters'
-    desc  : 'Basic Medium Course\n(70 students)'
+    desc  : 'Basic Medium Course\n(70 people)'
     statement : 'COCALC BASIC MD'
     price :
         week   : 119
@@ -481,7 +481,7 @@

 subscription.large_basic_course =
     icon  : 'battery-full'
-    desc : 'Basic Large Course\n(250 students)'
+    desc : 'Basic Large Course\n(250 people)'
     statement : 'COCALC BASIC LG'
     price :
         week   : 299


hide extraneous buttons

hide extraneous buttons

--- src/smc-webapp/project_page.cjsx
+++ src/smc-webapp/project_page.cjsx
@@ -215,16 +215,19 @@
         icon      : 'plus-circle'
         tooltip   : 'Create new file, folder, worksheet or terminal'
         is_public : false
+        no_anonymous : true
     log:
         label     : 'Log'
         icon      : 'history'
         tooltip   : 'Log of project activity'
         is_public : false
+        no_anonymous : true
     search :
         label     : 'Find'
         icon      : 'search'
         tooltip   : 'Search files in the project'
         is_public : false
+        no_anonymous : true
     settings :
         label     : 'Settings'
         icon      : 'wrench'
@@ -495,6 +498,8 @@
         />

     render_chat_indicator: (shrink_fixed_tabs) ->
+        if @props.is_anonymous  # no possibility to chat
+            return
         if @props.active_project_tab?.slice(0,7) != 'editor-'
             # TODO: This is the case where we would support project-wide side chat, or side chats
             # for each individual Files/Search, etc. page (not clear!)
@@ -511,6 +516,9 @@
         </div>

     render_share_indicator: (shrink_fixed_tabs) ->
+        if @props.is_anonymous
+            # anon users can't share anything
+            return
         if @props.active_project_tab == 'files'
             path = @props.current_path
         else


graph: fix for bug when long time span between follow activity caused missing activities

--- workers/social/lib/social/models/graph/fetch.coffee
+++ workers/social/lib/social/models/graph/fetch.coffee
@@ -101,7 +101,7 @@
     # get the right number of results
     overview = overview[-20..overview.length]

-    allTimes = _.map(overview, (activity)-> activity.createdAt)
+    allTimes = _.map(overview, (activity)-> activity.createdAt.first)
     allTimes = _.flatten allTimes
     sortedAllTimes = _.sortBy(allTimes, (activity)-> activity)


Groups: 'parent' field removed from group schema due to a type mismatch on the DB

JGroup.parent should be an ObjectRef, but when a group is created it puts the parent field as an array. should be fixed later.

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -248,7 +248,7 @@
           'visible'
           'hidden'
         ]]
-      parent        : ObjectRef
+      # parent        : ObjectRef
       counts        :
         members     : Number
       customize     :


server: don't send custom partials or referral campaign data as payload

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -11,8 +11,6 @@
   {argv} = require 'optimist'

   prefetchedFeeds  = null
-  customPartial    = null
-  campaignData     = null
   socialapidata    = null
   currentGroup     = null
   userMachines     = null
@@ -27,8 +25,6 @@
       {profile   : {nickname}, _id} = delegate

     replacer             = (k, v)-> if 'string' is typeof v then encoder.XSSEncode v else v
-    encodedCampaignData  = JSON.stringify campaignData, replacer
-    encodedCustomPartial = JSON.stringify customPartial, replacer
     encodedSocialApiData = JSON.stringify socialapidata, replacer
     currentGroup         = JSON.stringify currentGroup
     userAccount          = JSON.stringify delegate
@@ -48,8 +44,6 @@
     </script>

     <script>KD.config.usePremiumBroker=#{usePremiumBroker}</script>
-    <script>KD.customPartial=#{encodedCustomPartial}</script>
-    <script>KD.campaignData=#{encodedCampaignData}</script>
     <script>KD.socialApiData=#{encodedSocialApiData}</script>
     <script>KD.userMachines=#{userMachines}</script>
     <script>KD.userWorkspaces=#{userWorkspaces}</script>
@@ -99,21 +93,12 @@
         socialapidata = data
         queue.fin()
     ->
-      bongoModels.JCustomPartials.one selector, (err, partial)->
-        customPartial = partial.data  if not err and partial
-        queue.fin()
-    ->
       bongoModels.JGroup.one {slug : slug or 'koding'}, (err, group) ->
         console.log err if err
+        if group
+          currentGroup = group

-        bongoModels.JReferralCampaign.one isActive: yes, (err, campaignData_)->
-          if not err and campaignData_ and campaignData_.data
-            campaignData = campaignData_.data
-
-          if group
-            currentGroup = group
-
-          queue.fin()
+        queue.fin()
     ->
       bongoModels.JWorkspace.fetch client, {}, (err, workspaces) ->
         console.log err  if err


Remove a console.log sentence

Remove a console.log sentence

--- test/unit/controllers/taskboardControllerSpec.coffee
+++ test/unit/controllers/taskboardControllerSpec.coffee
@@ -245,7 +245,6 @@
             promise = ctrl.calculateStats()
             httpBackend.flush()
             promise.should.be.fulfilled.then ->
-                console.log ctrl.scope.stats
                 expect(ctrl.scope.stats).to.be.deep.equal({
                     totalPoints: 0
                     completedPoints: 0


Do not send `parent_id` is it's something else than a number

When moving element on top, `new_parent.attr("id")` was `taxonomy_tree` which was causing DB error

--- app/assets/javascripts/admin/spree/taxons/taxonomy.js.coffee
+++ app/assets/javascripts/admin/spree/taxons/taxonomy.js.coffee
@@ -9,12 +9,17 @@
   new_parent = data.rslt.np

   url = new URL(base_url)
-  url.pathname = url.pathname + '/' + node.attr("id")
+  url.pathname = url.pathname + '/' + node.attr("id")
+  data = {
+    _method: "put",
+    "taxon[position]": position,
+    "taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
+  }
   $.ajax
     type: "POST",
     dataType: "json",
     url: url.toString(),
-    data: ({_method: "put", "taxon[parent_id]": new_parent.attr("id"), "taxon[position]": position }),
+    data: data,
     error: handle_ajax_error

   true
@@ -26,11 +31,16 @@
   position = data.rslt.position
   new_parent = data.rslt.parent

+  data = {
+    "taxon[name]": name,
+    "taxon[position]": position
+    "taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
+  }
   $.ajax
     type: "POST",
     dataType: "json",
     url: base_url.toString(),
-    data: ({"taxon[name]": name, "taxon[parent_id]": new_parent.attr("id"), "taxon[position]": position }),
+    data: data,
     error: handle_ajax_error,
     success: (data,result) ->
       node.attr('id', data.id)


Use course home_wiki for user links in Details component

Resolves #921

--- app/assets/javascripts/components/overview/inline_users.cjsx
+++ app/assets/javascripts/components/overview/inline_users.cjsx
@@ -5,9 +5,10 @@
   displayName: 'InlineUsers'
   render: ->
     key = @props.title + '_' + @props.role
+    baseUrl = "https://#{@props.course.home_wiki.language}.#{@props.course.home_wiki.project}.org/wiki/User:"
     last_user_index = @props.users.length - 1
     user_list = @props.users.map (user, index) ->
-      link = "https://en.wikipedia.org/wiki/User:#{user.username}"
+      link = "#{baseUrl}#{user.username}"
       if user.real_name?
         extra_info = " (#{user.real_name}#{if user.email? then " / " + user.email else ""})"
       else


More helpful failure messages.

More helpful failure messages.

--- spec/fuzzy-finder-spec.coffee
+++ spec/fuzzy-finder-spec.coffee
@@ -913,7 +913,7 @@
       expect(projectView.filterEditorView.getText()).toBe 'this should show up next time we open finder'
       expect(projectView.filterEditorView.getModel().getSelectedText()).toBe 'this should show up next time we open finder'

-  fdescribe "Git integration", ->
+  describe "Git integration", ->
     [projectPath, gitRepository, gitDirectory] = []

     beforeEach ->
@@ -986,8 +986,8 @@
           runs ->
             dispatchCommand('toggle-buffer-finder')

-          waitsFor ->
-            bufferView.find('.status.status-modified').length is 1
+          waitsFor 'the modified item', ->
+            bufferView.find('.status.status-modified').length > 0

           runs ->
             expect(bufferView.find('.status.status-modified').closest('li').find('.file').text()).toBe 'a.txt'
@@ -1003,7 +1003,7 @@
           runs ->
             dispatchCommand('toggle-buffer-finder')

-          waitsFor ->
+          waitsFor 'the added item', ->
             bufferView.find('.status.status-added').length > 0

           runs ->


Try 50?

Try 50?

--- spec/menu-manager-spec.coffee
+++ spec/menu-manager-spec.coffee
@@ -62,7 +62,7 @@
       atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b'
       menu.update()

-      waits 1
+      waits 50

       runs -> expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toEqual ['ctrl-b']


- Add trigger to form

- Add trigger to form

--- vendor/assets/javascripts/ajax_nested_form.js.coffee
+++ vendor/assets/javascripts/ajax_nested_form.js.coffee
@@ -10,6 +10,10 @@
 	@addFields: (link, association, content, container, position) ->
 		new_id = new Date().getTime()
 		regexp = new RegExp("new_" + association, "g")
-		content_new = content.replace(regexp, new_id)
+		content_new = $(content.replace(regexp, new_id))
 		if position is "first" then $(container).prepend(content_new) else $(container).append(content_new)
+		$(document).trigger
+			type: "ajax_nested:fieldAdded",
+			field: content_new
+
 		return


Use `atom.activatePackage` directly in specs

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -8,7 +8,7 @@

   beforeEach ->
     window.rootView = new RootView
-    window.loadPackage("symbols-view")
+    atom.activatePackage("symbols-view")

     rootView.attachToDom()
     setArraySpy = spyOn(SymbolsView.prototype, 'setArray').andCallThrough()


Use emit instead of trigger to TextBuffer events

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -61,7 +61,7 @@
         expect(symbolsView.loading).toBeEmpty()
         expect(symbolsView.list.children('li').length).toBe 2
         expect(symbolsView.generateTags).not.toHaveBeenCalled()
-        editSession.getBuffer().trigger 'saved'
+        editSession.getBuffer().emit 'saved'
         setArraySpy.reset()
         symbolsView.cancel()
         rootView.getActiveView().trigger "symbols-view:toggle-file-symbols"


Wait on DOM elements instead of spies

Wait on DOM elements instead of spies

An attempt to prevent CI flakiness where sometimes the spy might have
called during the activation promise polling.

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -6,17 +6,14 @@
 TagGenerator = require '../lib/tag-generator'

 describe "SymbolsView", ->
-  [symbolsView, setItemsSpy, setErrorSpy, activationPromise] = []
+  [symbolsView, activationPromise] = []

   beforeEach ->
     atom.workspaceView = new WorkspaceView
     atom.project.setPath(temp.mkdirSync('atom-symbols-view-'))
     fs.copySync(path.join(__dirname, 'fixtures'), atom.project.getPath())
-
     activationPromise = atom.packages.activatePackage("symbols-view")
-
     atom.workspaceView.attachToDom()
-    setItemsSpy = spyOn(SymbolsView.prototype, 'setItems').andCallThrough()

   describe "when tags can be generated for a file", ->
     it "initially displays all JavaScript functions with line numbers", ->
@@ -31,7 +28,7 @@
         expect(symbolsView.loading).toHaveText 'Generating symbols...'

       waitsFor ->
-        setItemsSpy.callCount > 0
+        symbolsView.list.children('li').length > 0

       runs ->
         expect(symbolsView.loading).toBeEmpty()
@@ -54,28 +51,26 @@
         symbolsView = atom.workspaceView.find('.symbols-view').view()

       waitsFor ->
-        setItemsSpy.callCount > 0
-
-      runs ->
-        setItemsSpy.reset()
+        symbolsView.list.children('li').length > 0
+
+      runs ->
         symbolsView.cancel()
         spyOn(symbolsView, 'generateTags').andCallThrough()
         atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"

       waitsFor ->
-        setItemsSpy.callCount > 0
+        symbolsView.list.children('li').length > 0

       runs ->
         expect(symbolsView.loading).toBeEmpty()
         expect(symbolsView.list.children('li').length).toBe 2
         expect(symbolsView.generateTags).not.toHaveBeenCalled()
         editor.getBuffer().emit 'saved'
-        setItemsSpy.reset()
         symbolsView.cancel()
         atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"

       waitsFor ->
-        setItemsSpy.callCount > 0
+        symbolsView.list.children('li').length > 0

       runs ->
         expect(symbolsView.loading).toBeEmpty()
@@ -95,7 +90,7 @@
         symbolsView = atom.workspaceView.find('.symbols-view').view()

       waitsFor ->
-        setItemsSpy.callCount > 0
+        symbolsView.list.children('li').length > 0

       runs ->
         symbolsView.filterEditorView.setText("nothing will match this")
@@ -123,10 +118,9 @@

       runs ->
         symbolsView = atom.workspaceView.find('.symbols-view').view()
-        setErrorSpy = spyOn(symbolsView, "setError").andCallThrough()
-
-      waitsFor ->
-        setErrorSpy.callCount > 0
+
+      waitsFor ->
+        symbolsView.error.isVisible()

       runs ->
         expect(symbolsView).toExist()
@@ -260,7 +254,7 @@
         expect(symbolsView.loading).toHaveText 'Loading symbols...'

       waitsFor ->
-        setItemsSpy.callCount > 0
+        symbolsView.list.children('li').length > 0

       runs ->
         expect(symbolsView.loading).toBeEmpty()
@@ -287,7 +281,7 @@
             symbolsView = atom.workspaceView.find('.symbols-view').view()

           waitsFor ->
-            setItemsSpy.callCount > 0
+            symbolsView.list.children('li').length > 0

           runs ->
             spyOn(atom.workspaceView, 'open').andCallThrough()


Fix scroll to same hash

Fix scroll to same hash

Clicking a hash link did not trigger the adjustment if it was the same
hash as was loaded in the URL.

--- src/scripts/modules/media/media.coffee
+++ src/scripts/modules/media/media.coffee
@@ -42,6 +42,7 @@
     events:
       'keydown .media-title > .title input': 'checkKeySequence'
       'keyup .media-title > .title input': 'resetKeySequence'
+      'click a[href*="#"]': 'triggerHashChange'

     initialize: (options) ->
       super()
@@ -63,6 +64,9 @@
       @listenTo(@model, 'change:title change:currentPage change:currentPage.loaded', @updateUrl)
       @listenTo(@model, 'change:title change:currentPage change:currentPage.loaded', @updatePageInfo)
       @listenTo(@model, 'change:abstract', @updateSummary)
+
+    triggerHashChange: (e) ->
+      Backbone.trigger('window:hashChange')

     onRender: () =>
       @regions.media.append(new MediaEndorsedView({model: @model}))
@@ -98,19 +102,15 @@
         newHeight = window.innerHeight - tocTop
         $toc.height("#{newHeight}px")
       Backbone.on('window:optimizedResize', setTocHeight)
+
       adjustHashTop = ->
         handleHeaderViewPinning()
         if isPinned
           obscured = $pinnable.height()
           top = $(window.location.hash).position().top
           $(window).scrollTop(top - obscured)
-          # On page load, might need to re-adjust
-          setTimeout(->
-            top2 = $(window.location.hash).position().top
-            if top2 != top
-              $(window).scrollTop(top2 - obscured)
-          , 125)
-      Backbone.on('window:hashChange', adjustHashTop)
+
+      Backbone.on('window:hashChange', _.debounce(adjustHashTop, 150))

       # closing is triggered in 'onBeforeClose'
       @on('closing', ->


Actually send the data to Countly.

Actually send the data to Countly.

--- services/web/public/coffee/main/event.coffee
+++ services/web/public/coffee/main/event.coffee
@@ -6,10 +6,24 @@
 		return {
 			send: (category, action, label, value)->
 				ga('send', 'event', category, action, label, value)
-				event_name = "#{action}-#{category}"
-				window?.heap?.track?(event_name, {label, value})
+
+			sendCountly: (key, segmentation) ->
+				eventData = { key }
+				eventData.segmentation = segmentation if segmentation?
+
+				Countly.q.push(['add_event', eventData]);
 		}

+	# App.directive "countlyTrack", () ->
+	# 	return {
+	# 		restrict: "A"
+	# 		scope: false,
+	# 		link: (scope, el, attrs) ->
+	# 			eventKey  = attrs.countlyTrack
+	# 			if (eventKey?)
+	# 				el.on "click", () ->
+	# 					console.log eventKey
+	# 	}

 	#header
 	$('.navbar a').on "click", (e)->


added kite apps model to bongo models

added kite apps model to bongo models

--- server/lib/server/bongo.coffee
+++ server/lib/server/bongo.coffee
@@ -17,6 +17,7 @@
     "workers/social/lib/social/models/activity/cache.coffee",
     "workers/social/lib/social/models/account.coffee"
     "workers/social/lib/social/models/kites.coffee"
+    "workers/social/lib/social/models/kiteapps.coffee"
   ].map (path)-> nodePath.join projectRoot, path
   mq: new Broker mqOptions
   resourceName: webserver.queueName


Kite: Return free plan for os-kite.

Kite: Return free plan for os-kite.

--- server/lib/server/index.coffee
+++ server/lib/server/index.coffee
@@ -177,6 +177,11 @@

               if subscribed then res.send 200, userPlan
               else
+                if kite.name is "OsKite" # Find a better way to return free plan for os-kite
+                  freePlan = plan for plan in plans when plan.tags.indexOf("nosync") > -1
+                  {title, planCode} = freePlan
+                  return res.send 200, planName: title, planId: planCode
+
                 res.send 401, err: "NO_SUBSCRIPTION"

 app.get "/-/8a51a0a07e3d456c0b00dc6ec12ad85c", require './__notify-users'


Apply old Handlebars snippets to Mustache scope

Since the built-in language-mustache package highlights both Mustache
and Handlebars syntax, I've switched to it as a replacement for
language-handlebars. This commit switches over my old
language-handlebars snippets to reference the new scope name.

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -1044,7 +1044,7 @@
     'prefix': 'endfor'
     'body': '{% endfor %}'

-'.text.html.handlebars':
+'.text.html.mustache':

   'expression':
     'prefix': 'exp'


Fix permissions toggle for limited posts

--- assets/javascripts/views/permissions_fields_toggle.js.coffee
+++ assets/javascripts/views/permissions_fields_toggle.js.coffee
@@ -10,7 +10,7 @@

     @render()

-  context: (permissions = { public: true }) =>
+  context: (permissions = @parentView()?.post()?.get('permissions')) =>
     _.extend super,
       permissions: permissions


SocialWorker: Tests: fix tests for the modified method

--- workers/social/lib/social/models/account.test.coffee
+++ workers/social/lib/social/models/account.test.coffee
@@ -251,37 +251,17 @@
           appId      = generateRandomString()
           version    = '1.0.0'
           options    = { appId, version }
-          appStorage = null
-
-          queue = [
-
-            ->
-              # creating a new app storage
-              account.fetchOrCreateAppStorage options, (err, storage) ->
-                expect(err).to.not.exist
-                appStorage = storage
-                expect(storage.appId).to.be.equal appId
-                expect(storage.version).to.be.equal version
-                queue.next()
-
-            ->
-              # expecting relationship to be created
-              options =
-                as         : 'appStorage'
-                data       : { appId, version }
-                targetId   : appStorage.getId()
-                sourceId   : account.getId()
-                targetName : 'JAppStorage'
-                sourceName : 'JAccount'
-
-              expectRelation.toExist options, ->
-                queue.next()
-
-            -> done()
-
-          ]
-
-          daisy queue
+
+          # creating a new app storage
+          account.fetchOrCreateAppStorage options, (err, appStorage) ->
+            expect(err).to.not.exist
+            expect(appStorage).to.be.an 'object'
+            expect(appStorage.accountId).to.be.deep.equal account._id
+            expect(appStorage.storage[appId]).to.be.an 'object'
+            expect(appStorage.storage[appId]['version']).to.be.a 'string'
+            expect(appStorage.storage[appId]['data']).to.be.an 'object'
+            expect(appStorage.storage[appId]['data']).to.be.empty
+            done()

     describe 'when storage exists', ->
@@ -301,8 +281,12 @@
               account.fetchOrCreateAppStorage options, (err, storage) ->
                 expect(err).to.not.exist
                 appStorage = storage
-                expect(storage.appId).to.be.equal appId
-                expect(storage.version).to.be.equal version
+                expect(appStorage).to.be.an 'object'
+                expect(appStorage.accountId).to.be.deep.equal account._id
+                expect(appStorage.storage[appId]).to.be.an 'object'
+                expect(appStorage.storage[appId]['version']).to.be.a 'string'
+                expect(appStorage.storage[appId]['data']).to.be.an 'object'
+                expect(appStorage.storage[appId]['data']).to.be.empty
                 queue.next()

             ->
@@ -310,8 +294,8 @@
               account.fetchOrCreateAppStorage options, (err, storage) ->
                 expect(err).to.not.exist
                 expect(storage._id.toString()).to.be.equal appStorage._id.toString()
-                expect(storage.appId).to.be.equal appStorage.appId
-                expect(storage.version).to.be.equal appStorage.version
+                expect(storage.accountId).to.be.deep.equal appStorage.accountId
+                expect(storage.storage[appId]).to.be.deep.equal appStorage.storage[appId]
                 queue.next()

             -> done()


JSystemStatus: Allow ‘forceReload’ static event and 'restartCanceled', 'restartScheduled' instance events

--- workers/social/lib/social/models/systemstatus.coffee
+++ workers/social/lib/social/models/systemstatus.coffee
@@ -8,24 +8,26 @@
   @set
     sharedMethods    :
       static         : ['getCurrentSystemStatus','create'
-                        , 'stopCurrentSystemStatus','forceReload']
+                        'stopCurrentSystemStatus','forceReload']
+    sharedEvents     :
+      static         : ['forceReload']
+      instance       : ['restartCanceled', 'restartScheduled']
+
     schema           :
-      title : String
-      content : String
+      title          : String
+      content        : String
       scheduledAt    : Date
-      meta          : require 'bongo/bundles/meta'
+      meta           : require 'bongo/bundles/meta'
       status         :
         type         : String
         default      : 'active'
         enum         : ['Invalid status',
           ['active','stopped','paused']
         ]
-      type         :
+      type           :
         type         : String
         default      : 'restart'
-        enum         : ['Invalid type',
-          ['restart','info','reload']
-        ]
+        enum         : ['Invalid type', ['restart','info','reload']]

   createKodingError =(err)->
     if 'string' is typeof err


Remove unused file

Remove unused file

--- src/pages.coffee
+++ src/pages.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/


remove comment

remove comment

--- menus/atomizr.cson
+++ menus/atomizr.cson
@@ -1,4 +1,3 @@
-# See https://atom.io/docs/latest/hacking-atom-package-word-count#menus for more details
 'context-menu':
   'atom-text-editor': [
     {


Updated meta-engine.coffee

Updated meta-engine.coffee

--- meta-engine.coffee
+++ meta-engine.coffee
@@ -4,12 +4,14 @@
   optionMap: null
   contentProvider: null

-  constuctor: (optionMap)->
+  constructor: (optionMap = {})->

-    @optionMap = @__processOptionMap optionMap
+    @optionMap = optionMap = @__processOptionMap optionMap

     if optionMap.contentProvider
       @contentProvider = @__validateContentProvider optionMap.contentProvider
+
+

   setContentProvider: (contentProvider)->
@@ -54,6 +56,25 @@
     return optionMap

+  __ensureContentProvider: ->
+
+    unless @contentProvider
+      throw new Error "contentProvider is not assigned"
+
+
+  __processSync: (resourcePath)->
+
+    content = @contentProvider.getContentSync resourcePath
+
+    return content
+
+
+
+  processSync: (resourcePath)->
+
+    @__ensureContentProvider()
+
+    return @__processSync resourcePath


#266: adjust default to correct for "clean" no longer being applied at the end of the key generation

--- defaults/preferences/defaults.coffee
+++ defaults/preferences/defaults.coffee
@@ -9,7 +9,7 @@
 pref('extensions.zotero.translators.better-bibtex.cacheReset', 0)
 pref('extensions.zotero.translators.better-bibtex.caching', true)
 pref('extensions.zotero.translators.better-bibtex.citeCommand', 'cite')
-pref('extensions.zotero.translators.better-bibtex.citekeyFormat', '[auth:lower][Title:alphanum:fold:nopunct:skipwords:select,1,1:lower:prefix,_][year:prefix,_][0]')
+pref('extensions.zotero.translators.better-bibtex.citekeyFormat', '[auth:lower][Title:fold:nopunct:skipwords:select,1,1:lower:alphanum:prefix,_][year:prefix,_][0]')
 pref('extensions.zotero.translators.better-bibtex.debug', false)
 pref('extensions.zotero.translators.better-bibtex.DOIandURL', 'both')
 pref('extensions.zotero.translators.better-bibtex.fancyURLs', false)


Set downtime next to status for readability. Clean up coffeescript a bit - make the grid size a var

--- widgets/haproxy_down_hosts/haproxy_down_hosts.coffee
+++ widgets/haproxy_down_hosts/haproxy_down_hosts.coffee
@@ -1,10 +1,10 @@
 class Dashing.HaproxyDownHosts extends Dashing.Widget
+  width = 4

   ready: ->
-    # This is fired when the widget is done being rendered
     $(@node).find('.gridster ul').gridster({
-      max_cols: 4
-      min_cols: 4
+      max_cols: width,
+      min_cols: width
     })

   onData: (data) ->
@@ -20,14 +20,14 @@
       widget = grid.find('li[id="'+id+'"]')

       if !widget.length
-        widget = gridster.add_widget('

s

Sessions:

');
+        widget = gridster.add_widget('

 s

Sessions:

');
         widget.find('#host').text(host.label)
         widget.find('#svname').text(host.svname)
       else
         $(widget).removeClass "expired"

       widget.find('#status').text(host.status)
-      widget.find('#downtime span').text(host.downtime)
+      widget.find('#downtime').text(host.downtime)
       widget.find('#sessions span').text(host.sessions)

       if host.status == 'MAINT'
@@ -40,6 +40,6 @@
       gridster.remove_widget( widget, (removedNode) ->
         # Redraw the grid - this isn't automagical
         $(grid.find('li')).each (i, widget) ->
-          $(widget).attr 'data-col', i%4+1
-          $(widget).attr 'data-row', Math.ceil((i+1)/4)
+          $(widget).attr 'data-col', i%width+1
+          $(widget).attr 'data-row', Math.ceil((i+1)/width)
     )


remove unnecessary line

remove unnecessary line

--- src/quill.coffee
+++ src/quill.coffee
@@ -62,7 +62,6 @@
     @options = _.defaults(options, Quill.DEFAULTS)
     @options.modules = moduleOptions
     @options.id = @id = "ql-editor-#{Quill.editors.length + 1}"
-    @options.emitter = this
     @modules = {}
     @root = this.addContainer('ql-editor')
     @editor = new Editor(@root, this, @options)


Disable angular html5Mode.

Disable angular html5Mode.

--- services/web/public/coffee/main.coffee
+++ services/web/public/coffee/main.coffee
@@ -37,7 +37,7 @@
 		($locationProvider) ->
 			try
 				$locationProvider.html5Mode({
-					enabled: true,
+					enabled: false,
 					requireBase: false,
 					rewriteLinks: false
 				})


Add comment about st/mt

Add comment about st/mt

--- drivers/javascript/ast.coffee
+++ drivers/javascript/ast.coffee
@@ -440,10 +440,12 @@
             res.push(opts)
         res

+    # a class that extends RDBOp should have a property `st` or `mt` depending on
+    # how the term should be printed (see `compose` below)
     compose: (args, optargs) ->
         if @st
             return ['r.', @st, '(', intspallargs(args, optargs), ')']
-        else
+        else # @mt is defined
             if shouldWrap(@args[0])
                 args[0] = ['r(', args[0], ')']
             return [args[0], '.', @mt, '(', intspallargs(args[1..], optargs), ')']


Fix wrong class being added when results are hidden

--- resources/assets/coffee/react/contest/art-entry.coffee
+++ resources/assets/coffee/react/contest/art-entry.coffee
@@ -33,8 +33,9 @@

     div style: { backgroundImage: "url('#{@props.entry.preview}')" }, className: [
       'contest-art-list__entry',
-      'contest-art-list__entry--result' if showVotes
-      if showVotes && top3 then "contest-art-list__entry--placed-#{place}" else 'contest-art-list__entry--smaller',
+      'contest-art-list__entry--result' if showVotes,
+      "contest-art-list__entry--placed-#{place}" if showVotes && top3,
+      'contest-art-list__entry--smaller' if showVotes && !top3
     ].join(' '),
       a {
         className: [


input: add autocapitalize attribute. fixes #7

--- input/index.coffee
+++ input/index.coffee
@@ -19,7 +19,8 @@
       error: @error
     }

-  render: ({colors, hintText, type, isFloating, isDisabled, isDark}) =>
+  render: ({colors, hintText, type, isFloating,
+    isDisabled, isDark, autocapitalize}) =>
     {value, error, isFocused} = @state.getValue()

     colors ?= {
@@ -29,6 +30,7 @@
     type ?= 'text'
     isFloating ?= false
     isDisabled ?= false
+    autocapitalize ?= 'none'

     z '.zp-input',
       className: z.classKebab {
@@ -49,6 +51,7 @@
         attributes:
           disabled: if isDisabled then true else undefined
           type: type
+          autocapitalize: autocapitalize
         value: value
         oninput: z.ev (e, $$el) =>
           @value.onNext $$el.value


translate image script

translate image script

--- scripts/google-images.coffee
+++ scripts/google-images.coffee
@@ -8,12 +8,12 @@
 #   hubot mustache me <query> - Searches Google Images for the specified query and mustaches it.

 module.exports = (robot) ->
-  robot.respond /(image|img)( me)? (.*)/i, (msg) ->
-    imageMe msg, msg.match[3], (url) ->
+  robot.respond /покажи(?: мне)? (.*)/i, (msg) ->
+    imageMe msg, msg.match[1], (url) ->
       msg.send url

-  robot.respond /animate( me)? (.*)/i, (msg) ->
-    imageMe msg, msg.match[2], true, (url) ->
+  robot.respond /анимируй(?: мне)? (.*)/i, (msg) ->
+    imageMe msg, msg.match[1], true, (url) ->
       msg.send url

   robot.respond /(?:mo?u)?sta(?:s|c)he?(?: me)? (.*)/i, (msg) ->


pass in directly

pass in directly

--- models/fair.coffee
+++ models/fair.coffee
@@ -64,16 +64,15 @@
       error: ->
         options?.error()

-  fetchArtists: (options) ->
+  fetchArtists: (options = {}) ->
     artists = new @aToZCollection('artist')
     artists.fetchUntilEnd
       url: "#{@url()}/artists"
       cache: true
       success: ->
         aToZGroup = artists.groupByAlphaWithColumns 3
-        options?.success aToZGroup, artists
-      error: ->
-        options?.error()
+        options.success aToZGroup, artists
+      error: options.error

   fetchSections: (options) ->
     sections = new Backbone.Collection


Rephrased lintott locater

Rephrased lintott locater

--- scripts/lintott.coffee
+++ scripts/lintott.coffee
@@ -18,7 +18,7 @@
           json = JSON.parse(body)
           place = json.response.checkins.items[0].venue.name
           type = json.response.checkins.items[0].venue.categories[0].name
-          text = "Lintott is at #{place}, which is a #{type}"
+          text = "Lintott last spotted at #{place}, which is a #{type}"
         catch error
           text = "No info found"
         msg.send text


Fix typo

Fix typo

--- src/robot.coffee
+++ src/robot.coffee
@@ -159,7 +159,7 @@
       catch error
         @logger.error "Unable to load #{full}: #{error}\n#{error.stack}"

-  # Public: Load scripts specfic in the `hubot-scripts.json` file.
+  # Public: Load scripts specfied in the `hubot-scripts.json` file.
   #
   # path    - A String path to the hubot-scripts files.
   # scripts - An Array of scripts to load.


sane defaults in the xml2js parser

sane defaults in the xml2js parser

--- scripts/octocat.coffee
+++ scripts/octocat.coffee
@@ -15,7 +15,10 @@
   msg.http('http://feeds.feedburner.com/Octocats')
     .query(format: 'xml')
     .get() (err, res, body) ->
-      parser = new xml2js.Parser()
+      parser = new xml2js.Parser
+        explicitArray: false
+        normalizeTags: true
+        attrkey: '@'
       parser.parseString body, (err, result) ->
-        octocats = (r['content'][0]['div'][0]['a'][0]['img'][0]['$']['src'] for r in result.feed.entry)
+        octocats = (r['content']['div']['a']['img']['@']['src'] for r in result.feed.entry)
         msg.send msg.random octocats for i in [1..count]


tweaked discrete axis size buffer

tweaked discrete axis size buffer

--- src/viz/types/helpers/graph/includes/buffer.coffee
+++ src/viz/types/helpers/graph/includes/buffer.coffee
@@ -43,21 +43,31 @@

         else if vars.axes.scale

+          difference = Math.abs domain[1] - domain[0]
+          additional = difference / (vars[axis].ticks.values.length - 1)
+          additional = additional / 2
+
           rangeMax = vars[axis].scale.viz.range()[1]
-          maxSize  = vars.axes.scale.range()[1]
+          maxSize  = vars.axes.scale.range()[1] * 1.5
+          domainLow = vars[axis].scale.viz.invert -maxSize
+          domainHigh  = vars[axis].scale.viz.invert rangeMax + maxSize

-          domainLow = vars[axis].scale.viz.invert -maxSize*1.5
-          domainHigh  = vars[axis].scale.viz.invert rangeMax + maxSize*1.5
+          if domain[0] - additional < domainLow

-          domain = [domainLow, domainHigh]
-          domain = domain.reverse() if axis is "y"
+            domain[0] = domain[0] - additional
+            domain[1] = domain[1] + additional

-          domainCompare = vars[axis].scale.viz.domain()
-          domainCompare = domainCompare[1] - domainCompare[0]
+          else

-          unless domainCompare
-            domain[0] -= 1
-            domain[1] += 1
+            domain = [domainLow, domainHigh]
+            domain = domain.reverse() if axis is "y"
+
+            domainCompare = vars[axis].scale.viz.domain()
+            domainCompare = domainCompare[1] - domainCompare[0]
+
+            unless domainCompare
+              domain[0] -= 1
+              domain[1] += 1

         else
           difference = Math.abs domain[1] - domain[0]


Added myself.

Added myself.

--- scripts/name-expander.coffee
+++ scripts/name-expander.coffee
@@ -15,6 +15,7 @@
 steve = "@srude"
 fei = "@fei"
 brad = "@brad"
+cquinn = "@cquinn"

 noc_members = ["Carter1", "Cames1", "jdowns", "jdowns1", "jtramullas", "nstuhring", "ctrytten1", "ctrytten", "akim", "nsanches", "jkim", "ebednik", "nhooper"]
@@ -28,7 +29,8 @@
   kyle,
   fei,
   cliff,
-  brad
+  brad,
+  cquinn
 ]

 chef_repo_mergers = [


Call Events.unbind() regardless, just return only if there were arguments

--- src/route.coffee
+++ src/route.coffee
@@ -71,8 +71,8 @@
     @change()

   @unbind: ->
-    # forward to Events.unbind() if given arguments
-    return Spine.Events.unbind.apply this, arguments if arguments.length > 0
+    unbindResult = Spine.Events.unbind.apply this, arguments
+    return unbindResult if arguments.length > 0

     return if @options.shim


Add mouseover events to footnote triggers, adjust placement of footnotes for iPad

--- app/assets/javascripts/alongslide/alongslide.coffee
+++ app/assets/javascripts/alongslide/alongslide.coffee
@@ -119,20 +119,21 @@
       $column = $el.parents('.column')
       $column.append($footnote)

-      # Place footnote, prevent placing offscreen
-      #
-      bottom = $column.height() + $column.offset().top
-      if ($el.offset().top + $footnote.height()) > bottom
-        $footnote.css('bottom', 0)
-      else
-        $footnote.css('top', $el.parent().position().top )
-
-      $el.on 'click', (e) ->
+      $el.on 'click mouseenter', (e) ->
         e.preventDefault()
         e.stopImmediatePropagation()
+
+        # Place footnote, prevent placing offscreen
+        #
+        bottom = $column.height() + $column.offset().top
+        if ($el.offset().top + $footnote.height()) > bottom
+          $footnote.css('bottom', 0)
+        else
+          $footnote.css('top', $el.parent().position().top )
+
         $footnote.fadeIn(150)

-      $footnote.on 'click', (e) ->
+      $footnote.on 'click mouseleave', (e) ->
         setTimeout ->
           $footnote.fadeOut(150)
         , 100


fix sort

fix sort

--- app/assets/javascripts/app/models/message.js.coffee
+++ app/assets/javascripts/app/models/message.js.coffee
@@ -1,5 +1,5 @@
 class App.Message extends Spine.Model
-  @configure 'Message', 'subject', 'body', 'starred', 'to'
+  @configure 'Message', 'subject', 'body', 'starred', 'to', 'sent_at'
   @extend Spine.Model.Ajax
   @extend Spine.Timestamps

@@ -19,5 +19,5 @@
       record.conversation()?.save()

   @sort: (a, b) ->
-    later = Date.parse(a.updated_at) < Date.parse(b.updated_at)
+    later = Date.parse(a.sent_at) < Date.parse(b.sent_at)
     if later then -1 else 1


Fix selectAll for angular upgrade

Fix selectAll for angular upgrade

--- services/web/public/coffee/directives/selectAll.coffee
+++ services/web/public/coffee/directives/selectAll.coffee
@@ -5,20 +5,14 @@
 		return {
 			controller: ["$scope", ($scope) ->
 				# Selecting or deselecting all should apply to all projects
-				selectAll = () ->
+				@selectAll = () ->
 					$scope.$broadcast "select-all:select"

-				deselectAll = () ->
+				@deselectAll = () ->
 					$scope.$broadcast "select-all:deselect"

-				clearSelectAllState = () ->
+				@clearSelectAllState = () ->
 					$scope.$broadcast "select-all:clear"
-
-				return {
-					clearSelectAllState: clearSelectAllState
-					selectAll: selectAll
-					deselectAll: deselectAll
-				}
 			]
 			link: (scope, element, attrs) ->


fixed type in ajax pagination spinner

fixed type in ajax pagination spinner

--- app/assets/javascripts/pagination_spinner.js.coffee
+++ app/assets/javascripts/pagination_spinner.js.coffee
@@ -15,7 +15,7 @@
       @el.wrapInner( "

" );
       @el.find('.pagination-wrapper').wrapInner( "

" );
       # add the spinner element
-      @el.find('.pagination-wrapper').prepend('
' );
+      @el.find('.pagination-wrapper').prepend('' );
       # add event to the pagination links
       @el.find('ul.pagination a').click (event) =>
         @el.find('.pagination-content').addClass('dimmed')


Bugfix.

Bugfix.

--- app/assets/javascripts/views/discussion_view.coffee
+++ app/assets/javascripts/views/discussion_view.coffee
@@ -93,7 +93,6 @@
     view = new WipEventView(model: wipEvent)
     el = view.render().el
     @$('.timeline,.discussion').append el
-    @scrollToBottom()

   eventPushed: (msg) =>
     return if msg.socket_id == pusher.connection.socket_id
@@ -168,7 +167,6 @@
       timeout: 5
       notifyClick: =>
         $(window).focus()
-        @scrollToBottom()

     n.show()


Quiz deletion flash notice

Quiz deletion flash notice

FIxes CNVS-20919

Test Plan
 - There should be a flash message notifying successful deletion when a Quiz is deleted from the Quizzes page

Change-Id: Ib680d123b0bb261523250d3ccb95e0fdbf2ebb96
Reviewed-on: https://gerrit.instructure.com/56160
Tested-by: Jenkins
Product-Review: Jason Sparks <542e620f0c6c3447ce6e2e8c83a97102b99f89c6@instructure.com>
Reviewed-by: Ryan Taylor <2eafa1f6bf34395b543d6ca7f438461b0774d24f@instructure.com>
QA-Review: Michael Hargiss <59e3fde81b8d9a4f5e0cc016a3c2695591912604@instructure.com>

--- app/coffeescripts/views/quizzes/QuizItemView.coffee
+++ app/coffeescripts/views/quizzes/QuizItemView.coffee
@@ -27,6 +27,8 @@
     messages:
       confirm: I18n.t('confirms.delete_quiz', 'Are you sure you want to delete this quiz?')
       multipleDates: I18n.t('multiple_due_dates', 'Multiple Dates')
+      deleteSuccessful: I18n.t('flash.removed', 'Quiz successfully deleted.')
+      deleteFail: I18n.t('flash.fail', 'Quiz deletion failed.')

     initialize: (options) ->
       @initializeChildViews()
@@ -63,8 +65,14 @@

     # delete quiz item
     delete: ->
-      @model.destroy()
-      @$el.remove()
+      @$el.hide()
+      @model.destroy
+        success : =>
+          @$el.remove()
+          $.flashMessage @messages.deleteSuccessful
+        error : =>
+          @$el.show()
+          $.flashError @messages.deleteFail

     observeModel: ->
       @model.on('change:published', @updatePublishState)


COMMENT - TODO

COMMENT - TODO

--- source/Art/Engine/Core/Drawing/ElementDrawMixin.coffee
+++ source/Art/Engine/Core/Drawing/ElementDrawMixin.coffee
@@ -450,6 +450,10 @@
       d2eMatrix = Matrix.translateXY(-elementSpaceDrawArea.x, -elementSpaceDrawArea.y).scale(cacheScale).inv
       if d2eMatrix.eq(@_drawCacheToElementMatrix) && drawCacheManager.canUseBitmap @_drawCacheBitmap, cacheSpaceDrawArea
         drawCacheManager.useDrawCache @
+        # TODO:
+        #   REMOVE: clearOutsideArea all; instead, ensure we are properly setting dirtyDrawAreas:
+        #   We should actually be setting a dirtyDrawArea when the Element shrinks to redraw
+        #   the area it previously covered...
         @_drawCacheBitmap.clearOutsideArea cacheSpaceDrawArea.size
         return unless @_dirtyDrawAreas || @_redrawAll
       else


Use Slugify directly in app/core/utils.coffee (#61)

--- app/core/utils.ozar.coffee
+++ app/core/utils.ozar.coffee
@@ -1,4 +1,4 @@
-slugify = _.str?.slugify ? _.string?.slugify # TODO: why _.string on client and _.str on server?
+slugify = require('underscore.string').slugify # TODO: why _.string on client and _.str on server?

 clone = (obj) ->
   return obj if obj is null or typeof (obj) isnt 'object'


adding a customization for replacing the "what do you think?" callout

--- @client/franklin.coffee
+++ @client/franklin.coffee
@@ -227,16 +227,20 @@

         ProposalDescription()

-        H1
-          style: _.defaults {}, customization('list_label_style'),
-            fontSize: 36
-            fontWeight: 700
-            fontStyle: 'oblique'
-            textAlign: 'center'
-            marginBottom: 8
-            marginTop: 48
-
-          'What do you think?'
+
+        if customization('opinion_callout')
+          customization('opinion_callout')()
+        else
+          H1
+            style: _.defaults {}, customization('list_label_style'),
+              fontSize: 36
+              fontWeight: 700
+              fontStyle: 'oblique'
+              textAlign: 'center'
+              marginBottom: 8
+              marginTop: 48
+
+            'What do you think?'

         if customization('opinion_filters')


revert content type change, which was causing upload problems

--- lib/assets/javascripts/easiest_ever_ajax_uploads.coffee
+++ lib/assets/javascripts/easiest_ever_ajax_uploads.coffee
@@ -37,8 +37,7 @@
         type: "POST"
         data: fd
         processData: false
-        # contentType: file.type
-        enctype: 'multipart/form-data'
+        contentType: false
         crossDomain: true
         success: (data, textStatus, jqXHR) ->
           location = jqXHR.getResponseHeader("Location")


pivotal: add ability to notify on reopen

--- plugins/pivotal-tracker/index.coffee
+++ plugins/pivotal-tracker/index.coffee
@@ -4,9 +4,48 @@
 NotificationPlugin = require "../../notification-plugin"

 class PivotalTracker extends NotificationPlugin
-  @receiveEvent: (config, event, callback) ->
-    return if event?.trigger?.type == "reopened"
+  BASE_URL = "https://www.pivotaltracker.com/services/v3/projects"

+  @storiesUrl: (config) ->
+    "#{BASE_URL}/#{config.projectId}/stories"
+
+  @storyUrl: (config, storyId) ->
+    @storiesUrl(config) + "/" + storyId
+
+  @notesUrl: (config, storyId) ->
+    @storyUrl(config, storyId) + "/notes"
+
+  @ensureIssueOpen: (config, storyId, callback) ->
+    params =
+      "story[current_state]": "unstarted"
+
+    @request
+      .put(@storyUrl(config, storyId))
+      .timeout(4000)
+      .set("X-TrackerToken", config.apiToken)
+      .type("form")
+      .send(params)
+      .buffer(true)
+      .on "error", (err) ->
+        callback(err)
+      .end (res) ->
+        return callback(res.error) if res.error
+
+  @addCommentToIssue: (config, storyId, comment) ->
+    params =
+      "note[text]": comment
+
+    @request
+      .post(@notesUrl(config, storyId))
+      .timeout(4000)
+      .set("X-TrackerToken", config.apiToken)
+      .type("form")
+      .send(params)
+      .buffer(true)
+      .on("error", console.error)
+      .end()
+
+  @openIssue: (config, event, callback) ->
     # Build the request
     params =
       "story[name]": "#{event.error.exceptionClass} in #{event.error.context}".truncate(5000)
@@ -24,7 +63,7 @@

     # Send the request to the url
     @request
-      .post("https://www.pivotaltracker.com/services/v3/projects/#{config.projectId}/stories")
+      .post(@storiesUrl(config))
       .timeout(4000)
       .set("X-TrackerToken", config.apiToken)
       .type("form")
@@ -42,4 +81,12 @@
             id: result.story.id
             url: result.story.url

+  @receiveEvent: (config, event, callback) ->
+    if event?.trigger?.type == "reopened"
+      if event.error?.createdIssue?.id
+        @ensureIssueOpen(config, event.error.createdIssue.id, callback)
+        @addCommentToIssue(config, event.error.createdIssue.id, @markdownBody(event))
+    else
+      @openIssue(config, event, callback)
+
 module.exports = PivotalTracker


Fix XML/OFX parsing.

Fix XML/OFX parsing.

self is no longer needed since we're using the this-perserving '=>' syntax.

--- application/chrome/content/wesabe/xml/Parser.coffee
+++ application/chrome/content/wesabe/xml/Parser.coffee
@@ -194,8 +194,8 @@

     wesabe.tryThrow 'xml.Parser', (log) =>
       parser.parse(xml)
-      delete self.parser
-      delete self.work
+      delete @parser
+      delete @work
       return work.nodes

   stop: ->


OX-758 Hold second reference to nodeIDs for simplicity

--- app/assets/javascripts/oxalis/geometries/tree.coffee
+++ app/assets/javascripts/oxalis/geometries/tree.coffee
@@ -9,7 +9,7 @@

     edgeGeometry = new THREE.Geometry()
     nodeGeometry = new THREE.Geometry()
-    nodeGeometry.nodeIDs = new ResizableBuffer(1, 100, Int32Array)
+    @nodeIDs = nodeGeometry.nodeIDs = new ResizableBuffer(1, 100, Int32Array)
     edgeGeometry.dynamic = true
     nodeGeometry.dynamic = true

@@ -36,7 +36,7 @@

     @nodesBuffer.clear()
     @edgesBuffer.clear()
-    @nodes.geometry.nodeIDs.clear()
+    @nodeIDs.clear()

   isEmpty : ->
@@ -47,7 +47,7 @@
   addNode : (node) ->

     @nodesBuffer.push(node.pos)
-    @nodes.geometry.nodeIDs.push([node.id])
+    @nodeIDs.push([node.id])

     # Add any edge from smaller IDs to the node
     # ASSUMPTION: if this node is new, it should have a
@@ -67,21 +67,19 @@

   deleteNode : (node) ->

-    nodeIDs  = @nodes.geometry.nodeIDs
-
     swapLast = (array, index) =>
       lastElement = array.pop()
       for i in [0..array.elementLength]
         @nodesBuffer.getAllElements()[index * array.elementLength + i] = lastElement[i]

     # Find index
-    for i in [0...nodeIDs.getLength()]
-      if nodeIDs.get(i) == node.id
+    for i in [0...@nodeIDs.getLength()]
+      if @nodeIDs.get(i) == node.id
         nodesIndex = i
         break

     # swap IDs and nodes
-    swapLast( nodeIDs, nodesIndex )
+    swapLast( @nodeIDs, nodesIndex )
     swapLast( @nodesBuffer, nodesIndex )

     # Delete Edge by finding it in the array
@@ -108,7 +106,7 @@
       @[property].pushSubarray(otherTree[property].getAllElements())

     # merge IDs, nodes and edges
-    @nodes.geometry.nodeIDs.pushSubarray(otherTree.nodes.geometry.nodeIDs.getAllElements())
+    merge("nodeIDs")
     merge("nodesBuffer")
     merge("edgesBuffer")
     @edgesBuffer.push( @getEdgeArray(lastNode, activeNode) )


Add more peasant names, and soldiers cannot be both Will and William

--- app/lib/world/names.coffee
+++ app/lib/world/names.coffee
@@ -21,7 +21,6 @@
     "Thelonious"
     "Miles"
     "Bill"
-    "Will"
     "Kumar"
     "Ricardo"
     "Maxwell"
@@ -72,6 +71,10 @@
     "Yusef"
     "Hingle"
     "Azgot"
+    "Piers"
+    "Carlton"
+    "Giselle"
+    "Bernadette"
   ]
   "Archer": [
     "Phoebe"


Don't clear the answers list when clicking the same link redundantly

--- app/assets/javascripts/views/relation_view.js.coffee
+++ app/assets/javascripts/views/relation_view.js.coffee
@@ -21,10 +21,11 @@
     @subscriptions = []

   setRelation: (relation) ->
+    return if relation == @relation
     @unsubscribe()
     @empty()

-    return if !relation? or relation == @relation
+    return return unless relation
     @relation = relation

     @relation.each (record, index) =>


add keyboard shortcut help to syllabus

add keyboard shortcut help to syllabus

fixes CNVS-16721
fixes CNVS-15961

This adds the blue help icon next to HTML Editor text.

Test Plan
As a user that can edit the syllabus
When you go the the syllabux page
And you click on the 'edit' button
Then you should see the tinymce editor
And there should be a blue help icon next to "HTML Editor"
And clicking on it should show the keyboard shortcuts help menu

Change-Id: I3e629506b9c331a7d4a3640d0bceb4071506f21f
Reviewed-on: https://gerrit.instructure.com/43971
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Reviewed-by: Dan Minkevitch <2591e5f46f28d303f9dc027d475a5c60d8dea17a@instructure.com>
Product-Review: Cosme Salazar <d37f5e06cd46b8fd23ce5553334e09ede5394232@instructure.com>
QA-Review: Jahnavi Yetukuri <ed2b2672ccc2e59e56c0615df7f01b579202e014@instructure.com>

--- app/coffeescripts/behaviors/SyllabusBehaviors.coffee
+++ app/coffeescripts/behaviors/SyllabusBehaviors.coffee
@@ -20,6 +20,7 @@
   'jquery' # jQuery, $ #
   'calendar_move' # calendarMonths #
   'wikiSidebar'
+  'compiled/views/editor/KeyboardShortcuts'
   'jquery.instructure_date_and_time' # dateString, datepicker #
   'jquery.instructure_forms' # formSubmit, formErrors #
   'jquery.instructure_misc_helpers' # scrollSidebar #
@@ -29,7 +30,7 @@
   'tinymce.editor_box' # editorBox #
   'vendor/jquery.scrollTo' # /\.scrollTo/ #
   'jqueryui/datepicker' # /\.datepicker/ #
-], ($, calendarMonths, wikiSidebar) ->
+], ($, calendarMonths, wikiSidebar, KeyboardShortcuts) ->

   specialDatesAreHidden = false

@@ -170,6 +171,10 @@

   # Binds to edit syllabus dom events
   bindToEditSyllabus = ->
+
+    # Add the backbone view for keyboardshortup help here
+    $('.toggle_views_link').first().before((new KeyboardShortcuts()).render().$el)
+
     $edit_course_syllabus_form = $('#edit_course_syllabus_form')
     $course_syllabus_body = $('#course_syllabus_body')
     $course_syllabus = $('#course_syllabus')


trying to expose the relationship hiding bug...

--- spec/coffeescripts/filters_spec.coffee
+++ spec/coffeescripts/filters_spec.coffee
@@ -1,19 +1,37 @@
 describe 'filters', ->

-    it 'can build hiddenNodeData for circular subgraph hidden by user', ->
-      # n0--->---n1-----<-----n3
-      #           \-->--n2->--/
-      nodeData = [{"id":0},{"id":1},{"id":2},{"id":3}]
-      relData = [{"id":0, "start_node":0, "end_node":1, "data":{"rel_type":""}},
-                 {"id":1, "start_node":1, "end_node":2, "data":{"rel_type":""}},
-                 {"id":2, "start_node":2, "end_node":3, "data":{"rel_type":""}}
-                 {"id":2, "start_node":3, "end_node":1, "data":{"rel_type":""}}]
-      graph = new Graph(nodeData, relData)
+  it 'can build hiddenNodeData for circular subgraph hidden by user', ->
+    # n0--->---n1-----<-----n3
+    #           \-->--n2->--/
+    nodeData = [{"id":0},{"id":1},{"id":2},{"id":3}]
+    relData = [{"id":0, "start_node":0, "end_node":1, "data":{"rel_type":""}},
+               {"id":1, "start_node":1, "end_node":2, "data":{"rel_type":""}},
+               {"id":2, "start_node":2, "end_node":3, "data":{"rel_type":""}}
+               {"id":3, "start_node":3, "end_node":1, "data":{"rel_type":""}}]
+    graph = new Graph(nodeData, relData)

-      activatedNode = graph.load(0)
-      relsHiddenByUser = graph.relationships[0..0]
-      hiddenNodeData =  test_buildHiddenNodeData(graph, activatedNode, relsHiddenByUser)
+    activatedNode = graph.load(0)
+    relsHiddenByUser = graph.relationships[0..0]
+    hiddenNodeData =  test_buildHiddenNodeData(graph, activatedNode, relsHiddenByUser)

-      expect(hiddenNodeData.nodeIds.length).toEqual(3)
-      expect(hiddenNodeData.relIds.length).toEqual(4) #4: rels hidden by user should be included
+    expect(hiddenNodeData.nodeIds.length).toEqual(3)
+    expect(hiddenNodeData.relIds.length).toEqual(4) #4: rels hidden by user should be included

+  it 'can build hiddenNodeData for 6 node mesh', ->
+        nodeData = [{"id":0},{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}]
+        relData = [{"id":0, "start_node":0, "end_node":1, "data":{"rel_type":""}},
+                   {"id":1, "start_node":1, "end_node":2, "data":{"rel_type":""}},
+                   {"id":2, "start_node":2, "end_node":3, "data":{"rel_type":""}},
+                   {"id":3, "start_node":3, "end_node":0, "data":{"rel_type":""}},
+                   {"id":4, "start_node":3, "end_node":4, "data":{"rel_type":""}},
+                   {"id":5, "start_node":4, "end_node":5, "data":{"rel_type":""}},
+                   {"id":6, "start_node":5, "end_node":0, "data":{"rel_type":""}}]
+        graph = new Graph(nodeData, relData)
+
+        activatedNode = graph.load(0)
+        relsHiddenByUser = graph.relationships[0..0]
+        hiddenNodeData =  test_buildHiddenNodeData(graph, activatedNode, relsHiddenByUser)
+
+        expect(hiddenNodeData.nodeIds.length).toEqual(0)
+        expect(hiddenNodeData.relIds.length).toEqual(1)
+


:green_heart: Fix TextEditorPresenter specs

--- spec/text-editor-presenter-spec.coffee
+++ spec/text-editor-presenter-spec.coffee
@@ -1233,12 +1233,15 @@
           lineId = presenter.linesByScreenRow.get(row)?.id
           tilesState[presenter.tileForRow(row)]?.lines[lineId]

-        tokensIncludeTag = (tokens, tag) ->
-          includeTag = false
-          for {openTags, closeTags} in tokens
-            includeTag = true for openTag in openTags when openTag.indexOf(tag) isnt -1
-            includeTag = true for closeTag in closeTags when closeTag.indexOf(tag) isnt -1
-          includeTag
+        tagsForCodes = (presenter, tagCodes) ->
+          openTags = []
+          closeTags = []
+          for tagCode in tagCodes when tagCode < 0 # skip text codes
+            if presenter.isOpenTagCode(tagCode)
+              openTags.push(presenter.tagForCode(tagCode))
+            else
+              closeTags.push(presenter.tagForCode(tagCode))
+          {openTags, closeTags}

         tiledContentContract (presenter) -> getState(presenter).content

@@ -1248,56 +1251,12 @@
             presenter.setExplicitHeight(3)

             expect(lineStateForScreenRow(presenter, 2)).toBeUndefined()
-            expectValues lineStateForScreenRow(presenter, 3), {
-              screenRow: 3, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: 'var pivot = items.shift(), current, left = [], right = [];'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 4), {
-              screenRow: 4, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: 'while(items.length > 0) {'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 5), {
-              screenRow: 5, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: 'current = items.shift();'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 6), {
-              screenRow: 6, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: 'current < pivot ? left.push(current) : right.push(current);'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 7), {
-              screenRow: 7, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: '}'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 8), {
-              screenRow: 8, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: ['leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: 'return sort(left).concat(pivot).concat(sort(right));'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
+            expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 4), {screenRow: 4, tagCodes: editor.screenLineForScreenRow(4).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 5), {screenRow: 5, tagCodes: editor.screenLineForScreenRow(5).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 6), {screenRow: 6, tagCodes: editor.screenLineForScreenRow(6).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 7), {screenRow: 7, tagCodes: editor.screenLineForScreenRow(7).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 8), {screenRow: 8, tagCodes: editor.screenLineForScreenRow(8).tagCodes}
             expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()

           it "updates when the editor's content changes", ->
@@ -1305,36 +1264,20 @@

             expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")

-            expectValues lineStateForScreenRow(presenter, 1), {
-              screenRow: 1, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar', 'leading-whitespace'], text: '  '},
-                {closeTags: ['leading-whitespace'], openTags: [], text: 'var sort = function(items) {'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text : ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 2), {
-              screenRow: 2, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar'], text: 'hello'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
-            expectValues lineStateForScreenRow(presenter, 3), {
-              screenRow: 3, tokens: [
-                {closeTags: [], openTags: ['text.plain.null-grammar'], text: 'world'},
-                {closeTags: ['text.plain.null-grammar'], openTags: [], text: ''}
-              ]
-            }
+            expectValues lineStateForScreenRow(presenter, 1), {screenRow: 1, tagCodes: editor.screenLineForScreenRow(1).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 2), {screenRow: 2, tagCodes: editor.screenLineForScreenRow(2).tagCodes}
+            expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}

           it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", ->
             editor.setText("hello\nworld\r\n")
             presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
-            expect(tokensIncludeTag(lineStateForScreenRow(presenter, 0).tokens, 'eol')).toBe(false)
-            expect(tokensIncludeTag(lineStateForScreenRow(presenter, 1).tokens, 'eol')).toBe(false)
+            expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).not.toContain('invisible-character eol')
+            expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).not.toContain('invisible-character eol')

             atom.config.set('editor.showInvisibles', true)
             presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
-            expect(tokensIncludeTag(lineStateForScreenRow(presenter, 0).tokens, 'eol')).toBe(true)
-            expect(tokensIncludeTag(lineStateForScreenRow(presenter, 1).tokens, 'eol')).toBe(true)
+            expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).toContain('invisible-character eol')
+            expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).toContain('invisible-character eol')

           describe ".blockDecorations", ->
             it "contains all block decorations that are present before/after a line, both initially and when decorations change", ->


time-series: fixed snapping rule did not show up when first loading view

--- app/charts/time-series.coffee
+++ app/charts/time-series.coffee
@@ -518,19 +518,12 @@

-
-
-
-
-
-
-
     if showRule

       foreground
         .on("mousemove", ->
           date = xscale.invert(d3.mouse(foreground[0][0])[0])
-          closest = findValuesWithTheClosestDate(vis.datum(), date)
+          closest = findValuesWithTheClosestDate(propData(vis.datum()), date)
           if closest?
             d = closest[d3.keys(closest)[0]]
             chart.moveRule(d[dateProp])


add a close() method to LockManager to allow clean shutdown

--- app/coffee/LockManager.coffee
+++ app/coffee/LockManager.coffee
@@ -70,3 +70,7 @@
 		action = (releaseLock) ->
 			releaseLock()
 		LockManager.runWithLock	"HistoryLock:HealthCheck:host=#{HOST}:pid=#{PID}:random=#{RND}", action, callback
+
+	close: (callback) ->
+		rclient.quit()
+		rclient.once 'end', callback


fix infinite loop

fix infinite loop

--- app/models/notebook.coffee
+++ app/models/notebook.coffee
@@ -8,10 +8,11 @@

   @extend @Sync

-  @.bind "beforeUpdate", (notebook) ->
-    notebook.updateAttributes {
-      "date": Math.round(new Date()/1000)
-    }
+  @.bind "beforeUpdate", (notebook, context) ->
+    if context isnt "date"
+      notebook.updateAttributes {
+        "date": Math.round(new Date()/1000)
+      }, "date"

   @.bind "beforeDestroy", (notebook) ->
     for note in Note.filter(notebook.id, "all")


View: Improve attribute serialization Handle circular references Handle collections Add synced template data value

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -252,7 +252,7 @@
             name = otherName
             break

-      # Brak if no view and name were found
+      # Break if no view and name were found
       return unless name and view and view.dispose

       # Dispose the view
@@ -267,29 +267,62 @@
     # Rendering
     # ---------

-    # Get attributes from model or collection
+    # Create an object which delegates to the model attributes
+    # so a custom getTemplateData might safely add and alter
+    # properties (at least primitive values).
+    # Nested models are mapped to their attributes, recursively.
+    serializeAttributes = (model, attributes, modelStack) ->
+      # Create a delegator on initial call
+      unless modelStack
+        delegator = utils.beget attributes
+        modelStack = [model]
+      else
+        # Add model to stack
+        modelStack.push model
+      # Map models to their attributes
+      for key, value of attributes when value instanceof Model
+        # Don’t change the original attribute, create a property
+        # on the delegator which shadows the original attribute
+        delegator = delegator or utils.beget attributes
+        delegator[key] = if value is model or value in modelStack
+          # Nullify circular references
+          null
+        else
+          # Serialize recursively
+          serializeAttributes(
+            value, value.getAttributes(), modelStack
+          )
+      # Remove model from stack
+      modelStack.pop()
+      # Return the delegator if it was created, otherwise the plain attributes
+      delegator or attributes
+
+    # Return the serialized attributes for a model
+    getModelAttributes = (model) ->
+      serializeAttributes model, model.getAttributes()
+
+    # Get the model/collection data for the templating function
     getTemplateData: ->
-      serialize = (object) ->
-        result = {}
-        for key, value of object
-          result[key] = if value instanceof Model
-            serialize value.getAttributes()
-          else
-            value
-        result
-
-      modelAttributes = @model and @model.getAttributes()
-      templateData = if modelAttributes
-        # Return an object which delegates to the returned attributes
-        # object so a custom getTemplateData might safely add and alter
-        # properties (at least primitive values).
-        utils.beget serialize modelAttributes
-      else
-        {}
-
-      # If the model is a Deferred, add a flag to get the Deferred state
-      if @model and typeof @model.state is 'function'
-        templateData.resolved = @model.state() is 'resolved'
+
+      if @model
+        # Serialize the models
+        templateData = getModelAttributes @model
+
+      else if @collection
+        # Serialize all models
+        items = (getModelAttributes model for model in @collection.models)
+        templateData = {items}
+
+      modelOrCollection = @model or @collection
+      if modelOrCollection
+
+        # If the model/collection is a Deferred, add a `resolved` flag
+        if typeof modelOrCollection .state is 'function'
+          templateData.resolved = modelOrCollection.state() is 'resolved'
+
+        # If the model/collection is a SyncMachine, add a `synced` flag
+        if typeof modelOrCollection .isSynced is 'function'
+          templateData.synced = modelOrCollection.isSynced()

       templateData

@@ -349,7 +382,7 @@
       return if @disposed

       # Dispose subviews
-      view.dispose() for view in @subviews
+      subview.dispose() for subview in @subviews

       # Unbind handlers of global events
       @unsubscribeAllEvents()


Fix removal buttons within filtringselect parents

Fix #2372

--- app/assets/javascripts/rails_admin/ra.widgets.coffee
+++ app/assets/javascripts/rails_admin/ra.widgets.coffee
@@ -63,7 +63,7 @@
     content.find('[data-filteringmultiselect]').each ->
       $(this).filteringMultiselect $(this).data('options')
       if $(this).parents("#modal").length # hide link if we already are inside a dialog (endless issues on nested dialogs with JS)
-        $(this).parents('.control-group').find('.btn').remove()
+        $(this).siblings('.btn').remove()
       else
         $(this).parents('.control-group').first().remoteForm()

@@ -72,7 +72,7 @@
     content.find('[data-filteringselect]').each ->
       $(this).filteringSelect $(this).data('options')
       if $(this).parents("#modal").length # hide link if we already are inside a dialog (endless issues on nested dialogs with JS)
-        $(this).parents('.control-group').find('.btn').remove()
+        $(this).siblings('.btn').remove()
       else
         $(this).parents('.control-group').first().remoteForm()


Find events by id in store

Find events by id in store

--- app/assets/javascripts/routes/events/index.js.coffee
+++ app/assets/javascripts/routes/events/index.js.coffee
@@ -9,10 +9,12 @@
         countries: controller.get('selectedCountries')
         page: controller.get('currentPage')
       success: (data) =>
-        @store.unloadAll('event')
         @store.pushPayload('event', data)

-        controller.set('content', @store.all('event'))
+        events = data.events.map (event) => @store.getById('event', event.id)
+
+        controller.set('content', events)
+
         callback?()

   setupController: (controller) ->


Code review fixes

Code review fixes

--- app/assets/javascripts/uploadcare/widget/live.coffee
+++ app/assets/javascripts/uploadcare/widget/live.coffee
@@ -29,12 +29,12 @@
     initializeWidget(el)

   initializeWidget = (input, targetClass) ->
-    elCount = $(input).size()
-    if elCount == 0
-        throw new Error "No DOM elements found matching selector"
-    else if elCount > 1
-        uploadcare.utils.warn('There are multiple DOM elements matching selector');
-    input = $(input).eq(0)
+    inputArr = $(input)
+    if inputArr.length == 0
+        throw new Error("No DOM elements found matching selector")
+    else if inputArr.length > 1
+        utils.warn("There are multiple DOM elements matching selector")
+    input = inputArr.eq(0)
     s = settings.build(input.data())

     Widget = if s.multiple


Only timeout the manual trigger.

Only timeout the manual trigger.

--- app/assets/javascripts/web/views/gaze_view.js.coffee
+++ app/assets/javascripts/web/views/gaze_view.js.coffee
@@ -5,10 +5,8 @@

   didInsertElement: ->
     view = this
-    setTimeout =>
-      $(@scrollParent).on("scroll", (e) -> view.didScroll(view,e))
-      $(@scrollParent).trigger("scroll")
-    , 3000
+    $(@scrollParent).on("scroll", (e) -> view.didScroll(view,e))
+    setTimeout(=> $(@scrollParent).trigger("scroll"), 5000)

   didScroll: (view,e) ->
     @get('controller').send('more') if @isScrolledToBottom()


[DB] Fix dests view

[DB] Fix dests view

--- server/models/requests.coffee
+++ server/models/requests.coffee
@@ -65,7 +65,13 @@
                             emit ['from', boxid, null, sender.name, docDate], null
                         emit ['from', boxid, null, sender.address, docDate], null

-                    for dest in doc.to.concat(doc.cc)
+                    # some messages may not have to or cc fields
+                    dests = []
+                    if doc.to?
+                        dests = dests.concat(doc.to)
+                    if doc.cc?
+                        dests = dests.concat(doc.cc)
+                    for dest in dests
                         if dest.name?
                             emit ['dest', boxid, null, dest.name, docDate], null
                         emit ['dest', boxid, null, dest.address, docDate], null


Don’t use syntax themes in spec because they are inserted in shadow DOM

--- spec/theme-manager-spec.coffee
+++ spec/theme-manager-spec.coffee
@@ -85,7 +85,7 @@
       runs ->
         reloadHandler.reset()
         expect($('style.theme')).toHaveLength 0
-        atom.config.set('core.themes', ['atom-dark-syntax'])
+        atom.config.set('core.themes', ['atom-dark-ui'])

       waitsFor ->
         reloadHandler.callCount == 1
@@ -93,8 +93,8 @@
       runs ->
         reloadHandler.reset()
         expect($('style[group=theme]')).toHaveLength 2
-        expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-dark-syntax/
-        atom.config.set('core.themes', ['atom-light-syntax', 'atom-dark-syntax'])
+        expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-dark-ui/
+        atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui'])

       waitsFor ->
         reloadHandler.callCount == 1
@@ -102,8 +102,8 @@
       runs ->
         reloadHandler.reset()
         expect($('style[group=theme]')).toHaveLength 2
-        expect($('style[group=theme]:eq(0)').attr('source-path')).toMatch /atom-dark-syntax/
-        expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-light-syntax/
+        expect($('style[group=theme]:eq(0)').attr('source-path')).toMatch /atom-dark-ui/
+        expect($('style[group=theme]:eq(1)').attr('source-path')).toMatch /atom-light-ui/
         atom.config.set('core.themes', [])

       waitsFor ->


Added gets index

Added gets index

--- server/startup/indexes.coffee
+++ server/startup/indexes.coffee
@@ -11,5 +11,6 @@
 		try ChatSubscription._ensureIndex { 'ts': 1 } catch e then console.log e

 		try ChatMessage._ensureIndex { 'rid': 1, 'ts': 1 } catch e then console.log e
+		try ChatMessage._ensureIndex { 'ets': 1 }, { sparse: 1 } catch e then console.log e
 		try ChatMessage._ensureIndex { 'rid': 1, 't': 1, 'u._id': 1 } catch e then console.log e
 		try ChatMessage._ensureIndex { 'expireAt': 1 }, { expireAfterSeconds: 0 } catch e then console.log e


Remove unused code

Remove unused code

--- server/stream/messages.coffee
+++ server/stream/messages.coffee
@@ -20,16 +20,6 @@

 Meteor.startup ->
-	filter =
-		_hidden: { $ne: true }
-		$or: [
-			ts:
-				$gt: new Date()
-		,
-			ets:
-				$gt: new Date()
-		]
-
 	options = {}

 	if not RocketChat.settings.get 'Message_ShowEditedStatus'


early version of the ScatterSelectionOverlayView working

--- src/coffee/bokeh_views.coffee
+++ src/coffee/bokeh_views.coffee
@@ -25,6 +25,8 @@
     @plot_id = options.plot_id
     @plot_model = options.plot_model
     @plot_view = options.plot_view
+  addPolygon: (x,y) ->
+    @plot_view.ctx.fillRect(x,y,5,5)

 tojq = (d3selection) ->
   return $(d3selection[0][0])
@@ -592,8 +594,6 @@
     safebind(this, @mget_ref('ymapper'), 'change', @request_render)
     safebind(this, @mget_ref('data_source'), 'change', @request_render)

-  addPolygon: (x,y) ->
-    @plot_view.ctx.fillRect(x,y,5,5)

   fill_marks : (marks) ->
     return null
@@ -631,6 +631,8 @@
     circles = @get_marks()
     data = @model.get_ref('data_source').get('data')
     @calc_buffer(data)
+    @plot_view.ctx.fillStyle = 'blue'
+    @plot_view.ctx.strokeStyle = 'blue'

     _.each(@screenx, (val, i) =>
@@ -893,6 +895,26 @@
       @plotview.renderers[renderer.id].fill_marks(marks)
     return null

+  addPolygon: (x,y) ->
+    @plot_view.ctx.fillRect(x,y,5,5)
+
+  render : () ->
+    window.overlay_render += 1
+    super()
+    for temp in _.zip(@mget('renderers'), @rendererviews)
+      [renderer, rendererview] = temp
+      renderer = @model.resolve_ref(renderer)
+      selected = {}
+      if renderer.get_ref('data_source').get('selecting') == false
+        #skip data sources which are not selecting'
+        continue
+      @plot_view.ctx.fillStyle = 'orange'
+      @plot_view.ctx.strokeStyle = 'orange'
+      for idx in renderer.get_ref('data_source').get('selected')
+        @addPolygon(rendererview.screenx[idx], rendererview.screeny[idx])
+      @plot_view.ctx.stroke()
+    return null
+

 window.overlay_render = 0
 class ZoomToolView extends PlotWidget


use correct method to subscrive to live update

--- lineman/app/services/message_channel_service.coffee
+++ lineman/app/services/message_channel_service.coffee
@@ -16,7 +16,7 @@
     handleSubscriptions = (subscriptions) ->
       _.each subscriptions.data, (subscription) ->
         PrivatePub.sign(subscription)
-        PrivatePub.subscription subscription.channel, (data) ->
+        PrivatePub.subscribe subscription.channel, (data) ->
           if data.memo?
             switch data.memo.kind
               when 'comment_destroyed'


The session resolver must return the promise, not the session.

--- app/javascripts/routes.coffee
+++ app/javascripts/routes.coffee
@@ -106,8 +106,6 @@

                 # Fetch the session detail.
                 Router.getSessionDetail(sess)
-                # Return the session.
-                sess
             views:
               'main@':
                 templateUrl: '/partials/session-detail.html'


focus input fields

focus input fields

--- app/views/tabs_view.coffee
+++ app/views/tabs_view.coffee
@@ -11,6 +11,9 @@
   switchTo: (tab) ->
     @$('.tab').hide()
     switch tab
-      when 'show-playlists' then @$('.playlists').show()
-      when 'add-playlist' then @$('.playlist-new').show()
-      when 'add-sound' then @$('.sound-new').show()
+      when 'show-playlists'
+        @$('.playlists').show()
+      when 'add-playlist'
+        @$('.playlist-new').show().find('input').first().focus()
+      when 'add-sound'
+        @$('.sound-new').show().find('input').focus()


use preferred `or false` syntax vs !!

use preferred `or false` syntax vs !!

--- tutor/src/components/new-course/select-course.cjsx
+++ tutor/src/components/new-course/select-course.cjsx
@@ -26,7 +26,7 @@
   statics:
     title: 'Which course are you teaching?'
     shouldSkip: ->
-      !!( NewCourseStore.get('offering_id') and TutorRouter.currentParams().sourceId )
+      ( NewCourseStore.get('offering_id') and TutorRouter.currentParams().sourceId ) or false

   getInitialState: ->
     offerings = OfferingsStore.filter(is_concept_coach: NewCourseStore.get('course_type') is 'cc')


Rewrite rules for matching interpreted text roles

These were authored back in 2016, when I had only a modest understanding
of writing both reStructuredText and TextMate-compatible grammars. Rules
are clumsily-written, confusingly-named, and in need of... restructuring
(no pun intended).

--- grammars/restructuredtext.cson
+++ grammars/restructuredtext.cson
@@ -14,6 +14,22 @@
 		rst
 	(?=\\s|:|$)
 """
+injections:
+	"L:meta.numref.restructuredtext":
+		patterns: [{
+			# printf(3)-style placeholder
+			name: "constant.other.placeholder.figure-number.restructuredtext"
+			match: "(%)s"
+			captures:
+				1: name: "punctuation.definition.placeholder.restructuredtext"
+		},{
+			# “{name}” and “{number}” placeholders
+			name: "constant.other.placeholder.figure-$2.restructuredtext"
+			match: "({)(name|number)(})"
+			captures:
+				1: name: "punctuation.definition.placeholder.begin.restructuredtext"
+				3: name: "punctuation.definition.placeholder.end.restructuredtext"
+		}]

 repository:

@@ -40,15 +56,13 @@
 			{include: "#escape"}
 			{include: "#emphasis"}
 			{include: "#link-definition"}
+			{include: "#role"}
 			{include: "#substitution"}
 			{include: "#literal"}
-			{include: "#math-inline"}
-			{include: "#html-inline"}
 			{include: "#interpreted-line"}
 			{include: "#anonymous-link"}
 			{include: "#link-reference"}
 			{include: "#interpreted-block"}
-			{include: "#link-text"}
 			{include: "#footnotes"}
 			{include: "#citations"}
 		]
@@ -136,34 +150,6 @@

-
-	# Intepreted text: Multiline with role
-	"interpreted-block":
-		name: "markup.other.command.restructuredtext"
-		contentName: "string.other.interpreted.restructuredtext"
-		begin: "(:)([-A-z0-9:_.]*?)(:)(`)"
-		end:   "(`)"
-		beginCaptures:
-			1: name: "punctuation.definition.intepreted.restructuredtext"
-			2: name: "entity.name.role.restructuredtext"
-			3: name: "punctuation.definition.intepreted.restructuredtext"
-			4: name: "punctuation.definition.intepreted.restructuredtext"
-		endCaptures:
-			1: name: "punctuation.definition.intepreted.restructuredtext"
-		patterns: [
-			{include: "#escape"}
-
-			# Manpage reference: `ls(1)`
-			name: "meta.manpage.link.inline.restructuredtext"
-			match: "(?<=:manpage:`)\\G([-.\\w]+)(\\()(\\d+)(\\))"
-			captures:
-				1: name: "markup.bold.manpage-name.restructuredtext"
-				2: name: "punctuation.definition.round.bracket.begin.restructuredtext"
-				3: name: "constant.numeric.integer.restructuredtext"
-				4: name: "punctuation.definition.round.bracket.begin.restructuredtext"
-		]
-
-
 	# Intepreted text: Single line
 	"interpreted-line":
 		name: "markup.other.command.restructuredtext"
@@ -182,18 +168,30 @@
 			2: name: "markup.underline.link.restructuredtext"

-	# Links: `text`_
+	# Stuff matched inside hyperlinks and cross-references
+	link:
+		patterns: [{
+			# :ref:`Title <target>`
+			match: "\\G\\s*((?:[^<`\\\\]|\\.)++)?\\s*((<)((?:[^>`\\\\]|\\.)++)(>))\\s*(?=`|$)"
+			captures:
+				1: patterns: [include: "#link-text"]
+				2: name: "meta.link-destination.restructuredtext"
+				3: name: "punctuation.definition.angle.bracket.begin.restructuredtext"
+				4: name: "constant.other.reference.link.restructuredtext"
+				5: name: "punctuation.definition.angle.bracket.end.restructuredtext"
+		},{
+			# :ref:`target-only`
+			match: "\\G\\s*((?:[^`\\\\]|\\.)++)\\s*(?=`|$)"
+			captures:
+				0: name: "meta.link-destination.restructuredtext"
+				1: name: "constant.other.reference.link.restructuredtext"
+		}]
+
+	# Pattern rules “clamped” to a fixed-length capturing group, necessary for injections
 	"link-text":
-		name: "meta.link.inline.restructuredtext"
-		match: "(`)([^<`]+)\\s+(<)(.*?)(>)(`_)"
-		captures:
-			1: name: "punctuation.definition.link.restructuredtext"
-			2: name: "string.other.link.title.restructuredtext"
-			3: name: "punctuation.definition.location.restructuredtext"
-			4: name: "markup.underline.link.restructuredtext"
-			5: name: "punctuation.definition.location.restructuredtext"
-			6: name: "punctuation.definition.link.restructuredtext"
-
+		name:  "entity.name.reference.restructuredtext"
+		begin: "(?:^|\\G)"
+		end:   "$"

 	citations:
@@ -455,6 +453,7 @@
 			2: "name": "punctuation.definition.field.restructuredtext"

+	# Substitution reference: “|linked|_” or “|unlinked|”
 	substitution:
 		name: "support.variable.substitution.restructuredtext"
 		match: "(?<!\\\\)(\\|)[^|]+(?<!\\\\)(\\|_{0,2})"
@@ -466,7 +465,7 @@
 	"substitution-definition":
 		patterns: [

-			# Image substitution
+			# Image substitution – “.. |foo| image:: ./path/to/image.png”
 			{
 				name: "meta.substitution-definition.image.restructuredtext"
 				begin: "^(\\s*)(\\.\\.)\\s(\\|)(?=\\S)([^\\|]+)((?<=\\S)\\|)\\s+(image)(::)\\s*(\\S+)"
@@ -483,7 +482,7 @@
 			}

-			# Generic/custom definition
+			# Generic/custom definition – “.. |foo| something else:: …”
 			{
 				name: "meta.substitution-definition.restructuredtext"
 				begin: "^(\\s*)(\\.\\.)\\s(\\|)(?=\\S)([^\\|]+)((?<=\\S)\\|)\\s+(\\S+.*(?=::))(::)(.*)"
@@ -501,36 +500,261 @@
 		]

-	# Interpreted text: Embedded HTML markup
-	"html-inline":
-		name: "markup.other.command.restructuredtext"
-		contentName: "text.embedded.html.basic"
+	# Interpreted text role
+	role:
+		name:  "meta.interpreted-role.restructuredtext"
+		begin: "((:)\\w+(?:[-_.:+]\\w+)*+(:))(?=`)"
+		end:   "(?!\\G)"
+		beginCaptures:
+			1: name: "keyword.operator.role.restructuredtext"
+			2: name: "punctuation.definition.role.name.begin.restructuredtext"
+			3: name: "punctuation.definition.role.name.end.restructuredtext"
+		patterns: [{
+			begin: "\\G`"
+			end:   "`"
+			beginCaptures: 0: name: "punctuation.definition.role.body.begin.restructuredtext"
+			endCaptures:   0: name: "punctuation.definition.role.body.end.restructuredtext"
+			patterns: [
+				{include: "#role-abbr"}
+				{include: "#role-code"}
+				{include: "#role-command"}
+				{include: "#role-emphasis"}
+				{include: "#role-html"}
+				{include: "#role-literal"}
+				{include: "#role-manpage"}
+				{include: "#role-math"}
+				{include: "#role-numref"}
+				{include: "#role-pep"}
+				{include: "#role-ref"}
+				{include: "#role-rfc"}
+				{include: "#role-strong"}
+				{include: "#role-subscript"}
+				{include: "#role-superscript"}
+				{include: "#role-titleref"}
+				{include: "#role-other"}
+			]
+		}]
+
+
+	# Abbreviation — “:abbr:`FAQ (Frequently Asked Questions)`”
+	"role-abbr":
+		patterns: [{
+			# Sphinx's `:abbr:` role
+			begin: "(?i)(?<=:abbr:`)\\G"
+			end:   "(?=`)"
+			patterns: [
+				{include: "#escape"}
+				{
+					# The abbreviated text
+					name:  "meta.abbreviation.restructuredtext"
+					begin: "\\G\\s*"
+					end:   "(?=\\s*(?:\\(|`))"
+					contentName: "constant.other.abbreviation.restructuredtext"
+					patterns: [include: "#escape"]
+				}
+				{
+					# The meaning of the abbreviation, enclosed in brackets
+					name:  "meta.explanation.restructuredtext"
+					begin: "\\("
+					end:   "(\\))|((?:[^\\x29`]|\\.)*+)(?=`)"
+					contentName: "string.quoted.brackets.restructuredtext"
+					beginCaptures:
+						0: name: "punctuation.definition.string.begin.restructuredtext"
+					endCaptures:
+						1: name: "punctuation.definition.string.end.restructuredtext"
+						2: name: "invalid.illegal.unclosed-parenthetical.restructuredtext"
+					patterns: [include: "#escape"]
+				}
+			]
+		},{
+			# Docutils's default “:abbreviation:” and “:acronym:” roles, neither of which expose the “explanation”
+			# attribute of the doctree node (see https://docutils.sourceforge.io/docs/ref/doctree.html#examples).
+			name:  "meta.abbreviation.restructuredtext"
+			begin: "(?i)(?<=:abbreviation:`|:acronym:`|:a[bc]:`)\\G"
+			end:   "(?=`)"
+			contentName: "constant.other.abbreviation.restructuredtext"
+			patterns: [include: "#escape"]
+		}]
+
+
+	# Inline code
+	"role-code":
+		name:  "markup.raw.code.monospace.literal.restructuredtext"
+		begin: "(?i)(?<=:code:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Literal text, which isn't necessarily source code
+	"role-literal":
+		name:  "markup.raw.monospace.literal.restructuredtext"
+		begin: "(?i)(?<=:literal:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Command name, displayed in a monospaced bold face
+	"role-command":
+		name:  "markup.bold.restructuredtext"
+		begin: "(?i)(?<=:command:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Emphasised text, usually rendered in italics
+	"role-emphasis":
+		name:  "markup.italic.emphasis.restructuredtext"
+		begin: "(?i)(?<=:emphasis:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Strongly emphasised text, usually rendered in bold
+	"role-strong":
+		name:  "markup.bold.emphasis.strong.restructuredtext"
+		begin: "(?i)(?<=:strong:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Inline mathematical expression – :math:`1 + 2`
+	"role-math":
+		name:  "markup.math.inline.restructuredtext"
+		begin: "(?i)(?<=:math:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "text.tex#math"]
+
+
+	# Embedded HTML markup
+	"role-html":
+		name: "text.embedded.html.basic"
+		begin: "(?xi)
+			(?: (?<=:html:`)
+			|   (?<=:raw-html:`)
+			) \\G"
+		end: "(?=`)"
 		patterns: [include: "text.html.basic"]
-		begin: "(:)(html)(:)(`)"
-		end:   "`"
-		beginCaptures:
-			1: name: "punctuation.definition.intepreted.restructuredtext"
-			2: name: "entity.name.role.restructuredtext"
-			3: name: "punctuation.definition.intepreted.restructuredtext"
-			4: name: "punctuation.definition.intepreted.restructuredtext"
-		endCaptures:
-			0: name: "punctuation.section.intepreted.end.restructuredtext"
-
-
-	# Intepreted text: Multiline with role
-	"math-inline":
-		name: "markup.other.command.restructuredtext"
-		contentName: "markup.math.inline.restructuredtext"
-		patterns: [include: "text.tex#math"]
-		begin: "(:)(math)(:)(`)"
-		end:   "(`)"
-		beginCaptures:
-			1: name: "punctuation.definition.intepreted.restructuredtext"
-			2: name: "entity.name.role.restructuredtext"
-			3: name: "punctuation.definition.intepreted.restructuredtext"
-			4: name: "punctuation.definition.intepreted.restructuredtext"
-		endCaptures:
-			1: name: "punctuation.definition.intepreted.restructuredtext"
+
+
+	# Manpage reference – :manpage:`ls(1)`
+	"role-manpage":
+		name:  "meta.manpage.link.inline.restructuredtext"
+		begin: "(?i)(?<=:manpage:`)\\G"
+		end:   "(?=`)"
+		patterns: [
+			{include: "hidden.manref"}
+			{
+				# Copied from above grammar; see `manref.cson` at Alhadis/language-roff@6f08acf
+				match: "([-.\\w]+)(\\()(\\d+(?!\\d)|(?:[lnop]|tcl)(?=[/\\)]))([\\w:/]*?(?:/(?!/)[-\\w:./]+)?)(\\))"
+				captures:
+					1: name: "markup.bold.manpage-name.restructuredtext"
+					2: name: "punctuation.section.round.bracket.begin.restructuredtext"
+					3: name: "constant.language.manpage-section.restructuredtext"
+					4: name: "constant.language.manpage-group.restructuredtext"
+					5: name: "punctuation.section.round.bracket.end.restructuredtext"
+			}
+		]
+
+
+	# Link to a referenced figure, section, table or code-block
+	"role-numref":
+		name:  "meta.numref.restructuredtext"
+		begin: "(?i)(?<=:numref:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#link"]
+
+
+	# Link to a Python Enhancement Proposal (PEP)
+	"role-pep":
+		begin: "(?xi)
+			(?: (?<= :pep:`)
+			|   (?<= :pep-reference:`)
+			) \\G"
+		end: "(?=`)"
+		patterns: [{
+			# Valid integer
+			match: "\\G(\\d+)(?=`)"
+			captures:
+				0: name: "constant.numeric.pep-number.restructuredtext"
+				1: name: "markup.underline.link.pep.$1.hyperlink.restructuredtext"
+		},{
+			# Some weird junk
+			name: "invalid.illegal.bad-integer.restructuredtext"
+			match: "[^`]+"
+		}]
+
+
+	# Roles for linking and cross-referencing resources
+	"role-ref":
+		begin: "(?xi)
+			(?: (?<=:ref:`)
+			|   (?<=:doc:`)
+			|   (?<=:download:`)
+			) \\G"
+		end: "(?=`)"
+		patterns: [include: "#link"]
+
+
+	# Request for Comments link: RFC 2822
+	"role-rfc":
+		begin: "(?xi)
+			(?: (?<= :rfc:`)
+			|   (?<= :rfc-reference:`)
+			) \\G"
+		end: "(?=`)"
+		patterns: [{
+			# Valid integer
+			match: "\\G(\\d+)(?=`)"
+			captures:
+				0: name: "constant.numeric.rfc-number.restructuredtext"
+				1: name: "markup.underline.link.rfc.$1.hyperlink.restructuredtext"
+		},{
+			# Some weird junk
+			name: "invalid.illegal.bad-integer.restructuredtext"
+			match: "[^`]+"
+		}]
+
+
+	# Subscripted text
+	"role-subscript":
+		name: "markup.subscript.restructuredtext"
+		begin: "(?xi)
+			(?: (?<=:sub:`)
+			|   (?<=:subscript:`)
+			) \\G"
+		end: "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Superscripted text
+	"role-superscript":
+		name: "markup.superscript.restructuredtext"
+		begin: "(?xi)
+			(?: (?<=:sup:`)
+			|   (?<=:superscript:`)
+			) \\G"
+		end: "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Title of a referenced creative work
+	"role-titleref":
+		name: "markup.italic.title-reference.restructuredtext"
+		begin: "(?xi)
+			(?: (?<= :t:`)
+			|   (?<= :title:`)
+			|   (?<= :title-reference:`)
+			) \\G"
+		end: "(?=`)"
+		patterns: [include: "#escape"]
+
+
+	# Fallback for any text role we don't recognise
+	"role-other":
+		name:  "string.interpolated.restructuredtext"
+		begin: "(?<=:`)\\G"
+		end:   "(?=`)"
+		patterns: [include: "#escape"]

 	directives:


Minor fix

Minor fix

--- app/modules/tree/index.coffee
+++ app/modules/tree/index.coffee
@@ -254,7 +254,7 @@
   .directive 'draggable', () ->
     {
       link: (scope, element, attr) ->
-        element.bind 'dragstart', (event) ->
+        element.on 'dragstart', (event) ->
           event.originalEvent.dataTransfer.setData('vm', event.target.getAttribute('vm'))
       restrict: 'A'
     }


Fix error due to mediatype being undefined

--- server/models/contact.coffee
+++ server/models/contact.coffee
@@ -25,7 +25,7 @@
     # XXX What if several Cozy instances are linked to one user?
     cozy = dp.value for dp in @datapoints when (dp.name is 'other' and
         dp.type is 'COZY') or (dp.name is 'url' and
-        dp.mediatype.search 'cozy' isnt -1)
+        dp.mediatype?.search 'cozy' isnt -1)

     return simple =
         id         : @id


Use heroku app as backend

Use heroku app as backend

--- src/Game.coffee
+++ src/Game.coffee
@@ -8,7 +8,7 @@

     window.Game = @

-    @socket = io.connect('http://localhost:8080', {secure: true})
+    @socket = io.connect('https://snapgame.herokuapp.com', {secure: true})
     @socket.emit 'new player', 'name'

     @socket.on 'snap', (data) =>