set scrollWheelZoom to false on leaflet maps

--- app/assets/javascripts/shifts.coffee
+++ app/assets/javascripts/shifts.coffee
@@ -6,7 +6,7 @@
   initMap = (mapContainer) ->
     lat = mapContainer.data 'lat'
     long = mapContainer.data 'long'
-    map = L.map(mapContainer.attr('id'))
+    map = L.map(mapContainer.attr('id'), {scrollWheelZoom: false})
     osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
     osmAttrib = 'Map data © OpenStreetMap contributors'
     osm = new L.TileLayer(osmUrl, { minZoom: 8, maxZoom: 18, attribution: osmAttrib})


Disable stripe for the moment

Disable stripe for the moment

--- app/components/price-selector.coffee
+++ app/components/price-selector.coffee
@@ -1,5 +1,10 @@
 `import Ember from 'ember'`
 `import ENUMS from 'irene/enums';`
+
+# Fix these imports when stripe is activated
+`import ENV from 'irene/config/environment';`
+`import EmberCLIICAjax from 'ic-ajax';`
+`import Notify from 'ember-notify';`

 isNumber = (n) ->
   /^\d+$/.test n
@@ -67,6 +72,19 @@
       @set "count", count

     makePayment: ->
+      invoceUrl = [ENV.APP.API_BASE, ENV.endpoints.invoice].join '/'
+      data =
+        pricingId: @get "pricing.id"
+        count: @get "count"
+      xhr = EmberCLIICAjax url:invoceUrl, type: "get", data: data
+      xhr.then (result) ->
+        elem = $("#hidden-paypal-form").html result.form
+        elem.find("[name='submit']").click()
+      , ->
+        Notify.error "Oh Dang, some error occured!"
+
+      return  # Everything above return is a temproary fix.
+      # It will be reverted when subho activates stripe
       applicationController = @container.lookup "controller:application"
       applicationController.set "makePaymentModal.pricing", @get "pricing"
       applicationController.set "makePaymentModal.count", @get "count"


better cli help message

better cli help message

--- src/cli.coffee
+++ src/cli.coffee
@@ -30,7 +30,7 @@
   program
     .command('publish [env]')
     .description('deploy API, Lambdas to AWS infrastructure')
-    .option '-o, --output [output]', 'foobar'
+    .option '-o, --output [output]', 'Path to write API config file'
     .action (env, options) ->
       call ->
         stack = yield publish env


horizon3: clicking on a horizon canvas node applies filter

--- app/frontend/horizon/horizon3.coffee
+++ app/frontend/horizon/horizon3.coffee
@@ -191,9 +191,14 @@
       .append("div")
         .attr("class", "horizon")
         .on("click", (d) ->
-          unless d3.event.target?.type is "checkbox"
-            d3.select(this).select("input")[0][0].click()
+          #if d3.event.target?.type is "canvas"
+          tag = d3.event.target?.tagName
+          if tag is "CANVAS"
+            d3.select(this).select("input")[0][0].checked = true
             parent.select("button.filter")[0][0].click()
+          else
+            unless tag is "INPUT"
+              d3.select(this).select("input")[0][0].click()
         )


errors?

errors?

--- app/server/methods/generators.coffee
+++ app/server/methods/generators.coffee
@@ -117,9 +117,9 @@
             toPdf.stdout.on 'data', (data) ->
               console.log("wkhtmltopdf:", data)

-            #toPdf.stderr.setEncoding('utf8');
-            #toPdf.stderr.on 'data', (data) ->
-            #  console.log("wkhtmltopdf error:", data)
+            toPdf.stderr.setEncoding('utf8');
+            toPdf.stderr.on 'data', (data) ->
+              console.log("wkhtmltopdf error:", data)

             toPdf.on 'close', (code) ->
               console.log("wkhtmltopdf exit:", code)


Remove Magnitude attachment.

Remove Magnitude attachment.

--- server/attachment.coffee
+++ server/attachment.coffee
@@ -65,7 +65,3 @@
     if @turn == 2
       @pokemon.attach(new exports.SleepAttachment())
       @remove()
-
-class @MagnitudeAttachment extends @VolatileAttachment
-  constructor: (attributes) ->
-    super('MagnitudeAttachment', attributes)


Stop overriding constructor and use initialize.

--- server/attachment.coffee
+++ server/attachment.coffee
@@ -10,7 +10,7 @@
     throw new Error("Passed a non-existent Attachment.")  if !attachmentClass?
     attachment = @get(attachmentClass)
     if !attachment?
-      attachment = new attachmentClass(options)
+      attachment = new attachmentClass()
       for attribute, value of attributes
         attachment[attribute] = value
       @attachments.push(attachment)
@@ -154,8 +154,7 @@
 class @Attachment.Confusion extends @VolatileAttachment
   name: VolatileStatus.CONFUSION

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) ->
     @turns = attributes.battle.rng.randInt(1, 4, "confusion turns")
     @turn = 0

@@ -175,8 +174,7 @@
 class @Attachment.Disable extends @VolatileAttachment
   name: "DisableAttachment"

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) ->
     @blockedMove = attributes.move
     @turns = attributes.turns
     @turn = 0
@@ -195,8 +193,7 @@
 class @Attachment.Yawn extends @VolatileAttachment
   name: 'YawnAttachment'

-  constructor: ->
-    super()
+  initialize: =>
     @turn = 0

   endTurn: =>
@@ -227,8 +224,7 @@
 class @Attachment.Taunt extends @VolatileAttachment
   name: "TauntAttachment"

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) =>
     {@battle} = attributes
     @turns = 3
     @turn = 0
@@ -280,8 +276,7 @@
 class @Attachment.PerishSong extends @VolatileAttachment
   name: "PerishSongAttachment"

-  constructor: ->
-    super()
+  initialize: =>
     @turns = 4
     @turn = 0

@@ -307,12 +302,9 @@
 class @Attachment.Encore extends @VolatileAttachment
   name: "EncoreAttachment"

-  constructor: ->
-    super()
+  initialize: =>
     @turns = 3
     @turn = 0
-
-  initialize: =>
     @move = @pokemon.lastMove

   beginTurn: (battle) =>
@@ -405,8 +397,7 @@
 class @Attachment.Trap extends @VolatileAttachment
   name: "TrapAttachment"

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) =>
     {@moveName, @user, @turns} = attributes

   beginTurn: (battle) =>
@@ -439,8 +430,7 @@
 class @Attachment.TrapLeash extends @VolatileAttachment
   name: "TrapLeashAttachment"

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) =>
     {@target} = attributes

   remove: =>
@@ -452,13 +442,10 @@
 class @Attachment.Attract extends @VolatileAttachment
   name: "AttractAttachment"

-  constructor: (attributes={}) ->
-    super()
-    {@source} = attributes
-
-  initialize: =>
-    if @pokemon.hasItem("Destiny Knot") && !@source.hasAttachment(Attachment.Attract)
-      @source.attach(Attachment.Attract, {@source})
+  initialize: (attributes) =>
+    {source} = attributes
+    if @pokemon.hasItem("Destiny Knot") && !source.has(Attachment.Attract)
+      source.attach(Attachment.Attract, {source})
       @pokemon.removeItem()

   beforeMove: (battle, move, user, targets) =>
@@ -471,8 +458,8 @@

 class @Attachment.MicleBerry extends @VolatileAttachment
   name: "MicleBerryAttachment"
-  constructor: ->
-    super()
+
+  initialize: =>
     @turns = 1

   editAccuracy: (accuracy) =>
@@ -487,8 +474,7 @@
 class @Attachment.EvasionItem extends @VolatileAttachment
   name: "EvasionItemAttachment"

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) =>
     @ratio = attributes.ratio || 0.9

   editEvasion: (accuracy) =>
@@ -508,8 +494,7 @@
 class @Attachment.Screen extends @TeamAttachment
   name: "ScreenAttachment"

-  constructor: (attributes={}) ->
-    super(attributes)
+  initialize: (attributes) =>
     {user} = attributes
     @turns = (if user?.hasItem("Light Clay") then 8 else 5)

@@ -527,8 +512,7 @@
 class @Attachment.Identify extends @VolatileAttachment
   name: "IdentifyAttachment"

-  constructor: (attributes={}) ->
-    super(attributes)
+  initialize: (attributes) =>
     {@type} = attributes

   editBoosts: (stages) =>
@@ -553,8 +537,7 @@
 class @Attachment.MagnetRise extends @VolatileAttachment
   name: "MagnetRiseAttachment"

-  constructor: ->
-    super()
+  initialize: =>
     @turns = 5

   isImmune: (battle, type) =>
@@ -567,8 +550,7 @@
 class @Attachment.LockOn extends @VolatileAttachment
   name: "LockOnAttachment"

-  constructor: ->
-    super()
+  initialize: =>
     @turns = 2

   editAccuracy: (stat) =>
@@ -590,8 +572,7 @@
 class @Attachment.Recharge extends @VolatileAttachment
   name: "RechargeAttachment"

-  constructor: ->
-    super()
+  initialize: =>
     @turns = 2

   beginTurn: (battle) =>
@@ -613,8 +594,7 @@

   maxLayers: 5

-  constructor: (attributes={}) ->
-    super()
+  initialize: (attributes) =>
     {@move} = attributes
     @turns = 1

@@ -635,8 +615,7 @@
 class @Attachment.Charge extends @VolatileAttachment
   name: "ChargeAttachment"

-  constructor: ->
-    super()
+  initialize: =>
     @turns = 2

   endTurn: (battle) =>
@@ -646,8 +625,7 @@
 class @Attachment.LeechSeed extends @VolatileAttachment
   name: "LeechSeedAttachment"

-  constructor: (attributes) ->
-    super()
+  initialize: (attributes) =>
     {@user, @target} = attributes

   endTurn: (battle) =>
@@ -745,8 +723,7 @@
 class @Attachment.Substitute extends @VolatileAttachment
   name: "SubstituteAttachment"

-  constructor: (attributes) ->
-    super()
+  initialize: (attributes) =>
     {@battle, @hp} = attributes

   transformHealthChange: (damage) =>


Set position to relative not initial (static in this case)

Ran into case where I wanted to set the header to absolute so the
underlying background image could be the full height of the window.
For these situations "relative" makes everything still work
while not causing any troubles afaik in the normal case.

--- src/index.cjsx
+++ src/index.cjsx
@@ -123,7 +123,7 @@
     style =
       position:
         if @props.disable or @state.state is "unfixed"
-          'initial'
+          'relative'
         else
           'fixed'
       top: 0


average contributions per day

average contributions per day

--- src/iso.coffee
+++ src/iso.coffee
@@ -8,6 +8,7 @@
   ]

   yearTotal           = 0
+  averageCount        = 0
   maxCount            = 0
   bestDay             = null
   firstDay            = null
@@ -63,6 +64,7 @@

   resetValues: ->
     yearTotal           = 0
+    averageCount        = 0
     maxCount            = 0
     bestDay             = null
     firstDay            = null
@@ -213,6 +215,10 @@
     dateLast   = this.formatDateString lastDay, dateWithYearOptions
     datesTotal = dateFirst + " — " + dateLast

+    # Average Contribution per Day
+    dayDifference = this.datesDayDifference firstDay, lastDay
+    averageCount = this.precisionRound((yearTotal / dayDifference), 2)
+
     # Best day
     dateBest  = this.formatDateString bestDay, dateOptions
     if !dateBest
@@ -223,18 +229,19 @@
     longestStreakEnd   = this.formatDateString longestStreakEnd, dateOptions
     datesLongest       = longestStreakStart + " — " + longestStreakEnd

-    this.renderTopStats(countTotal, datesTotal, maxCount, dateBest)
+    this.renderTopStats(countTotal, averageCount, datesTotal, maxCount, dateBest)
     this.renderBottomStats(streakLongest, datesLongest, streakCurrent, datesCurrent)

