Link to modules in collections on the legacy site

--- src/scripts/modules/media/media.coffee
+++ src/scripts/modules/media/media.coffee
@@ -25,7 +25,7 @@
       @model = new Content({id: @uuid, page: options.page})

       @listenTo(@model, 'change:title', @updateTitle)
-      @listenTo(@model, 'change:legacy_id change:legacy_version', @updateLegacyLink)
+      @listenTo(@model, 'change:legacy_id change:legacy_version changePage', @updateLegacyLink)

     regions:
       media: '.media'
@@ -35,9 +35,19 @@
       super()

     updateLegacyLink: () ->
+      headerView = @parent.parent.regions.header.views[0]
       id = @model.get('legacy_id')
       version = @model.get('legacy_version')
-      @parent.parent.regions.header.views[0].setLegacyLink("content/#{id}/#{version}")
+
+      if @model.get('type') is 'book'
+        currentPage = @model.get('currentPage')
+        if currentPage
+          moduleID = currentPage?.get('legacy_id')
+          moduleVersion = currentPage?.get('legacy_version')
+          headerView.setLegacyLink("content/#{moduleID}/#{moduleVersion}/?collection=#{id}/#{version}")
+          return
+
+      headerView.setLegacyLink("content/#{id}/#{version}")

     onRender: () ->
       @regions.media.append(new MediaEndorsedView({model: @model}))


alert when finished (for QA only). Add details to Geordi logging.

--- app/models/experimental_subject.coffee
+++ app/models/experimental_subject.coffee
@@ -35,9 +35,9 @@
       subject = @first()
       if Experiments.ACTIVE_EXPERIMENT? && Experiments.ACTIVE_EXPERIMENT=="SerengetiInterestingAnimalsExperiment1"
         if Experiments.sources[subject.zooniverseId] == Experiments.SOURCE_INSERTED
-          AnalyticsLogger.logEvent 'insertion','specific'
+          AnalyticsLogger.logEvent 'insertion','specific',User.current?.zooniverse_id,subject.zooniverseId
         else if Experiments.sources[subject.zooniverseId] == Experiments.SOURCE_RANDOM
-          AnalyticsLogger.logEvent 'insertion','random'
+          AnalyticsLogger.logEvent 'insertion','random',User.current?.zooniverse_id,subject.zooniverseId
         @markAsSeen subject.zooniverseId
       subject.select()

@@ -72,7 +72,10 @@
           Experiments.currentParticipant = response.participant
           nextSubjectIDs = response.nextSubjectIDs
           if nextSubjectIDs? && nextSubjectIDs.length == 0
-            # TODO add some fallback code here (end of experiment)
+            Experiments.currentParticipant.cohort = Experiments.COHORT_CONTROL
+            Experiments.currentCohort = Experiments.COHORT_CONTROL
+            Experiments.currentParticipant.active = false
+            alert "You have now completed your experiment. Thanks for participating"
           else
             for subjectID in nextSubjectIDs
               do (subjectID) =>


Give the dev classifier a dark background

--- app/pages/dev-classifier/index.cjsx
+++ app/pages/dev-classifier/index.cjsx
@@ -52,6 +52,14 @@
   getDefaultProps: ->
     classification: mockData.classification

+  componentDidMount: ->
+    document.body.style.backgroundColor = '#444'
+    document.body.style.color = 'white'
+
+  componentWillUnmount: ->
+    document.body.style.backgroundColor = ''
+    document.body.style.color = ''
+
   reload: ->
     workflow = @props.classification._workflow
     firstTask = workflow.tasks[workflow.first_task]


remove css

remove css

--- app/scripts/controllers/article.coffee
+++ app/scripts/controllers/article.coffee
@@ -1,6 +1,8 @@
 'use strict'

 angular.module('xoceanApp')
-    .controller 'ArticleCtrl', ($scope, Article)->
+    .controller 'ArticleCtrl', ($scope, Article,$sce)->
         Article.get (res)->
             $scope.list = res.data
+            for item,index in $scope.list
+            	$scope.list[index].content = $sce.trustAsHtml res.data[index].content


Disallow move left, right, down while paused

--- app/scripts/controllers/main.coffee
+++ app/scripts/controllers/main.coffee
@@ -100,6 +100,7 @@
         $scope.$emit('resume')

     $scope.$on 'move_left', (event) ->
+      return unless $scope.game_info.in_progress
       block = Tetromino.get_active_block()
       return unless block
       return if block.y == 0
@@ -112,6 +113,7 @@
       block.sliding = false

     $scope.$on 'move_right', (event) ->
+      return unless $scope.game_info.in_progress
       block = Tetromino.get_active_block()
       return unless block
       return if block.y == $scope.game_info.cols - 1
@@ -124,6 +126,7 @@
       block.sliding = false

     $scope.$on 'move_down', (event) ->
+      return unless $scope.game_info.in_progress
       block = Tetromino.get_active_block()
       return unless block
       return if block.x == $scope.game_info.rows - 1


does nothing

does nothing

--- lib/runtime/evaluation.coffee
+++ lib/runtime/evaluation.coffee
@@ -20,7 +20,7 @@

   eval: ({move}={}) ->
     @withCurrentContext ({editor, mod, edpath}) =>
-      blocks.get(editor, move: true).forEach ({range, line, text, selection}) =>
+      blocks.get(editor).forEach ({range, line, text, selection}) =>
         blocks.moveNext editor, selection, range if move
         [[start], [end]] = range
         @ink.highlight editor, start, end


Use a dirty flag to keep track of when to redraw plots

--- app/scripts/natdashboardctrl.coffee
+++ app/scripts/natdashboardctrl.coffee
@@ -8,6 +8,10 @@
     # should http calls be cached
     # FIXME: should be application level setting
     cacheHttp = true
+
+    # a flag to keep track if the plots should be redrawn
+    # next time the tab is made visible
+    plotsDirty = false

     $scope.gridsterOpts = {
         margins: [10, 10],
@@ -213,11 +217,23 @@
           data = _.sortBy(data, (x) -> -x.popReach)
           leaderChart("#popReach", data, groupfield, (x) -> x.popReach)

+        plotsDirty = false
         modalSpinner.close())

     $scope.$on "gettextLanguageChanged", (e) ->
       # redraw the plots so axis labels, etc are translated
-      drawPlots()
+
+      # will only work if the tab is visible (else d3 fails)
+      if dashTabs.national.active
+        drawPlots()
+      else
+        # we have to remember to redraw the plots when the tab
+        # finally does become active
+        plotsDirty = true
+
+    $scope.$watch "dashTabs.national.active", (val) ->
+      if val and plotsDirty
+        drawPlots()

     ##########################################################################
     # Initialization code


check window.console and debug flag

check window.console and debug flag

--- js/umobi.scroller.coffee
+++ js/umobi.scroller.coffee
@@ -6,7 +6,7 @@
   //>>excludeEnd("umobiBuildExclude")
   ###
   (->
-    debug = true
+    debug = window.console and false

     u.ready ->
       # TODO: range slider need this.


bottom snap detection

bottom snap detection

--- js/umobi.scroller.coffee
+++ js/umobi.scroller.coffee
@@ -11,7 +11,7 @@
   )

   class Scroller
-    snapBoundery: 120
+    snapBoundary: 80
     snapDuration: 500
     constructor: (@element) ->

@@ -58,11 +58,18 @@
         deltaY: deltaY
         newY: newY
         contentStartOffsetY: @contentStartOffsetY
-        transform: @getCurrentTransform()
+        # transform: @getCurrentTransform()
       }

       @lastTouchY = currentY
-      newY = @snapBoundery if newY > @snapBoundery
+
+      # top boundary
+      newY = @snapBoundary if newY > @snapBoundary
+
+      # bottom boundary
+      $el = $(@element)
+      newY = - $el.height() + ($el.parent().height() - @snapBoundary) if ( $el.height() + newY + @snapBoundary ) < $el.parent().height()
+
       # @startTouchY = currentY
       # return
       @animateTo(newY)
@@ -103,10 +110,18 @@
       @element.style.webkitTransform = 'translate3d(0,' + newY + 'px, 0)'
       @contentOffsetY = newY

+    overBottomSnapLimit: (newY) ->
+      contentHeight = @getElementHeight(@element)
+      parentHeight  = @getElementHeight(@element.parentNode)
+      # console.log 'overBottomSnapLimit', (parentHeight - (contentHeight + newY)) , @snapBoundary
+      return (parentHeight - (contentHeight + newY)) >= @snapBoundary
+
     shouldStartMomentum: ->
       m = @calculateMomentum()
       return false if m.velocity < 1 and m.newY > 0
-      # return true if m.newY > @snapBoundery
+      return false if @overBottomSnapLimit(m.newY)
+
+      # return true if m.newY > @snapBoundary
       # @lastTouchY
       return true

@@ -134,12 +149,19 @@
         name = 'snaptobounds' + (@animationIndex++)
         frames = []
         time = m.time * 0.6
-        newY = if m.newY > @snapBoundery then @snapBoundery else m.newY
-        frames.push { time: time * 0.5, css: 'translate3d(0,' + newY + 'px,0)' }
-        frames.push { time: time, css: 'translate3d(0,' + 0 + 'px,0)' }
+        newY = if m.newY > @snapBoundary then @snapBoundary else m.newY
+        frames.push {
+          time: time * 0.2
+          transform: 'translate3d(0,' + newY + 'px,0)'
+        }
+        frames.push {
+          time: time
+          transform: 'translate3d(0,' + 0 + 'px,0)'
+        }
+
         framecss = @generateCSSKeyframes(frames,name,time)
         @globalStyleSheet.insertRule(framecss, 0)
-        @element.style.webkitAnimation = name + " " + time + "ms linear both"
+        @element.style.webkitAnimation = name + " " + time + "ms cubic-bezier(0.33,0.66,0.66,1)"
         @element.style.webkitAnimationPlayState = name ? "running" : "paused"
         console.log 'Playing snaptobounds animation', framecss if console.log
         normalEnd = (e) =>
@@ -172,8 +194,16 @@
         # Set the element transform to where it is right now.
         @animateTo(transform.m42)

+    getElementHeight: (el) -> parseInt(window.getComputedStyle(el).height)
+
     snapToBounds: () ->
-      @cubicBezierAnimateTo(@snapDuration,0)
+      offsetY = @getContentOffsetY()
+      if @overBottomSnapLimit(offsetY)
+        contentHeight = @getElementHeight(@element)
+        parentHeight  = @getElementHeight(@element.parentNode)
+        @cubicBezierAnimateTo(@snapDuration, (parentHeight - contentHeight) )
+      else
+        @cubicBezierAnimateTo(@snapDuration,0)

     # keyframes [array]
     # name [string]
@@ -183,7 +213,10 @@
       lines = [ '@-webkit-keyframes ' + name + ' {' ]
       keyframes.forEach (keyframe) ->
         percent = (keyframe.time / time) * 100
-        frame = Math.floor(percent) + '% { -webkit-transform: ' + keyframe.css + ';' + '}'
+        frame = Math.floor(percent) + '% { '
+        frame += '-webkit-transform: ' + keyframe.transform + ';'
+        frame += '-webkit-transition: ' + keyframe.transition + ';' if keyframe.transition
+        frame += '}'
         # D&&D(frame)
         lines.push frame
       lines.push '}'


Translate (index, offset) to position

Translate (index, offset) to position

--- src/trix/models/splittable_list.coffee
+++ src/trix/models/splittable_list.coffee
@@ -109,6 +109,16 @@
       currentPosition = nextPosition
     index: null, offset: null

+  findPositionAtIndexAndOffset: (index, offset) ->
+    position = 0
+    for object, currentIndex in @objects
+      if currentIndex < index
+        position += object.getLength()
+      else if currentIndex is index
+        position += offset
+        break
+    position
+
   getLength: ->
     length = 0
     length += object.getLength() for object in @objects


deploy to gh-pages

deploy to gh-pages

--- gulp.coffee
+++ gulp.coffee
@@ -73,10 +73,7 @@
 		gutil.log stderr if stderr

 	exec "git commit -am 'deploy to gh-pages'", logger
-
-	#gulp.commit('Deploy to gh-pages', args:'-a')
-
-	#git subtree push --prefix dist dist gh-pages
+	exec "subtree push --prefix dist dist gh-pages", logger

 ###
 	Running "gulp" in command line will automatically setup src watching + live reload


Using local dir instead of temp

Using local dir instead of temp

