minor factoring in r_help

minor factoring in r_help

--- salvus/page/r_help.cjsx
+++ salvus/page/r_help.cjsx
@@ -79,15 +79,15 @@

 HelpPageUsageSection = rclass

-    propTypes : ->
-        loading                : rtypes.boolean.isRequired
-        hub_servers            : rtypes.array.isRequired
-        accounts               : rtypes.number.isRequired
-        projects               : rtypes.number.isRequired
-        active_projects        : rtypes.number.isRequired
-        last_day_projects      : rtypes.number.isRequired
-        last_week_projects     : rtypes.number.isRequired
-        update_server_stats_cb : rtypes.function.isRequired
+    propTypes :
+        loading                : rtypes.bool.isRequired
+        hub_servers            : rtypes.array
+        accounts               : rtypes.number
+        projects               : rtypes.number
+        active_projects        : rtypes.number
+        last_day_projects      : rtypes.number
+        last_week_projects     : rtypes.number
+        update_server_stats_cb : rtypes.func.isRequired

     getDefaultProps : ->
        loading : true
@@ -217,12 +217,15 @@

 HelpPageSupportSection = rclass

+    propTypes :
+        support_links : rtypes.object
+
     get_support_links : ->
-        for name, item of SUPPORT_LINKS
-

-
-                    <Icon name={item.icon} /> {item.link}
-                 {if item.text? then item.text}
+        for name, data of @props.support_links
+

+
+                    <Icon name={data.icon} fixedWidth /> {data.link}
+                 {if data.text? then data.text}

     render : ->
@@ -258,7 +261,8 @@
              1147802,
              1020378 and
              1015114), and
-             The Google Education Grant program.
+             The Google
+            Education Grant program.

     launched :
         "SageMathCloud first launched in April, 2013."
@@ -286,10 +290,11 @@
 HelpPageGettingStartedSection = rclass

     get_panel_header : (icon, header) ->
-
<Icon name={icon} /> {header}

+
<Icon name={icon} fixedWidth /> {header}

     insert_sample_function : ->
         '$J_\\alpha(x) = \\sum\\limits_{m=0}^\\infty \\frac{(-1)^m}{m! \\, \\Gamma(m + \\alpha + 1)}{\\left({\\frac{x}{2}}\\right)}^{2 m + \\alpha}$'
+
     render : ->

<Icon name="cubes" /> Getting started with SageMathCloud

@@ -504,7 +509,7 @@

 SageMath, Python, LaTeX, and terminals in your browser

-                    <HelpPageSupportSection />
+                    <HelpPageSupportSection support_links={SUPPORT_LINKS} />
                     <HelpPageAboutSection />

                     <FluxComponent flux={flux} connectToStores={'server_stats'}>


Remove Resource service injection

Remove Resource service injection

--- app/assets/javascripts/hyper_admin/angularjs/controllers/main_ctrl.js.coffee
+++ app/assets/javascripts/hyper_admin/angularjs/controllers/main_ctrl.js.coffee
@@ -1,3 +1,3 @@
 angular.module("hyperadmin")
-  .controller "MainCtrl", (Resource) ->
+  .controller "MainCtrl", ->
     this


fixed ckeditor

fixed ckeditor

--- app/assets/javascripts/locomotive/views/content_entries/_form_view.js.coffee
+++ app/assets/javascripts/locomotive/views/content_entries/_form_view.js.coffee
@@ -319,13 +319,18 @@
   setup_image_repository_inputs: ->
     $(".image-repository").each () ->
       $input = $(this).find("input")
+      input_name = $input.attr("name")
+      CKEDITOR.replace(input_name)
+      CKEDITOR.on('instanceReady', ->
+        $(".cke").hide()
+      )
+
       field_id = $(this).attr("id")
       name = field_id.replace("_input", "").replace("content_entry_", "")
-      ckeditor_id = "#{name}_ckeditor"
+      ckeditor_id = "content_entry_#{name}"
       ckeditor_url = "/ckeditor/folders?CKEditor=#{ckeditor_id}&content_entry_field_id=#{field_id}&CKEditorFuncNum=1&langCode=en"
+
       value = $input.val()
-
-      $input.hide()

       if value.length > 0
         path_parts = value.split("/")


:lipstick: use ?= for assignments

:lipstick: use ?= for assignments

--- src/buffered-node-process.coffee
+++ src/buffered-node-process.coffee
@@ -18,8 +18,8 @@
         process.execPath

     # Tell atom-shell to run like upstream node.
-    options = {} if not options?
-    options.env = Object.create(process.env) if not options.env?
+    options ?= {}
+    options.env ?= Object.create(process.env)
     options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1

     args.unshift(command)


add perspective properties to Screen object

--- framer/Context.coffee
+++ framer/Context.coffee
@@ -132,6 +132,33 @@
 		get: ->
 			return @_perspective
 		set: (value) ->
-			@_perspective = value
-			@_rootElement?.style["perspective"] = @_perspective
-			@_rootElement?.style["webkitPerspective"] = @_perspective
+			if _.isNumber(value)
+				@_perspective = value
+				@_rootElement?.style["perspective"] = @_perspective
+				@_rootElement?.style["webkitPerspective"] = @_perspective
+
+	_updatePerspective: ->
+		@_rootElement?.style["webkitPerspectiveOrigin"] = "#{@perspectiveOriginX * 100}% #{@perspectiveOriginY * 100}%"
+
+	@define "perspectiveOriginX",
+		get: ->
+			if _.isNumber(@_perspectiveOriginX)
+				return @_perspectiveOriginX
+			else
+				return .5
+		set: (value) ->
+			if _.isNumber(value)
+				@_perspectiveOriginX = value
+				@_updatePerspective()
+
+	@define "perspectiveOriginY",
+		get: ->
+			if _.isNumber(@_perspectiveOriginY)
+				return @_perspectiveOriginY
+			else
+				return .5
+		set: (value) ->
+			if _.isNumber(value)
+				@_perspectiveOriginY = value
+				@_updatePerspective()
+


Fix error message wordings in command/query factory

--- src/command_query_factory.coffee
+++ src/command_query_factory.coffee
@@ -44,9 +44,9 @@
       context.query queryName, params
     , timeout
     .catch (error) ->
-      if error?.message?.indexOf('waitForCondition') > -1
+      if error?.message?.indexOf('waitForResult') > -1
         throw new Error """
-          waitUntilQueryIsReady timed out for query '#{queryName}' on context '#{context.name}'
+          waitForQueryToReturnResult timed out for query '#{queryName}' on context '#{context.name}'
           with params #{JSON.stringify(params)}
         """
       else
@@ -85,7 +85,7 @@
             return

           reject new Error """
-            waitForCondition timed out for command '#{promiseFactory.toString()}'
+            waitForResult timed out for '#{promiseFactory.toString()}'
           """
         .catch reject


fixed wrong parameter order

fixed wrong parameter order

--- main.coffee
+++ main.coffee
@@ -4,7 +4,7 @@
 module.exports =

   createServer: (port) ->
-    new serverTM false, port
+    new serverTM port

   createClient: (url, name, color) ->
     new clientTM url, name, color


Fixed browser incompatibility

Fixed browser incompatibility

--- main.coffee
+++ main.coffee
@@ -29,7 +29,10 @@
 	font_context.fillText das_text, padding/2, font_canvas.height-padding/2-4

 	image = new Image()
-	image.src = font_canvas.toDataURL('image/url')
+	# I have to set width and height or it won't work in Safari and Firefox
+	image.width = font_canvas.width
+	image.height = font_canvas.height
+	image.src = font_canvas.toDataURL()
 	return image

 class Player


local_hub -- make it so if the sage server dies for any reason then it gets automatically restarted (since this has happened a lot to users lately)

--- salvus/local_hub.coffee
+++ salvus/local_hub.coffee
@@ -245,7 +245,34 @@
 ###############################################
 # Direct Sage socket session -- used internally in local hub, e.g., to assist CodeMirror editors...
 ###############################################
-get_sage_socket = (cb) ->  # cb(err, socket that is ready to use)
+
+restart_sage_server = (cb) ->
+    misc_node.execute_code
+        command     : "sage_server stop; sage_server start"
+        timeout     : 30
+        err_on_exit : true
+        bash        : true
+        cb          : cb
+
+get_sage_socket = (cb) ->
+    _get_sage_socket (err, socket) ->
+        if not err
+            cb(undefined, socket)
+        else
+            # Failed for some reason: try to restart one time, then try again.
+            # We do this because the Sage server can easily get killed due to out of memory conditions.
+            # But we don't constantly try to restart the server, since it can easily fail to start if
+            # there is something wrong with a local Sage install.
+            # Note that restarting the sage server doesn't impact currently running worksheets (they
+            # have their own process that isn't killed).
+            restart_sage_server (err) ->
+                if err
+                    cb(err)
+                else
+                    _get_sage_socket(cb)
+
+
+_get_sage_socket = (cb) ->  # cb(err, socket that is ready to use)
     sage_socket = undefined
     port = undefined
     async.series([


local_hub: put a lock in when restarting sage server

--- salvus/local_hub.coffee
+++ salvus/local_hub.coffee
@@ -363,13 +363,20 @@
 # Direct Sage socket session -- used internally in local hub, e.g., to assist CodeMirror editors...
 ###############################################

+_restarting_sage_server = false
 restart_sage_server = (cb) ->
+    if _restarting_sage_server
+        cb("already restarting sage server")
+        return
+    _restarting_sage_server = true
     misc_node.execute_code
         command     : "sage_server stop; sage_server start"
         timeout     : 30
         err_on_exit : true
         bash        : true
-        cb          : cb
+        cb          : (err) ->
+            _restarting_sage_server = false
+            cb(err)

 get_sage_socket = (cb) ->
     _get_sage_socket (err, socket) ->


fix some edge cases due to fixing issues related to properly cleaning up sockets to local hubs

--- salvus/misc_node.coffee
+++ salvus/misc_node.coffee
@@ -203,6 +203,10 @@
         timeout : 5
         cb      : required

+    if not (port > 0 and port <  65536)
+        opts.cb("connect_to_locked_socket -- RangeError: port should be > 0 and < 65536: #{port}")
+        return
+
     winston.debug("misc_node: connecting to a locked socket on port #{port}...")
     timer = undefined

@@ -211,7 +215,7 @@
         winston.debug(m)
         cb?(m)
         cb = undefined  # NOTE: here and everywhere below we set cb to undefined after calling it, and only call it if defined, since the event and timer callback stuff is very hard to do right here without calling cb more than once (which is VERY bad to do).
-        socket.end()
+        socket?.end()
         timer = undefined

     timer = setTimeout(timed_out, timeout*1000)


Updated Hebrew translation to 'about.cson'

--- def/he/about.cson
+++ def/he/about.cson
@@ -1,6 +1,6 @@
 About:
   tabTitle: "אודות"
-  releaseNotes: "הערות עדכון"
+  releaseNotes: "RealA10N תורגם לעברית על ידי"
   newUpdate: "עדכון חדש"
   upToDate: "אטום מעודכן!"
   checkingForUpdates: "סורק עבור עדכונים..."


Adds image sets and artwork metadata

Adds image sets and artwork metadata

--- node/fabricate.coffee
+++ node/fabricate.coffee
@@ -998,6 +998,56 @@
               "541c4d3f7261692d70560000"
             ],
             "layout": "overflow_fillwidth"
+            "artworks": [
+              {
+                "type": "artwork",
+                "id": "5400a15e7261692d67530000",
+                "slug": "poppies",
+                "date": "1991",
+                "title": "Poppies",
+                "image": "https://d32dm0rphc51dk.cloudfront.net/drYmDIGBNsc49rrtiLzlbw/larger.jpg",
+                "partner": {
+                  "name": "Pace",
+                  "slug": "pace"
+                },
+                "artist": {
+                  "name": "Poppin Pop",
+                  "slug": "poppin-pop"
+                }
+              },
+              {
+                "type": "artwork",
+                "id": "541c4d3f7261692d91750000",
+                "slug": "lillies",
+                "date": "1992",
+                "title": "Lillies",
+                "image": "https://d32dm0rphc51dk.cloudfront.net/drYmDIGBNsc49rrtiLzlbw/larger.jpg",
+                "partner": {
+                  "name": "Pace",
+                  "slug": "pace"
+                },
+                "artist": {
+                  "name": "Poppin Pop",
+                  "slug": "poppin-pop"
+                }
+              },
+              {
+                "type": "artwork",
+                "id": "541c4d3f7261692d70560000",
+                "slug": "buffalo-herd",
+                "date": "1991",
+                "title": "Buffalo Herd",
+                "image": "https://d32dm0rphc51dk.cloudfront.net/drYmDIGBNsc49rrtiLzlbw/larger.jpg",
+                "partner": {
+                  "name": "Gagosian",
+                  "slug": "gagosian"
+                },
+                "artist": {
+                  "name": "Lovely Lady",
+                  "slug": "lovely-lady"
+                }
+              }
+            ]
           },
           {
             "type": "text",
@@ -1037,7 +1087,67 @@
               "541c4d3f7261692d84ba0000",
               "541c4d3f72616929e50e0600"
             ],