-  renderTopStats: (countTotal, datesTotal, maxCount, dateBest) ->
+  renderTopStats: (countTotal, averageCount, datesTotal, maxCount, dateBest) ->
     html = """

             1 year total
               #{countTotal}
-
-
+              #{averageCount} per day
+
+
               contributions
               #{datesTotal}

@@ -333,6 +340,28 @@

     return date

+  datesDayDifference: (dateStr1, dateStr2) ->
+    diffDays = null
+    date1 = null
+    date2 = null
+
+    if dateStr1
+      dateParts = dateStr1.split '-'
+      date1 = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], 0, 0, 0)
+    if dateStr2
+      dateParts = dateStr2.split '-'
+      date2 = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], 0, 0, 0)
+
+    if dateStr1 && dateStr2
+      timeDiff = Math.abs(date2.getTime() - date1.getTime())
+      diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24))
+
+    return diffDays
+
+  precisionRound: (number, precision) ->
+    factor = Math.pow(10, precision)
+    return Math.round(number * factor) / factor
+
 $ ->
   target = document.querySelector '.js-calendar-graph'
   iso = new Iso target


Fix position slider updating.

Fix position slider updating.

--- frontend/script.coffee
+++ frontend/script.coffee
@@ -530,8 +530,7 @@
   positionSlider.bind "change", ->
     me = $(@)
     player.setPosition me.val()
-
-  positionSlider.val player.getPosition()
+    positionSlider.val player.getPosition()

   durationElement = $(".duration", playerElement)
   player.on "durationchange", (duration) ->


set default log level methods

set default log level methods

--- src/log.coffee
+++ src/log.coffee
@@ -17,6 +17,18 @@

     # callbacks are called via callback(txt)
     @callbacks = [@consoleCallback]
+
+  error: (txt) ->
+    @log txt, "ERROR"
+
+  warning: (txt) ->
+    @log txt, "WARNING"
+
+  notice: (txt) ->
+    @log txt, "NOTICE"
+
+  info: (txt) ->
+    @log txt, "INFO"

   debug: (txt) ->
     if @debugModus


Fix previous merge

Fix previous merge

--- app/locale/nl-NL.coffee
+++ app/locale/nl-NL.coffee
@@ -26,7 +26,7 @@
     learn_more: "Lees verder"
     classroom_in_a_box: "Een kant-en-klare digitale klas voor programmeerlessen."
     codecombat_is: "CodeCombat is een platform waarmee leerlingen leren programmeren door het spelen van een spel."
-    our_courses: "Onze lessen zijn specifiek ontwikkeld om uit te springen in een klas-opzet, zelfs voor leraren met geen ervaring met programmeren.
+    our_courses: "Onze lessen zijn specifiek ontwikkeld om uit te springen in een klas-opzet, zelfs voor leraren met geen ervaring met programmeren."
     designed_with: "Gemaakt voor leraren"
     real_code: "Echte, getypde code"
     from_the_first_level: "vanaf het eerste level"


Update arena constants

Update arena constants


Update tundra-tower launch date

Update tundra-tower launch date


a dollar formatter

a dollar formatter

--- assets/javascripts/initialize.coffee
+++ assets/javascripts/initialize.coffee
@@ -2,6 +2,18 @@
   models: {}
   views: {}
   host: 'http://pov.knowtheory.net'
+  formatDollars: (value) ->
+    characters = '' + value
+    groups = []
+    group_count = Math.ceil characters.length / 3
+    end = characters.length
+    while (end > 0)
+      start = end - 3
+      start = 0 if start < 0
+      groups.push(characters.slice(start, end))
+      end -= 3
+    groups.reverse().join(',')
+

 Backbone.sync = _.wrap Backbone.sync, ((sync, method, model, options) ->
   getValue = (object,prop) ->
@@ -14,7 +26,3 @@
   url = getValue(model, 'url') or urlError() unless options.url
   url = unless /^http/.test url then POV.host + url else url
   sync(method, model, _.extend(options, {dataType: 'jsonp', url: url})))
-
-#jsonp_sync = _.wrap(Backbone.sync, ((sync, method, model, options) ->
-#  sync(method, model, _.extend(options, {dataType: 'jsonp'}))))
-


Travis.ajax ember-cli compat

Travis.ajax ember-cli compat

--- assets/scripts/app/utils/ajax.coffee
+++ assets/scripts/app/utils/ajax.coffee
@@ -6,7 +6,7 @@
   accepts:
     json: 'application/json; version=2'

-Travis.ajax = Em.Object.create
+ajax = Em.Object.create
   publicEndpoints: [/\/repos\/?.*/, /\/builds\/?.*/, /\/jobs\/?.*/]
   privateEndpoints: [/\/repos\/\d+\/caches/]

@@ -150,3 +150,5 @@
     xhr.send(data)

     return promise
+
+Travis.ajax = ajax


EPT-278 SITe accepts products without image

--- public/models/product.coffee
+++ public/models/product.coffee
@@ -91,16 +91,20 @@
     return "disabled"

   thumbnailImage: ->
-    _.findWhere(@get("images"), classifier: "Thumbnail").url
+    if _.findWhere(@get("images"), classifier: "Thumbnail")
+      _.findWhere(@get("images"), classifier: "Thumbnail").url

   smallImage: ->
-    _.findWhere(@get("images"), classifier: "Small").url
+    if _.findWhere(@get("images"), classifier: "Small")
+      _.findWhere(@get("images"), classifier: "Small").url

   mediumImage: ->
-    _.findWhere(@get("images"), classifier: "Medium").url
+    if _.findWhere(@get("images"), classifier: "Medium")
+      _.findWhere(@get("images"), classifier: "Medium").url

   largeImage: ->
-    _.findWhere(@get("images"), classifier: "Large").url
+    if _.findWhere(@get("images"), classifier: "Large")
+      _.findWhere(@get("images"), classifier: "Large").url

   productImage: ->
     @get("variationImage") || @largeImage()


Add `renderer.background` property

Add `renderer.background` property

--- src/core/Renderer.coffee
+++ src/core/Renderer.coffee
@@ -25,7 +25,7 @@
 			imageSmoothing: yes

 		# Copy options
-		for name in ['container', 'width', 'height', 'fps', 'showKeyPoints', 'showBounds', 'originAtCenter']
+		for name in ['container', 'background', 'width', 'height', 'fps', 'showKeyPoints', 'showBounds', 'originAtCenter']
 			@[name] = options[name]

 		# If options.width is not given, then fillParent is true
@@ -39,7 +39,7 @@
 		@dom = document.createElement 'canvas'
 		@dom.style.cursor = options.cursor or 'default'
 		@dom.style.boxSizing = 'content-box'
-		@dom.style.background = options.background
+		@dom.style.background = @background
 		@dom.oncontextmenu = -> false

 		# Set context


switch from staging to live exp server

switch from staging to live exp server

--- app/lib/experiments.coffee
+++ app/lib/experiments.coffee
@@ -16,7 +16,7 @@
 The URL of the experiment server to use
 ###
 # prod:
-EXPERIMENT_SERVER_URL = "http://experiments.staging.zooniverse.org/"
+EXPERIMENT_SERVER_URL = "http://experiments.zooniverse.org/"
 # dev:
 #EXPERIMENT_SERVER_URL = "http://localhost:4567/"


Update Twilio usage to new API

Update Twilio usage to new API

Seems like we were still using the 2.x syntax even though this app
depends on 3.x explicitly according to yarn.lock

https://www.twilio.com/docs/libraries/node/usage-guide

--- apps/home/routes.coffee
+++ apps/home/routes.coffee
@@ -1,4 +1,4 @@
-twilio = require 'twilio'
+Twilio = require 'twilio'
 { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_NUMBER, DEFAULT_CACHE_TIME, IPHONE_APP_URL } = require '../../config'

 memjs = require('memjs')
@@ -14,8 +14,8 @@
     if !err and ts?
       res.json 400, { success: false, message: 'You have already sent a download link to this number.' }
     else
-      twilioClient = new twilio.RestClient TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN
-      twilioClient.sendSms({
+      twilioClient = new Twilio TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN
+      twilioClient.messages.create({
         to: phone_number
         from: TWILIO_NUMBER
         body: "Download the new Artsy iPhone app here: #{IPHONE_APP_URL}"


Added some fixes for Internet Explorer compatibility

--- src/d3.timeslider.coffee
+++ src/d3.timeslider.coffee
@@ -1,4 +1,20 @@
 class TimeSlider
+
+    # TODO: Not sure if this is the only solution but this is needed to make sure
+    #       events can be dispatched in Internet Explorer 11 (and below)
+    `(function () {
+      function CustomEvent ( event, params ) {
+        params = params || { bubbles: false, cancelable: false, detail: undefined };
+        var evt = document.createEvent( 'CustomEvent' );
+        evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
+        return evt;
+       }
+
+      CustomEvent.prototype = window.Event.prototype;
+
+      window.CustomEvent = CustomEvent;
+    })();`
+

     # TODO
     #  * Implement a function to fetch dataset information from a WMS / WCS service
@@ -155,6 +171,11 @@
             )
             .on('brush', =>
                 if (@brush_tootlip)
+                    offheight = 0
+                    if @svg[0][0].parentElement?
+                        offheight = @svg[0][0].parentElement.offsetHeight
+                    else
+                        offheight = @svg[0][0].parentNode.offsetHeight
                     @options.zoom
                         .scale(@options.lastZoom.scale)
                         .translate(@options.lastZoom.translate)
@@ -164,7 +185,7 @@
                         .style("opacity", .9);
                     @tooltip_brush_min.html(@simplifyDate(@brush.extent()[0]))
                         .style("left", (@scales.x(@brush.extent()[0])+@brush_tooltip_offset[0]) + "px")
-                        .style("top", (@svg[0][0].parentElement.offsetHeight + @brush_tooltip_offset[1]) + "px");
+                        .style("top", (offheight + @brush_tooltip_offset[1]) + "px");

                     @tooltip_brush_max.transition()
@@ -172,7 +193,7 @@
                         .style("opacity", .9);
                     @tooltip_brush_max.html(@simplifyDate(@brush.extent()[1]))
                         .style("left", (@scales.x(@brush.extent()[1])+@brush_tooltip_offset[0]) + "px")
-                        .style("top", (@svg[0][0].parentElement.offsetHeight + @brush_tooltip_offset[1] + 20) + "px");
+                        .style("top", (offheight + @brush_tooltip_offset[1] + 20) + "px");

             )
             .extent([@options.brush.start, @options.brush.end])


mmd: fix indexof bug on values from last night

--- src/mmd.coffee
+++ src/mmd.coffee
@@ -13,7 +13,7 @@
   isLeaf: -> @values.length > 0

   addValue: (val) ->
-    @values.push val unless @values.indexOf val > -1
+    @values.push val unless @values.indexOf(val) > -1

   getChild: (name) ->
     if @children[name]?


Create initial data

Create initial data

--- src/coffee/main.cjsx
+++ src/coffee/main.cjsx
@@ -25,7 +25,12 @@

 Changer = React.createClass
   getInitialState: ->
-    csv: ''
+    csv: """
+         ship
+         type,class,name,weight
+         軽巡洋艦,川内型,那珂,5195
+         重巡洋艦,利根型,利根,11213
+         """
     sql: ''
     now: ''

@@ -61,16 +66,24 @@
     </form>

+convertCell = (cell) ->
+  cell = cell.trim()
+  if cell.length > 0 and (cell[0] not in ['"', "'"]) and isNaN(cell)
+    '"' + cell + '"'
+  else
+    cell
+
 class Table
   constructor: (@lines) ->
     @name = @lines[0]
     @columns = @lines[1].split(',')
-    @records = @lines.slice(2).map (l) -> l.split(',')
+    @records = @lines.slice(2).map (l) ->
+       l.split(',').map(convertCell)

   # nullable
   toSQL: (mani) ->
     switch mani
-      when 'INSERT' then console.log(@createInsert()); @createInsert()
+      when 'INSERT' then @createInsert()
       when 'DELETE' then @createDelete()
       when 'UPSERT' then @createUpsert()
       else null
@@ -82,6 +95,7 @@
       #{lines.join(',\n  ')};
     """

+
 isEmpty = (str) -> str == ''
 nonEmpty = (str) -> str != ''
 dropEmptyLine = (xs) -> _.dropWhile(xs, isEmpty)
@@ -92,7 +106,7 @@
     lines = _.dropWhile(lines, nonEmpty)
     lines = dropEmptyLine(lines)
     new Table(tableLines)
-  (tables.map (t) -> console.log(t); t.toSQL(mani)).join('\n\n')
+  (tables.map (t) -> t.toSQL(mani)).join('\n\n')

 window.onload = ->
   React.render <Changer />, document.getElementById('changer')


Add basecamp

Add basecamp

--- src/pow.coffee
+++ src/pow.coffee
@@ -4,6 +4,7 @@
   "/Volumes/37signals/iterations/config.ru": ["iterations.test"]
   "/Volumes/37signals/portfolio/config.ru":  ["37img.test"]
   "/Volumes/37signals/launchpad/config.ru":  ["launchpad.test"]
+  "/Volumes/37signals/basecamp/config.ru":   ["37s.basecamp.test"]

 server.listen 3000


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_levels.coffee
+++ src/engine_levels.coffee
@@ -15,12 +15,13 @@

   I.transitioning = false

-  loadLevel = (level) ->
+  loadLevel = (level, offset) ->
     unless I.transitioning
       I.transitioning = true

       levelState = LevelState
         level: level
+        offset: offset

       I.currentLevelName = level
       engine.setState levelState


Return empty string if task help is blank

Make sure the help text for a task is refreshed when the selected task
changes since a undefined property doesn't cause React to rerender