--- build/Gruntfile.coffee
+++ build/Gruntfile.coffee
@@ -1,32 +1,18 @@
-temp = require 'temp'
 path = require 'path'
-fs = require 'fs'
+fs = require 'fs-extra'
 _s = require 'underscore.string'

 module.exports = (grunt) ->
   grunt.loadTasks('tasks')

-  if !!grunt.option('workDir')
-    workDir = grunt.option('workDir')
-  else
-    # temp.track()
-    if process.platform is 'win32'
-      affixes =
-        prefix: 'particle-dev'
-        suffix: ''
-        dir: process.cwd().split(path.sep)[0]
-      workDir = temp.mkdirSync affixes
-    else
-      workDir = temp.mkdirSync 'particle-dev'
+  appName = 'Particle Dev'
+  workDir = path.join(__dirname, '..', 'dist', 'atom-work-dir')
+  buildDir = path.join(__dirname, '..', 'dist', process.platform)

-  grunt.log.writeln '(i) Work dir is ' + workDir

-  if grunt.option('showWorkDir')
-    cp = require 'child_process'
-    cp.exec 'open ' + workDir
-
-  appName = 'Particle Dev'
-  buildDir = path.join(__dirname, '..', 'dist', process.platform)
+  if fs.existsSync(workDir) && !!grunt.option('keepAtomWorkDir')
+    fs.removeSync(workDir)
+  fs.ensureDirSync(workDir)

   # Get Atom Version from .atomrc
   atomrc = fs.readFileSync(path.join(__dirname, '..', '.atomrc')).toString()


Initialize and set focus on add column form

--- app/assets/javascripts/erd/erd.js.coffee
+++ app/assets/javascripts/erd/erd.js.coffee
@@ -221,6 +221,7 @@
       .show()
       .find('a.cancel')
       .show()
+      .end()
       .find('input[name=type]')
       .val('string')
       .end()


More robust task showing on taskboard

More robust task showing on taskboard

--- app/coffee/modules/taskboard/main.coffee
+++ app/coffee/modules/taskboard/main.coffee
@@ -131,7 +131,8 @@
                     @scope.usTasks[us.id][status.id] = []

             for task in @scope.tasks
-                @scope.usTasks[task.user_story][task.status].push(task)
+                if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]?
+                    @scope.usTasks[task.user_story][task.status].push(task)

             return tasks


Simulate All button: includes session limit and mirror match info

--- app/views/ladder/LadderView.coffee
+++ app/views/ladder/LadderView.coffee
@@ -215,6 +215,8 @@
         originalLevelID: @level.get('original'),
         levelMajorVersion: 0,
         leagueID: @leagueID
+        mirrorMatch: @level.get('mirrorMatch') ? false
+        sessionLimit: 750
       type: 'POST'
       parse: true
       success: (res)->


cache-control: no-cache header set

cache-control: no-cache header set

--- rest_controller.coffee
+++ rest_controller.coffee
@@ -157,7 +157,7 @@
       JSONUtils.renderTemplate models, template, options, callback

   setHeaders: (req, res, next) ->
-    res.header('Cache-Control', 'no-cache')
+    res.header('cache-control', 'no-cache')
     next()

   _call: (fn) =>


add french translator, AminSai

add french translator, AminSai

--- app/views/contribute/DiplomatView.coffee
+++ app/views/contribute/DiplomatView.coffee
@@ -56,7 +56,7 @@
     'zh-HANT': ['Adam23', 'gintau', 'shuwn']      # 繁體中文, Chinese (Traditional)
     'zh-WUU-HANS': []  # 吴语, Wuu (Simplified)
     'zh-WUU-HANT': ['benojan']  # 吳語, Wuu (Traditional)
-    fr: ['Anon', 'Armaldio', 'ChrisLightman', 'Elfisen', 'Feugy', 'MartinDelille', 'Oaugereau', 'Xeonarno', 'dc55028', 'jaybi', 'pstweb', 'veritable', 'xavismeh', 'CatSkald']             # français, French
+    fr: ['AminSai', 'Anon', 'Armaldio', 'ChrisLightman', 'Elfisen', 'Feugy', 'MartinDelille', 'Oaugereau', 'Xeonarno', 'dc55028', 'jaybi', 'pstweb', 'veritable', 'xavismeh', 'CatSkald']             # français, French
     ja: ['Coderaulic', 'g1itch', 'kengos', 'treby']             # 日本語, Japanese
     ar: ['5y', 'ahmed80dz']             # العربية, Arabic
     'pt-BR': ['Bia41', 'Gutenberg Barros', 'Kieizroe', 'Matthew Burt', 'brunoporto', 'cassiocardoso', 'jklemm', 'Arkhad']        # português do Brasil, Portuguese (Brazil)


Render the date with the timezone. Related to #1269

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -2767,7 +2767,7 @@
             else if value_type is 'object' and value.$reql_type$ is 'TIME' and value.epoch_time?
                 return @template_json_tree.span
                     classname: 'jt_date'
-                    value: new Date(value.epoch_time*1000)
+                    value: @date_to_string(value)
             else if value_type is 'object'
                 sub_keys = []
                 for key of value
@@ -3006,8 +3006,8 @@
                     data['value'] = '[ ... ]'
                     data['data_to_expand'] = JSON.stringify(value)
             else if value_type is 'object' and value.$reql_type$ is 'TIME' and value.epoch_time?
-                data['value'] = (new Date(value.epoch_time*1000)).toString()
-                data['classname'] = 'jta_date'
+                data['value'] = @date_to_string(value)
+                data['classname'] = 'jta_date'
             else if value_type is 'object'
                 data['value'] = '{ ... }'
                 data['is_object'] = true
@@ -3025,6 +3025,31 @@
                 data.value = if value is true then 'true' else 'false'

             return data
+
+        date_to_string: (date) =>
+            if date.timezone?
+                timezone = date.timezone
+
+                # Extract data from the timezone
+                timezone_array = date.timezone.split(':')
+                sign = timezone_array[0][0] # Keep the sign
+                timezone_array[0] = timezone_array[0].slice(1) # Remove the sign
+
+                # Save the timezone in minutes
+                timezone_int = (parseInt(timezone_array[0])*60+parseInt(timezone_array[1]))*60
+                if sign is '-'
+                    timezone_int = -1*timezone_int
+                # Add the user local timezone
+                timezone_int += (new Date()).getTimezoneOffset()*60
+            else
+                timezone = '00:00'
+                timezone_int = (new Date()).getTimezoneOffset()*60
+
+            # Tweak epoch and create a date
+            raw_date_str = (new Date((date.epoch_time+timezone_int)*1000)).toString()
+
+            # Remove the timezone and replace it with the good one
+            return raw_date_str.slice(0, raw_date_str.indexOf('GMT')+3)+timezone

         # Expand a JSON object in a table. We just call the @json_to_tree
         expand_tree_in_table: (event) =>


Horizontally stack KPI chart legends to better use vertical space

--- app/core/d3_utils.coffee
+++ app/core/d3_utils.coffee
@@ -23,6 +23,7 @@
   yAxisWidth = 40
   containerWidth = $(containerSelector).width() unless containerWidth
   containerHeight = $(containerSelector).height()
+  leftKeyWidth = 0

   yScaleCount = 0
   yScaleCount++ for line in chartLines when line.showYScale
@@ -72,7 +73,7 @@

     if line.showYScale
       # y-Axis
-      lineColor = if yScaleCount > 1 then line.lineColor else 'black'
+      lineColor = if yScaleCount > 1 then line.lineColor else 'black'
       yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
       yAxis = d3.svg.axis()
         .scale(yRange)
@@ -90,19 +91,23 @@
       currentYScale++

     # Key
+    currentKeyLine = Math.floor currentLine / 2
+    currentKeyXOffset = margin + (if currentLine % 2 then Math.max(leftKeyWidth + margin + 40, containerWidth / 2) else 0)
+    currentKeyYOffset = margin + height + xAxisHeight + keyHeight * currentKeyLine
     svg.append("line")
-      .attr("x1", margin)
-      .attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
-      .attr("x2", margin + 40)
-      .attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
+      .attr("x1", currentKeyXOffset)
+      .attr("y1", currentKeyYOffset + keyHeight / 2)
+      .attr("x2", currentKeyXOffset + 40)
+      .attr("y2", currentKeyYOffset + keyHeight / 2)
       .attr("stroke", line.lineColor)
       .attr("class", "key-line")
-    svg.append("text")
-      .attr("x", margin + 40 + 10)
-      .attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
+    what = svg.append("text")
+      .attr("x", currentKeyXOffset + 40 + 10)
+      .attr("y", currentKeyYOffset + (keyHeight + 10) / 2)
       .attr("fill", line.lineColor)
       .attr("class", "key-text")
       .text(line.description)
+      .each((d, i) -> if currentLine % 2 is 0 then leftKeyWidth = Math.max(leftKeyWidth, @getComputedTextLength()))

     pointRadius = line.pointRadius ? 2
     # Path and points


Clean code

Clean code

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1534,14 +1534,13 @@
                 if cursor?
                     @saved_data.cursor = @cursor

-                debugger
                 #TODO Check for empty array?
                 if cursor?.hasNext?
                     @cursor = cursor
                     if cursor.hasNext() is true
-                        cursor.next @get_result_callback
+                        @cursor.next @get_result_callback
                     else
-                        @get_result_callback()
+                        @get_result_callback() # Display results
                 else
                     # Save the last executed query and the last displayed results
                     @current_results = cursor


Allow outdated reads for table counts

Allow outdated reads for table counts