-            "layout": "overflow_fillwidth"
+            "layout": "overflow_fillwidth",
+            "artworks": [
+              {
+                "type": "artwork",
+                "id": "541c4d3f7261692d84ba0000",
+                "slug": "canary",
+                "date": "1993",
+                "title": "Canary",
+                "image": "https://d32dm0rphc51dk.cloudfront.net/drYmDIGBNsc49rrtiLzlbw/larger.jpg",
+                "partner": {
+                  "name": "Pace",
+                  "slug": "pace"
+                },
+                "artist": {
+                  "name": "Can Canary",
+                  "slug": "can-canary"
+                }
+              },
+              {
+                "type": "artwork",
+                "id": "541c4d3f72616929e50e0600",
+                "slug": "burrito-man",
+                "date": "1992",
+                "title": "Burrito Man",
+                "image": "https://d32dm0rphc51dk.cloudfront.net/drYmDIGBNsc49rrtiLzlbw/larger.jpg",
+                "partner": {
+                  "name": "FoodOrg",
+                  "slug": "foodorg"
+                },
+                "artist": {
+                  "name": "Bob Burrito",
+                  "slug": "bob-burrito"
+                }
+              },
+            ]
+          },
+          {
+            "type": "image_set",
+            "images": [
+              {
+                "type": 'image',
+                "url": 'https://image.png',
+                "caption": 'Trademarked',
+              },
+              {
+                "type": 'artwork',
+                "id": '123',
+                "slug": 'andy-warhol',
+                "title": 'The Piece',
+                "date": '2015-04-01',
+                "image": 'http://image.png',
+                "partner": {
+                  "name": "FoodOrg",
+                  "slug": "foodorg"
+                },
+                "artist": {
+                  "name": "Bob Burrito",
+                  "slug": "bob-burrito"
+                }
+              }
+            ]
           },
           {
             "type": "text",
@@ -1073,7 +1183,9 @@
         "section_ids": [],
         "biography_for_artist_id": null,
         "keywords": ['stellar', 'ruby', 'asia'],
-        "id": _.uniqueId()
+        "id": _.uniqueId(),
+        "channel_id": "541c4d3f72616929e50e0600",
+        "partner_channel_id": null
       }

   , extObj


atom: update snippets for sns

atom: update snippets for sns

--- .atom/snippets/sns-snippets.cson
+++ .atom/snippets/sns-snippets.cson
@@ -44,21 +44,20 @@
     ngComponent:
         prefix: 'cmpt'
         body: """
-            import angular from 'angular';
             import mod from '$1';

             export const fullName = 'ks$2';
             mod.component(fullName, {
                 bindings: {
                 },
-                controller: $2Ctrl,
                 template: '',
+                controller: [
+                    $2Ctrl
+                ],
             });

-            $2Ctrl.$inject = [
-            ];
             function $2Ctrl() {
-                const $ctrl = angular.extend(this, {
+                const $ctrl = Object.assign(this, {
                 });
             }

@@ -70,12 +69,12 @@
             import mod from '$1';

             export const fullName = mod.name + '.$2';
-            mod.service(fullName, $3);
+            mod.service(fullName, [
+                $3
+            ]);

-            $3.$inject = [
-            ];
             function $3() {
-                const $2 = angular.extend(this, {
+                const $2 = Object.assign(this, {
                 });
             }
             """
