refactored parseDate method

refactored parseDate method

--- app/coffee/drm-timestamps/drm-timestamps.coffee
+++ app/coffee/drm-timestamps/drm-timestamps.coffee
@@ -55,66 +55,76 @@

         _getDaysInMonth = (month, year) ->
             # returns the number of days in a month
-            month = month + 1
-            new Date(year, month, 0).getDate()
+            _month = month + 1
+            new Date(year, _month, 0).getDate()

-        if item.search(/^(yesterday|today|tomorrow)/) isnt -1
-            fullDate = item.match /^(yesterday|today|tomorrow)/i
-            fullDate = fullDate[0]
-            lastMonth = if @today.month is 0 then 11 else @today.month - 1
-            nextMonth = if @today.month is 11 then 0 else @today.month + 1
+        _parseDate = (item) =>
+            date = {}
+            if item.search(/^(yesterday|today|tomorrow)/) isnt -1
+                _fullDate = item.match /^(yesterday|today|tomorrow)/i
+                _fullDate = _fullDate[0]
+                _lastMonth = if @today.month is 0 then 11 else @today.month - 1
+                _nextMonth = if @today.month is 11 then 0 else @today.month + 1

-            lastDateInMonth = _getDaysInMonth @today.month, @today.year
-            lastDateInLastMonth = _getDaysInMonth lastMonth, @today.year
+                _lastDateInMonth = _getDaysInMonth @today.month, @today.year
+                _lastDateInLastMonth = _getDaysInMonth _lastMonth, @today.year

-            if fullDate is 'yesterday'
-                date = if @today.date is 1 then lastDateInLastMonth else @today.date - 1
-                month = if @today.date is 1 then lastMonth else @today.month
-                year = if (@today.month is 0) and (@today.date is 1) then @today.year - 1 else @today.year
-            else if fullDate is 'today'
-                date = @today.date
-                month = @today.month
-                year = @today.year
-            else if fullDate is 'tomorrow'
-                date = if @today.date is lastDateInMonth then 1 else @today.date + 1
-                month = if @today.date is lastDateInMonth then nextMonth else @today.month
-                year = if (@today.month is 11) and (@today.date is lastDateInMonth) then @today.year + 1 else @today.year
+                if _fullDate is 'yesterday'
+                    date.date = if @today.date is 1 then _lastDateInLastMonth else @today.date - 1
+                    date.month = if @today.date is 1 then _lastMonth else @today.month
+                    date.year = if (@today.month is 0) and (@today.date is 1) then @today.year - 1 else @today.year
+                else if _fullDate is 'today'
+                    date.date = @today.date
+                    date.month = @today.month
+                    date.year = @today.year
+                else if _fullDate is 'tomorrow'
+                    date.date = if @today.date is _lastDateInMonth then 1 else @today.date + 1
+                    date.month = if @today.date is _lastDateInMonth then _nextMonth else @today.month
+                    date.year = if (@today.month is 11) and (@today.date is _lastDateInMonth) then @today.year + 1 else @today.year

-        else
-            fullDate = item.match /((?:[0]?[1-9]|[1][012]|[1-9])[-\/.](?:[0]?[1-9]|[12][0-9]|[3][01])[-\/.][0-9]{4})/
-            fullDate = fullDate[0]
+            else
+                _fullDate = item.match /((?:[0]?[1-9]|[1][012]|[1-9])[-\/.](?:[0]?[1-9]|[12][0-9]|[3][01])[-\/.][0-9]{4})/
+                _fullDate = _fullDate[0]

-            month = fullDate.match /^([0]?[1-9]|[1][012]|[1-9])/
-            month = parseInt(month[0], 10)
+                date.month = _fullDate.match /^([0]?[1-9]|[1][012]|[1-9])/
+                date.month = parseInt(date.month[0], 10)

-            date = fullDate.match /[\.\/\-]([012]?[0-9])[\.\/\-]/
-            date = parseInt(date[1], 10)
+                date.date = _fullDate.match /[\.\/\-]([012]?[0-9])[\.\/\-]/
+                date.date = parseInt(date.date[1], 10)

-            year = fullDate.match /([0-9]{4})/
-            year = parseInt(year[0], 10)
+                date.year = _fullDate.match /([0-9]{4})/
+                date.year = parseInt(date.year[0], 10)

-        time = item.match /((?:[12][012]:|[0]?[0-9]:)[012345][0-9](?:\:[012345][0-9])?(?:am|pm)?)/i
-
-        if time?
-            time = time[0]
-            ampm = time.match /(am|pm)/i
-            ampm = ampm[1]
+            date

-            hour = time.match /^(?:([12][012]):|([0]?[0-9]):)/
-            hour = if hour[1]? then parseInt(hour[1], 10) else parseInt(hour[2], 10)
+        _parseTime = (item) =>
+            time = {}
+            _fullTime = item.match /((?:[12][012]:|[0]?[0-9]:)[012345][0-9](?:\:[012345][0-9])?(?:am|pm)?)/i
+
+            if _fullTime?
+                _fullTime = _fullTime[0]
+                _ampm = _fullTime.match /(am|pm)/i
+                _ampm = _ampm[1]

-            hour = if ampm is 'pm' then hour + 12 else hour
-
-            minute = time.match /\:([012345][0-9])/
-            minute = parseInt(minute[1], 10)
-
-            second = time.match /\:(?:[012345][0-9])\:([012345][0-9])/
-            second = if second? then parseInt(second[1], 10) else @today.second
-        else
-            hour = @today.hour
-            minute = @today.minute
-            second = @today.second
+                time.hour = _fullTime.match /^(?:([12][012]):|([0]?[0-9]):)/
+                time.hour = if time.hour[1]? then parseInt(time.hour[1], 10) else parseInt(time.hour[2], 10)

-        return new Date year, month, date, hour, minute, second
+                time.hour = if _ampm is 'pm' then time.hour + 12 else time.hour
+
+                time.minute = _fullTime.match /\:([012345][0-9])/
+                time.minute = parseInt(time.minute[1], 10)
+
+                time.second = _fullTime.match /\:(?:[012345][0-9])\:([012345][0-9])/
+                time.second = if time.second? then parseInt(time.second[1], 10) else @today.second
+            else
+                time.hour = @today.hour
+                time.minute = @today.minute
+                time.second = @today.second
+
+            time
+
+        date = _parseDate item
+        time = _parseTime item
+        return new Date date.year, date.month, date.date, time.hour, time.minute, time.second

 new DrmTimeStamps()


Fixed features save button

Fixed features save button

--- app/coffee/modules/admin/project-profile.coffee
+++ app/coffee/modules/admin/project-profile.coffee
@@ -137,7 +137,7 @@
             event.preventDefault()
             submit()

-        $el.on "click", "form a.button-green", (event) ->
+        $el.on "click", ".admin-functionalities a.button-green", (event) ->
             event.preventDefault()
             submit()


charts: return immediately if not supported by browser

IE8 was raising errors too soon with D3 charts

--- app/assets/javascripts/lib/models/chart.coffee
+++ app/assets/javascripts/lib/models/chart.coffee
@@ -13,6 +13,7 @@
     @render()

   render : =>
+    return false unless @supported_by_current_browser()
     type = @get('type')
     view_class = switch type
       when 'bezier'                 then BezierChartView


Fix for copy paste formatted text

Fix for copy paste formatted text

--- app/assets/javascripts/markdown_area.js.coffee
+++ app/assets/javascripts/markdown_area.js.coffee
@@ -20,7 +20,6 @@
   $(".div-dropzone-hover").append iconPicture
   $(".div-dropzone").append divSpinner
   $(".div-dropzone-spinner").append iconSpinner
-

   dropzone = $(".div-dropzone").dropzone(
     url: project_image_path_upload
@@ -77,6 +76,107 @@
       return
   )

+  child = $(dropzone[0]).children("textarea")
+
+  formatLink = (str) ->
+    "![" + str.alt + "](" + str.url + ")"
+
+  handlePaste = (e) ->
+    e.preventDefault()
+    my_event = e.originalEvent
+
+    if my_event.clipboardData and my_event.clipboardData.items
+      processItem(my_event)
+
+  processItem = (e) ->
+    image = isImage(e)
+    if image
+      filename = getFilename(e) or "image.png"
+      text = "{{" + filename + "}}"
+      pasteText(text)
+      uploadFile image.getAsFile(), filename
+
+    else
+      text = e.clipboardData.getData("text/plain")
+      pasteText(text)
+
+  isImage = (data) ->
+    i = 0
+    while i < data.clipboardData.items.length
+      item = data.clipboardData.items[i]
+      if item.type.indexOf("image") isnt -1
+        return item
+      i++
+    return false
+
+  pasteText = (text) ->
+    caretStart = $(child)[0].selectionStart
+    caretEnd = $(child)[0].selectionEnd
+    textEnd = $(child).val().length
+
+    beforeSelection = $(child).val().substring 0, caretStart
+    afterSelection = $(child).val().substring caretEnd, textEnd
+    $(child).val beforeSelection + text + afterSelection
+    $(".markdown-area").trigger "input"
+
+  getFilename = (e) ->
+    if window.clipboardData and window.clipboardData.getData
+      value = window.clipboardData.getData("Text")
+    else if e.clipboardData and e.clipboardData.getData
+      value = e.clipboardData.getData("text/plain")
+
+    value = value.split("\r")
+    value.first()
+
+  uploadFile = (item, filename) ->
+    formData = new FormData()
+    formData.append "markdown_img", item, filename
+    $.ajax
+      url: project_image_path_upload
+      type: "POST"
+      data: formData
+      dataType: "json"
+      processData: false
+      contentType: false
+      headers:
+        "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+
+      beforeSend: ->
+        showSpinner()
+        closeAlertMessage()
+
+      success: (e, textStatus, response) ->
+        insertToTextArea(filename, formatLink(response.responseJSON.link))
+
+      error: (response) ->
+        showError(response.responseJSON.message)
+
+      complete: ->
+        closeSpinner()
+
+  insertToTextArea = (filename, url) ->
+    $(child).val (index, val) ->
+      val.replace("{{" + filename + "}}", url + "\n")
+
+  appendToTextArea = (url) ->
+    $(child).val (index, val) ->
+      val + url + "\n"
+
+  showSpinner = (e) ->
+    $(".div-dropzone-spinner").css "opacity", 0.7
+
+  closeSpinner = ->
+    $(".div-dropzone-spinner").css "opacity", 0
+
+  showError = (message) ->
+    checkIfMsgExists = $(".error-alert").children().length
+    if checkIfMsgExists is 0
+      $(".error-alert").append divAlert
+      $(".div-dropzone-alert").append btnAlert + message
+
+  closeAlertMessage = ->
+    $(".div-dropzone-alert").alert "close"
+
   $(".markdown-selector").click (e) ->
     e.preventDefault()
     $(".div-dropzone").click()


Use evalAsync instead of timeout

Use evalAsync instead of timeout

--- services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee
+++ services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee
@@ -23,10 +23,10 @@

 				scope.$watch "reviewPanel.entries", (value) ->
 					return if !value?
-					$timeout () ->
+					scope.$evalAsync () ->
 						layout()

 				scope.$on "review-panel:layout", () ->
-					$timeout () ->
+					scope.$evalAsync () ->
 						layout()
 		}


remove user id from config.cson

remove user id from config.cson

--- config.cson
+++ config.cson
@@ -1,6 +1,4 @@
 "*":
-  "exception-reporting":
-    userId: "11b68268-7d8f-3152-bfc7-df1340ecaac7"
   welcome:
     showOnStartup: false
   editor:


fixup for chrome after safari fixes

fixup for chrome after safari fixes

--- remcoaudio.coffee
+++ remcoaudio.coffee
@@ -19,6 +19,7 @@
   vco = audio.createOscillator()
   patch.oscillator vco
   vco.type = vco.SQUARE
+  vco.frequency.value = 0
   vco.start 0

   # this controls the modulation routing from the SLF to the vco.


removed debug code

removed debug code

--- js-library/app/js/views/fact.coffee
+++ js-library/app/js/views/fact.coffee
@@ -75,7 +75,6 @@
     setTimeout (=> @highlighter.dehighlight()), duration

   destroy: ->
-    console.info "DESTROY!!! MUHAHAHAHAHA"
     for el in @elements
       $el = $(el)
       unless $el.is('.fl-first')


add docs; restructured file; add empty matrix constructor; add transpose in place

--- matrixSpec.coffee
+++ matrixSpec.coffee
@@ -45,11 +45,27 @@
       expect(B.height).toBe 2
       expect(B.width).toBe 3