--- admin/static/coffee/tables/table.coffee
+++ admin/static/coffee/tables/table.coffee
@@ -24,7 +24,7 @@
             query =
                 r.db(system_db).table('server_config').count().do( (num_servers) ->
                     r.db(system_db).table('table_status').get(this_id).do((table) ->
-                        master_down = table('status')('ready_for_reads').not()
+                        master_down = table('status')('ready_for_outdated_reads').not()
                         r.branch(
                             table.eq(null),
                             null,
@@ -40,7 +40,7 @@
                                 num_replicas_per_shard: table("shards").map((shard) -> shard('replicas').count()).max()
                                 # Here we check if the table is available, otherwise table.info() will bomb
                                 distribution: r.branch(master_down, [], r.db(table('db'))
-                                    .table(table('name')) \
+                                    .table(table('name'), useOutdated: true) \
                                     .info()('doc_count_estimates')\
                                     .do((doc_counts) ->
                                         doc_counts.map(r.range(doc_counts.count()),
@@ -49,11 +49,13 @@
                                                 id: position
                                             ).coerceTo('array')))
                                 # Again, we must check if the table is available
-                                total_keys: r.branch(master_down, null, r.db(table('db')).table(table('name')).info()('doc_count_estimates').sum())
+                                total_keys: r.branch(master_down, null,
+                                    r.db(table('db')).table(table('name'), useOutdated: true).info()('doc_count_estimates').sum())
                                 status: table('status')
                                 shards_assignments: r.db(system_db).table('table_config').get(this_id)("shards").indexesOf( () -> true ).map (position) ->
                                     id: position.add(1)
-                                    num_keys: r.branch(master_down, 'N/A', r.db(table('db')).table(table('name')).info()('doc_count_estimates')(position))
+                                    num_keys: r.branch(master_down, 'N/A', r.db(table('db')).table(table('name'),
+                                        useOutdated: true).info()('doc_count_estimates')(position))
                                     primary:
                                         id: r.db(system_db).table('server_config').filter({name: r.db(system_db).table('table_config').get(this_id)("shards").nth(position)("primary_replica")}).nth(0)("id")
                                         name: r.db(system_db).table('table_config').get(this_id)("shards").nth(position)("primary_replica")


fixed restart problem

fixed restart problem

--- modules/http/lib/Context.coffee
+++ modules/http/lib/Context.coffee
@@ -69,6 +69,8 @@
                     ##
                     if @data.route
                         @delegate '_addRoute', @data.route, (req, res, next)=>
+                            if @_status.isSuspended || @_status.isStopped
+                                return next()

                             req.uri = req.uri.replace @data.route, '/'
                             @_handleRequest req, res, next


Fixing modals.js.coffee to actually work

[#37953317]

--- app/assets/javascripts/modals.js.coffee
+++ app/assets/javascripts/modals.js.coffee
@@ -8,7 +8,7 @@
   $modal_container = $('#modal-container')

   # Handle modal links with the data-remote attribute
-  $.on 'ajax:success', 'a[data-remote]', (xhr, data, status) ->
+  $(document).on 'ajax:success', 'a[data-remote]', (xhr, data, status) ->
     $modal
       .html(data)
       .prepend($modal_close)
@@ -16,7 +16,7 @@
       .show()
     $modal_container.show();

-  $.on 'click', '#modal .close', ->
+  $(document).on 'click', '#modal .close', ->
     $modal_container.hide()
     $modal.hide()
     false


reopen js

reopen js

--- app/assets/javascripts/questions.coffee
+++ app/assets/javascripts/questions.coffee
@@ -5,11 +5,18 @@
 $(document).on('ready page:load', () ->

   $('a[data-remote].close-question').on('ajax:success', (ev, data, status, xhr) ->
-    $('div.comments-container').before('

Question Closed

This
-      question was closed by ' + data['closed_by'] + '. New answers cannot be added.

')
+    $('div.comments-container').before('

Question Closed

+
This question was closed by ' + data['closed_by'] + '. New answers cannot be added.

')
     QPixel.createNotification('success', 'This question has been closed.', $(this))
   ).on('ajax:error', (ev, xhr, status, error) ->
     QPixel.createNotification('danger', 'Failed: ' + JSON.parse(xhr.responseText)['message'], $(this))
   )

+  $('a[data-remote].reopen-question').on('ajax:success', (ev, data, status, xhr) ->
+    $('div.question-closed').remove()
+    QPixel.createNotification('success', 'This question has been reopened.', $(this))
+  ).on('ajax:error', (ev, xhr, status, error) ->
+    QPixel.createNotification('danger', 'Failed: ' + JSON.parse(xhr.responseText)['message'], $(this))
+  )
+
 )


Create a individual method to fetch only one app metadata at once

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -67,6 +67,35 @@
         else
           callback? err, apps

+  fetchAppFromFs:(appName, cb)->
+
+    manifests = @constructor.manifests
+    appsPath = "/home/#{KD.nick()}/Applications/"
+    suffix   = ".kdapp/manifest.json"
+
+    manifest = FSHelper.createFileFromPath "#{appsPath}#{appName}#{suffix}"
+    manifest.fetchContents (err, response)=>
+      warn err  if err
+      return cb null  if err
+      return cb null  unless response
+
+      try
+        manifest = JSON.parse response
+        manifests["#{manifest.name}"] = manifest
+      catch e
+        warn "Manifest file is broken:", e
+        return cb null
+
+      @putAppsToAppStorage manifests
+      cb null
+
+  fetchIndividualAppsFromFS:(apps, callback)->
+
+    stack = []
+    apps.forEach (app)=>
+      stack.push (cb)=> @fetchAppFromFs app, cb
+    async.parallel stack, callback
+
   fetchAppsFromFs:(cb)->

     @_fetchQueue.push cb  if cb
@@ -81,32 +110,10 @@
         callback()  for callback in @_fetchQueue
         @_fetchQueue = []
       else
-        apps  = []
-        stack = []
-
-        files.forEach (file)->
-          if /\.kdapp$/.test(file.name) and file.type is 'folder'
-            apps.push file
-
-        apps.forEach (app)=>
-          stack.push (cb)=>
-            manifest = FSHelper.createFileFromPath "#{app.path}/manifest.json"
-            manifest.fetchContents (err, response)->
-              # shadowing the error is intentional here
-              # to not to break the result of the stack
-              cb null, response
-
-        manifests = @constructor.manifests
-        async.parallel stack, (err, result)=>
-          result.forEach (rawManifest)->
-            if rawManifest
-              try
-                manifest = JSON.parse rawManifest
-                manifests["#{manifest.name}"] = manifest
-              catch e
-                console.warn "Manifest file is broken", e
-          @putAppsToAppStorage manifests
-          callback null, manifests  for callback in @_fetchQueue
+        apps = @filterAppsFromFileList files
+        @fetchIndividualAppsFromFS apps, (result)=>
+          for callback in @_fetchQueue
+            callback null, @constructor.manifests
           @_fetchQueue = []
     , ->
       warn msg = "Timeout reached for kite request"
@@ -175,7 +182,9 @@
     @appStorage.reset()
     @appStorage.setValue 'shortcuts', defaultShortcuts, callback

-  putAppsToAppStorage:(apps)->
+  putAppsToAppStorage:(apps, callback = noop)->
+    apps or= @constructor.manifests
+    @appStorage.setValue 'apps', apps, callback

     @appStorage.setValue 'apps', apps


Components: Sidebar: Render stacks by stack type

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -65,17 +65,37 @@
           machine={machine} />

+  renderStack: (stack) ->
+    <SidebarStackSection
+      key={stack.get '_id'}
+      previewCount={PREVIEW_COUNT}
+      selectedId={@state.selectedThreadId}
+      stack={stack}
+      machines={stack.get 'machines'}/>
+
+
+
   renderStacks: ->

     stackSections = []
-    @state.stacks.toList().map (stack) =>
-      stackSections.push \
-        <SidebarStackSection
-          key={stack.get '_id'}
-          previewCount={PREVIEW_COUNT}
-          selectedId={@state.selectedThreadId}
-          stack={stack}
-          machines={stack.get 'machines'}/>
+    stackList     =
+      koding      : []
+      managed     : []
+
+    @state.stacks.toList().map (stack) ->
+      provider = if stack.get('title').toLowerCase() is 'managed vms'
+      then 'managed'
+      else 'koding'
+
+      stackList[provider].push stack
+
+    # Render stacks of koding as first.
+    stackList.koding.forEach (stack) =>
+      stackSections.push @renderStack stack
+
+    # Now render stack of managed vms as last
+    stackList.managed.forEach (stack) =>
+      stackSections.push @renderStack stack

     return stackSections


compute/storage: emit change events when set is called

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/providers/computestorage.coffee
+++ client/app/lib/providers/computestorage.coffee
@@ -112,6 +112,9 @@
     return this  if isGroupDisabled()

     @storage[type] = data
+
+    @emit 'change', { operation: 'set', type, data }
+    @emit "change:#{type}", { operation: 'set', data }

     return this


compute/connect: fix method name

compute/connect: fix method name

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/providers/connectcompute.coffee
+++ client/app/lib/providers/connectcompute.coffee
@@ -9,7 +9,7 @@
 makeState = (config, props) ->

   unless config.storage
-    console.warning \
+    console.warn \
       'You need to specify the requirements via `storage` config option.'
     return {}


Improve resource loading on issues detail.

--- app/coffee/modules/issues/detail.coffee
+++ app/coffee/modules/issues/detail.coffee
@@ -56,15 +56,20 @@

         promise = @.loadInitialData()

-        promise.then () =>
+        # On Success
+        promise.then =>
             @appTitle.set(@scope.issue.subject + " - " + @scope.project.name)

-        promise.then null, ->
-            console.log "FAIL" #TODO
-
-        @scope.$on "attachment:create", @loadHistory
-        @scope.$on "attachment:edit", @loadHistory
-        @scope.$on "attachment:delete", @loadHistory
+        # On Error
+        promise.then null, (xhr) =>
+            if xhr and xhr.status == 404
+                @location.path("/not-found")
+                @location.replace()
+            return @q.reject(xhr)
+
+        @scope.$on("attachment:create", => @.loadHistory())
+        @scope.$on("attachment:edit", => @.loadHistory())
+        @scope.$on("attachment:delete", => @.loadHistory())

     loadProject: ->
         return @rs.projects.get(@scope.projectId).then (project) =>
@@ -102,8 +107,8 @@

     loadHistory: =>
         return @rs.issues.history(@scope.issueId).then (history) =>
-            _.each history, (historyResult) ->
-                #If description was modified take only the description_html field
+            for historyResult in history
+                # If description was modified take only the description_html field
                 if historyResult.values_diff.description?
                     historyResult.values_diff.description = historyResult.values_diff.description_diff

@@ -124,15 +129,11 @@
             @scope.issueId = data.issue
             return data

-        promise.then null, =>
-            @location.path("/not-found")
-            @location.replace()
-
         return promise.then(=> @.loadProject())
-                      .then(=> @.loadUsersAndRoles())
-                      .then(=> @.loadIssue())
-                      .then(=> @.loadAttachments(@scope.issueId))
-                      .then(=> @.loadHistory())
+                      .then(=> @q.all([@.loadUsersAndRoles(),
+                                       @.loadIssue(),
+                                       @.loadAttachments(@scope.issueId),
+                                       @.loadHistory()]))

     block: ->
         @rootscope.$broadcast("block", @scope.issue)


Add mailcheck hint

Add mailcheck hint

--- desktop/apps/auth/client/auth.coffee
+++ desktop/apps/auth/client/auth.coffee
@@ -4,6 +4,8 @@
 { parse } = require 'url'
 { API_URL } = require('sharify').data
 Form = require '../../../components/mixins/form.coffee'
+Mailcheck = require '../../../components/mailcheck/index.coffee'
+mediator = require '../../../lib/mediator.coffee'
 LoggedOutUser = require '../../../models/logged_out_user.coffee'

 module.exports.SignUpView = class SignUpView extends Backbone.View
@@ -14,6 +16,10 @@

   initialize: ->
     @user = new LoggedOutUser
+    @initializeMailcheck()
+
+  initializeMailcheck: ->
+    Mailcheck.run('#js-mailcheck-input-modal', '#js-mailcheck-hint-modal', false)

   onRegisterSuccess: =>
     window.location.href = @afterAuthHref


client/home: update billing pane component

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/home/lib/billing/components/pane.coffee
+++ client/home/lib/billing/components/pane.coffee
@@ -1,32 +1,37 @@
 React = require 'react'
+{ connect } = require 'react-redux'

-SubscriptionSection = require './subscriptionsection'
+SubscriptionSection = require './subscriptioncontainer'
 PaymentSection = require './paymentsectioncontainer'

+customer = require 'app/redux/modules/payment/customer'
+subscription = require 'app/redux/modules/payment/subscription'
+bongo = require 'app/redux/modules/bongo'

-module.exports = BillingPane = (props) ->

-  { subscriptionTitle, pricePerSeat, teamSize,
-    endsAt, freeCredit, isSurveyTaken, isEmailVerified, isTrial
-    onClickTakeSurvey, onClickPricingDetails, onClickViewMembers } = props
+class BillingPane extends React.Component

-

-    <SubscriptionSection
-      subscriptionTitle={subscriptionTitle}
-      pricePerSeat={pricePerSeat}
-      teamSize={teamSize}
-      endsAt={endsAt}
-      freeCredit={freeCredit}
-      isSurveyTaken={isSurveyTaken}
-      isEmailVerified={isEmailVerified}
-      isTrial={isTrial}
-      onClickTakeSurvey={onClickTakeSurvey}
-      onClickPricingDetails={onClickPricingDetails}
-      onClickViewMembers={onClickViewMembers} />
-    <PaymentSection />
-

+  constructor: (props) ->
+    super props
+    @state = { loading: off }
+
+  render: ->
+
+

+      <SubscriptionSection loading={@state.loading} />
+      <PaymentSection loading={@state.loading} />
+

 BillingPane.defaultProps =
   onClickViewMembers: ->

+
+mapDispatchToProps = (dispatch) ->
+  return {
+    onClickTakeSurvey: ->
+    onClickPricingDetails: ->
+    onClickViewMembers: ->
+  }
+
+module.exports = connect(null, mapDispatchToProps)(BillingPane)


Add route info for Inbox.kdapp

Add route info for Inbox.kdapp

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -230,6 +230,7 @@
       '/:name?/Account'                 : createSectionHandler 'Account'
       '/:name?/Demos'                   : createSectionHandler 'Demos'
       '/:name?/Dashboard'               : createSectionHandler 'Dashboard'
+      '/:name?/Inbox'                   : createSectionHandler 'Inbox'

       # group dashboard
       # '/:name?/Dashboard'               : (routeInfo, state, route)->
@@ -299,8 +300,8 @@
           email = decodeURIComponent email
           token = decodeURIComponent token
           (
-            if opt is 'email'
-            then KD.remote.api.JMail
+            if opt is 'email'
+            then KD.remote.api.JMail
             else KD.remote.api.JMailNotification
           ).unsubscribeWithId token, email, opt, (err, content)=>
             if err or not content


code cleanup

code cleanup

disambiguate var names; whitespace

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -52,9 +52,9 @@

   stripTemplate =(str, konstructor)->
     {slugTemplate} = konstructor
-    slugRe = /^(.+)?(#\{slug\})(.+)?$/
-    re = RegExp slugTemplate.replace slugRe, (tmp, begin, slug, end)->
-      "^#{begin ? ''}(.*)#{end ? ''}$"
+    slugStripPattern = /^(.+)?(#\{slug\})(.+)?$/
+    re = RegExp slugTemplate.replace slugStripPattern,
+      (tmp, begin, slug, end)-> "^#{begin ? ''}(.*)#{end ? ''}$"
     str.match(re)?[1]

   handleNotFound:(route)->
@@ -130,12 +130,12 @@
   getRoutes =->
     mainController = KD.getSingleton 'mainController'

-    goToContent = createLinks(
+    nouns = createLinks(
       'Activity Apps Groups Members Topics'
       (sec)=> @createContentDisplayHandler sec
     )

-    goToSection = createLinks(
+    content = createLinks(
       'Account Activity Apps Groups Members StartTab Topics'
       (sec)-> ({params:{name}, query})-> @go sec, name, query
     )
@@ -153,18 +153,18 @@
       '/:name?/Recover'   : ({params:{name}})-> mainController.doRecover name

       # nouns
-      '/:name?/Groups'                  : goToSection.Groups
-      '/:name?/Activity'                : goToSection.Activity
-      '/:name?/Members'                 : goToSection.Members
-      '/:name?/Topics'                  : goToSection.Topics
-      '/:name?/Develop'                 : goToSection.StartTab
-      '/:name?/Apps'                    : goToSection.Apps
-      '/:name?/Account'                 : goToSection.Account
+      '/:name?/Groups'                  : nouns.Groups
+      '/:name?/Activity'                : nouns.Activity
+      '/:name?/Members'                 : nouns.Members
+      '/:name?/Topics'                  : nouns.Topics
+      '/:name?/Develop'                 : nouns.StartTab
+      '/:name?/Apps'                    : nouns.Apps
+      '/:name?/Account'                 : nouns.Account

       # content displays:
-      '/:name?/Topics/:topicSlug'       : goToContent.Topics
-      '/:name?/Activity/:activitySlug'  : goToContent.Activity
-      '/:name?/Apps/:appSlug'           : goToContent.Apps
+      '/:name?/Topics/:topicSlug'       : content.Topics
+      '/:name?/Activity/:activitySlug'  : content.Activity
+      '/:name?/Apps/:appSlug'           : content.Apps

       '/recover/:recoveryToken': ({params:{recoveryToken}})->
         mainController.appReady =>


get rid of !!! emoticon, which is never right!!!


added andrew to lefay

added andrew to lefay

--- scripts/name-expander.coffee
+++ scripts/name-expander.coffee
@@ -9,8 +9,9 @@
 jamie = "Jamie Winsor"
 jesse = "Jesse Howarth"
 kyle = "kallan"
+andrew = "Andrew Garson"

-dert_mermbers = ["CDERCKERSERN", "MAHCHERLERVER", "JERS HERERTH", "JKERL", "JERMAH WERNSER", "CHIN YERN", "KERLERN"]
+dert_mermbers = ["CDERCKERSERN", "MAHCHERLERVER", "JERS HERERTH", "JKERL", "JERMAH WERNSER", "CHIN YERN", "KERLERN", "ERNDRERW GERSERN"]

 merlin_remote_members = [
   cliff,
@@ -29,7 +30,8 @@
   josiah,
   jamie,
   chong,
-  kyle
+  kyle,
+  andrew
 ]

 chef_repo_mergers = [


added ctrytten to noc_members

added ctrytten to noc_members

--- scripts/name-expander.coffee
+++ scripts/name-expander.coffee
@@ -14,7 +14,7 @@
 trotter = "Trotter Cashion"
 afeng = "Aaron Feng"

-noc_members = ["Carter1", "mrobinson", "jdowns", "jdowns1", "jdowns2", "jtramullas", "nstuhring"]
+noc_members = ["Carter1", "mrobinson", "jdowns", "jdowns1", "jdowns2", "jtramullas", "nstuhring", "ctrytten"]

 dert_mermbers = ["CDERCKERSERN", "MAHCHERLERVER", "JERS HERERTH", "JKERL", "JERMAH WERNSER", "CHIN YERN", "KERLERN", "ERNDRERW GERSERN", "JERSTEN CERMPBELL"]


Reload current room after user registration

--- packages/rocketchat-ui-login/username/username.coffee
+++ packages/rocketchat-ui-login/username/username.coffee
@@ -52,3 +52,5 @@
 			RocketChat.Button.reset(button)
 			instance.username.set(username)
 			RocketChat.callbacks.run('usernameSet')
+			RoomManager.closeAllRooms()
+			FlowRouter._current.route.callAction(FlowRouter._current)


Pass ids as params to ajax to make it easier to debug it in chrome

--- assets/scripts/lib/travis/adapter.coffee
+++ assets/scripts/lib/travis/adapter.coffee
@@ -3,10 +3,10 @@
     Travis.ajax.ajax(url, method || 'get', data: params)

   findMany: (klass, records, ids) ->
-    url = @buildURL(klass) + '?' + ids.map( (id) -> "ids[]=#{id}" ).join('&')
+    url = @buildURL(klass)

     self = this
-    @ajax(url).then (data) ->
+    @ajax(url, ids: ids).then (data) ->
       self.didFindMany(klass, records, data)

   didFindMany: (klass, records, data) ->


fileserver

fileserver

--- components/backend/modules/files/server.coffee
+++ components/backend/modules/files/server.coffee
@@ -43,14 +43,14 @@
         if fs.existsSync(dir+title) is true
           title = title.replace /\.(?=[^.]*$)/, "_"+Date.now()+"_copy."
           file.setFieldValue "title", title
-        copyImages file, true # move = true, only moving
+        file = copyImages file, true # move = true, only moving
+        file.setFieldValue "link", title
       file.save ->
-        console.log("saveFile")
+        console.log("saveFile", file)
         req.io.broadcast 'updateCollection', cfg.collectionName

   app.post "/uploadFile", auth, (req,res)->
-    console.log("uploadFile")
     form = new multiparty.Form
     form.parse req, (err, fields, files)->
       if err then return console.log 'formparse error', err
@@ -81,8 +81,8 @@
     newFileName = oldFileName.replace /\.(?=[^.]*$)/, "_"+Date.now()+"_copy."
     fs.writeFileSync dir+newFileName, fs.readFileSync dir+oldFileName
     file.setFieldValue 'link', newFileName
-    createImages file, req
     file.setFieldValue 'title', newFileName
+    file = createImages file, req
     file.save ->
       req.io.broadcast "updateCollection", "Files"

@@ -97,19 +97,15 @@
         if err then throw err

   copyImages = (file, move)->
-    oldName = file.getFieldValue("title").split(/\.(?=[^.]*$)/)[0]
-    newName = file.getFieldValue("link").split(/\.(?=[^.]*$)/)[0]
     for type in types
-      newLink = file.getFieldValue(type).replace newName, oldName
       oldLink = file.getFieldValue type
+      newLink = file.getFieldValue('title').replace /\.(?=[^.]*$)/, '_'+type+'.'
       if move
         fs.move dir+oldLink, dir+newLink
       else
-        #newLink = newLink.replace '\$.', '_copy_'+Date.now()+'.'
         fs.writeFileSync dir+newLink, fs.readFileSync(dir+oldLink)
-
-      file.setFieldValue type, newLink
-      console.log(type, newLink)
+      file.setFieldValue type, newLink unless type is "link"
+    file

   createImages = (file, req) ->
@@ -121,8 +117,7 @@
       portrait = true if size.width < size.height
       addFile = (type, cb)->
         maxSize = moduleSetting.getFieldValue type
-        targetName = filename.replace '.', '_'+type+'.'
-        console.log targetName
+        targetName = filename.replace /\.(?=[^.]*$)/, '_'+type+'.'
         file.setFieldValue type, targetName
         image.quality parseInt(moduleSetting.fields.quality.value)
         if portrait then image.resize null, maxSize


Fix test failure in MS Edge

Fix test failure in MS Edge

--- test/src/system/basic_input_test.coffee
+++ test/src/system/basic_input_test.coffee
@@ -50,11 +50,9 @@
 editorTest "deleting a line (command + backspace) in a list item", (expectDocument) ->
   clickToolbarButton attribute: "bullet", ->
     typeCharacters "a\nb", ->
-      triggerEvent document.activeElement, "keydown", charCode: 0, keyCode: 8, which: 8, metaKey: true
-      itemElement = document.activeElement.querySelectorAll("li")[1]
-      range = rangy.getSelection().getRangeAt(0)
-      range.selectNodeContents(itemElement)
-      range.deleteContents()
+      triggerEvent(document.activeElement, "keydown", charCode: 0, keyCode: 8, which: 8, metaKey: true)
+      selectNode(document.activeElement.querySelectorAll("li")[1])
+      deleteSelection()
       defer ->
         assertLocationRange index: 1, offset: 0
         expectBlockAttributes [0, 1], ["bulletList", "bullet"]


multifallbacks

multifallbacks

--- src/leaflet.pouch.coffee
+++ src/leaflet.pouch.coffee
@@ -8,19 +8,19 @@
 			opts = remoteDB
 			remoteDB = undefined
 		if remoteDB
-			if remoteDB.slice(0,3)=="idb"
+			if remoteDB.slice(0,4)!=="http"
 				db = remoteDB
 				remoteDB = undefined
 			else if remoteDB.slice(0,4)=="http"
 				if opts and opts.idbName
-					db = "idb://"+opts.idbName
+					db = opts.idbName
 				else
-					db = "idb://"+remoteDB.split("/").pop()
+					db = remoteDB.split("/").pop()
 		else
 			if opts and opts.idbName
-				db = "idb://"+opts.idbName
+				db = opts.idbName
 			else
-				db = "idb://"+location.href.split("/").pop()
+				db = location.href.split("/").pop()
 		@_layers = {}
 		pouchParams = L.Util.extend({}, @defaultParams)
 		for i of opts
@@ -61,7 +61,26 @@
 								if cb
 									cb(null, true) unless noOpt
 									cb("No Option") if noOpt
-							@sync()
+							@sync()
+			else if remoteDB
+				Pouch remoteDB, (e3, db3) =>
+					unless e3
+						@localDB = db3
+						@localDB.changes(
+							continuous : @pouchParams.continuous
+							include_docs : true
+							onChange : (c) =>
+								doc = c.doc
+								if parseInt(doc._rev.slice(0, 1)) is 1
+									@addData doc if "geometry" of doc
+								else if parseInt(doc._rev.slice(0, 1)) > 1
+									@eachLayer (f) =>
+										@removeLayer f  if f.feature._id is doc._id
+
+									if "geometry" of doc
+										@addData doc  unless doc._deleted
+								true
+						)
 	addDoc: (doc, cb = ()-> true) ->
 		if "type" of doc and doc.type == "Feature"
 			unless "_id" of doc


minor adding a test variant

minor adding a test variant

--- tests.coffee
+++ tests.coffee
@@ -25,6 +25,23 @@
   # ---------------------------------------------------------------------------

   "console print 1+1;Number answer:(+(operandum))by:(console print self;console print \"+\";console print operandum);2+3;Number answer:(+(operandum))by:(self $plus_binary operandum);"
+  "22+3"
+
+  """
+  console print 1+1
+  Number answer:
+  ﹍+ (operandum)
+  by:
+  ﹍console print self
+  ﹍console print \"+\"
+  ﹍console print operandum
+  2+3
+
+  Number answer:
+  ﹍+ (operandum)
+  by:
+  ﹍self $plus_binary operandum
+  """
   "22+3"

   # ---------------------------------------------------------------------------


Remove debug logging

Remove debug logging

--- resources/assets/coffee/_classes/user-card.coffee
+++ resources/assets/coffee/_classes/user-card.coffee
@@ -74,7 +74,6 @@
               api.tooltip.find('.usercard')
                 .imagesLoaded()
                 .progress (instance, image) =>
-                  console.log '@fadeLength', @fadeLength
                   if image.isLoaded
                     $(image.img).fadeTo(@fadeLength, 1)
                 .always (instance) ->


Cleaned up URLs

Cleaned up URLs

--- SingularityUI/app/models/Request.coffee
+++ SingularityUI/app/models/Request.coffee
@@ -18,22 +18,22 @@

     deletePaused: =>
         $.ajax
-            url: "#{ config.apiRoot }/requests/request/#{ @get('id') }/paused"
+            url: "#{ @url() }/paused"
             type: 'DELETE'

     unpause: =>
         $.ajax
-            url: "#{ config.apiRoot }/requests/request/#{ @get('id') }/unpause?user=#{app.getUsername()}"
+            url: "#{ @url() }/unpause?user=#{app.getUsername()}"
             type: 'POST'

     pause: =>
         $.ajax
-            url: "#{ config.apiRoot }/requests/request/#{ @get('id') }/pause?user=#{app.getUsername()}"
+            url: "#{ @url() }/pause?user=#{app.getUsername()}"
             type: 'POST'

     run: (confirmedOrPromptData) ->
         options =
-            url: "#{ config.apiRoot }/requests/request/#{ @get('id') }/run"
+            url: "#{ @url() }/run"
             type: 'POST'
             contentType: 'application/json'

@@ -46,12 +46,12 @@

     bounce: =>
         $.ajax
-            url: "#{ config.apiRoot }/requests/request/#{ @get('id') }/bounce"
+            url: "#{ @url() }/bounce"
             type: "POST"

     destroy: =>
         $.ajax
-            url: "#{ config.apiRoot }/requests/request/#{ @get('id') }?user=#{app.getUsername()}"
+            url: "#{ @url() }?user=#{app.getUsername()}"
             type: "DELETE"

 module.exports = Request


use instanceof instead of typeof

use instanceof instead of typeof

--- ccss.coffee
+++ ccss.coffee
@@ -10,7 +10,7 @@

     if mixin = pairs.mixin
       delete pairs.mixin
-      if typeof mixin is 'object'
+      unless mixin instanceof Array
         mixin = [mixin]
       for mix in mixin
         for key, value of mix


rename vars

rename vars

--- ccss.coffee
+++ ccss.coffee
@@ -1,28 +1,29 @@
 css = ''

-compile = (rules) ->
-  for selector, declarations of rules
-    children = {}
-    text = ''
+compile = (ccss) ->
+  for selector, pairs of ccss
+    # a pair can be a css declaration, or a pair of the child ccss object
+    child = {}
+    declarations = ''

-    for key, value of declarations
+    for key, value of pairs
       if typeof value is 'object'
-        children["#{selector} #{key}"] = value
+        child["#{selector} #{key}"] = value
       else
         #borderRadius -> border-radius
         key = key.replace /[A-Z]/g, (s) -> '-' + s.toLowerCase()
-        text += "  #{key}: #{value};\n"
+        declarations += "  #{key}: #{value};\n"

-    if text
+    if declarations
       #we have to check; this level could just be for nesting.
       css += selector + ' {\n'
-      css += text
+      css += declarations
       css += '}\n'

-    compile children
+    compile child

-@compile = (rules) ->
-  compile rules
+@compile = (ccss) ->
+  compile ccss
   ret = css
   css = ''
   ret


View: Emit ViewHasParent event.

View: Emit ViewHasParent event.

--- src/lib/core/View.coffee
+++ src/lib/core/View.coffee
@@ -148,6 +148,7 @@
   ###
   appendToDocumentBody: ->
     document.body.appendChild @getElement()
+    @emit 'ViewHasParent'

   ###*
@@ -180,6 +181,7 @@

     @children.push view
     @getElement().appendChild view.getElement()
+    view.emit 'ViewHasParent'

   ###*


remove empty line

remove empty line

--- assets/javascripts/app/auth.coffee
+++ assets/javascripts/app/auth.coffee
@@ -38,7 +38,6 @@
     sessionStorage?.clear()
     @setUser()

-
   setUser: (data) ->
     data = JSON.parse(data) if typeof data == 'string'
     user = @storeUser(data) if data


1) A few bug fixes in json lint

1) A few bug fixes in json lint

--- assets/js/durable-json-lint.coffee
+++ assets/js/durable-json-lint.coffee
@@ -2,16 +2,16 @@
 falafel = if typeof module == 'undefined' then window.falafel else require('free-falafel')
 jsonLint=(src)->
     if !src or /^\s*$/.test(src) then return {json:null, errors:[{lineNumber:1,column:1,description:"An empty string is not valid Json",status:"crash"}]}
-    wrappedSrc = "(function(){return "+src+";\n})();"
+    wrappedSrc = "(function(){return "+src+'\n})();'
     errors = []
     try
-        ast = esprima.parse(wrappedSrc, {range:true, tolerant:true, loc:true, raw:true})
+        ast = esprima.parse(wrappedSrc, {range:true, tolerant:true, loc:true, raw:true, comment:true})
     catch err
         err.status = "crash"
         if err.index >= wrappedSrc.length-7
             if err.lineNumber >= wrappedSrc.match(/\r\n?|\n/g).length+1 then err.lineNumber--
             err.column = 1
-            err.description = "Invalid Json. Did you forget to a close bracket?"
+            err.description = "Invalid Json. Did you forget a close bracket?"
         errors.push(err)
         return {errors:errors,json:null}
     #^(?:-?(?=[1-9]|0(?!\d))\d+(\.\d+)?([eE][+-]?\d+)?|true|false|null|"([^"\\]|(?:\\["\\/bfnrt])|(?:\\u[][0-9a-f]{4}))*")$
@@ -26,7 +26,9 @@
         })
         if node.loc.start.line == 1
             errors[errors.length-1].column -= 19
-
+    #add all comments as errors
+    for comment in ast.comments
+        createError(comment, "correctable", "Comments are not valid in Json")
     rootExpr = null
     breadthFirstFunc=(node)->
         if rootExpr == null
@@ -61,19 +63,22 @@
                 key = node.key
                 if key.type=="Identifier"
                     createError(key, "correctable", "Keys must be double quoted in Json. Did you mean \"#{key.name}\"?")
-                    key.valid=false
+                    key.valid = false
                     key.correct = JSON.stringify(key.name)
                 else if key.type=="Literal" and typeof(key.value) == "number"
                     createError(key, "correctable", "Keys must be double quoted in Json. Did you mean \"#{key.raw}\"?")
-                    key.valid=false
+                    key.valid = false
                     key.correct = JSON.stringify(key.raw)
             when "Identifier"
                 node.valid = false
                 createError(node, "guessable", "An identifier is not a valid Json element. Did you mean \"#{node.name}\"?")
                 node.correct = JSON.stringify(node.name)
             when "CallExpression"
-                node.valid=false
+                node.valid = false
                 createError(node, "fail", "You can not make function calls in Json. Do you think I am a fool?")
+            when "Line","Block" #Handle comments
+                node.valid = false
+                node.correct = ""
             else
                 node.valid=false
                 createError(node, "fail", "A \"#{node.type}\" is an invalid Json element.")
@@ -100,7 +105,7 @@

     # do the processing
     res = falafel(wrappedSrc, {ast:ast}, depthFirstFunc, breadthFirstFunc).toString()
-    res = res.substring(19,res.length-7)
+    res = res.substring(19,res.length-6)
     return {json:res, errors:errors}
 #export
 if typeof module == 'undefined' then window.durableJsonLint = jsonLint else module.exports = jsonLint


Add method for adding links so specific rounds

--- client.coffee
+++ client.coffee
@@ -11,6 +11,7 @@
 Util = require 'util'

 exports.render = ->
+	Dom.h2 Page.state.get(0) + ': ' + Page.state.get(1)
 	if Page.state.get(0) is 'advanced'
 		renderAdvanded()
 	else if Page.state.get(0) is 'round'
@@ -35,8 +36,10 @@
 	Ui.list ->
 		Ui.item ->
 			Dom.h2 "This is a list entry!"
+			Dom.onTap -> Page.nav ['round', 5]
 		Ui.item ->
 			Dom.h2 "This is also a list entry!"
+			Dom.onTap -> Page.nav ['round', 0]

 renderAdvanded = ->
 	Dom.h2 "Hello, World!"


remove mess log

remove mess log

--- assets/scripts/controllers/task.coffee
+++ assets/scripts/controllers/task.coffee
@@ -182,7 +182,6 @@
       $(".terminals .term-#{data.from} ul.shell-body").scrollTop($(".terminals .term-#{data.from} ul.shell-body")[0].scrollHeight)

   socket.on 'workerIn', (data) ->
-    console.log "worker in",data
     if jobInfo.workers[data.from]
       jobInfo.workers[data.from].active = true
       jobInfo.workers[data.from].doms.light.getElementsByClassName('twinkle')[0].classList.add 'twinkle-on'
@@ -194,7 +193,6 @@
       jobInfo.flushDom data.from

   socket.on 'workerOut', (data) ->
-    console.log "worker lost",data
     return unless jobInfo.workers[data.from]
     jobInfo.workers[data.from].active = false
     jobInfo.workers[data.from].doms.light.getElementsByClassName('twinkle')[0].classList.remove 'twinkle-on'


Fixed sorting of captures/day

Fixed sorting of captures/day

--- client.coffee
+++ client.coffee
@@ -147,6 +147,8 @@
 					d = new Date((Config.startTimestamp+ day.key()*86400)*1000)
 					Dom.text d.getDate() + '-' + d.getMonth() + '-' + d.getFullYear()
 					Dom.style height: '20px', lineHeight: '20px', marginBottom: '3px'
+			, (day) ->
+				parseInt(day.key())
 		Dom.div !->
 			Dom.style width: 'auto', right: '8px', left: '100px', position: 'absolute'
 			Db.shared.iterate 'eventsPerDay', (day) !->
@@ -154,9 +156,12 @@
 					Dom.style width: (day.get()/maxEvents*100) + '%',height: '20px', backgroundColor: '#1E981E',  color: '#FFFFFF', textAlign: 'right', fontSize: '12px', paddingRight: '7px', lineHeight: '20px', marginBottom: '3px', textShadow: '0 0 3px rgba(0,0,0,0.9)'
 					Dom.style _boxSizing: 'border-box'
 					Dom.text day.get()
+			, (day) ->
+				parseInt(day.key())
 		Dom.div !->
 			Dom.style marginBottom: (rows*23) + 'px'
 		Dom.br()
+

 renderGroupsPerGroupsize = !->
 	Obs.observe ->


Decimal input, form checking, styling

Decimal input, form checking, styling

--- client.coffee
+++ client.coffee
@@ -225,21 +225,22 @@
 			renderBalanceSplitSection(transaction.get("total"), transaction.ref("for"))
 	# Comments
 	renderSystemComment = (comment) ->
-		log "Custom render called: "+JSON.stringify(comment.peek())
 		if comment.get('system')? and comment.get('system')
 			Dom.div !->
+				Dom.span !->
+					Dom.style color: '#999'
+					Time.deltaText comment.get('t')
+					Dom.text " • "
 				Dom.style
-					margin: '15px 0px 15px 50px'
-					fontSize: '85%'
+					margin: '6px 0px 6px 56px'
+					fontSize: '70%'
 				Event.styleNew(comment.get('t'))
-				Dom.text Plugin.userName(comment.get('u'))+" edited the transaction • "
-				Time.deltaText comment.get('t')
+				Dom.text Plugin.userName(comment.get('u'))+" edited the transaction"
 			return true
 		return false
 	Social.renderComments
 		path: [txId]
 		content: renderSystemComment
-

 renderBalanceSplitSection = (total, path) !->
 	remainder = Obs.create(total)
@@ -397,7 +398,7 @@
 					userKey = ""
 					byO.iterate (user) !->
 						userKey = user.key()
-					Ui.avatar Plugin.userAvatar(userKey),
+					Ui.avatar Plugin.userAvatar(userKey),
 						onTap: (!-> Plugin.userInfo(userKey))
 						style: marginRight: "10px"
 					Dom.div !->
@@ -408,23 +409,63 @@
 						if Db.shared.get("currency")
 							currency = Db.shared.get("currency")
 						Dom.text currency
-						Dom.style
+						Dom.style
 							margin: '-3px 5px 0 0'
 							fontSize: '21px'
+					inputField = undefined
+					centField = undefined
 					Dom.div !->
 						Dom.style width: '80px', margin: '-20px 0 -20px 0'
 						inputField = Form.input
-							name: 'total'
+							name: 'paidby'
 							type: 'number'
-							text: '0.-'
-							value: edit.peek('total') if edit
-							onChange: (value) ->
-								if value
-									if (value+"").indexOf(",") isnt -1
-										value = (value+"").replace(",", ".")
-									byO.set(userKey, +value)
-									log 'user='+userKey+', write', +value, " byO="+JSON.stringify(byO.peek()) + ", float="+parseFloat(value)
-								return
+							text: '0'
+							inScope: !->
+								Dom.style textAlign: 'right'
+							onChange: (v) !->
+								if v and inputField and centField
+									result = +(inputField.value()+"."+centField.value())
+									if !isNaN(result)
+										byO.set(userKey, result)
+						if byO.peek(userKey)
+							inputField.value Math.floor(byO.peek(userKey))
+						else
+							inputField.value null
+					Dom.div !->
+						Dom.style
+							width: '10px'
+							fontSize: '175%'
+							padding: '12px 0 0 5px'
+							margin: '-20px 0 -20px 0'
+						Dom.text ","
+					Dom.div !->
+						Dom.style width: '50px', margin: '-20px 0 -20px 0'
+						centField = Form.input
+							name: 'paidby2'
+							type: 'number'
+							text: '00'
+							onChange: (v) !->
+								if v<0
+									centField.value(0)
+								if v and inputField and centField
+									result = +(inputField.value()+"."+centField.value())
+									if !isNaN(result)
+										byO.set(userKey, result)
+						if byO.peek(userKey) and byO.peek(userKey)%1.0 isnt 0
+							mod = byO.peek(userKey%1.0)
+							string = (mod.toFixed(2))+""
+							if mod < 0
+								centField.value string.substr(3)
+							else
+								centField.value string.substr(2)
+						else
+							centField.value null
+					Dom.on 'keydown', (evt) !->
+						if evt.getKeyCode() in [188,190] # comma and dot
+							centField.focus()
+							centField.select()
+							evt.kill()
+					,true
 			else
 				# Set form input
 				Obs.observe !->
@@ -461,23 +502,19 @@
 									Dom.onTap
 										cb: !->
 											value = undefined
+											oldValue = undefined
 											update = Obs.create(false)
 											Obs.observe !->
 												update.get()
-												if value?
-													log "received update"
-													if (value+"").indexOf(",") isnt -1
-														value = (value+"").replace(",", ".")
-													number = +value
+												if oldValue?
+													number = +oldValue
 													if (not (isNaN(number)))
-														#log "number=", number, ", numberIsNaN=", number is NaN
 														if number is 0
 															byO.remove user.key()
 														else
 															byO.set user.key(), number
 													else
-														Modal.show "Incorrect input: \""+value+"\", use a number"
-													#log "Amount updated=", JSON.stringify(byO)
+														Modal.show "Incorrect input: \""+oldValue+"\", use a number"
 												# Do something
 											Modal.show tr("Amount paid by %1?", formatName(user.key())), !->
 												Dom.div !->
@@ -487,27 +524,65 @@
 														if Db.shared.get("currency")
 															currency = Db.shared.get("currency")
 														Dom.text currency
-														Dom.style
+														Dom.style
 															margin: '20px 5px 0px 0px'
 															fontSize: '21px'
+													inputField = undefined
+													centField = undefined
 													Dom.div !->
-														Dom.style Flex: true
-														defaultValue = undefined
-														if (byO.peek(user.key())+"") isnt "true" and ((byO.peek(user.key())+"").substr(-1) isnt "%")
-															defaultValue = +byO.peek(user.key())
+														Dom.style width: '80px'
 														inputField = Form.input
 															name: 'paidby'
 															type: 'number'
-															text: '0.-'
-															value: defaultValue
-															onChange: (v) ->
-																if v
-																	value = v
-																return
+															text: '0'
+															inScope: !->
+																Dom.style textAlign: 'right'
+															onChange: (v) !->
+																if v and inputField and centField
+																	oldValue = value
+																	value = inputField.value()+"."+centField.value()
+														if byO.peek(user.key())
+															inputField.value Math.floor(byO.peek(user.key()))
+														else
+															inputField.value null
+													Dom.div !->
+														Dom.style
+															width: '10px'
+															fontSize: '175%'
+															padding: '23px 0 0 4px'
+														Dom.text ","
+													Dom.div !->
+														Dom.style width: '50px'
+														centField = Form.input
+															name: 'paidby2'
+															type: 'number'
+															text: '00'
+															onChange: (v) !->
+																if v<0
+																	centField.value(0)
+																if inputField
+																	oldValue = value
+																	if v
+																		value = inputField.value()+"."+v
+																	else
+																		value = inputField.value()
+														if byO.peek(user.key()) and byO.peek(user.key())%1.0 isnt 0
+															mod = (byO.peek(user.key())%1.0)
+															string = (mod.toFixed(2))+""
+															if mod < 0
+																centField.value string.substr(3)
+															else
+																centField.value string.substr(2)
+														else
+															centField.value null
+													Dom.on 'keydown', (evt) !->
+														if evt.getKeyCode() in [188,190] # comma and dot
+															centField.focus()
+															centField.select()
+															evt.kill()
+													,true
 											, (value) !->
-												log "value="+value
 												if value isnt null and value isnt undefined and value is 'ok'
-													log "update"
 													update.set(true)
 											, ['ok', "Ok", 'cancel', "Cancel"]
 									Dom.style
@@ -537,7 +612,7 @@
 														data: 'good2'
 														size: 20
 														color: '#080'
-
+
 		Obs.observe !->
 			if not multiplePaidBy.get() and Plugin.users.count().get() > 1
 				Dom.div !->
@@ -606,7 +681,6 @@
 					amount = forO.get(user.key())
 					number = Obs.create 0
 					suffix = undefined
-					totalO.get()
 					Obs.observe !->
 						if amount
 							if (amount+"") is "true"
@@ -630,7 +704,7 @@
 									remainder.modify((v) -> v-number.get())
 									Obs.onClean !->
 										remainder.modify((v) -> v+number.get())
-								suffix = "fixed"
+								suffix = "fixed"
 					# TODO: Assign possibly remaining part of the total to someone (only for show, server handles balances correctly)
 					Dom.div !-> # Aligning div
 						Dom.style
@@ -655,17 +729,16 @@
 									if amount?
 										forO.set(user.key(), null)
 									else
-										forO.set(user.key(), true)
+										forO.set(user.key(), true)
 								longTap: !->
 									value = undefined
+									oldValue = undefined
 									update = Obs.create(false)
 									Obs.observe !->
 										update.get()
 										if value?
-											#log "received update"
+											log "received update: value="+value+", oldValue="+oldValue
 											v = value
-											if (v+"").indexOf(",") isnt -1
-												v = (v+"").replace(",", ".")
 											amount = +v
 											if (v+"").substr(-1) is "%"
 												#log "modal percent received"
@@ -680,7 +753,8 @@
 														forO.set user.key(), true
 													else
 														forO.set user.key(), v
-											else if not isNaN(amount)
+											else if not isNaN(+oldValue)
+												amount = +oldValue
 												#log "amount=", amount, ", amountIsNaN=", amount is NaN
 												if amount is 0
 													forO.remove user.key()
@@ -697,7 +771,7 @@
 												Dom.div !->
 													Dom.style Box: 'horizontal'
 													Dom.div !->
-														Dom.style Flex: true
+														Dom.style width: '80px'
 														defaultValue = undefined
 														if (forO.peek(user.key())+"").substr(-1) is "%"
 															defaultValue = (forO.peek(user.key())+"").substr(0, (forO.peek(user.key())+"").length-1)
@@ -713,7 +787,7 @@
 																log "value="+v
 																return
 													Dom.div !->
-														Dom.style
+														Dom.style
 															margin: '20px 5px 0px 0px'
 															fontSize: '21px'
 														Dom.text "%"
@@ -725,27 +799,66 @@
 														if Db.shared.get("currency")
 															currency = Db.shared.get("currency")
 														Dom.text currency
-														Dom.style
+														Dom.style
 															margin: '20px 5px 0px 0px'
 															fontSize: '21px'
+													inputField = undefined
+													centField = undefined
 													Dom.div !->
-														Dom.style Flex: true
-														defaultValue = undefined
-														if (not (isNaN(+forO.peek(user.key()))))
-															defaultValue = +forO.peek(user.key())
+														Dom.style width: '80px'
 														inputField = Form.input
-															name: 'paidForFixed'+user.key()
+															name: 'paidby'
 															type: 'number'
-															text: '0.-'
-															value:  defaultValue
-															onChange: (v) ->
-																if v
-																	value = v
-																log "value="+v
-																return
+															text: '0'
+															inScope: !->
+																Dom.style textAlign: 'right'
+															onChange: (v) !->
+																if v and inputField and centField
+																	oldValue = value
+																	value = inputField.value()+"."+centField.value()
+														if forO.peek(user.key())
+															inputField.value Math.floor(forO.peek(user.key()))
+														else
+															inputField.value null
+													Dom.div !->
+														Dom.style
+															width: '10px'
+															fontSize: '175%'
+															padding: '23px 0 0 4px'
+														Dom.text ","
+													Dom.div !->
+														Dom.style width: '50px'
+														centField = Form.input
+															name: 'paidby2'
+															type: 'number'
+															text: '00'
+															onChange: (v) !->
+																if v<0
+																	centField.value(0)
+																if inputField
+																	oldValue = value
+																	if v
+																		value = inputField.value()+"."+v
+																	else
+																		value = inputField.value()
+														if forO.peek(user.key()) and forO.peek(user.key())%1.0 isnt 0
+															mod = (byO.peek(user.key())%1.0)
+															string = (mod.toFixed(2))+""
+															if mod < 0
+																centField.value string.substr(3)
+															else
+																centField.value string.substr(2)
+														else
+															centField.value null
+													Dom.on 'keydown', (evt) !->
+														if evt.getKeyCode() in [188,190] # comma and dot
+															centField.focus()
+															centField.select()
+															evt.kill()
+													,true
 										Dom.br()
 										Dom.div !->
-											Dom.style
+											Dom.style
 												display: 'inline-block'
 												color: Plugin.colors().highlight
 												padding: "5px"
@@ -759,7 +872,7 @@
 												procentual.set false
 										Dom.text " | "
 										Dom.div !->
-											Dom.style
+											Dom.style
 												display: 'inline-block'
 												color: Plugin.colors().highlight
 												padding: "5px"
@@ -784,7 +897,7 @@
 								Dom.style
 									Flex: true
 								Dom.div !->
-									Dom.style
+									Dom.style
 										Flex: true
 										overflow: 'hidden'
 										textOverflow: 'ellipsis'
@@ -804,7 +917,7 @@
 												fontSize: '90%'
 											if suffix isnt undefined
 												Dom.div !->
-													Dom.style
+													Dom.style
 														fontWeight: 'normal'
 														fontSize: '80%'
 														display: 'inline-block'
@@ -815,8 +928,8 @@
 												data: 'good2'
 												size: 20
 												color: '#080'
-
 		Form.condition (values) ->
+			log "checking conditions"
 			if totalO.peek() is 0
 				text = "A transaction with a total of zero is not useful"
 				if Db.shared.peek("transactions", editId)?
@@ -824,15 +937,41 @@
 				return tr(text)
 			if totalO.peek() < 0
 				return tr("A negative paid by is not possible (switch paid by and paid for)")
-			good = false
-			forO.iterate (user) !->
-				amount = user.peek()
-				if (amount+"") is "true"
-					good = true
-					return
-				else if (amount+"").substr(-1) is "%"
-					good = true
-			if remainder.peek() isnt 0 and not good
+			wrong = false
+			byO.iterate (byEntry) !->
+				wrong = wrong or byEntry.peek() < 0
+			if wrong
+				return tr("You cannot use a negative number")
+			divide = []
+			remainderTemp = totalO.peek()
+			completeShare = 0
+			for userId,amount of forO.peek()
+				if (amount+"").substr(-1) is "%"
+					amount = amount+""
+					percent = +(amount.substring(0, amount.length-1))
+					completeShare += percent
+					divide.push userId
+				else if (""+amount) is "true"
+					divide.push userId
+					completeShare += 100
+				else
+					number = +amount
+					amount = Math.round(amount*100.0)/100.0
+					if amount < 0
+						return tr("You cannot use a negative number")
+					remainderTemp -= amount
+			if remainderTemp isnt 0 and divide.length > 0
+				while userId = divide.pop()
+					raw = forO.peek(userId)
+					percent = 100
+					if (raw+"").substr(-1) is "%"
+						raw = raw+""
+						percent = +(raw.substring(0, raw.length-1))
+					amount = Math.round((remainderTemp*100.0)/completeShare*percent)/100.0
+					if amount < 0
+						return tr("You cannot use a negative number")
+				remainderTemp = 0
+			if remainderTemp isnt 0
 				return tr("The by and for section do not match")
 	Dom.div !->
 		Dom.style


Added `locale` property support

Added `locale` property support

--- lib.coffee
+++ lib.coffee
@@ -34,6 +34,7 @@
     format: PropTypes.string
     value: PropTypes.string
     defaultValue: PropTypes.string
+    locale: PropTypes.string

   getDefaultProps: ->
     format: defaultFormat
@@ -63,9 +64,11 @@
     format.split formatRegex

   getMoment: ->
-    {format} = @props
+    {format, locale} = @props
     {value} = @state
-    moment value, format
+    datetime = moment value, format
+    datetime.locale locale if locale
+    datetime

   getValue: ->
     @state.value


add react global for dev tool support

add react global for dev tool support

--- assets/coffee/app.coffee
+++ assets/coffee/app.coffee
@@ -10,10 +10,11 @@
 # mount point for app
 appEl = document.getElementById 'app'

+# needed for React Developer Tools
+window.React = React
+
 # init FastClick
 FastClick appEl
-
-# React.initializeTouchEvents true

 App = React.createClass
   displayName: 'App'


fixes #99, do not treat empty string for 'type string' to be undefined

--- src/lang/typedefs.coffee
+++ src/lang/typedefs.coffee
@@ -90,7 +90,7 @@
           else (Number) max
         (v) -> (not min? or v.length >= min) and (not max? or v.length <= max)

-      return if value is null or value is ''
+      return if value is null

       type = typeof value
       value = String value


Webserver: if request has token use that token to fetch last members of a group

--- servers/lib/server/handlers/getteammembers.coffee
+++ servers/lib/server/handlers/getteammembers.coffee
@@ -1,25 +1,52 @@
-koding                 = require './../bongo'
-{ isLoggedIn }         = require './../helpers'
-{ generateFakeClient } = require "./../client"
+koding                   = require './../bongo'
+{ isLoggedIn }           = require './../helpers'
+{ generateFakeClient }   = require "./../client"

+# handleTokenedRequest handles the request if it has a token as query param,
+# fetchMembers is secured by a permission, that can be turned off by default,
+# but we want to show latest members to invited user
+handleTokenedRequest = (params, res, next) ->
+  { JInvitation } = koding.models
+  { options, token, client } = params
+
+  return res.status(403).send 'not authorized'  unless token
+
+  # fetch invitation
+  JInvitation.byCode token, (err, token_) ->
+    if err or not token_
+      return res.status(403).send 'not authorized'
+    else
+      # fetch the group that we have in token
+      JGroup.one slug : name, (err, group) ->
+        return res.status(500).send 'an error occured'  if err
+        return res.status(404).send 'no group found'    unless group
+
+        # override group name with the one in token
+        client.context.group = group.slug
+
+        # fetch members of that group
+        group.fetchMembers {}, options, (err, members) ->
+          if err
+            return res.status(500).send 'an error occured'
+          else
+            return res.status(200).send members
+
+# fetch last members of a group, if we have a permission issue for the current
+# user, try to fetch it with token
 module.exports = (req, res, next) ->
-
   { params, query } = req
   { name }          = params
-  { limit }         = query
+  { limit, token }  = query
   { JGroup }        = koding.models

   isLoggedIn req, res, (err, loggedIn, account) ->
-
     return res.status(500).send 'an error occured'  if err

     JGroup.one slug : name, (err, group) ->
-
       return res.status(500).send 'an error occured'  if err
       return res.status(404).send 'no group found'    unless group

       generateFakeClient req, res, (err, client) ->
-
         return res.status(500).send 'an error occured'  if err

         options       = {}
@@ -27,7 +54,9 @@
         options.limit = Math.min limit ? 10, 25

         group.fetchMembers$ client, {}, options, (err, members) ->
-
-          return res.status(403).send 'not authorized'    if err and err.name is 'AccessDenied'
-          return res.status(500).send 'an error occured'  if err
-          return res.status(200).send members
+          if err and err.message is 'AccessDenied'
+            return handleTokenedRequest { client, options, token }, res, next
+          else if err
+            return res.status(500).send 'an error occured'
+          else
+            return res.status(200).send members


Make day resolution more correct.

Make day resolution more correct.

--- bokehjs/src/coffee/common/ticking.coffee
+++ bokehjs/src/coffee/common/ticking.coffee
@@ -337,7 +337,7 @@
       best_scale_ix = scale_ixs[argmin(errors)]
       best_scale = @scales[best_scale_ix]

-#       console.log("Selected #{best_scale.constructor.name}")
+      console.log("Selected #{best_scale.constructor.name}")

       return best_scale

@@ -405,6 +405,15 @@
     d.setMilliseconds(0)
     return d.getTime()

+  last_month_no_later_than = (time) ->
+    d = new Date(time)
+    d.setDate(1)
+    d.setHours(0)
+    d.setMinutes(0)
+    d.setSeconds(0)
+    d.setMilliseconds(0)
+    return d.getTime()
+
   add_days = (time, n_days) ->
     d = new Date(time)
     d.setDate(d.getDate() + n_days)
@@ -432,6 +441,80 @@
       console.log(ticks.map((time) -> new Date(time)))
       return ticks

+  class DaysScale extends SingleIntervalScale
+    constructor: (@days) ->
+      typical_interval = if @days.length > 1
+          (@days[1] - @days[0]) * ONE_DAY
+        else
+          31 * ONE_DAY
+      super(typical_interval)
+
+    get_ticks: (data_low, data_high) ->
+      copy_date = (date) ->
+        return new Date(date.getTime())
+
+      date_range_by_month = (start_time, end_time) ->
+        start_date = new Date(last_month_no_later_than(start_time))
+
+        end_date = new Date(last_month_no_later_than(end_time))
+        # XXX This is not a reliable technique in general, but it should be
+        # safe when the day of the month is 1.  (The problem case is this:
+        # Mar 31 -> Apr 31, which becomes May 1.)
+        end_date.setMonth(end_date.getMonth() + 1)
+
+        console.log("start: #{start_time} -> #{new Date(start_time)} -> #{start_date} -> #{start_date.getTime()}")
+        console.log("  end: #{  end_time} -> #{  new Date(end_time)} -> #{  end_date} -> #{  end_date.getTime()}")
+
+        dates = []
+        date = start_date
+        while true
+          dates.push(copy_date(date))
+
+          date.setMonth(date.getMonth() + 1)
+          if date > end_date
+            break
+
+        return dates
+
+      month_dates = date_range_by_month(data_low, data_high)
+      console.log("months: #{month_dates}")
+
+      days = @days
+      days_of_month = (month_date) ->
+        dates = []
+        for day in days
+          day_date = copy_date(month_date)
+          day_date.setDate(day)
+#           console.log("  #{day} #{day_date} (#{day_date.getMonth() == month_date.getMonth()})")
+          # Some of the values of @days may not apply to the current month, in
+          # which case the resulting date will fall in the next month.
+          if day_date.getMonth() == month_date.getMonth()
+            dates.push(day_date)
+        return dates
+
+      # FIXME Use a list comprehension?
+      day_dates = _.flatten(month_dates.map((date) -> days_of_month(date)))
+
+      console.log("  days: #{day_dates}")
+
+      all_ticks = _.invoke(day_dates, 'getTime')
+      # FIXME Since the ticks are sorted, this could be done more efficiently.
+      ticks_in_range = _.filter(all_ticks,
+                                ((tick) -> data_low <= tick <= data_high))
+
+      console.log(":::")
+      console.log("#{data_low} [#{all_ticks}] #{data_high}")
+      console.log("[#{ ticks_in_range }]")
+
+      # FIXME Continue here:
+      # Transition between 12-hour and 24-hour resolutions is not correct.
+      # This is because the day resolutions are aligned with the local time
+      # zone, and the resolutions below are aligned with GMT.
+
+      console.log("#{ticks_in_range.map((tick) -> new Date(tick))}")
+
+      return ticks_in_range
+
   class SimpleScale extends CompositeScale
     constructor: (intervals) ->
       super(intervals.map((interval) ->
@@ -462,7 +545,11 @@

     # Days.
     # FIXME Formatting is not happening quite right at the boundaries.
-    new DayScale(),
+    new DaysScale(arange(1, 32)), #FIXME
+    new DaysScale(arange(1, 31, 3)),
+    new DaysScale([1, 8, 15, 22]),
+    new DaysScale([1, 15]),
+    new DaysScale([1]),

     # Catchall for large timescales.
     new AdaptiveScale([1.0, 2.0, 5.0], 10.0, 10 * ONE_DAY, Infinity),


Throw meaningful error when instantiating undefined type

--- bokehjs/src/coffee/core/has_props.coffee
+++ bokehjs/src/coffee/core/has_props.coffee
@@ -94,6 +94,8 @@
     @properties = {}
     props = _.result(this, 'props')
     for name, [type, default_value] of props
+      if not type?
+        throw new Error("undefined property type for #{@type}.#{name}")
       @properties[name] = new type({obj: @, attr: name, default_value: default_value})

     if options.parse


fix typos in support contracts

fix typos in support contracts

--- assets/coffeescript/product_list.coffee
+++ assets/coffeescript/product_list.coffee
@@ -320,15 +320,15 @@
       cost: 1000
       title_product: 'Support Contract'
       title_store: '6 Month Support Contract'
-      description_store: '6 month support contract. 4 video support sessions and email support.'
-      description_cart: '6 month support contract. 4 video support sessions and email support.'
+      description_store: '6 month support contract. 3 one hour video support sessions and email support.'
+      description_cart: '6 month support contract. 3 one hour video support sessions and email support.'
       img: [additional_product_path,'support6.svg'].join('/')
     support12:
       cost: 2000
       title_product: 'Support Contract'
       title_store: '12 Month Support Contract'
-      description_store: '12 month support contract. 4 video support sessions and email support.'
-      description_cart: '12 month support contract. 4 video support sessions and email support.'
+      description_store: '12 month support contract. 7 one hour video support sessions and email support.'
+      description_cart: '12 month support contract. 7 one hour video support sessions and email support.'
       img: [additional_product_path,'support12.svg'].join('/')

   return product_db


Fix #426 - RangeError (call stack overflow)

--- src/local/watcher.coffee
+++ src/local/watcher.coffee
@@ -280,7 +280,7 @@
                 else
                     async.eachSeries docs.reverse(), (doc, next) =>
                         if doc.path in @paths
-                            next()
+                            async.setImmediate next
                         else
                             @prep.deleteDoc @side, doc, next
                     , (err) =>


Optimize `dropEmptyBrackets` to skip it if unused

--- src/lib/Component.coffee
+++ src/lib/Component.coffee
@@ -97,6 +97,10 @@
       else
         @forwardBrackets[inPort] = tmp
         @bracketCounter[inPort] = 0
+    tmp = []
+    for outPort in @dropEmptyBrackets
+      tmp.push outPort if outPort of @outPorts.ports
+    @dropEmptyBrackets = tmp

   # Sets process handler function
   process: (handle) ->
@@ -122,7 +126,7 @@
     (ip.type is 'openBracket' or ip.type is 'closeBracket')
       # Bracket forwarding
       outputEntry =
-        __resolved: ip.type is 'closeBracket'
+        __resolved: ip.type is 'closeBracket' or not @dropEmptyBrackets.length
         __forwarded: true
         __type: ip.type
         __scope: ip.scope
@@ -131,7 +135,7 @@
         outputEntry[outPort].push ip
       port.buffer.pop()
       # Drop empty brackets if needed
-      if ip.type is 'closeBracket'
+      if ip.type is 'closeBracket' and @dropEmptyBrackets.length
         haveData = []
         for i in [@outputQ.length - 1..0]
           entry = @outputQ[i]


#439, alt+shift issue workaround

#439, alt+shift issue workaround

--- src/ui/views/menu.coffee
+++ src/ui/views/menu.coffee
@@ -393,6 +393,9 @@
 templateMenu = (viewstate) ->
     [
         {
+            label: ''
+        }
+        {
             label: 'YakYak'
             submenu: templateYakYak viewstate
         }


Reformat capturing rules of string-matching patterns

--- grammars/apl.cson
+++ grammars/apl.cson
@@ -146,10 +146,8 @@
 			name: "string.quoted.single.apl"
 			begin: "'"
 			end:   "'|$"
-			beginCaptures:
-				0: {name: "punctuation.definition.string.begin.apl"}
-			endCaptures:
-				0: {name: "punctuation.definition.string.end.apl"}
+			beginCaptures: {0: {name: "punctuation.definition.string.begin.apl"}}
+			endCaptures:   {0: {name: "punctuation.definition.string.end.apl"}}
 			patterns: [{
 				match: "[^']*[^'\\n\\r\\\\]$"
 				name: "invalid.illegal.string.apl"
@@ -158,10 +156,8 @@
 			name: "string.quoted.double.apl",
 			begin: '"'
 			end:   '"|$'
-			beginCaptures:
-				0: {name: "punctuation.definition.string.begin.apl"}
-			endCaptures:
-				0: {name: "punctuation.definition.string.end.apl"}
+			beginCaptures: {0: {name: "punctuation.definition.string.begin.apl"}}
+			endCaptures:   {0: {name: "punctuation.definition.string.end.apl"}}
 			patterns: [{
 				match: '[^"]*[^"\\n\\r\\\\]$'
 				name: "invalid.illegal.string.apl"


Do not show cluster marker bounds on hover

--- src/map-base-view.coffee
+++ src/map-base-view.coffee
@@ -82,6 +82,7 @@
             @visualizationLayer = L.featureGroup()
             @visualizationLayer.addTo @map
             @publicTransitStopsLayer = L.markerClusterGroup
+                showCoverageOnHover: false
                 singleMarkerMode: true
                 spiderfyOnMaxZoom: false
                 zoomToBoundsOnClick: false


fix slots image

fix slots image

--- PanelItem.cjsx
+++ PanelItem.cjsx
@@ -15,7 +15,7 @@
             {
               for slot, index in @props.ship[3]
                 <OverlayTrigger key={index} placement='bottom' overlay={<Tooltip>{slot[1]}{if slot[2] > 0 then ★+{slot[2]} else ''}</Tooltip>}>
-
+
                 </OverlayTrigger>
             }

@@ -39,7 +39,7 @@
             {
               for slot,index in @props.ship[3]
                 <OverlayTrigger placement='right' overlay={<Tooltip>{slot[1]}{if slot[2] > 0 then ★+{slot[2]} else ''}</Tooltip>}>
-
+
                 </OverlayTrigger>
             }


Make @internal work with undefined default_value

--- bokehjs/src/coffee/core/has_props.coffee
+++ bokehjs/src/coffee/core/has_props.coffee
@@ -44,7 +44,8 @@
     _object = {}
     for name, prop of object
       do (name, prop) =>
-        _object[name] = prop.concat([true])
+        [type, default_value] = prop
+        _object[name] = [type, default_value, true]
     @define(_object)

   @mixin: (names...) ->


Fixed: github commits konnectors. Only 1 page was fetched instead of 10.

--- server/konnectors/githubcommits.coffee
+++ server/konnectors/githubcommits.coffee
@@ -88,7 +88,7 @@

     data.commits = []
     log.info "Fetch commits sha from events..."
-    async.eachSeries [1..1], (page, callback) ->
+    async.eachSeries [1..10], (page, callback) ->

         client.get path + page, (err, res, events) ->
             unless err?


Allow to update flags of messages in Test accounts

--- server/processes/imap_processes.coffee
+++ server/processes/imap_processes.coffee
@@ -330,3 +330,25 @@
                         .then -> delete msg.mailboxIDs[boxid]
                         .tap ->
                             log.info "  DELETED #{path}:#{uid}"
+        else
+            # This is a test account
+            # ERROR CASES
+            for boxid in boxOps.addTo when not boxIndex[boxid]
+                throw new Error "the box ID=#{box} doesn't exists"
+
+            # step 1 - get the first box + UID
+            boxid = Object.keys(msg.mailboxIDs)[0]
+            uid = msg.mailboxIDs[boxid]
+
+            # step 2 - apply flags change
+            if flagsOps.add.length or flags.remove.length
+                msg.flags = _.union msg.flags, flagsOps.add
+                msg.flags = _.difference msg.flags, flagsOps.remove
+                log.info "  CHANGED FLAGS. RESULT = ", msg.flags
+
+            # step 3 - copy the message to its destinations
+            if boxOps.addTo
+                log.error "UNABLE to move messages inside TEST accounts"
+            # step 4 - remove the message from the box it shouldn't be in
+            if boxOps.removeFrom
+                log.error "UNABLE to delete messages inside TEST accounts"


Added DialogView "closed" event.

Added DialogView "closed" event.

--- lib/ST/DialogView.coffee
+++ lib/ST/DialogView.coffee
@@ -95,5 +95,6 @@

   @method 'close', ->
     self = this
+    @trigger 'closed'
     @hideBlanker()
     @hideDialog -> self.release()


Adds some atom config

Adds some atom config

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -46,7 +46,7 @@
     globalNodePath: "/Users/benoit/.node"
   "linter-jshint": {}
   "linter-scss-lint":
-    executablePath: "/usr/local/bin/scss-lint"
+    executablePath: "/Users/benoit/.rbenv/shims/scss-lint"
   "markdown-preview":
     grammars: [
       "source.gfm"
@@ -65,7 +65,8 @@
   "tool-bar":
     iconSize: "16px"
     position: "Left"
-  "tree-view": {}
+  "tree-view":
+    hideIgnoredNames: true
   welcome:
     showOnStartup: false
   whitespace:
@@ -80,3 +81,9 @@
 ".js.jsx.source":
   editor:
     preferredLineLength: 100
+".matlab.source":
+  editor:
+    preferredLineLength: 100
+".octave.source":
+  editor:
+    preferredLineLength: 100


Add atom ctags for Ruby.

Add atom ctags for Ruby.

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -6,7 +6,6 @@
     invisibles: {}
     preferredLineLength: 120
     softWrap: true
-    fontSize: 14
   core:
     ignoredNames: [
       ".bundle"
@@ -50,5 +49,8 @@
       "rubocop"
     ]
   "atom-ctags":
-    cmdArgs: "--exclude app/assets/javascripts/bundle* --exclude client/node_modules"
+    cmdArgs: "-R --languages=ruby,javascript --exclude=.git --exclude=log --exclude=app/assets/javascripts --exclude=client/node_modules . $(bundle list --paths)"
+    cmd: "ctags"
+    autoBuildTagsWhenActive: true
   "linter-rubocop": {}
+  "autocomplete-plus": {}


Fixes eventEmitter

Fixes eventEmitter

--- src/utils/emitter.coffee
+++ src/utils/emitter.coffee
@@ -1,13 +1,13 @@
 ee = null
-methods = ['on', 'onAny', 'offAny', 'once', 'many', 'off', 'removeAllListeners', 'listeners', 'listenersAny', 'emit']
+methods = ['on', 'onAny', 'offAny', 'once', 'many', 'off', 'removeAllListeners', 'listeners', 'listenersAny', 'emit', 'setMaxListeners']

 define ['eventemitter'], (EventEmitter2)->
   return ee if ee?
-  ee = new EventEmitter2
+  _ee = new EventEmitter2
     wildcard: true
     delimiter: '.'
     newListener: false
     maxListeners: 100
-  emitter = {}
-  emitter[method] = ee[method] for method in methods
-  emitter
+  ee = {}
+  ee[method] = _ee[method] for method in methods
+  ee


Use a spy

Use a spy

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -12,6 +12,8 @@
   getEditorView = -> atom.views.getView(atom.workspace.getActiveTextEditor())

   beforeEach ->
+    spyOn(SymbolsView::, "setLoading").andCallThrough()
+
     atom.project.setPaths([
       temp.mkdirSync("other-dir-")
       temp.mkdirSync('atom-symbols-view-')
@@ -37,7 +39,9 @@

       runs ->
         symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
-        expect(symbolsView.loading).toBeVisible()
+
+      waitsFor "loading", ->
+        symbolsView.setLoading.callCount > 1

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -361,7 +365,9 @@

       runs ->
         symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
-        expect(symbolsView.loading).toBeVisible()
+
+      waitsFor "loading", ->
+        symbolsView.setLoading.callCount is 1

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -468,7 +474,9 @@
         atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.js'))
         atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
         symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
-        expect(symbolsView.loading).toBeVisible()
+
+      waitsFor "loading", ->
+        symbolsView.setLoading.callCount > 1

       waitsFor ->
         symbolsView.list.children('li').length is 1


Don't remove nonexisting parameters from tasks

--- lib/api-functions.coffee
+++ lib/api-functions.coffee
@@ -202,37 +202,7 @@
     else
       task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000)

-  switch task.name
-    when 'getParameterValues'
-      projection = {}
-      for p in task.parameterNames
-        projection[p] = 1
-      db.devicesCollection.findOne({_id : task.device}, projection, (err, device) ->
-        parameterNames = []
-        for k of projection
-          if common.getParamValueFromPath(device, k)?
-            parameterNames.push(k)
-        task.parameterNames = parameterNames
-        callback(task)
-      )
-    when 'setParameterValues'
-      projection = {}
-      values = {}
-      for p in task.parameterValues
-        projection[p[0]] = 1
-        values[p[0]] = p[1]
-      db.devicesCollection.findOne({_id : task.device}, projection, (err, device) ->
-        parameterValues = []
-        for k of projection
-          param = common.getParamValueFromPath(device, k)
-          if param?
-            parameterValues.push([k, values[k], param._type])
-        task.parameterValues = parameterValues
-        callback(task)
-      )
-    else
-      # TODO implement setParameterValues
-      callback(task)
+  callback(task)

 insertTasks = (tasks, callback) ->


Made Gorbypuff less trigger-happy

Made Gorbypuff less trigger-happy

--- scripts/gorbypuff.coffee
+++ scripts/gorbypuff.coffee
@@ -8,7 +8,7 @@
 #   None
 #
 # Commands:
-#   gorby - Display a picture of everyone's favorite flat-faced cat
+#   gorbypuff - Display a picture of everyone's favorite flat-faced cat
 #
 # Author:
 #   bradly
@@ -58,6 +58,6 @@
 ]

 module.exports = (robot) ->
-  robot.hear /gorby|puff|thunderhorse/i, (msg) ->
+  robot.hear /gorbypuff|thunderhorse/i, (msg) ->
     msg.send msg.random gorbies


Do not use  in the data explorer

Do not use  in the data explorer

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -2369,6 +2369,10 @@
             @skip_value += @current_results.length
             try
                 @current_results = []
+                if @extra_row isnt undefined
+                    @current_results.push @extra_row
+                    @extra_row = undefined
+
                 @start_time = new Date()

                 @id_execution++
@@ -2518,7 +2522,6 @@
                         @save_query
                             query: @raw_query
                             broken_query: true
-
                         return false

                     if results?.profile? and @state.last_query_has_profile is true
@@ -2532,12 +2535,9 @@

                     if @index is @queries.length # @index was incremented in execute_portion
-                        if cursor?.hasNext?
+                        if typeof cursor._next is 'function'
                             @state.cursor = cursor
-                            if cursor.hasNext() is true
-                                @state.cursor.next get_result_callback
-                            else
-                                get_result_callback() # Display results
+                            @state.cursor.next get_result_callback
                         else
                             @toggle_executing false

@@ -2573,17 +2573,21 @@
             get_result_callback = (error, data) =>
                 if @id_execution is id_execution
                     if error?
-                        if @queries.length > 1
-                            @results_view.render_error(@query, error)
-                        else
-                            @results_view.render_error(null, error)
-                        return false
+                        if error.message isnt 'No more rows in the cursor.'
+                            if @queries.length > 1
+                                @results_view.render_error(@query, error)
+                            else
+                                @results_view.render_error(null, error)
+                            return false

                     if data isnt undefined
-                        @current_results.push data
-                        if @current_results.length < @limit and @state.cursor.hasNext() is true
+                        if @current_results.length < @limit
+                            @current_results.push data
                             @state.cursor.next get_result_callback
-                            return true
+                        else if @current_results.length is @limit
+                            @extra_row = data
+                            get_result_callback() # Display results
+                        return true

                     @toggle_executing false

@@ -2595,7 +2599,7 @@
                         skip_value: @skip_value
                         execution_time: new Date() - @start_time
                         query: @query
-                        has_more_data: @state.cursor.hasNext()
+                        has_more_data: @extra_row isnt undefined

                     @results_view.render_result
                         results: @current_results # The first parameter is null ( = query, so we don't display it)