Closes #1090

--- app/classifier/tasks/generic-editor.cjsx
+++ app/classifier/tasks/generic-editor.cjsx
@@ -63,7 +63,7 @@
           <AutoSave resource={@props.workflow}>
             Help text

-            <textarea  name="#{@props.taskPrefix}.help" value={@props.task.help} rows="7" className="standard-input full" onChange={handleChange} />
+            <textarea  name="#{@props.taskPrefix}.help" value={@props.task.help ? ""} rows="7" className="standard-input full" onChange={handleChange} />
           </AutoSave>
           Add text and images for a window that pops up when volunteers click “Need some help?” You can use markdown to format this text and add images. The help text can be as long as you need, but you should try to keep it simple and avoid jargon.

}


increment the badge (iOS)

increment the badge (iOS)

--- app/packages/_notifications/index.coffee
+++ app/packages/_notifications/index.coffee
@@ -52,6 +52,7 @@
     data:
       alert: message
       formId: formId
+      badge: "Increment"
   Parse.Push.send(push, opts).then(
       Meteor.bindEnvironment (s) ->
         Notifications.update(notificationId, {$set: {sent:  true, updated_at: new Date()}});


missed cart items...

missed cart items...

--- src/features/cart.coffee
+++ src/features/cart.coffee
@@ -26,7 +26,7 @@

 		Update: (id, data, callback, error) ->

-			return @m.Request 'carts/'+@identifier+'/item/'+id, 'PUT', data, callback, error
+			return @m.Request 'carts/'+@identifier+'/items/'+id, 'PUT', data, callback, error

 		Delete: (callback, error) ->

@@ -34,11 +34,11 @@

 		Remove: (id, callback, error) ->

-			return @m.Request 'carts/'+@identifier+'/item/'+id, 'DELETE', null, callback, error
+			return @m.Request 'carts/'+@identifier+'/items/'+id, 'DELETE', null, callback, error

 		Item: (id, callback, error) ->

-			return @m.Request 'carts/'+@identifier+'/item/'+id, 'GET', null, callback, error
+			return @m.Request 'carts/'+@identifier+'/items/'+id, 'GET', null, callback, error

 		InCart: (id, callback, error) ->


Reverting palette optimization.

Reverting palette optimization.

--- packages/landsofillusions-assets/engine/sprite/sprite.coffee
+++ packages/landsofillusions-assets/engine/sprite/sprite.coffee
@@ -3,21 +3,17 @@

 class LOI.Assets.Engine.Sprite
   constructor: (@options) ->
-    @palette = new ComputedField =>
-      return unless spriteData = @options.spriteData()
-      spriteData.customPalette or LOI.Assets.Palette.documents.findOne(spriteData.palette?._id)
-
     @ready = new ComputedField =>
       return unless spriteData = @options.spriteData()
       return unless spriteData.layers?.length and spriteData.bounds
-      return unless @palette() or @options.visualizeNormals?()
+      return unless spriteData.customPalette or LOI.Assets.Palette.documents.findOne(spriteData.palette?._id) or @options.visualizeNormals?()

       true

     # Create temporary objects and constants.
     @_normal = new THREE.Vector3
     @_backward = new THREE.Vector3 0, 0, 1
-    @_destinationColor = new THREE.Color
+    @_color = new THREE.Color
     @_lightColor = new THREE.Color
     @_averageNormal = new THREE.Vector3

@@ -59,7 +55,7 @@
     # On the server we need to manually request pixel maps.
     spriteData.requirePixelMaps() if Meteor.isServer

-    palette = @palette()
+    palette = spriteData.customPalette or LOI.Assets.Palette.documents.findOne(spriteData.palette?._id)

     # Build a new canvas if needed.
     unless @_canvas?.width is spriteData.bounds.width and @_canvas?.height is spriteData.bounds.height
@@ -133,19 +129,24 @@
             else
               lightness = 0.5

-            @_destinationColor.setHSL hue, saturation, lightness
+            @_color.setHSL hue, saturation, lightness
+            destinationColor = @_color

           else
-            @_destinationColor = r: 0, g: 0, b: 0
+            destinationColor = r: 0, g: 0, b: 0

         else if renderOptions.renderNormalData
           # Rendering of raw normal data for use in shaders.
           if pixel.normal
-            @_destinationColor = r: pixel.normal.x * 0.5 + 0.5, g: pixel.normal.y * 0.5 + 0.5, b: pixel.normal.z * 0.5 + 0.5
-            @_destinationColor.r = -pixel.normal.x * 0.5 + 0.5 if flippedHorizontal
+            destinationColor =
+              r: pixel.normal.x * 0.5 + 0.5
+              g: pixel.normal.y * 0.5 + 0.5
+              b: pixel.normal.z * 0.5 + 0.5
+
+            destinationColor.r = -pixel.normal.x * 0.5 + 0.5 if flippedHorizontal

           else
-            @_destinationColor = r: 0, g: 0, b: 0
+            destinationColor = r: 0, g: 0, b: 0

         else if renderOptions.renderPaletteData
           # Rendering of ramp + shade + dither data for use in shaders.
@@ -166,13 +167,13 @@
             paletteColor = pixel.paletteColor

           if paletteColor
-            @_destinationColor =
+            destinationColor =
               r: paletteColor.ramp / 255
               g: paletteColor.shade / 255
               b: paletteColor.dither or 0

           else
-            @_destinationColor = r: 255, g: 255, b: 255, a: 0
+            destinationColor = r: 1, g: 1, b: 1, a: 0

         else
           paletteColor = null
@@ -295,7 +296,7 @@
                     secondBestColor = shade
                     secondBestColorDistance = distance

-              @_destinationColor = bestColor
+              destinationColor = bestColor

               # Apply dithering.

@@ -303,7 +304,7 @@

               if ditherPercentage > 1 - (paletteColor?.dither or 0)
                 if Math.abs(pixel.x % 2) + Math.abs(pixel.y % 2) is 1
-                  @_destinationColor = secondBestColor
+                  destinationColor = secondBestColor

               ### Smooth shading routine
               #closest = THREE.Color.fromObject bestColor
@@ -315,18 +316,18 @@
               ###

             else
-              @_destinationColor = shadedColor
+              destinationColor = shadedColor

           else
-            @_destinationColor = sourceColor
+            destinationColor = sourceColor

         if erase
           @_imageData.data[pixelIndex + 3] = 0

         else
-          @_imageData.data[pixelIndex] = @_destinationColor.r * 255
-          @_imageData.data[pixelIndex + 1] = @_destinationColor.g * 255
-          @_imageData.data[pixelIndex + 2] = @_destinationColor.b * 255
-          @_imageData.data[pixelIndex + 3] = (@_destinationColor.a or 1) * 255
+          @_imageData.data[pixelIndex] = destinationColor.r * 255
+          @_imageData.data[pixelIndex + 1] = destinationColor.g * 255
+          @_imageData.data[pixelIndex + 2] = destinationColor.b * 255
+          @_imageData.data[pixelIndex + 3] = (destinationColor.a or 1) * 255

     @_canvas.putFullImageData @_imageData


only check pdf page visibility on scroll every 50ms

--- services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
+++ services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
@@ -258,16 +258,28 @@
 					#scope.$apply()

 				element.on 'scroll', () ->
-					# console.log 'scroll detected', scope.adjustingScroll
+					if scope.adjustingScroll
+						updateContainer()
+						scope.$apply()
+						scope.adjustingScroll = false
+						return
+					scope.scrolled = true
+					return if scope.handler?
+					scope.handler = requestAnimationFrame(scrollHandler)
+
+				scrollStart = null
+				SCROLL_TIMEOUT = 50
+				scrollHandler = (timestamp) ->
+					scrollStart = timestamp if !scrollStart?
+					progress = timestamp - scrollStart
+					if (progress < SCROLL_TIMEOUT)
+						scope.handler = requestAnimationFrame(scrollHandler)
+						return
+					scrollStart = null
+					scope.handler = null
 					updateContainer()
 					scope.$apply()
-					#console.log 'pdfposition', element.parent().scrollTop()
-					if scope.adjustingScroll
-						scope.adjustingScroll = false
-						return
-					#console.log 'not from auto scroll'
 					scope.position = ctrl.getPdfPosition()
-					# console.log 'position is', scope.position.page, scope.position.offset
 					scope.$apply()

 				scope.$watch 'pdfSrc', (newVal, oldVal) ->


Add optional dropzone_target if not specified

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -37,7 +37,8 @@
   setUploadForm = ->
     $uploadForm.fileupload

-      dropZone: settings.dropzone_target
+      if settings.dropzone_target
+        dropZone: settings.dropzone_target

       add: (e, data) ->
         file = data.files[0]


Ashok - Removing the progress bar of the removed file

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -41,6 +41,8 @@
         form.files[0].name isnt fileName
       )
       current_files = forms_for_submit
+      progress_bar = settings.progress_bar_target or $uploadForm
+      progress_bar.find("#file-" + removedFile.unique_id).remove()
       removedFile

     $.extend $uploadForm, obj


Clampable#clampToBounds

Clampable#clampToBounds

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -8,3 +8,5 @@

   clamp: (data) ->
     Object.extend(I.clampData, data)
+
+  clampToBounds: ()->


evaluate numbers properly

evaluate numbers properly

--- src/evaluator.coffee
+++ src/evaluator.coffee
@@ -25,8 +25,10 @@
         Builder = String
       when 'decimal'
         Builder = Decimal
+      when 'integer'
+        Builder = Integer
       else
-        Builder = Literal
+        throw 'unknown node type'
     new Builder node

 factory = new Factory()
@@ -99,9 +101,17 @@
       when '>='
         left >= right

-class Literal
-  constructor: (@value) ->
-  evaluate: (data) -> @value
+class Decimal
+  constructor: (node) ->
+    @value = parseFloat( node.whole + '.' + node.part )
+  evaluate: (data) ->
+    @value
+
+class Integer
+  constructor: (node) ->
+    @value = parseInt node.value
+  evaluate: (data) ->
+    @value

 evaluate = (formula, data) ->
   f = factory.build formula


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/expirable.coffee
+++ src/expirable.coffee
@@ -3,6 +3,7 @@
     duration: -1
     alpha: 1
     fadeOut: false
+    age: 0

   startingAlpha = I.alpha


Return last completed step as default step index

--- src/flux/task.coffee
+++ src/flux/task.coffee
@@ -118,13 +118,7 @@

     getDefaultStepIndex: (taskId) ->
       steps = getSteps(@_steps[taskId])
-
-      if steps.length is 1
-        return 0
-      stepIndex = getCurrentStepIndex(steps)
-
-      completeStep = _.find steps, {is_completed: true}
-      if stepIndex is 0 and not completeStep? then -1 else stepIndex
+      getCurrentStepIndex(steps)

     getStepsIds: (id) ->
       _.map(@_steps[id], (step) ->


Making some methods private.

Making some methods private.

--- src/glyphtree.coffee
+++ src/glyphtree.coffee
@@ -8,7 +8,6 @@

 defaults =
   classPrefix: "filetree-"
-  typePrefix: "type-"
   types:
     default:
       icon:
@@ -64,23 +63,23 @@
     style = (typeName, type) =>
       # Make the default state being the absence of a state class
       stateSelector = (state) =>
-        if state == 'default' then '' else '.'+@stateClass(state)
+        if state == 'default' then '' else '.'+@_stateClass(state)
       typeSelector = (t) =>
-        if t == 'default' then '' else '.'+@typeClass(t)
+        if t == 'default' then '' else '.'+@_typeClass(t)
       # Setup icons for each of the states.
       # Any missing will have the default state icon.
       ("""
-      .#{@idClass} ul li.#{@nodeClass()}#{stateSelector(state)}#{typeSelector(typeName)}:before {
+      .#{@idClass} ul li.#{@_nodeClass()}#{stateSelector(state)}#{typeSelector(typeName)}:before {
         font-family: #{type.icon[state].font};
         content: '#{type.icon[state].content}';
       }
       """ for state in ['default', 'leaf', 'expanded'] when type.icon[state]).join("\n")+
       # Hide children except when expanded
       """
-      .#{@idClass} ul li.#{@nodeClass()}#{stateSelector('default')}#{typeSelector(typeName)} > ul.#{@treeClass()} {
+      .#{@idClass} ul li.#{@_nodeClass()}#{stateSelector('default')}#{typeSelector(typeName)} > ul.#{@_treeClass()} {
         display: none;
       }
-      .#{@idClass} ul li.#{@nodeClass()}#{stateSelector('expanded')}#{typeSelector(typeName)} > ul.#{@treeClass()} {
+      .#{@idClass} ul li.#{@_nodeClass()}#{stateSelector('expanded')}#{typeSelector(typeName)} > ul.#{@_treeClass()} {
         display: block;
       }
       """
@@ -88,10 +87,10 @@
     .#{@idClass} ul {
       list-style-type: none;
     }
