:speak_no_evil: Remove console logging

:speak_no_evil: Remove console logging

--- spec/keymap-spec.coffee
+++ spec/keymap-spec.coffee
@@ -198,9 +198,7 @@
           expect(events).toEqual ['enter-visual-mode']

           events = []
-          console.log "v"
           keymap.handleKeyboardEvent(keydownEvent('v', target: editor))
-          console.log "i"
           keymap.handleKeyboardEvent(keydownEvent('i', target: editor))
           expect(events).toEqual []
           advanceClock(keymap.partialMatchTimeout)


Add specs for buffer changes that surround a marker

--- spec/marker-spec.coffee
+++ spec/marker-spec.coffee
@@ -509,3 +509,21 @@
           for marker in allStrategies
             expect(marker.getRange()).toEqual [[0, 6], [0, 9]]
             expect(marker.isValid()).toBe true
+
+    describe "when a change surrounds a marker", ->
+      it "truncates the marker to the end of the change and invalidates every invalidation strategy except 'never'", ->
+        buffer.setTextInRange([[0, 5], [0, 10]], "ABC")
+
+        for marker in allStrategies
+          expect(marker.getRange()).toEqual [[0, 8], [0, 8]]
+
+        for marker in difference(allStrategies, [neverMarker])
+          expect(marker.isValid()).toBe false
+
+        expect(neverMarker.isValid()).toBe true
+
+        buffer.undo()
+
+        for marker in allStrategies
+          expect(marker.getRange()).toEqual [[0, 6], [0, 9]]
+          expect(marker.isValid()).toBe true


Make tree-sitter parsing synchronous in specs, like TextMate

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -12,6 +12,7 @@
 TextEditor = require '../src/text-editor'
 TextEditorElement = require '../src/text-editor-element'
 TextMateLanguageMode = require '../src/text-mate-language-mode'
+TreeSitterLanguageMode = require '../src/tree-sitter-language-mode'
 {clipboard} = require 'electron'

 jasmineStyle = document.createElement('style')
@@ -101,6 +102,7 @@

   # make tokenization synchronous
   TextMateLanguageMode.prototype.chunkSize = Infinity