+    it 'should be able to create a new empty square matrix', ->
+
+      B = M 3
+      (expect B.height).toBe 3
+      (expect B.width).toBe 3
+      u = undefined
+      (expect B.array).toEqual [u,u,u,u,u,u,u,u,u]
+
+    it 'should be able to create a new empty rectangular matrix', ->
+
+      B = M 3, 2
+      (expect B.height).toBe 2
+      (expect B.width).toBe 3
+      u = undefined
+      (expect B.array).toEqual [u,u,u,u,u,u]
+
     it 'should throw on wrong width', ->

       expect(-> M [ 1, 2, 3 ], 2).toThrow()

-    it 'should throw on on param call with not square array size', ->
+    it 'should throw on param call with not square array size', ->

       expect(-> M [ 1, 2, 3 ]).toThrow()

@@ -233,6 +249,14 @@
                             5, 6 ], 2 # expected result?
       expect(C).not.toBe B # not in place?
       expect(B).toEqual A # source unchanged ?
+
+    it 'transposes a matrix in place', ->
+      C = A.transp A # tested function
+      expect(C).toEqual M [ 1, 2
+                            3, 4
+                            5, 6 ], 2 # expected result?
+      expect(C).toBe A # in place?
+      expect(C.array).toBe A.array

   describe 'mult() function', ->
@@ -270,7 +294,7 @@

   describe 'diag()', ->

-    it 'creates a new diagonal matrix', ->
+    it 'creates a new diagonal matrix from a column vector', ->
       expect(M.diag(M [2, 3, 4], 1)).toEqual M [ 2, 0, 0
                                                  0, 3, 0
                                                  0, 0, 4 ]


Fixup for sporadic spaces in hlint parse error

--- lib/ghc-mod/ghc-modi-process.coffee
+++ lib/ghc-mod/ghc-modi-process.coffee
@@ -367,7 +367,7 @@
       args: args
     .then (lines) =>
       rootDir = @getRootDir buffer
-      rx = /^(.*?):([0-9]+):([0-9]+): *(?:(Warning|Error): *)?/
+      rx = /^(.*?):([0-9\s]+):([0-9\s]+): *(?:(Warning|Error): *)?/
       lines
       .filter (line) ->
         switch


Filter packages after they finish loading

--- lib/installed-packages-panel.coffee
+++ lib/installed-packages-panel.coffee
@@ -142,6 +142,8 @@

         @updateSectionCounts()
         @displayPackageUpdates(packagesWithUpdates)
+
+        @matchPackages()

       .catch (error) =>
         console.error error.message, error.stack


Add missing errors div and outlet

Add missing errors div and outlet

Closes #317

--- lib/installed-packages-panel.coffee
+++ lib/installed-packages-panel.coffee
@@ -19,6 +19,8 @@
             @span outlet: 'totalPackages', class:'section-heading-count', ' (…)'
           @div class: 'editor-container', =>
             @subview 'filterEditor', new TextEditorView(mini: true, placeholderText: 'Filter packages by name')
+
+          @div outlet: 'updateErrors'

           @section class: 'sub-section installed-packages', =>
             @h3 class: 'sub-section-heading icon icon-package', =>
@@ -44,10 +46,7 @@
   initialize: (@packageManager) ->
     @packageViews = []

-    @subscribe @packageManager, 'package-install-failed', (pack, error) =>
-      @searchErrors.append(new ErrorView(@packageManager, error))
-
-    @subscribe @packageManager, 'package-update-failed theme-update-failed', (pack, error) =>
+    @subscribe @packageManager, 'package-install-failed theme-install-failed package-uninstall-failed theme-uninstall-failed package-update-failed theme-update-failed', (pack, error) =>
       @updateErrors.append(new ErrorView(@packageManager, error))

     @filterEditor.getModel().onDidStopChanging => @matchPackages()


Cleaner unpacking of data from scope

Cleaner unpacking of data from scope

--- services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee
+++ services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee
@@ -252,13 +252,11 @@
 				$scope.state.error = isError

 			$scope.shouldEnableProjectSelect = () ->
-				state = $scope.state
-				data = $scope.data
+				{ state, data } = $scope
 				return !state.inFlight.projects && data.projects

 			$scope.shouldEnableProjectEntitySelect = () ->
-				state = $scope.state
-				data = $scope.data
+				{ state, data } = $scope
 				return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId

 			$scope.shouldEnableCreateButton = () ->


Remove raw Path as valid node shape.

Remove raw Path as valid node shape.

--- app/scripts/viewmodels/drawings/elements.coffee
+++ app/scripts/viewmodels/drawings/elements.coffee
@@ -36,11 +36,6 @@
           options.sides or= 3
           options.radius or= 50
           new paper.Path.RegularPolygon(new paper.Point(0, 0), options.sides, options.radius)
-        when "path"
-          path = new paper.Path()
-          for point in options.points
-            path.add(new paper.Point(point.x, point.y))
-          path

     getOption: (content, node, defaultValue) =>
       if (typeof content is 'string' or content instanceof String) and content.length and content[0] is "$"


let the userArea fall back to the group entryPoint.slug

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -87,7 +87,10 @@
             @emit 'GroupChanged', groupName, group
             @openGroupChannel group, => @emit 'GroupChannelReady'

-  getUserArea:-> @userArea
+  getUserArea:->
+    @userArea ?
+      if KD.config.entryPoint?.type is 'group'
+      then KD.config.entryPoint.slug

   setUserArea:(userArea)->
     @emit 'UserAreaChanged', userArea  if not _.isEqual userArea, @userArea


Lazy-load prefs.get, too.

Lazy-load prefs.get, too.

--- application/chrome/content/wesabe/Logger.coffee
+++ application/chrome/content/wesabe/Logger.coffee
@@ -1,4 +1,4 @@
-prefs = require 'util/prefs'
+#prefs = require 'util/prefs'
 #
 # NOTE: instead of using `require' for all these we lazy-load them,
 # otherwise `logger' will not be available for them since, duh, we're
@@ -10,6 +10,9 @@
 untaint = (args...) ->
   {untaint} = require 'util/privacy'
   untaint args...
+getPref = (args...) ->
+  getPref = (require 'util/prefs').get
+  getPref args...

 loggersByName = {}

@@ -41,7 +44,7 @@
   # The higher the level, the more attention you should pay.
   #
   @::__defineGetter__ 'level', ->
-    @_level ? @_parent?._level ? levelCodeForName prefs.get('wesabe.logger.level')
+    @_level ? @_parent?._level ? levelCodeForName getPref('wesabe.logger.level')

   @::__defineSetter__ 'level', (level) ->
     @_level = (levelCodeForName level) ? level


Remove Groups from left sidebar menu

Remove Groups from left sidebar menu

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -4,11 +4,11 @@
     name         : "Groups"
     route        : "/Groups"
     hiddenHandle : yes
-    navItem      :
-      title      : "Groups"
-      path       : "/Groups"
-      order      : 40
-      topLevel   : yes
+    # navItem      :
+    #   title      : "Groups"
+    #   path       : "/Groups"
+    #   order      : 40
+    #   topLevel   : yes

   @privateGroupOpenHandler =(event)->
     event.preventDefault()


Fixed file finder plugin's path for os kite

--- client/landing/app/CommonViews/fatih/plugins/fatihfilefinderplugin.coffee
+++ client/landing/app/CommonViews/fatih/plugins/fatihfilefinderplugin.coffee
@@ -11,14 +11,8 @@
     @on "ListItemClicked", -> @fatihView.destroy()

   action: (keyword) ->
-    # path    = "/Users/#{KD.whoami().profile.nickname}/"
-    # command = "find '#{path}' -type f -iname '*#{keyword}*'"
-    path = "."
-
-    command =
-      kiteName : "os"
-      method   : "exec"
-      withArgs : "find '#{path}' -type f -iname '*#{keyword}*'"
+    path    = "/home/#{KD.whoami().profile.nickname}/"
+    command = "find \"#{path}\" -type f -iname \"*#{keyword}*\""

     @getSingleton('kiteController').run command, (err, res) =>
       @searchHelper keyword, @parseResponse res


Adding isolated scope and maxCallNumber

Adding isolated scope and maxCallNumber

--- src/app/components/directive.coffee
+++ src/app/components/directive.coffee
@@ -6,29 +6,36 @@
     ngFillHeightLink = (scope, element, attrs) ->
       ngFillHeightOption = ($parse attrs.ngFillHeight)(scope)
       ngFillHeightOption.minHeight = ngFillHeightOption.minHeight or 0
+      ngFillHeightOption.maxCallNumber or= 10000

       if typeof ngFillHeightOption isnt 'object'
         console.error 'The value of ngFillHeight has to be an Object'
         return

       parentObject = angular.element(ngFillHeightOption.parentSelector)
-      currObject =  angular.element(element)
+      currObject   =  angular.element(element)
+      timesCalled  = 0

-      ngFillHeightOption.api = {
+      ngFillHeightOption.api =
         recalcHeight : () ->

           recurrFunc = (increment) ->
             currObject.height(currObject.height() + increment)
+            timesCalled++
+            return

-          if parentObject.prop('scrollHeight')
+          if parentObject.prop('scrollHeight') and timesCalled <= ngFillHeightOption.maxCallNumber
             recurrFunc(10) while currObject.height() < ngFillHeightOption.minHeight or parentObject.height() >= parentObject.prop('scrollHeight')

             recurrFunc(-1) while currObject.height() > ngFillHeightOption.minHeight and parentObject.height() < parentObject.prop('scrollHeight')
+          else
+            $log.warn("[ng-fill-height] : Maximum number of calls reached (#{ngFillHeightOption.maxCallNumber})")

           return currObject.height()
-      }
+

     return {
       restrict: 'A'
+      scope: {}
       link : ngFillHeightLink
     }


Send appropriate log details for the action of a user clicking the Undo button

--- src/code/models/lara-connect.coffee
+++ src/code/models/lara-connect.coffee
@@ -24,6 +24,7 @@
     @graphStore = GraphStore.store
     @paletteStore = PaletteStore.store
     @laraPhone = IframePhone.getIFrameEndpoint()
+    @lastCommandStackPosition = -1

     # Setup listeners
     @laraPhone.addListener 'initInteractive', (data) =>
@@ -57,4 +58,9 @@
   onUndoRedoStateChange: (state) ->
     lastAction = @undoRedoManager.commands[@undoRedoManager.stackPosition]
     if lastAction and @loaded
-      @laraPhone.post 'log', {action: lastAction.name}
+      if @undoRedoManager.stackPosition < (@undoRedoManager.commands.length - 1) and @lastCommandStackPosition > @undoRedoManager.stackPosition
+        # User clicked Undo
+        @laraPhone.post 'log', {action: "undo"}
+      else
+        @laraPhone.post 'log', {action: lastAction.name}
+    @lastCommandStackPosition = @undoRedoManager.stackPosition


Get added parcel from diff patch patch array.

--- src/coffee/utils/order-utils.coffee
+++ src/coffee/utils/order-utils.coffee
@@ -52,14 +52,14 @@
             actions.push action

           else
-            delivery = deliveryDiff
             # iterate over parcel instances
-            _.each _.keys(delivery.parcels), (parcelIndex) ->
+            _.each _.keys(deliveryDiff.parcels), (parcelIndex) ->
               if parcelIndex != '_t'
-                parcel = delivery.parcels[parcelIndex]
-                if _.isArray parcel
+                parcelDiff = deliveryDiff.parcels[parcelIndex]
+
+                if _.isArray parcelDiff
                   # delivery was added
-                  delivery = _.last parcel
+                  parcel = _.last parcelDiff
                   action =
                     action: 'addParcelToDelivery'
                     measurements: old_obj.deliveries[deliveryIndex].parcels[parcelIndex].measurements


make the if statement & assigment more readable

--- client/app/Applications/Environments.kdapplication/views/DomainMapperView.coffee
+++ client/app/Applications/Environments.kdapplication/views/DomainMapperView.coffee
@@ -60,11 +60,10 @@
           vmName     : @getData().name
           state      : state
         , (err) =>
-          if not err
-            if state is on
-              notificationMsg = "Your domain is connected to the #{@getData().name} VM."
-            if state is off
-              notificationMsg = "Your domain is disconnected from the #{@getData().name} VM."
+          unless err
+            notificationMsg = if state
+            then "Your domain is connected to the #{@getData().name} VM."
+            else "Your domain is disconnected from the #{@getData().name} VM."
             new KDNotificationView {type: "top", title: notificationMsg}
           else
             new KDNotificationView {type: "top", title: err}


Fix copy/paste error

Fix copy/paste error

--- services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee
+++ services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee
@@ -43,7 +43,7 @@
 			return { message: 'password is too short' }
 		if (Settings.passwordStrengthOptions?.length?.min? and
 				Settings.passwordStrengthOptions?.length?.min > password.length)
