fixes chart buffers (closes #487)

fixes chart buffers (closes #487)

--- src/viz/types/helpers/graph/includes/buffer.coffee
+++ src/viz/types/helpers/graph/includes/buffer.coffee
@@ -5,16 +5,18 @@

   if vars[axis].scale.value isnt "share" and !vars[axis].range.value and vars[axis].reset

+    testScale = vars[axis].scale.viz.copy().clamp(false)
+
     if axis is vars.axes.discrete

-      domain = vars[axis].scale.viz.domain()
+      domain = testScale.domain()

       if typeof domain[0] is "string"
         i = domain.length
         while i >= 0
           domain.splice(i, 0, "d3plus_buffer_"+i)
           i--
-        range = vars[axis].scale.viz.range()
+        range = testScale.range()
         range = buckets d3.extent(range), domain.length
         vars[axis].scale.viz.domain(domain).range(range)

@@ -47,10 +49,10 @@
           additional = difference / (vars[axis].ticks.values.length - 1)
           additional = additional / 2

-          rangeMax = vars[axis].scale.viz.range()[1]
+          rangeMax = testScale.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 = testScale.invert -maxSize
+          domainHigh  = testScale.invert rangeMax + maxSize

           if domain[0] - additional < domainLow

@@ -62,7 +64,7 @@
             domain = [domainLow, domainHigh]
             domain = domain.reverse() if axis.indexOf("y") is 0

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

             unless domainCompare
@@ -87,7 +89,7 @@
             d = if i then orig_domain[i - 1] + add else orig_domain[i] - add
             domain.splice(i, 0, d)
             i--
-          range = vars[axis].scale.viz.range()
+          range = testScale.range()
           range = buckets d3.extent(range), domain.length
           vars[axis].scale.viz.domain(domain).range(range)

@@ -99,7 +101,7 @@
             (buffer is "y" and axis.indexOf("y") is 0) or
             (buffer is true)

-      domain = vars[axis].scale.viz.domain()
+      domain = testScale.domain()

       allPositive = domain[0] >= 0 and domain[1] >= 0
       allNegative = domain[0] <= 0 and domain[1] <= 0
@@ -165,16 +167,16 @@

       else

-        rangeMax = vars[axis].scale.viz.range()[1]
+        rangeMax = testScale.range()[1]
         maxSize  = vars.axes.scale.range()[1]

-        domainLow = vars[axis].scale.viz.invert -maxSize*1.5
-        domainHigh  = vars[axis].scale.viz.invert rangeMax + maxSize*1.5
+        domainLow = testScale.invert -maxSize*1.5
+        domainHigh  = testScale.invert rangeMax + maxSize*1.5

         domain = [domainLow, domainHigh]
         domain = domain.reverse() if axis.indexOf("y") is 0

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

         unless domainCompare


refactor getting updated selection values

--- resources/assets/coffee/react/beatmaps/search-filter.coffee
+++ resources/assets/coffee/react/beatmaps/search-filter.coffee
@@ -16,7 +16,7 @@
 #    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 ###

-{div,a,span} = ReactDOMFactories
+{div, a, span} = ReactDOMFactories
 el = React.createElement

 class Beatmaps.SearchFilter extends React.PureComponent
@@ -56,27 +56,21 @@

   href: ({ id, name }) =>
-    i = @cast(id)
-    newSelection =
-      if @props.multiselect
-        _(@currentSelection())[if @selected(i) then 'without' else 'concat'](i).sort().join('.')
-      else
-        if @selected(i) then @props.default else i
-
-    osu.updateQueryString(null, "#{@char}": newSelection)
+    osu.updateQueryString(null, "#{@char}": @newSelection(id))

   select: (e) =>
     e.preventDefault()
-    i = @cast(e.target.dataset.filterValue)
+    $(document).trigger 'beatmap:search:filtered', "#{@props.name}": @newSelection(e.target.dataset.filterValue)

-    newSelection =
-      if @props.multiselect
-        _(@currentSelection())[if @selected(i) then 'without' else 'concat'](i).sort().join('.')
-      else
-        if @selected(i) then @props.default else i

-    $(document).trigger 'beatmap:search:filtered', "#{@props.name}": newSelection
+  # TODO: rename
+  newSelection: (id) =>
+    i = @cast(id)
+    if @props.multiselect
+      _(@currentSelection())[if @selected(i) then 'without' else 'concat'](i).sort().join('.')
+    else
+      if @selected(i) then @props.default else i

   selected: (i) =>


focus on file name after editing it

focus on file name after editing it

fixes CNVS-18670

Just a simple fix. The previously focused item isn't always the file
name so would get set to the body. This ensures its always focusing on
the files name.

Test Plan
Given I have new files enabled
And I'm working in the files directory
And I have a file uploaded
And I'm using only the keyboard
When I select the option to rename a file
And I cancel renaming by selecting the "x" or cancel option
Then focus should be set on the file name I was editing

Change-Id: If00494b2f03861dad6b2bfe98a2f4de5d13a0330
Reviewed-on: https://gerrit.instructure.com/49248
Tested-by: Jenkins
Reviewed-by: Clay Diffrient <9dff2e5c98626d20d2848250c411e8009465bb87@instructure.com>
QA-Review: Clare Strong <40d6bb3ed7fef516c73274131d5a837c16de74fd@instructure.com>
Product-Review: Sterling Cobb <7a57d889ea7f266299434459898a90c7f6a1ca4d@instructure.com>

--- app/coffeescripts/react_files/components/FolderChild.coffee
+++ app/coffeescripts/react_files/components/FolderChild.coffee
@@ -40,7 +40,14 @@
       @focusNameLink()

     focusNameInput: ->
-      @previouslyFocusedElement = document.activeElement
+
+      # If the activeElement is currently the "body" that means they clicked on some type of cog to enable this state.
+      # This is an edge case that ensures focus remains in context of whats being edited, in this case, the nameLink
+      @previouslyFocusedElement = if document.activeElement.nodeName == "BODY"
+                                    @refs.nameLink?.getDOMNode()
+                                  else
+                                    document.activeElement
+
       setTimeout () =>
         @refs.newName?.getDOMNode().focus()
       , 0


Remove multiple MRUs.

Remove multiple MRUs.

--- src/lib/strata_.coffee
+++ src/lib/strata_.coffee
@@ -229,10 +229,9 @@
   # list.
   constructor: (@directory, @options) ->
     @cache          = {}
-    @mru            = {}
-    # TODO OUTGOING
-    @mru.core       = @createMRU()
-    @mru.balance    = @createMRU()
+    @mru            = { address: null }
+    @mru.next       = @mru
+    @mru.previous   = @mru
     @nextAddress    = 0
     @length         = 1024
     @balancer       = new Balancer
@@ -360,9 +359,6 @@
   # Create an most-recently used list head node and return it. We call this to
   # create the core and balance list in the constructor above.
   createMRU: ->
-    head            = { address: -1 }
-    head.next       = head
-    head.previous   = head

   # Link tier to the head of the most-recently used list.
   link: (head, entry) ->
@@ -430,7 +426,7 @@
   # of the next leaf page, and a cache that maps record file positions to
   # records that have been loaded from the file.
   createLeaf: (address, override) ->
-    page = @cache[address] = @link @mru.core,
+    page = @cache[address] = @link @mru,
       balancers: 0
       loaded: false
       leaf: true
@@ -934,7 +930,7 @@

   #
   createBranch: (address, override) ->
-    page = @cache[address] = @link @mru.core,
+    page = @cache[address] = @link @mru,
       balancers: 0
       count: 0
       penultimate: true


adding list loader selector

adding list loader selector

--- src/mixins/list.coffee
+++ src/mixins/list.coffee
@@ -7,6 +7,7 @@
   listSelector: null
   emptySelector: ".empty"
   existsSelector: ".exists"
+  loadingSelector: ".loading"
   emptyToggleClass: "hide"
   filter: null
   filterFunc: (model, filters) ->
@@ -25,6 +26,8 @@
     @listenTo @collection, "add", @added
     @listenTo @collection, 'reset', @addAll
     @listenTo @collection, "remove", @removed
+    @listenTo @collection, "request", @showLoading.bind(@, true)
+    @listenTo @collection, "sync", @showLoading.bind(@, false)

     @listenTo @, "render:post", @addAll
     @listenTo @, "close", (e) =>
@@ -79,17 +82,30 @@

     @showEmpty()

+  showLoading: (value) ->
+    console.log "loading?", value
+    @loading = value
+    if not value
+      console.log "adding #{@emptyToggleClass}"
+      @$el.find(@loadingSelector).addClass @emptyToggleClass
+      @showEmpty()
+    else
+      console.log "remove #{@emptyToggleClass}"
+      @$el.find(@loadingSelector).removeClass @emptyToggleClass
+      @$el.find(@emptySelector).addClass @emptyToggleClass
+
   showEmpty: () ->
-    if @emptySelector
-      if @collection.length == 0
-        @$el.find(@emptySelector).removeClass @emptyToggleClass
-      else
-        @$el.find(@emptySelector).addClass @emptyToggleClass
-    if @existsSelector
-      if @collection.length > 0
-        @$el.find(@existsSelector).removeClass @emptyToggleClass
-      else
-        @$el.find(@existsSelector).addClass @emptyToggleClass
+    if not @loading
+      if @emptySelector
+        if @collection.length == 0
+          @$el.find(@emptySelector).removeClass @emptyToggleClass
+        else
+          @$el.find(@emptySelector).addClass @emptyToggleClass
+      if @existsSelector
+        if @collection.length > 0
+          @$el.find(@existsSelector).removeClass @emptyToggleClass
+        else
+          @$el.find(@existsSelector).addClass @emptyToggleClass

   remove: () ->
     for cid, view of @views


Unify dropdown toggle code

Unify dropdown toggle code


Global marks; always treat ` and ' as local marks.

On some keyboards (who knows?) "`" or "'" could be shift keys.  In this
case, with the previous implementation, these would be treated as global
marks and `` would be unusable.  This commit fixes that problem.

--- content_scripts/marks.coffee
+++ content_scripts/marks.coffee
@@ -23,6 +23,12 @@
   showMessage: (message, keyChar) ->
     HUD.showForDuration "#{message} \"#{keyChar}\".", 1000

+  # If <Shift> is depressed, then it's a global mark, otherwise it's a local mark.  This is consistent
+  # vim's [A-Z] for global marks and [a-z] for local marks.  However, it also admits other non-Latin
+  # characters.  The exceptions are "`" and "'", which are always considered local marks.
+  isGlobalMark: (event, keyChar) ->
+    event.shiftKey and keyChar not in @previousPositionRegisters
+
   activateCreateMode: ->
     @mode = new Mode
       name: "create-mark"
@@ -31,11 +37,8 @@
       suppressAllKeyboardEvents: true
       keypress: (event) =>
         keyChar = String.fromCharCode event.charCode
-        # If <Shift> is depressed, then it's a global mark, otherwise it's a local mark.  This is consistent
-        # vim's [A-Z] for global marks, [a-z] for local marks.  However, it also admits other non-Latin
-        # characters.
         @exit =>
-          if event.shiftKey
+          if @isGlobalMark event, keyChar
             # We record the current scroll position, but only if this is the top frame within the tab.
             # Otherwise, we'll fetch the scroll position of the top frame from the background page later.
             [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame()
@@ -58,7 +61,7 @@
       keypress: (event) =>
         @exit =>
           keyChar = String.fromCharCode event.charCode
-          if event.shiftKey
+          if @isGlobalMark event, keyChar
             chrome.runtime.sendMessage
               handler: 'gotoMark'
               markName: keyChar


notifications: do not glance activities if loggedout

--- client/landing/app/MainApp/avatararea/avatarareanotificationspopup.coffee
+++ client/landing/app/MainApp/avatararea/avatarareanotificationspopup.coffee
@@ -32,9 +32,11 @@
         @hide()

   hide:->
-    KD.whoami()?.glanceActivities =>
-      for item in @listController.itemsOrdered
-        item.unsetClass 'unread'
-      @noNotification.show()
-      @listController.emit 'NotificationCountDidChange', 0
+    if KD.isLoggedIn()
+      KD.whoami().glanceActivities =>
+        for item in @listController.itemsOrdered
+          item.unsetClass 'unread'
+        @noNotification.show()
+        @listController.emit 'NotificationCountDidChange', 0
+
     super


modalService is part of modalController.

--- coffee/codesearch.coffee
+++ coffee/codesearch.coffee
@@ -24,7 +24,13 @@
     return

-controller.factory "modalService", ->
+
+modalController = app.controller "ModalCtrl", ($scope, modalService) ->
+    $scope.modal = modalService
+    return
+
+
+modalController.factory "modalService", ->
     href = "#"
     title = ""
     body = ""
@@ -45,10 +51,6 @@
         $('#myModal').modal(show: true)
         return

-
-modalController = app.controller "ModalCtrl", ($scope, modalService) ->
-    $scope.modal = modalService
-    return

 modalController.directive "popup", ($http, modalService) ->


removed unused bootstrap plugins

removed unused bootstrap plugins

Signed-off-by: Vítor Avelino <1a73af9e7ae00182733b2292511b814be66f065f@vitoravelino.me>

--- app/assets/javascripts/application.coffee
+++ app/assets/javascripts/application.coffee
@@ -1,6 +1,9 @@
 #= require jquery
 #= require jquery_ujs
-#= require bootstrap-sprockets
+#= require bootstrap/transition
+#= require bootstrap/tab
+#= require bootstrap/tooltip
+#= require bootstrap/popover
 #= require lifeitup_layout
 #= require bootstrap-typeahead-rails
 #= require_tree .


configure facebook omniauth for heroku

configure facebook omniauth for heroku

--- app/assets/javascripts/facebook.js.coffee
+++ app/assets/javascripts/facebook.js.coffee
@@ -8,11 +8,11 @@

 window.fbAsyncInit = ->
-  # localhost, testing
-  FB.init(appId: '1740995652791642', cookie: true)
+  # # localhost, testing
+  # FB.init(appId: '1740995652791642', cookie: true)

   # Heroku
-  #FB.init(appId: '1737505913140616', cookie: true)
+  FB.init(appId: '1737505913140616', cookie: true)

   $('#sign_in').click (e) ->
     e.preventDefault()


Authenticate to current user's private channel on load

--- app/assets/javascripts/models/user.coffee
+++ app/assets/javascripts/models/user.coffee
@@ -19,3 +19,12 @@

   preferencesForQuestion: (question) ->
     @preferences().where(questionId: question.id())
+
+  afterCreate: ->
+    @subscribe() if @id() is @constructor.currentUserId
+
+  subscribe: ->
+    Monarch.subscribe(@getChannelName())
+
+  getChannelName: ->
+    "private-user-#{@id()}"


Clean up flippy drop-down. References #45.

--- app/assets/javascripts/postings.js.coffee
+++ app/assets/javascripts/postings.js.coffee
@@ -101,7 +101,7 @@
     $(document).on "click", (event) ->
       if !$(event.target).closest(".filter-buttons").length
         if $(".filter-buttons ul").is(":visible")
-          $(".filter-buttons ul").slideToggle(400)
+          $(".filter-buttons ul").slideUp(400)
           $(".filter-buttons button").expanderSwap()

     # Show and hide postings according to user's filter


tests

tests

--- vendor/assets/javascripts/planning_view.js.coffee
+++ vendor/assets/javascripts/planning_view.js.coffee
@@ -109,7 +109,7 @@
           break
       break
   if property == undefined
-    css = "#{selector} { #{style}: #{value} }"
+    css = "#{selector} { margin-left: 0%; #{style}: #{value} }"
     console.log css
     head = document.head || document.getElementsByTagName('head')[0]
     style_node = document.createElement 'style'


KodingRouter: Prevent calling "open" second time if a model related with the url not found.

--- client/Main/kodingrouter.coffee
+++ client/Main/kodingrouter.coffee
@@ -350,12 +350,14 @@
               @handleNotFound routeInfo.params.name

         (routeInfo, state, route)->
+
           if state?
             open.call this, routeInfo, state

           else
             KD.remote.cacheable routeInfo.params.name, (err, models, name)=>
-              model = models.first  if models
-              open.call this, routeInfo, model
+              if models?
+              then open.call this, routeInfo, models.first
+              else @handleNotFound routeInfo.params.name

     routes


fixing classpath semantics, per #92

fixing classpath semantics, per #92

--- browser/frontend.coffee
+++ browser/frontend.coffee
@@ -8,7 +8,6 @@
 controller = null
 editor = null
 progress = null
-jvm.classpath = [ "./", "/home/doppio/vendor/classes/", "/home/doppio/" ]

 class_cache = {}
 raw_cache = {}
@@ -196,6 +195,7 @@

 commands =
   javac: (args, cb) ->
+    jvm.classpath = [ "./", "/home/doppio/vendor/classes/" ]
     rs = new runtime.RuntimeState(stdout, user_input, read_classfile)
     jvm.run_class(rs, 'classes/util/Javac', args, -> controller.reprompt())
     return null  # no reprompt, because we handle it ourselves
@@ -203,18 +203,16 @@
     if !args[0]? or (args[0] == '-classpath' and args.length < 3)
       return "Usage: java [-classpath path1:path2...] class [args...]"
     if args[0] == '-classpath'
-      paths = args[1].split(':')
+      jvm.classpath = args[1].split(':')
+      jvm.classpath.push "/home/doppio/vendor/classes/"
       class_name = args[2]
       class_args = args[3..]
-      for path in paths
-        jvm.classpath.unshift(path + "/")
     else
+      jvm.classpath = [ "./", "/home/doppio/vendor/classes/" ]
       class_name = args[0]
       class_args = args[1..]
     rs = new runtime.RuntimeState(stdout, user_input, read_classfile)
     jvm.run_class(rs, class_name, class_args, -> controller.reprompt())
-    # reset the classpath to the default
-    jvm.classpath = [ "./", "/home/doppio/vendor/classes/", "/home/doppio/" ]
     return null  # no reprompt, because we handle it ourselves
   javap: (args) ->
     return "Usage: javap class" unless args[0]?
@@ -225,6 +223,7 @@
     disassembler.disassemble process_bytecode raw_data
     return null  # no reprompt, because we handle it ourselves
   rhino: (args, cb) ->
+    jvm.classpath = [ "./", "/home/doppio/vendor/classes/" ]
     rs = new runtime.RuntimeState(stdout, user_input, read_classfile)
     jvm.run_class(rs, '!rhino', args, -> controller.reprompt())
     return null  # no reprompt, because we handle it ourselves


Fix console.log under Windows.

Fix console.log under Windows.

The process.log is not added until the next iteration of uv loop.

--- browser/lib/init.coffee
+++ browser/lib/init.coffee
@@ -8,18 +8,6 @@
 # We modified the original process.argv to let node.js load the atom.js,
 # we need to restore it here.
 process.argv.splice 1, 1
-
-if process.platform is 'win32'
-  # Redirect node's console to use our own implementations, since node can not
-  # handle output when running as GUI program.
-  console.log = console.error = console.warn = process.log
-  process.stdout.write = process.stderr.write = process.log
-
-  # Always returns EOF for stdin stream.
-  Readable = require('stream').Readable
-  stdin = new Readable
-  stdin.push null
-  process.__defineGetter__ 'stdin', -> stdin

 # Add browser/api/lib to require's search paths,
 # which contains javascript part of Atom's built-in libraries.
@@ -32,6 +20,18 @@
 # Do loading in next tick since we still need some initialize work before
 # native bindings can work.
 setImmediate ->
+  if process.platform is 'win32'
+    # Redirect node's console to use our own implementations, since node can not
+    # handle console output when running as GUI program.
+    console.log = console.error = console.warn = process.log
+    process.stdout.write = process.stderr.write = process.log
+
+    # Always returns EOF for stdin stream.
+    Readable = require('stream').Readable
+    stdin = new Readable
+    stdin.push null
+    process.__defineGetter__ 'stdin', -> stdin
+
   # Don't quit on fatal error.
   process.on 'uncaughtException', (error) ->
     # Show error in GUI.


binding

binding

--- addon/components/map-grid-edge-scroll.coffee
+++ addon/components/map-grid-edge-scroll.coffee
@@ -6,7 +6,8 @@
 MapGridEdgeScrollComponent = Ember.Component.extend
   layout: layout
   tagName: "rect"
-  classNames: ["map-grid-edge-scroll", "positionalClassNames"]
+  classNames: ["map-grid-edge-scroll"]
+  classNameBindings: ["positionalClassNames"]
   positionalClassNames: computed "position",
     get: ->
       "map-grid-edge-scroll-#{@get "position"}"


make workspace's render return itself

make workspace's render return itself

--- core-coffee/workspace.coffee
+++ core-coffee/workspace.coffee
@@ -123,5 +123,7 @@
         tr = $("<div id=\"top-right-container\" class=\"container\"/>")
         tr.append nodeSearch.el
         @$el.append tr
+
+      return this
     )
   return Workspace


Client: Activity: Renamed method

Client: Activity: Renamed method

--- client/activity/lib/components/chatinputwidget/index.coffee
+++ client/activity/lib/components/chatinputwidget/index.coffee
@@ -303,7 +303,7 @@
     dropbox.close()  for dropbox in @getDropboxes() when dropbox?

-  isLastItemEditing: ->
+  isLastItemBeingEdited: ->

     chatItems = document.querySelectorAll '.ChatItem'

@@ -321,7 +321,7 @@

     { scrollTop, scrollHeight } = scrollContainer

-    if @isLastItemEditing()
+    if @isLastItemBeingEdited()
       scrollContainer.scrollTop = scrollContainer.scrollHeight

     @props.onResize()


Client: Teams: Updated button title and seperators

--- client/admin/lib/views/invitations/invitesomeoneview.coffee
+++ client/admin/lib/views/invitations/invitesomeoneview.coffee
@@ -153,7 +153,7 @@
                 style         : 'solid green medium'
                 loader        : color: '#444444'
                 callback      : => @handleResendInvitations pendingInvites, newInvites
-              Cancel          :
+              "Just send the new ones" :
                 itemClass     : kd.ButtonView
                 style         : 'solid medium'
                 callback      : => @sendInvitations newInvites
@@ -243,7 +243,7 @@

       @createInitialInputs()

-      title = "Invitation is sent to #{invites[0].email}"
+      title = "Invitation is sent to #{invites.first.email}"

       if invites.length > 1 or pendingInvites?.length
         title = 'All invitations are sent.'
@@ -286,7 +286,7 @@
   [0...len].forEach (i)->
     emails += "#{pendingInvites[i].email}"
     if i + 2 is len
-      emails += " and "
+      emails += ", and "
     else if i + 1 is len
       emails += ""
     else


teams: admin: member no item found logic fixed

--- client/admin/lib/views/members/teammemberscommonview.coffee
+++ client/admin/lib/views/members/teammemberscommonview.coffee
@@ -156,9 +156,10 @@

   listMembers: (members) ->

-    unless members.length
+    if members.length is 0 and @listController.getItemCount() is 0
       @listController.lazyLoader.hide()
-      return @listController.noItemView.show()
+      @listController.noItemView.show()
+      return

     @skip += members.length


Adds tests

Adds tests

--- client/test/models/article.coffee
+++ client/test/models/article.coffee
@@ -39,16 +39,15 @@
       @article.toJSON().featured_artist_ids.length.should.equal 1
       @article.toJSON().primary_featured_artist_ids.length.should.equal 1

-    it 'does not set a hero section if there isnt any data', ->
-      @article.set hero_section: null
-      @article.heroSection.clear()
-      console.log @article.toJSON()
-
     it 'serializes the hero section if there is data', ->
       @article.set hero_section: {}
       @article.heroSection.set type: 'video', url: 'foo'
       @article.toJSON().hero_section.type.should.equal 'video'
       @article.toJSON().hero_section.url.should.equal 'foo'
+
+    it 'sets the hero section to null if there is no data', ->
+      @article.set hero_section: {}
+      JSON.stringify(@article.toJSON()).should.containEql '"hero_section":null'

     it 'serializes the lead paragraph if there is data', ->
       @article.setLeadParagraph('
hello
')
@@ -99,7 +98,6 @@
       @article.set hero_section: { type: 'foo' }
       @article.trigger 'sync'
       @article.heroSection.get('type').should.equal 'foo'
-

   describe '#setLeadParagraph', ->

@@ -176,3 +174,25 @@

     it 'finds custom thumbnail_title', ->
       @article.getThumbnailTitle('search_title').should.containEql 'Search Title'
+
+describe "Article simple mode", ->
+
+  beforeEach ->
+    sinon.stub Backbone, 'sync'
+    data =
+      daily_email: true
+    @article = new Article data, simple: true
+
+  afterEach ->
+    Backbone.sync.restore()
+
+  it 'does not create additional associations', ->
+    @article.keys().length.should.equal 1
+
+  describe '#toJSON', ->
+
+    it 'serializes article data', ->
+      JSON.stringify(@article.toJSON()).should.equal '{"daily_email":true}'
+
+    it 'does not serialize associations', ->
+      JSON.stringify(@article.toJSON()).should.not.containEql '"hero_section"'


business = true

business = true

--- client/views/signUp/signUp.coffee
+++ client/views/signUp/signUp.coffee
@@ -102,6 +102,7 @@
     profile.first_name = businessname
     profile.contact_first_name = firstname
     profile.business_name = businessname
+    profile.business = true
     # profile.lastname = lastname
     # profile.zip = zip
     # profile.name = "#{firstname} #{lastname}"


Handle undefined text area values

Handle undefined text area values

Check to see if a text area's `val` is defined before trying to call
`replace()` on it.

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -54,7 +54,7 @@
   return split( term ).pop()

 window.rstrip = (val) ->
-  return val.replace(/\s+$/, '')
+  return if val then val.replace(/\s+$/, '') else val

 # Disable button if text field is empty
 window.disableButtonIfEmptyField = (field_selector, button_selector) ->


builder: throttle chokidar calls (styles)

--- client/builder/lib/index.coffee
+++ client/builder/lib/index.coffee
@@ -25,6 +25,7 @@
 nib          = require 'nib'
 stylus       = require 'gulp-stylus'
 concat       = require 'gulp-concat'
+throttle     = require 'throttleit'

 JS_OUTFILE                  = 'bundle.js'
 THIRDPARTY_OUTDIR           = 'thirdparty'
@@ -41,6 +42,7 @@
 STYLES_KDJS_CSS_FILE        = 'dist/kd.css'
 STYLES_COMMONS_GLOB         = 'app/lib/styl/commons/*.styl'
 STYLES_EXTENSION            = 'styl'
+THROTTLE_WAIT               = 500

 module.exports =

@@ -332,7 +334,8 @@
     notify  = @_notify.bind this

     manifests  = opts.manifests
-    watchingKd = false
+    watchingKd = no
+    watchingDirs = []

     commons  = path.join opts.basedir, STYLES_COMMONS_GLOB
     includes = [ commons ]
@@ -366,7 +369,7 @@
         callback?()

       if opts.watchJs and not watchingKd
-        watchingKd = true
+        watchingKd = yes
         w = chokidar.watch kdCssFile, persistent: yes
         w.on 'change', copyKd

@@ -418,14 +421,20 @@

         styl manifest, globs

-        if opts.watchCss
+        onRaw = (e, file) ->
+          return  unless file
+          if e is 'modified' or e is 'deleted'
+            console.log "updated #{dir}"
+            start = Date.now()
+            styl manifest, globs
+
+        onRaw = throttle onRaw, THROTTLE_WAIT
+
+        if opts.watchCss and ~watchingDirs.indexOf(dir) is 0
+          watchingDirs.push dir
           w = chokidar.watch dir, persistent: yes
           w.on 'ready', ->
-            w.on 'raw', (e, file) ->
-              if e is 'modified' or e is 'deleted'
-                console.log "#{e} file"
-                start = Date.now()
-                styl manifest, globs
+            w.on 'raw', onRaw

     init = =>


builder: reverted a breaking change

builder: reverted a breaking change

--- client/builder/lib/index.coffee
+++ client/builder/lib/index.coffee
@@ -263,8 +263,8 @@
         }
       }

-    b = if opts.watchJs
-      bant.watch opts_
+    if opts.watchJs
+      b = bant.watch opts_

       onUpdate = (files) ->
         files.forEach (file) -> console.log "updated #{file}"
@@ -273,7 +273,7 @@
       onUpdate = throttle onUpdate, THROTTLE_WAIT
       b.on 'update', onUpdate
     else
-      bant opts_
+      b = bant opts_

     b.require require.resolve('kd.js'), expose: 'kd'
     b.use manifests


redirect to login if your are not logged in the user-profile

--- app/coffee/modules/user-settings/main.coffee
+++ app/coffee/modules/user-settings/main.coffee
@@ -51,6 +51,11 @@

         @scope.project = {}
         @scope.user = @auth.getUser()
+
+        if !@scope.user
+            @location.path(@navUrls.resolve("permission-denied"))
+            @location.replace()
+
         @scope.lang = @getLan()

         maxFileSize = @config.get("maxUploadFileSize", null)


clean up comments

clean up comments

--- scripts/chart.coffee
+++ scripts/chart.coffee
@@ -20,27 +20,8 @@
 childArgs = [path.join(__dirname, '../module/phantomjs-script.coffee')]

 module.exports = (robot) ->
-  #page = require('webpage').create()
-  #Chart = require('chart.js/Chart')

   robot.respond /chart/i, (msg) ->
     childProcess.execFile binPath, childArgs, (err, stdout, stderr) ->
-      console.log('stdout: ' + stdout)
       msg.send "OK"

-    #phantom.create (ph) ->
-    #  ph.createPage (page) ->
-    #    page.evaluate () ->
-    #      canvas = $('<canvas />').attr(width: 600, height: 480).appendTo($('body'))
-    #      ph.exit()
-    #      msg.send "OK"
-        #page.open "http://www.google.com", (status) ->
-        #  console.log "opened google? ", status
-        #  page.evaluate (-> document.title), (result) ->
-        #    console.log 'Page title is ' + result
-        #    ph.exit()
-
-    #page.evaluate () ->
-    #  #canvas = $('<canvas />').attr(width: 600, height: 480).appendTo($('body'))
-    #  msg.send "OK"
-


[Group Notifications] disable notifications for koding group

--- workers/social/lib/social/traits/notifying.coffee
+++ workers/social/lib/social/traits/notifying.coffee
@@ -15,7 +15,7 @@
   notify:(receiver, event, contents, callback)->
     callback ?= (err) ->
       console.err err if err
-
+
     JMailNotification  = require '../models/emailnotification'
     JAccount = require '../models/account'
     CBucket  = require '../models/bucket'
@@ -31,14 +31,25 @@
         CBucket.addActivities relationship, origin, actor, recipient, callback

     sendNotification = =>
-      if receiver instanceof JAccount and receiver.type isnt 'unregistered'
+      if receiver instanceof JAccount and receiver.type isnt 'unregistered'
         if not (contents.subject.constructorName is 'JGroup' and contents.origin.slug is 'koding')
           JMailNotification.create {actor, receiver, event, contents}, \
           (err)->
             console.error err if err

+      if receiver instanceof JAccount and receiver.type isnt 'unregistered'
+        JMailNotification.create {actor, receiver, event, contents}, (err)->
+          console.error err if err
+
     if actor? and not receiver.getId().equals actor.id
       receiver?.sendNotification? event, contents
+
+      # do not create activity for koding group
+      # do not send mail notification for koding group
+      # return if koding
+      subject = contents.subject
+      return  if subject and subject.constructorName is 'JGroup' and subject.slug is "koding"
+
       do createActivity
       do sendNotification


Add method to update a single option for the loading page

--- src/please-wait.coffee
+++ src/please-wait.coffee
@@ -71,8 +71,8 @@
       # If true, we can transition immediately to a new message/HTML
       @_readyToShowLoadingHtml = false
       # Find the element that displays the loading logo and set the src if supplied
-      logoElem = @_loadingElem.getElementsByClassName("pg-loading-logo")[0]
-      logoElem.src = @options.logo if logoElem?
+      @_logoElem = @_loadingElem.getElementsByClassName("pg-loading-logo")[0]
+      @_logoElem.src = @options.logo if @_logoElem?
       # Add the loading screen to the body
       document.body.appendChild(@_loadingElem)
       # Add the CSS class that will trigger the initial transitions of the logo/loading HTML
@@ -133,6 +133,17 @@
       else
         listener()

+    updateOption: (option, value) ->
+      switch option
+        when 'backgroundColor'
+          @_loadingElem.style.backgroundColor = value
+        when 'logo'
+          @_logoElem.src = value
+        when 'loadingHtml'
+          @updateLoadingHtml(value)
+        else
+          throw new Error("Unknown option '#{option}'")
+
     updateLoadingHtml: (loadingHtml, immediately=false) ->
       unless @_loadingHtmlElem? then throw new Error("The loading template does not have an element of class 'pg-loading-html'")
       if immediately


AppController: Clean up commented code.

AppController: Clean up commented code.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -305,23 +305,6 @@

         callback()

-        # unless hideOnboardingModal
-        #   callback yes
-        #   appStorage.setValue 'hideOnboardingModal', yes
-
-        #   # FIXME: Onboarding modal is currently disabled.
-        #   # To make a quick workaround for `initiateFakeCounter`
-        #   # I just set appStorage flag to true. When we want to
-        #   # put onboarding modal back we need to change flag name.
-
-        #   # @onboardingModal = new IDE.OnboardingModal { container }
-        #   # @onboardingModal.once 'OnboardingModalDismissed', =>
-        #   #   mainView.activitySidebar.initiateFakeCounter()
-        #   #   callback()
-        # else
-        #   callback()
-
-
   createMachineStateModal: (options = {}) ->
     { state, container, machineItem, initial } = options
     modalOptions = { state, container, initial }


Collaboration: Realtime reference refactor.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -900,9 +900,7 @@

   resurrectSnapshot: ->

-    snapshot = @rtm.getFromModel "#{KD.nick()}Snapshot"
-
-    for change in snapshot.values() when change.context
+    for change in @mySnapshot.values() when change.context
       @createPaneFromChange change

@@ -914,9 +912,6 @@

     {paneHash} = context
     nickname   = KD.nick()
-    snapshot   = @rtm.getFromModel "#{nickname}Snapshot"
-    changes    = @rtm.getFromModel 'changes'
-

     if change.origin is nickname

@@ -939,18 +934,15 @@

         delete context.file?.content?

-
-      changes.push change
-
-    return  unless snapshot
+      @changes.push change

     switch change.type

       when 'NewPaneCreated'
-        snapshot.set paneHash, change
+        @mySnapshot.set paneHash, change

       when 'PaneRemoved'
-        snapshot.delete paneHash
+        @mySnapshot.delete paneHash

   watchParticipant: (nickname) -> @myWatchMap.set nickname, nickname


don't set the dcNotification to null

don't set the dcNotification to null

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -275,9 +275,7 @@

           computeController.on "public-#{machineId}", (event) =>
             if event.status in [Stopping, Stopped, Terminating, Terminated]
-
               KodingKontrol.dcNotification?.destroy()
-              KodingKontrol.dcNotification = null

               machineItem.getBaseKite( no ).disconnect()


Access keymap via atom global

Access keymap via atom global

--- src/window.coffee
+++ src/window.coffee
@@ -36,8 +36,8 @@
 # This should only be called after setUpEnvironment() has been called.
 window.setUpDefaultEvents = ->
   windowEventHandler = new WindowEventHandler
-  keymap.loadBundledKeymaps()
-  ipc.sendChannel 'update-application-menu', keymap.keystrokesByCommandForSelector('body')
+  atom.keymap.loadBundledKeymaps()
+  ipc.sendChannel 'update-application-menu', atom.keymap.keystrokesByCommandForSelector('body')

 # This method is only called when opening a real application window
 window.startEditorWindow = ->


Avoid overriding 'false' values with null.

--- src/window.coffee
+++ src/window.coffee
@@ -40,10 +40,10 @@
         .filter (obj) ->
             obj?
     # Return either the first object or null
-    possibleObjects[0] or null
+    possibleObjects[0] ? null

 # Default action, just proxy the request
-windowProxy = (name) -> globalRef[name]
+windowProxy = (name) -> globalRef[name] ? null

 # Expanded action, try to directly proxy,
 # otherwise try to resolve with vendor prefixes


Justering på default-skjerm (la til api-key i lenker)

--- coffee/nasjonalturbase.api.coffee
+++ coffee/nasjonalturbase.api.coffee
@@ -40,9 +40,10 @@
   intro = "
   API for Nasjonal Turbase. Versjon 0.

http://api.nasjonalturbase.no/v0/turer/
-
http://api.nasjonalturbase.no/v0/turer/508598979f938fd06740ee75
-
http://api.nasjonalturbase.no/v0/turer/?method=put&data={%22Navn%22:%22Testtur%22,%22Beskrivelse%22:%22Dette%20er%20en%20test%22}
-
http://api.nasjonalturbase.no/v0/turer/508ec09cd71b8f0000000001?method=put&data={%22Beskrivelse%22:%22N%C3%A5%20funker%20det%20som%20snuuuus%22}
+
http://api.nasjonalturbase.no/v0/turer/508598979f938fd06740ee75?api_key=dnt
+
http://api.nasjonalturbase.no/v0/turer/?api_key=dnt&method=post&data={%22Navn%22:%22Testtur%22,%22Beskrivelse%22:%22Dette%20er%20en%20test%22}
+
http://api.nasjonalturbase.no/v0/turer/508ec09cd71b8f0000000001?api_key=dnt&method=put&data={%22Beskrivelse%22:%22N%C3%A5%20funker%20det%20som%20snuuuus%22}
+
http://api.nasjonalturbase.no/v0/turer/508ec09cd71b8f0000000001?api_key=dnt&method=del
   "
   res.send intro


Bugfix (due to coffeescript) to JS behaviour

--- collector/scripts/frontend.coffee
+++ collector/scripts/frontend.coffee
@@ -70,12 +70,14 @@
 	eventTarget.value = ""
 	replySpace.textContent = reply
 	replySpace.className += " show"
-	parent.scroll(0,100000) #Hack
+	parent.scrollTop = parent.scrollHeight
 	nextResponse = parent.querySelector(".robot:not(.show)")
 	if nextResponse?
-		showNext = -> nextResponse.className += " show"
+		showNext = ->
+			nextResponse.className += " show"
+			parent.scrollTop = parent.scrollHeight
+			true
 		setTimeout(showNext, 1000)
-		setTimeout(parent.scrollY.bind(parent, 100000), 1200) #Hack
 	if not parent.querySelector(".tester:not(.show)")?
 		eventTarget.setAttribute("disabled", "disabled")
 		eventTarget.removeAttribute("placeholder")


Beterer test names for reports

Beterer test names for reports

--- server/test/integration/api_report.coffee
+++ server/test/integration/api_report.coffee
@@ -41,7 +41,7 @@
     callback(report)

-test('show', (done) ->
+test("show returns a report's data", (done) ->
   createReport( (report) ->
     request.get({
       url: helpers.appurl("api/report/#{report.id}")
@@ -58,7 +58,7 @@
   )
 )

-test('list', (done) ->
+test('index lists all reports', (done) ->
   createReport( (report) ->
     request.get({
       url: helpers.appurl("api/report")
@@ -77,7 +77,7 @@

 test('returns full nested sections')

-test('destroy', (done) ->
+test('can delete a report', (done) ->
   createReport( (report) ->
     request.del({
       url: helpers.appurl("api/report/#{report.id}")
@@ -89,7 +89,7 @@
   )
 )

-test('update', (done) ->
+test('can update a report', (done) ->
   createReport( (report) ->
     new_title = "Updated title"
     request.put({


handler methods moved to static block

handler methods moved to static block

--- src/pseudositer.coffee
+++ src/pseudositer.coffee
@@ -36,13 +36,38 @@
 (($) ->

   ###
-  # Constants
-  ###
-  imageLoadEvent = 'image.load.pseudositer'
-  textLoadEvent = 'text.load.pseudositer'
-
-  ###
+  #
+  # Static constant events
+  #
+  ###
+
+  events = [
+    'loading.pseudositer'       # ( spawningPath )
+    'destroy.index.pseudositer' # ( dfd, aboveIndexLevel )
+    'create.index.pseudositer'  # ( dfd, path, $links )
+    'load.image.pseudositer'    # ( dfd($elem), pathToImage )
+    'load.text.pseudositer'     # ( dfd($elem), pathToText )
+    'hide.content.pseudositer'  # ( dfd, $content )
+    'show.content.pseudositer'  # ( dfd, $content )
+    'show.error.pseudositer'    # ( spawningPath, errMsg )
+    'done.pseudositer'          # ( spawningPath, loadedPath )
+    'always.pseudositer'        # ( spawningPath, loadedPath )
+  ]
+
+  ###
+  #
+  # Static class names for default handlers
+  #
+  ###
+  contentClass = 'pseudositer-content'
+  staleClass   = 'pseudositer-stale'
+  indexClass   = 'pseudositer-index'
+  loadingClass = 'pseudositer-loading'
+
+  ###
+  #
   # Static functions
+  #
   ###

   # Log an object.
@@ -52,6 +77,184 @@

     if console?.log?
       console.log obj
+
+    undefined
+
+  # Get the path of the currently displayed location
+  #
+  # @return {String} the currently displayed path, without fragment
+  getCurrentPath = ->
+    path = document.location.pathname
+
+  # Get the fragment of the currently displayed location
+  #
+  # @return {String} the currently displayed fragment, without the
+  # leading '#'
+  getCurrentFragment = ->
+    fragment = document.location.hash.substr 1
+
+  # Obtain the paths to indexes leading up to and including the path of the provided
+  # index.  For example:
+  #
+  # getIndexTrail( "/path/to/my/file.txt" ) ->
+  #  ["/", "/path/to/", "/path/to/my/" ]
+  #
+  # files at the end of the path are ignored.
+  #
+  # @param url an absolute URL
+  #
+  # @return {Array} An array of paths to indexes, starting with the most interior
+  # index.
+
+  getIndexTrail = ( path ) ->
+    ary = path.split '/'
+
+    # the first path is always a '/'
+    trail = ['/']
+
+    # for each element after that before the last, append it to the trail with a trailing
+    # '/'
+    trail.push( ary[ 0..i ].join( '/' ) + '/' ) for i in [ 1...ary.length - 1 ]
+
+    trail
+
+  # Obtain the lowercase file type suffix (after a period)
+  #
+  # @param path the path to the file
+  #
+  # @return {String} the lowercase suffix, or null if there is none.
+  getSuffix = ( path ) ->
+    splitBySlash = path.split '/'
+    fileName = splitBySlash[ splitBySlash.length - 1 ]
+
+    splitByDot = fileName.split('.')
+
+    if splitByDot.length > 1
+      splitByDot[ splitByDot.length - 1 ].toLowerCase()
+    else
+      null
+
+  # Retrieve the content container for a pseudositer element. Creates it if
+  # it does not exist yet
+  #
+  # $pseudositer the pseudositer element
+  #
+  # @return {DOM} element for content
+  getContentContainer = ( $pseudositer ) ->
+    # Find a container if we have one, create one otherwise.
+    $container = $('.' + contentClass)
+    if( $container.length == 0 )
+      $container = $('<div />').addClass( contentClass ).appendTo( $pseudositer )
+
+    $container
+
+  # Get the class name for an index of a specified level.
+  #
+  # @param level the {int} level, starting with 0, of the index.
+  #
+  # @return {String} the name of the class
+  getIndexClassForLevel = ( level ) ->
+
+    indexClass + '-' + level
+
+  # Get a safe class name for a path
+  #
+  # @param path the {path} to turn into a class name
+  #
+  # @return {String} usable as class name
+  getClassForPath = ( path ) ->
+
+    path.replace(/[^A-Za-z0-9]/g, '-')
+
+  ###
+  #
+  # Default handlers -- these are all called with the pseudositer object
+  # as this.
+  #
+  ###
+
+  # Show the default loading notice
+  #
+  # @param spawningPath the path that caused the loading
+  showLoadingNotice = ( spawningPath ) ->
+    $container = getContentContainer( this )
+    classForPath = getClassForPath spawningPath
+    $loadingNotice = $( '<div />' ).hide()
+      .text( "Loading #{spawningPath}..." )
+      .addClass( "#{loadingClass} #{classForPath}" )
+      .appendTo( $container )
+      .fadeIn( 'fast' )
+
+    undefined
+
+  # Hide the default loading notice
+  #
+  # @param spawningPath the path that caused the loading
+  showLoadingNotice = ( spawningPath, loadedPath ) ->
+    $container = getContentContainer( this )
+    classForPath = getClassForPath spawningPath
+    $elem = $(".#{loadingClass}.#{classForPath}")
+      .fadeOut 'fast', -> $elem.remove()
+
+    undefined
+
+  # Destroy index elements that are at a deeper level than the new view.
+  #
+  # @param dfd A {Deferred} to resolve when the indices are destroyed
+  # @param aboveIndexLevel the {int} level above which indices should be
+  # destroyed
+  destroyIndex = ( dfd, aboveIndexLevel ) ->
+    $indices = $( '.' + indexClass )
+    # pipe each destruction onto this
+    destroyed = new $.Deferred().resolve()
+    # look through all of the indices, remove those with levels above
+    $.each($indices, function(idx, $index) {
+      if $index.data( 'pseudositer' ).level > aboveIndexLevel
+        # destroy the next level after the previous is done each time
+        destroyed = destroyed.pipe ->
+          $index.fadeOut 'slow', ->
+            $index.remove()
+            destroyed.resolve()
+    });
+
+    # when all destroying is done, resolve the original deferred.
+    destroyed.done -> dfd.resolve()
+
+    undefined
+
+  # Create an index element to hold links if it does not already exist.
+  #
+  # @param dfd A {Deferred} to resolve when the index is drawn
+  # @param path the path to the index
+  # @param $links an array of {DOM} links
+  createIndex = ( dfd, path, $links ) ->
+    indexLevel = path.split( '/' ).length - 2
+
+    # If an element with the index level's class is not on the page,
+    # create a div with the class, which is initially hidden
+    levelClass = getIndexClassForLevel indexLevel
+    $index = $( '.' + levelClass )
+    if $index.length is 0
+      $index = $( '<div />' )
+        .addClass( indexClass + ' ' + levelClass )
+        .data( 'pseudositer', { } )
+        .hide()
+      this.append $index
+
+    # If the previous path is the same, leave the element alone
+    # and resolve immediately.
+    prevPath = $index.data( 'pseudositer' ).path
+    # lock = $index.data( 'pseudositer' ).lock # in the midst of animation
+    if path is prevPath
+      dfd.resolve()
+    else # otherwise, change the path, empty, and append new links, then resolve
+      # fade out old index, slowly, then insert new one
+      $index.data( 'pseudositer', fadeOut('slow', ->
+        $index.empty()
+          .data( 'pseudositer', path : path, level : indexLevel )
+          .append $links
+          .fadeIn('slow', ->
+            dfd.resolve()

     undefined

@@ -90,104 +293,47 @@
     $.get( pathToText )
       .done( ( responseText ) -> dfd.resolve $( '<div />' ).text responseText )
       .fail( ( failObj )      -> dfd.reject  failObj )
-    dfd
+
     undefined

   # Hide an element
   #
+  # @param dfd the {Deferred} to resolve when the element is hidden
   # @param $elem the element to hide
-  hide = ( $elem ) ->
-    $elem.hide()
-    undefined
-
-  # Display an element
-  #
+  hideContent = ( dfd, $elem ) ->
+    $elem.fadeOut('slow', ->
+      $elem.remove()
+      dfd.resolve() )
+
+    undefined
+
+  # Display an element within content
+  #
+  # @param dfd the {Deferred} to resolve when the element is shown
   # @param $elem the element to display
-  display = ( $elem ) ->
-    $elem.show()
+  showContent = ( dfd, $elem ) ->
+    $container = getContentContainer( this )
+    # hide the element, append it to container
+    $elem
+      .hide().appendTo( $container )
+      .fadeIn('slow', -> dfd.resolve() )
     undefined

   # Populate $elem with an error message
   #
-  # @param $elem an element
   # @param errorMsg the error message
-  displayFailure = ( $elem, errorMsg ) ->
-    log "displayFailure( #{elem}, text )"
-
-    $elem.text( errorMsg )
-    undefined
-
-  # Get the path of the currently displayed location
-  #
-  # @return {String} the currently displayed path, without fragment
-  getCurrentPath = ->
-    path = document.location.pathname
-
-  # Get the fragment of the currently displayed location
-  #
-  # @return {String} the currently displayed fragment, without the
-  # leading '#'
-  getCurrentFragment = ->
-    fragment = document.location.hash.substr 1
-
-  # Obtain the paths to indexes leading up to and including the path of the provided
-  # index.  For example:
-  #
-  # getIndexTrail( "/path/to/my/file.txt" ) ->
-  #  ["/", "/path/to/", "/path/to/my/" ]
-  #
-  # files at the end of the path are ignored.
-  #
-  # @param url an absolute URL
-  #
-  # @return {Array} An array of paths to indexes, starting with the most interior
-  # index.
-  getIndexTrail = ( path ) ->
-    ary = path.split '/'
-
-    # the first path is always a '/'
-    trail = ['/']
-
-    # for each element after that before the last, append it to the trail with a trailing
-    # '/'
-    trail.push( ary[ 0..i ].join( '/' ) + '/' ) for i in [ 1...ary.length - 1 ]
-
-    trail
-
-  # Obtain the fragment portion of a URL, or null if there is none
-  #
-  # For example:
-  #   getFragmentFromUrl(/path/without/fragment)     =>  null
-  #   getFragmentFromUrl(/path/with/fragment/#)      =>  ""
-  #   getFragmentFromUrl(/path/with/fragment/##)     =>  "#"
-  #   getFragmentFromUrl(/path/with/fragment/#word)  =>  "word"
-  #
-  # @param url the url
-  #
-  # @return {String} the fragment, or null if there is none
-  # getFragmentFromUrl = ( url ) ->
-
-  #   loc = url.indexOf( '#' )
-  #   if loc > -1 then url.substr( loc + 1 ) else null
-
-  # Obtain the lowercase file type suffix (after a period)
-  #
-  # @param path the path to the file
-  #
-  # @return {String} the lowercase suffix, or null if there is none.
-  getSuffix = ( path ) ->
-    splitBySlash = path.split '/'
-    fileName = splitBySlash[ splitBySlash.length - 1 ]
-
-    splitByDot = fileName.split('.')
-
-    if splitByDot.length > 1
-      splitByDot[ splitByDot.length - 1 ].toLowerCase()
-    else
-      null
-
-  ###
-  # Plugin code
+  showError = ( errorMsg ) ->
+    log "showError( #{errorMsg} )"
+
+    $container = getContentContainer( this )
+
+    $container.text( errorMsg )
+    undefined
+
+  ###
+  #
+  # Plugin object
+  #
   ###
   $.pseudositer = (el, hiddenPath, options, map) ->

@@ -229,22 +375,13 @@
           else
             @realPath = @visiblePath + '/' + hiddenPath

-      # Create content div if one doesn't already exist,
-      # and keep reference to the div.
-      if $( '.' + @options.content ).length is 0
-        @$content = $( '<div>' ).addClass @options.content
-        @$el.append @$content
-      else
-        @$content = $( '.' + @options.content )
-
       # Bind hash change to callback
       $( window ).bind 'hashchange', update

-      @$el
-        # Bind default image loading
-        .bind imageLoadEvent, ( dfd, path) -> loadImage dfd, path
-        # Bind default text loading
-        .bind textLoadEvent, ( dfd, path ) -> loadText  dfd, path
+      # Set up handlers
+      for event in events
+        for handler in @options[ event ]
+          @$el.bind event, ( args... ) => handler.apply( @$el, args )

       # Immediately update view
       update()
@@ -253,7 +390,9 @@
       this

     ###
+    #
     # Public methods
+    #
     ###

     # Destroy pseudositer & remove all its content.
@@ -261,27 +400,28 @@
     # @return this
     @destroy = =>

-      # Unbind events
       # Empty out the original element and unbind events
       $( window ).unbind 'hashchange', update
-      @$el.empty()
-        .unbind imageLoadEvent
-        .unbind textLoadEvent
+      @$el.empty().unbind '.pseudositer'

       this

     ###
+    #
     # Private methods.
+    #
     ###

-    # Get the class name for an index of a specified level.
-    #
-    # @param level the {int} level, starting with 0, of the index.
-    #
-    # @return {String} the name of the class
-    getIndexClassForLevel = ( level ) =>
-
-      @options.index + '-' + level
+    # Trigger an event on the pseudositer element (uses triggerHandler).
+    # Adds ".pseudositer" namespace.
+    #
+    # @param eventName the name of the event, without namespace
+    # @param arguments a splat of arguments to pass
+    #
+    # @return this
+    trigger = ( eventName, arguments... ) =>
+      @$el.triggerHandler "#{eventName}.pseudositer", arguments
+      @

     # Construct the absolute path for an ajax request from an absolute
     # path that would have been found in a fragment.
@@ -315,11 +455,31 @@
       # if the content is cached, everything is ready to display.
       if @cachedContent[ path ]?
         showIndices path
-        showContent @cachedContent[ path ]
-        @$el.triggerHandler @options.loadedEvent
+
+        contentShown = new $.Deferred()
+        contentHidden = new $.Deferred()
+
+        # Start off by hiding the current content, calling back
+        trigger 'hide.content' contentHidden
+
+        # When hide content is done, trigger show content
+        # if there's a failure, make sure alwaysEvent is called.
+        contentHidden
+          .done =>
+            trigger 'show.content' contentShown, @cachedContent[ path ]
+          .fail (failObj) ->
+            trigger 'show.error' failObj
+            trigger 'always'
+
+        # When show content is done, trigger final doneEvent
+        # and alwaysEvent.
+        contentShown
+          .done           => trigger 'done', path
+          .fail (failObj) -> trigger 'show.error', failObj
+          .always         => trigger 'always', path

       else # load the data for the state
-        @$el.triggerHandler @options.loadingEvent
+        trigger 'loading', path

         progress = new $.Deferred()

@@ -351,7 +511,9 @@
               update()
             else # update will happen due to hash change
               document.location.hash = loadedPath )
-          .fail( (errObj)     -> log errObj )
+          .fail( (errObj) ->
+            trigger 'show.error' errObj
+            trigger 'always' )

       this

@@ -471,7 +633,7 @@
       #   dfd.resolve()
       # Use the handler in @options.map
       if @options.map[ suffix ]?
-        @$el.triggerHandler @options.map[ suffix ], dfd, pathToContent
+        trigger @options.map[ suffix ], dfd, pathToContent
         dfd.done( ( $content ) =>
           # save to @cachedContent upon success
           @cachedContent[ pathToContent ] = $content
@@ -489,84 +651,57 @@
     #
     # @param path the path to show links for
     #
-    # @return this
+    # @return {Promise} that is resolved when all the indices are visible
     showIndices = ( path ) =>
       log "showIndices( #{path} )"

+      dfd = new $.Deferred().resolve()
       trails = getIndexTrail path

-      # mark all link elements as stale before we iterate through the
-      # path
-      $( '.' + @options.index ).addClass @options.stale
-
       for level in [ 0...trails.length ]

         trail = trails[ level ]
-
-        # If an element with the index level's class is not on the page,
-        # create a div with the class
-        levelClass = getIndexClassForLevel level
-        $index = $( '.' + levelClass )
-        if $index.length is 0
-          $index = $( '<div />' )
-            .addClass( @options.index + ' ' + levelClass )
-            .data( 'pseudositer', { } )
-          @$el.append $index
-
-        # If the previous path is the same, leave the element alone
-        # otherwise, change the path, empty, and append new links
-        prevPath = $index.data( 'pseudositer' ).path
-        unless trail is prevPath
-          $index
-            .empty()
-            .data( 'pseudositer', path : trail )
-            .append @pathIndices[ trail ]
-
-        # ensure that this index element is not removed.
-        $index.removeClass @options.stale
+        dfd = dfd.pipe -> trigger 'create.index' dfd, level, @pathIndices[ trail ]
+
+

       # remove any index elements that are deeper than the new path
-      $( '.' + @options.index + '.' + @options.stale ).remove()
-
-      this
-
-    # Clear the displayed content
-    #
-    # @return this
-    clearContent = ( ) =>
-      log "clearContent( )"
-
-      $( @$content ).empty()
-
-      this
+      trigger 'destroy.index', new $.Deferred(), trails.length
+
+      dfd.promise()

     # call init, and return the output
     @init()

   # default options
   $.pseudositer.defaultOptions =
-    content   : 'pseudositer-content'
-    stale     : 'pseudositer-stale'
-    index     : 'pseudositer-index'
-    linkSelector     : 'a:not([href^="?"],[href^="/"])' # Find links from an index page that go deeper
-    loadingEvent     : 'loading.pseudositer'
-    loadedEvent      : 'loaded.pseudositer'
-    finishedEvent    : 'finished.pseudositer'
-
-    hideContentEvent : 'hide.content.pseudositer'
-    showContentEvent : 'show.content.pseudositer'
+
+    linkSelector   : 'a:not([href^="?"],[href^="/"])' # Find links from an index page that go deeper
+
+    # default handlers
+    loading.pseudositer      : [ showLoadingNotice ]
+    destroy.index.pseudositer: [ destroyIndex ]
+    create.index.pseudositer : [ createIndex ]
+    load.image.pseudositer   : [ loadImage ]
+    load.text.pseudositer    : [ loadText ]
+    hide.content.pseudositer : [ hideContent ]
+    show.content.pseudositer : [ showContent ]
+    show.error.pseudositer   : [ showError ]
+    done.pseudositer         : []
+    always.pseudositer       : [ hideLoadingNotice ]
+
     timeout          : 2000
     recursion        : true

   # Default handler map.  The handlers will be called with a deferred and
   # a path to the content with the named extension.
   $.pseudositer.defaultMap  =
-    png : imageLoadEvent
-    gif : imageLoadEvent
-    jpg : imageLoadEvent
-    jpeg: imageLoadEvent
-    txt : textLoadEvent
-    html: textLoadEvent
+    png : 'load.image'
+    gif : 'load.image'
+    jpg : 'load.image'
+    jpeg: 'load.image'
+    txt : 'load.text'
+    html: 'load.text'

   $.fn.pseudositer = (hiddenPath, options, map = {}) ->


Fix missing shape fc data art asset import (#740)

--- app/lib/sprites/SpriteBuilder.ozar.coffee
+++ app/lib/sprites/SpriteBuilder.ozar.coffee
@@ -188,7 +188,7 @@
     colors = {}
     for shapeKey in shapes
       shape = @shapeStore[shapeKey]
-      continue if (not shape.fc?) or colors[shape.fc]
+      continue if (not shape?.fc?) or colors[shape.fc]
       hsl = hexToHSL(shape.fc)
       colors[shape.fc] = hsl
     colors
@@ -218,7 +218,7 @@
   applyColorMap: (shapes, colors) ->
     for shapeKey in shapes
       shape = @shapeStore[shapeKey]
-      continue if (not shape.fc?) or not(colors[shape.fc])
+      continue if (not shape?.fc?) or not(colors[shape.fc])
       @colorMap[shapeKey] = hslToHex(colors[shape.fc])

 sum = (nums) -> _.reduce(nums, (s, num) -> s + num)


Fix error with 2FA recovery code, refs #292

--- server/middlewares/authentication.coffee
+++ server/middlewares/authentication.coffee
@@ -43,12 +43,12 @@

 attemptRecoveryCodes = (user, req, res, next) ->
     User.first (err, user) ->
-        codes = JSON.parse(user.encryptedRecoveryCodes)
         if err
             next makeError 401, 'no user found', err
         else if not user.encryptedRecoveryCodes?
             next makeError 401, 'error otp invalid code'
         else
+            codes = JSON.parse(user.encryptedRecoveryCodes)
             index = codes.indexOf(parseInt req.body.authcode)
             if index is -1 # invalid code
                 next makeError 401, 'error otp invalid code'


Fixed a crash (and a wrong table splice)

--- server/middlewares/authentication.coffee
+++ server/middlewares/authentication.coffee
@@ -37,8 +37,9 @@
     notificationHelper.createTemporary {text} , callback

 disableRecoveryCode = (user, codes, index, callback) ->
-    changes = encryptedRecoveryCodes: JSON.stringify(codes.splice index, 1)
-    user.updateAttributes  codes, changes, callback
+    codes.splice index, 1
+    changes = encryptedRecoveryCodes: JSON.stringify(codes)
+    user.updateAttributes changes, callback

 attemptRecoveryCodes = (user, req, res, next) ->
     User.first (err, user) ->


Fix specs

Fix specs

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -102,7 +102,7 @@

     describe "when the matched prefix is highlighted", ->
       it 'highlights the prefix of the word in the suggestion list', ->
-        spyOn(provider, 'requestHandler').andCallFake ({prefix}) ->
+        spyOn(provider, 'getSuggestions').andCallFake ({prefix}) ->
           [{text: 'items', replacementPrefix: prefix}]

         expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
@@ -126,7 +126,7 @@
           expect(word.childNodes[4].nodeType).toBe NodeTypeText

       it 'highlights repeated characters in the prefix', ->
-        spyOn(provider, 'requestHandler').andCallFake ({prefix}) ->
+        spyOn(provider, 'getSuggestions').andCallFake ({prefix}) ->
           [{text: 'apply', replacementPrefix: prefix}]

         expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
@@ -151,7 +151,7 @@

       describe "when the prefix does not match the word", ->
         it "does not render any character-match spans", ->
-          spyOn(provider, 'requestHandler').andCallFake ({prefix}) ->
+          spyOn(provider, 'getSuggestions').andCallFake ({prefix}) ->
             [{text: 'omgnope', replacementPrefix: prefix}]

           editor.moveToBottom()


Log analytics events for layered search navigation

--- apps/artwork/client/layered-search.coffee
+++ apps/artwork/client/layered-search.coffee
@@ -2,6 +2,7 @@
 Backbone        = require 'backbone'
 { ARTSY_URL }   = require('sharify').data
 { Markdown }    = require 'artsy-backbone-mixins'
+analytics       = require '../../../lib/analytics.coffee'

 Artworks            = require '../../../collections/artworks.coffee'
 ArtworkColumnsView  = require '../../../components/artwork_columns/view.coffee'
@@ -16,6 +17,10 @@
     @set fair: @collection.fair if @get('type') is 'fair'
     @artworks = new Artworks
     @artworks.url = "#{ARTSY_URL}/api/v1/related/layer/#{@get('type')}/#{@id}/artworks?artwork[]=#{@get('artwork_id')}"
+
+  label: ->
+    return @id if (@get('type') is 'synthetic') and (@id is 'for-sale')
+    @get 'type'

 module.exports.Layers = class Layers extends Backbone.Collection
   url: "#{ARTSY_URL}/api/v1/related/layers"
@@ -55,12 +60,20 @@
       success:  => @render()
       error:    => @remove()

+  # Activate the clicked layer or
+  # activate the first layer if called without a click event
   selectLayer: (e) ->
-    id = ($target = $(e.currentTarget)).data 'id'
+    id = if e
+      ($target = $(e.currentTarget)).data 'id'
+    else
+      ($target = @$layerButtons.first()).data 'id'
+
     @__activeLayer__ = @layers.get id
     @activateLayerButton $target
     @$layeredSearchResults.html '

'
     @fetchAndRenderActiveLayer()
+
+    analytics.track.click "Switched to related artworks: #{@__activeLayer__.label()}" if e

   fetchAndRenderActiveLayer: ->
     if @activeLayer().artworks.length
@@ -92,10 +105,9 @@
       artworkSize: 'tall'

   postRender: ->
-    @$layeredSearchResults = @$('#layered-search-results')
-    # Activate the first tab
-    (@$layerButtons ?= @$('.layered-search-layer-button')).
-      first().click()
+    @$layeredSearchResults  = @$('#layered-search-results')
+    @$layerButtons          = @$('.layered-search-layer-button')
+    @selectLayer() # Activate the first tab

   render: ->
     # If only one layer is returned it will be "For sale"


inline code highlight fix

inline code highlight fix

--- coffee/codemirror-syntax.coffee
+++ coffee/codemirror-syntax.coffee
@@ -1,6 +1,6 @@
 CodeMirror.defineMode "docscript", ((cmCfg, modeCfg) ->
   operator = "tag strong"
-  operatorInline = "keyword"
+  operatorInline = "string"
   console.log cmCfg
   htmlMode = CodeMirror.getMode cmCfg, name: "xml", htmlMode: true

@@ -55,6 +55,14 @@
     return "#{operator} header"

   matchInline = (stream, state) ->
+   match = stream.match /^``/
+   if match
+    state.code = not state.code
+    return operatorInline
+
+   if state.code
+    return null
+
    match = stream.match /^\*\*/
    if match
     state.bold = not state.bold
@@ -75,11 +83,6 @@
     state.superscript = not state.superscript
     return operatorInline

-   match = stream.match /^``/
-   if match
-    state.code = not state.code
-    return operatorInline
-
    match = stream.match /^<</
    if match
     state.link = true
@@ -89,6 +92,8 @@
    if match
     state.link = false
     return operatorInline
+
+   return null

   clearState = (state) ->
    state.bold = false


example FAQ section

example FAQ section

Former-commit-id: 6d814e10447f0139c3cb2c485283f708cbbb21ef [formerly 9442eb7536dea2cee027e1cc8dc7d9da226433e4] [formerly be8ee9ba24b473119fcb3a8071624f35cfa85d1e [formerly 06da84aed6520e839d27003983db3b2a6aac3017]] [formerly 81a260111eb1723aef8993f7fe158d1ad7593e73 [formerly 50a10ad0e26f6f729afd66846e1655eadd6853ca] [formerly 1f5bf9c45d8da9f13a3e10a009b2012487c75902 [formerly 1f5bf9c45d8da9f13a3e10a009b2012487c75902 [formerly 9a362bd5fe9267ff89c0eba4f3916a5c8c1a1923]]]]
Former-commit-id: 237cc07a0bdbb2ef74e33fce60fc47bf24c56dab [formerly e4d121eae9870e72a923e8cf8ce4af8f62c7c978] [formerly a6f89f7edd406e66bda1e78dc6b840ea557b23de [formerly 7298faedb41592bbaba1282d3b851b8062bf69b7]]
Former-commit-id: aeb522ea2a753093aa0c29684c92fc3408f42d88 [formerly 18f9664abc4ba922239d1a2167ba0074a04d4704]
Former-commit-id: e45625952a88a0853b03ac451d7a68c82a4fd2be
Former-commit-id: 7d3e3cbb0d2fd49b3350aff86b904bddbab27885

--- @client/saas_landing_page/saas_landing_page.coffee
+++ @client/saas_landing_page/saas_landing_page.coffee
@@ -25,7 +25,7 @@

 window.small_text = _.extend {}, base_text,
   fontSize: 20
-  fontWeight: 300
+  fontWeight: if browser.high_density_display then 300 else 400

 window.h1 = _.extend {}, base_text,
   fontSize: 48
@@ -57,6 +57,7 @@
       Video()
       tech()
       enables()
+      FAQ()
       pricing()
       Contact()
       Story()
@@ -71,7 +72,7 @@

     docked = fetch('header-dock').docked

-    nav_links = ['demo', 'price', 'contact', 'story']
+    nav_links = ['demo', 'faq', 'price', 'contact', 'story']

     DIV
       style:
@@ -114,11 +115,12 @@
             for nav in nav_links
               A
                 style: _.extend {}, base_text,
-                  fontWeight: 300
+                  fontWeight: if nav == 'faq' then 400 else 300
                   fontSize: 20
                   color: 'white'
                   marginLeft: 25
                   cursor: 'pointer'
+                  fontVariant: if nav == 'faq' then 'small-caps'
                 href: "##{nav}"
                 nav

@@ -181,7 +183,7 @@
             right: 0
             marginTop: 5
             display: 'inline-block'
-          'Try this example'
+          'Explore this example yourself'

   drawVideo : ->

@@ -458,7 +460,7 @@
             """
     bullet
       point_style: 'bullet'
-      strong: "Produce an interactive summary of group thought"
+      strong: "Produce an interactive summary of individual opinions"
       body: """
             . Patterns of thought across the whole group can
             be identified. Perhaps 80% of opposers have a
@@ -532,6 +534,447 @@
         'Contact us'

       ' to get started today. '
+
+
+ExpandingQuestion = ReactiveComponent
+  displayName: 'ExpandingQuestion'
+
+  render : ->
+    DIV
+      style:
+        paddingLeft: 30
+        position: "relative"
+        margin: "30px auto"
+        width: TEXT_WIDTH
+
+
+
+      I
+        className: "fa fa-chevron-#{if @local.active then 'down' else 'right'}"
+        style:
+          fontSize: 30
+          position: "absolute"
+          left: -15
+          top: 6
+          color: logo_red
+          cursor: 'pointer'
+
+        onClick: =>
+          @local.active = !@local.active
+          save @local
+
+      DIV
+        style: _.extend {}, base_text,
+          fontWeight: if @local.active then 600
+          cursor: 'pointer'
+        onClick: =>
+          @local.active = !@local.active
+          save @local
+
+        @props.question
+
+      DIV
+        style: _.extend {}, small_text,
+          #fontWeight: 300
+          display: if !@local.active then 'none'
+
+        @props.answer()
+
+
+FAQ = ReactiveComponent
+  displayName: 'FAQ'
+
+  render : ->
+    p =
+      marginTop: 15
+
+    qs = [{
+      question: 'What happens when I sign up for Consider.it?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'You\'ll get your very own customizable Consider.it site. '
+          P style: p,
+            """
+            Say that you’re a fantasy football fan and want to use Consider.it
+            to debate fantasy football decisions with other rabid fans. When
+            you sign up for Consider.it, you
+            specify a name like "fantasy-football". Congratulations, you are
+            now the proud administrator of https://fantasy-football.consider.it!
+            """
+          P style: p,
+            """
+            First, you might want to give it the look and feel you desire by
+            adding a custom logo and fancy homepage banner image you
+            designed. You might also want to configure who can access
+            the site (in this case, everyone) and who can post content (any
+            registered user). A variety of options are available to make
+            your Consider.it site work the way you wish.
+            """
+          P style: p,
+            """
+            Next you'll want to add a question or two. Or ten. You create a
+            section of questions that allow people to debate whether it is a
+            good idea to start a particular player, and another section that
+            solicits suggestions about the best strategies for managing your
+            fantasy team. Each section is shown on the homepage and you
+            configure it so that any user can add new suggestions.
+            """
+          P style: p,
+            """Finally, invite your friends and family, or whomever you want
+            to participate!"""
+
+      }, {
+      question: 'Can I solicit responses to open ended questions like "Which policies should we revise?"'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Can I moderate user content? Is it hard?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'You\'ll get your very own customizable Consider.it site. '
+          P style: p,
+            """
+            Say that you’re a fantasy football fan and want to use Consider.it
+            to debate fantasy football decisions with other rabid fans. When
+            you sign up for Consider.it, you
+            specify a name like "fantasy-football". Congratulations, you are
+            now the proud administrator of https://fantasy-football.consider.it!
+            """
+          P style: p,
+            """
+            First, you might want to give it the look and feel you desire by
+            adding a custom logo and fancy homepage banner image you
+            designed. You might also want to configure who can access
+            the site (in this case, everyone) and who can post content (any
+            registered user). A variety of options are available to make
+            your Consider.it site work the way you wish.
+            """
+          P style: p,
+            """
+            Next you'll want to add a question or two. Or ten. You create a
+            section of questions that allow people to debate whether it is a
+            good idea to start a particular player, and another section that
+            solicits suggestions about the best strategies for managing your
+            fantasy team. Each section is shown on the homepage and you
+            configure it so that any user can add new suggestions.
+            """
+          P style: p,
+            """Finally, invite your friends and family, or whomever you want
+            to participate!"""
+
+      }, {
+      question: 'Can I create a private discussion and send out invitations?"'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Is it possible to set different permissions for who can, for example, post a new idea or write a new pro/con point?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'You\'ll get your very own customizable Consider.it site. '
+          P style: p,
+            """
+            Say that you’re a fantasy football fan and want to use Consider.it
+            to debate fantasy football decisions with other rabid fans. When
+            you sign up for Consider.it, you
+            specify a name like "fantasy-football". Congratulations, you are
+            now the proud administrator of https://fantasy-football.consider.it!
+            """
+          P style: p,
+            """
+            First, you might want to give it the look and feel you desire by
+            adding a custom logo and fancy homepage banner image you
+            designed. You might also want to configure who can access
+            the site (in this case, everyone) and who can post content (any
+            registered user). A variety of options are available to make
+            your Consider.it site work the way you wish.
+            """
+          P style: p,
+            """
+            Next you'll want to add a question or two. Or ten. You create a
+            section of questions that allow people to debate whether it is a
+            good idea to start a particular player, and another section that
+            solicits suggestions about the best strategies for managing your
+            fantasy team. Each section is shown on the homepage and you
+            configure it so that any user can add new suggestions.
+            """
+          P style: p,
+            """Finally, invite your friends and family, or whomever you want
+            to participate!"""
+
+      }, {
+      question: 'Can I ask custom questions of users of my site?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Can I style my Consider.it site so that it fits with my brand?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'You\'ll get your very own customizable Consider.it site. '
+          P style: p,
+            """
+            Say that you’re a fantasy football fan and want to use Consider.it
+            to debate fantasy football decisions with other rabid fans. When
+            you sign up for Consider.it, you
+            specify a name like "fantasy-football". Congratulations, you are
+            now the proud administrator of https://fantasy-football.consider.it!
+            """
+          P style: p,
+            """
+            First, you might want to give it the look and feel you desire by
+            adding a custom logo and fancy homepage banner image you
+            designed. You might also want to configure who can access
+            the site (in this case, everyone) and who can post content (any
+            registered user). A variety of options are available to make
+            your Consider.it site work the way you wish.
+            """
+          P style: p,
+            """
+            Next you'll want to add a question or two. Or ten. You create a
+            section of questions that allow people to debate whether it is a
+            good idea to start a particular player, and another section that
+            solicits suggestions about the best strategies for managing your
+            fantasy team. Each section is shown on the homepage and you
+            configure it so that any user can add new suggestions.
+            """
+          P style: p,
+            """Finally, invite your friends and family, or whomever you want
+            to participate!"""
+
+      }, {
+      question: 'Does Consider.it work on mobile devices?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'What browsers do you support?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Can we self-host Consider.it on our own servers?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Can we use our own URL?"'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Is Consider.it open source? Can we use and modify the source code?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Consider.it is missing X feature that I need. What do can I do?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Do you support Single-Sign On (SSO)?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }, {
+      question: 'Do you integrate with X service?'
+      answer: ->
+        DIV null,
+          P style: p,
+            'Yes.'
+
+          P style: p,
+            """
+            Say that you are using https://startup-inc.consider.it to help your startup
+            of 25 employees (and soon 30) deliberate about how to improve process
+            given rapid growth. You want to help maintain the fun, casual atmosphere,
+            but you also recognize that some formalization of policy may be necessary.
+            """
+
+          P style: p,
+            'You decide to solicit ideas from your employees. '
+
+      }]
+
+    if !@local.show_all_questions
+      qs = qs.slice(0,4)
+
+    DIV
+      id: 'faq'
+      style:
+        marginTop: 60
+
+
+      H1
+        style: _.extend {}, h1,
+          marginBottom: 30
+
+        "Frequently Asked Questions"
+
+      DIV
+        style:
+          postion: 'relative'
+
+        for q in qs
+          ExpandingQuestion q
+
+
+      DIV
+        style:
+          width: TEXT_WIDTH
+          backgroundColor: '#F7F7F7'
+          textAlign: 'center'
+          textDecoration: 'underline'
+          cursor: 'pointer'
+          padding: '5px 0'
+          margin: 'auto'
+        onClick: =>
+          @local.show_all_questions = !@local.show_all_questions
+          save @local
+
+        if @local.show_all_questions
+          "Collapse questions"
+        else
+          "Show more questions"
+
+
+

 Contact = ReactiveComponent
   displayName: 'Contact'


Add disabled property to button to delete namespaces (instead of just adding a 'disabled' class)

--- admin/static/coffee/namespaces/index.coffee
+++ admin/static/coffee/namespaces/index.coffee
@@ -95,7 +95,12 @@

         # Callback that will be registered: updates the toolbar buttons based on how many namespaces have been selected
         update_toolbar_buttons: =>
-            @.$('.btn.remove-namespace').toggleClass 'disabled', @get_selected_namespaces().length < 1
+            if @get_selected_namespaces().length < 1 and @.$('.btn.remove-namespace').prop('disabled') is false
+                @.$('.btn.remove-namespace').prop 'disabled', 'disabled'
+                @.$('.btn.remove-namespace').addClass 'disabled'
+            else if @get_selected_namespaces().length > 0 and @.$('.btn.remove-namespace').prop('disabled') is true
+                @.$('.btn.remove-namespace').prop 'disabled', 'disabled'
+                @.$('.btn.remove-namespace').removeClass 'disabled'

         destroy: =>
             super


Remove a useless assignment since the animation does already the assignment

--- lib/assets/javascripts/slider.js.coffee
+++ lib/assets/javascripts/slider.js.coffee
@@ -110,7 +110,6 @@
       value - (@step * @item_width)

   handle_arrow_event: (direction) ->
-    @tray.css 'left',
     @tray.animate {'left': (@calculate_left_value direction, (@tray.css 'left'))},
       duration: @options.duration or 500


Fix some unit test

Fix some unit test

--- spec/linter-mypy_smoke_indent-spec.coffee
+++ spec/linter-mypy_smoke_indent-spec.coffee
@@ -48,7 +48,7 @@
           lint(editor).then (msgs) ->
             messages = msgs
       it 'should have detected all the errors', ->
-        expect(messages.length).toBe(11)
+        expect(messages.length).toBe(17)
       it 'should have put the good attributes in each warnings', ->
         msg0 = 'Incompatible types in assignment (expression has type "str", variable has type "int")'
         msg1 = 'Function is missing a type annotation'


Kill typo :bomb:

Kill typo :bomb:

--- assets/scripts/app/views/repo/show.coffee
+++ assets/scripts/app/views/repo/show.coffee
@@ -145,8 +145,8 @@
     ).property('build.state', 'hasPermission', 'tab')

     canRegenerateKey: (->
-      @get('hasPermissions')
-    ).property('hasPermissions')
+      @get('hasPermission')
+    ).property('hasPermission')

     canCancelJob: (->
       @get('isJobTab') && @get('job.canCancel') && @get('hasPermission')
@@ -163,7 +163,7 @@

     hasPermission: (->
       if permissions = Travis.app.get('currentUser.permissions')
-        permissions.indexOf(@get('repo.id')) > -1
+        permissions.contains @get('repo.id')
     ).property('Travis.app.currentUser.permissions.length', 'repo.id')


Avoids logging out users if the API is 500ing

--- components/layout/client.coffee
+++ components/layout/client.coffee
@@ -87,7 +87,10 @@
       # '/me/refresh' hits the account endpoint
       # and re-logs in the resulting user
       url: '/me/refresh'
-      error: ->
+      error: (_model, xhr) ->
+        # Ignore internal server errors
+        return if xhr.status is 500
+
         $.get '/me/sign_out', ->
           location.reload()


Show errors without location on first line of file.

--- lib/existdb.coffee
+++ lib/existdb.coffee
@@ -441,7 +441,7 @@
                 success: (data) ->
                     if data.result == "fail"
                         error = self.parseErrMsg(data.error)
-                        range = new Range([0, 0], [0, 0])
+                        range = null
                         if error.line > -1
                             line = (error.line - chunk.prologOffset) + chunk.offset
                             text = editor.lineTextForBufferRow(line)
@@ -451,6 +451,9 @@
                                     [line, error.column - 1],
                                     [line, end - 1]
                                 )
+                        if not range?
+                            text = editor.lineTextForBufferRow(0)
+                            range = new Range([0, 0], [0, text.length - 1])
                         message = {
                             severity: "error",
                             excerpt: error.msg,


fix date rate timeseries bugs

fix date rate timeseries bugs

--- server/router.coffee
+++ server/router.coffee
@@ -300,6 +300,8 @@
     dateRange =
       start: new Date(@request.query.startDate)
       end: new Date(@request.query.endDate)
+    if dateRange.start >= dateRange.end
+      throw new Error("Invalid date range")
   if @request.query.eventType == 'user'
     events = UserEvents.find(
       _id:
@@ -453,20 +455,23 @@
         .pairs()
         .map ([end, group]) -> [new Date(parseInt(end)), group]
         .sortBy (x) -> x[0]
+        .filter ([date, noop]) ->
+          date > new Date(dateWindow.startDate)
         .reduce((sofar, [endDate, group]) ->
           value = group.reduce(((sofar, cur) -> sofar + cur.rate), 0)
           if sofar
-            sofar.concat([endDate, value])
+            sofar.concat([[endDate, value]])
           else
             [
-              [new Date(group[0].start), value]
+              [
+                new Date(Math.max(new Date(group[0].start), new Date(dateWindow.startDate)))
+                value
+              ]
             ,
               [endDate, value]
             ]
         , null)
-        .filter ([date, value]) ->
-          date >= new Date(dateWindow.startDate)
-        .map ([date, value]) -> [("" + date).split('T')[0], value]
+        .map ([date, value]) -> [date.toISOString().split('T')[0], value]
         .value()
       result = {
         eventId: event._id


Add public comment.

Add public comment.

--- server/server.coffee
+++ server/server.coffee
@@ -24,7 +24,10 @@
 QIREST_PORT = 5000

 # The grunt build tasks place all compiled and copied files within
-# the public directory.
+# the public directory. However, jspm and the test environment
+# expect files in the parent directory. Therefore, we make the
+# parent directory the root and qualify the HTML with the
+# public subdirectory.
 root = path.join(__dirname, '..')

 # The REST request handler.
@@ -46,7 +49,7 @@
     '/var/log/qiprofile.log'
   catch err
     './log/qiprofile.log'
-
+
 # The log file is either set in the environment or defaults to
 # log/qiprofile.log in the current directory.
 relLogFile = app.get('log') or defaultLogFile()
@@ -111,7 +114,7 @@
     headers:
       'x-timestamp': Date.now()
       'x-sent': true
-
+
   res.sendFile file, options, (err) ->
     if err
       console.log(err)
@@ -170,7 +173,7 @@
   restMode = if env is 'test' then 'development' else env
   # The REST server command.
   qirest = if restMode? then "qirest --#{ restMode }" else 'qirest'
-
+
   # The callback after the REST server is started.
   start_eve = ->
     # The server port.
@@ -184,7 +187,7 @@
       env = app.settings.env
       console.log "The qiprofile server is listening on port #{ port }" +
                   " in #{ env } mode."
-
+
   # Start the REST app without logging to the console.
   spawn(qirest, QIREST_PORT, start_eve, {silent: true})


Update status to 'confirmed' in tracking identifier

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1911,11 +1911,15 @@
       console.log "ALERT: #{username} is trying to confirm '#{status}' email"
       return callback null

-    @update { $set: { status: 'confirmed' } }, (err, res) =>
+    modifier = { status: 'confirmed' }
+
+    @update { $set: modifier }, (err, res) =>
       return callback err if err
       JUser.emit 'EmailConfirmed', this

       callback null
+
+      Tracker.identify username, modifier

       Tracker.track username, { subject : Tracker.types.FINISH_REGISTER }


StyleBlock: Use Koding bundle style

StyleBlock: Use Koding bundle style

--- workers/social/lib/social/render/styleblock.coffee
+++ workers/social/lib/social/render/styleblock.coffee
@@ -12,5 +12,5 @@
   <link rel="fluid-icon" href="/images/kd-fluid-icon512.png" title="Koding" />
   <link rel="stylesheet" href="/css/kd.#{KONFIG.version}.css" />
   <link rel="stylesheet" href="/css/introapp.#{KONFIG.version}.css" />
-  <link rel="stylesheet" href="/css/kdapp.#{KONFIG.version}.css" />
+  <link rel="stylesheet" href="/css/koding.#{KONFIG.version}.css" />
   """


Player: bugfix: event is show:editor

Player: bugfix: event is show:editor

--- coffee/src/player.coffee
+++ coffee/src/player.coffee
@@ -35,7 +35,7 @@
       eventhub: @eventhub

     if @acornModel.isNew()
-      @eventhub.trigger 'show:edit'
+      @eventhub.trigger 'show:editor'
     else
       @eventhub.trigger 'show:splash'


Fix some corner-cases in members admin panel

--- app/coffee/modules/admin/memberships.coffee
+++ app/coffee/modules/admin/memberships.coffee
@@ -69,7 +69,7 @@
         promise.then null, @.onInitialDataError.bind(@)

         @scope.$on "membersform:new:success", =>
-            @.loadMembers()
+            @.loadInitialData()
             @analytics.trackEvent("membership", "create", "create memberships on admin", 1)

     loadProject: ->


Handle \\ a little better

Handle \\ a little better

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -87,6 +87,7 @@
     console.log def, val
     tex = tex.replace new RegExp("\\\\#{def}\\s*", 'g'), val
   tex = '
' + tex
+  .replace /\\\\/g, '[DOUBLEBACKSLASH]'
   .replace /\\(BY|YEAR)\s*{([^{}]*)}/g, '$2'
   .replace /\\protect\s*/g, ''
   .replace /\\textbf\s*{([^{}]*)}/g, '$1'
@@ -118,6 +119,7 @@
   .replace /---/g, '—'
   .replace /--/g, '–'
   .replace /\n\n/g, '\n
\n'
+  .replace /\[DOUBLEBACKSLASH\]/g, '\\\\'

 @formats =
   file: (text, title) ->


add linum to teacher-student-view (#6395)

Co-authored-by: smallst_wsl <6c42689ff77371401bf43b7d56b93ac1ca3e92d3@163.com>

--- app/views/teachers/TeacherStudentView.coffee
+++ app/views/teachers/TeacherStudentView.coffee
@@ -124,6 +124,7 @@
       codeElem = $(@).first().children().first()
       lang = mode for mode of aceUtils.aceEditModes when codeElem?.hasClass('lang-' + mode)
       aceEditor = aceUtils.initializeACE(@, lang or classLang)
+      aceEditor.renderer.setShowGutter true
       aceEditors.push aceEditor

   updateSolutions: ->


:bug: Fix screen delta not set in decoration changes

--- lib/mixins/decoration-management.coffee
+++ lib/mixins/decoration-management.coffee
@@ -130,7 +130,7 @@

       [start, end] = [end, start] if start.row > end.row

-      @stackRangeChanges({start, end})
+      @stackRangeChanges({start, end, screenDelta: end - start})

     decoration = new Decoration(marker, this, decorationParams)
     @decorationsByMarkerId[marker.id] ?= []


Deep-clean before diffing

Deep-clean before diffing

--- src/code/providers/document-store-provider.coffee
+++ src/code/providers/document-store-provider.coffee
@@ -315,10 +315,9 @@
       opts =
         hash: @options.patchObjectHash if typeof @options.patchObjectHash is "function"
       # clean objects before diffing
-      [obj1,obj2].map (content) ->
-        for key of content
-          delete content[key] unless content[key]?
-      diff = jiff.diff(obj1, obj2, opts)
+      cleanedObj1 = JSON.parse JSON.stringify obj1
+      cleanedObj2 = JSON.parse JSON.stringify obj2
+      diff = jiff.diff(cleanedObj1, cleanedObj2, opts)
       return diff
     catch
       return null


Fix changed option name

Fix changed option name

--- components/search_bar/view.coffee
+++ components/search_bar/view.coffee
@@ -12,7 +12,7 @@
     return unless @$el.length

     # Search takes a fair_id param specific to fairs. Doesn't work for other models
-    { @mode, @restrictType, @$input, @fairId, @includePrivateResults, @limit, @autoselect, @displayModel } =
+    { @mode, @restrictType, @$input, @fairId, @includePrivateResults, @limit, @autoselect, @displayKind } =
       _.defaults options, limit: 10, autoselect: false, displayKind: true

     @$input ?= @$('input')
@@ -64,7 +64,7 @@
     @$el.removeClass 'is-open'

   suggestionTemplate: (item) =>
-    itemTemplate fill: fill, item: item, displayModel: @displayModel
+    itemTemplate fill: fill, item: item, displayKind: @displayKind

   announceQuery: (query) ->
     mediator.trigger 'search:doge'     if query is 'doge'


Adds equal matcher

Adds equal matcher

--- spec/main-spec.coffee
+++ spec/main-spec.coffee
@@ -78,3 +78,6 @@

                 #{JSON.stringify(sample, null, 2)}
               """
+
+          if test.equal
+            expect(sample).toEqual test.equal


Simplify $.scrollbarWidth

Simplify $.scrollbarWidth

--- file/js/jCounter.jQuery.coffee
+++ file/js/jCounter.jQuery.coffee
@@ -36,16 +36,14 @@
 	# See http://chris-spittles.co.uk/jquery-calculate-scrollbar-width/#comment-1603
 	###
 	$.scrollbarWidth = ->
-		$inner = $ '<div>'
-		$outer = $('<div>').append $inner
-		inner = $inner[0]
-		outer = $outer[0]
-
+		inner = $ '<div>'
+		outer = $('<div>').append inner
+
 		$('body').append outer
-		width1 = inner.offsetWidth
-		$outer.css 'overflow', 'scroll'
-		width2 = outer.clientWidth
-		do $outer.remove
+		width1 = inner.prop 'offsetWidth'
+		outer.css 'overflow', 'scroll'
+		width2 = outer.prop 'clientWidth'
+		do outer.remove
 		result = width1 - width2

 		$.scrollbarWidth = ->


Fixed unit tests

Fixed unit tests

--- app/tamarind/tests/WebGLCanvas.tests.coffee
+++ app/tamarind/tests/WebGLCanvas.tests.coffee
@@ -88,7 +88,7 @@

   createCanvasAndState = ->
     state = new State()
-    canvas = new WebGLCanvas(document.createElement('canvas'), state)
+    canvas = new WebGLCanvas(state)
     return [canvas, state]

   it 'should render a test image', (done) ->


fix some errors

fix some errors

--- app/templates/app/scripts/coffee/app.coffee
+++ app/templates/app/scripts/coffee/app.coffee
@@ -1,5 +1,5 @@
 define (require, exports, module)->
-  Backone = require "backbone"
+  Backbone = require "backbone"
   Router = require "Router"
   ServerApi = require "ServerApi"
   preprocess = require "preprocess"
@@ -9,13 +9,13 @@
   Widget = require "view/widget"

   #GAConstructor = require "sp-utils-gaconstructor"
-  #UserModel = require "models/UserModel"
-  #require "packages/social"
+  #UserModel = require "model/UserModel"
+  #social = require "packages/social"

-  $ = Backone.$
+  $ = Backbone.$

   class Application
-    constructor:(common)->
+    constructor: (common)->
       common.router = new Router
       common.api = new ServerApi

@@ -23,7 +23,7 @@
       #common.user = new UserModel

       # Init google analitics
-      # common.ga = new GAConstructor preprocess.GA, Backbone
+      #common.ga = new GAConstructor preprocess.GA, Backbone

       #common.sapi = new social.SocialApi
@@ -33,11 +33,11 @@

     start:->
       layout = {}
-      layout.header   = new Layout.HeaderLayout   el:"#header-layout"
-      layout.content  = new Layout.ContentLayout  el:"#content-layout"
-      layout.footer   = new Layout.FooterLayout   el:"#footer-layout"
-      layout.modal    = new Layout.ModalLayout    el:"#modal-layout"
+      layout.header   = new Layout.HeaderLayout   el: "#header-layout"
+      layout.content  = new Layout.ContentLayout  el: "#content-layout"
+      layout.footer   = new Layout.FooterLayout   el: "#footer-layout"
+      layout.modal    = new Layout.ModalLayout    el: "#modal-layout"
       for key, item of layout
         item.showCurrent()
         this[key] = item
-      Backone.history.start()
+      Backbone.history.start()


we're not writing this in CoffeeScript, so get rid of the src dir