@@ -85,9 +84,10 @@
             import * as tests from 'tests/main';

             describe(__filename, function () {
-                let ctx;
+                let ctx, local, uf;
                 Given(function () {
                     ctx = tests.getCtx();
+                    local = {};
                 });

                 describe('$1', function () {


Adding gist links for unauthed users

Adding gist links for unauthed users

--- scripts/triggers.coffee
+++ scripts/triggers.coffee
@@ -7,17 +7,20 @@
 # Notes:
 #   Add helpful triggers when you think of them!

+pasteUrl = "http://laravel.io/bin or https://gist.github.com"
+helpUrl  = "http://help.laravel.io"
+
 triggers =
   # Question Assistance
   '!dataja'       : "Don't ask to ask, Just ask!"
   '!tryit'        : "Try it and see™. You learn much more by experimentation than by asking without having even tried."
   '!explain'      : "Explain your business goals, such as what website feature you're making, instead of the technical problem you're facing. We can give you much better answers this way."
-  '!help'         : "Before asking for help, use http://help.laravel.io to provide us the information we need to help you - Laravel version, expected/actual behavior, and all relevant code. Paste the link here when done. Thanks!"
-  '!helpme'       : "Before asking for help, use http://help.laravel.io to provide us the information we need to help you - Laravel version, expected/actual behavior, and all relevant code. Paste the link here when done. Thanks!"
-  '!paste'        : "You may paste your code at http://laravel.io/bin"
-  '!dontspam'     : "Please don't paste your code here. Instead, use http://laravel.io/bin"
+  '!help'         : "Before asking for help, use #{helpUrl} to provide us the information we need to help you - Laravel version, expected/actual behavior, and all relevant code. Paste the link here when done. Thanks!"
+  '!helpme'       : "Before asking for help, use #{helpUrl} to provide us the information we need to help you - Laravel version, expected/actual behavior, and all relevant code. Paste the link here when done. Thanks!"
+  '!paste'        : "You may paste your code at #{pasteUrl}.  Please avoid using pastebin.com"
+  '!dontspam'     : "Please don't paste your code here. Instead, use #{pasteUrl}"
   '!debug'        : "Open your <project>/app/config/app.php and set the debug key to true"
-  '!pb'           : "Please avoid using pastebin.com as it is slow and forces others to look at ads.  Please use http://laravel.io/bin.  Thanks!"
+  '!pb'           : "Please avoid using pastebin.com as it is slow and forces others to look at ads.  Please use #{pasteUrl}.  Thanks!"
   '!rules'        : "You may review our room rules at http://goo.gl/Tl77U2"
   '!docs'         : "The !docs syntax is deprecated, please use `Liona show <user> docs for <topic>`"


adding who will be triumphant

adding who will be triumphant

--- scripts/offshore.coffee
+++ scripts/offshore.coffee
@@ -13,6 +13,7 @@
 #   hubot who will be victorious
 #   hubot who will prevail
 #   hubot who will unleash righteous vengeance upon evil
+#   hubot who will be triumphant
 #
 # Author:
 #   Mateusz Kopij <sagasu>
@@ -36,6 +37,9 @@
   robot.respond /who will be victorious/i, (msg)->
     msg.send "The Offshore team! :sachin: :prem: :kostas: :chris: :drmatt:"

+  robot.respond /who will be triumphant/i, (msg)->
+    msg.send "The Offshore team! :sachin: :prem: :kostas: :chris: :drmatt:"
+
   robot.respond /who will prevail/i, (msg)->
     msg.send "The Offshore team! :sachin: :prem: :kostas: :chris: :drmatt:"


add crash reason to similarity check

add crash reason to similarity check

--- server/query-api.coffee
+++ server/query-api.coffee
@@ -8,7 +8,7 @@

 crash_similarity = (crashreport) ->

-    stackframes_to_similarity_value = (stackframes) ->
+    stack_similarity_value = (stackframes) ->
         similarity = []
         for i in [0..(STACK_SIMILARITY_LINES-1)]
             if stackframes[i]?
@@ -39,7 +39,10 @@
         #console.log stackframes #debug
         return stackframes

-    return stackframes_to_similarity_value(parse_stacktrace(crashreport["stack-trace"].crashstack))
+    similarity_value =  crashreport["stack-trace"].crash_reason
+    similarity_value += " " + stack_similarity_value(parse_stacktrace(crashreport["stack-trace"].crashstack))
+
+    return similarity_value

 get_crashreports = (cb) ->


generate unknown app shorturls

generate unknown app shorturls

--- cache_builder/cb.coffee
+++ cache_builder/cb.coffee
@@ -96,6 +96,8 @@
       url = geturl+'?u='+encodeURIComponent(fileurl)+'&t='+encodeURIComponent(title)

       add_shorturl shorturls,url
+      # no helper
+      add_shorturl shorturls,url+'&a='

       # each helper app
       for appentry in feed.entry
@@ -106,6 +108,7 @@
             link.$.href
           if appurls?.length > 0
             add_shorturl shorturls,url+'&a='+encodeURIComponent(appurls[0])
+

 # cache entry for each
 make_cache = (feed,cache) ->


Fixed gruntfile to use new bower folder

Fixed gruntfile to use new bower folder


Added default profile.name (matches username)

--- server/autoRoles.coffee
+++ server/autoRoles.coffee
@@ -2,6 +2,8 @@
     Accounts.onCreateUser (options, user) ->
         # Keep profile copy
         if options.profile then user.profile = options.profile
+        # Add a default profile.name if necessary
+        if not user.profile.name then user.profile.name = user.username
         # Add automatic roles
         user.roles = Meteor.settings.automaticRoles
         return user


updated deploy.coffee and add adapater support

--- core/deploy.coffee
+++ core/deploy.coffee
@@ -147,6 +147,53 @@
         else
             callback()
     moduleArrayCallback()
+
+initLocationAdapter = (mew,location,config,callback)->
+    adapterArray = []
+    if config.adapters
+        for adapter of config.adapters
+            idxAdapter = adapter.indexOf("$")
+            adapterName = ""
+            adapterIndex = ""
+            if idxAdapter < 0
+                adapterName = adapter
+                adapterIndex = ""
+            else
+                adapterName = adapter.substr(0,idxAdapter)
+                adapterIndex = adapter.substr(idxAdapter+1)
+            if Fs.existsSync(getSourceFile("/mew_modules/@#{adapterName}/package.json"))
+                adapterArray.push {
+                    name : adapterName,
+                    index : adapterIndex,
+                    config : config.adapters[adapter]
+                }
+    adapterArrayCallback = ->
+        adapter = adapterArray.shift()
+        if adapter
+            mew.logger.debug "adapter : [#{adapter.name}$#{adapter.index}] deploy with config : #{JSON.stringify(adapter.config,null,4)}"
+            profileFileContent = ""
+            profileFile = getLocationFile(location,"/var/conf/@#{adapter.name}$#{adapter.index}")
+            for key of adapter.config
+                if typeof key is 'string' and typeof adapter.config[key] isnt 'object'
+                    profileFileContent = "#{profileFileContent}\n#{key}=#{adapter.config[key]}"
+            Fs.writeFile profileFile,profileFileContent,(err)->
+                if err
+                    callback(err)
+                else
+                    Fse.overwriteRecursive getSourceFile("/mew_modules/@#{adapter.name}"),makeLocationDir(location,"/mew_modules/@#{adapter.name}"),(err)->
+                        if err
+                            callback(err)
+                        else
+                            if typeof config.profile is 'undefined'
+                                config.profile={}
+                            if config.profile.MEWBOT_ADAPTER
+                                config.profile.MEWBOT_ADAPTER = "#{config.profile.MEWBOT_ADAPTER},#{adapter.name}[#{adapter.name}$#{adapter.index}]"
+                            else
+                                config.profile.MEWBOT_ADAPTER = "#{adapter.name}[#{adapter.name}$#{adapter.index}]"
+                            adapterArrayCallback()
+        else
+            callback()
+    adapterArrayCallback()

 initLocationService = (location,config,callback)->
     serviceArray = []
@@ -216,12 +263,14 @@
                                     return callback(err) if err
                                     initLocationService location,config,(err)=>
                                         return callback(err) if err
-                                        initLocationProfile @mew,location,config,(err)=>
+                                        initLocationAdapter @mew,location,config,(err)=>
                                             return callback(err) if err
-                                            initLocationDataCopy @mew,location,config,(err)=>
+                                            initLocationProfile @mew,location,config,(err)=>
                                                 return callback(err) if err
-                                                initIgnoreProfile @mew,location,config,(err)=>
-                                                    callback(err)
+                                                initLocationDataCopy @mew,location,config,(err)=>
+                                                    return callback(err) if err
+                                                    initIgnoreProfile @mew,location,config,(err)=>
+                                                        callback(err)
                         else
                             callback("location is not a directory")
         else


fix(send): fix linting error

fix(send): fix linting error

--- src/flux/tasks/send-draft.coffee
+++ src/flux/tasks/send-draft.coffee
@@ -31,7 +31,7 @@
     new Promise (resolve, reject) =>
       # Fetch the latest draft data to make sure we make the request with the most
       # recent draft version
-      DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
+      DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) ->
         # The draft may have been deleted by another task. Nothing we can do.
         return reject(new Error("We couldn't find the saved draft.")) unless draft

@@ -47,7 +47,7 @@
           method: 'POST'
           body: body
           returnsModel: true
-          success: =>
+          success: ->
             atom.playSound('mail_sent.ogg')
             Actions.postNotification({message: "Sent!", type: 'success'})
             DatabaseStore.unpersistModel(draft).then(resolve)


Fix conditional

Fix conditional

--- lib/actions/device.coffee
+++ lib/actions/device.coffee
@@ -190,7 +190,7 @@

 		], (error) ->
 			return done(error) if os.platform() isnt 'win32'
-			if error? and error.code isnt 'EPERM'
+			if error? and (error.code is 'EPERM' or error.code is 'EACCES')
 				windosu = require('windosu')

 				# Need to escape everypath to avoid errors


added a custom event named hide for dropdown and for modularity

--- src/coffee/responsive.breadcrumbs.coffee
+++ src/coffee/responsive.breadcrumbs.coffee
@@ -262,6 +262,26 @@

                 e.preventDefault()

+            # bind custom event named close to close or hide the dropdown
+            o.dropdownWrapper.on "hide.#{ pluginName }", (e) ->
+                current = $ this
+
+                o.opts.holderAnimation.onBeforeHide(_this) if $.isFunction(o.opts.holderAnimation.onBeforeHide)
+
+                current
+                    .stop(true, true)
+                    .fadeOut
+                        duration: o.opts.holderAnimation.hideDuration
+                        easing:   o.opts.holderAnimation.hideEasing
+                        complete: ->
+                            o.opts.holderAnimation.onHide(_this) if $.isFunction(o.opts.holderAnimation.onHide)
+
+                            # remove the hover class to the holder element
+                            o.el
+                                .children(".#{ pluginName.toLowerCase() }-holder")
+                                .removeClass o.opts.holder.hoverClass
+
+                            o.opts.holderAnimation.onAfterHide(_this) if $.isFunction(o.opts.holderAnimation.onAfterHide)

             # delegate events for non-touch devices
             unless Modernizr.touch
@@ -272,7 +292,7 @@
                 # delegate the mouseleave event for hoverOut to the holder element
                 o.el.on "mouseleave.#{ pluginName }", ".#{ pluginName.toLowerCase() }-holder", ->
                     o.dropdownTimer = window.setTimeout( ->
-                        o.dropdownWrapper.trigger "#{ o.hoverOut }.#{ pluginName }"
+                        o.dropdownWrapper.trigger "mouseleave.#{ pluginName }"
                     , 500)

                 # bind the mouseenter event for hoverIn to the dropdownWrapper
@@ -281,46 +301,13 @@

                 # bind the mouseleave event for hoverOut to the dropdownWrapper
                 o.dropdownWrapper.on "mouseleave.#{ pluginName }", ->
-                    current = $ this
-
-                    o.opts.holderAnimation.onBeforeHide(_this) if $.isFunction(o.opts.holderAnimation.onBeforeHide)
-
-                    current
-                        .stop(true, true)
-                        .fadeOut
-                            duration: o.opts.holderAnimation.hideDuration
-                            easing:   o.opts.holderAnimation.hideEasing
-                            complete: ->
-                                o.opts.holderAnimation.onHide(_this) if $.isFunction(o.opts.holderAnimation.onHide)
-
-                                # remove the hover class to the holder element
-                                o.el
-                                    .children(".#{ pluginName.toLowerCase() }-holder")
-                                    .removeClass o.opts.holder.hoverClass
-
-                                o.opts.holderAnimation.onAfterHide(_this) if $.isFunction(o.opts.holderAnimation.onAfterHide)
+                    ($ this).trigger "hide.#{ pluginName }"
             else
                 ($ document.body).on "#{ o.hoverIn }.#{ pluginName }", (e) ->
                     target    = $ e.target
                     wrapperId = o.dropdownWrapper.attr 'id'

-                    if target.parents(".#{ o.opts.holder.class }").length is 0 and target.parents("##{ wrapperId }").length is 0
-                        o.opts.holderAnimation.onBeforeHide(_this) if $.isFunction(o.opts.holderAnimation.onBeforeHide)
-
-                        o.dropdownWrapper
-                            .stop(true, true)
-                            .fadeOut
-                                duration: o.opts.holderAnimation.hideDuration
-                                easing:   o.opts.holderAnimation.hideEasing
-                                complete: ->
-                                    o.opts.holderAnimation.onHide(_this) if $.isFunction(o.opts.holderAnimation.onHide)
-
-                                    # remove the hover class to the holder element
-                                    o.el
-                                        .children(".#{ pluginName.toLowerCase() }-holder")
-                                        .removeClass o.opts.holder.hoverClass
-
-                                    o.opts.holderAnimation.onAfterHide(_this) if $.isFunction(o.opts.holderAnimation.onAfterHide)
+                    o.dropdownWrapper.trigger("hide.#{ pluginName }") if target.parents(".#{ o.opts.holder.class }").length is 0 and target.parents("##{ wrapperId }").length is 0

             # bind resize event to the window


fix a bug

fix a bug

--- lib/atom-chameleon.coffee
+++ lib/atom-chameleon.coffee
@@ -156,7 +156,7 @@
     .then (projectPath)->
       atom.open {pathsToOpen: [projectPath]}

-      unless atom.packages.getActivePackage('tree-view').mainModule.treeView.isHidden() is null
+      if atom.packages.getActivePackage('tree-view').mainModule.treeView
         if atom.packages.getActivePackage('tree-view').mainModule.treeView.isHidden()
           atom.workspaceView.trigger "tree-view:toggle"


Pinch fix

Pinch fix

--- framer/GestureInputRecognizer.coffee
+++ framer/GestureInputRecognizer.coffee
@@ -16,10 +16,12 @@
 TouchMove = "touchmove"
 TouchEnd = "touchend"

-if not Utils.isTouch()
-	TouchStart = "mousedown"
-	TouchMove = "mousemove"
-	TouchEnd = "mouseup"
+# Fixme: this breaks pinch
+
+# if not Utils.isTouch()
+# 	TouchStart = "mousedown"
+# 	TouchMove = "mousemove"
+# 	TouchEnd = "mouseup"

 class exports.GestureInputRecognizer


Parámetros para Kruskal arreglados

Parámetros para Kruskal arreglados

--- greedy/kruskal.coffee
+++ greedy/kruskal.coffee
@@ -3,7 +3,7 @@
 ###*
  * Kruskal
  * @param {object} Grafo
- * @return {object} Árbol de recubrimiento mínimo
+ * @return {object[]} Árbol de recubrimiento mínimo (arreglo de aristas)
 ###
 kruskal = (grafo) ->
   vertices = grafo.vertices.slice() # clono los vertices


update resolveDiscussion state after reply and resolve/reopen button completes.

--- resources/assets/coffee/react/beatmap-discussions/new-reply.coffee
+++ resources/assets/coffee/react/beatmap-discussions/new-reply.coffee
@@ -176,6 +176,7 @@
       @setState
         message: ''
         editing: false
+        resolveDiscussion: resolved
       $.publish 'beatmapDiscussionPost:markRead', id: data.beatmap_discussion_post_ids
       $.publish 'beatmapsetDiscussion:update', beatmapsetDiscussion: data.beatmapset_discussion


feat: add the ability to set filename just before upload

--- cgFileUploadCtrl.coffee
+++ cgFileUploadCtrl.coffee
@@ -49,12 +49,7 @@
             @_disabled = false
             @_createInput()

-
-        _setDestFolder: (destFolder) =>
-            @awscredentials.destFolder = destFolder
-
-
-        _uploadS3: (file, filename) ->
+        _uploadS3: ({ file, filename, destFolder }) ->
             defer = $q.defer()
             awsS3 = @awscredentials

@@ -71,7 +66,7 @@
                 params: Bucket: awsS3.bucket
             )

-            filename = "#{ awsS3.destFolder }/#{ filename }" if awsS3.destFolder
+            filename = "#{ destFolder }/#{ filename }" if destFolder

             fileParams =
                 Key: filename
@@ -92,8 +87,7 @@

             return defer.promise

-
-        _uploadWorker: (file, filename) ->
+        _uploadWorker: ({ file, filename }) ->
             defer = $q.defer()
             script = document.querySelectorAll('[src*="cg-file-upload.js"]')[0]
             workerUrl = new URL script.src.replace 'file-upload.js', 'file-upload-worker.js'
@@ -123,26 +117,30 @@
             return unless file
             @_size = (file.size / Math.pow(1024, 2)).toFixed(2)
             @_mimetype = file.type
+            _filename = @_normalizeName(file.name)
             @onUploadStart?(
                 size: @_size
-                filename: file.name
+                filename: _filename
                 progress: 0
             )

-            @onBeforeUpload?(
-                filename: file.name
-                setDestFolder: @_setDestFolder
-            )
+            _ctrl =
+                filename: _filename
+                setDestFolder: (destFolder) -> _ctrl.destFolder = destFolder
+                setFileName: (filename) -> _ctrl.filename = filename
+            @onBeforeUpload?(_ctrl)

             @_disabled = true

             func = if @awscredentials then '_uploadS3' else '_uploadWorker'
-            filename = @_normalizeName(file.name)
-            this[func](file, filename).then(
+            this[func](
+                file: file
+                filename: _ctrl.filename
+                destFolder: _ctrl.destFolder
+            ).then(
                 (data) => @_loadHandler(data) # success
                 (err) => @_errorHandler(err) # error
                 (data) => @onProgress?(data) # notify
             )

-
     return cgFileUploadCtrl


use socket.io connect method instead of reconnect

the reconnect method tries 10 times by default, but we want to manage
reconnections ourselves

--- services/web/public/coffee/ide/connection/ConnectionManager.coffee
+++ services/web/public/coffee/ide/connection/ConnectionManager.coffee
@@ -207,7 +207,9 @@
 			delete @$scope.connection.reconnection_countdown
 			return if @connected
 			@$scope.connection.reconnecting = true
-			@ide.socket.socket.reconnect()
+			# use socket.io connect() here to make a single attempt, the
+			# reconnect() method makes multiple attempts
+			@ide.socket.socket.connect()
 			setTimeout (=> @startAutoReconnectCountdown() if !@connected), 2000

 		disconnectIfInactive: ()->


add unit test for precompile resources check

--- services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
+++ services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
@@ -33,7 +33,7 @@
 				getProjectDocsIfMatch: sinon.stub().callsArgWith(2,null,null)
 			"./ClsiCookieManager": @ClsiCookieManager
 			"./ClsiStateManager": @ClsiStateManager
-			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
+			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err: sinon.stub(), warn: sinon.stub() }
 			"request": @request = sinon.stub()
 			"./ClsiFormatChecker": @ClsiFormatChecker
 			"metrics-sharelatex": @Metrics =
@@ -121,6 +121,21 @@

 			it "should call the callback with a success status", ->
 				@callback.calledWith(null, @status, ).should.equal true
+
+		describe "when the resources fail the precompile check", ->
+			beforeEach ->
+				@ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed"))
+				@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
+					compile:
+						status: @status = "failure"
+				})
+				@ClsiManager.sendRequest @project_id, @user_id, {}, @callback
+
+			it "should call the callback only once", ->
+				@callback.calledOnce.should.equal true
+
+			it "should call the callback with an error", ->
+				@callback.calledWithExactly(new Error("failed")).should.equal true

 	describe "deleteAuxFiles", ->
 		beforeEach ->


Fix logging in event extension.

Fix logging in event extension.

--- src/js/jquery.extension.hlf.event.coffee
+++ src/js/jquery.extension.hlf.event.coffee
@@ -120,8 +120,8 @@
     # mouse-leave and will always trigger `truemouseleave`.
     teardownCheckIfNeeded = (event, $trigger, state) ->
       return no if event.type isnt 'mouseleave'
-      debugLog 'teardown'
       if state.timer.cleared is no
+        debugLog 'teardown'
         clearTimeout state.timer.timeout
         $trigger
           .removeData attr('timer')