-			return { message: "password is too short" }
+			return { message: "password is too long" }
 		return null

 	setUserPassword: (user_id, password, callback = (error) ->) ->


Troubleshoot: recover method is added to HealthChecker

--- client/Main/troubleshoot/healthchecker.coffee
+++ client/Main/troubleshoot/healthchecker.coffee
@@ -10,16 +10,17 @@

   run: ->
     @emit "healthCheckStarted"
-    @status = "waiting"
-    @startTime = Date.now()
-    @setPingTimeout()
-    @cb @finish.bind(this)
+
+    {troubleshoot} = @getOptions()
     return @forceComplete "undefined callback"  unless troubleshoot
+    @completeEvent = "healthCheckCompleted"
+    @startCheck()
+    troubleshoot @finish.bind(this)

   setPingTimeout: ->
     @pingTimeout = setTimeout =>
       @status = "fail"
-      @emit "healthCheckCompleted"
+      @emit @completeEvent
     , @getOptions().timeout

   finish: (data)->
@@ -31,7 +32,12 @@
       @status = if speedCheck and @getResponseTime() > slownessIndicator then "slow" else "success"
       clearTimeout @pingTimeout
       @pingTimeout = null
-      @emit "healthCheckCompleted"
+      @emit @completeEvent
+
+  startCheck: ->
+    @status = "waiting"
+    @startTime = Date.now()
+    @setPingTimeout @completeEvent

   reset: ->
     @status = "waiting"
@@ -45,3 +51,14 @@
     warn err  if err
     @status = "fail"
     @emit "healthCheckCompleted"
+
+  recover: ->
+    @completeEvent = "recoveryCompleted"
+    @startCheck()
+    @emit "recoveryStarted"
+    {recover} = @getOptions()
+    recover @finish.bind(this)
+
+  canBeRecovered: ->
+    @getOptions().recover?
+


Add or remove devices tags

Add or remove devices tags

--- api.coffee
+++ api.coffee
@@ -8,6 +8,7 @@

 # regular expression objects
 TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_]+)\/tasks\/?$/
+TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_]+)\/tags\/([a-zA-Z0-9\-\_]+)\/?$/

 connectionRequest = (deviceId, callback) ->
   devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, (err, device)->
@@ -58,11 +59,12 @@
 dbserver = new mongo.Server(config.MONGODB_SOCKET, 0, {auto_reconnect: true, safe: true})
 tasksCollection = null
 devicesCollection = null
-db = new mongo.Db(config.DATABASE_NAME, dbserver, {native_parser:true})
+db = new mongo.Db(config.DATABASE_NAME, dbserver, {native_parser:true, safe:true})
 db.open( (err, db) ->
   db.collection('tasks', (err, collection) ->
     tasksCollection = collection
-    tasksCollection.ensureIndex({device: 1, timestamp: 1})
+    tasksCollection.ensureIndex({device: 1, timestamp: 1}, (err) ->
+    )
   )

   db.collection('devices', (err, collection) ->
@@ -102,7 +104,32 @@

     request.addListener 'end', () ->
       urlParts = url.parse(request.url, true)
-      if TASKS_REGEX.test(urlParts.pathname)
+      if TAGS_REGEX.test(urlParts.pathname)
+        r = TAGS_REGEX.exec(urlParts.pathname)
+        deviceId = r[1]
+        tag = r[2]
+        if request.method == 'POST'
+          devicesCollection.update({'_id' : deviceId}, {'$addToSet' : {'_tags' : tag}}, {safe: true}, (err) ->
+            if err
+              response.writeHead(500)
+              response.end(err)
+              return
+            response.writeHead(200)
+            response.end()
+          )
+        else if request.method == 'DELETE'
+          devicesCollection.update({'_id' : deviceId}, {'$pull' : {'_tags' : tag}}, {safe: true}, (err) ->
+            if err
+              response.writeHead(500)
+              response.end(err)
+              return
+            response.writeHead(200)
+            response.end()
+          )
+        else
+          response.writeHead 405, {'Allow': 'GET'}
+          response.end('405 Method Not Allowed')
+      else if TASKS_REGEX.test(urlParts.pathname)
         if request.method == 'POST'
           deviceId = TASKS_REGEX.exec(urlParts.pathname)[1]
           if request.content


Added a global expire for redis.

Added a global expire for redis.

--- app.coffee
+++ app.coffee
@@ -3,6 +3,8 @@

 # Setup the environment configuration
 global.config = require("./config/config.json")[app_env]
+
+global.redisExpire = 86400

 # Load all dependent libraries
 daemon = require("daemon")


Remove HTTPS from express

Remove HTTPS from express

--- app.coffee
+++ app.coffee
@@ -33,15 +33,14 @@
 app.get "/stylesheets/*", serveAsset
 app.get "/", routes.index

-if app.get("env") is "production"
-  options =
-    key: fs.readFileSync('/etc/haproxy/certs/cyberia2020-key.pem')
-    cert: fs.readFileSync('/etc/haproxy/certs/cyberia2020-cert.pem')
-    ca: fs.readFileSync('/etc/haproxy/certs/cyberia2020-intermediate.pem')
+###
+options =
+  key: fs.readFileSync('/etc/haproxy/certs/cyberia2020.key', "utf8")
+  cert: fs.readFileSync('/etc/haproxy/certs/cyberia2020.crt', "utf8")
+  ca: fs.readFileSync('/etc/haproxy/certs/cyberia2020-intermediate.pem', "utf8")
+###

-  https.createServer(options, app).listen app.get("port"), ->
-    console.log "Express server listening on port " + app.get("port")
-
-else
-  http.createServer(app).listen app.get("port"), ->
-    console.log "Express server listening on port " + app.get("port")
+httpServer = http.createServer(app).listen app.get("port"), ->
+  console.log "Express server listening on port " + app.get("port")
+#httpsServer = https.createServer(options, app).listen app.get("port"), ->
+#  console.log "Express http server listening on port " + app.get("port")


hub: fix this -- The sagemath collaboration request email should have a reply-to of the user sending the request.  People often respond to it.

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -1750,8 +1750,10 @@
                             cb()
                         else
                             # send an email to the user
+                            s = @signed_in_mesg
                             send_email
                                 to      : email_address
+                                from    : if s? then "#{s.first_name} #{s.last_name} <#{s.email_address}>" else undefined
                                 subject : "SageMathCloud Invitation"
                                 body    : email.replace("https://cloud.sagemath.com", "Sign up at https://cloud.sagemath.com using the email address #{email_address}.")
                                 cb      : cb


change to save_blob to debug an issue.

change to save_blob to debug an issue.

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -3884,9 +3884,16 @@
 save_blob = (opts) ->
     opts = defaults opts,
         uuid  : undefined  # if not give, is generated; function always returns the uuid that was used
-        value : required   # NOTE: value *must* be a Buffer.
+        #value : required   # NOTE: value *must* be a Buffer.
+        value : undefined
         cb    : required
         ttl   : undefined  # object in blobstore will have *at least* this ttl; if there is already something,  in blobstore with longer ttl, we leave it.
+
+    if not opts.value?
+        err = "BUG -- error in save_blob; received a save_blob request with undefined value"
+        winston.debug(err)
+        opts.cb(err)
+        return

     if opts.value.length > 5000000
         if not opts.ttl?


Parse `\newenvironment{}` commands.

Parse `\newenvironment{}` commands.

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee
@@ -97,9 +97,24 @@
 		meta: "env"
 	}]

+	CUSTOM_ENVIRONMENT_REGEX = /^\\newenvironment{(\w+)}.*$/gm
+
+	parseCustomEnvironmentNames = (text) ->
+		names = []
+		iterations = 0
+		while match = CUSTOM_ENVIRONMENT_REGEX.exec(text)
+			names.push match[1]
+			iterations += 1
+			if iterations >= 1000
+				return names
+		return names
+
 	class SnippetManager
 		getCompletions: (editor, session, pos, prefix, callback) ->
-			console.log ">> get snippet completions"
+			# console.log ">> get snippet completions", editor, session, pos, prefix
+			docText = session.getValue()
+			customEnvironmentNames = parseCustomEnvironmentNames(docText)
+			# console.log customEnvironmentNames
 			callback null, staticSnippets

 	return SnippetManager


integration: fetch channelintegration data before configuring it

# Conflicts:
#	client/admin/lib/views/integrations/adminconfiguredintegrationitemview.coffee

--- client/admin/lib/views/integrations/adminconfiguredintegrationitemview.coffee
+++ client/admin/lib/views/integrations/adminconfiguredintegrationitemview.coffee
@@ -78,29 +78,37 @@
     @fetchChannels (err, channels) =>
       return showError err  if err

-      data              =
-        channels        : channels
-        id              : channelIntegration.id
-        name            : integration.name
-        title           : integration.title
-        token           : channelIntegration.token
-        summary         : integration.summary
-        settings        : channelIntegration.settings
-        createdAt       : channelIntegration.createdAt
-        iconPath        : integration.iconPath
-        updatedAt       : channelIntegration.updatedAt
-        description     : integration.description
-        instructions    : integration.instructions
-        typeConstant    : integration.typeConstant
-        integrationId   : channelIntegration.integrationId
-        selectedChannel : channelIntegration.channelId
-        webhookUrl      : "#{globals.config.integration.url}/#{integration.name}/#{channelIntegration.token}"
-        integrationType : 'configured'
+      kd.singletons.socialapi.integrations.fetch {id: channelIntegration.id}, (err, response) =>

-      # DUMMY DATA
-      data.selectedEvents = [ 'added_comment', 'edited_feature' ]
+        return showError err  if err

-      @emit 'IntegrationCustomizeRequested', data
+        { channelIntegration } = response
+
+        data              =
+          channels        : channels
+          id              : channelIntegration.id
+          name            : integration.name
+          title           : integration.title
+          token           : channelIntegration.token
+          summary         : integration.summary
+          settings        : channelIntegration.settings
+          createdAt       : channelIntegration.createdAt
+          iconPath        : integration.iconPath
+          updatedAt       : channelIntegration.updatedAt
+          description     : channelIntegration.description or integration.summary
+          instructions    : integration.instructions
+          typeConstant    : integration.typeConstant
+          integrationId   : channelIntegration.integrationId
+          selectedChannel : channelIntegration.channelId
+          webhookUrl      : "#{globals.config.integration.url}/#{integration.name}/#{channelIntegration.token}"
+          integrationType : 'configured'
+          isDisabled      : channelIntegration.isDisabled
+
+
+        # DUMMY DATA
+        data.selectedEvents = [ 'added_comment', 'edited_feature' ]
+
+        @emit 'IntegrationCustomizeRequested', data

   pistachio: ->


profile/groups: custom view now autosaves when selecting an option or uploading an image

--- client/app/Applications/Groups.kdapplication/Views/groupscustomizeviews.coffee
+++ client/app/Applications/Groups.kdapplication/Views/groupscustomizeviews.coffee
@@ -25,69 +25,6 @@
       callback  : =>
         contentWrapper = @getDelegate().groupContentWrapperView or @getDelegate().profileContentWrapperView
         contentWrapper.unsetClass 'edit'
-
-    @saveButton = new KDButtonView
-      title     : 'Save Changes'
-      cssClass  : 'save-button clean-gray'
-      loader    :
-        color   : '#444'
-        diameter : 12
-      callback :=>
-        if @group
-
-          @saveButton.showLoader()
-
-          if @customColorChanged
-            defaultIndexItem = @bgColorView.thumbsController.selectedItems.first
-
-            if defaultIndexItem
-              pickedColor = defaultIndexItem.color.picker.getValue() or 'fff'
-              log pickedColor
-              @group.setBackgroundImage 'customColor', pickedColor, (err,res)=>
-                @saveButton.hideLoader()
-                unless err
-                  new KDNotificationView
-                    title : "Background updated with custom color"
-                  @customColorChanged = no
-
-          else if @customImageChanged
-            url = @bgSelectView.thumbsController.selectedItems.first.customUrl \
-            or @bgSelectView.thumbsController.selectedItems.first.getData().url
-            if url
-              @group.setBackgroundImage 'customImage', url, (err,res)=>
-                @saveButton.hideLoader()
-                unless err
-                  new KDNotificationView
-                    title : "Background updated with custom image"
-                  @customImageChanged = no
-
-          else if @defaultImageChanged
-              defaultIndexItem = @bgSelectView.thumbsController.selectedItems.first
-
-              if defaultIndexItem
-                defaultIndex = defaultIndexItem.getData().dataIndex or 0
-                @group.setBackgroundImage 'defaultImage', defaultIndex, (err,res)=>
-                  @saveButton.hideLoader()
-                  unless err
-                    new KDNotificationView
-                      title : "Background updated to #{defaultIndexItem.getData().title}"
-                    @defaultImageChanged = no
-
-          else if @defaultColorChanged
-              defaultIndexItem = @bgColorView.thumbsController.selectedItems.first
-
-              if defaultIndexItem
-                defaultHex = defaultIndexItem.getData().colorValue or 0
-                @group.setBackgroundImage 'defaultColor', defaultHex, (err,res)=>
-                  @saveButton.hideLoader()
-                  unless err
-                    new KDNotificationView
-                      title : "Background updated to color #{defaultIndexItem.getData().title}"
-                    @defaultColorChanged = no
-
-          else
-            # no changes
-            @saveButton.hideLoader()

     @attachListeners()
     @addSettingsButton()