+  TreeSitterLanguageMode.prototype.syncTimeoutMicros = Infinity
   spyOn(TextMateLanguageMode.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk()

   # Without this spy, TextEditor.onDidTokenize callbacks would not be called


Don't modify mode array passed to ensureMode in case of re-use in another ensure

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -401,7 +401,7 @@
       expect(actual).toEqual(point)

   ensureMode: (mode) ->
-    mode = toArray(mode)
+    mode = toArray(mode).slice()
     expect(@vimState.isMode(mode...)).toBe(true)

     mode[0] = "#{mode[0]}-mode"


Filter out duplicate course instances client-side

--- app/views/courses/ClassroomView.coffee
+++ app/views/courses/ClassroomView.coffee
@@ -53,6 +53,11 @@
     @sessions = new CocoCollection([], { model: LevelSession })

   onCourseInstancesSync: ->
+    # clear duplicates with lodash magic
+    groups = _.groupBy @courseInstances.models, (ci) -> ci.get('courseID')
+    @courseInstances.reset(_.map(_.values(groups), _.first))
+    # TODO: Make having multiple course instances for a classroom/course pair impossible in the db
+
     @sessions = new CocoCollection([], { model: LevelSession })
     for courseInstance in @courseInstances.models
       sessions = new CocoCollection([], { url: "/db/course_instance/#{courseInstance.id}/level_sessions", model: LevelSession })


giving keys to proposal sorting functions

--- @client/proposal_sort_and_search.coffee
+++ @client/proposal_sort_and_search.coffee
@@ -154,6 +154,7 @@
 sort_options = [

   {
+    key: 'total_score'
     name: 'Total Score'
     description: "Each proposal is scored by the sum of all opinions, where each opinion expresses a score on a spectrum from -1 to 1. "
     order: (proposals) ->
@@ -173,6 +174,7 @@
         cache[proposal.key]
       proposals.sort (a, b) -> val(b) - val(a)
   }, {
+    key: 'trending'
     name: 'Trending'
     description: "Same as 'Total Score', except newer proposals and opinions are weighed more heavily."

@@ -234,16 +236,19 @@
         val(b) - val(a)
   },
   {
+    key: 'recency'
     order: (proposals) ->
       proposals.sort (a,b) -> new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
     name: 'Date: Most recent first'
     description: "The proposals submitted most recently are shown first."
   }, {
+    key: 'earliest'
     order: (proposals) ->
       proposals.sort (a,b) -> new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
     name: 'Date: Earliest first'
     description: "The proposals submitted first are shown first."
   }, {
+    key: 'unifying'
     name: 'Most unifying first'
     description: "The proposals on which participants are most united for or against are shown first."
     order: (proposals) ->
@@ -255,6 +260,7 @@
       return []

   }, {
+    key: 'polarizing'
     name: 'Most polarizing first'
     description: "The proposals on which participants are most split are shown highest."
     order: (proposals) ->
@@ -296,6 +302,7 @@
   },

   {
+    key: 'average_score'
     name: 'Average Score'
     description: "each proposal is scored by the average opinion, where each opinion expresses a score on a spectrum from -1 to 1. "
     order: (proposals) ->
@@ -343,12 +350,14 @@
   #
   # },
   {
+    key: 'alphabetical'
     name: 'Alphabetical order'
     order: (proposals) ->
       proposals.sort (a, b) -> a.name.localeCompare b.name
     description: "Sort alphabetically by the item's title"
   },
   {
+    key: 'random'
     name: 'Randomized'
     description: "Item order is randomized on each page load."

@@ -377,7 +386,7 @@
     loc = fetch('location')
     if loc.query_params?.sort_by
       for s in sort_options
-        if s.name == loc.query_params.sort_by.replace('_', ' ')
+        if loc.query_params.sort_by.replace('_', ' ') in [s.name, s.key]
           _.extend sort, s or sort_options[1]
           found = true
           break
@@ -386,7 +395,7 @@
       if def_sort = customization('default_proposal_sort_order')
         def = null
         for s in sort_options
-          if s.name == def_sort
+          if s.key == def_sort || s.name == def_sort
             def = s
             break
         _.extend sort, def or sort_options[1]


Refactor background tokenization

Refactor background tokenization

--- src/app/tokenized-buffer.coffee
+++ src/app/tokenized-buffer.coffee
@@ -70,23 +70,43 @@
       lastRow = @getLastRow()
       continue if invalidRow > lastRow

-      filledRegion = false
       row = invalidRow
       loop
         previousStack = @stackForRow(row)
         @screenLines[row] = @buildTokenizedScreenLineForRow(row, @stackForRow(row - 1))
-        @validateRow(row)
         if --rowsRemaining == 0
+          filledRegion = false
           break
         if row == lastRow or _.isEqual(@stackForRow(row), previousStack)
           filledRegion = true
           break
         row++

-      @trigger "change", { start: invalidRow, end: row, delta: 0}
+      @validateRow(row)
       @invalidateRow(row + 1) unless filledRegion
+      @trigger "change", { start: invalidRow, end: row, delta: 0 }

     @tokenizeInBackground()
+
+  firstInvalidRow: ->
+    @invalidRows[0]
+
+  validateRow: (row) ->
+    @invalidRows.shift() while @invalidRows[0] <= row
+
+  invalidateRow: (row) ->
+    @invalidRows.push(row)
+    @invalidRows.sort (a, b) -> a - b
+    @tokenizeInBackground()
+
+  updateInvalidRows: (start, end, delta) ->
+    @invalidRows = @invalidRows.map (row) ->
+      if row < start
+        row
+      else if start <= row <= end
+        end + delta + 1
+      else if row > end
+        row + delta

   buildPlaceholderScreenLinesForRows: (startRow, endRow) ->
     @buildPlaceholderScreenLineForRow(row) for row in [startRow..endRow]
@@ -188,29 +208,6 @@
   getLastRow: ->
     @buffer.getLastRow()

-  firstInvalidRow: ->
-    @invalidRows[0]
-
-  invalidateRow: (row) ->
-    @invalidRows.push(row)
-    @invalidRows.sort (a, b) -> a - b
-    @tokenizeInBackground()
-
-  validateRow: (row) ->
-    @invalidRows.shift() if @invalidRows[0] == row
-
-  updateInvalidRows: (start, end, delta) ->
-    updatedRows = []
-    for row in @invalidRows
-      if row < start
-        updatedRows.push(row)
-      else if start <= row <= end
-        updatedRows.push(end + delta + 1)
-      else if row > end
-        updatedRows.push(row + delta)
-
-    @invalidRows = updatedRows
-
   logLines: (start=0, end=@buffer.getLastRow()) ->
     for row in [start..end]
       line = @lineForScreenRow(row).text


Use correct console functions

Use correct console functions

--- src/coffee/helpers/logger.coffee
+++ src/coffee/helpers/logger.coffee
@@ -93,12 +93,12 @@
         streams: streams

     if silent
-      logger.trace = -> console.trace.apply(this, Array::slice.call(arguments))
-      logger.debug = -> console.debug.apply(this, Array::slice.call(arguments))
+      logger.trace = -> console.log.apply(this, Array::slice.call(arguments))
+      logger.debug = -> console.log.apply(this, Array::slice.call(arguments))
       logger.info = -> console.info.apply(this, Array::slice.call(arguments))
       logger.warn = -> console.warn.apply(this, Array::slice.call(arguments))
       logger.error = -> console.error.apply(this, Array::slice.call(arguments))
-      logger.fatal = -> console.fatal.apply(this, Array::slice.call(arguments))
+      logger.fatal = -> console.error.apply(this, Array::slice.call(arguments))

     return logger


fix(list-tabular): Invariant error caused by onDoneReceivingScrollEvents

--- src/components/list-tabular.cjsx
+++ src/components/list-tabular.cjsx
@@ -78,6 +78,9 @@
   componentDidMount: =>
     @updateRangeState()

+  componentWillUnmount: =>
+    clearTimeout(@_scrollTimer) if @_scrollTimer
+
   componentDidUpdate: (prevProps, prevState) =>
     # If our view has been swapped out for an entirely different one,
     # reset our scroll position to the top.
@@ -92,10 +95,8 @@

       # Create an event that fires when we stop receiving scroll events.
       # There is no "scrollend" event, but we really need one.
-      @_scrollTick ?= _.debounce =>
-        @onDoneReceivingScrollEvents()
-      , 100
-      @_scrollTick()
+      clearTimeout(@_scrollTimer) if @_scrollTimer
+      @_scrollTimer = setTimeout(@onDoneReceivingScrollEvents, 100)

       # If we just started scrolling, scrollInProgress changes our CSS styles
       # and disables pointer events to our contents for rendering speed


helper to "return undefined if selector is empty"

--- zechat/static/test_conversation.coffee
+++ zechat/static/test_conversation.coffee
@@ -19,6 +19,10 @@
   return deferred.promise

+zc.some = ($qs) ->
+  return $qs if $qs.length > 0
+
+
 describe 'conversation', ->

   # TODO don't touch browser's localstorage
@@ -30,7 +34,7 @@
       el: $app[0]
     )

-    zc.waitfor(-> $app.find('.conversation-compose').length or null)
+    zc.waitfor(-> zc.some($app.find('.conversation-compose')))
     .then ->
       $form = $app.find('.conversation-compose form')
       $form.find('[name=message]').val('hello world')


Coding style (@ vs. this)

Coding style (@ vs. this)

--- src/chaplin/models/model.coffee
+++ src/chaplin/models/model.coffee
@@ -59,7 +59,7 @@
     # so primitive values might be added and altered safely.
     # Map models to their attributes, recursively.
     serialize: (model) ->
-      serializeAttributes @, @getAttributes()
+      serializeAttributes this, @getAttributes()

     # Disposal
     # --------


Removed unneeded Course Action

Removed unneeded Course Action

--- app/assets/javascripts/actions/course_actions.coffee
+++ app/assets/javascripts/actions/course_actions.coffee
@@ -43,12 +43,5 @@
           CourseActions.persistCourse(data, course_id)
       .catch (data) ->
         { actionType: 'API_FAIL', data: data }
-  checkIfCourseExists: (key, course_id) ->
-    API.fetch(course_id, 'check').then (data) ->
-      message = if data.course_exists then 'This course already exists. Consider changing the name, school, or term to make it unique.' else null
-      { actionType: 'CHECK_SERVER', data: {
-        key: key
-        message: message
-      }}

 module.exports = CourseActions


fixed bug with clearing the i18n meta informations from element's text

--- app/assets/javascripts/i18n_viz/processing.js.coffee
+++ app/assets/javascripts/i18n_viz/processing.js.coffee
@@ -19,9 +19,10 @@

 # clear i18n data from element text
 $.fn.clearI18nText = () ->
-  $(this).textNodes().each () ->
-    $(this).replaceWith $(this).text().replace(I18nViz.global_regex, "")
-  $(this)
+  $el = $(this)
+  $el.textNodes().each () ->
+    $el.text $el.text().replace(I18nViz.global_regex, "")
+  $el

 # custom :i18n selectors
 $.extend $.expr[':'], {


removing code that was preventing follow point button from working in IE8

Former-commit-id: 65f796140125c3f840c76bc103b8708ed8f121cd [formerly 67622d82bc4abbc2bef75b1327272faf67b519cd] [formerly 66f475ab6dfe14289e236e49bd4f6dc65bf7fec7 [formerly 66f475ab6dfe14289e236e49bd4f6dc65bf7fec7 [formerly b7bbde8694475a1336f52f4f52b23e30abb128b0]]]
Former-commit-id: ccdfa5d234841b660ae4c44e47162473153adc65 [formerly 37728dc66daca3a97622f4b8413decbf9daa302f]
Former-commit-id: 0e9d2ff52e44eb014812e23b5d347392c825a1be
Former-commit-id: 41db817289b94588e8a142867565d0d692cc82fd

--- app/assets/javascripts/views/pointdetailsview.coffee
+++ app/assets/javascripts/views/pointdetailsview.coffee
@@ -81,9 +81,6 @@
         if $(ev.target).closest('.m-point-expanded').length == 0 || $(ev.target).closest('.m-point-expanded').data('id') != @model.id
           @close_details( $(ev.target).closest('[data-role="m-point"]').length == 0 && $(ev.target).closest('.l-navigate-wrap').length == 0 )

-      @$el.on 'click.m-point-details', (ev) =>
-        ev.stopPropagation() if !$(ev.target).data('target')
-
       $(document).on 'keyup.m-point-details', (ev) => @close_details() if ev.keyCode == 27 && $('.l-dialog-detachable').length == 0

       next()


Fix DOM prependHTML and appendHTML methods to work with tables

--- assets/javascripts/dom.js.coffee
+++ assets/javascripts/dom.js.coffee
@@ -39,7 +39,7 @@
     el.parentNode?.removeChild(el)

   prependHTML: (el, html) ->
-    tmp_el = document.createElement('div')
+    tmp_el = document.createElement('tbody')
     tmp_el.innerHTML = html
     child_nodes = tmp_el.childNodes
     for index in [(child_nodes.length-1)..0]
@@ -49,7 +49,7 @@
     el

   appendHTML: (el, html) ->
-    tmp_el = document.createElement('div')
+    tmp_el = document.createElement('tbody')
     tmp_el.innerHTML = html
     for node in tmp_el.childNodes
       continue unless node


Remove comment

Remove comment

--- apps/editorial_features/components/eoy/client.coffee
+++ apps/editorial_features/components/eoy/client.coffee
@@ -58,7 +58,6 @@
     return closest

   watchScrolling: =>
-    # @loadBody()
     scrollTop = $(window).scrollTop()
     scrollTop = Math.round(scrollTop)
     if scrollTop == 0


preload sputnik.ogg

preload sputnik.ogg

--- src/game/states/preloader.coffee
+++ src/game/states/preloader.coffee
@@ -11,6 +11,7 @@
     @load.image 'player', 'assets/images/player.png'
     @load.image 'Plough', 'assets/images/Plough.png'
     @load.image 'star', 'assets/images/star.png'
+    @load.audio 'sputnik', 'assets/sounds/sputnik.ogg'
     @load.bitmapFont 'minecraftia', 'assets/fonts/minecraftia.png', 'assets/fonts/minecraftia.xml'


only show step of parts once in homework review

--- src/components/task/review.cjsx
+++ src/components/task/review.cjsx
@@ -22,15 +22,22 @@

     stepProps = _.omit(@props, 'steps', 'focus')

-    stepsList = _.map steps, (step, index) ->
-      <TaskStep
-        {...stepProps}
-        id={step.id}
-        key="task-review-#{step.id}"
-        # focus on first problem
-        focus={focus and index is 0}
-        pinned={false}
-      />
+    uniqueSteps = _.uniq steps, false, (step) ->
+      step.content_url
+
+    stepsList = _.chain steps
+      .uniq false, (step) ->
+        step.content_url
+      .map (step, index) ->
+        <TaskStep
+          {...stepProps}
+          id={step.id}
+          key="task-review-#{step.id}"
+          # focus on first problem
+          focus={focus and index is 0}
+          pinned={false}
+        />
+      .value()

     <ReactCSSTransitionGroup transitionName="homework-review-problem">
       {stepsList}


Don't call Grim.deprecate for undefined context menus

This preserves the original behavior, in which passing undefined to
atom.contextMenu.add was a no-op that returned a no-op disposable.

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@gmail.com>

--- src/context-menu-manager.coffee
+++ src/context-menu-manager.coffee
@@ -76,8 +76,8 @@
   #     * `event` The click event that deployed the context menu.
   add: (itemsBySelector) ->
     # Detect deprecated file path as first argument
-    unless typeof itemsBySelector is 'object'
-      Grim.deprecate("ContextMenuManage::add has changed to take a single object as its argument. Please consult the documentation.")
+    if itemsBySelector? and typeof itemsBySelector isnt 'object'
+      Grim.deprecate("ContextMenuManager::add has changed to take a single object as its argument. Please consult the documentation.")
       itemsBySelector = arguments[1]
       devMode = arguments[2]?.devMode


* fix if race is not started, show "--:--"

--- widgets/countdown/countdown.coffee
+++ widgets/countdown/countdown.coffee
@@ -5,38 +5,42 @@

   startCountdown: =>
     current_timestamp = Math.round(new Date().getTime()/1000)
-    end_timestamp = Math.round( Date.parse($(@node).find(".more-info").html().replace('-', '/'))/1000 )
-    seconds_until_end = end_timestamp - current_timestamp
-    if seconds_until_end < 0
-      seconds_until_end = -seconds_until_end
-      d = Math.floor(seconds_until_end/86400)
-      h = Math.floor((seconds_until_end-(d*86400))/3600)
-      m = Math.floor((seconds_until_end-(d*86400)-(h*3600))/60)
-      s = seconds_until_end-(d*86400)-(h*3600)-(m*60)
-      @set('timeleft', @formatTime(m) + ":" + @formatTime(s))
+    nodeValue = $(@node).find(".more-info").html().replace('-', '/')
+    if nodeValue == ""
+      @set('timeleft', '--:--')
+    else
+      end_timestamp = Math.round( Date.parse(nodeValue)/1000 )
+      seconds_until_end = end_timestamp - current_timestamp
+      if seconds_until_end < 0
+        seconds_until_end = -seconds_until_end
+        d = Math.floor(seconds_until_end/86400)
+        h = Math.floor((seconds_until_end-(d*86400))/3600)
+        m = Math.floor((seconds_until_end-(d*86400)-(h*3600))/60)
+        s = seconds_until_end-(d*86400)-(h*3600)-(m*60)
+        @set('timeleft', @formatTime(m) + ":" + @formatTime(s))

-      #@set('timeleft', "00:00")
-      #for i in [0..10] by 1
-      #$(@node).fadeTo('fast', 0).fadeTo('fast', 1.0)
-      if $(@node).hasClass('warning')
-        $(@node).removeClass('warning')
-      $(@node).addClass('timeup')
-    else
-      d = Math.floor(seconds_until_end/86400)
-      h = Math.floor((seconds_until_end-(d*86400))/3600)
-      m = Math.floor((seconds_until_end-(d*86400)-(h*3600))/60)
-      s = seconds_until_end-(d*86400)-(h*3600)-(m*60)
-      #if d >0
-      #  dayname = 'day'
-      #  if d > 1
-      #    dayname = 'days'
-      #  @set('timeleft', d + " "+dayname+" " + @formatTime(h) + ":" + @formatTime(m) + ":" + @formatTime(s))
-      #else
-      @set('timeleft', @formatTime(m) + ":" + @formatTime(s))
-      $(@node).removeClass('timeup')
-      if seconds_until_end < 120
-        if $(@node).hasClass('warning') == false
-          $(@node).addClass('warning')
+        #@set('timeleft', "00:00")
+        #for i in [0..10] by 1
+        #$(@node).fadeTo('fast', 0).fadeTo('fast', 1.0)
+        if $(@node).hasClass('warning')
+          $(@node).removeClass('warning')
+        $(@node).addClass('timeup')
+      else
+        d = Math.floor(seconds_until_end/86400)
+        h = Math.floor((seconds_until_end-(d*86400))/3600)
+        m = Math.floor((seconds_until_end-(d*86400)-(h*3600))/60)
+        s = seconds_until_end-(d*86400)-(h*3600)-(m*60)
+        #if d >0
+        #  dayname = 'day'
+        #  if d > 1
+        #    dayname = 'days'
+        #  @set('timeleft', d + " "+dayname+" " + @formatTime(h) + ":" + @formatTime(m) + ":" + @formatTime(s))
+        #else
+        @set('timeleft', @formatTime(m) + ":" + @formatTime(s))
+        $(@node).removeClass('timeup')
+        if seconds_until_end < 120
+          if $(@node).hasClass('warning') == false
+            $(@node).addClass('warning')

   formatTime: (i) ->
     if i < 10 then "0" + i else i


Added reference to event on the .on method('ajax:success',...

--- app/assets/javascripts/prevent_double_submission.js.coffee
+++ app/assets/javascripts/prevent_double_submission.js.coffee
@@ -25,7 +25,7 @@
       unless event.target.id == "new_officing_voter"
         buttons = $(this).find(':button, :submit')
         App.PreventDoubleSubmission.disable_buttons(buttons)
-    ).on('ajax:success', ->
+    ).on('ajax:success', (event) ->
       unless event.target.id == "new_officing_voter"
         buttons = $(this).find(':button, :submit')
         App.PreventDoubleSubmission.reset_buttons(buttons)


Configure webpack dev server to accept connections from mobile

--- webpack.config.coffee
+++ webpack.config.coffee
@@ -22,7 +22,7 @@

   devServer:
     # contentBase : './build'
-    host        : 'localhost'
+    host        : '0.0.0.0'
     port        : 8080
     # colors      : true
     # progress    : true


UI tweak - key selector btn variant change

--- app/coffee/modules/main/dashboard/views/keySelector.coffee
+++ app/coffee/modules/main/dashboard/views/keySelector.coffee
@@ -1,7 +1,7 @@

 class KeyChild extends Mn.LayoutView
   tagName: 'li'
-  className: 'btn btn-light key--child d-flex justify-content-center align-items-center mx-2'
+  className: 'btn btn-outline-light key--child d-flex justify-content-center align-items-center mx-2'
   template: require('./templates/key_child')

   behaviors:
@@ -20,16 +20,16 @@

     # Macro
     if config.get('type') == 'macro'
-      return { label: 'Macro' }
+      return { label: 'M' }

     # Text
     if config.get('type') == 'text'
-      return { label: 'Text' }
+      return { label: 'T' }

     # Key
     # TODO - DISPLAY KEY IN VIEW
     if config.get('type') == 'key'
-      return { label: 'Key' }
+      return { label: 'K' }

 # # # # #


Something is working, altough very wrong so far

--- API/src/scraper.coffee
+++ API/src/scraper.coffee
@@ -133,7 +133,9 @@
                         console.log(tier_urls.length)
                         for tier_url in tier_urls
                           console.log(tier_url)
-                          urls.push tier_url
+                          for animal in tier_url
+                            console.log(animal)
+                            urls.push animal
                   .then () ->
                     f urls
                   #    new Promise (f, r) ->


Remove deprecated file

Remove deprecated file

--- client/Terminal/vmselectionview.coffee
+++ client/Terminal/vmselectionview.coffee
@@ -1,41 +0,0 @@
-class VMSelection extends KDModalView
-
-  constructor:(options={}, data)->
-
-    super
-      width           : 400
-      title           : "Select a VM"
-      overlay         : yes
-      draggable       : no
-      cancellable     : yes
-      appendToDomBody : yes
-      delegate        : options.delegate
-    , data
-
-    @setClass 'vm-selection'
-
-  viewAppended:->
-
-    @addSubView view = new KDCustomHTMLView tagName : 'ul'
-
-    { vmController } = KD.singletons
-    { vms }          = vmController
-
-    vmController.on 'vm.progress.start', ({alias, update}) => @[alias].handleVMStart update
-    vmController.on 'vm.progress.stop',  ({alias, update}) => @[alias].handleVMStop update
-    vmController.on 'vm.state.info',     ({alias, state})  => @[alias].handleVMInfo state
-
-    @addSubView ul = new KDCustomHTMLView tagName : 'ul'
-
-    vms.forEach (vm)=>
-      alias             = vm.hostnameAlias
-      @[alias] = new TerminalStartTabVMItem {}, vm
-      ul.addSubView @[alias]
-      appView = @getDelegate()
-      @[alias].on 'VMItemClicked', (vm)=>
-        appView.emit 'VMItemClicked', vm
-
-    for own alias, kite of vmController.kites
-      if kite.recentState
-        @[alias].handleVMInfo kite.recentState
-


fix filter

fix filter

--- client/app/stores/message_store.coffee
+++ client/app/stores/message_store.coffee
@@ -251,6 +251,7 @@

     handleFetchResult = (result) ->

+        _messages  = _messages.clear()
         if result.links? and result.links.next?
             # reinit params here for pagination on filtered lists
             _params = {}


Handle scroll position when pasting long content

Closes #143. Editor will still jump to the top but we restore the
position so there is a flicker in some cases.

--- src/modules/paste-manager.coffee
+++ src/modules/paste-manager.coffee
@@ -35,8 +35,11 @@
       delta.endLength += (@quill.getLength() - (range.end - range.start))
       delta.startLength = oldDocLength
       @quill.updateContents(delta, 'user')
-      @quill.focus()
       @quill.setSelection(range.start + lengthAdded, range.start + lengthAdded)
+      [line, offset] = @quill.editor.doc.findLineAt(range.start + lengthAdded)
+      lineBottom = line.node.offsetTop + line.node.offsetHeight
+      if lineBottom > scrollY + @quill.root.offsetHeight
+        scrollY = line.node.offsetTop - @quill.root.offsetHeight / 2
       iframe.scrollTo(0, scrollY)
     )


add will conley quote

add will conley quote

--- src/smc-webapp/landing_page.cjsx
+++ src/smc-webapp/landing_page.cjsx
@@ -641,7 +641,17 @@
                 </Row>
                 <Row>
                     <Col sm=7 className="hidden-xs" style=marginTop:'10px'>
-                        <SMC_Commercial />
+                        <Well style={'textAlign': 'center', 'float':'right'}>
+                            <SMC_Commercial />
+

+
+
+
+
"SageMathCloud provides a user friendly interface. Students don’t need to install any software at all.
+                            They just open up a web browser and go to cloud.sagemath.com and that’s it. They just type code directly
+                            in, hit shift+enter and it runs and they can see if it works. It provides immediate feedback. The course
+                            management features work really well."

+                        </Well>
                     </Col>
                     <Col sm=5>
                         <SignUp


Refactor: re-order source code

Refactor: re-order source code

--- app/assets/javascripts/home.js.coffee
+++ app/assets/javascripts/home.js.coffee
@@ -100,6 +100,11 @@
     element.removeClass('visible')
     element.addClass('hidden')

+set_fields_to_edit_mode = (in_edit_mode) ->
+  set_edit_mode_class(user_name(), in_edit_mode)
+  set_edit_mode_class(user_title(), in_edit_mode)
+  set_edit_mode_class(user_email(), false)                # always 'false' because email is not editable
+
 # TODO: rename to set_edit_mode?
 set_edit_mode_class = (element, is_in_edit_mode) ->
   element.attr('contentEditable', is_in_edit_mode)
@@ -147,11 +152,6 @@
   window.onbeforeunload = ->
     return 'Data you have entered may not be saved.' if is_dirty()
     return undefined                                      # 'undefined' suppresses 'leave page?' prompt
-
-set_fields_to_edit_mode = (in_edit_mode) ->
-  set_edit_mode_class(user_name(), in_edit_mode)
-  set_edit_mode_class(user_title(), in_edit_mode)
-  set_edit_mode_class(user_email(), false)              # always 'false' because email is not editable

 ready = ->
   if is_resume_page()


Allow links to still 'work' within tr[data-target]

--- app/assets/javascripts/jazz.js.coffee
+++ app/assets/javascripts/jazz.js.coffee
@@ -23,8 +23,6 @@
   decorate_plus_button($('form a.jazz.btn.plus'))

   # Make table rows with data-target attributes clickable
-  $('tr[data-target]').click ->
-    window.location = $(this).data('target')
-  # Prevent clicking on links from trigger the above
-  $('tr[data-target] a').click (e) ->
-    e.stopPropagation();
+  $('tr[data-target]').click (e) ->
+    if 'TD' == e.originalEvent.srcElement.tagName
+      window.location = $(this).data('target')


whitespace

whitespace

--- src/ansi_to_html.coffee
+++ src/ansi_to_html.coffee
@@ -63,9 +63,9 @@
 	STYLES["eb#{c}"] = "background-color:##{l}#{l}#{l}"

 extend = (dest, objs...) ->
-        for obj in objs
-                dest[k] = v for k, v of obj
-        dest
+	for obj in objs
+		dest[k] = v for k, v of obj
+	dest

 defaults =
 	fg: '#FFF'
@@ -124,10 +124,8 @@
 		["<#{tag}", (" style=\"#{style}\"" if style), ">"].join('')

 	pushText: (text) ->
-		if @opts.escapeXML
-			entities.encodeXML(text)
-		else
-			text
+		if @opts.escapeXML then entities.encodeXML(text)
+		else text

 	pushStyle: (style) ->
 		@pushTag "span", style


OHIE-131: Remove unwanted properties from export object

--- src/api/metadata.coffee
+++ src/api/metadata.coffee
@@ -19,26 +19,23 @@

 #Function to remove properties from export object
-# removeProperties = (obj) ->
-#   propertyID = '_id'
-#   propertyV = '__v'
-#
-#   for prop of obj
-#     if (prop == propertyID || prop == propertyV)
-#       delete obj[prop]
-#     else if ( typeof obj[prop] == 'object' || obj[prop] instanceof Array )
-#       console.log('new')
-#       console.log(prop)
-#       console.log(obj[prop])
-#       removeProperties(obj[prop])
-#   return obj
+removeProperties = (obj) ->
+  propertyID = '_id'
+  propertyV = '__v'
+
+  for prop of obj
+    if (prop == propertyID || prop == propertyV)
+      delete obj[prop]
+    else if ( typeof obj[prop] == 'object' || obj[prop] instanceof Array )
+      removeProperties(obj[prop])
+  return obj

 # Function to return unique identifier key and value for a collection
 getUniqueIdentifierForCollection = (collection, doc) ->
   switch collection
-    when 'Channels' then uidKey = 'name'; uid = doc.name
-    when 'Clients' then uidKey = 'clientID'; uid = doc.clientID
+    when 'Channels' then uidKey = 'name'; uid = doc.name
+    when 'Clients' then uidKey = 'clientID'; uid = doc.clientID
     when 'Mediators' then uidKey = 'urn'; uid = doc.urn
     when 'Users' then uidKey = 'email'; uid = doc.email
     when 'ContactGroups' then uidKey = 'groups'; uid = doc.groups
@@ -70,12 +67,10 @@

     # Return all documents from all collections for export
     for col of collections
-      exportObject[col] = yield collections[col].find().exec()
-
-    # for col of exportObject
-    #   for doc of exportObject[col]
-    #     if exportObject[col][doc]._id
-    #       exportObject[col][doc] = removeProperties exportObject[col][doc]
+      exportObject[col] = yield collections[col].find().lean().exec()
+      for doc of exportObject[col]
+        if exportObject[col][doc]._id
+          exportObject[col][doc] = removeProperties exportObject[col][doc]

     this.body = [exportObject]
     this.status = 200


Make advanced view persistent. PP-132 PP-141

--- client/src/scripts/scoring-view.coffee
+++ client/src/scripts/scoring-view.coffee
@@ -402,7 +402,9 @@
                 _thresholdPlot null
                 _stripPlot null
             when 'sheet'
-              _sheetView Steam.ScoringSheetView _, scorings
+              sheetView = _sheetView()
+              unless sheetView
+                _sheetView Steam.ScoringSheetView _, scorings

     _scoringType item.type


fix for ORDER HISTORY's time shown as NaN.NaN (#678)

--- app/js/services/market_service.coffee
+++ app/js/services/market_service.coffee
@@ -719,7 +719,7 @@
                 td = new TradeData
                 @helper.trade_history_to_order(r, td, market.assets_by_id, inverted)

-                td.localtime = new Date(td.timestamp.timestamp.replace('T',' ')+ ' UTC');
+                td.localtime = @helper.utils.toDate(td.timestamp.timestamp)
                 price_string = @helper.split_price td.price, market, inverted
                 td.price_int = price_string[0]
                 td.price_dec = price_string[1]


fix for pending order not being replaced by normal order and two orders are shown together for quite a while

--- app/js/services/market_service.coffee
+++ app/js/services/market_service.coffee
@@ -30,7 +30,7 @@
     touch: ->
         @timestamp = Date.now()
     expired: ->
-        return Date.now() - @timestamp > 15000
+        return Date.now() - @timestamp > 2000

 class Market
@@ -232,15 +232,17 @@
         return type

     find_order_by_transaction: (orders, t) ->
-        res = jsonPath.eval(t, "$.ledger_entries[0].to_account")
-        return null if not res or res.length == 0
-        to_account = res[0]
-        match = /^([A-Z]+)\-(\w+)/.exec(to_account)
-        return null unless match
-        subid = match[2]
-        return null unless subid.length > 5
+#        res = jsonPath.eval(t, "$.ledger_entries[0].to_account")
+#        return null if not res or res.length == 0
+#        to_account = res[0]
+#        match = /^([A-Z]+)\-(\w+)/.exec(to_account)
+#        return null unless match
+#        subid = match[2]
+#        return null unless subid.length > 5
+#        for o in orders
+#            return o if o.id and o.id.indexOf(subid) > -1
         for o in orders
-            return o if o.id and o.id.indexOf(subid) > -1
+            return o if o.id == t.trx_id
         return null

     date: (t) ->
@@ -526,33 +528,48 @@

     pull_orders: (market, inverted, account_name) ->
         orders = []
-        #console.log " ---- pull_orders"
-        @wallet_api.market_order_list(market.asset_base_symbol, market.asset_quantity_symbol, 100, account_name).then (results) =>
-            console.log results
-            for r in results
-                td = @helper.order_to_trade_data(r, market.base_asset, market.quantity_asset, inverted, inverted, inverted)
-                #console.log("------ market_order_list ------>", r, td) if r.type == "cover_order"
-                td.status = "posted" if td.status != "cover"
-                continue if (td.type == "short_order" or td.type == "cover_order") and not market.margins_available
-                orders.push td
-            @helper.update_array
-                target: @orders
-                data: orders
-                update: (target_el, data_el) =>
-                    if data_el.status and target_el.status != "canceled" and !(target_el.status == "pending" and !target_el.expired())
-                        target_el.status = data_el.status
-                    target_el.type = data_el.type
-                    target_el.cost = data_el.cost
-                    target_el.quantity = data_el.quantity unless target_el.status == "pending"
-                    target_el.collateral = data_el.collateral
-                    target_el.type = data_el.type
-                    target_el.display_type = @helper.capitalize(target_el.type.split("_")[0])
-                can_remove: (o) ->
-                    #!(o.status == "unconfirmed" or (o.status == "pending" and !o.expired()))
-                    !(o.status == "unconfirmed" or (o.status == "pending" and !o.expired()))
-            @helper.sort_array(@orders, "price", "quantity", false)
-            if magic_unicorn?
-                magic_unicorn.log_message("in MarketService.pull_orders - received orders: #{results.length}, orders shown: #{@orders.length}")
+        deferred = @q.defer()
+        @wallet_api.account_transaction_history(account_name).then (results) =>
+            for t in results
+                continue if t.is_confirmed
+                order = @helper.find_order_by_transaction(@orders, t)
+                if order
+                    order.status = "pending"
+                    order.touch()
+
+            @wallet_api.market_order_list(market.asset_base_symbol, market.asset_quantity_symbol, 100, account_name).then (results) =>
+                console.log results
+                for r in results
+                    td = @helper.order_to_trade_data(r, market.base_asset, market.quantity_asset, inverted, inverted, inverted)
+                    #console.log("------ market_order_list ------>", r, td) if r.type == "cover_order"
+                    td.status = "posted" if td.status != "cover"
+                    continue if (td.type == "short_order" or td.type == "cover_order") and not market.margins_available
+                    orders.push td
+                @helper.update_array
+                    target: @orders
+                    data: orders
+                    update: (target_el, data_el) =>
+                        if data_el.status and target_el.status != "canceled" and !(target_el.status == "pending" and !target_el.expired())
+                            target_el.status = data_el.status
+                        target_el.type = data_el.type
+                        target_el.cost = data_el.cost
+                        target_el.quantity = data_el.quantity unless target_el.status == "pending"
+                        target_el.collateral = data_el.collateral
+                        target_el.type = data_el.type
+                        target_el.display_type = @helper.capitalize(target_el.type.split("_")[0])
+                    can_remove: (o) ->
+                        #!(o.status == "unconfirmed" or (o.status == "pending" and !o.expired()))
+                        !(o.status == "unconfirmed" or (o.status == "pending" and !o.expired()))
+                @helper.sort_array(@orders, "price", "quantity", false)
+                if magic_unicorn?
+                    magic_unicorn.log_message("in MarketService.pull_orders - received orders: #{results.length}, orders shown: #{@orders.length}")
+
+                deferred.resolve(true)
+
+            , (error) -> deferred.reject(error)
+        , (error) -> deferred.reject(error)
+
+        return deferred.promise

     pull_trades: (market, inverted) ->
@@ -594,14 +611,15 @@
             for t in new_trades.reverse()
                 @my_trades.unshift t

-    pull_unconfirmed_transactions: (account_name) ->
-        @wallet_api.account_transaction_history(account_name).then (results) =>
-            for t in results
-                continue if t.is_confirmed
-                order = @helper.find_order_by_transaction(@orders, t)
-                if order
-                    order.status = "pending"
-                    order.touch()
+#    pull_unconfirmed_transactions: (account_name) ->
+#        @wallet_api.account_transaction_history(account_name).then (results) =>
+#            for t in results
+#                continue if t.is_confirmed
+#                order = @helper.find_order_by_transaction(@orders, t)
+#                if order
+#                    console.log "------ pull_unconfirmed_transactions order found ------>", order
+#                    order.status = "pending"
+#                    order.touch()

     pull_price_history: (market, inverted) ->
         #console.log "------ pull_price_history ------>"
@@ -642,8 +660,8 @@
             self.pull_asks(market, self.market.inverted),
             self.pull_orders(market, self.market.inverted, data.account_name),
             self.pull_trades(market, self.market.inverted),
-            self.pull_my_trades(market, self.market.inverted, data.account_name),
-            self.pull_unconfirmed_transactions(data.account_name)
+            self.pull_my_trades(market, self.market.inverted, data.account_name)
+            #self.pull_unconfirmed_transactions(data.account_name)
         ]
         if market.margins_available
             promises.push(self.pull_shorts(market, self.market.inverted, data.account_name))


relative percentage

relative percentage

Former-commit-id: fbc0a12bdda5a11e6af70083bf731f41e882f991 [formerly 40156c53959c5c5918b614046723b0823adbe458]
Former-commit-id: 897e5437b5fb8805e567df32ac1abd94aec98982

--- src/views/labels/MetaView.coffee
+++ src/views/labels/MetaView.coffee
@@ -35,7 +35,8 @@
       # adds gaps
       seq = @model.get('seq')
       gaps = _.reduce seq, ((memo, c) -> memo++ if c is '-';memo),0
-      gaps = (gaps / seq.length).toFixed(1)
+      # 2-place percentage , e.g. 42%
+      gaps = (gaps * 100 / seq.length).toFixed(0)

       # append gap count
       gapSpan = document.createElement 'span'


Missing arguments in async callback

Missing arguments in async callback

--- bootstrap.coffee
+++ bootstrap.coffee
@@ -42,7 +42,8 @@
 	(callback) ->
 		console.log('Starting VPN client..')
 		exec('systemctl start openvpn@client', callback)
-	(callback) ->
+
+	(stdout, stderr, callback) ->
 		console.log('Enabling VPN client..')
 		exec('systemctl enable openvpn@client', callback)
 ]


Add debugging information for when listeners matches message. Fixes #398

Signed-off-by: Josh Nichols <c028c213ed5efcf30c3f4fc7361dbde0c893c5b7@technicalpickles.com>

--- src/listener.coffee
+++ src/listener.coffee
@@ -19,6 +19,7 @@
   # Returns a boolean of whether the matcher matched.
   call: (message) ->
     if match = @matcher message
+      @robot.logger.debug "Message '#{message}' matched regex /#{inspect @regex}/" if @regex
       @callback new @robot.Response(@robot, message, match)
       true
     else


Perf: Category

Perf: Category

Rather than computing the types ahead of time when the object is created and generating methods, keep a few standard methods around and change the hardcoded names to maps so the lookup is O[1]

--- src/flux/models/category.coffee
+++ src/flux/models/category.coffee
@@ -5,29 +5,29 @@
 # We look for a few standard categories and display them in the Mailboxes
 # portion of the left sidebar. Note that these may not all be present on
 # a particular account.
-StandardCategoryNames = [
-  "inbox"
-  "important"
+StandardCategories = {
+  "inbox",
+  "important",
+  "sent",
+  "drafts",
+  "all",
+  "spam",
+  "archive",
+  "trash"
+}
+
+LockedCategories = {
   "sent"
-  "drafts"
-  "all"
-  "spam"
-  "archive"
-  "trash"
-]
+}

-LockedCategoryNames = [
-  "sent"
-]
-
-HiddenCategoryNames = [
+HiddenCategories = {
   "sent"
   "drafts"
   "all"
   "archive"
   "starred"
   "important"
-]
+}

 AllMailName = "all"

@@ -71,51 +71,16 @@
     User: 'user'
     Hidden: 'hidden'

-  @StandardCategoryNames: StandardCategoryNames
-  @LockedCategoryNames: LockedCategoryNames
-  @HiddenCategoryNames: HiddenCategoryNames
+  @StandardCategoryNames: Object.keys(StandardCategories)
+  @LockedCategoryNames: Object.keys(LockedCategories)
+  @HiddenCategoryNames: Object.keys(HiddenCategories)

   constructor: ->
     super
-    @_initCategoryTypes()

   fromJSON: (json) ->
     super
-    @_initCategoryTypes()
     @
-
-  _initCategoryTypes: =>
-    @types = []
-    if not (@name in StandardCategoryNames) and not (@name in HiddenCategoryNames)
-      @types.push @constructor.Types.User
-    if @name in LockedCategoryNames
-      @types.push @constructor.Types.Hidden
-    if @name in StandardCategoryNames
-      @types.push @constructor.Types.Standard
-    if @name in HiddenCategoryNames
-      @types.push @constructor.Types.Hidden
-
-    # Define getter for isStandardCategory. Must take into account important
-    # setting
-    Object.defineProperty @, "isStandardCategory",
-      enumerable: true
-      configurable: true
-      value: (showImportant)=>
-        showImportant ?= NylasEnv.config.get('core.workspace.showImportant')
-        val = @constructor.Types.Standard
-        if showImportant is true
-          val in @types
-        else
-          val in @types and @name isnt 'important'
-
-    # Define getters for other category types
-    for key, val of @constructor.Types
-      continue if val is @constructor.Types.Standard
-      do (key, val) =>
-        Object.defineProperty @, "is#{key}Category",
-          enumerable: true
-          configurable: true
-          value: => val in @types

   hue: ->
     return 0 unless @displayName
@@ -125,4 +90,20 @@
     hue = hue * (396.0/512.0)
     hue

+  isStandardCategory: (showImportant) ->
+    showImportant ?= NylasEnv.config.get('core.workspace.showImportant')
+    if showImportant is true
+      StandardCategories[@name]?
+    else
+      StandardCategories[@name]? and @name isnt 'important'
+
+  isLockedCategory: ->
+     LockedCategories[@name]?
+
+  isHiddenCategory: ->
+    HiddenCategories[@name]?
+
+  isUserCategory: ->
+    not @isStandardCategory() and not @isHiddenCategory()
+
 module.exports = Category


Add NylasLongConnection to nylas-exports

--- src/global/nylas-exports.coffee
+++ src/global/nylas-exports.coffee
@@ -53,6 +53,7 @@

   # API Endpoints
   @lazyLoad "NylasAPI", 'flux/nylas-api'
+  @lazyLoad "NylasAPIRequest", 'flux/nylas-api-request'
   @lazyLoad "EdgehillAPI", 'flux/edgehill-api'
   @lazyLoad "NylasLongConnection", 'flux/nylas-long-connection'
   @lazyLoad "NylasSyncStatusStore", 'flux/stores/nylas-sync-status-store'


YouTubeShell: decomposed YouTubePlayerView

@tenedor

--- coffee/src/shells/youtube.shell.coffee
+++ coffee/src/shells/youtube.shell.coffee
@@ -87,85 +87,152 @@
 class YouTubeShell.MediaView extends VideoLinkShell.MediaView

+  className: @classNameExtend 'youtube-shell'
+
+
+  initialize: =>
+    super
+
+    @playerView = new YouTubeShell.YouTubePlayerView
+      model: @model
+      eventhub: @eventhub
+
+    @listenTo @playerView, 'YouTubePlayer:StateChange', =>
+      if @isPlaying() then @timer.startTick() else @timer.stopTick()
+
+
+  render: =>
+    super
+    @$el.empty()
+    @$el.append @playerView.render().el
+    # start playing once ready
+    # @vimeoPlayerView.once 'VimeoPlayer:Ready', @play
+    @
+
+
+  # Implement MediaView APi
+
+
+  play: =>
+    @playerView.play()
+
+
+  pause: =>
+    @playerView.pause()
+
+
+  isPlaying: =>
+    @playerView.isPlaying() ? false
+
+
+  seek: (seconds) =>
+    @playerView.seekTo seconds
+
+
+  seekOffset: =>
+    @playerView.seekOffset() ? 0
+
+
+
+class YouTubeShell.RemixView extends VideoLinkShell.RemixView
+
+
   className: @classNameExtend('youtube-shell')

+
+class YouTubeShell.YouTubePlayerView extends athena.lib.View
+
+
+  className: @classNameExtend 'youtube-player-view'
+
+
+  initialize: =>
+    super
+    @totalTime = Infinity
+
+
   render: =>
     super
     @$el.empty()
-
-    # initialize YouTube setup and add the YouTube player iframe
-    @onYTInitialize()
-    link = @model.embedLink()
-    @$el.append(acorn.util.iframe(link, 'ytplayer'))
-
+    @$el.append acorn.util.iframe @model.embedLink(), @playerId()
+    @initializeYouTubeAPI()
     @

-  play: => @ytplayer?.playVideo()
-
-
-  pause: => @ytplayer?.pauseVideo()
-
-
-  isPlaying: => @ytplayer?.getPlayerState() == YT.PlayerState.PLAYING
-
-
-  seek: (seconds) => @ytplayer?.seekTo(seconds, true)
-
-
-  seekOffset: => @ytplayer?.getCurrentTime() ? 0
+  # id for the player element. id must be unique in the entire page to allow
+  # the YouTube API to differentiate between multiple players.
+  playerId: =>
+    "youtube-player-#{@cid}"
+
+
+  # Control the player
+
+
+  play: =>
+    @player?.playVideo()
+
+
+  pause: =>
+    @player?.pauseVideo()
+
+
+  isPlaying: =>
+    @player?.getPlayerState() == YT.PlayerState.PLAYING
+
+
+  seekTo: (seconds) =>
+    @player?.seekTo(seconds, true)
+
+
+  seekOffset: =>
+    @player?.getCurrentTime() ? 0
+

   # YouTube API - communication between the YouTube iframe API and the shell.
   # see https://developers.google.com/youtube/iframe_api_reference
-  youtubePlayerApiSrc: 'http://www.youtube.com/iframe_api'
-
-
-  # initialize youtube API. initialization should happen only once per page load
-  onYTInitialize: =>
-    if window.onYouTubeIframeAPIReady
-      # YT API expects the id of a player currently on the page. Backbone may
-      # have not yet added the current DOM subtree to the page DOM.
-      setTimeout @onYTReady, 0
-
-    else
-      # setup YT ready callback and include the YouTubePlayerAPI code
-      window.onYouTubeIframeAPIReady = @onYTReady
-
-      script = $('<script>').attr('src', @youtubePlayerApiSrc)
-      $('body').append(script)
-
-
-  onYTReady: =>
-    @ytplayer = new YT.Player 'ytplayer', events:
-      onReady: @onYTPlayerReady
-      onStateChange: @onYTPlayerStateChange
-
+  youTubePlayerApiSrc: 'http://www.youtube.com/iframe_api'
+
+
+  # initialize youtube API. should happen only once per page load
+  initializeYouTubeAPI: =>
+
+    # if Vimeo hasn't been initialized, initialize it.
+    unless window.onYouTubeIframeAPIReady
+      $.getScript @youTubePlayerApiSrc, @onYouTubeAPIReady
+      return
+
+    # must call onYouTubeAPIReady once the current render call stack finishes
+    # YouTube API expects the id of a player currently on the page. Backbone may
+    # have not yet added the current DOM subtree to the page DOM.
+    setTimeout @onYouTubeAPIReady, 0
+
+
+  onYouTubeAPIReady: =>
     # replace the callback with a no-op
     window.onYouTubeIframeAPIReady = ->

-
-  onYTPlayerReady: =>
-    # this *should* initialize the playback at the correct point but doesn't.
-    # Need a robust solution (tick)
-    start = parseInt(@model.get('time_start') ? 0, 10)
-
-    if @options.autoplay
-      @ytplayer.loadVideoById(@model.youtubeId(), start)
-    else
-      @ytplayer.cueVideoById(@model.youtubeId(), start)
-
-
-  onYTPlayerStateChange: (event) =>
-    if @isPlaying() then @timer.startTick() else @timer.stopTick()
-
-
-
-class YouTubeShell.RemixView extends VideoLinkShell.RemixView
-
-
-  className: @classNameExtend('youtube-shell')
+    # wait until the YT.Player class is there
+    unless YT.Player
+      setTimeout @onYouTubeAPIReady, 100
+      return
+
+    # create the player object
+    @player = new YT.Player @playerId(), events:
+
+      onReady: =>
+        # this *should* initialize the playback at the correct point but
+        # doesn't. Need a more robust solution (tick)
+        start = parseInt(@model.timeStart() ? 0, 10)
+
+        if @options.autoplay
+          @player.loadVideoById(@model.youtubeId(), start)
+        else
+          @player.cueVideoById(@model.youtubeId(), start)
+
+      onStateChange: (event) =>
+        @trigger 'YouTubePlayer:StateChange', @, @player.getPlayerState()


Use symbol provided rather than hardcoding $

--- components/buyers_premium/index.coffee
+++ components/buyers_premium/index.coffee
@@ -13,6 +13,7 @@
       [hammerPerc, portionPerc] = _.pluck(
         auction.get('buyers_premium').schedule, 'percent'
       )
+      symbol = auction.get('buyers_premium').symbol
       callback null, """

@@ -21,7 +22,7 @@

                   On the hammer price up to and including \
-                  #{formatMoney(maxCents / 100, '$', 0)}
+                  #{formatMoney(maxCents / 100, symbol, 0)}

#{hammerPerc * 100}%

@@ -29,7 +30,7 @@

                   On the portion of the hammer price in excess of \
-                  #{formatMoney(maxCents / 100, '$', 0)}
+                  #{formatMoney(maxCents / 100, symbol, 0)}

#{portionPerc * 100}%


Tracks create and destroy connection

Tracks create and destroy connection

--- components/connect/client/index.coffee
+++ components/connect/client/index.coffee
@@ -1,6 +1,7 @@
 Promise = require 'bluebird-q'
 { API_URL } = require('sharify').data
 mediator = require '../../../lib/mediator.coffee'
+{ track, en } = require '../../../lib/analytics.coffee'
 ConnectView = require './view.coffee'
 ConnectCollections = require './collections.coffee'

@@ -17,12 +18,16 @@
             connectable_id: connectable.id
             connectable_type: connectable.get 'base_class'

+      track.submit en.CREATE_CONNECTION
+
     .on 'connection:removed', (channel, connectable, queue) ->
       queue.enqueue ->
         Promise $.ajax
           type: 'DELETE'
           url: "#{API_URL}/channels/#{channel.id}/blocks/#{connectable.id}"

+      track.submit en.DESTROY_CONNECTION
+
   new ConnectView
     model: block
     collection: recentConnections


adding dumb responses

adding dumb responses

--- scripts/vote.coffee
+++ scripts/vote.coffee
@@ -139,7 +139,7 @@

     switch action
       # Vote on a current issue
-      when "yes","po","bai","da","si","ano","ja","jah","kylia","oui","vai","igen","taip","iva","tak","sim","ie","yog","ha","inde","ee","haa","oo","ya","yea","yeah","iya nih","jes"
+      when "yes","po","bai","da","si","ano","ja","jah","kylia","oui","vai","igen","taip","iva","tak","sim","ie","yog","ha","inde","ee","haa","oo","ya","yea","yeah","iya nih","jes", "yee"
         return msg.reply "There's no vote going on." unless Vote.current?
         result = Vote.current?.yes msg.message.user.name
         reply = "Your vote " +
@@ -153,7 +153,7 @@
           (if result then "totally counted." else "was absolutely worthless!")
         msg.reply reply

-      when "random"
+      when "random", "meh", "ehem"
        return msg.reply "You can't randomly vote on nothing!" unless Vote.current?
        voteResult = msg.random ["yes", "no"]
        result = if voteResult == "yes"


Update vote.coffee

Update vote.coffee

--- scripts/vote.coffee
+++ scripts/vote.coffee
@@ -139,11 +139,7 @@

     switch action
       # Vote on a current issue
-<<<<<<< HEAD
-      when "yes","po","bai","da","si","ano","ja","jah","kylia","oui","vai","igen","taip","iva","tak","sim","ie","yog","ha","inde","ee","haa","oo","ya","yea","yeah","iya nih","jes", "yee"
-=======
-      when "yes","po","bai","da","si","ano","ja","jah","kylia","oui","vai","igen","taip","iva","tak","sim","ie","yog","ha","inde","ee","haa","oo","ya","yea","yeah","iya nih","jes","ye","okay"
->>>>>>> 6a8b06a94532502c3b801b7eea644256fc8e9310
+      when "yes","po","bai","da","si","ano","ja","jah","kylia","oui","vai","igen","taip","iva","tak","sim","ie","yog","ha","inde","ee","haa","oo","ya","yea","yeah","iya nih","jes","ye","okay","yee"
         return msg.reply "There's no vote going on." unless Vote.current?
         result = Vote.current?.yes msg.message.user.name
         reply = "Your vote " +
@@ -157,11 +153,7 @@
           (if result then "totally counted." else "was absolutely worthless!")
         msg.reply reply

-<<<<<<< HEAD
-      when "random", "meh", "ehem"
-=======
-      when "random","rando"
->>>>>>> 6a8b06a94532502c3b801b7eea644256fc8e9310
+      when "random","rando","meh","ehem"
        return msg.reply "You can't randomly vote on nothing!" unless Vote.current?
        voteResult = msg.random ["yes", "no"]
        result = if voteResult == "yes"


another solution (#6369)

another solution (#6369)

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

--- app/views/ladder/LadderTabView.coffee
+++ app/views/ladder/LadderTabView.coffee
@@ -313,15 +313,15 @@
   onClickPlayerName: (e) ->
     if me.isAdmin()
       row = $(e.target).parent()
-      player = new User _id: row.data 'player-id'
-      playerName = row.find('.name-col-cell').text()
       session = new LevelSession _id: row.data 'session-id'
-      if /^cq/.test(playerName) and not features.china
-        # CodeQuest users use cq name prefix and don't exist on global server; hack around this
-        models = [session]
-      else
-        models = [session, player]
-      @openModalView new ModelModal models: models
+      @supermodel.loadModel session
+      @listenToOnce session, 'sync', (_session) =>
+        if _session.get('source')?.name
+          models = [_session]
+        else
+          player = new User _id: row.data 'player-id'
+          models = [_session, player]
+        @openModalView new ModelModal models: models
     else if me.isTeacher()
       ;
     else


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -41,11 +41,7 @@
     width: 32
     height: 32

-  # This is kind of gross, but we need to make sure
-  # App.width and App.height are around for the tests.
-  App ||= {}
-  App.width ||= 480
-  App.height ||= 320
+

   self.bind "update", ->
     I.x = I.x.clamp(I.width / 2, App.width - I.width / 2)


Fix draggable and slider component

Fix draggable and slider component

The fix I added in https://github.com/koenbok/Framer/commit/74b8b3b75a717c453b471d8ea2bca971284c257d broke this, and it seems not to be needed anymore.

--- framer/LayerDraggable.coffee
+++ framer/LayerDraggable.coffee
@@ -189,16 +189,6 @@
 			x: touchEvent.clientX
 			y: touchEvent.clientY
 			t: Date.now() # We don't use timeStamp because it's different on Chrome/Safari
-
-		# Disable constraints drag if overdrag is disabled
-		if @overdrag is false
-			# TODO: We still need to account for the cursor offset here
-			frame = Utils.convertFrameToContext(@constraints, @layer, true, false)
-			return if event.point.x < Utils.frameGetMinX(frame)
-			return if event.point.x > Utils.frameGetMaxX(frame)
-			return if event.point.y < Utils.frameGetMinY(frame)
-			return if event.point.y > Utils.frameGetMaxY(frame)
-

 		point = _.clone(@_point)


monkey-patch to precompile these until we get a solution to this.

--- client/app/CommonViews/comments/commentlistitemview.coffee
+++ client/app/CommonViews/comments/commentlistitemview.coffee
@@ -99,13 +99,16 @@
                 title    : "Error, please try again later!"

   updateTemplate:(force = no)->
+    # TODO: these pistachios are written in JS, pending a solution
+    #  to the problem of statically not being able to find pistachios unless
+    #  they are contained inside a property called "pistachio".
     if @getData().getAt 'deletedAt'
       {type} = @getOptions()
       @setClass "deleted"
       if @deleter
-        @setTemplate "
{{> @author}}'s #{type} has been deleted by {{> @deleter}}.
"
+        @setTemplate "
{{> this.author}}'s #{type} has been deleted by {{> this.deleter}}.
"
       else
-       @setTemplate "
{{> @author}}'s #{type} has been deleted.
"
+       @setTemplate "
{{> this.author}}'s #{type} has been deleted.
"
     else if force
       @setTemplate @pistachio()


Client: Activity: ActivityPromptModal extended from base ActivityModal

--- client/app/lib/components/activitypromptmodal/index.coffee
+++ client/app/lib/components/activitypromptmodal/index.coffee
@@ -1,7 +1,7 @@
-kd     = require 'kd'
-React  = require 'kd-react'
-Portal = require 'react-portal'
-
+kd            = require 'kd'
+React         = require 'kd-react'
+Portal        = require 'react-portal'
+ActivityModal = require 'app/components/activitymodal'
 noop = ->

@@ -10,8 +10,6 @@
   render: ->
     <div className="ModalOverlay" />

-
-module.exports = class ActivityPromptModal extends React.Component

 module.exports = class ActivityPromptModal extends ActivityModal


[grunt] remove copypast from `clean` task


Add extra information to GrabBackFocus' pushState helper

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -135,8 +135,9 @@

 # Pages can load new content dynamically and change the displayed URL using history.pushState. Since this can
 # often be indistinguishable from an actual new page load for the user, we should also re-start GrabBackFocus
-# for these as well.
+# for these as well. This fixes issue #1622.
 handlerStack.push
+  _name: "GrabBackFocus-pushState-monitor"
   click: (event) ->
     target = event.target
     while target


Add missing scope config.

Add missing scope config.

--- server/index.coffee
+++ server/index.coffee
@@ -79,7 +79,7 @@
   else
     next()

-app.get '/auth/example', passport.authenticate('oauth2')
+app.get '/auth/example', passport.authenticate('oauth2', scope: 'read')

 app.get '/auth/example/callback', passport.authenticate('oauth2', failureRedirect: '/'), (req, res) ->
   res.redirect('/')


ContentSearch: Set workspace root path as default search path.

--- client/ide/lib/views/contentsearch/idecontentsearch.coffee
+++ client/ide/lib/views/contentsearch/idecontentsearch.coffee
@@ -10,6 +10,8 @@
 module.exports = class IDEContentSearch extends kd.ModalViewWithForms

   constructor: (options = {}, data) ->
+
+    { rootPath } = data.workspace

     options.cssClass        = 'content-search-modal'
     options.tabs            =
@@ -38,8 +40,8 @@
             whereInput      :
               type          : 'text'
               label         : 'Where'
-              placeholder   : "/home/#{nick()}"
-              defaultValue  : "/home/#{nick()}"
+              placeholder   : rootPath
+              defaultValue  : rootPath
               keydown       : _.bind @handleKeyDown, this
             caseToggle      :
               cssClass      : 'checkbox'


FlexSplitStorage: fix setter and store callback issues

--- client/new-stack-editor/lib/views/flexsplit/storage.coffee
+++ client/new-stack-editor/lib/views/flexsplit/storage.coffee
@@ -59,7 +59,10 @@

   set: (identifier, fractions, callback = kd.noop) ->
     @storage[identifier] = fractions
+    callback null, @storage

   store: (callback = kd.noop) ->
-    @adapter?.store @storage, callback
+    if @adapter?.store?
+    then @adapter.store @storage, callback
+    else callback null


activity: skip tag transformation in blockquotes

--- client/Social/Activity/views/activitylistitem.coffee
+++ client/Social/Activity/views/activitylistitem.coffee
@@ -104,7 +104,16 @@

     {slug}   = KD.getGroup()

+    skipRanges  = @getBlockquoteRanges text
+    inSkipRange = (position) ->
+      for [start, end] in skipRanges
+        return yes  if start <= position <= end
+      return no
+
     return text.replace /#(\w+)/g, (match, tag, offset) ->
+
+      return match  if inSkipRange offset
+
       pre  = text[offset - 1]
       post = text[offset + match.length]

@@ -118,6 +127,22 @@

       href = KD.utils.groupifyLink "Activity/Topic/#{tag}", yes
       return "[##{tag}](#{href})"
+
+
+  getBlockquoteRanges: (text = '') ->
+
+    ranges = []
+    read   = 0
+
+    for part, index in text.split '```'
+      blockquote = index %% 2 is 1
+
+      if blockquote
+        ranges.push [read, read + part.length - 1]
+
+      read += part.length + 3
+
+    return ranges

   formatBlockquotes: (text = '') ->


Reach editorView and inputTitle from StackTemplateView

--- client/admin/lib/views/stacks/definestackview.coffee
+++ client/admin/lib/views/stacks/definestackview.coffee
@@ -239,8 +239,8 @@

     { stackTemplate } = @getData()

-    { title }         = @inputTitle.getData()
-    templateContent   = @editorView.getValue()
+    { title }         = @stackTemplateView.inputTitle.getData()
+    templateContent   = @stackTemplateView.editorView.getValue()

     # TODO split following into their own helper methods
     # and call them in here ~ GG
@@ -271,9 +271,9 @@
     templateDetails = null

     # TODO Make this to support multiple credentials
-    credential      = @credentialStatus.credentialsData.first
-
-    if 'yaml' is @editorView.getOption 'contentType'
+    credential      = @stackTemplateView.credentialStatus.credentialsData.first
+
+    if 'yaml' is @stackTemplateView.editorView.getOption 'contentType'
       convertedDoc = yamlToJson templateContent

       if convertedDoc.err


fix avatar click route handler

fix avatar click route handler

--- client/app/CommonViews/avatarviews/avatarview.coffee
+++ client/app/CommonViews/avatarviews/avatarview.coffee
@@ -55,7 +55,7 @@
     flags = account.globalFlags?.join(" ") ? ""
     @$('cite').addClass flags

-    @$().attr href: profile.nickname
+    @setDomAttributes href: "/#{profile.nickname}"

   viewAppended:->
     super


ComputeEventListener: trigger revive after apply event is done as well

--- client/app/lib/providers/computeeventlistener.coffee
+++ client/app/lib/providers/computeeventlistener.coffee
@@ -108,6 +108,9 @@
     createSnapshot :
       public       : "MachineSnapshotted"
       private      : Snapshotting
+    apply          :
+      public       : "MachineBuilt"
+      private      : Running

   tick: (force) ->


Encode tab title for XSS vulnerabilities

--- client/ide/lib/views/tabview/idetabhandleview.coffee
+++ client/ide/lib/views/tabview/idetabhandleview.coffee
@@ -5,7 +5,7 @@
 KDCustomHTMLView    = kd.CustomHTMLView
 IDEEditorPane       = require 'ide/workspace/panes/ideeditorpane'
 FSHelper            = require 'app/util/fs/fshelper'
-
+Encoder             = require 'htmlencode'

 module.exports = class IDETabHandleView extends KDTabHandleView

@@ -36,7 +36,7 @@
     @titleText  = new KDView
       tagName  : 'b'
       cssClass : 'tab-handle-text'
-      partial  : title
+      partial  : Encoder.XSSEncode title

     view.addSubView @titleText

@@ -130,4 +130,3 @@
       @menu.once 'KDObjectWillBeDestroyed', =>
         @unsetClass 'menu-visible'
         @menu = null
-


Add coverage for position: 'after' in cursor's state specs

--- spec/text-editor-presenter-spec.coffee
+++ spec/text-editor-presenter-spec.coffee
@@ -1589,15 +1589,15 @@
           expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10}

           blockDecoration1 = addBlockDecorationBeforeScreenRow(0)
-          blockDecoration2 = addBlockDecorationBeforeScreenRow(1)
+          blockDecoration2 = addBlockDecorationAfterScreenRow(1)

           waitsForStateToUpdate presenter, ->
             presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30)
             presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10)

           runs ->
-            expect(stateForCursor(presenter, 0)).toEqual {top: 50, left: 2 * 10, width: 10, height: 10}
-            expect(stateForCursor(presenter, 1)).toEqual {top: 60, left: 4 * 10, width: 10, height: 10}
+            expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10 + 30, left: 2 * 10, width: 10, height: 10}
+            expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10 + 30 + 10, left: 4 * 10, width: 10, height: 10}
             expect(stateForCursor(presenter, 2)).toBeUndefined()
             expect(stateForCursor(presenter, 3)).toBeUndefined()
             expect(stateForCursor(presenter, 4)).toBeUndefined()


Test ignoreScrollPastEnd.

Test ignoreScrollPastEnd.

--- spec/text-editor-presenter-spec.coffee
+++ spec/text-editor-presenter-spec.coffee
@@ -635,16 +635,28 @@
           expectStateUpdate presenter, -> presenter.setExplicitHeight(500)
           expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500

-        it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
-          presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
-          expectStateUpdate presenter, -> presenter.setScrollTop(300)
-          expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
-
-          expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
-          expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
-
-          expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
-          expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+        describe "scrollPastEnd", ->
+          it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
+            presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
+            expectStateUpdate presenter, -> presenter.setScrollTop(300)
+            expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+            expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
+            expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
+
+            expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
+            expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+          it "doesn't add the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true but ignoreScrollPastEnd is true", ->
+            presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10, ignoreScrollPastEnd: true)
+            expectStateUpdate presenter, -> presenter.setScrollTop(300)
+            expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+            expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
+            expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+            expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
+            expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight

       describe ".scrollTop", ->
         it "tracks the value of ::scrollTop", ->


Added offset to scrollTo for emssages, preventing the topbar to be over part of the message

--- core/app/backbone/views/conversations/messages_view.coffee
+++ core/app/backbone/views/conversations/messages_view.coffee
@@ -16,7 +16,7 @@
     'scroll': 'scroll'

   scroll: ->
-    $.scrollTo @el, 800
+    $.scrollTo $(@el).offset().top - 72 , 800

 class MessageListView extends Backbone.Marionette.CollectionView
   tagName: 'ul'


Add stack to express errors posted to slack.

--- server_setup.coffee
+++ server_setup.coffee
@@ -87,7 +87,7 @@
         return

       res.status(err.status ? 500).send(error: "Something went wrong!")
-      message = "Express error: #{req.method} #{req.path}: #{err.message}"
+      message = "Express error: #{req.method} #{req.path}: #{err.message} \n #{err.stack}"
       log.error "#{message}, stack: #{err.stack}"
       if global.testing
         console.log "#{message}, stack: #{err.stack}"


fixes wrong icon behaviour

fixes wrong icon behaviour

--- app/assets/javascripts/consumo.js.coffee
+++ app/assets/javascripts/consumo.js.coffee
@@ -21,12 +21,12 @@
   return

 window.toggle_order_item = (element) ->
-  $(element).next().toggle null, ->
-    sub_element = $(element).children('.toggle_icon')
-    if $(sub_element).hasClass('glyphicon-chevron-up')
-      sub_element.removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down')
-    else
-      sub_element.removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up')
+  $(element).next().toggle()
+  sub_element = $(element).children('.toggle_icon')
+  if $(sub_element).hasClass('glyphicon-chevron-up')
+    sub_element.removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down')
+  else
+    sub_element.removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up')

 window.toggle_stats = ->
   $('#dashboard').children('.stats').toggle()


add a button

add a button

--- app/assets/javascripts/locales.js.coffee
+++ app/assets/javascripts/locales.js.coffee
@@ -1,20 +1,19 @@
 $(document).on 'ready page:load', ->
-  if document.URL.includes("tolk/locales")
-    alert "Double click anywhere to start autotranslating."
-    autotranslatingClicked = 0
-    $('#container').dblclick ->
-      autotranslatingClicked += 1
-      to_lang = document.URL.split('/').slice(-1).pop()
-      $translation_rows = $('table.translations tbody tr:not(:first-child)')
-      for $translation_row in $translation_rows
-        original = $translation_row.querySelector('td.original textarea#translations__original_text').innerText
-        $translationArea = $translation_row.querySelector('td.translation textarea')
-        if $translationArea.innerText == '' && autotranslatingClicked == 1
-          translate(original, to_lang, $translationArea)
+  $( "#container div.translations" ).prepend( "
<button onclick='startTranslating()'> Start autotranslating!</button>
" )
+  autotranslatingClicked = 0
+
+startTranslating = ->
+  autotranslatingClicked += 1
+  to_lang = document.URL.split('/').slice(-1).pop()
+  $translation_rows = $('table.translations tbody tr:not(:first-child)')
+  for $translation_row in $translation_rows
+    original = $translation_row.querySelector('td.original textarea#translations__original_text').innerText
+    $translationArea = $translation_row.querySelector('td.translation textarea')
+    if $translationArea.innerText == '' && autotranslatingClicked == 1
+      translate(original, to_lang, $translationArea)

 translate = (original, to, textarea) ->
   $.get '/translate', {original: original, to: to}, ((response) ->
-    alert 'udalo sie'
     if response.status == 200
       textarea.innerText = response.translated
   ), 'json'


Remove event handlers for enabling/disabling TaskLists

--- app/assets/javascripts/task_lists.coffee
+++ app/assets/javascripts/task_lists.coffee
@@ -180,10 +180,6 @@
   for container in this
     enableTaskList $(container)

-# (Re)enable a TaskList container.
-$(document).on 'tasklist:enable', '.js-task-list-container', (event) ->
-  enableTaskList $(this)
-
 # Disable TaskList item changes.
 disableTaskList = ($container) ->
   $container.
@@ -197,10 +193,6 @@
   for container in this
     disableTaskList $(container)

-# Disable a TaskList container.
-$(document).on 'tasklist:disable', '.js-task-list-container', (event) ->
-  disableTaskList $(this)
-
 # When the page is updated, enable new TaskList containers.
 $.pageUpdate ->
   $('.js-task-list-container').enableTaskList()


Overriding toaster auto-build

Overriding toaster auto-build

--- cli/src/commands/compiler.coffee
+++ cli/src/commands/compiler.coffee
@@ -37,6 +37,9 @@

 		# start watching/compiling coffeescript
 		@toaster = new Toaster @BASE_DIR, {w:1, d:1, config: config}, true
+		@toaster.before_build = =>
+			@compile()
+			false

 		# compiling everything at startup
 		@compile()


Convert HelptextPopoverView to Layout

Convert HelptextPopoverView to Layout

--- core/app/backbone/views/helptext_popover_view.coffee
+++ core/app/backbone/views/helptext_popover_view.coffee
@@ -1,15 +1,20 @@
-class window.HelptextPopoverView extends Backbone.Marionette.ItemView
+class window.HelptextPopoverView extends Backbone.Marionette.Layout
   template: 'generic/helptext_popover'
   className: 'help-text-container'

   ui:
     arrow: '.js-arrow'

+  regions:
+    contentRegion: '.js-help-text-content'
+
   initialize: ->
     @on 'position', @handleposition, @

   onRender: ->
     @$el.addClass @options.side
+
+    @contentRegion.show @options.view if @options.view

   handleposition: (position)->
     top = 0


a bit more work, that's it for now.

a bit more work, that's it for now.

--- src/nacl/eddsa.iced
+++ src/nacl/eddsa.iced
@@ -1,13 +1,17 @@
-
 twnacl = require 'tweetnacl'
+{SRF} = require '../rand'

 #=============================================

 class Pub

+  constructor : (@key) ->
+
 #=============================================

 class Priv
+
+  constructor : (@key) ->

 #=============================================

@@ -16,7 +20,12 @@
   constructor : ({@pub, @priv}) ->

   @generate : (params, cb) ->
+    await SRF.random_bytes twnacl.sign.seedLength, defer seed
+    {secretKey, publicKey} = twnacl.sign.keyPair.fromSeed(seed)
+    pub = new Pub publicKey
+    priv = new Priv secretKey
+    cb null, new Pair {pub, priv}

 #=============================================

-exports.EdDSA = exports.Pair = PAir
+exports.EdDSA = exports.Pair = Pair


Fixed Horizontal directive

Fixed Horizontal directive

--- app/scripts/directives/horizontal.coffee
+++ app/scripts/directives/horizontal.coffee
@@ -48,14 +48,16 @@
       setElementTotalValue = (totalValue) ->
         totalValueElement.html(totalValue)

-      setElementTotalValue(scope.total)
+      if totalValueElement
+        setElementTotalValue(scope.total)

       scope.$watch('value', (newValue, oldValue) ->
         setElementValue(newValue)
       )

       scope.$watch('total', (newValue, oldValue) ->
-        setElementTotalValue(newValue)
+        if totalValueElement
+          setElementTotalValue(newValue)
       )

       scope.$watch('percentage', (newValue, oldValue) ->


workarounds for firefox

workarounds for firefox

--- src/renderer.coffee
+++ src/renderer.coffee
@@ -5,8 +5,8 @@
   return fn.call(this) if @iframe.contentWindow.document.readyState == 'complete'
   if @iframe.contentWindow.onload
     @iframe.contentWindow.onload = _.wrap(@iframe.contentWindow.onload, (wrapper) =>
+      wrapper.call(this)
       fn.call(this)
-      wrapper.call(this)
     )
   else
     @iframe.contentWindow.onload = fn
@@ -127,8 +127,8 @@
     )
     styles = _.extend(Scribe.Renderer.DEFAULT_STYLES, styles)
     this.addStyles(styles)
-    runWhenLoaded.call(this, ->
-      doc.body.appendChild(@root)
+    runWhenLoaded.call(this, =>
+      @iframe.contentWindow.document.body.appendChild(@root) # Firefox does not like doc.body
     )

 module.exports = Scribe


Updated the commit message for the puppetmaster module

--- src/scripts/puppetmaster.coffee
+++ src/scripts/puppetmaster.coffee
@@ -1,5 +1,5 @@
 # Description:
-#   Display current puppetmaster node status
+#   Queries the puppetmaster radiator page for the latest node status
 #
 #
 # Commands:


Handle TBD date and time

Handle TBD date and time

If the next game does not have a start time yet (e.g. playoffs), the TBD flag is used. Prior to this PR, the incorrect time would be displayed.

--- src/hubot-hockey.coffee
+++ src/hubot-hockey.coffee
@@ -99,11 +99,15 @@
       robot.logger.debug apiresponse
       if apiresponse['events'].length > 0
         nextEvent = apiresponse['events'][0]
-        datetime = moment(nextEvent.datetime_local).format('LLL')
+        dateTime = moment(nextEvent.datetime_local).format('LLL')
+        if nextEvent.date_tbd
+          dateTime = 'TBD'
+        if nextEvent.time_tbd
+          dateTime = moment(nextEvent.datetime_local).format('LL')
         robot.logger.debug nextEvent
-        msg.send "Next Game: #{nextEvent.title} - #{datetime} (local time)"
+        msg.send "Next Game: #{nextEvent.title} - #{dateTime}"
     .catch (error) ->
-      robot.logger.error err
+      robot.logger.error error
       msg.send '(An error ocurred retrieving next game)'

   getSCSData = (team, msg) ->


updating the diplomat list

updating the diplomat list

--- app/views/contribute/DiplomatView.coffee
+++ app/views/contribute/DiplomatView.coffee
@@ -39,54 +39,54 @@
     'en-US': []        # English (US), English (US)
     'en-GB': []        # English (UK), English (UK)
     'en-AU': []        # English (AU), English (AU)
-    ru: ['fess89', 'ser-storchak', 'Mr A', 'a1ip']             # русский язык, Russian
-    de: ['Dirk', 'faabsen', 'HiroP0', 'Anon', 'bkimminich']             # Deutsch, German
+    ru: ['fess89', 'ser-storchak', 'Mr A', 'a1ip', 'iulianR', 'EagleTA', 'kisik21', 'Shpionus', 'kerradus', 'ImmortalJoker']             # русский язык, Russian
+    de: ['Dirk', 'faabsen', 'HiroP0', 'Anon', 'bkimminich', 'bahuma20', 'djsmith85', 'greyhusky']             # Deutsch, German
     'de-DE': []        # Deutsch (Deutschland), German (Germany)
     'de-AT': []        # Deutsch (Österreich), German (Austria)
     'de-CH': []        # Deutsch (Schweiz), German (Switzerland)
     es: []             # español, Spanish
-    'es-419': ['Jesús Ruppel', 'Matthew Burt', 'Mariano Luzza']       # español (América Latina), Spanish (Latin America)
-    'es-ES': ['Matthew Burt', 'DanielRodriguezRivero', 'Anon', 'Pouyio']        # español (ES), Spanish (Spain)
-    zh: ['Adam23', 'spacepope', 'yangxuan8282', 'Cheng Zheng']             # 中文, Chinese
+    'es-419': ['Jesús Ruppel', 'Matthew Burt', 'Mariano Luzza', '2xG', ]       # español (América Latina), Spanish (Latin America)
+    'es-ES': ['Matthew Burt', 'DanielRodriguezRivero', 'Anon', 'Pouyio', '3rr3s3v3n', 'OviiiOne', 'Vindurrin']        # español (ES), Spanish (Spain)
+    zh: ['Adam23', 'spacepope', 'yangxuan8282', 'Cheng Zheng', 'yfdyh000', 'julycoolwind', 'Vic020', 'onion7878', 'BonnieBBS', '1c7', 'benojan', 'ZephyrSails']             # 中文, Chinese
     'zh-HANS': []      # 简体中文, Chinese (Simplified)
     'zh-HANT': []      # 繁体中文, Chinese (Traditional)
     'zh-WUU-HANS': []  # 吴语, Wuu (Simplified)
     'zh-WUU-HANT': []  # 吳語, Wuu (Traditional)
-    fr: ['Xeonarno', 'Elfisen', 'Armaldio', 'MartinDelille', 'pstweb', 'veritable', 'jaybi', 'xavismeh', 'Anon', 'Feugy']             # français, French
+    fr: ['Xeonarno', 'Elfisen', 'Armaldio', 'MartinDelille', 'pstweb', 'veritable', 'jaybi', 'xavismeh', 'Anon', 'Feugy', 'dc55028', 'ChrisLightman', 'Oaugereau']             # français, French
     ja: ['g1itch', 'kengos', 'treby']             # 日本語, Japanese
-    ar: []             # العربية, Arabic
+    ar: ['ahmed80dz', '5y']             # العربية, Arabic
     pt: []             # português, Portuguese
-    'pt-BR': ['Gutenberg Barros', 'Kieizroe', 'Matthew Burt', 'brunoporto', 'cassiocardoso']        # português do Brasil, Portuguese (Brazil)
-    'pt-PT': ['Matthew Burt', 'ReiDuKuduro', 'Imperadeiro98']        # Português (Portugal), Portuguese (Portugal)
-    pl: ['Anon', 'Kacper Ciepielewski']             # język polski, Polish
-    it: ['flauta']             # italiano, Italian
-    tr: ['Nazım Gediz Aydındoğmuş', 'cobaimelan', 'wakeup']             # Türkçe, Turkish
+    'pt-BR': ['Gutenberg Barros', 'Kieizroe', 'Matthew Burt', 'brunoporto', 'cassiocardoso', 'Bia41']        # português do Brasil, Portuguese (Brazil)
+    'pt-PT': ['Matthew Burt', 'ReiDuKuduro', 'Imperadeiro98', 'batista', 'ProgramadorLucas', 'gutierri']        # Português (Portugal), Portuguese (Portugal)
+    pl: ['Anon', 'Kacper Ciepielewski', 'TigroTigro', 'kvasnyk']             # język polski, Polish
+    it: ['flauta', 'AlessioPaternoster']             # italiano, Italian
+    tr: ['Nazım Gediz Aydındoğmuş', 'cobaimelan', 'wakeup', 'gediz', 'ilisyus']             # Türkçe, Turkish
     nl: ['Glen De Cauwsemaecker', 'Guido Zuidhof', 'Ruben Vereecken', 'Jasper D\'haene']             # Nederlands, Dutch
     'nl-BE': []        # Nederlands (België), Dutch (Belgium)
     'nl-NL': []        # Nederlands (Nederland), Dutch (Netherlands)
     fa: ['Reza Habibi (Rehb)']             # فارسی, Persian
     cs: ['vanous']             # čeština, Czech
-    sv: []             # Svenska, Swedish
-    id: []             # Bahasa Indonesia, Indonesian
+    sv: ['iamhj']             # Svenska, Swedish
+    id: ['mlewisno-oberlin']             # Bahasa Indonesia, Indonesian
     el: ['Stergios']             # ελληνικά, Greek
     ro: []             # limba română, Romanian
     vi: ['An Nguyen Hoang Thien']             # Tiếng Việt, Vietnamese
-    hu: ['ferpeter', 'csuvsaregal', 'atlantisguru', 'Anon']             # magyar, Hungarian
+    hu: ['ferpeter', 'csuvsaregal', 'atlantisguru', 'Anon', 'kinez', 'bbeasmile']             # magyar, Hungarian
     th: ['Kamolchanok Jittrepit']             # ไทย, Thai
-    da: ['Einar Rasmussen', 'sorsjen', 'Randi Hillerøe', 'Anon']             # dansk, Danish
-    ko: []             # 한국어, Korean
+    da: ['Einar Rasmussen', 'sorsjen', 'Randi Hillerøe', 'Anon', 'Silwing', 'Rahazan', 'marc-portier']             # dansk, Danish
+    ko: ['Melondonut']             # 한국어, Korean
     sk: ['Anon']             # slovenčina, Slovak
     sl: []             # slovenščina, Slovene
     fi: []             # suomi, Finnish
     bg: []             # български език, Bulgarian
-    no: ['bardeh']             # Norsk, Norwegian
+    no: ['bardeh', 'torehaug']             # Norsk, Norwegian
     nn: []             # Norwegian (Nynorsk), Norwegian Nynorsk
     nb: []             # Norsk Bokmål, Norwegian (Bokmål)
-    he: []             # עברית, Hebrew
+    he: ['OverProgram', 'monetita']             # עברית, Hebrew
     lt: []             # lietuvių kalba, Lithuanian
     sr: []             # српски, Serbian
-    uk: ['fess89']             # українська мова, Ukrainian
+    uk: ['fess89', 'ImmortalJoker', 'gorodsb', 'endrilian', 'OlenaGapak']             # українська мова, Ukrainian
     hi: []             # मानक हिन्दी, Hindi
     ur: []             # اُردُو, Urdu
     ms: []             # Bahasa Melayu, Bahasa Malaysia
-    ca: []             # Català, Catalan
+    ca: ['ArniMcFrag']             # Català, Catalan


IE support

IE support

--- src/jquery.caret.coffee
+++ src/jquery.caret.coffee
@@ -162,7 +162,7 @@
       {left: x, top: y, height: h}

     getIEOffset: (pos) ->
-      range = @domInputor.createRange()
+      range = @domInputor.createTextRange()
       range.move('character', pos) if pos
       x = range.boundingLeft + $inputor.scrollLeft()
       y = range.boundingTop + $(window).scrollTop() + $inputor.scrollTop()


Fix: escape regex chars

Fix: escape regex chars

--- assets/javascripts/helpers/url.js.coffee
+++ assets/javascripts/helpers/url.js.coffee
@@ -60,7 +60,7 @@
       for i in TentStatus.Helpers.extractUrlsWithIndices(text)
         entity = i.url
         _is_carrot_mention = text.substr(i.indices[0]-1, 1) == '^'
-        _is_tent_subdomain = entity.match(new RegExp("\\w+\.#{TentStatus.config.tent_host_domain}"))
+        _is_tent_subdomain = entity.match(new RegExp("\\w+\.#{TentStatus.Helpers.escapeRegExChars(TentStatus.config.tent_host_domain)}"))
         continue unless _is_carrot_mention or _is_tent_subdomain

         original_text = entity


Extract schedule update check helper

Extract schedule update check helper

--- src/browser/auto-update-manager.coffee
+++ src/browser/auto-update-manager.coffee
@@ -53,11 +53,7 @@
       @emitUpdateAvailableEvent(@getWindows()...)

     # Only released versions should check for updates.
-    unless /\w{7}/.test(@version)
-      checkForUpdates = => @check(hidePopups: true)
-      thirtyMinutes = 1000 * 60 * 30
-      setInterval(checkForUpdates, thirtyMinutes)
-      checkForUpdates()
+    @scheduleUpdateCheck() unless /\w{7}/.test(@version)

     switch process.platform
       when 'win32'
@@ -78,6 +74,12 @@

   getState: ->
     @state
+
+  scheduleUpdateCheck: ->
+    checkForUpdates = => @check(hidePopups: true)
+    thirtyMinutes = 1000 * 60 * 30
+    setInterval(checkForUpdates, thirtyMinutes)
+    checkForUpdates()

   check: ({hidePopups}={}) ->
     unless hidePopups


Chat Editor: Prevent automatic scroll to bottom on remount

Also add a scroll to bottom button.

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -40,7 +40,7 @@
 # React libraries
 {React, ReactDOM, rclass, rtypes, Actions, Store, Redux}  = require('./smc-react')
 {Icon, Loading, TimeAgo} = require('./r_misc')
-{Button, Col, Grid, Input, ListGroup, ListGroupItem, Panel, Row} = require('react-bootstrap')
+{Button, Col, Grid, Input, ListGroup, ListGroupItem, Panel, Row, ButtonToolbar} = require('react-bootstrap')

 {User} = require('./users')

@@ -74,6 +74,9 @@

     set_input: (input) =>
         @setState(input:input)
+
+    save_position: (position) =>
+        @setState(position:position)

 # boilerplate setting up actions, stores, sync'd file, etc.
 syncdbs = {}
@@ -216,6 +219,7 @@
         "#{name}" :
             messages : rtypes.immutable
             input    : rtypes.string
+            position : rtypes.string
         users :
             user_map : rtypes.immutable
         account :
@@ -295,19 +299,28 @@
             return
         node = ReactDOM.findDOMNode(@refs.log_container)
         node.scrollTop = node.scrollHeight
+        @props.redux.getActions(@props.name).save_position(node.scrollTop)
         @_ignore_next_scroll = true
         @_scrolled = false
+
+    scroll_to_position: ->
+        if not @refs.log_container?
+            return
+        @_scrolled = true
+        node = ReactDOM.findDOMNode(@refs.log_container)
+        node.scrollTop = @props.position

     on_scroll: (e) ->
         if @_ignore_next_scroll
             @_ignore_next_scroll = false
             return
         @_scrolled = true
+        node = ReactDOM.findDOMNode(@refs.log_container)
+        @props.redux.getActions(@props.name).save_position(node.scrollTop)
         e.preventDefault()

     componentDidMount: ->
-        if not @_scrolled
-            @scroll_to_bottom()
+        @scroll_to_position()

     componentDidUpdate: ->
         if not @_scrolled
@@ -342,9 +355,14 @@
                     </div>
                 </Col>
                 <Col xs={4}>
-                    <Button onClick={@show_timetravel} bsStyle='info' style={float:'right'}>
-                        <Icon name='history'/> TimeTravel
-                    </Button>
+                    <ButtonToolbar>
+                        <Button onClick={@show_timetravel} bsStyle='info' style={float:'right'}>
+                            <Icon name='history'/> TimeTravel
+                        </Button>
+                        <Button onClick={@scroll_to_bottom} bsStyle='info' style={float:'right'}>
+                            <Icon name='arrow-down'/> Scroll to Bottom
+                        </Button>
+                    </ButtonToolbar>
                 </Col>
             </Row>
             <Row>


move some +New buttons around...

move some +New buttons around...

--- src/smc-webapp/project_new.cjsx
+++ src/smc-webapp/project_new.cjsx
@@ -198,17 +198,21 @@
                         tip='Create a professional quality technical paper that contains sophisticated mathematical formulas.'>
                         <NewFileButton icon='cc-icon-tex-file' name='LaTeX document' on_click={@props.create_file} ext='tex' />
                     </Tip> if available.latex}
+                    <Tip title='Linux terminal'  icon='terminal'
+                        tip="Create a command line Linux terminal.  CoCalc includes a full Linux environment.  Run command line software, vim, emacs and more.">
+                        <NewFileButton icon='terminal' name='Linux terminal' on_click={@props.create_file} ext='term' />
+                    </Tip>
                 </Col>
             </Row>
             <Row style={row_style}>
                 <Col sm={12}>
+                    <Tip title='Create a chatroom'  placement='bottom'  icon='comment'
+                        tip='Create a chatroom for chatting with other collaborators on this project.'>
+                        <NewFileButton icon='comment' name='Create a chatroom' on_click={@props.create_file} ext='sage-chat' />
+                    </Tip>
                     <Tip title='Manage a course'  placement='bottom'  icon='graduation-cap'
                         tip='If you are a teacher, click here to create a new course.  This is a file that you can add students and assignments to, and use to automatically create projects for everybody, send assignments to students, collect them, grade them, etc.'>
                         <NewFileButton icon='graduation-cap' name='Manage a course' on_click={@props.create_file} ext='course' />
-                    </Tip>
-                    <Tip title='Create a chatroom'  placement='bottom'  icon='comment'
-                        tip='Create a chatroom for chatting with other collaborators on this project.'>
-                        <NewFileButton icon='comment' name='Create a chatroom' on_click={@props.create_file} ext='sage-chat' />
                     </Tip>
                 </Col>
             </Row>
@@ -234,29 +238,25 @@
             </Row>
             <Row style={row_style}>
                 <Col sm={12}>
-                    <Tip title='Linux Terminal'  icon='terminal'
-                        tip="Create a command line terminal.  CoCalc includes a full interactive Linux command line console and color xterm.  Run command line software, vim, emacs and more.">
-                        <NewFileButton icon='terminal' name='Linux Terminal' on_click={@props.create_file} ext='term' />
-                    </Tip>
-                    {<Tip title='X11 Desktop'   icon='window-restore'
-                        tip='Create an X11 desktop for running graphical applications.'>
-                        <NewFileButton icon='window-restore' name='X11 Desktop' on_click={@props.create_file} ext='x11' />
+                    {<Tip title='X11 desktop'   icon='window-restore'
+                        tip='Create an X11 desktop for running graphical applications.  CoCalc lets you collaboratively run any graphical Linux application in your browser.'>
+                        <NewFileButton icon='window-restore' name='X11 desktop' on_click={@props.create_file} ext='x11' />
                     </Tip> if available.x11}
                    {@props.children}
                 </Col>
             </Row>
             <Row style={row_style}>
                 <Col sm={12}>
-                    {<Tip title={'Jupyter Server'}  icon={'cc-icon-ipynb'}
-                        tip={"Start a Jupyter notebook server..."}>
-                        <NewFileButton  name={'Jupyter Classic...'}
+                    {<Tip title={'Jupyter server'}  icon={'cc-icon-ipynb'}
+                        tip={"Start a Jupyter classic notebook server running from your project, which only project collaborators can access."}>
+                        <NewFileButton  name={'Jupyter classic server...'}
                         icon={'cc-icon-ipynb'}
                         on_click={=>@setState(show_jupyter_server_panel:true)}
                         disabled={@state.show_jupyter_server_panel}/>
                     </Tip> if available.jupyter_notebook}
-                    {<Tip title={'JupyterLab Server'} icon={'cc-icon-ipynb'}
-                        tip={'Start a JupyterLab server...'}>
-                        <NewFileButton name={'JupyterLab...'}
+                    {<Tip title={'JupyterLab server'} icon={'cc-icon-ipynb'}
+                        tip={'Start a JupyterLab server running from your project, which only project collaborators can access.'}>
+                        <NewFileButton name={'JupyterLab server...'}
                         icon={'cc-icon-ipynb'}
                         on_click={=>@setState(show_jupyterlab_server_panel:true)}
                         disabled={@state.show_jupyterlab_server_panel}/>


Allow mapping configuration without 'specialMapping'

--- src/coffee/import/productimport.coffee
+++ src/coffee/import/productimport.coffee
@@ -73,8 +73,8 @@
         manufacturers = @_buildManufacturers(manufacturersXML, @productType, @mappings.product) if manufacturersXML
         api.updateProductType(@rest, manufacturers) if manufacturers
     .then (productTypeUpdateResult) =>
-      @logger.info '[Categories] Categories XML import started...'
-      @logger.info "[Categories] Fetched categories count before create: '#{_.size @fetchedCategories}'"
+      @logger.info '[Categories] Categories XML import started...' if @categoriesXML
+      @logger.info "[Categories] Fetched categories count before create: '#{_.size @fetchedCategories}'" if @categoriesXML
       @categories = @_buildCategories(@categoriesXML) if @categoriesXML
       @categoryCreates = @_buildCategoryCreates(@categories, @fetchedCategories) if @categories
       utils.batch(_.map(@categoryCreates, (c) => api.createCategory(@rest, c))) if @categoryCreates
@@ -446,8 +446,8 @@
       if _.has(mappings, key)
         mapping = mappings[key]
         url = value[0]
-        if not _s.startsWith(url, 'http')
-          url = "#{mapping.specialMapping.baseURL}#{url}" if mapping.specialMapping?.baseURL
+        if not _s.startsWith(url, 'http') and mapping.specialMapping?.baseURL
+          url = "#{mapping.specialMapping.baseURL}#{url}"
         image =
           url: url
           dimensions:
@@ -478,9 +478,9 @@
           price.value =
             centAmount: @_getPriceAmount(value[0], mapping)
             currencyCode: currency
-          country = mapping.specialMapping.country
-          customerGroup = mapping.specialMapping.customerGroup
-          channel = mapping.specialMapping.channel
+          country = mapping.specialMapping?.country
+          customerGroup = mapping.specialMapping?.customerGroup
+          channel = mapping.specialMapping?.channel
           if country
             price.country = mapping.specialMapping.country
           if customerGroup


Simplify error log.

Simplify error log.

--- src/coffee/import/productimport.coffee
+++ src/coffee/import/productimport.coffee
@@ -71,9 +71,8 @@
       else
         @_batch _.tail(productList, numberOfParallelRequest), callback, numberOfParallelRequest
     .fail (error) ->
-      @logger.error 'Error on create new product batch processing.'
-      if error.stack then @logger.error "Error stack: #{error.stack}"
-      @logger.error error
+      @logger.error "Error on create new product batch processing; #{error}"
+      @logger.error "Error stack: #{error.stack}" if error.stack
       callback false

   _createProduct: (product) =>