BaseView::remove() also removes all its childViews

--- shared/base/view.coffee
+++ shared/base/view.coffee
@@ -207,6 +207,7 @@
     @childViews = BaseView.attach(@app, @)

   remove: ->
+    view.remove() for view in @childViews
     @childViews = null
     @parentView = null
     super


Adding getChildViewsByName and tests.

Adding getChildViewsByName and tests.

--- shared/base/view.coffee
+++ shared/base/view.coffee
@@ -50,6 +50,11 @@

   # Children of the current view.
   childViews: null
+
+  # Gets array of child views by their name
+  # Empty array is returned when no match is found
+  getChildViewsByName: (name) ->
+    _.where(@childViews, {name})

   # Get data for template.  This also acts as a view-model.
   # Try to return proper data if model or collection is available.


Fix Marbles.Collection: model_ids always defined

--- src/marbles/collection.js.coffee
+++ src/marbles/collection.js.coffee
@@ -1,5 +1,6 @@
 Marbles.Collection = class Collection
   @model: Marbles.Model
+  model_ids: new Array

   constructor: (options = {}) ->
     @append_raw(options.raw) if _.isArray(options.raw)


define heavily loaded slightly more aggressively

--- src/smc-hub/postgres-base.coffee
+++ src/smc-hub/postgres-base.coffee
@@ -65,7 +65,7 @@
                                     # identical permission checks in a single user query.
             cache_size      : 300   # cache this many queries; use @_query(cache:true, ...) to cache result
             concurrent_warn : 500
-            concurrent_heavily_loaded : 100 # when concurrent hits this, consider load "heavy"; this changes home some queries behave to be faster but provide less info
+            concurrent_heavily_loaded : 70 # when concurrent hits this, consider load "heavy"; this changes home some queries behave to be faster but provide less info
             ensure_exists   : true  # ensure database exists on startup (runs psql in a shell)
             timeout_ms      : DEFAULT_TIMEOUS_MS # **IMPORTANT: if *any* query takes this long, entire connection is terminated and recreated!**
             timeout_delay_ms : DEFAULT_TIMEOUT_DELAY_MS # Only reconnect on timeout this many ms after connect.  Motivation: on initial startup queries may take much longer due to competition with other clients.


project: use COCALC_PROJECT_ID and COCALC_USERNAME if they are both set.

--- src/smc-project/local_hub.coffee
+++ src/smc-project/local_hub.coffee
@@ -128,7 +128,7 @@
 init_info_json = (cb) ->  # NOTE: cb should only be required to guarantee info.json file written, not that INFO var is initialized.
     winston.debug("initializing INFO")
     filename = "#{SMC}/info.json"
-    if kucalc.IN_KUCALC and process.env.COCALC_PROJECT_ID? and process.env.COCALC_USERNAME?
+    if process.env.COCALC_PROJECT_ID? and process.env.COCALC_USERNAME?
         project_id = process.env.COCALC_PROJECT_ID
         username   = process.env.COCALC_USERNAME
     else


make it so shift click in edit mode does not clear selection

--- src/smc-webapp/jupyter/cell.cjsx
+++ src/smc-webapp/jupyter/cell.cjsx
@@ -85,7 +85,7 @@
     click_on_cell: (event) ->
         if not @props.actions?
             return
-        if event.shiftKey
+        if event.shiftKey and not @props.is_current
             misc_page.clear_selection()
             @props.actions.select_cell_range(@props.id)
         else


fix padding on WARNING bar at top of project

--- src/smc-webapp/project_page.cjsx
+++ src/smc-webapp/project_page.cjsx
@@ -182,20 +182,21 @@
         if not host and not internet
             return null
         styles =
-            padding : 2
-            paddingLeft : 7
+            padding      : 2
+            paddingLeft  : 7
             paddingRight : 7
-            cursor : 'pointer'
+            cursor       : 'pointer'
             marginBottom : 0
-            fontSize : 12
+            fontSize     : 12
         dismiss_styles =
-            display : 'inline-block'
-            float : 'right'
+            display    : 'inline-block'
+            float      : 'right'
             fontWeight : 700
-            top : -5
-            fontSize : 18
-            color : 'gray'
-            position : 'relative'
+            top        : -5
+            fontSize   : 18
+            color      : 'gray'
+            position   : 'relative'
+            height     : 0
         <Alert bsStyle='warning' style={styles}>
             <Icon name='exclamation-triangle' /> WARNING: This project runs {on a free server if host} {without internet access if internet} —
             @actions(project_id: @props.project_id).show_extra_free_warning()}> learn more...


Enhance spec by calling sync again.

Enhance spec by calling sync again.

--- src/spec/integration.spec.coffee
+++ src/spec/integration.spec.coffee
@@ -128,7 +128,6 @@
                   console.log msg
                   done()

-                console.log msg
                 expect(msg.status).toBe true
                 expect(_.size msg.message).toBe 4
                 expect(msg.message['No mastersku attribute!']).toBe 1
@@ -136,14 +135,26 @@
                 expect(msg.message['Price update postponed.']).toBe 1
                 expect(msg.message['There is no product in master for sku \'We add some content here in order to create the variant\'.']).toBe 1

-                @rest.GET "/products/#{masterProductId}", (error, response, body) ->
-                  console.log "P %j", body
+                @rest.GET "/products/#{masterProductId}", (error, response, body) =>
                   expect(response.statusCode).toBe 200
                   expect(_.size(body.masterData.current.masterVariant.prices) + _.size(body.masterData.current.variants[0].prices)).toBe 1
                   price = body.masterData.current.masterVariant.prices[0]
                   price or= body.masterData.current.variants[0].prices[0]
                   expect(price.value.currencyCode).toBe 'EUR'
-                  #expect(price.value.centAmount).toBe 1
                   expect(price.channel.typeId).toBe 'channel'
                   expect(price.channel.id).toBeDefined()
-                  done()+                  @priceSync.run (msg) =>
+                    unless msg.status
+                      console.log msg
+                      done()
+
+                    expect(msg.message['No mastersku attribute!']).toBe 1
+                    expect(msg.message['Prices updated.']).toBe 2
+                    expect(msg.message['There is no product in master for sku \'We add some content here in order to create the variant\'.']).toBe 1
+
+                    @rest.GET "/products/#{masterProductId}", (error, response, body) =>
+                      expect(response.statusCode).toBe 200
+                      expect(_.size(body.masterData.current.masterVariant.prices) + _.size(body.masterData.current.variants[0].prices)).toBe 2
+                      expect(price.channel.id).toBeDefined()
+
+                      done()


Raise timout to avoid problems.

Raise timout to avoid problems.

--- src/spec/integration.spec.coffee
+++ src/spec/integration.spec.coffee
@@ -8,6 +8,8 @@
 {SphereClient} = require 'sphere-node-sdk'

 describe 'integration test', ->
+
+  jasmine.getEnv().defaultTimeoutInterval = 10000

   beforeEach (done) ->


Start integrating the yardstick

Start integrating the yardstick

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -68,14 +68,15 @@
     @updating is false

   getStateForMeasurements: (screenRows) ->
-    screenRows = _.uniq(screenRows.concat(@getVisibleRows()))
-
     @updateVerticalDimensions()
     @updateScrollbarDimensions()
     @updateStartRow()
     @updateEndRow()

-    @updateTilesState(screenRows)
+    screenRows = _.without(screenRows, null, undefined, @getVisibleRows()...)
+
+    @updateVisibleTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState
+    @updateTilesState(screenRows, true)

     @state

@@ -84,10 +85,11 @@
   getState: ->
     @updating = true

-    @updateVerticalDimensions()
-    @updateScrollbarDimensions()
-    @updateStartRow()
-    @updateEndRow()
+    @linesYardstick.prepareScreenRowsForMeasurement([
+      @model.getLongestScreenRow()
+    ])
+    @deleteTemporaryTiles()
+
     @updateCommonGutterState()
     @updateHorizontalDimensions()
     @updateReflowState()
@@ -359,11 +361,16 @@
     [startRow...endRow]

   updateVisibleTilesState: ->
+    @updateTilesState(@getVisibleRows())
+
+  deleteTemporaryTiles: ->
+    for tileId, tileState of @state.content.tiles when tileState.temporary
+      delete @state.content.tiles[tileId]
+      delete @lineNumberGutter.tiles[tileId]
+
+  updateTilesState: (screenRows, temporary = false) ->
     return unless @startRow? and @endRow? and @lineHeight?

-    @updateTilesState(@getVisibleRows())
-
-  updateTilesState: (screenRows) ->
     screenRows.sort (a, b) -> a - b

     visibleTiles = {}
@@ -391,18 +398,22 @@
       tile.display = "block"
       tile.zIndex = zIndex
       tile.highlights ?= {}
+      tile.temporary = temporary

       gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {}
       gutterTile.top = tileStartRow * @lineHeight - @scrollTop
       gutterTile.height = @tileSize * @lineHeight
       gutterTile.display = "block"
       gutterTile.zIndex = zIndex
+      gutterTile.temporary = temporary

       @updateLinesState(tile, rowsWithinTile) if @shouldUpdateLinesState
       @updateLineNumbersState(gutterTile, rowsWithinTile) if @shouldUpdateLineNumbersState

       visibleTiles[tileStartRow] = true
       zIndex++
+
+    return if temporary

     if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
       mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
@@ -424,7 +435,7 @@
     for screenRow in screenRows
       line = @model.tokenizedLineForScreenRow(screenRow)
       unless line?
-        throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}")
+        throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")

       visibleLineIds[line.id] = true
       if tileState.lines.hasOwnProperty(line.id)


Remove caching by screen rows

Remove caching by screen rows

Because we cannot use them as a cache key, because markers' position can change
at any time. Performance-wise this is slow with many markers, as we need to do a
lot of buffer-to-screen conversions.

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -74,7 +74,7 @@
   getPreMeasurementState: ->
     @updating = true

-    @updateBlockDecorations() if @shouldUpdateBlockDecorations
+    @updateBlockDecorations()
     @updateVerticalDimensions()
     @updateScrollbarDimensions()

@@ -144,7 +144,6 @@
     @shouldUpdateHiddenInputState = false
     @shouldUpdateContentState = false
     @shouldUpdateDecorations = false
-    @shouldUpdateBlockDecorations = false
     @shouldUpdateLinesState = false
     @shouldUpdateTilesState = false
     @shouldUpdateCursorsState = false
@@ -163,7 +162,6 @@
     @shouldUpdateHiddenInputState = true
     @shouldUpdateContentState = true
     @shouldUpdateDecorations = true
-    @shouldUpdateBlockDecorations = true
     @shouldUpdateLinesState = true
     @shouldUpdateTilesState = true
     @shouldUpdateCursorsState = true
@@ -194,7 +192,6 @@
       @shouldUpdateLineNumbersState = true
       @shouldUpdateDecorations = true
       @shouldUpdateOverlaysState = true
-      @shouldUpdateBlockDecorations = true
       @shouldUpdateVerticalScrollState = true
       @shouldUpdateCustomGutterDecorationState = true
       @emitDidUpdateState()
@@ -1410,32 +1407,27 @@
   setBlockDecorationDimensions: (decoration, width, height) ->
     screenRow = decoration.getMarker().getHeadScreenPosition().row
     dimensions = {width, height}
-
-    screenRowDecorations = @blockDecorationsDimensionsByScreenRow[screenRow] ?= {}
-    screenRowDecorations[decoration.id] = dimensions
     @blockDecorationsDimensionsById[decoration.id] = dimensions

-    @setScreenRowHeight(
-      screenRow,
-      @lineHeight + @getBlockDecorationsHeight(_.values(screenRowDecorations))
-    )
-
-    @shouldUpdateBlockDecorations = true
     @shouldUpdateVerticalScrollState = true
     @emitDidUpdateState()

   updateBlockDecorations: ->
-    # TODO: Move this inside `DisplayBuffer`
+    @heightsByScreenRow = {}
     blockDecorations = {}
+
+    # TODO: move into DisplayBuffer
     for decoration in @model.getDecorations(type: "block")
       blockDecorations[decoration.id] = decoration

-    for screenRow, decorations of @blockDecorationsDimensionsByScreenRow
-      for decorationId of decorations when not blockDecorations.hasOwnProperty(decorationId)
+    for decorationId of @blockDecorationsDimensionsById
+      unless blockDecorations.hasOwnProperty(decorationId)
         delete @blockDecorationsDimensionsById[decorationId]