@@ -120,13 +57,16 @@
       @customColorChanged = no
       @defaultImageChanged = no
       @bgSelectView.thumbsController.deselectAllItems()
+      @utils.defer => @emit 'OptionChanged'

     @on 'DefaultImageSelected',=>
+      # @emit 'OptionChanged'
       @customImageChanged = no
       @defaultImageChanged = yes
       @customColorChanged = no
       @defaultColorChanged = no
       @bgColorView.thumbsController.deselectAllItems()
+      @utils.defer => @emit 'OptionChanged'

     @on 'CustomImageSelected',=>
       @customImageChanged = yes
@@ -134,6 +74,7 @@
       @customColorChanged = no
       @defaultColorChanged = no
       @bgColorView.thumbsController.deselectAllItems()
+      @utils.defer => @emit 'OptionChanged'

     @on 'CustomColorSelected',=>
       @customImageChanged = no
@@ -141,6 +82,55 @@
       @defaultImageChanged = no
       @defaultColorChanged = no
       @bgSelectView.thumbsController.deselectAllItems()
+      @utils.defer => @emit 'OptionChanged'
+
+    @on 'OptionChanged',=>
+
+        if @group
+
+          if @customColorChanged
+            defaultIndexItem = @bgColorView.thumbsController.selectedItems.first
+
+            if defaultIndexItem
+              pickedColor = defaultIndexItem.color.picker.getValue() or 'fff'
+              log pickedColor
+              @group.setBackgroundImage 'customColor', pickedColor, (err,res)=>
+                unless err
+                  new KDNotificationView
+                    title : "Background updated with custom color"
+                  @customColorChanged = no
+
+          else if @customImageChanged
+            url = @bgSelectView.thumbsController.selectedItems.first.customUrl \
+            or @bgSelectView.thumbsController.selectedItems.first.getData().url
+            if url
+              @group.setBackgroundImage 'customImage', url, (err,res)=>
+                unless err
+                  new KDNotificationView
+                    title : "Background updated with custom image"
+                  @customImageChanged = no
+
+          else if @defaultImageChanged
+              defaultIndexItem = @bgSelectView.thumbsController.selectedItems.first
+
+              if defaultIndexItem
+                defaultIndex = defaultIndexItem.getData().dataIndex or 0
+                @group.setBackgroundImage 'defaultImage', defaultIndex, (err,res)=>
+                  unless err
+                    new KDNotificationView
+                      title : "Background updated to #{defaultIndexItem.getData().title}"
+                    @defaultImageChanged = no
+
+          else if @defaultColorChanged
+              defaultIndexItem = @bgColorView.thumbsController.selectedItems.first
+
+              if defaultIndexItem
+                defaultHex = defaultIndexItem.getData().colorValue or 0
+                @group.setBackgroundImage 'defaultColor', defaultHex, (err,res)=>
+                  unless err
+                    new KDNotificationView
+                      title : "Background updated to color #{defaultIndexItem.getData().title}"
+                    @defaultColorChanged = no

   viewAppended:->
     super
@@ -162,7 +152,6 @@
     </h1>
     {{> @bgSelectView}}
     {{> @bgColorView}}
-    {{> @saveButton}}
     """

@@ -184,10 +173,11 @@

     @uploader.on 'FileUploadComplete', (res)=>
       if res.length and res[0].resource
-        # log @getDelegate().getDelegate()
         @getDelegate().getDelegate().getDelegate().staticController.setBackground 'customImage', res[0].resource
         @$().css backgroundImage : "url(#{res[0].resource})"
         @getDelegate().customUrl = res[0].resource
+
+        @getDelegate().emit 'FileUploaded',@

   viewAppended:->
     super
@@ -237,6 +227,12 @@

     @thumbsController.listView.on 'CustomImageSelected', (view)=>
       @getDelegate().emit 'CustomImageSelected', view
+
+    @thumbsController.listView.on 'FileUploaded', (view)=>
+      log 'selecting item',view.getDelegate()
+      @thumbsController.selectItem view.getDelegate()
+      @getDelegate().emit 'CustomImageSelected', view
+

   decorateList:(group={})->
     backgroundData = @getDelegate().getBackgroundData group
@@ -296,9 +292,10 @@

     @customUrl = null

+    @on 'FileUploaded', (view)=>
+      @getDelegate().emit 'FileUploaded', view
   click: ->
     # preview live
-
     if @getData().type is 'defaultImage'
       @getDelegate().emit 'DefaultImageSelected', @
       @getDelegate().getDelegate().staticController.setBackground @type, @getData().url


changed the msg we show when collab is terminated.

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -849,7 +849,7 @@

   showSessionEndedModal: (content) ->

-    content ?= "This session is ended by @#{@collaborationHost} You won't be able to access it anymore."
+    content ?= "This collaboration session has been terminated by the host @#{@collaborationHost}."

     options        =
       title        : 'Session ended'


Moar defensive code

Moar defensive code

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -961,7 +961,7 @@
           callback : =>
             @modal.destroy()

-    @chat.end()
+    @chat?.end()
     @showModal options
     @handleCollaborationEndedForParticipant()

@@ -981,7 +981,7 @@
           callback : =>
             @modal.destroy()

-    @chat.end()
+    @chat?.end()
     @showModal options
     @handleCollaborationEndedForParticipant()


Update unit tests

Update unit tests

--- services/document-updater/test/unit/coffee/RedisBackend/RedisBackendTests.coffee
+++ services/document-updater/test/unit/coffee/RedisBackend/RedisBackendTests.coffee
@@ -368,15 +368,8 @@

 			it "should return the primary result", ->
 				@result.should.deep.equal [@doclines, @version]
-
-			it "should log out the secondary error", ->
-				@logger.error
-					.calledWith({
-						err: @error
-					}, "error in redis backend")
-					.should.equal true
-
-		describe "when the secondary errors", ->
+
+		describe "when the primary errors", ->
 			beforeEach (done) ->
 				@rclient_redis.get = sinon.stub()
 				@rclient_redis.exec = sinon.stub().yields(@error = new Error("oops"))
@@ -391,13 +384,6 @@

 			it "should return the error", ->
 				@returned_error.should.equal @error
-
-			it "should log out the error", ->
-				@logger.error
-					.calledWith({
-						err: @error
-					}, "error in redis backend")
-					.should.equal true

 		describe "when the secondary takes longer than SECONDARY_TIMEOUT", ->
 			beforeEach (done) ->


Fixed the random function

Fixed the random function

--- bot.coffee
+++ bot.coffee
@@ -9,7 +9,7 @@

 rand = (n) ->
-  Math.round(Math.random()*n)
+  Math.floor(Math.random()*n)

 randomMove = ->
   "#{rand(7)}#{rand(7)}"


add a log hint for mismatched brackets

add a log hint for mismatched brackets

--- services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -93,6 +93,14 @@
 		regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/
 		newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
 		humanReadableHint: """