-    .#{@idClass} ul li.#{@nodeClass()} {
+    .#{@idClass} ul li.#{@_nodeClass()} {
       cursor: pointer;
     }
-    .#{@idClass} ul li.#{@nodeClass()}:before {
+    .#{@idClass} ul li.#{@_nodeClass()}:before {
       width: 1em;
       text-align: center;
       display: inline-block;
@@ -123,13 +122,13 @@
     makeElement = (node) =>
       $li = $("<li/>")
       $li.text(node.name)
-      $li.addClass(@nodeClass())
-      $li.addClass(@typeClass(node.type ? 'default'))
+      $li.addClass(@_nodeClass())
+      $li.addClass(@_typeClass(node.type ? 'default'))
       $li.attr("data-#{k}", v) for k, v of node.attributes
       if (node.children ? []).length == 0
-        $li.addClass(@stateClass('leaf'))
+        $li.addClass(@_stateClass('leaf'))
       else
-        expandedClass = @stateClass('expanded')
+        expandedClass = @_stateClass('expanded')
         $li.click (e) ->
           if this == e.target
             $(this).toggleClass(expandedClass)
@@ -139,19 +138,19 @@

     makeElements = (nodes) =>
       $list = $("<ul/>")
-      $list.addClass(@treeClass())
+      $list.addClass(@_treeClass())
       $list.append(makeElement(node)) for node in nodes
       $list

     $(@element).empty()
     $(@element).append(makeElements(structure))
     this
+
+  _nodeClass: () -> @options.classPrefix+"node"
+  _treeClass: () -> @options.classPrefix+"tree"

-  nodeClass: () -> @options.classPrefix+"node"
-  treeClass: () -> @options.classPrefix+"tree"
-
-  typeClass: (typeName) ->
-    @options.classPrefix+@options.typePrefix+typeName
+  _typeClass: (typeName) ->
+    @options.classPrefix+'type-'+typeName

-  stateClass: (state) ->
-    @options.classPrefix+state+  _stateClass: (state) ->
+    @options.classPrefix+state


append only chainable tokens

append only chainable tokens

--- src/interface.coffee
+++ src/interface.coffee
@@ -172,6 +172,11 @@
     targetIndex = if side == 'left' then mostOuterTokenIndex - 1 else mostOuterTokenIndex + 1
     return if targetIndex < 0 || targetIndex >= this.$tokens.length

+    # check if target index is chainabel
+    $targetToken = this.$tokens.get(targetIndex)
+    $chainableTokens = $('.token', mostOuterToken.$token.parent())
+    return unless $chainableTokens.filter($targetToken).length
+
     mostOuterToken.$token.removeClass("#{side}-end")
     $token = $(this.$tokens.get(targetIndex))
     this.removeChunkWithIndex(targetIndex) if $token.data('token-id') >= 0


处理注入参数会被压缩的问题

处理注入参数会被压缩的问题

--- src/js/routes.coffee
+++ src/js/routes.coffee
@@ -512,9 +512,9 @@

   ])

-  _app.run ($rootScope, $timeout, $state, API)->
+  _app.run(['$rootScope', '$state', 'API', ($rootScope, $state, API)->
     #如果用户没有获取登录信息， 则先获取用户的登录信息，则继续流程
-    $rootScope.$on('$stateChangeStart', (event, toState, toParams, fromState, fromParams)->
+    $rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams)->
       #没有获取到用户的信息，阻止用户
       return if $rootScope.__member

@@ -524,4 +524,4 @@
       API.session().retrieve().then (result)->
         $rootScope.__member = result
         $state.go(toState, toParams)
-    )+  ]);


Export AsyncComponent.

Export AsyncComponent.

--- src/lib/NoFlo.coffee
+++ src/lib/NoFlo.coffee
@@ -3,6 +3,7 @@
 #     NoFlo may be freely distributed under the MIT license
 internalSocket = require "./InternalSocket"
 component = require "./Component"
+asynccomponent = require "./AsyncComponent"
 port = require "./Port"
 arrayport = require "./ArrayPort"
 graph = require "./Graph"
@@ -222,6 +223,7 @@
         success file

 exports.Component = component.Component
+exports.AsyncComponent = asynccomponent.AsyncComponent
 exports.Port = port.Port
 exports.ArrayPort = arrayport.ArrayPort
 exports.Graph = graph.Graph


[➦] Merged changes.

[➦] Merged changes.

--- vendor/assets/javascripts/jquery/plugins/shadify/shady.js.coffee
+++ vendor/assets/javascripts/jquery/plugins/shadify/shady.js.coffee
@@ -39,7 +39,7 @@
       maxLimit: null
       minDistance: 20
       maxDistance: 400
-      autopilot: true
+      autopilot: false
       draw: true
       bounds: do FSS.Vector3.create
       step: FSS.Vector3.create Math.randomInRange(0.2, 1.0), Math.randomInRange(0.2, 1.0), Math.randomInRange(0.2, 1.0)


Refactored dialog JS

Refactored dialog JS

I largely refactored this to make a very basic change - to make it so that setting `data-confirm-proceed-class` overrides `btn-primary` as opposed to appending after it, as this doesn't style the element correctly, for example, a button with class `btn-primary btn-danger` can end up primary styled, they're not for use in conjunction.

Other than that, this should work exactly as it did before, just a hell of a lot easier to read (I hope!).

--- vendor/assets/javascripts/twitter/bootstrap/rails/confirm.coffee
+++ vendor/assets/javascripts/twitter/bootstrap/rails/confirm.coffee
@@ -1,42 +1,65 @@
 $.fn.twitter_bootstrap_confirmbox =
   defaults:
-    fade: false
     title: null
+    proceed: "OK"
+    proceed_class: "btn proceed"
     cancel: "Cancel"
     cancel_class: "btn cancel"
-    proceed: "OK"
-    proceed_class: "btn proceed btn-primary"
+    fade: false

 TwitterBootstrapConfirmBox = (message, element, callback) ->