-        delete @blockDecorationsDimensionsByScreenRow[screenRow][decorationId]
-
-    @shouldUpdateVerticalScrollState = true
+
+    for decorationId, decoration of blockDecorations
+      screenRow = decoration.getMarker().getHeadScreenPosition().row
+      @heightsByScreenRow[screenRow] ?= @lineHeight
+      @heightsByScreenRow[screenRow] += @blockDecorationsDimensionsById[decorationId].height

   observeCursor: (cursor) ->
     didChangePositionDisposable = cursor.onDidChangePosition =>


adding monolingualtext as a possible datatype at simplify_claims

--- src/utils/simplify_claims.coffee
+++ src/utils/simplify_claims.coffee
@@ -15,6 +15,7 @@
     [datatype, datavalue] = [mainsnak.datatype, mainsnak.datavalue]
     switch datatype
       when 'string', 'commonsMedia' then value = datavalue.value
+      when 'monolingualtext' then value = datavalue.value.text
       when 'wikibase-item' then value = 'Q' + datavalue.value['numeric-id']
       when 'time' then value = wd_.normalizeWikidataTime(datavalue.value.time)
       else value = null


only clip renderers, other layer unclipped

--- static/coffee/common/plot.coffee
+++ static/coffee/common/plot.coffee
@@ -175,6 +175,9 @@

   render_deferred_components: (force) ->
     @ctx.clearRect(0,0,  @view_state.get('canvas_width'), @view_state.get('canvas_height'))
+
+    @ctx.save()
+
     @ctx.beginPath()
     @ctx.rect(
       @view_state.get('border_left'), @view_state.get('border_top'),
@@ -182,9 +185,16 @@
     )
     @ctx.clip()
     @ctx.beginPath()
-    all_views = _.flatten(_.map([@renderers, @axes, @tools], _.values))
-    for v in all_views
+
+    for v in _.flatten(_.map([@renderers], _.values))
       v.render()
+
+    @ctx.restore()
+
+    for v in _.flatten(_.map([@axes, @tools], _.values))
+      v.render()
+
+

 class Plot extends HasParent


Tests: Support running tests against multiple transports

--- spec/coordinator.coffee
+++ spec/coordinator.coffee
@@ -6,63 +6,70 @@
 runtime = require '../src/fakeruntime'
 transport = require '../src/transport'

-address = 'direct://broker1'
-address = 'amqp://localhost'
-address = 'mqtt://localhost'
+# Note: most require running an external broker service
+transports =
+  'MQTT': 'mqtt://localhost'
+#  'AMQP': 'amqp://localhost'
+  'direct': 'direct://broker1'

 describe 'Coordinator', ->
-  coordinator = null
-  first = null

-  beforeEach (done) ->
-    broker = transport.getBroker address
-    coordinator = new Coordinator broker
-    coordinator.start (err) ->
-      chai.expect(err).to.be.a 'null'
-      done()
+  Object.keys(transports).forEach (type) =>
+    address = transports[type]
+    coordinator = null
+    first = null

-  afterEach (done) ->
-    coordinator.stop () ->
-      coordinator = null
-      done()
+    describe ", transport=#{type}: ", () ->

-  describe 'creating participant', ->
-    it 'should emit participant-added', (done) ->
-      first = runtime.HelloParticipant transport.getClient address
-      coordinator.once 'participant-added', (participant) ->
-        chai.expect(participant).to.be.a 'object'
-        chai.expect(participant.id).to.equal first.definition.id
-        done()
-      first.start (err) -> chai.expect(err).to.be.a 'null'
+      beforeEach (done) ->
+        broker = transport.getBroker address
+        coordinator = new Coordinator broker
+        coordinator.start (err) ->
+          chai.expect(err).to.be.a 'null'
+          done()

-  describe 'sending data into participant input queue', ->
-    it 'should receive results on output queue', (done) ->
-      @timeout 4000
-      first = runtime.HelloParticipant transport.getClient address
-      coordinator.once 'participant-added', (participant) ->
+      afterEach (done) ->
+        coordinator.stop () ->
+          coordinator = null
+          done()

-        console.log 'SPECT participant added'
-        id = first.definition.id
-        coordinator.subscribeTo id, 'out', (data) ->
-          chai.expect(data).to.equal 'Hello Jon'
-          done()
-        coordinator.sendTo id, 'name', 'Jon'
-        console.log 'SPEC sent'
-      first.start (err) -> chai.expect(err).to.be.a 'null'
+      describe 'creating participant', ->
+        it 'should emit participant-added', (done) ->
+          first = runtime.HelloParticipant transport.getClient address
+          coordinator.once 'participant-added', (participant) ->
+            chai.expect(participant).to.be.a 'object'
+            chai.expect(participant.id).to.equal first.definition.id
+            done()
+          first.start (err) -> chai.expect(err).to.be.a 'null'

-  describe 'sending data to participant connected to another', ->
-    it 'should receive results at end of flow', (done) ->
-      first = runtime.HelloParticipant transport.getClient address
-      second = runtime.HelloParticipant transport.getClient address
-      participants = 0
-      coordinator.on 'participant-added', (participant) ->
-        participants = participants+1
-        return if participants != 2
-        coordinator.connect first.definition.id, 'out', second.definition.id, 'name'
-        coordinator.subscribeTo second.definition.id, 'out', (data) ->
-          chai.expect(data).to.equal 'Hello Hello Johnny'
-          done()
-        coordinator.sendTo first.definition.id, 'name', 'Johnny'
-        console.log 'SPEC sent'
-      first.start (err) -> chai.expect(err).to.be.a 'null'
-      second.start (err) -> chai.expect(err).to.be.a 'null'
+      describe 'sending data into participant input queue', ->
+        it 'should receive results on output queue', (done) ->
+          @timeout 4000
+          first = runtime.HelloParticipant transport.getClient address
+          coordinator.once 'participant-added', (participant) ->
+
+            console.log 'SPECT participant added'
+            id = first.definition.id
+            coordinator.subscribeTo id, 'out', (data) ->
+              chai.expect(data).to.equal 'Hello Jon'
+              done()
+            coordinator.sendTo id, 'name', 'Jon'
+            console.log 'SPEC sent'
+          first.start (err) -> chai.expect(err).to.be.a 'null'
+
+      describe 'sending data to participant connected to another', ->
+        it 'should receive results at end of flow', (done) ->
+          first = runtime.HelloParticipant transport.getClient address
+          second = runtime.HelloParticipant transport.getClient address
+          participants = 0
+          coordinator.on 'participant-added', (participant) ->
+            participants = participants+1
+            return if participants != 2
+            coordinator.connect first.definition.id, 'out', second.definition.id, 'name'
+            coordinator.subscribeTo second.definition.id, 'out', (data) ->
+              chai.expect(data).to.equal 'Hello Hello Johnny'
+              done()
+            coordinator.sendTo first.definition.id, 'name', 'Johnny'
+            console.log 'SPEC sent'
+          first.start (err) -> chai.expect(err).to.be.a 'null'
+          second.start (err) -> chai.expect(err).to.be.a 'null'


Test undo in all marker specs

Test undo in all marker specs

--- spec/marker-spec.coffee
+++ spec/marker-spec.coffee
@@ -350,7 +350,7 @@
           marker.on 'changed', (change) -> marker.changes.push(change)

       bufferChanges = []
-      buffer.on 'changed', (change) ->
+      buffer.once 'changed', (change) ->
         bufferChanges.push(change)
         for marker in allStrategies
           expect(marker.getRange()).toEqual [[0, 8], [0, 11]]
@@ -370,6 +370,28 @@
           bufferChanged: true
         }]

+      marker.changes = [] for marker in allStrategies
+      bufferChanges = []
+      buffer.once 'changed', (change) ->
+        bufferChanges.push(change)
+        for marker in allStrategies
+          expect(marker.getRange()).toEqual [[0, 6], [0, 9]]
+          expect(marker.isValid()).toBe true
+          expect(marker.changes.length).toBe 0
+
+      buffer.undo()
+      expect(bufferChanges.length).toBe 1
+
+      for marker in allStrategies
+        expect(marker.changes).toEqual [{
+          oldHeadPosition: [0, 11], newHeadPosition: [0, 9]
+          oldTailPosition: [0, 8], newTailPosition: [0, 6]
+          hadTail: true, hasTail: true
+          wasValid: true, isValid: true
+          oldState: {}, newState: {}
+          bufferChanged: true
+        }]
+
     describe "when a change precedes a marker", ->
       it "shifts the marker based on the characters inserted or removed by the change", ->
         buffer.setTextInRange([[0, 1], [0, 2]], "ABC")
@@ -380,6 +402,16 @@
         buffer.setTextInRange([[0, 1], [0, 1]], '\nDEF')
         for marker in allStrategies
           expect(marker.getRange()).toEqual [[1, 10], [1, 13]]
+          expect(marker.isValid()).toBe true
+
+        buffer.undo()
+        for marker in allStrategies
+          expect(marker.getRange()).toEqual [[0, 8], [0, 11]]
+          expect(marker.isValid()).toBe true
+
+        buffer.undo()
+        for marker in allStrategies
+          expect(marker.getRange()).toEqual [[0, 6], [0, 9]]
           expect(marker.isValid()).toBe true

     describe "when a change follows a marker", ->


delete status_monitor, use inner monitor instead


Don't require HistoryManager in spec-helper

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -9,7 +9,6 @@
 FindParentDir = require 'find-parent-dir'
 {CompositeDisposable} = require 'event-kit'

-{HistoryManager} = require '../src/history-manager'
 TextEditor = require '../src/text-editor'
 TextEditorElement = require '../src/text-editor-element'
 TextMateLanguageMode = require '../src/text-mate-language-mode'
@@ -64,7 +63,7 @@

 beforeEach ->
   # Do not clobber recent project history
-  spyOn(HistoryManager::, 'saveState').andReturn(Promise.resolve())
+  spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve())

   atom.project.setPaths([specProjectPath])


:lipstick:

:lipstick:

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -18,32 +18,13 @@
   constructor: (@message) ->
     @name = 'SpecError'

-getVimState = (args...) ->
-  [editor, file, callback] = []
-  switch args.length
-    when 1 then [callback] = args
-    when 2 then [file, callback] = args
-
-  waitsForPromise ->
-    atom.packages.activatePackage(packageName)
-
-  waitsForPromise ->
-    file = atom.project.resolvePath(file) if file
-    atom.workspace.open(file).then (e) ->
-      editor = e
-
-  runs ->
-    pack = atom.packages.getActivePackage(packageName)
-    main = pack.mainModule
-    vimState = main.getEditorState(editor)
-    {editorElement} = vimState
-    editorElement.addEventListener 'keydown', (e) ->
-      atom.keymaps.handleKeyboardEvent(e)
-
-    callback(vimState, new VimEditor(vimState))
-
+# Utils
+# -------------------------
 getView = (model) ->
   atom.views.getView(model)
+
+dispatch = (target, command) ->
+  atom.commands.dispatch(target, command)

 mockPlatform = (editorElement, platform) ->
   wrapper = document.createElement('div')
@@ -99,6 +80,33 @@
 toArray = (obj, cond=null) ->
   if _.isArray(cond ? obj) then obj else [obj]

+
+# Main
+# -------------------------
+getVimState = (args...) ->
+  [editor, file, callback] = []
+  switch args.length
+    when 1 then [callback] = args
+    when 2 then [file, callback] = args
+
+  waitsForPromise ->
+    atom.packages.activatePackage(packageName)
+
+  waitsForPromise ->
+    file = atom.project.resolvePath(file) if file
+    atom.workspace.open(file).then (e) ->
+      editor = e
+
+  runs ->
+    pack = atom.packages.getActivePackage(packageName)
+    main = pack.mainModule
+    vimState = main.getEditorState(editor)
+    {editorElement} = vimState
+    editorElement.addEventListener 'keydown', (e) ->
+      atom.keymaps.handleKeyboardEvent(e)
+
+    callback(vimState, new VimEditor(vimState))
+
 class VimEditor
   constructor: (@vimState) ->
     {@editor, @editorElement} = @vimState
@@ -112,37 +120,154 @@
     'text', 'cursor', 'cursorBuffer', 'addCursor', 'addCursors', 'register'
     'selectedBufferRange'
   ]
+  # Public
   set: (options) =>
     @validateOptions(options, setOptionsOrdered, 'Invalid set options')
     for name in setOptionsOrdered when options[name]?
       method = 'set' + _.capitalize(_.camelize(name))
       this[method](options[name])

-  setText: (text) =>
+  setText: (text) ->
     @editor.setText(text)