-			You have used \\begin{...} without a corresponding \\end{...}
+			You have used \\begin{...} without a corresponding \\end{...}.
 		"""
+	,
+		ruleId: "hint_mismatched_brackets"
+		regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/
+		newMessage: "Error: brackets do not match, found '$2' instead of '$1'"
+		humanReadableHint: """
+			You have used an open bracket without a corresponding close bracket.
+		"""
+
 ]


ComputeController: reset stack and machines when a new machine built or an existing one destroyed

--- client/Main/providers/computecontroller.coffee
+++ client/Main/providers/computecontroller.coffee
@@ -17,7 +17,8 @@

       @eventListener = new ComputeEventListener

-      @on "machineBuildCompleted", => delete @stacks
+      @on "MachineBuilt",   @bound 'reset'
+      @on "MachineDestroy", @bound 'reset'

       @fetchStacks => @emit 'ready'

@@ -66,6 +67,7 @@

       callback null, @machines = machines

+
   fetchMachine: (idOrUid, callback = noop)->

     KD.remote.api.JMachine.one idOrUid, (err, machine)->
@@ -90,9 +92,14 @@
     KD.remote.api.ComputeProvider.createGroupStack (err, stack)=>
       return if KD.showError err

-      delete @stacks
+      @reset()
       @emit "renderStacks"

+
+  reset:->
+
+    @stacks   = null
+    @machines = null

   errorHandler = (task, eL, machine)-> (err)->


ComputeController: add default timeout for all tasks

--- client/Main/providers/computecontroller.coffee
+++ client/Main/providers/computecontroller.coffee
@@ -1,6 +1,8 @@
 class ComputeController extends KDController

   @providers = KD.config.providers
+
+  @timeout = 5000

   constructor:->
     super
@@ -84,6 +86,8 @@

       @kloud.destroy { machineId: machine._id }

+      .timeout ComputeController.timeout
+
       .then (res)=>

         @eventListener.addListener 'destroy', machine._id
@@ -103,6 +107,8 @@

     @kloud.build { machineId: machine._id }

+    .timeout ComputeController.timeout
+
     .then (res)=>

       @eventListener.addListener 'build', machine._id
@@ -122,6 +128,8 @@

     @kloud.start { machineId: machine._id }

+    .timeout ComputeController.timeout
+
     .then (res)=>

       @eventListener.addListener 'start', machine._id
@@ -140,6 +148,8 @@
       percentage  : 0

     @kloud.stop { machineId: machine._id }
+
+    .timeout ComputeController.timeout

     .then (res)=>

@@ -165,6 +175,8 @@
       percentage  : 0

     @kloud.info { machineId: machine._id }
+
+    .timeout ComputeController.timeout

     .then (response)=>


Pass file type as folder

Pass file type as folder

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -57,8 +57,8 @@

   fetchAppsFromFs:(callback)->

-    path = "/home/#{KD.whoami().profile.nickname}/Applications"
-    appDir = FSHelper.createFileFromPath path
+    path   = "/home/#{KD.whoami().profile.nickname}/Applications"
+    appDir = FSHelper.createFileFromPath path, 'folder'
     appDir.fetchContents KD.utils.getTimedOutCallback (files)=>
       if not Array.isArray files or files.length is 0
         @putAppsToAppStorage {}


rollbar: log 'Timeout reached for kite request' only if user is not guest

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -104,7 +104,7 @@
           callback? null, manifests
     , ->
       msg = "Timeout reached for kite request"
-      KD.logToExternal msg
+      KD.logToExternal msg  unless KD.isGuest()
       log msg
       callback()


sidebar: show draft item menu on header click

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -12,7 +12,9 @@
 SidebarSharedMachinesSection = require 'app/components/sidebarsharedmachinessection'
 SharingMachineInvitationWidget = require 'app/components/sidebarmachineslistitem/sharingmachineinvitationwidget'
 SidebarDifferentStackResources = require 'app/components/sidebarstacksection/sidebardifferentstackresources'
-
+{ findDOMNode } = require 'react-dom'
+
+MENU = null

 module.exports = class Sidebar extends React.Component

@@ -76,6 +78,41 @@
         EnvironmentFlux.actions.loadMachines()

+  onMenuItemClick: (id, item, event) ->
+
+    { router } = kd.singletons
+
+    { title } = item.getData()
+    MENU.destroy()
+
+    draft = @state.drafts.get id
+    switch title
+      when 'Edit' then router.handleRoute "/Stack-Editor/#{id}"
+      when 'Initialize' then EnvironmentFlux.actions.generateStack id
+
+
+  onDraftTitleClick: (id, event) ->
+
+    kd.utils.stopDOMEvent event
+
+    lastLayer = kd.singletons.windowController.layers?.first
+
+    return  if MENU
+
+    callback = (item, event) => @onMenuItemClick id, item, event
+
+    menuItems = {}
+    ['Edit', 'Initialize'].forEach (name) => menuItems[name] = { callback }
+
+    { top } = findDOMNode(@refs["draft-#{id}"]).getBoundingClientRect()
+
+    menuOptions = { cssClass: 'SidebarMenu', x: 36, y: top + 31 }
+
+    MENU = new kd.ContextMenu menuOptions, menuItems
+
+    MENU.once 'KDObjectWillBeDestroyed', -> kd.utils.wait 50, -> MENU = null
+
+
   setActiveInvitationMachineId: ->

     { setActiveInvitationMachineId } = EnvironmentFlux.actions
@@ -137,12 +174,15 @@

   renderDrafts: ->

-    @state.drafts?.toList().toJS().map (template) ->
+    @state.drafts?.toList().toJS().map (template) =>
       id = template._id
       title = _.unescape template.title
-      <header key={id} className="SidebarSection-header">
+      <header
+        ref="draft-#{id}"
+        key={id}
+        className="SidebarSection-header">
         <h4 className='SidebarSection-headerTitle'>
-          <Link href="/Stack-Editor/#{id}">{title}</Link>
+          <Link href="/Stack-Editor/#{id}" onClick={@onDraftTitleClick.bind this, id}>{title}</Link>
         </h4>
       </header>


kd zip

kd zip

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -355,6 +355,7 @@
           KD.remote.api.JKodingKey.fetchByKey
             key: key
           , (err, kodingKey) =>
+            console.log "err", err, kodingKey
             unless kodingKey?.length
               KD.remote.api.JKodingKey.create {hostname, key}, (err, data)=>
                 if err or not data


Prettier getSplitTestResult method

Prettier getSplitTestResult method

--- components/artwork_filter/models/filter.coffee
+++ components/artwork_filter/models/filter.coffee
@@ -69,10 +69,7 @@
       @sorts[@selected.get('sort') or '-date_added']

   getSplitTestResult: ->
-    if splitTest('artwork_column_sort').outcome() is 'merchandisability'
-      return 'merchandisability'
-    else
-      return 'date_added'
+    if splitTest('artwork_column_sort').outcome() is 'merchandisability' then 'merchandisability' else 'date_added'

   buildState: ->
     new FilterState { id: @stateId() }, modelId: @model.id


payment: whoops, can't call #getOptions() before super()

--- client/landing/app/Applications/Dashboard.kdapplication/views/producteditform.coffee
+++ client/landing/app/Applications/Dashboard.kdapplication/views/producteditform.coffee
@@ -34,7 +34,7 @@
         label           : "Subscription type"
         itemClass       : KDSelectBox
         defaultValue    : data.subscriptionType ? "mo"
-        selectOptions   : @getSubscriptionOptions()
+        selectOptions   : @getSubscriptionTypes options
         callback        : @bound 'subscriptionTypeChanged'

     options.fields.feeAmount ?=
@@ -110,7 +110,7 @@
         priceIsVolatile
       }

-  getSubscriptionOptions:->
+  getSubscriptionTypes: (options) ->

     selectOptions = [
       { title: "Recurs every month",     value: 'mo' }
@@ -121,7 +121,7 @@
       { title: "Recurs every 5 years",   value: '5 yr' }
     ]

-    if @getOptions().isRecurOptional
+    if options.isRecurOptional
       selectOptions.push { title: "Single payment", value: 'single' }

     return selectOptions


code cleanup in the embedBox

code cleanup in the embedBox

--- client/app/Applications/Activity.kdapplication/views/embedboxviews.coffee
+++ client/app/Applications/Activity.kdapplication/views/embedboxviews.coffee
@@ -298,6 +298,8 @@
     super options,data
     @options      = options
     @loader       = new KDLoaderView
+
+    # @getDelegate().setClass 'condensed'

     # @imageTooltip =
     #   constructorName : KDCustomHTMLView
@@ -337,9 +339,11 @@
     @setData data
     @options = data.link_options

-    if data.link_embed?.images?[0]?.url
+    url = data.link_embed?.images?[0]?.url
+
+    if url
       @image.setDomAttributes
-        'data-src' : @utils.proxifyUrl data.link_embed.images[0].url
+        'data-src' : @utils.proxifyUrl url

     @setTemplate @pistachio()
     @template.update()


groups invitations: more copy changes

groups invitations: more copy changes

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -256,11 +256,11 @@
     if isApproval
       title   = 'Request Access'
       content = 'Membership to this group requires administrative approval.'
-      success = "Thanks! You'll receive an email an email shortly."
+      success = "Thanks! You'll receive an email when group's admin accepts you."
     else
       title   = 'Request an Invite'
       content = 'Membership to this group requires an invitation.'
-      success = 'Invitation has been requested!'
+      success = "Invitation has been sent to the group's admin."

     modal = new KDModalView
       title          : title


Social/Client: change default filter to a function and add `announcement` type

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -36,13 +36,13 @@

     @fakeMessageMap = {}

-    @setFilter @defaultFilter
+    @setFilter @getDefaultFilter()

     {socialapi} = KD.singletons
     @once 'ChannelReady', @bound 'bindChannelEvents'
     socialapi.onChannelReady data, @lazyBound 'emit', 'ChannelReady'

-    if typeConstant in ['group', 'topic']
+    if typeConstant in ['group', 'topic', 'announcement']
       @on 'LazyLoadThresholdReached', @bound 'lazyLoad'

     KD.singletons.windowController.addFocusListener @bound 'handleFocus'
@@ -54,7 +54,7 @@
           listView.on 'ItemWasAdded', @bound 'scrollDown'
       when 'privatemessage'
         @listController.getListView().on 'ItemWasAdded', @bound 'scrollDown'
-      when 'group'
+      when 'group', 'announcement'
       else
         @listController.getListView().on 'ItemWasAdded', @bound 'scrollUp'

@@ -386,4 +386,7 @@
     @populate()

-  defaultFilter: 'Most Liked'
+  getDefaultFilter:->
+    if @shouldHide()
+    then 'Most Recent'
+    else 'Most Liked'


Social: fix styling

Social: fix styling

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -171,21 +171,14 @@

     if type is 'privatemessage' or type is 'post' then return

-    {name, isParticipant} = @getData()
+    {name, isParticipant, typeConstant} = @getData()

     @channelTitleView = new KDCustomHTMLView
       partial   : "##{name}"
       cssClass  : "channel-title #{if isParticipant then 'participant' else ''}"

-    unless name is 'public'
+    if typeConstant not in ['group', 'announcement']
       @channelTitleView.addSubView new TopicFollowButton null, @getData()
-
-  shouldHide: ->
-    {id}                             = @getData()
-    {socialApiAnnouncementChannelId} = KD.getGroup()
-
-    # todo add is admin check
-    return  id is socialApiAnnouncementChannelId

   createInputWidget: ->

@@ -285,7 +278,7 @@

     return  unless item?.count
     # no need to send updatelastSeenTime or glance when checking publicfeeds
-    return  if name is 'public'
+    return  if name in ['public', 'announcement']

     if typeConstant is 'post'
     then socialapi.channel.glancePinnedPost   messageId : id, @bound 'glanced'
@@ -299,12 +292,16 @@

   focus: ->
+
+    # do not focus if we are in announcement channel
+    {socialapi} = KD.singletons
+    return  if socialapi.isAnnouncementItem @getData().id

     if @input
       @input.focus()
     else
       # TODO - undefined is not a function
-      @listController?.getListItems()?.first?.commentBox?.input?.focus()
+      @listController?.getListItems().first.commentBox.input.focus()

   populate: (callback = noop) ->
@@ -388,6 +385,9 @@

   getDefaultFilter:->
-    if @shouldHide()
+
+    {socialapi} = KD.singletons
+
+    if socialapi.isAnnouncementItem @getData().id
     then 'Most Recent'
     else 'Most Liked'


peer grading clear flagging checkbox on submit

--- common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee
+++ common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee
@@ -445,8 +445,8 @@
       @flag_student_container.show()
       @answer_unknown_container.show()
       @feedback_area.val("")
-      @remove_flag
-      @remove_answer_unknown
+      @answer_unknown_checkbox.removeAttr("checked")
+      @flag_student_checkbox.removeAttr("checked")

       @submit_button.unbind('click')
       @submit_button.click @submit_grade


bug fix on show me how button

bug fix on show me how button

--- client/landing/app/MainApp/book/bookview.coffee
+++ client/landing/app/MainApp/book/bookview.coffee
@@ -154,12 +154,14 @@
     if @pointer then @destroyPointer()

     # check if page has tutorial
-    if @page.getData().howToSteps.length < 1 or
-       KD.getSingleton("vmController").defaultVmName
+    if @page.getData().howToSteps.length < 1
       @showMeButton.hide()
-    else
-      @showMeButton.show()
-      debugger
+    else
+      if @page.getData().menuItem is "Develop" and
+        KD.getSingleton("vmController").defaultVmName is null
+          @showMeButton.hide()
+      else
+        @showMeButton.show()

   showMeButtonClicked:->
     @pointer?.destroy()


Client navigation works properly now, so enable Factlink links in the client :-)

--- core/app/backbone/views/evidence/ndp_fact_relation_or_comment_view.coffee
+++ core/app/backbone/views/evidence/ndp_fact_relation_or_comment_view.coffee
@@ -42,7 +42,7 @@
         collection: new SubComments([], parentModel: @model)

   _factBaseView: ->
-    view = new FactBaseView model: @model.getFact(), clickable_body: Factlink.Global.signed_in
+    view = new FactBaseView model: @model.getFact(), clickable_body: true

     @listenTo @model.getFact().getFactWheel(), 'sync', ->
       @model.fetch()


fix(attach): when attaching requester methods when already attached, it should return the resource instance

--- coffee/factories/091_base_requester.tail.coffee
+++ coffee/factories/091_base_requester.tail.coffee
@@ -143,7 +143,7 @@
 	# This is called automatically on all resource instances produced by the actions of this requester; however if you want to attach requester methods
 	# to a resource instance you make, you can do so by calling this with the instance of your resource.
 	$_attachRequesterMethods: ( resourceInstance ) ->
-		return if resourceInstance.$_requesterMethodsAttached
+		return resourceInstance if resourceInstance.$_requesterMethodsAttached

 		@$_initResourceStats resourceInstance


Fix plugin-tag in single tabarea

Fix plugin-tag in single tabarea

--- views/single-tabarea.cjsx
+++ views/single-tabarea.cjsx
@@ -98,7 +98,16 @@
               index: index
           }
           </TabPane>
-        <DropdownButton key={components.length} eventKey={-1} tab={插件 - {plugins[@state.key].displayName}} navItem={true}>
+        <DropdownButton key={components.length}
+                        eventKey={-1}
+                        tab=
+                        {
+                          if @state.key >= components.length and @state.key < 1000
+                            插件 - {plugins[@state.key - components.length].displayName}
+                          else
+                            '插件'
+                        }
+                        navItem={true}>
         {
           counter = 0
           plugins.map (plugin, index) =>


Remove comparator in mobile fair collections that sorts fairs by banner image size

This comparator sorts after data has been fetched, overriding sort order passed in via sort parameters to /fairs

--- src/mobile/collections/fairs.coffee
+++ src/mobile/collections/fairs.coffee
@@ -12,15 +12,6 @@

   model: Fair

-  comparator: (fair) ->
-    sizes =
-      'x-large' : 1
-      'large' : 2
-      'medium' : 3
-      'small' : 4
-      'x-small' : 5
-    sizes[fair.get('banner_size')]
-
   url: "#{sd.API_URL}/api/v1/fairs"

   # fairs is the array of links to the previous fairs,


added missing stubbed module call breaking tests

--- services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee
+++ services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee
@@ -30,6 +30,7 @@
 			"../Project/ProjectUpdateHandler":@ProjectUpdateHandler
 			"../Project/ProjectGetter":@ProjectGetter
 			"../TrackChanges/TrackChangesManager":@TrackChangesManager
+			"../../models/Project":{}
 		@project_id = "1234"

 	describe "reactivateProjectIfRequired", ->


Updated the lazy runtime visualizer

Updated the lazy runtime visualizer

--- demos/visualizer-lazy/Grid.coffee
+++ demos/visualizer-lazy/Grid.coffee
@@ -1,169 +1,182 @@
 'use strict'

 Grid = (@playfield, @pathSet, @canvas) ->
-  @cellSize = 36
-  @COLORS =
-    BACKGROUND: '#272822'
-    PATHBACKGROUND: '#49483E'
-    GRID: '#EDF'
-    PATHINDICATOR: 'rgb(24, 155, 230)'
-    FONT: '#EDF'
+	@cellSize = 36
+	@COLORS =
+		BACKGROUND: '#272822'
+		PATHBACKGROUND: '#49483E'
+		GRID: '#EDF'
+		PATHINDICATOR: 'rgb(24, 155, 230)'
+		FONT: '#EDF'

-  @canvas.width = @playfield.width * @cellSize
-  @canvas.height = @playfield.height * @cellSize
+	@canvas.width = @playfield.width * @cellSize
+	@canvas.height = @playfield.height * @cellSize

-  @con2d = @canvas.getContext '2d'
-  @con2d.textAlign = 'center'
-  @con2d.textBaseline = 'middle'
-  @con2d.font = '20px Consolas, monospace'
-  @con2d.strokeStyle = '#FFF'
-  @con2d.lineWidth = 0.8
+	@con2d = @canvas.getContext '2d'
+	@con2d.textAlign = 'center'
+	@con2d.textBaseline = 'middle'
+	@con2d.font = '20px Consolas, monospace'
+	@con2d.strokeStyle = '#FFF'
+	@con2d.lineWidth = 0.8

-  @onChange = null
-  @mouseState =
-    x: -1
-    y: -1
+	@onChange = null
+	@mouseState =
+		x: -1
+		y: -1

-  @_setupMouseListener()
+	@_setupMouseListener()

-  @hitRegions = []
-  @_setupHitRegions()
-  @arrowImages = getArrowImages @cellSize / 3, @COLORS
-  @draw()
-  return
+	@hitRegions = []
+	@_setupHitRegions()
+	@arrowImages = getArrowImages @cellSize / 3, @COLORS
+	@draw()
+	return

 getArrowImage = (size, angle, COLORS) ->
-  canvas = document.createElement 'canvas'
-  canvas.width = size
-  canvas.height = size
-  con2d = canvas.getContext '2d'
-  con2d.strokeStyle = COLORS.PATHINDICATOR
-  con2d.lineWidth = 2
+	canvas = document.createElement 'canvas'
+	canvas.width = size
+	canvas.height = size
+	con2d = canvas.getContext '2d'
+	con2d.strokeStyle = COLORS.PATHINDICATOR
+	con2d.lineWidth = 2

-  con2d.translate size / 2, size / 2
-  con2d.rotate angle
+	con2d.translate size / 2, size / 2
+	con2d.rotate angle

-  con2d.beginPath()
-  con2d.moveTo -size / 2 - 1, size / 2 - 4
-  con2d.lineTo  0, -size / 2 + 3
-  con2d.lineTo  size / 2 + 1, size / 2 - 4
-  con2d.stroke()
-  canvas
+	con2d.beginPath()
+	con2d.moveTo -size / 2 - 1, size / 2 - 4
+	con2d.lineTo  0, -size / 2 + 3
+	con2d.lineTo  size / 2 + 1, size / 2 - 4
+	con2d.stroke()
+	canvas

 getArrowImages = (size, COLORS) ->
-  '^': getArrowImage size, 0, COLORS
-  '<': getArrowImage size, -Math.PI / 2, COLORS
-  'v': getArrowImage size, Math.PI, COLORS
-  '>': getArrowImage size, Math.PI / 2, COLORS
+	'^': getArrowImage size, 0, COLORS
+	'<': getArrowImage size, -Math.PI / 2, COLORS
+	'v': getArrowImage size, Math.PI, COLORS
+	'>': getArrowImage size, Math.PI / 2, COLORS

 directions = [
-  { char: '<', offset: { x: 0, y: 1 } }
-  { char: '^', offset: { x: 1, y: 0 } }
-  { char: 'v', offset: { x: 1, y: 2 } }
-  { char: '>', offset: { x: 2, y: 1 } }
+	{ char: '<', offset: { x: 0, y: 1 } }
+	{ char: '^', offset: { x: 1, y: 0 } }
+	{ char: 'v', offset: { x: 1, y: 2 } }
+	{ char: '>', offset: { x: 2, y: 1 } }
 ]

+
+directionsIndexed = directions.reduce (ret, { char, offset }) ->
+		ret.set char, offset
+		ret
+	, new Map
+
+
 Grid::_setupHitRegions = ->
-  getCellRegion = ((x, y, offX, offY, dir) ->
-    x: x
-    y: y
-    dir: dir
-    start:
-      x: x * @cellSize + @cellSize / 3 * offX
-      y: y * @cellSize + @cellSize / 3 * offY
-    end:
-      x: x * @cellSize + @cellSize / 3 * (offX + 1)
-      y: y * @cellSize + @cellSize / 3 * (offY + 1)
-    size:
-      width: @cellSize / 3
-      height: @cellSize / 3
-  ).bind @
+	getCellRegion = ((x, y, offX, offY, dir) ->
+		x: x
+		y: y
+		dir: dir
+		start:
+			x: x * @cellSize + @cellSize / 3 * offX
+			y: y * @cellSize + @cellSize / 3 * offY
+		end:
+			x: x * @cellSize + @cellSize / 3 * (offX + 1)
+			y: y * @cellSize + @cellSize / 3 * (offY + 1)
+		size:
+			width: @cellSize / 3
+			height: @cellSize / 3
+	).bind @

-  for i in [0...@playfield.width]
-    for j in [0...@playfield.height]
-      for dir in directions
-        if @pathSet.has i, j, dir.char
-          offset = dir.offset
-          @hitRegions.push getCellRegion i, j, offset.x, offset.y, dir.char
+	for i in [0...@playfield.width]
+		for j in [0...@playfield.height]
+			char = @playfield.getAt i, j

-  return
+			if char in ['^', '<', 'v', '>'] and (@pathSet.getStartingFrom i, j, '')?
+				offset = directionsIndexed.get char
+				@hitRegions.push getCellRegion i, j, offset.x, offset.y, char
+			else
+				for dir in directions
+					if (@pathSet.getStartingFrom i, j, dir.char)?
+						offset = dir.offset
+						@hitRegions.push getCellRegion i, j, offset.x, offset.y, dir.char
+
+	return

 Grid::_getRegion = (x, y) ->
-  for region in @hitRegions
-    if region.start.x <= x <= region.end.x and region.start.y <= y <= region.end.y
-      return region
-  return null
+	for region in @hitRegions
+		if region.start.x <= x <= region.end.x and region.start.y <= y <= region.end.y
+			return region
+	null

 # throttle this
 mouseMove = (e) ->
-  @mouseState.x = e.offsetX ? e.layerX
-  @mouseState.y = e.offsetY ? e.layerY
-  newRegion = @_getRegion @mouseState.x, @mouseState.y
-  if newRegion != @currentRegion
-    @currentRegion = newRegion
-    if @currentRegion?
-      @highlightedPath = @pathSet.getStartingFrom @currentRegion.x, @currentRegion.y, @currentRegion.dir
-    else
-      @highlightedPath = null
-    @draw()
+	@mouseState.x = e.offsetX ? e.layerX
+	@mouseState.y = e.offsetY ? e.layerY
+	newRegion = @_getRegion @mouseState.x, @mouseState.y
+	if newRegion != @currentRegion
+		@currentRegion = newRegion
+		if @currentRegion?
+			@highlightedPath = @pathSet.getStartingFrom @currentRegion.x, @currentRegion.y, @currentRegion.dir
+		else
+			@highlightedPath = null
+		@draw()

-    @onChange? @highlightedPath
+		@onChange? @highlightedPath

 Grid::_setupMouseListener = ->
-  @_mouseMove = mouseMove.bind @
-  @canvas.addEventListener 'mousemove', @_mouseMove
-  return
+	@_mouseMove = mouseMove.bind @
+	@canvas.addEventListener 'mousemove', @_mouseMove
+	return

 Grid::draw = ->
-  #background
-  @con2d.fillStyle = @COLORS.BACKGROUND
-  @con2d.fillRect 0, 0, @canvas.width, @canvas.height
+	#background
+	@con2d.fillStyle = @COLORS.BACKGROUND
+	@con2d.fillRect 0, 0, @canvas.width, @canvas.height

-  #active path
-  @con2d.fillStyle = @COLORS.PATHBACKGROUND
-  @highlightedPath?.list.forEach (entry) =>
-    @con2d.fillRect entry.x * @cellSize, entry.y * @cellSize, @cellSize, @cellSize
+	#active path
+	@con2d.fillStyle = @COLORS.PATHBACKGROUND
+	@highlightedPath?.list.forEach (entry) =>
+		@con2d.fillRect entry.x * @cellSize, entry.y * @cellSize, @cellSize, @cellSize

-  #chars
-  @con2d.fillStyle = @COLORS.FONT
-  for i in [0...@playfield.width]
-    for j in [0...@playfield.height]
-      @con2d.fillText(
-        (@playfield.getAt i, j),
-        i * @cellSize + @cellSize / 2,
-        j * @cellSize + @cellSize / 2
-      )
+	#chars
+	@con2d.fillStyle = @COLORS.FONT
+	for i in [0...@playfield.width]
+		for j in [0...@playfield.height]
+			@con2d.fillText(
+				(@playfield.getAt i, j),
+				i * @cellSize + @cellSize / 2,
+				j * @cellSize + @cellSize / 2
+			)

-  #path indicators
-    for hitRegion in @hitRegions
-      @con2d.drawImage @arrowImages[hitRegion.dir], hitRegion.start.x, hitRegion.start.y
+	#path indicators
+		for hitRegion in @hitRegions
+			@con2d.drawImage @arrowImages[hitRegion.dir], hitRegion.start.x, hitRegion.start.y

-  #grid
-  @con2d.strokeStyle = @COLORS.GRID
-  @con2d.beginPath()
-  for i in [1...@playfield.width]
-    @con2d.moveTo i * @cellSize, 0
-    @con2d.lineTo i * @cellSize, @canvas.height
+	#grid
+	@con2d.strokeStyle = @COLORS.GRID
+	@con2d.beginPath()
+	for i in [1...@playfield.width]
+		@con2d.moveTo i * @cellSize, 0
+		@con2d.lineTo i * @cellSize, @canvas.height

-  for i in [1...@playfield.height]
-    @con2d.moveTo 0, i * @cellSize
-    @con2d.lineTo @canvas.width, i * @cellSize
-  @con2d.stroke()
+	for i in [1...@playfield.height]
+		@con2d.moveTo 0, i * @cellSize
+		@con2d.lineTo @canvas.width, i * @cellSize
+	@con2d.stroke()

-  return
+	return

 Grid::setListener = (@onChange) ->
-  return
+	return

 Grid:: destroy = ->
-  @canvas.removeEventListener 'mousemove', @_mouseMove
+	@canvas.removeEventListener 'mousemove', @_mouseMove

 window.viz ?= {}


fix: resolve conflict

fix: resolve conflict

--- app/models/file.coffee
+++ app/models/file.coffee
@@ -106,7 +106,6 @@
       {"value": countRiskUnknown, "color": _getComputedColor "default"}
     ]

-<<<<<<< HEAD
   unknownRiskAnalyses: Ember.computed 'analyses.@each.risk', ->
     @get("analyses").filterBy 'risk', ENUMS.RISK.UNKNOWN

@@ -148,8 +147,6 @@
   manualScanProgress: Ember.computed "analyses.@each.risk", ->
     @scanProgress ENUMS.VULNERABILITY_TYPE.MANUAL

-=======
->>>>>>> origin/358-ui-feedbacks
   isNoneStaus: (->
     status = @get 'dynamicStatus'
     status is ENUMS.DYNAMIC_STATUS.NONE


Changes to SourceProviderTemplate

Changes to SourceProviderTemplate

--- docs/SourceProviderTemplate.coffee
+++ docs/SourceProviderTemplate.coffee
@@ -8,7 +8,7 @@
   # Constants should be defined on the class itself.

   # The name of your source provider, in kebab-case.
-  @NAME = 'your_internal_source_provider_name'
+  @NAME = 'your-internal-source-provider-name'

   # The header that the user will see the repos from this source listed under.
   @DISPLAY_NAME = 'Your Source Provider Display Name'
@@ -16,10 +16,12 @@
   # The icon file for this source provider, as an absolute path - or `null` if there's no icon.
   @ICON_FILE_PATH = __dirname + '/path/to/the/icon.png'

-  # The initial endpoint to hit in order to authenticate this service provider, relative to '/plugins/source-providers/{@NAME}'.
-  # This endpoint *must* be registered in initializeAuthEndpoints().
-  # This can be null if this service provider expects to be manually configured; in this case,
-  # isAuthenticated() should always return true.
+  ###
+  The initial endpoint to hit in order to authenticate this service provider, relative to '/plugins/source-providers/{@NAME}'.
+  This endpoint *must* be registered in initializeAuthEndpoints().
+  This can be null if this service provider expects to be manually configured; in this case,
+  isAuthenticated() must always return true.
+  ###
   @AUTH_ENDPOINT = '/auth'

   ###
@@ -47,6 +49,7 @@
   @return {Boolean} true, if the request is authenticated.
   ###
   isAuthenticated: (req) ->
+
   ###
   Get a list of available repositories for the currently logged in user.
   @param {Object} user the Mongoose model for the user


Fixed typo.

Fixed typo.

--- app/assets/javascripts/app/lib/app_post/z_searchable_ajax_select.js.coffee
+++ app/assets/javascripts/app/lib/app_post/z_searchable_ajax_select.js.coffee
@@ -19,7 +19,7 @@
     if @searchResultCache[@cacheKey]
       return @onAjaxResponse( @searchResultCache[@cacheKey] )

-    # add timout for loader icon
+    # add timeout for loader icon
     clearTimeout @loaderTimeoutId
     @loaderTimeoutId = setTimeout @showLoader, 1000


Remove property calls for default ones

Remove property calls for default ones

--- app/assets/javascripts/controllers/admin_journal_user_controller.js.coffee
+++ app/assets/javascripts/controllers/admin_journal_user_controller.js.coffee
@@ -1,11 +1,7 @@
 ETahi.AdminJournalUserController = Ember.ObjectController.extend
-  resetPasswordSuccess: (->
-    false
-  ).property()
+  resetPasswordSuccess: false

-  resetPasswordFailure: (->
-    false
-  ).property()
+  resetPasswordFailure: false

   modalId: (->
     "#{@get('id')}-#{@get('username')}"


default minutes and seconds in datetime pickers to 0

--- app/assets/javascripts/modules/temporal/temporal_range_selection.js.coffee
+++ app/assets/javascripts/modules/temporal/temporal_range_selection.js.coffee
@@ -18,16 +18,22 @@
     yearEnd: current_year,
     roundTime: 'round',
     onChangeDateTime: (dp,$input) ->
-      # queryField = if $input.hasClass('temporal-range-start') then 'range-start' else 'range-stop'
-      # updateQueryModel(queryField, $input.val().replace(' ','T') + 'Z')
-    onClose: (dp,$input) ->
-      # queryField = if $input.hasClass('temporal-range-start') then 'range-start' else 'range-stop'
-      # updateQueryModel(queryField, $input.val().replace(' ','T') + 'Z') if $input.val()?.length > 0
+      # Default minutes and seconds to 00
+      datetime = $input.val().split(":")
+      datetime[1] = "00"
+      datetime[2] = "00"
+      $input.val(datetime.join(":"))
   })

   $('.temporal-recurring-picker').datetimepicker({
     format: 'm-d H:i:s',
-    className: 'recurring-datetimepicker'
+    className: 'recurring-datetimepicker',
+    onChangeDateTime: (dp,$input) ->
+      # Default minutes and seconds to 00
+      datetime = $input.val().split(":")
+      datetime[1] = "00"
+      datetime[2] = "00"
+      $input.val(datetime.join(":"))
   })

   $('.temporal-recurring-year-range').slider({


Add modal modify folder

Add modal modify folder

--- BackofficeBundle/Resources/public/coffee/mediatheque/GalleryCollectionView.coffee
+++ BackofficeBundle/Resources/public/coffee/mediatheque/GalleryCollectionView.coffee
@@ -1,7 +1,6 @@
 GalleryCollectionView = Backbone.View.extend(
   events:
     'click #none': 'clickAdd'
-    'click #none': 'clickRedirect'
   initialize: (options) ->
     @medias = options.medias
     @title = options.title
@@ -59,6 +58,5 @@
           )
   clickRedirect: (event) ->
     event.preventDefault()
-    $('.modal-title').text 'Please wait ...'
     view = new adminFormView(url: @medias.get('links')._self_folder)
 )


Dont isolate scope for forms

Dont isolate scope for forms

--- app/assets/javascripts/rails/angulate/directives/form_extensions.js.coffee
+++ app/assets/javascripts/rails/angulate/directives/form_extensions.js.coffee
@@ -20,7 +20,6 @@
   restrict: 'EAC'
   priority: 10
   require: 'form'
-  scope: true
   compile: ->
     pre: (scope, element, attrs, form) ->
       element.attr('novalidate', 'novalidate')


[#65105556] hide dropdown on second click

--- app/assets/javascripts/trade/views/search_form/multi_select_view.js.coffee
+++ app/assets/javascripts/trade/views/search_form/multi_select_view.js.coffee
@@ -9,8 +9,10 @@
   templateName: 'trade/shipments/multi_select'

   click: ->
+    wasVisible = @.$('.popup-holder01').is(':visible')
     $('.popup-holder01').hide()
-    @.$('.popup-holder01').show()
+    unless wasVisible
+      @.$('.popup-holder01').show()

 Trade.MultiSelectButton = Ember.View.extend
   tagName: 'a'


Improve display of no data values

Improve display of no data values

--- app/assets/javascripts/workbench/workbench/views/datastream/latest_view.js.coffee
+++ app/assets/javascripts/workbench/workbench/views/datastream/latest_view.js.coffee
@@ -12,14 +12,22 @@
   render: ->
     latest = _.last(@model.get("seriesData"))

-    @swapHtml(@$el, @template({
-      latest: latest.value
-      date:   new Date(latest.timestamp)
-      unit:   @model.get("unit")
-    }))
+    if latest?
+      @swapHtml(@$el, @template({
+        latest: latest.value
+        date:   new Date(latest.timestamp)
+        unit:   @model.get("unit")
+      }))
+    else
+      @swapHtml(@$el, @template({
+        latest: "No data"
+        date:   "No data"
+        unit:   ""
+      }))
     this

   update: (date, value) ->
     @swapHtml(@$("date"), date)
     @swapHtml(@$(".value"), value)
+    @swapHtml(@$(".unit"), @model.get("unit"))
     this


Fixing server run.

Fixing server run.

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -1 +1 @@
-define(['bootstrap', './bootstrap-app'], (ng) -> )+requirejs(['bootstrap', './bootstrap-app'], (ng) -> )


Fix bug that appeared in production.

Fix bug that appeared in production.

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -46,6 +46,7 @@

                 aMarker = newMarker.marker
                 aMarker._____refToSelf = newMarker
+                aMarker.infoWindow.ourStuff = val
                 self = this
                 google.maps.event.addListener aMarker, "click", () ->
                     if self._infoWindow?
@@ -61,8 +62,8 @@
                     self._infoWindow.setContent('

')
                     x = document.getElementById('x')
                     if x
-                        x.innerHTML = templates.per_item_entry(val)
-                    val.foundPlaceName = ""
+                        x.innerHTML = templates.per_item_entry(self._infoWindow.ourStuff)
+                    self._infoWindow.ourStuff.foundPlaceName = ""

     _navigateToNewBounds: () =>
         this._navTimeoutId = undefined


BF-147 Implementing scaling of route according to zoom level in XY View

--- app/assets/javascripts/view.coffee
+++ app/assets/javascripts/view.coffee
@@ -292,20 +292,20 @@

   moveActivePlane : (p) ->
     cam2d.moveActivePlane p
-    @updateRoutePosition p
+    @updateRoute()

   #FIXME: why can't I call move() from within this function?
   moveX : (x) ->
     cam2d.moveActivePlane [x, 0, 0]
-    View.updateRoutePosition [x, 0, 0]
+    View.updateRoute()

   moveY : (y) ->
     cam2d.moveActivePlane [0, y, 0]
-    View.updateRoutePosition [0, y, 0]
+    View.updateRoute()

   moveZ : (z) ->
     cam2d.moveActivePlane [0, 0, z]
-    View.updateRoutePosition [0, 0, z]
+    View.updateRoute()

   prevViewportSize : =>
     (View.camera[VIEW_3D].right - View.camera[VIEW_3D].left)         # always quadratic
@@ -353,10 +353,12 @@

   zoomIn : ->
     cam2d.zoomIn(cam2d.getActivePlane())
+    View.updateRoute()

   #todo: validation in Model
   zoomOut : ->
     cam2d.zoomOut(cam2d.getActivePlane())
+    View.updateRoute()

   setActivePlaneXY : ->
     View.setActivePlane PLANE_XY
@@ -379,10 +381,7 @@
     # calculate the global position of the rightclick
     position = [curGlobalPos[0] - 192/curZoomStep + position[0]/curZoomStep, curGlobalPos[1] - 192/curZoomStep + position[1]/curZoomStep, curGlobalPos[2]]
     unless @curIndex
-      @curIndex = 1
-      @route.geometry.vertices[0] = new THREE.Vector3(400, Game.dataSet.upperBoundary[1] - 500, 340)
-      @routeView.geometry.vertices[0] = new THREE.Vector3(400, -340, -500)
-      @particleSystem.geometry.vertices[0] = new THREE.Vector3(400, Game.dataSet.upperBoundary[1] - 500, 340)
+      @curIndex = 0
     # Translating ThreeJS' coordinate system to the preview's one
     if @curIndex < @maxRouteLen
       @route.geometry.vertices[@curIndex] = new THREE.Vector3(position[0], Game.dataSet.upperBoundary[1] - position[2], position[1])
@@ -433,7 +432,10 @@
     @route = route
     @routeView = routeView
     @particleSystem = particleSystem
-    @routeView.position = new THREE.Vector3(-400, 340, 501)
+
+    # Initializing Position
+    gPos = cam2d.getGlobalPos()
+    @routeView.position = new THREE.Vector3(-gPos[0], gPos[1], gPos[2]+1)

     particle = new THREE.Mesh(new THREE.CubeGeometry(300, 300, 300, 10, 10, 10), new THREE.MeshBasicMaterial({color: 0xff0000}))
     particle.position.x = 0
@@ -447,6 +449,12 @@
     @addGeometry VIEW_3D, particleSystem
     @addGeometry PLANE_XY, routeView

-  updateRoutePosition : (p) ->
-    @routeView.position = new THREE.Vector3(@routeView.position.x - p[0], @routeView.position.y + p[1], cam2d.getGlobalPos()[2]+1)
+  updateRoute : ->
+    gPos                = cam2d.getGlobalPos()
+    scale               = cam2d.getPlaneScalingFactor PLANE_XY
+    console.log "scale (alt): " + @routeView.scale
+    console.log "scale: " + scale
+    @routeView.scale    = new THREE.Vector3(1/scale, 1/scale, 1/scale)
+    @routeView.position = new THREE.Vector3(-gPos[0]/scale, gPos[1]/scale, gPos[2]/scale+1)
+    #@routeView.position = new THREE.Vector3(@routeView.position.x - p[0], @routeView.position.y + p[1], cam2d.getGlobalPos()[2]+1)
     @routeView.geometry.verticesNeedUpdate = true


Update object references.

Update object references.

--- src/spec/product-import.spec.coffee
+++ src/spec/product-import.spec.coffee
@@ -88,7 +88,7 @@
   ]

 sampleProductProjectionResponse = [
-  masterVariant :
+  masterVariant:
     sku: 'e'
   variants: []
 ]
@@ -215,7 +215,7 @@

     it 'should resolve list of category references and cache the result', (done) ->
       @import._resetCache()
-      spyOn(@import.client.categories, "fetch").andCallFake => Promise.resolve(sampleCategoriesResponse)
+      spyOn(@import.client.categories, "fetch").andCallFake -> Promise.resolve(sampleCategoriesResponse)
       categoryRef = { id: 'category_external_id1' }
       @import._resolveReference(@import.client.categories, 'categories', categoryRef, "externalId=\"#{categoryRef.id}\"")
       .then (result) =>
@@ -229,7 +229,7 @@
     it 'should resolve reference from cache', (done) ->
       @import._resetCache()
       @import._cache.taxCategory['defaultTax_AT'] = "tax_category_internal_id"
-      spyOn(@import.client.taxCategories, "fetch").andCallFake => Promise.resolve(sampleTaxCategoryResponse)
+      spyOn(@import.client.taxCategories, "fetch").andCallFake -> Promise.resolve(sampleTaxCategoryResponse)
       taxCategoryRef = { id: 'defaultTax_AT' }
       @import._resolveReference(@import.client.taxCategories, 'taxCategory', taxCategoryRef, "name=\"#{taxCategoryRef.id}\"")
       .then (result) =>
@@ -250,7 +250,7 @@
   describe '::_resolveProductCategories', ->

     it 'should resolve a list of categories', (done) ->
-      spyOn(@import, "_resolveReference").andCallFake => Promise.resolve("foo")
+      spyOn(@import, "_resolveReference").andCallFake -> Promise.resolve("foo")
       @import._resolveProductCategories(sampleReferenceCats)
       .then (result) =>
         expect(result.length).toBe 2
@@ -264,14 +264,14 @@

     it 'should resolve with empty array on undefined list of product category references', (done) ->
       @import._resolveProductCategories(undefined)
-      .then (result) =>
+      .then (result) ->
         expect(result).toBe undefined
         done()
       .catch done

     it 'should resolve with empty array on empty list of product category references', (done) ->
       @import._resolveProductCategories([])
-      .then (result) =>
+      .then (result) ->
         expect(result).toBe undefined
         done()
       .catch done
@@ -279,7 +279,7 @@
   describe '::_prepareNewProduct', ->

     beforeEach ->
-      spyOn(@import, "_resolveReference").andCallFake (service, refKey, ref) =>
+      spyOn(@import, "_resolveReference").andCallFake (service, refKey, ref) ->
         switch refKey
           when "productType"
             if ref then Promise.resolve(sampleProductTypeResponse.body.results[0].id) else Promise.resolve()
@@ -378,7 +378,7 @@
         ],
         version: 1

-      spyOn(@import, "_prepareNewProduct").andCallFake (prepareProduct) => Promise.resolve(prepareProduct)
+      spyOn(@import, "_prepareNewProduct").andCallFake (prepareProduct) -> Promise.resolve(prepareProduct)
       spyOn(@import.client._rest, 'POST').andCallFake (endpoint, payload, callback) ->
         callback(null, {statusCode: 200}, {})
       @import._createOrUpdate([newProduct,updateProduct],existingProducts)
@@ -399,7 +399,7 @@

       spyOn(@import, "_extractUniqueSkus").andCallThrough()
       spyOn(@import, "_prepareProductFetchBySkuQueryPredicate").andCallThrough()
-      spyOn(@import.client.productProjections,"fetch").andCallFake => Promise.resolve({body: {results: existingProducts}})
+      spyOn(@import.client.productProjections,"fetch").andCallFake -> Promise.resolve({body: {results: existingProducts}})
       spyOn(@import, "_createOrUpdate").andCallFake -> Promise.all([Promise.resolve({statusCode: 201}), Promise.resolve({statusCode: 200})])
       @import._processBatches(sampleProducts)
       .then =>


Fix double save of member on site controller.

--- app/coffee/controllers/site.coffee
+++ app/coffee/controllers/site.coffee
@@ -1,4 +1,4 @@
-# Copyright 2013 Andrey Antukh <niwi@niwi.be>
+# Copyright 2013-2014 Andrey Antukh <niwi@niwi.be>
 #
 # Licensed under the Apache License, Version 2.0 (the "License")
 # you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
     $rootScope.pageBreadcrumb = [
         [$i18next.t('common.administer-site'), null],
     ]
+
     $scope.activeTab = "data"

     $scope.languageOptions = config.languageOptions
@@ -39,9 +40,12 @@
         else if role == 'normal'
             mbr.is_owner = false
             mbr.is_staff = false
-        mbr.save().then ->
+
+        promise = mbr.save()
+        promise.then ->
             $gmFlash.info($i18next.t("admin-site.role-changed"))
-        mbr.save().then null, ->
+
+        promise.then null, ->
             mbr.revert()
             $gmFlash.warn($i18next.t("admin-site.error-changing-the-role"))


Adjust Conditional in Subject Tools

Adjust Conditional in Subject Tools

--- app/components/subject-viewer.cjsx
+++ app/components/subject-viewer.cjsx
@@ -129,7 +129,7 @@

         }

-          {if @props.workflow?.configuration?.invert_subject?
+          {if @props.workflow?.configuration?.invert_subject
             <button type="button" className="secret-button" aria-label="Invert image" title="Invert image" onClick={@toggleModification.bind this, 'invert'}>

             </button>}{' '}


fix readable class names and add role button doesnt reqire loader, because there is nothing for waiting

--- client/app/Applications/Groups.kdapplication/Views/permissionsmodal.coffee
+++ client/app/Applications/Groups.kdapplication/Views/permissionsmodal.coffee
@@ -11,15 +11,13 @@

     roles = (role.title for role in options.roles when role.title isnt 'owner')

+    addRoleDialog = null
     options.buttons or=
       "Add Role"          :
         style             : "modal-clean-gray"
         cssClass          : 'add-role'
-        loader            :
-          color           : "#444444"
-          diameter        : 12
         callback          : =>
-
+          addRoleDialog?.destroy()
           KD.getSingleton('contentPanel').addSubView addRoleDialog = new KDDialogView
             cssClass      : "add-role-dialog"
             duration      : 200
@@ -124,10 +122,14 @@
   readableText = (text)->
     dictionary =
       "JTag"        : "Tags"
-      "JGroup"      : 'Groups'
-      "JPost"       :'Posts'
-      "JVocabulary" : 'Vocabularies'
+      "JGroup"      : "Groups"
+      "JPost"       : "Posts"
+      "JVocabulary" : "Vocabularies"
       "JVM"         : "Compute"
+      "CActivity"   : "Activity"
+      "JGroupBundle": "Group Bundles"
+      "JDomain"     : "Domains"
+      "JProxyFilter": "Proxy Filters"
     return dictionary[text] or text.charAt(0).toUpperCase()+text.slice(1)

   _getCheckboxName =(module, permission, role)->


Use itemClass for new Feeder

Use itemClass for new Feeder

--- client/landing/app/Applications/Members.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Members.kdapplication/AppController.coffee
@@ -21,7 +21,7 @@

   createFeed:(view)->
     appManager.tell 'Feeder', 'createContentFeedController', {
-      itemClass          : MembersListItemView
+      itemClass             : MembersListItemView
       listControllerClass   : MembersListViewController
       limitPerPage          : 10
       help                  :
@@ -67,7 +67,7 @@
   createFeedForContentDisplay:(view, account, followersOrFollowing)->

     appManager.tell 'Feeder', 'createContentFeedController', {
-      itemClass          : MembersListItemView
+      itemClass             : MembersListItemView
       listControllerClass   : MembersListViewController
       limitPerPage          : 10
       # singleDataSource      : (selector, options, callback)=>
@@ -110,7 +110,7 @@
   createLikedFeedForContentDisplay:(view, account)->

     appManager.tell 'Feeder', 'createContentFeedController', {
-      subItemClass          : ActivityListItemView
+      itemClass             : ActivityListItemView
       listCssClass          : "activity-related"
       limitPerPage          : 8
       help                  :


Pass $cookies to app.run()

Pass $cookies to app.run()

--- app/scripts/app.coffee
+++ app/scripts/app.coffee
@@ -46,7 +46,7 @@
     return (s) ->
       return s.toString().toLowerCase().replace( /\b([a-z])/g, (ch) -> return ch.toUpperCase()))

-  .run (gettextCatalog,$rootScope) ->
+  .run (gettextCatalog, $rootScope, $cookies) ->
     $cookies.locale = 'en' unless !!$cookies.locale
     gettextCatalog.currentLanguage = $cookies.locale
     $rootScope.fields =


Hide when loading

Hide when loading

--- core/app/backbone/views/evidence/ndp_evidence_collection_view.coffee
+++ core/app/backbone/views/evidence/ndp_evidence_collection_view.coffee
@@ -86,9 +86,10 @@
       @addRegion.show new NDPAddEvidenceView
         collection: @collection
         fact_id: @collection.fact.id
-    else
-      @ui.terminator.addClass 'evidence-terminator-circle'
-      @learnMoreRegion.show new NDPLearnMoreBottomView

   _updateLoading: ->
     @$el.toggleClass 'evidence-container-loaded', !@collection.loading()
+
+    unless Factlink.Global.signed_in || @collection.loading()
+      @ui.terminator.addClass 'evidence-terminator-circle'
+      @learnMoreRegion.show new NDPLearnMoreBottomView


add route checking, not working yet...

add route checking, not working yet...

--- app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee
+++ app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee
@@ -1,8 +1,10 @@
 angular.module("admin.enterprises")
-  .controller "enterpriseCtrl", ($scope, Enterprise, PaymentMethods, ShippingMethods) ->
+  .controller "enterpriseCtrl", ($scope, $rootScope, Enterprise, PaymentMethods, ShippingMethods) ->
     $scope.Enterprise = Enterprise.enterprise
     $scope.PaymentMethods = PaymentMethods.paymentMethods
     $scope.ShippingMethods = ShippingMethods.shippingMethods
+    $scope.$on "$routeChangeStart", (event, newUrl, oldUrl) ->
+      event.preventDefault()

     for payment_method in $scope.PaymentMethods
       payment_method.selected = payment_method.id in $scope.Enterprise.payment_method_ids
@@ -32,4 +34,4 @@
       $scope.ShippingMethods.reduce (count, shipping_method) ->
         count++ if shipping_method.selected
         count
-      , 0+      , 0


WIP: Smooth scroll to checkout errors

WIP: Smooth scroll to checkout errors

--- app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee
+++ app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee
@@ -1,9 +1,10 @@
-Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout, CurrentHub) ->
+Darkswarm.controller "AccordionCtrl", ($scope, storage, $timeout, $document, CurrentHub) ->
   $scope.accordion =
     details: true
+    billing: false
     shipping: false
     payment: false
-    billing: false
+  $scope.accordionSections = ["details", "billing", "shipping", "payment"]
   storage.bind $scope, "accordion", {storeName: "accordion_#{$scope.order.id}#{CurrentHub.hub.id}#{$scope.order.user_id}"}

   $scope.show = (section)->
@@ -11,10 +12,17 @@

   $scope.$on 'purchaseFormInvalid', (event, form) ->
     # Scroll to first invalid section
-    # TODO: hide all first
-    # TODO: Use Object.keys($scope.accordion)
-    sections = ["details", "billing", "shipping", "payment"]
-    for section in sections
+    for section in $scope.accordionSections
       if not form[section].$valid
         $scope.show section
+
+        # If we call scrollTo() directly after show(), when one of the accordions above the
+        # scroll location is closed by show(), scrollTo() will scroll to the old location of
+        # the element. Putting this in a zero-length timeout is enough delay for the DOM to
+        # have updated.
+        $timeout ->
+          # Scrolling is confused by our position:fixed top bar - add an offset to scroll
+          # to the correct location, plus 5px buffer
+          offset_height = $("nav.top-bar").height() + 5
+          $document.scrollTo $("##{section}"), offset_height, 500
         break


Update vansteki.gy.coffee

Update vansteki.gy.coffee

--- scripts/vansteki.gy.coffee
+++ scripts/vansteki.gy.coffee
@@ -4,7 +4,6 @@
 	robot.hear /gy (https?:\/\/www.youtube.com\/watch\?\w=.*)/i, (msg) ->
   		msg.send u.inspect msg.match[1]
   		youtube.get msg.match[1], ((data, title) ->
-  			msg.send title
-  			msg.send data[0].title
-  			msg.send data[0].link
+  			msg.send ">" + title
+  			msg.send "<" + data[0].title + "|" + data[0].link + ">"
   		).bind(msg)


Use new selector to filter activities, code snips, status updates.

--- client/landing/app/Applications/Topics.kdapplication/ContentDisplays/Topic.coffee
+++ client/landing/app/Applications/Topics.kdapplication/ContentDisplays/Topic.coffee
@@ -32,16 +32,22 @@
       limitPerPage        : 5
       filter              :
         content           :
-          title           : 'All content'
+          title           : 'Everything'
           dataSource      : (selector, options, callback)->
             topic.fetchContentTeasers options, (err, teasers)->
               callback err, teasers
-        codeshares        :
-          title           : 'Code shares'
-          dataSource      : -> log 'just code shares'
-        developers        :
-          title           : 'Developers'
-          dataSource      : -> log 'just developers'
+        statusupdates     :
+          title           : 'Status Updates'
+          dataSource      : (selector, options, callback)->
+            selector = {targetName: 'JStatusUpdate'}
+            topic.fetchContentTeasers options, selector, (err, teasers)->
+              callback err, teasers
+        codesnippets      :
+          title           : 'Code Snippets'
+          dataSource      : (selector, options, callback)->
+            selector = {targetName: 'JCodeSnip'}
+            topic.fetchContentTeasers options, selector, (err, teasers)->
+              callback err, teasers
       sort                :
         'timestamp|new'   :
           title           : 'Latest activity'


altering job output options

altering job output options

--- server/job.coffee
+++ server/job.coffee
@@ -38,7 +38,15 @@
           return 401
         else
           job.progress = API.job.progress(job) if this.queryParams.progress
-          delete job.processes if this.queryParams.processes
+          if this.queryParams.processes
+            if typeof this.queryParams.processes is 'number'
+              try job.processes = job.processes.slice(0,this.queryParams.processes)
+            else if '-' in this.queryParams.processes
+              try job.processes = job.processes.slice(parseInt(this.queryParams.processes.split('-')[0]),parseInt(this.queryParams.processes.split('-')[1]))
+            else
+              try job.processes = job.processes.slice(0,parseInt(this.queryParams.processes))
+          else
+            delete job.processes
           return job
       else
         return 404