-  $(document.body).append(
-    $(
-      '

-
<button type="button" class="close" data-dismiss="modal">×</button>
...

-

-
......

-
'
-    )
-  )
+  $dialog = $('
+

+

+        <button type="button" class="close" data-dismiss="modal">×</button>
+
...

+

+

+

+

+  ')

-  $("#confirmation_dialog").addClass("fade") if element.data("confirm-fade") || $.fn.twitter_bootstrap_confirmbox.defaults.fade
-  $("#confirmation_dialog .modal-body").html(message.replace(/\n/g, "
"))
-  $("#confirmation_dialog .modal-header h3").html(element.data("confirm-title") || $.fn.twitter_bootstrap_confirmbox.defaults.title || window.top.location.origin)
-  $("#confirmation_dialog .modal-footer .cancel").html(element.data("confirm-cancel") || $.fn.twitter_bootstrap_confirmbox.defaults.cancel).attr("class", $.fn.twitter_bootstrap_confirmbox.defaults.cancel_class).addClass(element.data("confirm-cancel-class"))
-  $("#confirmation_dialog .modal-footer .proceed").html(element.data("confirm-proceed") || $.fn.twitter_bootstrap_confirmbox.defaults.proceed).attr("class", $.fn.twitter_bootstrap_confirmbox.defaults.proceed_class).addClass(element.data("confirm-proceed-class"))
-  $("#confirmation_dialog").modal("show").on("hidden", ->
-    $(this).remove()
-  )
+  $dialog.addClass("fade") if element.data("confirm-fade") || $.fn.twitter_bootstrap_confirmbox.defaults.fade

-  $("#confirmation_dialog .proceed").click (event) ->
-    event.preventDefault()
-    $("#confirmation_dialog").modal("hide")
-    callback()
-    false
+  $dialog
+    .find(".modal-header")
+      .find("h3")
+        .html(element.data("confirm-title") || $.fn.twitter_bootstrap_confirmbox.defaults.title || window.top.location.origin)
+      .end()
+    .end()

-  $("#confirmation_dialog .cancel").click (event) ->
-    event.preventDefault()
-    $("#confirmation_dialog").modal("hide")
-    false
+    .find(".modal-body")
+      .html(message.replace(/\n/g, "
"))
+    .end()
+
+    .find(".modal-footer")
+      .append( ->
+        $("", {href: "#", "data-dismiss": "modal"})
+          .html(element.data("confirm-cancel") || $.fn.twitter_bootstrap_confirmbox.defaults.cancel)
+          .addClass($.fn.twitter_bootstrap_confirmbox.defaults.cancel_class)
+          .addClass(element.data("confirm-cancel-class"))
+          .click((event) ->
+            event.preventDefault()
+            $dialog.modal("hide")
+          )
+        ,
+        $("", {href: "#"})
+          .html(element.data("confirm-proceed") || $.fn.twitter_bootstrap_confirmbox.defaults.proceed)
+          .addClass($.fn.twitter_bootstrap_confirmbox.defaults.proceed_class)
+          .addClass(element.data("confirm-proceed-class") || "btn-primary")
+          .click((event) ->
+            event.preventDefault()
+            $dialog.modal("hide")
+            callback()
+          )
+      )
+    .end()
+
+    .on("hidden", -> $(this).remove())
+
+    .modal("show")
+
+    .appendTo(document.body)

 $.rails.allowAction = (element) ->
   message = element.data("confirm")


redirect old profile pages to the user

redirect old profile pages to the user

--- apps/channel/routes.coffee
+++ apps/channel/routes.coffee
@@ -21,6 +21,7 @@

   channel.fetch
     success: ->
+      return res.redirect 301, "/#{channel.get('slug')}" if channel.get('class') is 'User'
       blocks.add channel.get 'contents'

       res.locals.sd.CHANNEL = channel.toJSON()
@@ -44,6 +45,7 @@

   channel.fetch
     success: =>
+      return res.redirect 301, "/#{channel.get('slug')}" if channel.get('class') is 'User'
       res.locals.sd.CHANNEL = channel.toJSON()
       author = new User channel.get('user')
       render()


Wait more time in ChunkBufferSpec, to make it more reliable

--- assets/scripts/spec/unit/chunk_buffer_spec.coffee
+++ assets/scripts/spec/unit/chunk_buffer_spec.coffee
@@ -27,7 +27,7 @@

     expect(buffer.get('length')).toEqual(1)

-    waits 40
+    waits 100
     runs ->
       expect(buffer.get('length')).toEqual(2)
       expect(buffer.toArray()).toEqual(['foo', 'baz'])


worker: read bucket name from config

worker: read bucket name from config

--- workers/social/lib/social/models/s3.coffee
+++ workers/social/lib/social/models/s3.coffee
@@ -16,7 +16,7 @@
   AWS_KEY      = KONFIG.awsKeys.worker_koding_client_s3_put_only.accessKeyId
   AWS_SECRET   = KONFIG.awsKeys.worker_koding_client_s3_put_only.secretAccessKey

-  AWS_BUCKET   = 'koding-client'
+  AWS_BUCKET   = KONFIG.clientUploadS3BucketName
   AWS_URL      = "https://#{AWS_BUCKET}.s3.amazonaws.com"

   EXPIREIN     = 100     # in seconds.


[VM] add defensive checks for user object existence

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -154,7 +154,7 @@
     domain = 'kd.io'
     if type is 'user'
       requiredDomains = ["#{nickname}.#{groupSlug}.#{domain}"]
-      if groupSlug is 'koding'
+      if groupSlug in ['koding', 'guests']
         requiredDomains.push "#{nickname}.#{domain}"
     else
       requiredDomains = ["#{groupSlug}.#{domain}", "shared.#{groupSlug}.#{domain}"]
@@ -166,7 +166,7 @@
     if type is 'user'
       if uid is 0
         aliases.push "#{nickname}.#{groupSlug}.#{domain}"
-      if groupSlug is 'koding'
+      if groupSlug in ['koding', 'guests']
         aliases.push "#{nickname}.#{domain}"  if uid is 0
         aliases.push "vm-#{uid}.#{nickname}.#{domain}"
       aliases.push "vm-#{uid}.#{nickname}.#{groupSlug}.#{domain}"
@@ -206,6 +206,7 @@
       return callback err  if err
       account.fetchUser (err, user)=>
         return callback err  if err
+        return callback new Error "user is not defined"  unless user

         # We are keeping this names just for counter
         planOwner   = "group_#{group._id}"
@@ -268,14 +269,14 @@
       return callback err  if err
       members.forEach (member)->
         member.fetchUser (err, user)->
-          if err then callback err
-          else
-            member.checkPermission group, 'sudoer', (err, hasPermission)->
-              if err then handleError err
-              else
-                vm.update {
-                  $addToSet: users: { id: user.getId(), sudo: hasPermission }
-                }, callback
+          return callback err if err
+          return callback new Error "user not found" unless user
+          member.checkPermission group, 'sudoer', (err, hasPermission)->
+            if err then handleError err
+            else
+              vm.update {
+                $addToSet: users: { id: user.getId(), sudo: hasPermission }
+              }, callback

   # @getUsageTemplate = -> { cpu: 0, ram: 0, disk: 0 }

@@ -303,6 +304,7 @@

     delegate.fetchUser (err, user) ->
       return callback err  if err
+      return callback new Error "user not found" unless user

       JVM.one
         hostnameAlias : hostnameAlias
@@ -327,6 +329,8 @@
     {delegate} = client.connection
     delegate.fetchUser (err, user) ->
       return callback err  if err
+      return callback new Error "user not found" unless user
+
       JGroup = require './group'
       JGroup.one slug:'koding', (err, fetchedGroup)=>
         return callback err  if err
@@ -346,6 +350,7 @@

     account.fetchUser (err, user) ->
       return callback err  if err
+      return callback new Error "user not found" unless user

       selector.users = $elemMatch: id: user.getId()

@@ -395,6 +400,7 @@

     delegate.fetchUser (err, user) ->
       return callback err  if err
+      return callback new Error "user not found" unless user

       selector =
         hostnameAlias : hostnameAlias
@@ -464,6 +470,7 @@

     delegate.fetchUser (err, user)=>
       return callback err  if err
+      return callback new Error "user not found" unless user

       selector =
         hostnameAlias : hostnameAlias
@@ -497,9 +504,11 @@
     addVm = ({ account, target, user, sudo, groups, groupSlug
                type, planCode, planOwner, webHome })->

+      return handleError new Error "user is not defined"  unless user
+      nickname = account.profile.nickname or user.username
       uid = 0
       hostnameAliases = JVM.createAliases {
-        nickname : user.username
+        nickname
         type, uid, groupSlug
       }

@@ -526,7 +535,9 @@
           return console.warn "Failed to create VM for ", \
                                {users, groups, hostnameAlias}

-        JVM.createDomains account, hostnameAliases, hostnameAlias
+        JVM.ensureDomainSettings \
+          {account, vm, type, nickname, groupSlug}
+        # JVM.createDomains account, hostnameAliases, hostnameAlias
         target.addVm vm, handleError

     wrapGroup =(group)-> [ { id: group.getId() } ]
@@ -549,6 +560,7 @@
         else user.update { $set: { uid } }, handleError

     JUser.on "UserBlocked", (user)->
+      return handleError new Error "user not found" unless user
       selector =
         'users.id'    : user.getId()
         'users.owner' : yes
@@ -563,6 +575,7 @@
             console.error err if err

     JUser.on "UserUnblocked", (user)->
+      return handleError new Error "user not found" unless user
       selector =
         'users.id'    : user.getId()
         'users.owner' : yes
@@ -631,8 +644,10 @@

     JGroup.on 'MemberAdded', ({group, member})->
       member.fetchUser (err, user)->
-        if err then handleError err
-        else if group.slug is 'guests'
+        return handleError err  if err
+        return handleError new Error "user not defined" unless user
+
+        if group.slug is 'guests'
           # Following is just here to register this name in the counters collection
           ((require 'koding-counter') {
             db          : JVM.getClient()
@@ -672,10 +687,12 @@

     JGroup.on 'MemberRemoved', ({group, member})->
       member.fetchUser (err, user)->
-        if err then handleError err
+        return handleError err  if err
+        return handleError new Error "user not found" unless user
+
         # Do we need to take care guests here? Like when guests ends up session
         # Do we also need to remove their vms? ~ GG
-        else if group.slug is 'koding'
+        if group.slug is 'koding'
           member.fetchVms (err, vms)->
             if err then handleError err
             else vms.forEach (vm)->
@@ -693,18 +710,19 @@
     JGroup.on 'MemberRolesChanged', ({group, member})->
       return  if group.slug 'koding'  # TODO: remove this special case
       member.fetchUser (err, user)->
-        if err then handleError err
-        else
-          member.checkPermission group, 'sudoer', (err, hasPermission)->
-            if err then handleError err
-            else if hasPermission
-              member.fetchVms (err, vms)->
-                if err then handleError err
-                else
-                  vms.forEach (vm)->
-                    vm.update {
-                      $set: users: vm.users.map (userRecord)->
-                        isMatch = userRecord.id.equals user.getId()
-                        return userRecord  unless isMatch
-                        return { id, sudo: hasPermission }
-                    }, handleError
+        return handleError err  if err
+        return handleError new Error "user not found"  unless user
+
+        member.checkPermission group, 'sudoer', (err, hasPermission)->
+          if err then handleError err
+          else if hasPermission
+            member.fetchVms (err, vms)->
+              if err then handleError err
+              else
+                vms.forEach (vm)->
+                  vm.update {
+                    $set: users: vm.users.map (userRecord)->
+                      isMatch = userRecord.id.equals user.getId()
+                      return userRecord  unless isMatch
+                      return { id, sudo: hasPermission }
+                  }, handleError


Don't fail for hostnames with '-' in them. Also assign mandatory fields.

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -86,18 +86,25 @@
       JDomain.assure { domain }, (err, domainObj) ->
         console.log err  if err

+        if domainObj.domain.indexOf("vm-") is -1
+          hostnameAliases = []
+        else
+          hostnameAliases = [hostnameAlias]
+
         if domainObj.isNew
-          domainObj.hostnameAlias = [hostnameAlias]
+          domainObj.hostnameAlias = hostnameAliases
           domainObj.proxy         = { mode: 'vm' }
           domainObj.regYears      = 0
+          domainObj.loadBalancer  = { persistance: 'disabled' }
           domainObj.save (err)->
             console.log err  if err
             updateRelationship domainObj
         else
           fields =
-            hostnameAlias : [hostnameAlias]
+            hostnameAlias : hostnameAliases
             proxy         : { mode: 'vm' }
             regYears      : 0
+            loadBalancer  : { persistance: 'disabled' }
           domainObj.update { $set: fields }, (err)->
             console.log err  if err
             updateRelationship domainObj
@@ -122,8 +129,11 @@
           JUser.one
             _id: vmUser[0].id
           , (err, user)=>
+            if err
+              cosole.log "ERR:", err
             if not err and user
               user.fetchAccount 'koding', (err, account)=>
+                console.log "WORKING ON VM FOR #{nickname} - #{hostnameAliases[0]}"
                 if not err and account
                   @ensureDomainSettings {account, vm, type, nickname, groupSlug}
                   @createDomains account, hostnameAliases, hostnameAliases[0]
@@ -162,14 +172,14 @@
   @parseAlias = (alias)->
     # group-vm alias
     if /^shared-[0-9]/.test alias
-      result = alias.match /(.*)\.(\w+).kd.io$/
+      result = alias.match /(.*)\.([a-z0-9-]+).kd.io$/
       if result
         [rest..., prefix, groupSlug] = result
         uid = parseInt(prefix.split(/-/)[1], 10)
         return {groupSlug, prefix, uid, type:'group', alias}
     # personal-vm alias
     else if /^vm-[0-9]/.test alias
-      result = alias.match /(.*)\.(\w+)\.(\w+).kd.io$/
+      result = alias.match /(.*)\.([a-z0-9-]+)\.([a-z0-9-]+).kd.io$/
       if result
         [rest..., prefix, nickname, groupSlug] = result
         uid = parseInt(prefix.split(/-/)[1], 10)


pricing: don't start the workflow when there is already a one in progress

--- client/Pricing/AppView.coffee
+++ client/Pricing/AppView.coffee
@@ -186,36 +186,49 @@
    * before filter, that filter will decide
    * if this method will be called or not.
   ###
-  planSelected: (options) -> @preventBlockedUser options, =>
-
-    return KD.singletons
-      .router
-      .handleRoute '/Login'  unless KD.isLoggedIn()
-
-    # wait for loading the current plan,
-    # call this method until it's ready.
-    unless @state.currentPlan?
-      return @loadPlan => @planSelected options
-
-    isCurrentPlan =
-      options.planTitle    is @state.currentPlan and
-      options.planInterval is @state.currentPlanInterval and
-      'expired'          isnt @state.subscriptionState
-
-    return KD.showError "That's already your current plan."  if isCurrentPlan
-
-    @setState options
-
-    @workflowController = new PaymentWorkflow { @state, delegate: this }
-
-    @workflowController.once 'PaymentWorkflowFinishedSuccessfully', (state) =>
-
-      @state.currentPlan = state.planTitle
-      @plans.setState @state
-
-      KD.singletons
+  planSelected: do (inProcess = no) -> (options) ->
+
+    return  if inProcess
+
+    @preventBlockedUser options, =>
+
+      return KD.singletons
         .router
-        .handleRoute '/'
+        .handleRoute '/Login'  unless KD.isLoggedIn()
+
+      # To prevent any other thing to happen when a plan selected
+      # (not starting the whole payment process)
+      # we are not letting the rest of the process happen.
+      return  if inProcess
+
+      # wait for loading the current plan,
+      # call this method until it's ready.
+      unless @state.currentPlan?
+        return @loadPlan => @planSelected options
+
+      inProcess = yes
+
+      isCurrentPlan =
+        options.planTitle    is @state.currentPlan and
+        options.planInterval is @state.currentPlanInterval and
+        'expired'          isnt @state.subscriptionState
+
+      return KD.showError "That's already your current plan."  if isCurrentPlan
+
+      @setState options
+
+      @workflowController = new PaymentWorkflow { @state, delegate: this }
+
+      @workflowController.on 'WorkflowStarted', -> inProcess = no
+
+      @workflowController.once 'PaymentWorkflowFinishedSuccessfully', (state) =>
+
+        @state.currentPlan = state.planTitle
+        @plans.setState @state
+
+        KD.singletons
+          .router
+          .handleRoute '/'

   continueFrom: (planTitle, planInterval) ->


Improving logging

Improving logging

--- client/project/project.coffee
+++ client/project/project.coffee
@@ -76,7 +76,7 @@
     data = @data()
     if data?
       title = "#{data.owner}/#{data.projectId}"
-      logger.debug("Setting title: #{title}")
+      logger.debug("Setting SEO properties")
       SEO.set({
         author: data.owner,
         title: title,


Profile: fix wording

Profile: fix wording

--- client/Account/views/username.coffee
+++ client/Account/views/username.coffee
@@ -110,7 +110,7 @@
           return queue.next()
       =>
         # if everything is OK or didnt change, show profile updated modal
-        notify "Profile Updated" if profileUpdated
+        notify "Your account information is updated." if profileUpdated
     ]
     daisy queue


make lazy init function private

make lazy init function private

--- src/color/rgb.coffee
+++ src/color/rgb.coffee
@@ -100,6 +100,27 @@
 # RGB space constructor
 # ---------------------

+lazyInitRgbBase = ->
+  # create xyz base (luminance is unknown => need to multiply each column by a scalar)
+  bxyz = M [
+        @red.x         ,     @green.x           ,     @blue.x
+                 @red.y,                @green.y,               @blue.y
+    1 - @red.x - @red.y, 1 - @green.x - @green.y, 1 - @blue.x - @blue.y
+  ]
+  # calculate LU decomposition of xyz base
+  bxyzLU = lu bxyz
+  w = M [ @white.X, @white.Y, @white.Z ], 1
+  # get the needed scales or the columns of bxyz (sum of the columns of the base must be the white point)
+  bxyzLU.solve w, w # calculate in place
+  # scale bxyz to get the wanted XYZ base (sum of columns is white point)
+  bxyz = bxyz.mult M.diag w
+  @base = bxyz.array
+  @baseInv = (lu bxyz).getInverse().array
+
+  delete @toXYZ
+  delete @fromXYZ
+  return
+
 rgbSpaceConstructor = (@red, @green, @blue, @white, gamma, gammaInv) ->
   if typeof gamma == "function"
     @gamma = gamma
@@ -107,40 +128,21 @@
   else
     @g = gamma
     @gInv = 1 / gamma
-  # assume: white.Y = 1
-  @fromXYZ = (XYZ, T) ->
-    do @init
-    @fromXYZ XYZ, T
-  @toXYZ = (Rgb, T) ->
-    do @init
-    @toXYZ Rgb, T
+  # Lazy definition of following two methods.
+  @fromXYZ = ->
+    lazyInitRgbBase.call @
+    @fromXYZ.apply @, arguments
+  @toXYZ = ->
+    lazyInitRgbBase.call @
+    @toXYZ.apply @, arguments
+
+

 # RGB space prototype
 # -------------------

 rgbSpacePrototype =

-  init: ->
-    if not @base?
-      # create xyz base (luminance is unknown => need to multiply each column by a scalar)
-      bxyz = M [
-            @red.x         ,     @green.x           ,     @blue.x
-                     @red.y,                @green.y,               @blue.y
-        1 - @red.x - @red.y, 1 - @green.x - @green.y, 1 - @blue.x - @blue.y
-      ]
-      # calculate LU decomposition of xyz base
-      bxyzLU = lu bxyz
-      w = M [ @white.X, @white.Y, @white.Z ], 1
-      # get the needed scales or the columns of bxyz (sum of the columns of the base must be the white point)
-      bxyzLU.solve w, w # calculate in place
-      # scale bxyz to get the wanted XYZ base (sum of columns is white point)
-      bxyz = bxyz.mult M.diag w
-      @base = bxyz.array
-      @baseInv = (lu bxyz).getInverse().array
-
-      delete @toXYZ
-      delete @fromXYZ
-      return

   gamma: (x) ->
     pow(x, @g)


Klient: add webtermKillSessions helper for webterm.killSessions method

--- client/Main/kite/kites/klient.coffee
+++ client/Main/kite/kites/klient.coffee
@@ -20,6 +20,7 @@
     fsMove             : 'fs.move'
     fsCreateDirectory  : 'fs.createDirectory'

+    webtermKillSessions: 'webterm.killSessions'
     webtermGetSessions : 'webterm.getSessions'
     webtermPing        : 'webterm.ping'


Machine: instead connecting to kite in the constructor, move the logic to baseKite getter

--- client/Main/providers/machine.coffee
+++ client/Main/providers/machine.coffee
@@ -28,27 +28,32 @@
     { @label, @publicAddress, @_id, @provisioners
       @status, @uid, @queryString } = @jMachine = @getData()

-    if @queryString?
-
-      @kites   =
-        klient : KD.singletons.kontrol.getKite {
-          @queryString, correlationName: @uid
-        }
-
-    else
-      @kites = {}
-
     @fs =
       # TODO: add options check
       create : (options, callback)=>
         options.machine = this
         FSItem.create options, callback

+
   getName: ->
     @label or @publicAddress or @uid or "one of #{KD.nick()}'s machine"

+
   getBaseKite: ->
-    return @kites.klient  if @kites.klient?
-    return {
-      init : -> Promise.reject()
-    }
+
+    { kontrol } = KD.singletons
+
+    klient = kontrol.kites?.klient?[@uid]
+    return klient  if klient
+
+    if @queryString?
+
+      kontrol.getKite { @queryString, correlationName: @uid }
+
+    else
+
+      {
+        init       : -> Promise.reject()
+        connect    : noop
+        disconnect : noop
+      }


FIX : redirection when draft is sent

FIX : redirection when draft is sent

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -62,7 +62,7 @@
     componentWillReceiveProps: (nextProps) ->
         if nextProps.message
             # Save current message
-            @saveMessage @state,
+            @closeSaveDraft @state,
                 silent: true
                 hasChanged: @hasChanged @props, @state

@@ -120,9 +120,9 @@
         (props.lastUpdate and props.lastUpdate isnt state.date) or false

     componentWillUnmount: ->
-        @saveMessage @state, hasChanged: @hasChanged(@props, @state)
-
-    saveMessage: (state, options={}) ->
+        @closeSaveDraft @state, hasChanged: @hasChanged(@props, @state)
+
+    closeSaveDraft: (state, options={}) ->
         fetch = (error, message) =>
             return if error or not message

@@ -297,13 +297,17 @@
                 secondPanel:
                     action: 'conversation'
                     parameters: {conversationID, messageID}
+            return

         # Else it should bring to the default view
-        else
-            @redirect
-                direction: 'first'
-                action: 'default'
-                fullWidth: true
+        @redirect
+            direction: 'first'
+            action: 'account.mailbox.messages'
+            fullWidth: true
+            parameters: [
+                @props.selectedAccountID
+                @props.selectedMailboxID
+            ]

     # Cancel brings back to default view. If it's while replying to a message,
     # it brings back to this message.
@@ -344,8 +348,18 @@
     sendMessage: (event) ->
         event.preventDefault() if event?
         @state.isDraft = false
-        @sendActionMessage =>
-            @refs.toolbox.setState action: null if @refs.toolbox
+        @sendActionMessage (error, message) =>
+            if error
+                msgKo = t "message action sent ko"
+                LayoutActionCreator.alertError "#{msgKo} #{error}"
+                return
+
+            # Display confirmation message
+            # for no-draft email
+            msgOk = t "message action sent ok"
+            LayoutActionCreator.notify msgOk, autoclose: true
+
+            @finalRedirect()

     validateMessage: ->
         return if @state.isDraft
@@ -356,7 +370,7 @@
         return if @props.isSaving
         if (validate = @validateMessage())
             LayoutActionCreator.alertError t 'compose error no ' + validate[1]
-            success() if _.isFunction success
+            success(null, @state) if _.isFunction success
             return

         @props.isSaving = true
@@ -364,12 +378,10 @@
             if error? or not message?
                 if @state.isDraft
                     msgKo = t "message action draft ko"
-                else
-                    msgKo = t "message action sent ko"
-                LayoutActionCreator.alertError "#{msgKo} #{error}"
+                    LayoutActionCreator.alertError "#{msgKo} #{error}"

                 @props.isSaving = false
-                success() if _.isFunction success
+                success(error, message) if _.isFunction success
                 return

             unless @state.id
@@ -382,22 +394,9 @@
                     if _.isUndefined @state[key]
                         @state[key] = message[key]

-            # TODO : move this into render
-            unless @state.isDraft
-                # Display confirmation message
-                # for no-draft email
-                msgOk = t "message action sent ok"
-                LayoutActionCreator.notify msgOk, autoclose: true
-
-                # reload conversation to update its length
-                if (cid = message.conversationID)
-                    MessageActionCreator.fetchConversation cid
-
-                @finalRedirect()
-
             @props.lastUpdate = @state.date
             @props.isSaving = false
-            success() if _.isFunction success
+            success(error, message) if _.isFunction success

     deleteDraft: (event) ->
         event.preventDefault() if event


Fix Modal

Fix Modal

--- app/components/vnc-viewer.coffee
+++ app/components/vnc-viewer.coffee
@@ -60,6 +60,7 @@
       $.get dynamicUrl

     dynamicShutdown: ->
+      @set "showAsModal", false
       file_id = @get "file.id"
       shutdownUrl = [ENV.APP.API_BASE, ENV.endpoints.dynamicShutdown, file_id].join '/'
       $.get shutdownUrl


Rename greenmine namespace to gm on coffee/utils.coffee

--- app/coffee/utils.coffee
+++ app/coffee/utils.coffee
@@ -1,4 +1,4 @@
-utils = @greenmine.utils = {}
+utils = @gm.utils = {}

 utils.pointIdToOrder = (points, roles) ->
     return (us_points) ->


Remove Coffee stuff (moved to coffee branch)

--- suncalc.coffee
+++ suncalc.coffee
@@ -1,98 +0,0 @@
-do ->
-	"use strict"
-
-	SunCalc = exports ? @SunCalc = {}
-
-	m = Math
-	{PI, sin, cos, tan, asin, acos, atan2, round} = m
-
-	rad     = PI / 180
-	msInDay = 1000 * 60 * 60 * 24
-
-	J1970   = 2440588
-	J2000   = 2451545
-	M0      = rad * 357.5291
-	M1      = rad * 0.98560028
-	J0      = 0.0009
-	J1      = 0.0053
-	J2      = -0.0069
-	C1      = rad * 1.9148
-	C2      = rad * 0.0200
-	C3      = rad * 0.0003
-	P       = rad * 102.9372
-	e       = rad * 23.45
-	th0     = rad * 280.1600
-	th1     = rad * 360.9856235
-
-	dateToJulianDate      = (date) -> date.valueOf() / msInDay - 0.5 + J1970
-	julianDateToDate      = (j)    -> new Date((j + 0.5 - J1970) * msInDay)
-
-	getSolarMeanAnomaly   = (Js)   -> M0 + M1 * (Js - J2000)
-	getEquationOfCenter   = (M)    -> C1 * sin(M) + C2 * sin(2 * M) + C3 * sin(3 * M)
-	getEclipticLongitude  = (M, C) -> M + P + C + PI
-	getSunDeclination     = (Ls)   -> asin(sin(Ls) * sin(e))
-
-	getJulianCycle        = (J, lw)     -> round(J - J2000 - J0 - lw / (2 * PI))
-	getApproxSolarTransit = (Ht, lw, n) -> J2000 + J0 + (Ht + lw) / (2 * PI) + n
-	getSolarTransit       = (Js, M, Ls) -> Js + (J1 * sin(M)) + (J2 * sin(2 * Ls))
-	getHourAngle          = (h, phi, d) -> acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d)))
-
-	getRightAscension     = (Ls)        -> atan2(sin(Ls) * cos(e), cos(Ls))
-	getSiderealTime       = (J, lw)     -> th0 + th1 * (J - J2000) - lw
-	getAzimuth            = (h, phi, d) -> atan2(sin(h), cos(h) * sin(phi) - tan(d) * cos(phi))
-	getAltitude           = (h, phi, d) -> asin(sin(phi) * sin(d) + cos(phi) * cos(d) * cos(h))
-
-
-	times = [[-0.83, 'sunrise',       'sunset']
-			 [ -0.3, 'sunriseEnd',    'sunsetStart']
-			 [   -6, 'dawn',          'dusk']
-			 [  -12, 'nauticalDawn',  'nauticalDusk']
-			 [  -18, 'night',         'nightEnd']
-			 [    6, 'goldenHourEnd', 'goldenHour']]
-
-	SunCalc.addTime = (angle, morningName, eveningName) ->
-		times.push [angle, morningName, eveningName]
-
-	SunCalc.getTimes = (date, lat, lng) ->
-		lw  = rad * -lng
-		phi = rad * lat
-		J   = dateToJulianDate      date
-		n   = getJulianCycle        J, lw
-		Js  = getApproxSolarTransit 0, lw, n
-		M   = getSolarMeanAnomaly   Js
-		C   = getEquationOfCenter   M
-		Ls  = getEclipticLongitude  M, C
-		d   = getSunDeclination     Ls
-		Jnoon = getSolarTransit     Js, M, Ls
-
-		getSet = (h) ->
-			w = getHourAngle          h, phi, d
-			a = getApproxSolarTransit w, lw, n
-			getSolarTransit a, M, Ls
-
-		result = {solarNoon: julianDateToDate Jnoon}
-
-		for [angle, morningName, eveningName] in times
-			Jset  = getSet(rad * angle)
-			Jrise = Jnoon - (Jset - Jnoon)
-			result[eveningName] = julianDateToDate Jset
-			result[morningName] = julianDateToDate Jrise
-
-		result
-
-	SunCalc.getSunPosition = (date, lat, lng) ->
-		lw  = rad * -lng
-		phi = rad * lat
-		J   = dateToJulianDate     date
-		M   = getSolarMeanAnomaly  J
-		C   = getEquationOfCenter  M
-		Ls  = getEclipticLongitude M, C
-		d   = getSunDeclination    Ls
-		a   = getRightAscension    Ls
-		th  = getSiderealTime      J, lw
-		h   = th - a
-
-		azimuth  = getAzimuth  h, phi, d
-		altitude = getAltitude h, phi, d
-
-		{azimuth, altitude}


Fixed HTML Compiler

Fixed HTML Compiler

--- src/compilers.coffee
+++ src/compilers.coffee
@@ -11,7 +11,7 @@
   coffee.compile body, bare: true, header: false

 exports.html = (body, filename) ->
-  "module.exports = #{JSON.stringify}"
+  "module.exports = #{JSON.stringify body}"

 require.extensions['.html'] = (module, filename) ->
   body = fs.readFileSync filename, 'utf8'


Add hero setup for Escort Duty

Add hero setup for Escort Duty

--- app/lib/LevelSetupManager.coffee
+++ app/lib/LevelSetupManager.coffee
@@ -63,6 +63,7 @@
       @waitingToLoadModals = true

   loadModals: ->
+
     # build modals and prevent them from disappearing.
     if @level.get('slug') is 'zero-sum'
       sorcerer = '52fd1524c7e6cf99160e7bc9'
@@ -72,6 +73,35 @@
       @onInventoryModalPlayClicked()
       return
     # TODO: Remove post-KR
+
+
+
+    if @level.get('slug') is 'escort-duty'
+      potionmaster = '52fd1524c7e6cf99160e7bc9'
+      if @session.get('creator') is '532dbc73a622924444b68ed9'  # Wizard Dude gets his own avatar
+        potionmaster = '53e126a4e06b897606d38bef'
+      @session.set 'heroConfig', {"thangType":potionmaster,"inventory":{
+          "eyes": "546941fda2b1f53ce794441d",
+          "feet": "546d4d8e9df4a17d0d449acd",
+          "programming-book": "557871261ff17fef5abee3ee",
+          "head": "546d4ca19df4a17d0d449abf",
+          "torso": "546d4a549df4a17d0d449a97",
+          "left-ring": "5441c35c4e9aeb727cc9711d",
+          "minion": "54eb5d1649fa2d5c905ddf52",
+          "neck": "54693363a2b1f53ce79443d1",
+          "wrists": "54693830a2b1f53ce79443f1",
+          "left-hand": "546376ea3839c6e02811d320",
+          "right-hand": "54eab92b2b7506e891ca720a",
+          "waist": "54694af7a2b1f53ce7944441",
+          "right-ring": "54692d2aa2b1f53ce794438f",
+          "pet": "5744e3683af6bf590cd27371"
+
+          }} #pet is cougar
+          # "pet": "57586f0a22179b2800efda37"  This is Baby Griffin
+
+      @onInventoryModalPlayClicked()
+      return
+
     if @level.get('slug') is 'the-gauntlet-kr'
       lightseeker = '583d2cca6ffa3e65d170f29f'
       @session.set 'heroConfig', {"thangType":lightseeker,"inventory":{
@@ -138,7 +168,7 @@
           "left-ring": "54692d2aa2b1f53ce794438f"
         }
       }
-
+
     if @level.get('slug') is 'tesla-tesoro'
       #Need to update this for my level
       assassin = '566a2202e132c81f00f38c81'
@@ -146,7 +176,7 @@
         "thangType": assassin
         "inventory":{
           "eyes": "546941fda2b1f53ce794441d",
-          "feet": "546d4d8e9df4a17d0d449acd",
+          "feet": "546d4d8e9df4a17d0d449acd",
           "minion": "54eb5d1649fa2d5c905ddf52",
           "neck": "54693363a2b1f53ce79443d1",
           "wrists": "54693830a2b1f53ce79443f1",


default import io data

default import io data

--- client/src/app/redWire.coffee
+++ client/src/app/redWire.coffee
@@ -698,7 +698,7 @@
     # OPT: do this outside of stepLoop()
     preparedInputIoData = {}
     for circuitMeta in options.circuitMetas
-      preparedInputIoData[circuitMeta.id] = options.inputIoData[circuitMeta.id] || options.inputIoData.global
+      preparedInputIoData[circuitMeta.id] = options.inputIoData[circuitMeta.id] || options.inputIoData.global || {}
       for ioName of options.io
         if ioName not of preparedInputIoData[circuitMeta.id]
           preparedInputIoData[circuitMeta.id][ioName] = options.inputIoData.global[ioName]


stack-editor: redirect to ide if being edited template is deleted

--- client/stack-editor/lib/index.coffee
+++ client/stack-editor/lib/index.coffee
@@ -42,6 +42,13 @@
       computeController.fetchStackTemplate stackTemplateId, (err, stackTemplate) =>
         return showError err  if err
         @showView stackTemplate
+
+        # If selected template is deleted, then redirect them to ide.
+        # TODO: show an information modal to the user if he/she is admin. ~Umut
+        stackTemplate.on 'deleteInstance', =>
+          return  unless @selectedEditor.getData()._id isnt stackTemplate._id
+          @removeEditor stackTemplate._id
+          kd.singletons.router.handleRoute '/IDE'
     else
       @showView()


tidy up home

tidy up home

--- client/views/home/home.coffee
+++ client/views/home/home.coffee
@@ -1,5 +1,7 @@
 Template.home.rendered = () ->
 	w = new WOW().init()
+
+	# TODO: End after home destroyed

 	# make sure div stays full width/height on resize
 	# global vars
@@ -16,13 +18,8 @@
 	    width: $(window).width()
 	    height: $(window).height()

-
-	# s = new skrollr.init({smoothScrolling: true})
-	# s.refresh()
-
-	if  (/Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i).test(navigator.userAgent or navigator.vendor or window.opera)
-		$('#intro').addClass('mobile')
-	else
+	#Skroll doesn't work so well on mobile imo
+	unless Utils.isMobile
 		options =
 			forceHeight: false
 			smoothScrolling: false


Fix race condition: session destruction when moving between levels

--- app/lib/LevelBus.coffee
+++ app/lib/LevelBus.coffee
@@ -164,7 +164,7 @@
   onScriptEnded: (e) ->
     return unless @onPoint()
     state = @session.get('state')
-    scripts = state.scripts
+    return unless scripts = state.scripts
     scripts.ended ?= {}
     return if scripts.ended[e.scriptID]?
     index = _.keys(scripts.ended).length + 1


fix case of "E-Mail" and make it consistent with other occurrences

--- app/locale/de-DE.coffee
+++ app/locale/de-DE.coffee
@@ -1273,7 +1273,7 @@
     concepts: "Konzepten"
     play_time: "Spielzeit:"
     completed: "Abgeschlossen:"
-    enter_emails: "E-mail-Adressen durch Komma oder Zeilenumbruch trennen"
+    enter_emails: "E-Mail-Adressen durch Komma oder Zeilenumbruch trennen"
     send_invites: "Schüler einladen"
     number_programming_students: "Anzahl der Schüler"
     number_total_students: "Gesamtzahl der Schüler in Schule/Bezirk"


Add new translations and fix a typo

Add new translations and fix a typo

--- app/locale/mk-MK.coffee
+++ app/locale/mk-MK.coffee
@@ -514,7 +514,7 @@
     time_current: "Сега:"
     time_total: "Максимум:"
     time_goto: "Оди до:"
-    non_user_code_problem_title: "Неможам да го вчитам нивото"
+    non_user_code_problem_title: "Не можам да го вчитам нивото"
     infinite_loop_title: "Забележана е бесконечна јамка"
     infinite_loop_description: "Првичниот код за изградба на светот никогаш не завршил со своето извршување. Веројатно е, или премногу спор, или соджи бесконечна јамка. Или можеби има грешка. Можеш да се обидеш да го извршиш овој код повторно или да го ресетираш кодот во првичната состојба. Ако тоа не го поправи проблемот, те молиме да не известиш."
 #    check_dev_console: "You can also open the developer console to see what might be going wrong."
@@ -526,7 +526,7 @@
     tip_scrub_shortcut: "Ctrl+[ и Ctrl+] премотуваат назад и напред." # {change}
     tip_guide_exists: "Кликни на водичот, во менито (на врвот на страната), за корисни информации."
     tip_open_source: "CodeCombat е 100% open source!"
-#    tip_tell_friends: "Enjoying CodeCombat? Tell your friends about us!"
+    tip_tell_friends: "Уживаш во CodeCombat? Кажи им на пријателите за нас!"
     tip_beta_launch: "CodeCombat ја пушти својата бета верзија во октомври, 2013."
     tip_think_solution: "Мисли на решението, не на проблемот."
     tip_theory_practice: "Теоретски, не постои разлика помеѓу теорија и пракса. Но, во пракса, постои. - Yogi Berra"
@@ -571,11 +571,11 @@
     tip_free_your_mind: "Мораш да го оставиш сето тоа, Нео. Стравот, сомнежот, и невербата. Ослободи го твојот ум. - Morpheus"
     tip_strong_opponents: "Дури и најсилниот противник има некоја слаба точка. - Itachi Uchiha"
 #    tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen."
-#    tip_solve_then_write: "First, solve the problem. Then, write the code. - John Johnson"
-#    tip_compiler_ignores_comments: "Sometimes I think that the compiler ignores my comments."
-#    tip_understand_recursion: "The only way to understand recursion is to understand recursion."
+    tip_solve_then_write: "Прво, реши го проблемот. Потоа, напиши го кодот. - John Johnson"
+    tip_compiler_ignores_comments: "Понекогаш мислам дека компајлерот ги игнорира моите коментари."
+    tip_understand_recursion: "Единствениот начин да разбереш рекурзија е да разбереш рекурзија."
 #    tip_life_and_polymorphism: "Open Source is like a totally polymorphic heterogeneous structure: All types are welcome."
-#    tip_mistakes_proof_of_trying: "Mistakes in your code are just proof that you are trying."
+    tip_mistakes_proof_of_trying: "Грешките во твојот код се само доказ дека се обидуваш."
 #    tip_adding_orgres: "Rounding up ogres."
 #    tip_sharpening_swords: "Sharpening the swords."
 #    tip_ratatouille: "You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille"
@@ -585,11 +585,11 @@
 #    tip_self_taught: "I taught myself 90% of what I've learned. And that's normal! - Hank Green"
 #    tip_luna_lovegood: "Don't worry, you're just as sane as I am. - Luna Lovegood"
 #    tip_good_idea: "The best way to have a good idea is to have a lot of ideas. - Linus Pauling"
-#    tip_programming_not_about_computers: "Computer Science is no more about computers than astronomy is about telescopes. - Edsger Dijkstra"
+    tip_programming_not_about_computers: "Компјутерската Наука е исто толку за компјутери колку што астрономијата е за телескопи. - Edsger Dijkstra"
 #    tip_mulan: "Believe you can, then you will. - Mulan"
 #    project_complete: "Project Complete!"
 #    share_this_project: "Share this project with friends or family:"
-#    ready_to_share: "Ready to publish your project?"
+    ready_to_share: "Подготвен/а за објавување на проектот?"
 #    click_publish: "Click \"Publish\" to make it appear in the class gallery, then check out what your classmates built! You can come back and continue to work on this project. Any further changes will automatically be saved and shared with your classmates."
 #    already_published_prefix: "Your changes have been published to the class gallery."
 #    already_published_suffix: "Keep experimenting and making this project even better, or see what the rest of your class has built! Your changes will automatically be saved and shared with your classmates."


Update nl-NL.coffee (#3713)

Update nl-NL.coffee (#3713)

Some UX updates and some minor spelling checks.

--- app/locale/nl-NL.coffee
+++ app/locale/nl-NL.coffee
@@ -5,8 +5,8 @@
     no_mobile: "CodeCombat is niet gemaakt voor mobiele apparaten en werkt misschien niet!" # Warning that shows up on mobile devices
     play: "Speel" # The big play button that opens up the campaign view.
     play_campaign_version: "Speel de Verhaallijn" # Shows up under big play button if you only play /courses
-    old_browser: "Oh oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari
-    old_browser_suffix: "Je kan toch proberen, maar het zal waarschijnlijk niet werken!"
+    old_browser: "uh-oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari
+    old_browser_suffix: "Je kan alsnog proberen, maar het zal waarschijnlijk niet werken!"
     ipad_browser: "Slecht nieuws: CodeCombat draait niet in je browser op de iPad. Goed nieuws: onze iPad-app wordt op het moment beoordeeld door Apple."
     campaign: "Verhaallijn"
     for_beginners: "Voor Beginners"
@@ -24,25 +24,25 @@
     im_a_teacher: "Ik ben een leraar"
     im_a_student: "Ik ben een leerling"
     learn_more: "Lees verder"
-    classroom_in_a_box: "Een kant-en-klare digitale klas voor programmeerlessen."
+    classroom_in_a_box: "Kant-en-klare programmeerlessen."
     codecombat_is: "CodeCombat is een platform waarmee leerlingen leren programmeren door het spelen van een spel." # {change}
     our_courses: "Onze lessen zijn specifiek ontwikkeld voor een klasomgeving, zelfs voor leraren zonder programmeerervaring." # {change}
-    top_screenshots_hint: "Leerlingen schrijven code en zien direct het resultaat van de verandering."
+    top_screenshots_hint: "Leerlingen schrijven code en zien direct resultaat."
     designed_with: "Gemaakt voor leraren"
     real_code: "Echte, getypte code"
     from_the_first_level: "vanaf het eerste level"
-    getting_students: "Leerlingen zo snel mogelijk echte code laten schrijven is noodzakelijk voor het leren van programmeer syntax en correcte structuur."
+    getting_students: "Doordat leerlingen code schrijven in 'echte programmeertaal', leren ze niet alleen hoe computers denken, maar kunnen ze het ook echt toepassen."
     educator_resources: "Lesbrieven voor docenten"
     course_guides: "en ondersteuningsmateriaal"
     teaching_computer_science: "Je hebt geen informatica diploma nodig om te kunnen programmeren, wij verschaffen de materialen waarmee elke docent programmeerles kan geven."
-    accessible_to: "Bereikbaar voor"
+    accessible_to: "Toegankelijk voor"
     everyone: "iedereen"
     democratizing: "Programmeerles toegankelijk maken is onze filosofie. Iedereen moet de kans krijgen om te leren programmeren."
     forgot_learning: "Volgens mij hadden ze niet meer door dat ze eigenlijk bezig waren met leren."
     wanted_to_do: " Ik wilde altijd al leren programmeren, maar op school was hier nooit aandacht voor."
     why_games: "Waarom is spelenderwijs leren belangrijk?"
     games_reward: "Games vergroten de productiviteit."
-    encourage: "Gaming is een middel dat interactie, nieuwschierigheid, en trial-and-error aanmoedigt. Een goed spel daagt de speler uit zijn vaardigheden te perfectioneren, wat hetzelfde noodzakelijke proces is waar leerlingen doorheen gaan wanneer zij iets leren."
+    encourage: "Iedereen wordt geboren als klein onderzoekertje dat de wereld ontdekt door vallen en opstaan. Deze intrinsieke motivatie om te leren ziet men ook terug in een spelomgeving. Voor de speler wordt 'leren' een middel om het spel te winnen, in plaats van een doel op zich."
     excel: "Games helpen bij de"
     struggle: "productiviteit-strijd"
     kind_of_struggle: "het soort worsteling dat uitmondt in een leerproces dat uitdagend is en "
@@ -58,7 +58,7 @@
     great_game: "Een goed spel is meer dan alleen medailles en prestaties - het gaat om een reis, nauwkeurig ontworpen puzzels, en de mogelijkheid om uitdagingen vol zelfvertrouwen aan te pakken."
     agency: "CodeCombat is een game die de mogelijkheid en het zelfvertrouwen geeft om met echte code te werken, wat zowel beginners als gevorderde helpt bij het schrijven van goede, valide code."
     request_demo_title: "Laat je leerlingen vandaag nog starten!"
-    request_demo_subtitle: "Vraag een demo aan en start binnen een uur met programmeerlessen."
+    request_demo_subtitle: "Vraag een demo aan en start met programmeerlessen."
     get_started_title: "Maak vandaag nog een klas aan!"
     get_started_subtitle: "Maak een klas aan, voeg je leerlingen toe, en monitor hun vooruitgang."
     request_demo: "Vraag een demo aan"
@@ -74,7 +74,7 @@
     coming_soon: "Binnenkort beschikbaar!"
     courses_available_in: "Lessen zijn beschikbaar in JavaScript, Python, en Java (Java is binnenkort beschikbaar!)"
     boast: "Uitdagende raadsels die zowel gamers als fanatieke programmeurs weten te prikkelen."
-    winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kind-vriendelijk en oprecht aangenaam onderwijs."
+    winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kindvriendelijk en oprecht aangenaam onderwijs."
     run_class: "Alles wat je nodig hebt om vandaag nog programmeerlessen in jouw klas te geven, geen voorkennis vereist."
     teachers: "Docenten!"
     teachers_and_educators: "Docenten & Mentoren"
@@ -92,7 +92,7 @@
     check_out_wiki: "Bekijk onze nieuwe leraren Wiki"
     want_coco: "Wil je CodeCombat op jouw school?"
     form_select_role: "Selecteer je rol"
-    form_select_range: "Selecteer klassengrootte"
+    form_select_range: "Selecteer klasomvang"

   nav:
     play: "Levels" # The top nav bar entry where players choose which levels to play
@@ -221,19 +221,19 @@
     "/": "gedeeld door"
     "+": "plus"
     "-": "min"
-#    "+=": "add and assign"
-#    "-=": "subtract and assign"
+    "+=": "tel op en wijs toe"
+    "-=": "trek af en wijs toe"
     True: "Waar"
     true: "waar"
     False: "onwaar"
     false: "onwaar"
     undefined: "ongedefinieerd"
-#    null: "null"
-#    nil: "nil"
+    null: "nul"
+    nil: "nihil"
     None: "Geen"

   share_progress_modal:
-    blurb: "Je gaat snel vooruit! Vertel aan je ouders hoeveel je geleerd hebt van CodeCombat."
+    blurb: "Je gaat snel vooruit! Vertel je ouders hoeveel je geleerd hebt van CodeCombat."
     email_invalid: "E-mailadres klopt niet."
     form_blurb: "Vul het e-mailadres van je ouders hieronder in en we zullen het ze laten zien!"
     form_label: "E-mailadres"
@@ -472,19 +472,19 @@
     tip_reticulating: "Paden aan het verknopen."
     tip_harry: "Je bent een tovenaar, "
     tip_great_responsibility: "Met een groots talent voor programmeren komt een grootse debug verantwoordelijkheid."
-    tip_munchkin: "Als je je groentjes niet opeet zal een munchkin je ontvoeren terwijl je slaapt."
+    tip_munchkin: "Als je je groenten niet opeet zal een munchkin je ontvoeren terwijl je slaapt."
     tip_binary: "Er zijn 10 soorten mensen in de wereld: Mensen die binair kunnen tellen en mensen die dat niet kunnen."
     tip_commitment_yoda: "Een programmeur moet de grootste inzet hebben, een meest serieuze geest. ~ Yoda"
     tip_no_try: "Doe het. Of doe het niet. Je kunt niet proberen. - Yoda"
     tip_patience: "Geduld moet je hebben, jonge Padawan. - Yoda"
     tip_documented_bug: "Een gedocumenteerde fout is geen fout; het is deel van het programma."
-    tip_impossible: "Het lijkt altijd onmogelijk tot het gedaan wordt. - Nelson Mandela"
+    tip_impossible: "Het lijkt altijd onmogelijk totdat iemand het doet. - Nelson Mandela"
     tip_talk_is_cheap: "Je kunt het goed uitleggen, maar toon me de code. - Linus Torvalds"
     tip_first_language: "Het ergste dat je kan leren is je eerste programmeertaal. - Alan Kay"
     tip_hardware_problem: "Q: Hoeveel programmeurs heb je nodig om een lampje te vervangen?  A: Nul, het is een a hardware probleem."
     tip_hofstadters_law: "De Wet van Hofstadter: Het duurt altijd langer dan je verwacht, zelfs wanneer je rekening houdt met de Wet van Hofstadter."
     tip_premature_optimization: "vroegtijdig optimaliseren is de wortel van al het kwaad. - Donald Knuth"
-    tip_brute_force: "Wanneer je twijfelt, gebruik brute force. - Ken Thompson"
+    tip_brute_force: "Wanneer je twijfelt, gebruik dan brute force. - Ken Thompson"
     tip_extrapolation: "Er zijn twee soorten mensen: Zij die iets kunnen afleiden van onvolledige gegevens..."
     tip_superpower: "Van alle dingen komt programmeren het dichtst in de buurt van een superkracht."
     tip_control_destiny: "In echte open source, hebt je het recht om je eigen toekomst te bepalen. - Linus Torvalds"
@@ -820,8 +820,8 @@
   teachers_quote:
     name: "Demo Formulier"
     title: "Demo aanvragen"
-    subtitle: "Haal CodeCombat jouw klaslokaal of club!"
-    email_exists: "Er bestaat al een gebruiker met dit email adres."
+    subtitle: "Gebruik CodeCombat voor jouw klas of programmeerclub!"
+    email_exists: "Er bestaat al een gebruiker met dit emailadres."
     phone_number: "Telefoonnummer"
     phone_number_help: "Waarop kunnen we je bereiken tijdens kantooruren?"
     primary_role_label: "Uw primaire rol"


Fix spawning Hero kind ThangTypes in game-dev levels

--- app/models/Level.coffee
+++ app/models/Level.coffee
@@ -33,7 +33,7 @@
     for tt in supermodel.getModels ThangType
       if tmap[tt.get('original')] or
         (tt.get('kind') isnt 'Hero' and tt.get('kind')? and tt.get('components') and not tt.notInLevel) or
-        (tt.get('kind') is 'Hero' and ((@get('type', true) in ['course', 'course-ladder']) or tt.get('original') in sessionHeroes))
+        (tt.get('kind') is 'Hero' and ((@get('type', true) in ['course', 'course-ladder', 'game-dev']) or tt.get('original') in sessionHeroes))
           o.thangTypes.push (original: tt.get('original'), name: tt.get('name'), components: $.extend(true, [], tt.get('components')))
     @sortThangComponents o.thangTypes, o.levelComponents, 'ThangType'
     @fillInDefaultComponentConfiguration o.thangTypes, o.levelComponents


Change darts-ui size to fit screen

Change darts-ui size to fit screen

--- app/scripts/game.coffee
+++ app/scripts/game.coffee
@@ -23,7 +23,8 @@
         bodyHeight = $('body').height()
         headerHeight = $('.header').height() + 11
         footerHeight = $('.footer').height() + 11
-        canvasHeight = bodyHeight - headerHeight - footerHeight - 20
-        $('#darts-ui').width(canvasHeight).height(canvasHeight)
+        canvasHeight = bodyHeight - headerHeight - footerHeight
+        length = (Math.min $('body').width(), canvasHeight) - 20
+        $('#darts-ui').width(length).height(length)

 window.Game = Game


Improve empty state of subject page

Improve empty state of subject page

- Notifies volunteers earlier if there is no subject default board setup
- Closes  #1344

--- app/subjects/index.cjsx
+++ app/subjects/index.cjsx
@@ -10,7 +10,7 @@
 projectSection = require '../talk/lib/project-section'
 parseSection = require '../talk/lib/parse-section'
 QuickSubjectCommentForm= require '../talk/quick-subject-comment-form'
-{Navigation} = require 'react-router'
+{Navigation, Link} = require 'react-router'
 {Markdown} = require 'markdownz'
 alert = require '../lib/alert'
 SignInPrompt = require '../partials/sign-in-prompt'
@@ -57,6 +57,10 @@
       [owner, name] = project.slug.split('/')
       @transitionTo('project-talk-discussion', {owner: owner, name: name, board: discussion.board_id, discussion: discussion.id})

+  linkToClassifier: (text) ->
+    [owner, name] = @props.project.slug.split('/')
+    <Link to="project-classify" params={{owner, name}}>{text}</Link>
+
   render: ->
     {subject} = @state

@@ -78,40 +82,48 @@
           }</PromiseRenderer>

           {if @props.user
-            {# these wrapping promises ensure that there are boards for a project}
-            {# & could be removed if a default board is put in place}
             {# TODO remove subject.get('project'), replace with params but browser freezes on get to projects with slug}
-            <PromiseRenderer promise={subject.get('project')}>{(project) =>
-              <PromiseRenderer promise={talkClient.type('boards').get(section: projectSection(project))}>{(boards) =>
-                if boards?.length
-

-

-

-

-
 @setState({tab: 0})}>
-                            Add a note about this subject
-

+            project = subject.get('project')
+            boards = project.then (project) -> talkClient.type('boards').get(section: projectSection(project), subject_default: false)
+            subjectDefaultBoard = project.then (project) -> talkClient.type('boards').get(section: projectSection(project), subject_default: true)

-
 @setState({tab: 1})}>
-                            or Start a new discussion
-

+            <PromiseRenderer promise={Promise.all([boards, subjectDefaultBoard])}>{([boards, subjectDefaultBoard]) =>
+              defaultExists = subjectDefaultBoard.length
+              if boards.length or defaultExists
+

+

+

+

+
 @setState({tab: 0})}>
+                          Add a note about this subject
+

+
+
 @setState({tab: 1})}>
+                          Start a new discussion

-

-                      {if @state.tab is 0
+

+

+                    {if @state.tab is 0
+                      if defaultExists
                         <QuickSubjectCommentForm subject={subject} user={@props.user} />
-                       else if @state.tab is 1
-                        <NewDiscussionForm
-                          user={@props.user}
-                          subject={subject}
-                          onCreateDiscussion={@onCreateDiscussion} />
-                          }
-

+                      else
+

+                          There is no default board for subject comments setup yet, Please{' '}
+                          <button className="link-style" onClick={=> @setState(tab: 1)}>start a new discussion</button>{' '}
+                          or {@linkToClassifier('return to classifying')}
+

+                     else if @state.tab is 1
+                      <NewDiscussionForm
+                        user={@props.user}
+                        subject={subject}
+                        onCreateDiscussion={@onCreateDiscussion} />
+                        }

-                else
-
There are no discussion boards setup for this project yet. Check back soon!

-              }</PromiseRenderer>
+

+              else
+
There are no discussion boards setup for this project yet. Check back soon!

             }</PromiseRenderer>
           else

Please <button className="link-style" type="button" onClick={promptToSignIn}>sign in</button> to contribute to subject discussions
}


Fix naming of SidekiqMonitor.QueuesTable

--- app/assets/javascripts/sidekiq/monitor/queue_jobs_table.js.coffee
+++ app/assets/javascripts/sidekiq/monitor/queue_jobs_table.js.coffee
@@ -1,4 +1,4 @@
-class SidekiqMonitor.QueuesTable extends SidekiqMonitor.AbstractJobsTable
+class SidekiqMonitor.QueueJobsTable extends SidekiqMonitor.AbstractJobsTable

   initialize: =>
     options =
@@ -91,4 +91,4 @@
     @reload_table()

 $ ->
-  new SidekiqMonitor.QueuesTable
+  new SidekiqMonitor.QueueJobsTable