-  setCursor: (point) =>
+  setCursor: (point) ->
     @editor.setCursorScreenPosition(point)

-  setCursorBuffer: (cursor) =>
+  setCursorBuffer: (cursor) ->
     @editor.setCursorBufferPosition(cursor)

-  setAddCursor: (point) =>
+  setAddCursor: (point) ->
     @editor.addCursorAtBufferPosition(point)

-  setAddCursors: (points) =>
+  setAddCursors: (points) ->
     @setAddCursor(point) for point in points

-  setRegister: (register) =>
+  setRegister: (register) ->
     if _.isObject(register)
       for name, value of register
         @vimState.register.set(name, value)
     else
       @vimState.register.set '"', text: register

-  setSelectedBufferRange: (range) =>
+  setSelectedBufferRange: (range) ->
     @editor.setSelectedBufferRange(range)

+  ensureOptionsOrdered = [
+    'text', 'selectedText', 'selectedTextOrdered'
+    'cursor', 'cursors', 'cursorBuffer'
+    'register', 'numCursors', 'selectedScreenRange',
+    'selectedBufferRange',
+    'selectedBufferRangeOrdered',
+    'selectedBufferRangeStartRow',
+    'selectedBufferRangeEndRow',
+    'selectionIsReversed',
+    'scrollTop',
+    'mode',
+  ]
+  # Public
+  ensure: (args...) =>
+    switch args.length
+      when 1 then [options] = args
+      when 2 then [keystroke, options] = args
+    @validateOptions(options, ensureOptionsOrdered, 'Invalid ensure option')
+    # Input
+    unless _.isEmpty(keystroke)
+      @keystroke(keystroke, {element: @editorElement})
+
+    for name in ensureOptionsOrdered when options[name]?
+      method = 'ensure' + _.capitalize(_.camelize(name))
+      this[method](options[name])
+
+  ensureText: (text) ->
+    expect(@editor.getText()).toEqual(text)
+
+  ensureSelectedText: (text) ->
+    selections = @editor.getSelections()
+    texts = (s.getText() for s in selections)
+    expect(texts).toEqual(toArray(text))
+
+  ensureSelectedTextOrdered: (text) ->
+    selections = @editor.getSelectionsOrderedByBufferPosition()
+    texts = (s.getText() for s in selections)
+    expect(texts).toEqual(toArray(text))
+
+  ensureCursor: (cursor) ->
+    cursor = if cursor instanceof Point or not _.isArray(cursor[0])
+      [cursor]
+    else
+      cursor
+    points = @editor.getCursorScreenPositions()
+    expect(points).toEqual(toArray(cursor, cursor))
+
+  ensureCursors: (cursors) ->
+    points = @editor.getCursorScreenPositions()
+    expect(points).toEqual(cursors)
+
+  ensureCursorBuffer: (cursor) ->
+    points = @editor.getCursorBufferPositions()
+    expect(points).toEqual(toArray(cursor, cursor[0]))
+
+  ensureRegister: (register) ->
+    if _.isObject(register)
+      for name, value of register
+        reg = @vimState.register.get(name)
+        for prop, _value of value
+          expect(reg[prop]).toEqual(_value)
+    else
+      expect(@vimState.register.get('"').text).toBe register
+
+  ensureNumCursors: (number) ->
+    expect(@editor.getCursors()).toHaveLength number
+
+  ensureSelectedScreenRange: (range) ->
+    actual = @editor.getSelectedScreenRanges()
+    expected = toArray(range, range[0][0])
+    expect(actual).toEqual(expected)
+
+  ensureSelectedBufferRange: (range) ->
+    if (range instanceof Range) or
+        (not (range[0] instanceof Range)) and (not _.isArray(range[0][0]))
+      range = [range]
+    actual = @editor.getSelectedBufferRanges()
+    expect(actual).toEqual(range)
+
+  ensureSelectedBufferRangeOrdered: (range) ->
+    if (range instanceof Range) or
+        (not (range[0] instanceof Range)) and (not _.isArray(range[0][0]))
+      range = [range]
+    actual = @editor.getSelectionsOrderedByBufferPosition().map (e) -> e.getBufferRange()
+    expect(actual).toEqual(range)
+
+  ensureSelectedBufferRangeStartRow: (row) ->
+    {start} = @editor.getSelectedBufferRange()
+    expect(start.row).toEqual row
+
+  ensureSelectedBufferRangeEndRow: (row) ->
+    {end} = @editor.getSelectedBufferRange()
+    expect(end.row).toEqual row
+
+  ensureSelectionIsReversed: (reversed) ->
+    actual = @editor.getLastSelection().isReversed()
+    expect(actual).toBe(reversed)
+
+  ensureScrollTop: (scrollTop) ->
+    actual = @editorElement.getScrollTop()
+    expect(actual).toEqual scrollTop
+
+  ensureMode: (mode) ->
+    mode = toArray(mode)
+    expect(@vimState.isMode(mode...)).toBe(true)
+
+    mode[0] = "#{mode[0]}-mode"
+    mode = mode.filter((m) -> m)
+    expect(@editorElement.classList.contains('vim-mode-plus')).toBe(true)
+    for m in mode
+      expect(@editorElement.classList.contains(m)).toBe(true)
+    shouldNotContainClasses = _.difference(supportedModeClass, mode)
+    for m in shouldNotContainClasses
+      expect(@editorElement.classList.contains(m)).toBe(false)
+
+  # Public
   keystroke: (keys, {element}={}) =>
     # keys must be String or Array
     # Not support Object for keys to avoid ambiguity.
@@ -176,121 +301,4 @@
     if mocked
       unmockPlatform(element)

-  ensureOptionsOrdered = [
-    'text', 'selectedText', 'selectedTextOrdered'
-    'cursor', 'cursors', 'cursorBuffer'
-    'register', 'numCursors', 'selectedScreenRange',
-    'selectedBufferRange',
-    'selectedBufferRangeOrdered',
-    'selectedBufferRangeStartRow',
-    'selectedBufferRangeEndRow',
-    'selectionIsReversed',
-    'scrollTop',
-    'mode',
-  ]
-  ensure: (args...) =>
-    switch args.length
-      when 1 then [options] = args
-      when 2 then [keystroke, options] = args
-    @validateOptions(options, ensureOptionsOrdered, 'Invalid ensure option')
-    # Input
-    unless _.isEmpty(keystroke)
-      @keystroke(keystroke, {element: @editorElement})
-
-    for name in ensureOptionsOrdered when options[name]?
-      method = 'ensure' + _.capitalize(_.camelize(name))
-      this[method](options[name])
-
-  ensureText: (text) ->
-    expect(@editor.getText()).toEqual(text)
-
-  ensureSelectedText: (text) ->
-    selections = @editor.getSelections()
-    texts = (s.getText() for s in selections)
-    expect(texts).toEqual(toArray(text))
-
-  ensureSelectedTextOrdered: (text) ->
-    selections = @editor.getSelectionsOrderedByBufferPosition()
-    texts = (s.getText() for s in selections)
-    expect(texts).toEqual(toArray(text))
-
-  ensureCursor: (cursor) ->
-    cursor = if cursor instanceof Point or not _.isArray(cursor[0])
-      [cursor]
-    else
-      cursor
-    points = @editor.getCursorScreenPositions()
-    expect(points).toEqual(toArray(cursor, cursor))
-
-  ensureCursors: (cursors) ->
-    points = @editor.getCursorScreenPositions()
-    expect(points).toEqual(cursors)
-
-  ensureCursorBuffer: (cursor) ->
-    points = @editor.getCursorBufferPositions()
-    expect(points).toEqual(toArray(cursor, cursor[0]))
-
-  ensureRegister: (register) ->
-    if _.isObject(register)
-      for name, value of register
-        reg = @vimState.register.get(name)
-        for prop, _value of value
-          expect(reg[prop]).toEqual(_value)
-    else
-      expect(@vimState.register.get('"').text).toBe register
-
-  ensureNumCursors: (number) ->
-    expect(@editor.getCursors()).toHaveLength number
-
-  ensureSelectedScreenRange: (range) ->
-    actual = @editor.getSelectedScreenRanges()
-    expected = toArray(range, range[0][0])
-    expect(actual).toEqual(expected)
-
-  ensureSelectedBufferRange: (range) ->
-    if (range instanceof Range) or
-        (not (range[0] instanceof Range)) and (not _.isArray(range[0][0]))
-      range = [range]
-    actual = @editor.getSelectedBufferRanges()
-    expect(actual).toEqual(range)
-
-  ensureSelectedBufferRangeOrdered: (range) ->
-    if (range instanceof Range) or
-        (not (range[0] instanceof Range)) and (not _.isArray(range[0][0]))
-      range = [range]
-    actual = @editor.getSelectionsOrderedByBufferPosition().map (e) -> e.getBufferRange()
-    expect(actual).toEqual(range)
-
-  ensureSelectedBufferRangeStartRow: (row) ->
-    {start} = @editor.getSelectedBufferRange()
-    expect(start.row).toEqual row
-
-  ensureSelectedBufferRangeEndRow: (row) ->
-    {end} = @editor.getSelectedBufferRange()
-    expect(end.row).toEqual row
-
-  ensureSelectionIsReversed: (reversed) ->
-    actual = @editor.getLastSelection().isReversed()
-    expect(actual).toBe(reversed)
-
-  ensureScrollTop: (scrollTop) ->
-    actual = @editorElement.getScrollTop()
-    expect(actual).toEqual scrollTop
-
-  ensureMode: (mode) ->
-    mode = toArray(mode)
-    expect(@vimState.isMode(mode...)).toBe(true)
-
-    mode[0] = "#{mode[0]}-mode"
-    mode = mode.filter((m) -> m)
-    expect(@editorElement.classList.contains('vim-mode-plus')).toBe(true)
-    for m in mode
-      expect(@editorElement.classList.contains(m)).toBe(true)
-    shouldNotContainClasses = _.difference(supportedModeClass, mode)
-    for m in shouldNotContainClasses
-      expect(@editorElement.classList.contains(m)).toBe(false)
-
-dispatch = (target, command) ->
-  atom.commands.dispatch(target, command)
-
 module.exports = {getVimState, getView, dispatch}


:non-potable_water: Destroy current project in beforeEach

Previously the window specs were calling window::deserializeEditorWindow
without first destroying the spec project causing the global to be
reassigned over and leaking the initial project and repo.

--- spec/window-spec.coffee
+++ spec/window-spec.coffee
@@ -10,6 +10,7 @@

   beforeEach ->
     atom.getLoadSettings().initialPath = project.getPath()
+    project.destroy()
     windowEventHandler = new WindowEventHandler()
     window.deserializeEditorWindow()
     projectPath = project.getPath()


calendar test helper mid-refactor

calendar test helper mid-refactor

--- test-integration/helpers/calendar.coffee
+++ test-integration/helpers/calendar.coffee
@@ -1,5 +1,84 @@
 selenium = require 'selenium-webdriver'
 {expect} = require 'chai'
+
+{TestHelper} = require './test-element'
+
+COMMON_ELEMENTS =
+  forecastLink:
+    locator:
+      linkText: 'Performance Forecast'
+  addToggle:
+    locator:
+      css: '.add-assignment .dropdown-toggle'
+  addReadingButton:
+    locator:
+      linkText: 'Add Reading'
+  addHomeworkButton:
+    locator:
+      linkText: 'Add Homework'
+  addExternalButton:
+    locator:
+      linkText: 'Add External Assignment'
+
+
+COMMON_POPUP_ELEMENTS =
+  closeButton:
+    locator:
+      css: '.plan-modal .close'
+  editLink:
+    locator:
+      linkText: 'Edit Assignment'
+  reviewLink:
+    locator:
+      linkText: 'Review Metrics'
+
+class PlanPopupHelper extends  TestHelper
+  constructor: (test, testElementLocator) ->
+
+    testElementLocator ?= '.plan-modal .panel.panel-default'
+    super(test, testElementLocator, COMMON_POPUP_ELEMENTS)
+
+  close: =>
+    @getCloseButton().click()
+    @test.sleep(2000)
+
+  goEdit: =>
+    @getEditLink().click()
+
+  goReview: =>
+    @getReviewLink().click()
+
+class CalendarHelper extends TestHelper
+  constructor: (test, testElementLocator) ->
+
+    testElementLocator ?= '.calendar-container'
+    super(test, testElementLocator, COMMON_ELEMENTS, loadingLocator: '.calendar-loading')
+    @setCommonHelper('planPopup', new PlanPopupHelper(@test))
+
+  createNew: (type) =>
+    @waitUntilLoaded()
+
+    @getAddToggle().click()
+
+    switch type
+      when 'READING' then @getAddReadingButton().click()
+      when 'HOMEWORK' then @getAddHomeworkButton().click()
+      when 'EXTERNAL' then @getAddExternalButton().click()
+      else expect(false, 'Invalid assignment type').to.be.true
+
+  goPerformanceForecast: =>
+    @getForecastLink().click()
+
+  goOpen: (title) =>
+    # wait until the calendar is open
+    @waitUntilLoaded()
+    # TODO: Make this a `data-title` attribute
+    # HACK: Might need to scroll the item to click on into view
+    el = @test.waitAnd(css: "[data-title='#{title}']")
+    @test.scrollTo(el)
+    el.click()
+    @test.scrollTop()
+

 # Make sure the current screen is the calendar
 verify = (test, ms) ->


less dire error message if the user stopped the compiler manually

--- lib/idris-ide-mode.coffee
+++ lib/idris-ide-mode.coffee
@@ -39,13 +39,18 @@
     atom.notifications.addError e.short, detail: e.long

   exited: (code, signal) ->
-    short = "The idris compiler was closed or crashed"
-    long =
-      if signal
-        "It was closed with the signal: #{signal}"
-      else
-        "It (probably) crashed with the error code: #{code}"
-    atom.notifications.addError short, detail: long
+    if signal == "SIGTERM"
+      short = "The idris compiler was closed"
+      long = "You stopped the compiler"
+      atom.notifications.addInfo short, detail: long
+    else
+      short = "The idris compiler was closed or crashed"
+      long =
+        if signal
+          "It was closed with the signal: #{signal}"
+        else
+          "It (probably) crashed with the error code: #{code}"
+      atom.notifications.addError short, detail: long

   running: ->
     !!@process


fixing lint warnings

fixing lint warnings

--- test/components/reading-plan.spec.coffee
+++ test/components/reading-plan.spec.coffee
@@ -17,8 +17,8 @@
   }
 ]

-tomorrow = Date.now() + 1000*3600*24
-dayAfter = tomorrow + 1000*3600*24
+tomorrow = Date.now() + 1000 * 3600 * 24
+dayAfter = tomorrow + 1000 * 3600 * 24

 VALID_MODEL =
   type: 'reading'
@@ -83,7 +83,7 @@

   it 'should not show delete if plan is published', ->
-    yesterday = Date.now() - 3600*1000*24
+    yesterday = Date.now() - 3600 * 1000 * 24
     model =
       type: 'reading'
       id: 1


Uncommented an header

Uncommented an header

--- app/locale/es-ES.coffee
+++ app/locale/es-ES.coffee
@@ -1243,7 +1243,7 @@
     retrying: "Error del servidor, reintentando."
     success: "Pago realizado correctamente. ¡Muchas gracias!"

-#  account_prepaid:
+  account_prepaid:
 #    purchase_code: "Purchase a Subscription Code"
 #    purchase_amount: "Amount"
     purchase_total: "Total"


Update pt-PT.coffee

Update pt-PT.coffee

--- app/locale/pt-PT.coffee
+++ app/locale/pt-PT.coffee
@@ -406,10 +406,11 @@
     feature3: "70+ níveis de bónus"
     feature4: "3500 gemas de bónus por mês!"
     feature5: "Tutoriais em vídeo"
-    feature6: "Apoio por e-mail superior"
+    feature6: "Apoio por e-mail prioritário"
     feature7: "Clãs Privados"
     free: "Grátis"
     month: "mês"
+    must_be_logged: "Primeiro tens de ter sessão iniciada. Por favor, cria uma conta ou inicia sessão a partir do menu acima."
     subscribe_title: "Subscrever"
     unsubscribe: "Cancelar Subscrição"
     confirm_unsubscribe: "Confirmar Cancelamento da Subscrição"
@@ -420,7 +421,7 @@
     sorry_to_see_you_go: "Lamentamos ver-te partir! Por favor, diz-nos o que podíamos ter feito melhor."
     unsubscribe_feedback_placeholder: "Oh, o que fomos fazer?"
     parent_button: "Pergunta ao teu educador"
-    parent_email_description: "Vamos mandar-lhe um e-mail para que ele possa comprar-te uma subscrição do CodeCombat."
+    parent_email_description: "Vamos enviar-lhe um e-mail para que possa comprar-te uma subscrição do CodeCombat."
     parent_email_input_invalid: "Endereço de e-mail inválido."
     parent_email_input_label: "Endereço de e-mail do educador"
     parent_email_input_placeholder: "Introduz o e-mail do educador"
@@ -429,12 +430,12 @@
     parent_email_title: "Qual é o e-mail do teu educador?"
     parents: "Para Educadores"
     parents_title: "Caro Educador: O seu educando está a aprender a programar. Vai ajudá-lo a continuar?"
-    parents_blurb1: "O seu educando já jogou __nLevels__ níveis e aprendeu as bases da programação. Ajude a desenvolver o interesse dele, comprando-lhe uma subscrição para que possa continuar a jogar."
+    parents_blurb1: "O seu educando já jogou __nLevels__ níveis e aprendeu as bases da programação. Ajude a desenvolver o interesse dele, comprando-lhe uma subscrição para que ele possa continuar a jogar."
     parents_blurb1a: "A programação de computadores é uma habilidade fundamental que o seu educando vai usar incontestavelmente quando for adulto. Em 2020, habilidades de programação básicas serão requisitadas por 77% dos empregos e, atualmente, há uma grande procura por engenheiros informáticos no mundo. Sabia que os cursos universitários ligados às Ciências da Computação são os mais bem pagos?"
     parents_blurb2: "Por $9.99 USD/mês, o seu educando recebe novos desafios todas as semanas e suporte pessoal, via e-mail, de programadores profissionais."
     parents_blurb3: "Sem Risco: 100% de garantia de devolução do dinheiro, com anulação fácil de 1 clique."
-    payment_methods: "Formas de Pagamento"
-    payment_methods_title: "Formas de Pagamento Aceites"
+    payment_methods: "Métodos de Pagamento"
+    payment_methods_title: "Métodos de Pagamento Aceites"
     payment_methods_blurb1: "Atualmente aceitamos cartões de crédito e Alipay."
     payment_methods_blurb2: "Se precisares de uma outra forma de pagamento, por favor contacta"
     sale_already_subscribed: "Já estás subscrito!"
@@ -475,7 +476,7 @@
     group_discounts_20: "20% de desconto"
     group_discounts_12th: "12ª+ subscrições"
     group_discounts_40: "40% de desconto"
-    subscribing: "A subscrever..."
+    subscribing: "A Subscrever..."
     recipient_emails_placeholder: "Introduz endereços de e-mail para subscrever, um por linha."
     subscribe_users: "Subscrever Utilizadores"
     users_subscribed: "Utilizadores subscritos:"
@@ -519,13 +520,15 @@
     health_3: "apresentado."
     speed_1: "Move a"
     speed_2: "metros por segundo."
-    available_for_purchase: "Disponível para Aquirir" # Shows up when you have unlocked, but not purchased, a hero in the hero store
+    available_for_purchase: "Disponível para Aquisição" # Shows up when you have unlocked, but not purchased, a hero in the hero store
     level_to_unlock: "Nível para desbloquear:" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
     restricted_to_certain_heroes: "Apenas certos heróis podem jogar este nível."

   skill_docs:
-    writable: "escrevível" # Hover over "attack" in Your Skills while playing a level to see most of this
+    writable: "editável" # Hover over "attack" in Your Skills while playing a level to see most of this
     read_only: "apenas leitura"
+    action: "Ação-"
+    spell: "Feitiço-"
     action_name: "nome"
     action_cooldown: "Demora"
     action_specific_cooldown: "Tempo de Recarga"


image upload functionm

image upload functionm

--- app/scripts/main.coffee
+++ app/scripts/main.coffee
@@ -36,8 +36,11 @@
     $.ajax
       url: 'http://localhost:3000'
       success: (data)->
-        console.log 'upload success'
+        console.log 'image upload success'
         editor.setConnecting false
+        # append image to editor
+          $('hovered').append(data)
+
         setTimeout ->
           $('#server-status').text 'Saved'
           setTimeout ->


Move js-link handler to main.coffee

Move js-link handler to main.coffee

--- app/scripts/main.coffee
+++ app/scripts/main.coffee
@@ -5,11 +5,16 @@
 require('bootstrap')
 require('jade')

-require('scripts/modules/navigation')
 require('scripts/modules/landing')
 require('scripts/modules/dashboard')
 require('scripts/modules/articles')
+require('scripts/modules/navigation')

 App = require('scripts/application')

 $ -> App.start()
+
+$(document).on 'click', '.js-link', (event) ->
+  event.preventDefault()
+  href = $(event.currentTarget).attr('href')
+  Backbone.history.navigate(href, trigger: true)


Fixed project parsing for hubot-slack 4.0 (which strips # signs)

--- src/models/punch.coffee
+++ src/models/punch.coffee
@@ -491,7 +491,6 @@
       modeQualifier = "for #{article} #{blockTimeQualifier} block"
     else
       modeQualifier = @mode
-    Logger.debug "mode: #{modeQualifier}"
     if @projects and @projects.length > 0
       projectsQualifier = " ("
       projectsQualifier += @projects.map((el) ->
@@ -675,12 +674,9 @@
   command_copy = command.split(' ').slice()

   for word in command_copy
-    if word.charAt(0) is '#'
-      if project = Organization.getProjectByName word
-        projects.push project
-        command = command.replace ///#{word} ?///i, ''
-    else
-      break
+    if project = Organization.getProjectByName word
+      projects.push project
+      command = command.replace ///#{word} ?///i, ''
   [projects, command]

 module.exports = Punch


:white_check_mark: for N% motion

:white_check_mark: for N% motion

--- spec/motion-spec.coffee
+++ spec/motion-spec.coffee
@@ -795,8 +795,17 @@
           selectedText: "    2\n 3abc\n "
           cursor: [3, 1]

-  # [FIXME] need spec
   describe "the N% keybinding", ->
+    beforeEach ->
+      set
+        text: [0..99].join("\n")
+        cursor: [0, 0]
+
+    describe "put cursor on line specified by percent", ->
+      it "50%", -> ensure '50%', cursor: [49, 0]
+      it "30%", -> ensure '30%', cursor: [29, 0]
+      it "100%", -> ensure '100%', cursor: [99, 0]
+      it "120%", -> ensure '120%', cursor: [99, 0]

   describe "the / keybinding", ->
     pane = null


Increase `waitsFor` timeout in attempt to address intermittent failure

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -72,7 +72,7 @@
   throw new Error("Not a spy") unless object[methodName].originalValue?
   object[methodName] = object[methodName].originalValue

-jasmine.getEnv().defaultTimeoutInterval = 200
+jasmine.getEnv().defaultTimeoutInterval = 500

 window.keyIdentifierForKey = (key) ->
   if key.length > 1 # named key


Add expect for project state

Add expect for project state

--- spec/window-spec.coffee
+++ spec/window-spec.coffee
@@ -90,11 +90,13 @@
     it "saves the serialized state of the window so it can be deserialized after reload", ->
       workspaceState = atom.workspace.serialize()
       syntaxState = atom.syntax.serialize()
+      projectState = atom.project.serialize()

       atom.unloadEditorWindow()

       expect(atom.state.workspace).toEqual workspaceState
       expect(atom.state.syntax).toEqual syntaxState
+      expect(atom.state.project).toEqual projectState
       expect(atom.saveSync).toHaveBeenCalled()

     it "unsubscribes from all buffers", ->


relative -> relativePath

relative -> relativePath

--- src/module-cache.coffee
+++ src/module-cache.coffee
@@ -77,7 +77,7 @@
   return if relativePath[relativePath.length - 1] is '/'
   return if fs.isAbsolute(relativePath)

-  console.log "looking up #{relative} from #{parentModule.id}"
+  console.log "looking up #{relativePath} from #{parentModule.id}"

   undefined


improve display if speed is not available

--- src/sprint/states/edit/controller.coffee
+++ src/sprint/states/edit/controller.coffee
@@ -42,7 +42,7 @@
   Sprint.getLastSpeeds($scope.project.objectId)
   .then (speedsInfo) ->
     $scope.speedInfo =
-      previousSpeeds: _.map(speedsInfo, (speedInfo) ->
+      previousSpeeds: _.map(_.filter(speedsInfo, (speedInfo) -> speedInfo.speed != '?'), (speedInfo) ->
         "Sprint #{speedInfo.number}: #{speedInfo.speed}"
       ).join ', '

@@ -52,4 +52,5 @@
       else
         0
     speedAverage = sum / (speedsInfo.length or 1)
-    $scope.speedInfo.average = speedAverage.toFixed(1)
+    if speedAverage > 0
+      $scope.speedInfo.average = speedAverage.toFixed(1)


client.js: store uri for execution

client.js: store uri for execution

--- src-client/index.coffee
+++ src-client/index.coffee
@@ -154,6 +154,7 @@
 						opts.onProgress execution
 						return opts.onComplete execution
 					execution = res.body
+					execution.uri = uri
 					if execution.status in ['FINISHED','FAILED']
 						clearInterval pollId
 						opts.onProgress execution


Fixed changing to undefined inventory

Fixed changing to undefined inventory

--- src/SceneManager.coffee
+++ src/SceneManager.coffee
@@ -103,6 +103,8 @@
   readItemEdits: (source) ->
     if source.changeInventory != undefined
       novelData.novel.currentInventory = parser.parseStatement(source.changeInventory)
+      if novelData.novel.currentInventory == undefined
+        novelData.novel.currentInventory = []
     if source.removeItem != undefined
       inventoryManager.editItems(parser.parseItems(source.removeItem),"remove")
     if source.addItem != undefined


Fix: pairs of blank lines in body disappear

--- lib/replay/catalog.coffee
+++ lib/replay/catalog.coffee
@@ -106,7 +106,7 @@
       status:   status
       version:  version
       headers:  headers
-      body:     body
+      body:     body.join("\n\n")

     return { request: request, response: response }


use name variable for generating OS X bash

--- lib/update_manager.coffee
+++ lib/update_manager.coffee
@@ -166,9 +166,9 @@
 ###
 osxMajor = (install_path, update_path) ->
   cmd = _.template([
-    'echo -n -e "\\033]0;Updating Championify\\007"'
-    'echo Updating Championify, please wait...'
-    'killall Championify'
+    'echo -n -e "\\033]0;Updating ${name}\\007"'
+    'echo Updating ${name}, please wait...'
+    'killall ${name}'
     'mv "${update_path}/Contents/Resources/atom-asar" "${update_path}/Contents/Resources/atom.asar"'
     'mv "${update_path}/Contents/Resources/app-asar" "${update_path}/Contents/Resources/app.asar"'
     'rm -rf "${install_path}"'


Adding these returns may make a difference. May.

--- lib/zombie/scripts.coffee
+++ lib/zombie/scripts.coffee
@@ -6,10 +6,11 @@

 # If you're using CoffeeScript, you get client-side support.
 try
-  CoffeeScript  = require("coffee-script")
+  CoffeeScript = require("coffee-script")
   HTML.languageProcessors.coffeescript = (element, code, filename)->
     @javascript(element, CoffeeScript.compile(code), filename)
 catch ex
+  # Oh, well

 # If JSDOM encounters a JS error, it fires on the element.  We expect it to be
@@ -55,6 +56,7 @@
     return unless language
     # Queue so inline scripts execute in order with external scripts
     HTML.resourceLoader.enqueue(node, -> language(this, code, document.location.href))()
+    return

 # -- Utility methods --
@@ -86,6 +88,7 @@
   event.message = error.message
   event.error = error
   window.browser.dispatchEvent window, event
+  return

 module.exports = { raise, addInlineScriptSupport }


beaglebone-green.coffee: Point to the beaglebone yocto machine

All the beaglebone family is now using the beaglebone black resinOS image.
We shall be using it this way until we will just keep the beaglebone black
device type and remove these obsolete types.

Also change the getting started guide links.

Signed-off-by: Florin Sarbu <3a837a37a2ac9072a088641ec330e0e5d5b86f4e@resin.io>

--- beaglebone-green.coffee
+++ beaglebone-green.coffee
@@ -28,18 +28,18 @@
 	].concat(postProvisioningInstructions)

 	gettingStartedLink:
-		windows: 'http://docs.resin.io/#/pages/installing/gettingStarted-BBB.md#windows'
-		osx: 'http://docs.resin.io/#/pages/installing/gettingStarted-BBB.md#on-mac-and-linux'
-		linux: 'http://docs.resin.io/#/pages/installing/gettingStarted-BBB.md#on-mac-and-linux'
+		windows: 'https://docs.resin.io/beaglebone/nodejs/getting-started/#adding-your-first-device'
+		osx: 'https://docs.resin.io/beaglebone/nodejs/getting-started/#adding-your-first-device'
+		linux: 'https://docs.resin.io/beaglebone/nodejs/getting-started/#adding-your-first-device'

 	supportsBlink: true

 	yocto:
-		machine: 'beaglebone-green'
+		machine: 'beaglebone'
 		image: 'resin-image-flasher'
 		fstype: 'resinos-img'
 		version: 'yocto-morty'
-		deployArtifact: 'resin-image-flasher-beaglebone-green.resinos-img'
+		deployArtifact: 'resin-image-flasher-beaglebone.resinos-img'
 		compressed: true

 	options: [ networkOptions.group ]


messed with rope placement

messed with rope placement

moved ropes to inner corner of engines

--- src/racer/Engine.coffee
+++ src/racer/Engine.coffee
@@ -29,7 +29,10 @@
     @body.addShape(shape, [0, 0], 0)

     @throttle = 0.0
-    @ropePoint = [0, 0.45 * h] # point the rope connects in local coordinates
+    if @side == 'right'
+      @ropePoint = [-0.4 * w, 0.45 * h] # point the rope connects in local coordinates
+    else
+      @ropePoint = [0.4 * w, 0.45 * h]

     @flaps = []
     for flapDef in @engineDef.flaps


more logging

more logging

--- noddy/service/v2/permissions.coffee
+++ noddy/service/v2/permissions.coffee
@@ -30,7 +30,7 @@

   formats = ['doc','tex','pdf','htm','xml','txt','rtf','odf','odt','page']

-  perms = meta.permissions ? {permissions: {archiving_allowed: false, version_allowed: undefined, embargo: undefined, required_statement: undefined, licence_required: undefined}, file: undefined}
+  perms = {permissions: {archiving_allowed: false, version_allowed: undefined, embargo: undefined, required_statement: undefined, licence_required: undefined}, file: undefined}
   meta = meta.metadata if meta.metadata? # if passed a catalogue object

   if typeof file is 'string'
@@ -72,25 +72,26 @@
   if meta.doi? and not perms.ricks?
     try
       # https://rickscafe-api.herokuapp.com/permissions/doi/
-      try cached = API.http.cache meta.doi, 'ricks_permissions'
+      cached = API.http.cache meta.doi, 'ricks_permissions'
       if cached and typeof cached is 'object' and cached.application?
         perms.ricks = cached
-      else
-        ru = 'https://api.greenoait.org/permissions/doi/' + meta.doi
-        if uid and uc = API.service.deposit.config(uid)
-          ru += '?affiliation=' + uc.ROR_ID if uc.ROR_ID
-        API.log 'Permissions check connecting to Ricks for ' + ru
-        try
-          perms.ricks = HTTP.call('GET',ru).data.authoritative_permission
-        catch
-          API.log 'Permissions check connection to Ricks failed for ' + meta.doi
+        API.log 'Permissions check found in Ricks cache for ' + meta.doi
+    if not perms.ricks?
+      ru = 'https://api.greenoait.org/permissions/doi/' + meta.doi
+      if uid and uc = API.service.deposit.config(uid)
+        ru += '?affiliation=' + uc.ROR_ID if uc.ROR_ID
+      API.log 'Permissions check connecting to Ricks for ' + ru
+      try
+        perms.ricks = HTTP.call('GET',ru).data.authoritative_permission
         try API.http.cache(meta.doi, 'ricks_permissions', perms.ricks) if perms.ricks?.application?
+      catch
+        API.log 'Permissions check connection to Ricks failed for ' + meta.doi
+        perms.error = 'Could not connect to Ricks'
+    try
       perms.permissions.archiving_allowed = perms.ricks?.application?.can_archive_conditions?.versions_archivable? and ('postprint' in perms.ricks.application.can_archive_conditions.versions_archivable or 'publisher pdf' in perms.ricks.application.can_archive_conditions.versions_archivable)
       perms.permissions.required_statement = perms.ricks.application.can_archive_conditions.deposit_statement_required_calculated if typeof perms.ricks?.application?.can_archive_conditions?.deposit_statement_required_calculated is 'string' and perms.ricks.application.can_archive_conditions.deposit_statement_required_calculated.indexOf('cc-') isnt 0
       perms.permissions.licence_required = perms.ricks.application.can_archive_conditions.licenses_required[0] if perms.ricks?.application?.can_archive_conditions?.licenses_required?
       perms.permissions.policy_full_text = perms.ricks.provenance.policy_full_text if perms.ricks?.provenance?.policy_full_text
-    catch
-      perms.error = 'Could not connect to Ricks'
     try
       if perms.permissions.archiving_allowed
         perms.permissions.version_allowed = if 'publisher pdf' in perms.ricks.application.can_archive_conditions.versions_archivable then 'publisher pdf' else if 'postprint' in perms.ricks.application.can_archive_conditions.versions_archivable then 'postprint' else 'preprint'


extending classes fix

extending classes fix

--- src/atomicrecord.coffee
+++ src/atomicrecord.coffee
@@ -105,6 +105,7 @@
                 @[src] = val
           else
             throw new Error('Initializer must be a record, string or number')
+

     ### Initializers ###
     database: database
@@ -289,9 +290,55 @@

     @index = index

-    @extend = (constructor) ->
-      ctorProto = constructor::
-      constructor extends @
-      constructor::[k] = v for k, v of ctorProto
-      AtomicRecord.serializer.AtomicRecord = constructor
-      constructor+    @extend = (child) ->
+      extension = child
+      childProto = {}
+
+      # console.log(require('util').inspect(child::, { showHidden: true, depth: null }))
+
+      # console.log('extproto0', child::)
+
+      childProto[propName] = child::[propName] for propName in Object.getOwnPropertyNames(child::)
+
+      extension extends AtomicRecord
+      extension::[k] = v for k, v of childProto
+      AtomicRecord.serializer.AtomicRecord = extension
+      console.log('extension', new extension().serialize)
+      extension
+
+      # ctorProto = child::
+      # child extends @
+      # child::[k] = v for k, v of ctorProto
+      # AtomicRecord.serializer.AtomicRecord = child
+      # child
+
+      # childProto = {}
+
+      # # console.log(require('util').inspect(child::, { showHidden: true, depth: null }))
+
+      # # console.log('extproto0', child::)
+
+      # childProto[propName] = child::[propName] for propName in Object.getOwnPropertyNames(child::)
+
+      # hasProp = {}.hasOwnProperty
+      # parent = AtomicRecord
+
+      # for k, v of parent
+      #   child[k] = v if hasProp.call(parent, k)
+
+      # ctor = ->
+      #   @constructor = child
+
+      # ctor:: = parent::
+      # child:: = new ctor()
+      # child::[k] = v for k, v of childProto
+      # child.__super__ = parent::
+
+      # # console.log('extproto1', extproto)
+
+      # # for k, v of extproto
+      # #   console.log('child function', k)
+
+
+      # AtomicRecord.serializer.AtomicRecord = child
+      # child


No metemos más de una vez los scripts solicitados.

--- spec/javascripts/helpers/spec_helper.coffee
+++ spec/javascripts/helpers/spec_helper.coffee
@@ -1,5 +1,8 @@
+cached_files = {}
 require = (file) ->
-  document.write('<script type="text/javascript" src="/public/javascripts/' + file + '.js"></script>')
+  if !cached_files[file]
+    document.write('<script type="text/javascript" src="/public/javascripts/' + file + '.js"></script>')
+    cached_files[file] = file

 beforeEach ->
   this.addMatchers({


Re-rename the branchActions you shouldn't have in the first place

--- src/bitbucket-pr.coffee
+++ src/bitbucket-pr.coffee
@@ -52,7 +52,7 @@
     else
       'no one in particular'

-  @branchAction: (action_name, action_desc) ->
+  branchAction: (action_name, action_desc) ->
     "#{@actor} *#{action_name}* pull request \"#{@title},\" #{action_desc}
     `#{@source_branch}` and `#{@destination_branch}` into a `#{@repo_name}`
     super branch#{@reason}"
@@ -126,7 +126,7 @@
   PURPLE: '#AA82E5'
   ORANGE: '#F1A56F'

-  @branchAction: (action_name, color) ->
+  branchAction: (action_name, color) ->
     fields = []
     fields.push
       title: @title