Remove test from watch task since its misleading, due to caching the tests will never fail after having succeeded

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -58,7 +58,7 @@
             verbose: true

 gulp.task 'watch', ['build:distribution'], ->
-    gulp.watch 'src/**/*.coffee', ['test', 'build:distribution']
+    gulp.watch 'src/**/*.coffee', ['build:distribution']

 gulp.task 'clean', (cb) ->
     del ['dist', 'gen-js'], cb


Fix something

Fix something

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -5,9 +5,10 @@
 notify     = require 'gulp-notify'
 sourcemaps = require 'gulp-sourcemaps'
 webserver  = require 'gulp-webserver'
+exec       = require 'gulp-exec'
 _          = require 'underscore'

-class IOs
+class FileIOs
     constructor: (data) ->
         unless data instanceof Array
             throw new Error 'constructor reqire Array.'
@@ -34,7 +35,7 @@
         [@src(name), @dest(name)]

-ios = new IOs [
+fileIOs = new FileIOs [
     {
         name: 'app'
         src: ['./src/**/*.coffee']
@@ -53,6 +54,8 @@
     }
 ]

+{host, port} = {host:'localhost', port:8000}
+
 coffeePipeline = (src, dest) ->
     ->
         gulp.src src
@@ -62,23 +65,27 @@
             .pipe sourcemaps.write()
             .pipe gulp.dest dest

-gulp.task 'coffee-app', coffeePipeline.apply(null, ios.of 'app')
+gulp.task 'coffee-app', coffeePipeline.apply(null, fileIOs.of 'app')

-gulp.task 'coffee-spec', coffeePipeline.apply(null, ios.of 'spec')
+gulp.task 'coffee-spec', coffeePipeline.apply(null, fileIOs.of 'spec')

 gulp.task 'sass', ->
-    gulp.src(ios.src 'sass')
+    gulp.src(fileIOs.src 'sass')
         .pipe plumber(errorHandler: notify.onError '<%= error.message %>')
         .pipe sass()
-        .pipe gulp.dest(ios.dest 'sass')
+        .pipe gulp.dest(fileIOs.dest 'sass')

 gulp.task 'build', ['coffee-app','coffee-spec', 'sass']

 gulp.task 'webserver', ->
     gulp.src('./')
-        .pipe webserver {livereload: true}
+        .pipe webserver {
+            host
+            port
+            livereload: true
+        }

 gulp.task 'default', ['build']

 gulp.task 'dev', ['build','webserver'], ->
-    gulp.watch ios.src(), ['build']
+    gulp.watch fileIOs.src(), ['build']


add browser-sync support

add browser-sync support

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -3,26 +3,33 @@
 plumber = require "gulp-plumber"
 jade    = require "gulp-jade"
 uglify  = require "gulp-uglify"
+browser = require "browser-sync"
+
+gulp.task 'server', () ->
+  browser({server:{baseDir:"app/"}})

 gulp.task 'compile-coffee', () ->
   gulp.src('app/source/coffee/*.coffee')
     .pipe(plumber())
     .pipe(coffee())
     .pipe(gulp.dest('app/source/js/'))
+    .pipe(browser.reload({stream:true}))

 gulp.task 'compile-jade', () ->
   gulp.src('app/source/jade/*.jade')
     .pipe(plumber())
     .pipe(jade({pretty:true}))
     .pipe(gulp.dest('app/'))
+    .pipe(browser.reload({stream:true}))

 gulp.task 'js', () ->
   gulp.src('app/source/js/*.js')
     .pipe(plumber())
     .pipe(uglify())
     .pipe(gulp.dest('app/assets/js'))
+    .pipe(browser.reload({stream:true}))

-gulp.task 'default', () ->
+gulp.task 'default', ['server'], () ->
   gulp.watch(["app/source/coffee/*.coffee"], ["compile-coffee"])
   gulp.watch(["app/source/jade/*.jade"], ["compile-jade"])
   gulp.watch(["app/source/js/*.js"], ["js"])


Deploy at default task

Deploy at default task

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -98,4 +98,4 @@
 	watch paths.server, -> gulp.start 'deploy'

 gulp.task 'build', ['assets', 'jade', 'coffee', 'sass']
-gulp.task 'default', ['build', 'browser-sync', 'watch']+gulp.task 'default', ['build', 'deploy', 'browser-sync', 'watch']


remove task make:samples and make:test

remove task make:samples and make:test

remove task make:samples and make:test, which exists only for browserify by twoside.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -83,41 +83,7 @@

 merge = (files) -> files = files.split(' '); path = files[0]; (for file in files.splice(1) then path+file).join(' ')

-client = folders_destClient
-
-# remove twoside temporalily. add them when distributing.
-#destLib = folders_dest+'lib/'
-#destLibClient = destLib+'client/'
-#files_taijiview = merge('nodes/ nodes index')+' '+'parser lex taijiview'
-#files_taijiview = for item in files_taijiview.split(' ') then destLib+item+'.js'
-#task 'transform:taijiview', (cb) ->
-#  if not distributing then return src(files_taijiview).pipelog(twoside(destLib, 'taijiview')).to(destLib)
-#  src(files_taijiview)
-#  .pipelog(twoside(folders_dest, 'taijiview', {only_wrap_for_browser:true})).to(destLibClient)
-#  .pipe(concat("taijiview-package.js")).pipe(size()).to(destLibClient)
-#  #minify
-#  .pipe(closureCompiler()).pipe(rename(suffix: "-min")).pipe(size()).to(destLibClient)
-
-task 'make:samples', (cb) -> # twoside, concat
-  # twoside and concat for samples
-  files = 'some .js examples filename for taijiview'
-  files = for name in files.split(' ') then folders_dest+'examples/'+name+'.js'
-#  console.log files.join(' ')
-  stream = src(files).pipelog(twoside(folders_dest+'examples', 'taijiview/examples')) #, {only_wrap_for_browser:true}
-  if not distributing then return stream.to(folders_dest+'examples')
-  stream.pipe(concat('examples-concat.js')).pipe(size()) # when debuggin, dont concat
-  stream.to(folders_dest+'examples')
-
-task 'make:test', (cb) -> # twoside, concat, minify
-  # twoside and concat for test/karma
-  stream = src(folders_dest+'test/karma/**/*.js')
-  .pipelog(twoside(folders_dest+'test/karma', 'peasy/karma', {only_wrap_for_browser:true}))
-  if not distributing then return  stream.to(folders_dest+'test/karma')
-  stream.pipe(concat('karma-concat.js')) # when debugging, dont concat
-  .to(folders_dest+'test/karma')
-
-build = (callback) ->
-  runSequence('clean', ['copy', 'coffee'], ['transform:taijiview', 'make:samples', 'make:test'], callback)
+build = (callback) -> runSequence('clean', ['copy', 'coffee'], callback)
 # make is for debugging and test, dont concat and minify
 task 'make', (callback) -> distributing = false; build(callback)
 # dist is for release, so concat and minify packages
@@ -127,21 +93,6 @@

 onErrorContinue = (err) -> console.log(err.stack); @emit 'end'
 task 'mocha', ->  src(files_mocha).pipe(mocha({reporter: 'dot'})).on("error", onErrorContinue)
-
-# removes content for karma, if need, please see the content in project peasy, and rewrite for taijiview.
-#files_karma_debug = 'twoside peasy linepeasy logicpeasy index '+\
-#  merge('samples/ statemachine dsl arithmatic arithmatic2')+' '+\
-#  merge('test/karma/ peasy logicpeasy samples-arithmatic samples-arithmatic samples-arithmatic')
-#files_karma_debug = for item in files_karma_debug.split(' ') then folders_dest+item+'.js'
-#files_karma_dist = 'twoside client/full-peasy-package samples/sample-concat test/karma/karma-concat'
-#files_karma_dist = for item in files_karma_dist.split(' ') then folders_dest+item+'.js'
-#karmaOnce = karma({configFile: folders_dest+'test/karma-conf.js', action: 'run'})
-#task 'karma1', -> src(files_karma_debug).pipe(karmaOnce)
-#task 'karma1/dist', -> src(files_karma_dist).pipe(karmaOnce)
-#karmaWatch = karma({configFile: folders_dest+'test/karma-conf.js', action: 'watch'})
-##console.log files_karma_dist.join(' ')
-#task 'karma', -> src(files_karma_debug).pipe(karmaWatch)
-#task 'karma/dist', -> src(files_karma_dist).pipe(karmaWatch)

 task 'runapp', shell.task ['node dist/examples/sockio/app.js']
 task 'express',  ->
@@ -159,7 +110,7 @@
 task 'watch/all', -> ['watch/copy', 'watch/coffee', 'watch/mocha'] #  , 'watch/reload'

 task 'mocha/auto', ['watch/copy', 'watch/coffee', 'watch/mocha']
-task 'test', (callback) -> runSequence('make', ['mocha', 'karma1'], callback)
-task 'test/dist', (callback) -> runSequence('dist', ['mocha'], callback)   #, 'karma1/dist'
+task 'test', (callback) -> runSequence('make', ['mocha'], callback)
+task 'test/dist', (callback) -> runSequence('dist', ['mocha'], callback)
 task 'default',['test']


Fixed build error "'import' and 'export' may appear only with 'sourceType: module'".

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -43,7 +43,7 @@
       sourcemap: !args.nosourcemap
       transform: [
         {name:'txtify', opt: {extensions: ['.json','.html','.css','.glsl','.xmml','.rsml','.xml']}}
-        {name: 'babelify'}
+        {name: 'babelify', opt:{presets: ["es2015"]}}
         {name: 'envify', opt: {NODE_ENV: (if env_production then 'production' else 'development')}}
       ]
       detectGlobals: true


create prefixes for 2 latest versions of chrome, firefox & ios

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -61,7 +61,7 @@
   styles = gulp
     .src paths.styles.source
     .pipe(stylus({set: ['include css']}))
-    .pipe(prefix("last 1 version", "> 1%", "ie 8"))
+    .pipe prefix 'last 2 versions', 'Chrome 34', 'Firefox 28', 'iOS 7'

   styles = styles.pipe(CSSmin()) if production


Fixed typo in atoms icons

Fixed typo in atoms icons

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -42,7 +42,7 @@
       "bower_components/atoms-app-calendar/atoms.app.calendar.js"]
     css: [
       "bower_components/atoms/atoms.app.css"
-      "bower_components/atoms.icons/atoms.icons.css"
+      "bower_components/atoms-icons/atoms.icons.css"
       "bower_components/atoms-app-carousel/atoms.app.carousel.css"
       "bower_components/atoms-app-gmaps/atoms.app.gmaps.css"
       "bower_components/atoms-app-calendar/atoms.app.calendar.css"]


make css_clean less greedy

make css_clean less greedy

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -177,7 +177,7 @@
       html: ['build/**/*.html'],
       ignore: [
                 new RegExp('^.no-touch.*'),
-                '.Header-bkg-transparent','.Header-bkg-transparent .Header-nav-item','.Header-bkg-opaque','.Header-nav-item','.Header-nav-item:after','.Header-nav-item:hover:after','.Header-cart-button-container',
+                '.Header-logo','.logotype','.Header-bkg-transparent','.Header-bkg-transparent .Header-nav-item','.Header-bkg-opaque','.Header-nav-item','.Header-nav-item:after','.Header-nav-item:hover:after','.Header-cart-button-container',
                 '.cart-full','.Cart-table-container','.Cart-rowContainer',
                 'cursor default:hover','.no-touch','.no-touch a:hover','.no-touch .Button:hover','.no-touch .button-flex:hover','.no-touch .Button-inverse:hover','.no-touch .Button-sm:hover','.no-touch .Button--cart:hover','.no-touch .Button-player:hover','.no-touch .Button-dataset:hover','.no-touch .Button--cart:active',
                 '.Wallop-dot','.Wallop-dot--current','.Wallop-item','.Wallop-item--hidePrevious','.Wallop-item--current','.Wallop-item--showNext',


0.0.5

--- hexaflip.coffee
+++ hexaflip.coffee
@@ -1,5 +1,5 @@
-# hexaFlip
-# 0.0.4
+# HexaFlip
+# 0.0.5
 # Dan Motzenbecker
 # http://oxism.com
 # Copyright 2013, MIT License


show tag in availability

show tag in availability

--- app/assets/javascripts/controllers/calendar.coffee
+++ app/assets/javascripts/controllers/calendar.coffee
@@ -32,6 +32,8 @@
       calendarEventClickCb(event, jsEvent, view)
     viewRender: (view, element) ->
       viewRenderCb(view, element)
+    eventRender: (event, element, view) ->
+      eventRenderCb(event, element)

   ### PRIVATE SCOPE ###
@@ -71,4 +73,12 @@
     if view.type == 'agendaDay'
       # get availabilties by 1 day for show machine slots
       uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents')
+
+  eventRenderCb = (event, element) ->
+    if event.tags.length > 0
+      html = ''
+      for tag in event.tags
+        html += "#{tag.name} "
+      element.find('.fc-title').append("
"+html)
+    return
 ]


try assign

try assign

--- app/assets/javascripts/shared/mobile_sms.js.coffee
+++ app/assets/javascripts/shared/mobile_sms.js.coffee
@@ -9,6 +9,6 @@
   $sms_field = $('.mobile-sms')
   if $sms_field.length > 0
     mobile_href = $sms_field.prop('href').replace('?', sms_delimiter())
-    $(location).attr('href', mobile_href);
+    window.location.assign(mobile_href)


Removed unnecessary code. Reworked min-width enforcement

--- app/classifier/drawing-tools/column-rectangle.cjsx
+++ app/classifier/drawing-tools/column-rectangle.cjsx
@@ -4,7 +4,6 @@
 Draggable = require '../../lib/draggable'
 DeleteButton = require './delete-button'

-DELETE_BUTTON_DISTANCE = 9 / 10
 DEFAULT_WIDTH = 25
 MINIMUM_WIDTH = 25

@@ -14,12 +13,11 @@
   statics:
     initCoords: null

-    defaultValues: ({x, y}) ->
+    defaultValues: ({x}) ->
       x: x
-      y: 0
       width: DEFAULT_WIDTH

-    initStart: ({x, y}, mark) ->
+    initStart: ({x}, mark) ->
       @initCoords = {x, 0}
       {x, 0, _inProgress: true}

@@ -31,9 +29,7 @@
         width = @initCoords.x - cursor.x
         x = cursor.x

-      y = mark.y
-
-      {x, y, width}
+      {x, width}

     initRelease: ->
       _inProgress: false
@@ -44,19 +40,19 @@
   initCoords: null

   render: ->
-    {x, y, width} = @props.mark
-    allowedWidth = if @props.mark.width < MINIMUM_WIDTH then MINIMUM_WIDTH else width
+    {x, width} = @props.mark
+
     <DrawingToolRoot tool={this}>
       <Draggable onDrag={@handleMainDrag} disabled={@props.disabled}>
-        <rect ref="rect" x={x} y={y} width={allowedWidth} height={@props.containerRect.height / @props.scale.vertical} />
+        <rect ref="rect" x={x} y={0} width={width} height={@props.containerRect.height / @props.scale.vertical} />
       </Draggable>

       {if @props.selected
         <g>
-          <DeleteButton tool={this} x={x + allowedWidth + 20} y={y + 15} />
+          <DeleteButton tool={this} x={x + width + 20} y={15} />

-          <DragHandle x={x} y={(@props.containerRect.height / @props.scale.vertical) / 2} scale={@props.scale} onDrag={@handleLeftDrag} onEnd={@normalizeMark} />
-          <DragHandle x={x + allowedWidth} y={(@props.containerRect.height / @props.scale.vertical) / 2} scale={@props.scale} onDrag={@handleRightDrag} onEnd={@normalizeMark} />
+          <DragHandle x={x} y={(@props.containerRect.height / @props.scale.vertical) / 2} scale={@props.scale} onDrag={@handleLeftDrag} />
+          <DragHandle x={x + width} y={(@props.containerRect.height / @props.scale.vertical) / 2} scale={@props.scale} onDrag={@handleRightDrag} />
         </g>}
     </DrawingToolRoot>

@@ -65,17 +61,12 @@
     @props.onChange @props.mark

   handleLeftDrag: (e, d) ->
-    @props.mark.x += d.x / @props.scale.horizontal if @props.mark.width >= MINIMUM_WIDTH
-    @props.mark.width -= d.x / @props.scale.horizontal
-    @props.onChange @props.mark if @props.mark.width >= MINIMUM_WIDTH
+    if @props.mark.width - d.x / @props.scale.horizontal >= MINIMUM_WIDTH
+      @props.mark.x += d.x / @props.scale.horizontal
+      @props.mark.width -= d.x / @props.scale.horizontal
+      @props.onChange @props.mark

   handleRightDrag: (e, d) ->
-    @props.mark.width += d.x / @props.scale.horizontal
-    @props.onChange @props.mark
-
-  normalizeMark: ->
-    if @props.mark.width < MINIMUM_WIDTH
-      @props.mark.x += @props.mark.width if @props.mark.width >= MINIMUM_WIDTH
-      @props.mark.width is MINIMUM_WIDTH
-
-    @props.onChange @props.mark if @props.mark.width >= MINIMUM_WIDTH
+    if @props.mark.width + d.x / @props.scale.horizontal >= MINIMUM_WIDTH
+      @props.mark.width += d.x / @props.scale.horizontal
+      @props.onChange @props.mark


Omit svgtiler version from .tex files for consistent builds

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -1010,7 +1010,7 @@
     ## LaTeX based loosely on Inkscape's PDF/EPS/PS + LaTeX output extension.
     ## See http://tug.ctan.org/tex-archive/info/svg-inkscape/
     lines = ["""
-      %% Creator: svgtiler #{metadata.version}, https://github.com/edemaine/svgtiler
+      %% Creator: svgtiler, https://github.com/edemaine/svgtiler
       %% This LaTeX file includes and overlays text on top of companion file
       %% #{basename}.pdf/.png
       %%


Rename the participant fields as per API documentation. #49

--- src/telegram.coffee
+++ src/telegram.coffee
@@ -189,14 +189,14 @@
       @receive new TextMessage user, text, message.message_id

     # Join event
-    else if message.new_chat_participant
-      user = @createUser message.new_chat_participant, message.chat
+    else if message.new_chat_member
+      user = @createUser message.new_chat_member, message.chat
       @robot.logger.info "User " + user.id + " joined chat " + message.chat.id
       @receive new EnterMessage user, null, message.message_id

     # Exit event
-    else if message.left_chat_participant
-      user = @createUser message.left_chat_participant, message.chat
+    else if message.left_chat_member
+      user = @createUser message.left_chat_member, message.chat
       @robot.logger.info "User " + user.id + " left chat " + message.chat.id
       @receive new LeaveMessage user, null, message.message_id

@@ -236,7 +236,7 @@
         res.send 'OK'

     else
-# Clear Webhook
+      # Clear Webhook
       @api.invoke 'setWebHook', {url: ''}, (err, result) ->
         if (err)
           self.emit 'error', err


its now called nodeValue

its now called nodeValue

--- src/traverse.coffee
+++ src/traverse.coffee
@@ -37,8 +37,8 @@
         if el.nodeType is DOMElementType.NORMAL
             res.push slim el
         else if el.nodeType is DOMElementType.TEXT
-            if el.value.trim().length
-                res.push el.value
+            if el.nodeValue.trim().length
+                res.push el.nodeValue
         else continue
     return res


fix scale formula

fix scale formula

--- src/ui/scale.coffee
+++ src/ui/scale.coffee
@@ -9,7 +9,7 @@
   renderScale = ->
     deg_150px = (map.projection.invert([150, 0])[0] - map.projection.invert([0, 0])[0])
     mapscale = d3.scale.linear()
-     .domain([0, deg_150px * app.DEGM])
+     .domain([0, deg_150px])
      .rangeRound([0, 150])

     scaleg.selectAll().remove()


Use real url duh

Use real url duh

--- src/worldcup.coffee
+++ src/worldcup.coffee
@@ -118,7 +118,7 @@
     else
       ""

-    msg.http("http://localhost:4002/scores/now?timezone=#{timezone}")
+    msg.http("http://worldcup2014bot.herokuapp.com/scores/now?timezone=#{timezone}")
       .get() (err, res, body) ->
         scores = JSON.parse(body).scores
         if scores.length > 0


Fix undefined variable when paging backwards in RSM

--- src/xmpp/rsm.coffee
+++ src/xmpp/rsm.coffee
@@ -39,7 +39,7 @@
         if 'before' of @
             # Paging backwards
             results = results.slice(Math.max(0, results.length - @max), results.length)
-            @firstIndex = count - results.length
+            @firstIndex = @count - results.length
         else
             # Paging forward
             results = results.slice(0, Math.min(@max, results.length))


Remove unused classes and outlets

Remove unused classes and outlets

--- lib/package-keymap-view.coffee
+++ lib/package-keymap-view.coffee
@@ -6,7 +6,7 @@
   @content: ->
     @section =>
       @div class: 'section-heading icon icon-keyboard', 'Keybindings'
-      @table outlet: 'keymapTable', class: 'package-keymap-table table native-key-bindings text', tabindex: -1, =>
+      @table class: 'package-keymap-table table native-key-bindings text', tabindex: -1, =>
         @thead =>
           @tr =>
             @th 'Keystroke'
@@ -19,9 +19,9 @@
       continue unless command.indexOf("#{namespace}:") is 0

       @keybindingItems.append $$$ ->
-        @tr class: 'package-keymap-item', =>
-          @td class: 'keystroke', keystroke
-          @td class: 'command', command
-          @td class: 'selector', selector
+        @tr =>
+          @td keystroke
+          @td command
+          @td selector

     @hide() unless @keybindingItems.children().length > 0


no more warnings about number vs string

no more warnings about number vs string

--- tutor/src/flux/scores.coffee
+++ tutor/src/flux/scores.coffee
@@ -18,6 +18,10 @@
   _.chain(scores)
     .pluck('students')
     .flatten(true)
+    .each((student) ->
+      # TODO remove when BE fixes role to be string
+      student.role = "#{student.role}"
+    )
     .value()

 computeTaskCache = (data) ->
@@ -153,8 +157,7 @@

     getStudent: (courseId, roleId) ->
       students = allStudents @_get(courseId)
-      # TODO remove parseInt when BE fixes role to be string
-      _.findWhere(allStudents(@_get(courseId)), role: parseInt(roleId))
+      _.findWhere(allStudents(@_get(courseId)), role: roleId)

     getAllStudents: (courseId) ->
       allStudents @_get(courseId)


more minor tweaks

more minor tweaks

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -110,9 +110,9 @@
         Response::setHeader = (name, value) -> undefined
         Response::end = (data, encoding) -> undefined
         cookies handshake, response = new Response, =>
-            session handshake, response, =>
-                session = handshake.session
-                ns = new Error "no session found"
+            session handshake, response, (parameter) =>
+                session = handshake.session or null
+                ns = new Error "no session detected"
                 return accept ns, no unless session
                 handshaken = @downstream handshaken: ->
                     return accept undefined, yes
@@ -130,11 +130,11 @@
         assert _.isFunction i = Marshal.deserialize
         location = "Breakpoint at @#{method}#%s"
         message = "Error running provider:\r\n%s"
-        guarded.on "error", (error) ->
+        guarded.on "error", (error, optional) ->
             logger.error location.red, identify
             logger.error message.red, error.stack
             socket.emit "exception", o([error])...
-            try socket.disconnect?()
+            try socket.disconnect?() catch error
         return guarded

     # A utility method to mark the certain function as the provider.


minor mention in the doc

minor mention in the doc

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -53,6 +53,7 @@
 # its dependencies, such as classes and class hierarchies and even more.
 # This ABC constitutes a primary tool for writing UI/UX with Granite.
 # Please consult the implementation for more information on the system.
+# Also, please reference parent class for the important external APIs.
 module.exports.Screenplay = class Screenplay extends VisualBillet

     # This is a marker that indicates to some internal subsystems


Better place for the RTE installer

Better place for the RTE installer

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -76,8 +76,6 @@
     # method is wired in an synchronous way for greater functionality.
     # This is the place where you would be importing the dependencies.
     prelude: (context, request, next) ->
-        runtime = "(#{coffee}).apply(this)"
-        context.sources.unshift runtime
         context.service = @constructor.identify()
         context.session = request.session
         context.uuid = request: request.uuid
@@ -127,12 +125,14 @@
     deployContext: (context) ->
         assert _.isObject context
         prepared = JSON.stringify context
+        runtime = "(#{coffee}).apply(this)"
         installer = "var context = #{prepared}"
         _.forIn this, (value, key, object) ->
             return unless _.isObject value.remote
             return unless src = value.remote.source
             set = "context.%s = (#{src})()"
             installer += "\r\n#{format set, key}\r\n"
+        context.sources.unshift runtime
         context.sources.unshift installer
         return context


Updating

Updating

--- packages.cson
+++ packages.cson
@@ -1,26 +1,19 @@
 packages: [
-  "autocomplete-go"
-  "autocomplete-haskell"
   "autocomplete-html-entities"
-  "autocomplete-java"
   "autocomplete-json"
   "autocomplete-modules"
-  "autocomplete-php"
   "autocomplete-python"
-  "autocomplete-ruby"
   "autocomplete-sass"
   "file-icons"
-  "fusion-ui"
   "git-plus"
   "git-time-machine"
   "language-asciidoc"
+  "language-debian"
   "language-docker"
   "language-gentoo"
+  "language-lisp"
   "language-rpm-spec"
   "md-writer"
   "minimap"
   "pigments"
-  "platformio-ide-terminal"
-  "fusion809/language-ini2"
-  "fusion809/language-patch2"
 ]


GameObject.Effect

GameObject.Effect

--- test/game_object_effect.coffee
+++ test/game_object_effect.coffee
@@ -8,8 +8,8 @@
   player.fadeOut 1, ->
     fadedOut = true

-  player.trigger "update", 0.5
-  player.trigger "afterUpdate",
+  player.trigger "update", 1
+  player.trigger "afterUpdate", 1

   equals player.I.alpha, 0, "Player has faded out"
   ok fadedOut, "callback was called"


add test to parser for last operation

add test to parser for last operation

--- test/gerber-parser_test.coffee
+++ test/gerber-parser_test.coffee
@@ -196,7 +196,7 @@
       p.parseCommand(block 'X1Y1D3').should.eql {
         op: { do: 'flash', x: 0.01, y: 0.01 }
       }
-    it 'should interpolat with an inline mode set', ->
+    it 'should interpolate with an inline mode set', ->
       p.parseCommand(block 'G01X01Y01D01').should.eql {
         set: { mode: 'i' }, op: { do: 'int', x: 0.01, y:0.01 }
       }
@@ -206,3 +206,7 @@
       p.parseCommand(block 'G03X01Y01D01').should.eql {
         set: { mode: 'ccw' }, op: { do: 'int', x: 0.01, y:0.01 }
       }
+    it 'should send a last operation command if the op code is missing', ->
+      p.parseCommand(block 'X01Y01').should.eql {
+        op: { do: 'last', x: 0.01, y: 0.01 }
+      }


temporarily disabling color detection while trying to solve cross domain issue

Former-commit-id: d167c0b3b2ae8227a7ab4ca808c4eab542f71f5b [formerly 01b81b3da3ee4c9a6aa71ba34a9fa67063c6d727] [formerly b55b531c8b47a5144bade1eba68d93f761f6f6d7 [formerly 872ad6ffdb95d12f945de9f6403e78531369d772]] [formerly 2aeb96ad69552203fc19c7a5c8232de2ea93d420 [formerly 762282b45a83e3b0029bdb0f70ed07276092ad8e] [formerly d0b30df6b8cdb1439526b453cedcddc9d66081c3 [formerly d0b30df6b8cdb1439526b453cedcddc9d66081c3 [formerly c31285fe63aef408524448da49d178b3f3e2e7ac]]]]
Former-commit-id: e6665393275b018b0ab6f71d0f9209f94cbee2cb [formerly e00060e7322e35c8bd92e1eceefa474705a72c9e] [formerly 71e4261c4da6594dadbcb81d2a9855e1984a880c [formerly 949a4655876cb90d1a0613688924263d392cd7e3]]
Former-commit-id: 1518a204944a9a413017f6784a0230f99a359b80 [formerly e61d305a590f5cdcd63f493857a87451357fa55f]
Former-commit-id: 6e72e4de3e893d3f44dfc0c67130ea340bc9910f
Former-commit-id: 41cf9cca2864e6394ac620a236e256edc2623044

--- @client/shared.coffee
+++ @client/shared.coffee
@@ -150,7 +150,6 @@

       imagePoll = ->
         if img[0].complete
-          img[0].crossOrigin = "Anonymous"
           colorThief = new ColorThief()
           rgb = colorThief.getColor img[0], 5, true
           rgb =
@@ -211,10 +210,11 @@
 window.isLightBackground = (el, callback) ->
   coords = getCoords el
   color = backgroundColorAtCoord coords.cx, coords.cy, (color) ->
-    callback color.hsl.l > .75
+    callback true #color.hsl.l > .75
   , el

-  color?.hsl.l > .75
+  #color?.hsl.l > .75
+  return true

 #########################


test($filter): fix #49, add test-case(use keyword) for parse condition.

--- test/odata.query.filter.coffee
+++ test/odata.query.filter.coffee
@@ -36,6 +36,14 @@
           return done(err)  if(err)
           res.body.value.length.should.be.equal(1)
           res.body.value[0].title.should.be.equal(books[1].title)
+          done()
+    it 'should filter items when field has keyword', (done) ->
+      request("http://localhost:#{PORT}")
+        .get("/book?$filter=author eq 'Ralls, Kim'")
+        .expect(200)
+        .end (err, res) ->
+          return done(err)  if(err)
+          res.body.value.length.should.be.equal(1)
           done()
     it 'should filter items when it has extra spaces at begin', (done) ->
       request("http://localhost:#{PORT}")


rename

rename

--- test/redis.storage.spec.coffee
+++ test/redis.storage.spec.coffee
@@ -100,7 +100,7 @@
                     args = ['events:123', 0, 4, 'WITHSCORES']
                     cli.zrangebyscore args, (err, reply) ->
                         done()
-    describe '#write stream revision changed', ->
+    describe '#write stream with old revision', ->
         it 'should return concurrency error', (done) ->
             sut = redisStorage
             commit1 =


Tween: remove logging

Tween: remove logging

--- components/Tween.coffee
+++ components/Tween.coffee
@@ -64,7 +64,6 @@
     s = s.replace(/out$/, "Out")
     s = s.replace(/inout$/, "InOut")

-    console.log(name + ' -> ' + s)

     return null unless trident.EasingFunctions[s]
     return new trident.EasingFunctions[s]()
@@ -77,12 +76,7 @@
     @easing = @getEasing('linear')
     @tween.addEventListener('onpulse', (timeline, durationFraction, timelinePosition) =>
       return unless @outPorts.value.isAttached()
-      console.log('pulse durationF=' + durationFraction)
-      console.log('pulse timelineP=' + timelinePosition)
-      console.log('pulse start=' + @startValue)
-      console.log('pulse stop=' + @stopValue)
       value = @startValue + @easing.map(durationFraction) * (@stopValue - @startValue)
-      console.log('pulse value=' + value)
       @outPorts.value.send(value)
       @outPorts.value.disconnect())
     @tween.addEventListener('onstatechange', (timeline, oldState, newState, durationFraction, timelinePosition) =>


Kissmetrics is always defined now, no need for check

--- js/inner.coffee
+++ js/inner.coffee
@@ -890,8 +890,7 @@

 $(document).ready () ->
   SammyApp.run window.location.pathname.replace(/(.+)\/$/, "$1")
-  if window._kmq?
-    _kmq.push(['identify', VM.current_user().login])
+  _kmq.push(['identify', VM.current_user().login])


"Occasional committers" is more obvious in its intentions

--- js/inner.coffee
+++ js/inner.coffee
@@ -411,7 +411,7 @@
       price: 14
     ,
       title: "Pay as you go"
-      reason: "For occasional contributors"
+      reason: "For occasional committers"
       parallel: 8
       cost: 1
       limits: null,


Update Spanish - Español (América Latina) (#5131)

--- app/locale/es-419.coffee
+++ app/locale/es-419.coffee
@@ -237,13 +237,13 @@
     signup_switch: "¿Quieres crear una cuenta?"

   signup:
-#    complete_subscription: "Complete Subscription"
+    complete_subscription: "Completa Suscripción"
     create_student_header: "Crear Cuenta de Estudiante"
     create_teacher_header: "Crear Cuenta de Maestro"
     create_individual_header: "Crear Cuenta Individual"
     email_announcements: "Recibe noticias por email" # {change}
-#    sign_in_to_continue: "Sign in or create an account to continue"
-#    teacher_email_announcements: "Keep me updated on new teacher resources, curriculum, and courses!"
+    sign_in_to_continue: "Inicia sesión o crea una cuenta para continuar"
+    teacher_email_announcements: "Mantenme actualizado sobre nuevos recursos para maestros, currículo y cursos!"
     creating: "Creando Cuenta..."
     sign_up: "Registrarse"
     log_in: "Inicia sesión con tu contraseña"
@@ -256,8 +256,8 @@
     connected_facebook_p: "Termina el registro para que inicies sesión con tu cuenta de Facebook."
     hey_students: "Estudiante, ingresa el coódigo de clase de tu maestro."
     birthday: "Cumpleaños"
-#    parent_email_blurb: "We know you can't wait to learn programming — we're excited too! Your parents will receive an email with further instructions on how to create an account for you. Email {{email_link}} if you have any questions."
-#    classroom_not_found: "No classes exist with this Class Code. Check your spelling or ask your teacher for help."
+    parent_email_blurb: "Nosotros sabemos que tu no puedes esperar para aprender programación — nosotros tambien estamos emocionados! Tus padres recibirán un correo electrónico con más instrucciones sobre cómo crear una cuenta para ti. Envíe un correo electrónico a {{email_link}} si tienes alguna pregunta"
+    classroom_not_found: "Ninguna clase existe con este código de clase. Revisa tu ortografía o pidele ayuda a tu maestro."
     checking: "Comprobando..."
     account_exists: "Este email ya esta en uso:"
     sign_in: "Iniciar Sesión"
@@ -267,15 +267,15 @@
     name_is_email: "Tu email no puede ser tu nombre de usuario"
     choose_type: "Elige el tipo de cuenta:"
     teacher_type_1: "Enseña a como programar con CodeCombat!"
-#    teacher_type_2: "Set up your class"
-#    teacher_type_3: "Access Course Guides"
-#    teacher_type_4: "View student progress"
+    teacher_type_2: "Configura a tu clase"
+    teacher_type_3: "Accesa guías de cursos"
+    teacher_type_4: "Ve el progreso de estudiante"
     signup_as_teacher: "Iniciar sesión como Maestro"
-#    student_type_1: "Learn to program while playing an engaging game!"
+    student_type_1: "Aprende a programar mientras que juegas un juego atractivo"
     student_type_2: "Jugar con tu clase"
-#    student_type_3: "Compete in arenas"
-#    student_type_4: "Choose your hero!"
-#    student_type_5: "Have your Class Code ready!"
+    student_type_3: "Compite in arenas"
+    student_type_4: "Escoge tu héroe!"
+    student_type_5: "Ten tu código de clase listo!"
     signup_as_student: "Iniciar sesión como Estudiante"
 #    individuals_or_parents: "Individuals & Parents"
 #    individual_type: "For players learning to code outside of a class. Parents should sign up for an account here."


Let emmet override the atom autocomplete

--- public/.atom/keymap.cson
+++ public/.atom/keymap.cson
@@ -1,3 +1,3 @@
 # Enable Emmet in JSX
-'atom-text-editor[data-grammar~="jsx"]:not([mini]):not(.autocomplete-active)':
+'atom-text-editor[data-grammar~="jsx"]:not([mini])':
   'tab': 'emmet:expand-abbreviation-with-tab'


little clean up

little clean up

--- app/src/i18n/i18n.coffee
+++ app/src/i18n/i18n.coffee
@@ -300,8 +300,6 @@
     return deferred.promise

-  # User Favorite Language ####
-
   ###
     Set user favorite language into stores from memory
   ###
@@ -452,7 +450,7 @@

   ###
-    Set and Get @userFavLocale from one of the store
+    Load @favLocale.memoryValue from one of the store

     @return [Promise]
   ###
@@ -486,7 +484,7 @@
     deferred = Q.defer()
     if ledger.storage.sync?
       ledger.storage.sync.get 'i18n_favLocale', (r) =>
-        if r.i18n_favLocale isnt undefined  # and r.i18n_favLocale is @favLang.memoryValue
+        if r.i18n_favLocale isnt undefined
           @favLocale.syncStoreIsSet = true
           deferred.resolve('ledger.storage.sync.get r.i18n_favLocale ' + r.i18n_favLocale + ' is set into synced Store')
         else
@@ -534,6 +532,13 @@

   ###
+   Set the locale for Moment.js
+  ###
+  @setMomentLocale: () =>
+    moment.locale(@favLocale.memoryValue.toLowerCase())
+
+
+  ###
     Translate a message id to a localized text

     @param [String] messageId Unique identifier of the message
@@ -564,13 +569,6 @@

   ###
-    Set the locale for Moment.js
-  ###
-  @setMomentLocale: () =>
-    moment.locale(@favLocale.memoryValue.toLowerCase())
-
-
-  ###
     Formats date and time

     @param [Date] dateTime The date and time to format


Remove `~/Labs/Utils` from Atom-FS's project entry

--- projects.cson
+++ projects.cson
@@ -31,7 +31,6 @@
 		title: "Atom-FS"
 		paths: [
 			"~/Labs/Atom-FS"
-			"~/Labs/Utils"
 		]
 		devMode: true
 	}


test(filters): removed extra *p* tags and newlines

--- tests/unit/filters.spec.coffee
+++ tests/unit/filters.spec.coffee
@@ -10,24 +10,24 @@
     it 'should insert  tags around text surrounded by single asterisks',
       inject ($sce, markdownFilter) ->
         expect($sce.getTrustedHtml(markdownFilter '*Markdown*'))
-        .toEqual "
Markdown
\n"
+        .toEqual 'Markdown'

     it 'should insert  tags around text surrounded by double asterisks',
       inject ($sce, markdownFilter) ->
         expect($sce.getTrustedHtml(markdownFilter '**Markdown**'))
-        .toEqual "
Markdown
\n"
+        .toEqual 'Markdown'

   describe 'markdown sanitizer', ->
     it 'should strip unbalanced tags from the output',
       inject ($sce, markdownFilter) ->
         expect($sce.getTrustedHtml(markdownFilter 'Unmatched tag'))
-        .toEqual "
<b>Unmatched tag
\n"
+        .toEqual '<b>Unmatched tag'

     it 'should remove script tags from the output',
       inject ($sce, markdownFilter) ->
         expect(
           $sce.getTrustedHtml(markdownFilter ''))
-        .toEqual """
<script>alert(“Bug”);</script>
\n"""
+        .toEqual '<script>alert(“Bug”);</script>'

   describe 'markdown extra', ->
     it 'should render tables', inject ($sce, markdownFilter) ->


Fix emojize

Fix emojize

--- app/utils/helpers.coffee
+++ app/utils/helpers.coffee
@@ -144,7 +144,7 @@
   if emojis isnt null
     emojis.uniq().forEach (emoji, ix) ->
       strippedEmoji = emoji.substring(1, emoji.length - 1)
-      unless EmojiDictionary.indexOf(strippedEmoji) is -1
+      unless emojiDictionary.indexOf(strippedEmoji) is -1
         image = '<img class=\'emoji\' title=\'' + emoji + '\' alt=\'' + emoji + '\' src=\'' + '/images/emoji/' + strippedEmoji + '.png\'/>'
         text = text.replace(new RegExp(emoji, 'g'), image)
   text


New flake8 shows full module path for F401, Fixes #211 (#218)

This change will split on . and take the last part, should still work for old flake8 users.

--- lib/main.coffee
+++ lib/main.coffee
@@ -55,6 +55,7 @@
     when 'F401'
       # F401 - 'SYMBOL' imported but unused
       symbol = /'([^']+)'/.exec(message)[1]
+      [prefix..., symbol] = symbol.split('.')
       foundImport = false
       while true
         offset = 0


:bug: Render views on linter dispose So we can remove outdated messages from view

--- lib/main.coffee
+++ lib/main.coffee
@@ -38,6 +38,7 @@
             @instance.eachLinter (editorLinter) -> editorLinter.messages.delete(item)
           false
         true
+      @instance.views.render()
   consumeStatusBar: (statusBar) ->
     @instance.views.attachBottom(statusBar)


Fix large file check

Fix large file check

--- lib/main.coffee
+++ lib/main.coffee
@@ -2,6 +2,8 @@

 SpellCheckView = null
 spellCheckViews = {}
+
+LARGE_FILE_SIZE = 2 * 1024 * 1024

 module.exports =
   activate: ->
@@ -59,7 +61,7 @@
       return if @viewsByEditor.has(editor)

       # For now, just don't spell check large files.
-      return if editor.largeFileMode
+      return if editor.getBuffer().getLength() > LARGE_FILE_SIZE

       # Defer loading the spell check view if we actually need it. This also
       # avoids slowing down Atom's startup by getting it loaded on demand.


virtualization controller: setPacks => setContents

--- client/app/MainApp/VirtualizationController.coffee
+++ client/app/MainApp/VirtualizationController.coffee
@@ -373,12 +373,12 @@

       productForm.setCurrentSubscriptions subscriptions

-    productForm.on 'PackOfferingRequested', ->
+    productForm.on 'PackOfferingRequested', (subscription) ->
       options = targetOptions: selector: tags: 'vm'
       KD.getGroup().fetchProducts 'pack', options, (err, packs) ->
         return  if KD.showError err

-        productForm.setPacks packs
+        productForm.setContents packs

     workflow = new PaymentWorkflow
       productForm : productForm


collaboration: social helpers add documentation, improve

--- client/ide/lib/collaboration/helpers/social.coffee
+++ client/ide/lib/collaboration/helpers/social.coffee
@@ -3,28 +3,84 @@
 getNick = require 'app/util/nick'
 getCollaborativeChannelPrefix = require 'app/util/getCollaborativeChannelPrefix'

+###
+ * Wrapper function around `SocialApiController#addParticipants`
+ *
+ * @param {object} opts
+ * @param {function(err: object)}
+###
 addParticipants = (opts, callback) ->
+
   kd.singletons.socialapi.channel.addParticipants opts, callback

+
+###
+ * Wrapper function around `SocialApiController#removeParticipants`
+ *
+ * @param {object} opts
+ * @param {function(err: object)}
+###
 removeParticipants = (opts, callback) ->
+
   kd.singletons.socialapi.channel.removeParticipants opts, callback

-kickParticipants = (channel, accounts = [], callback) ->
+
+###
+ * Wrapper function around `SocialApiController#kickParticipants`
+ *
+ * @param {object} opts
+ * @param {function(err: object)}
+###
+kickParticipants = (channel, accounts, callback) ->
+
+  return callback 'accounts are missing'  unless accounts
+
   accounts = [].concat accounts  unless Array.isArray accounts
+
   opts = { channelId: channel.id, accountIds: accounts.map (a) -> a.socialApiId }
   kd.singletons.socialapi.channel.kickParticipants opts, callback

+
+###
+ * Wrapper function around `SocialApiController#fetchChannel`
+ *
+ * @param {object} opts
+ * @param {function(err: object)}
+###
 fetchChannel = (id, callback) ->
+
   kd.singletons.socialapi.cacheable 'channel', id, callback

+
+###
+ * Wrapper function around `SocialApiController#destroyChannel`
+ *
+ * @param {SocialChannel} channel
+ * @param {function(err: object)}
+###
 destroyChannel = (channel, callback) ->
   {id} = channel
   kd.singletons.socialapi.channel.delete {channelId: id}, callback

+
+###
+ * Wrapper function around `SocialApiController#leaveChannel`
+ *
+ * @param {SocialChannel} channel
+ * @param {function(err: object)}
+###
 leaveChannel = (channel, callback) ->
   options = { channelId: channel.id }
   kd.singletons.socialapi.channel.leave options, callback

+
+###
+ * Wrapper function around `SocialApiController#initChannel`
+ *
+ * Difference is it sets `collaboration` defaults.
+ *
+ * @param {function(err: object)}
+###
 initChannel = (callback) ->
   {message} = kd.singletons.socialapi
   nickname  = getNick()
@@ -41,8 +97,17 @@
     return callback {message: 'error'}  unless channels?.length
     return callback null, channels[0]

+
+###
+ * Wrapper function around `JAccount#cacheable`
+ *
+ * @param {string} userId
+ * @param {function(err: object)}
+###
 fetchAccount = (userId, callback) ->
+
   remote.cacheable 'JAccount', userId, callback
+

 module.exports = {
   addParticipants


Dispatch a event when the user couldnt start video collar.

--- client/ide/lib/videocollaborationcontroller.coffee
+++ client/ide/lib/videocollaborationcontroller.coffee
@@ -204,6 +204,7 @@
         participantsCount = @participants.asArray().length

         if participantsCount > limit or (participantsCount is 2 and isVideoActive)
+          @emit 'UserCouldNotStartVideo'
           return new LimitedVideoCollaborationModal plan: planTitle

         callback()


remove 'mode' property when creating a new CodeMirror control. This appears to completely crash if the user puts in a <style ...> tag in the input. This was causing imported static tabs to not be editable

--- common/lib/xmodule/xmodule/js/src/html/edit.coffee
+++ common/lib/xmodule/xmodule/js/src/html/edit.coffee
@@ -1,7 +1,6 @@
 class @HTMLEditingDescriptor
   constructor: (@element) ->
     @edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
-      mode: "text/html"
       lineNumbers: true
       lineWrapping: true
     })


Add bogus data to fix IR bug when not using 'data' field (#41)

--- router.coffee
+++ router.coffee
@@ -126,6 +126,9 @@
       [ Meteor.subscribe 'posts'
         Meteor.subscribe 'authors' ]

+    data: ->
+      true
+
   #
   # New/Edit Blog
   #
@@ -151,3 +154,6 @@
       Meteor.subscribe 'singlePostById', @params.id
       Meteor.subscribe 'authors'
     ]
+
+    data: ->
+      true


Now responds to SSDP search messages

Now responds to SSDP search messages

Create a faux HTTP request to parse SSDP headers

--- lib/ssdp.coffee
+++ lib/ssdp.coffee
@@ -1,5 +1,6 @@
 dgram = require 'dgram'
 url = require 'url'
+http = require 'http'
 event = require('events').EventEmitter
 device = require './device'

@@ -90,8 +91,35 @@
                     callback err, socket.address().port
                     socket.close()

-    advertise (err, port) ->
-        setInterval advertise, makeAdvDelay(), (err, port) ->
-            callback err
+    advertise (err) ->
+        throw err if err
+        setInterval advertise, makeAdvDelay(), (err) ->
+            throw err if err
+
+    parseHeaders = (msg, callback) ->
+        httpParser = http.parsers.alloc()
+        httpParser.reinitialize('request')
+        httpParser.onIncoming = (req) ->
+            callback null, req
+            http.parsers.free(httpParser)
+        httpParser.execute(msg, 0, msg.length)
+
+    listen = (callback) ->
+        socket = dgram.createSocket 'udp4'
+        socket.on 'message', (msg, rinfo) ->
+            parseHeaders msg, (err, headers) ->
+                # these are the ST values we should respond to
+                respondTo = ['ssdp:all', 'upnp:rootdevice', config.device.uuid]
+
+                if headers.method is 'M-SEARCH' and headers.headers.st in respondTo
+                    # specification says to wait between 0 and MX seconds before responding
+                    wait = Math.floor(Math.random() * (parseInt(headers.headers.mx) + 1))
+                    setTimeout advertise, wait, (err) ->
+                        callback err
+
+        socket.bind(1900)
+
+    listen (err, msg) ->
+        console.log msg

 exports.start = start


Check for existence of sandbox bindir

Check for existence of sandbox bindir

before prepending it to PATH

--- lib/util.coffee
+++ lib/util.coffee
@@ -50,7 +50,9 @@
     apd = atom.config.get('haskell-ghc-mod.additionalPathDirectories')
           .concat process.env.PATH.split delimiter
     if rootPath
-      apd.unshift "#{rootPath}#{sep}.cabal-sandbox#{sep}bin"
+      sandbox = "#{rootPath}#{sep}.cabal-sandbox#{sep}bin"
+      try if FS.statSync(sandbox).isDirectory()
+        apd.unshift sandbox
     env.PATH = "#{apd.join(delimiter)}"
     Util.debug "PATH = #{env.PATH}"
     options =


Pass in a destination option for artwork saves

--- components/artwork_item/views/save_controls.coffee
+++ components/artwork_item/views/save_controls.coffee
@@ -28,7 +28,10 @@

   save: (e) ->
     unless @artworkCollection
-      mediator.trigger 'open:auth', { mode: 'register', copy: 'Sign up to save artworks' }
+      mediator.trigger 'open:auth',
+        mode: 'register'
+        copy: 'Sign up to save artworks'
+        destination: "#{@model.href()}/save"
       return false

     if @model.isSaved @artworkCollection


missed comment

missed comment

--- server.coffee
+++ server.coffee
@@ -10,8 +10,8 @@
   console.log "CozyMail server listening on port %d within %s environment", port, app.settings.env

   # setup KUE
-  #@kue = require 'kue'
-  @kue.app.listen 3003
+  @kue = require 'kue'
+  #@kue.app.listen 3003
   Job = @kue.Job
   @jobs = @kue.createQueue()


Disable indexes for now.

Disable indexes for now.

--- server.coffee
+++ server.coffee
@@ -29,12 +29,9 @@

     glob "#{@folder}/*.xml", {}, (er, files) =>
       # if !_.find(files, "#{@folder}/index.html")
-      indexXml = new IndexScene(files).toXml()
-
-      console.log indexXml
-
-      Scene.load indexXml, (scene) =>
-        @onLoaded(scene, '/index.xml')
+      # indexXml = new IndexScene(files).toXml()
+      # Scene.load indexXml, (scene) =>
+      #   @onLoaded(scene, '/index.xml')

       files.forEach (filename) =>
         Scene.load filename, (scene) =>


Further optimize exists.

Further optimize exists.

--- server.coffee
+++ server.coffee
@@ -26,10 +26,12 @@
   throw new Error "Cannot call exists on a tailable cursor" if @_cursorDescription.options.tailable

   unless @_synchronousCursorForExists
-    # A special cursor with limit forced to 1.
+    # A special cursor with limit forced to 1 and fields to only _id.
     cursorDescription = _.clone @_cursorDescription
     cursorDescription.options = _.clone cursorDescription.options
     cursorDescription.options.limit = 1
+    cursorDescription.options.fields =
+      _id: 1
     @_synchronousCursorForExists = @_mongo._createSynchronousCursor cursorDescription,
       selfForIteration: @
       useTransform: false


removed duplicated server.coffee

removed duplicated server.coffee

--- server.coffee
+++ server.coffee
@@ -1,15 +0,0 @@
-SocketThread = (require './socket_thread').SocketThread
-util = (require './util')
-util.setId 'Server'
-logUser = util.logUser
-
-server = new SocketThread server: true, spawn: false
-server.getRecord = (data, callback) ->
- logUser "Data receved"
- logUser data
- callback null, {id: 0, vendor: "pamona", amount: 1231.32, date: "12312013"}
-
-server.onStarted (err, data) ->
- console.log "Server started : ", err
-
-console.log "Starting the server"


Support for running on multiple instances.

--- server.coffee
+++ server.coffee
@@ -2,6 +2,22 @@
 Future = Npm.require 'fibers/future'

 globals = @
+
+# From Meteor's random/random.js
+UNMISTAKABLE_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'
+
+INSTANCES = parseInt(process.env.PEERDB_INSTANCES ? 1)
+INSTANCE = parseInt(process.env.PEERDB_INSTANCE ? 0)
+
+throw new Error "Invalid number of instances: #{ INSTANCES }" unless 0 <= INSTANCES <= UNMISTAKABLE_CHARS.length
+throw new Error "Invalid instance index: #{ INSTANCE }" unless (INSTANCES is 0 and INSTANCE is 0) or 0 <= INSTANCE < INSTANCES
+
+# TODO: Support also other types of _id generation (like ObjectID)
+PREFIX = UNMISTAKABLE_CHARS.split ''
+
+if INSTANCES > 1
+  range = UNMISTAKABLE_CHARS.length / INSTANCES
+  PREFIX = PREFIX[Math.round(INSTANCE * range)...Math.round((INSTANCE + 1) * range)]

 # Fields:
 #   serial
@@ -26,10 +42,13 @@
   projection

 # TODO: Should we add retry?
-catchErrors = (f) ->
-  return (args...) ->
+observerCallback = (f) ->
+  return (id, args...) ->
     try
-      f args...
+      # We call f only if the first character of id is in PREFIX.
+      # By that we allow each instance to operate only on a subset
+      # of documents, allowing simple coordination while scaling.
+      f id, args... if id[0] in PREFIX
     catch e
       Log.error "PeerDB exception: #{ e }: #{ util.inspect args, depth: 10 }"
       Log.debug e.stack
@@ -45,14 +64,14 @@
   initializing = true

   observers =
-    added: catchErrors (id, fields) =>
+    added: observerCallback (id, fields) =>
       @updateSource id, fields if updateAll or not initializing

   unless updateAll
-    observers.changed = catchErrors (id, fields) =>
+    observers.changed = observerCallback (id, fields) =>
       @updateSource id, fields

-    observers.removed = catchErrors (id) =>
+    observers.removed = observerCallback (id) =>
       @removeSource id

   referenceFields = fieldsToProjection @fields
@@ -389,11 +408,11 @@
     initializing = true

     observers =
-      added: catchErrors (id, fields) =>
+      added: observerCallback (id, fields) =>
         @_sourceUpdated id, fields if updateAll or not initializing

     unless updateAll
-      observers.changed = catchErrors (id, fields) =>
+      observers.changed = observerCallback (id, fields) =>
         @_sourceUpdated id, fields

     handle = @Meta.collection.find({}, fields: sourceFields).observeChanges observers
@@ -556,6 +575,13 @@
         newName = migration.newName
       else
         newName = currentName
+
+      if process.env.PEERDB_SKIP_MIGRATIONS
+        # We are skipping migrations but are still running
+        # the code around just to compute latest schema version
+        currentSchema = newSchema
+        currentName = newName
+        continue

       migration._updateAll = false

@@ -610,25 +636,27 @@
       currentSchema = newSchema
       currentName = newName

-    # For all those documents which lack schema information we assume they have the last schema
-    @Meta.collection.update
-      _schema:
-        $exists: false
-    ,
-      $set:
-        _schema: currentSchema
-    ,
-      multi: true
-
-    notMigrated = _.pluck @Meta.collection.find(
-      _schema:
-        $ne: currentSchema
-    ,
-      fields:
-        _id: 1
-    ).fetch(), '_id'
-
-    throw new Error "Not all documents migrated to the latest schema version (#{ currentSchema }): #{ notMigrated }" if notMigrated.length
+    # We do not check for not migrated documents if we are skipping migrations
+    unless process.env.PEERDB_SKIP_MIGRATIONS
+      # For all those documents which lack schema information we assume they have the last schema
+      @Meta.collection.update
+        _schema:
+          $exists: false
+      ,
+        $set:
+          _schema: currentSchema
+      ,
+        multi: true
+
+      notMigrated = _.pluck @Meta.collection.find(
+        _schema:
+          $ne: currentSchema
+      ,
+        fields:
+          _id: 1
+      ).fetch(), '_id'
+
+      throw new Error "Not all documents migrated to the latest schema version (#{ currentSchema }): #{ notMigrated }" if notMigrated.length

     @Meta.schema = currentSchema

@@ -638,20 +666,21 @@
   @setupMigrations: ->
     updateAll = @migrate()

-    @Meta.collection.find(
-      _schema:
-        $exists: false
-    ,
-      fields:
-        _id: 1
-        _schema: 1
-    ).observeChanges
-      added: catchErrors (id, fields) =>
-        return if fields._schema
-
-        @Meta.collection.update id,
-          $set:
-            _schema: @Meta.schema
+    if INSTANCES > 0
+      @Meta.collection.find(
+        _schema:
+          $exists: false
+      ,
+        fields:
+          _id: 1
+          _schema: 1
+      ).observeChanges
+        added: observerCallback (id, fields) =>
+          return if fields._schema
+
+          @Meta.collection.update id,
+            $set:
+              _schema: @Meta.schema

     # Return if updateAll should be called
     updateAll
@@ -705,9 +734,21 @@
   # (Otherwise setupObservers would trigger strange exceptions anyway)
   globals.Document.defineAll()

+  Log.warn "Skipped migrations." if process.env.PEERDB_SKIP_MIGRATIONS
+  # We still run the code to determine schema version and setup
+  # observer to set schema version when inserting new documents,
+  # but we then inside the code skip running migrations themselves
   migrations()

-  setupObservers()
+  if INSTANCES is 0
+    Log.warn "Skipped observers."
+  else
+    if INSTANCES is 1
+      Log.warn "Enabling observers..."
+    else
+      Log.warn "Enabling observers, instance #{ INSTANCE }/#{ INSTANCES }, matching ID prefix: #{ PREFIX.join '' }"
+    setupObservers()
+    Log.warn "Done."

 Document = globals.Document


added support for process.env.PORT

added support for process.env.PORT

--- server.coffee
+++ server.coffee
@@ -1,7 +1,7 @@
 express = require 'express'
 routes = require './routes'
 dir = "#{__dirname}/dist"
-port = process.argv.splice(2)[0] ? 3005
+port = process.env.PORT ? process.argv.splice(2)[0] ? 3005
 app = express()

 app.configure ->


Minor refactor, added CLI test cases, updating documentation

--- test/cliSpec.coffee
+++ test/cliSpec.coffee
@@ -170,6 +170,3 @@

       it 'should be removed the host', ->
         expect(auth.find('my.site.org').exists()).to.be.false
-
-  # todo
-


Metered changing max attribute

Metered changing max attribute

--- test/metered.coffee
+++ test/metered.coffee
@@ -10,7 +10,7 @@
 test "should respect 0 being set as the meter attribute", ->
   obj = GameObject
     health: 0
-    maxHealth: 110
+    healthMax: 110

   obj.include Metered


movable velocity and acceleration getters

--- test/movable.coffee
+++ test/movable.coffee
@@ -32,7 +32,6 @@

   ok object.acceleration()

-# TODO fix me
 test "should increase velocity according to acceleration", ->
   particle = GameObject
     velocity: Point(7, 4)


syncstring:  fix race issue with caching syncdb strings.

--- salvus/syncstring.coffee
+++ salvus/syncstring.coffee
@@ -874,6 +874,7 @@
 x={};require('bup_server').global_client(cb:(e,c)->x.c=c; x.s = require('syncstring'); x.s.init_syncstring_db(x.c.database); x.ss=x.s.syncdb(string_id:'c26db83a-7fa2-44a4-832b-579c18fac65f', cb:(e,t)->x.t=t))
 ###
 _syncdb_cache = {}
+_syncdb_callbacks = {}
 exports.syncdb = (opts) ->
     opts = defaults opts,
         string_id      : required
@@ -883,21 +884,27 @@
     if d?
         opts.cb(undefined, d)
         return
+    x = _syncdb_callbacks[opts.string_id]
+    if x?
+        x.push(opts.cb)
+        return
+    _syncdb_callbacks[opts.string_id] = [opts.cb]
     get_syncstring
         string_id  : opts.string_id
         max_length : opts.max_length
         cb         : (err, S) =>
-            if err
-                opts.cb(err)
-            else
+            if not err
                 doc = new diffsync.SynchronizedDB_DiffSyncWrapper(S.in_memory_client)
                 S.db_client.on 'changed', () =>
                     doc.emit("sync")
                 d = _syncdb_cache[opts.string_id] = new diffsync.SynchronizedDB(doc)
                 d.string_id = opts.string_id
-                opts.cb(undefined, d)
-
-
+            for cb in _syncdb_callbacks[opts.string_id]
+                if err
+                    cb(err)
+                else
+                    cb(undefined, d)
+            delete _syncdb_callbacks[opts.string_id]

 ######################################################################
 # Microservice API
@@ -1147,16 +1154,17 @@
                     if listeners[socket.id]?
                         s.removeListener('change', listeners[socket.id])
                     f = (changes) =>
-                        @dbg("syncdb_listen(string_id=#{mesg.string_id})", "telling socket.id=#{socket.id}) that got changes #{misc.to_json(changes)}")
+                        #@dbg("syncdb_listen(string_id=#{mesg.string_id})", "telling socket.id=#{socket.id}) that got changes #{misc.to_json(changes)}")
                         @send_mesg
                             socket : socket
                             mesg   :
                                 event     : 'syncdb_change'
                                 string_id : mesg.string_id
                                 changes   : changes
-                    s.on('change', f)
+                    s.addListener('change', f)
+                    @dbg("syncdb_listen", "now we have this many listeners: #{s.listeners('change').length}")
                     listeners[socket.id] = f
-                    @dbg("syncdb_listen(string_id=#{mesg.string_id})", "now listening to these sockets: #{misc.to_json(misc.keys(listeners))}")
+                    #@dbg("syncdb_listen(string_id=#{mesg.string_id})", "now listening to these sockets: #{misc.to_json(misc.keys(listeners))}")
                     resp = message.success()
                 resp.id = mesg.id
                 @send_mesg


removing recent changes v2

removing recent changes v2

--- scripts/TM-Github.coffee
+++ scripts/TM-Github.coffee
@@ -41,7 +41,7 @@
     request.post options, (error, response, data)->
       res.reply 'Request sent, take a look at https://ci.appveyor.com/project/DinisCruz/tm-4-0-build where a new build should have just started'
       res.reply 'In a bit, the compiled node_modules will be at the https://github.com/tm-build/TM_4_0_Windows repo'
-  ###
+
   robot.respond /deploy to azure/i,(res) ->
     res.reply "About to trigger a deploy to azure..."
     options =
@@ -55,10 +55,6 @@
       res.reply 'Request sent, take a look at https://ci.appveyor.com/project/DinisCruz/tm-ci-deploy where a new build will have just started'
       res.reply 'In a bit, the deployed website will be at https://tm-qa-6.azurewebsites.net'

-  robot.hear 'TM_4_0_Design.Dev(.*)commits', (res)->
-    res.reply 'New commit to TM_4_0_Design/Dev detected ...'
-    res.reply '...time to deploy to azure'
-  ###
   robot.hear /show(.*)tm graph/i,(res) ->
     res.reply 'http://4.bp.blogspot.com/-7g82Ltxt7wI/VLqbd1CLOyI/AAAAAAAAKcc/kisGogPKcmo/s1600/Screen%2BShot%2B2014-11-02%2Bat%2B16.49.16.png'
     res.reply 'http://2.bp.blogspot.com/-PUi2erHHCKA/VLqbmSw69pI/AAAAAAAAKeY/fWA8nqSam8k/s1600/Screen%2BShot%2B2014-11-18%2Bat%2B10.57.13.png'


Reload stack template list

Reload stack template list

--- client/stacks/lib/views/stacks/basestacktemplatesview.coffee
+++ client/stacks/lib/views/stacks/basestacktemplatesview.coffee
@@ -126,5 +126,6 @@

     @defineStackView.on [ 'Cancel', 'Completed' ], =>
       @defineStackView.destroy()
+      @initialView.reload()
       @initialView.show()
       @setRoute()


fixed evidence bottom view so it is compatible with comments again

--- core/app/backbone/views/evidence/evidence_bottom_view.coffee
+++ core/app/backbone/views/evidence/evidence_bottom_view.coffee
@@ -17,7 +17,7 @@
     @bindTo @model, 'change', @render, @

   templateHelpers: ->
-    fact = @model.getFact()
+    fact = @model.getFact?()

     showTime: false
     showRepost: false
@@ -30,8 +30,8 @@
     fact_url_host: ->
       if @fact_base?.fact_url?
         new Backbone.Factlink.Url(@fact_base?.fact_url).host()
-    believe_percentage: fact.opinionPercentage('believe')
-    disbelieve_percentage: fact.opinionPercentage('disbelieve')
+    believe_percentage: fact?.opinionPercentage('believe')
+    disbelieve_percentage: fact?.opinionPercentage('disbelieve')

   onRender: ->
     @bindTo @model, 'change:sub_comments_count', @updateSubCommentsLink, @


bug fix

bug fix

--- lib/generators/webrtc_rails/install/templates/main.js.coffee
+++ lib/generators/webrtc_rails/install/templates/main.js.coffee
@@ -72,7 +72,7 @@
   _sendMessage: (message) ->
     $.ajax(
       type: 'POST'
-      url: 'webrtc'
+      url: '/webrtc'
       data:
         user_id: @remoteUserID
         message: message
@@ -190,6 +190,7 @@
     @_peerConnection.setRemoteDescription(new RTCSessionDescription(event))

   _stop: ->
+    @_peerConnection.removeStream(@_peerConnection.getRemoteStreams()[0])
     @_peerConnection.close()
     @_peerConnection = null
     @_peerStarted = false


fixed NPE

fixed NPE

--- manager/app/assets/javascripts/inventory/inventory.js.coffee
+++ manager/app/assets/javascripts/inventory/inventory.js.coffee
@@ -37,7 +37,14 @@
       models:     { hosts: Bixby.model.HostList, query: "" }

       load_data: (data) ->
-        @query = data.query || data.params.query
+
+        if data.query
+          @query = data.query
+        else if data.params
+          @query = data.params.query
+        else
+          @query = ""
+
         @app.trigger("search:set_query", @query)
         @hosts = new Bixby.model.HostList()
         @hosts.query = @query


fix for slugifying international characters

--- @client/shared.coffee
+++ @client/shared.coffee
@@ -769,7 +769,7 @@
     .replace(/-+$/, '')             # Trim - from end of text

   if text?.length > 0 && slug?.length == 0
-    slug = md5 slug
+    slug = md5 text

   slug


try setting current local a different way

--- app.coffee
+++ app.coffee
@@ -41,12 +41,14 @@
   app.use passport.initialize()
   app.use passport.session()
   app.use (req, res, next) ->
-    res.locals.locale = i18n.setLocale = req.locale
+    currentLocal = req.locale
     if req.user and req.user.language
-      res.locals.locale = i18n.setLocale = req.user.language
+      currentLocal = req.user.language
     if req.query.lang
       supported = new locale.Locales(config.languages)
-      res.locals.locale = i18n.setLocale = new locale.Locales(req.query.lang).best(supported)
+      currentLocal = new locale.Locales(req.query.lang).best(supported)
+    res.locals.locale = currentLocal
+    req.setLocale = currentLocal
     next()
   app.locals
     __i: i18n.__


Default to 64bit flash player now

Default to 64bit flash player now

--- app.coffee
+++ app.coffee
@@ -52,11 +52,11 @@
     app.commandLine.appendSwitch 'ppapi-flash-version', '19.0.0.219'
 else if process.platform == 'win32'
   try
-    fs.accessSync path.join(EXECROOT, 'PepperFlash', 'win32', 'pepflashplayer32.dll')
-    app.commandLine.appendSwitch 'ppapi-flash-path', path.join(EXECROOT, 'PepperFlash', 'win32', 'pepflashplayer32.dll')
+    fs.accessSync path.join(EXECROOT, 'PepperFlash', 'win32', 'pepflashplayer.dll')
+    app.commandLine.appendSwitch 'ppapi-flash-path', path.join(EXECROOT, 'PepperFlash', 'win32', 'pepflashplayer.dll')
     app.commandLine.appendSwitch 'ppapi-flash-version', '19.0.0.219'
   catch e
-    app.commandLine.appendSwitch 'ppapi-flash-path', path.join(ROOT, 'PepperFlash', 'win32', 'pepflashplayer32.dll')
+    app.commandLine.appendSwitch 'ppapi-flash-path', path.join(ROOT, 'PepperFlash', 'win32', 'pepflashplayer.dll')
     app.commandLine.appendSwitch 'ppapi-flash-version', '19.0.0.219'
 else if process.platform == 'darwin'
   try


Wrapped haki user exec() calls into hakiExec, got rid of hakisetEnv

--- app.coffee
+++ app.coffee
@@ -2,7 +2,7 @@
 async = require('async')
 request = require('request')
 posix = require('posix')
-{exec,spawn} = require('child_process')
+{exec, spawn} = require('child_process')

 STATE_FILE = '/opt/ewa-client-bootstrap/state.json'
 API_ENDPOINT = 'http://paras.rulemotion.com:1337'
@@ -47,27 +47,39 @@
 		)
 ]

-setHakiEnv = (callback) ->
-	process.setuid(posix.getpwnam('haki').uid)
-	process.chdir(HAKI_PATH)
-	callback()
+hakiExec = (command, options, callback) ->
+	options.uid = posix.getpwnam('haki').uid
+
+	ps = spawn(process.env.SHELL, ['-c', command], options)
+	stdout = ''
+	stderr = ''
+	ps.stdout.on('data', (chunk) ->
+		stdout += chunk
+	)
+	ps.stderr.on('data', (chunk) ->
+		stderr += chunk
+	)
+	ps.on('exit', (error) -> callback(error, stdout, stderr))
+	ps.on('error', (error) -> callback(error, stdout, stderr))

 stage1Tasks = [
+	# superuser tasks
 	(callback) -> async.waterfall(bootstrapTasks, callback)
 	(callback) -> fs.writeFileSync(STATE_FILE, JSON.stringify(state)) ; callback()
 	(callback) -> exec('systemctl start openvpn@client', callback)
 	(callback) -> exec('systemctl enable openvpn@client', callback)
-	setHakiEnv
-	(callback) -> fs.mkdir('hakiapp', callback)
-	(callback) -> exec('git init', cwd: 'hakiapp', callback)
-	(callback) -> exec("git remote add origin #{state.gitUrl}", cwd: 'hakiapp', callback)
+	# haki user tasks
+	(callback) -> hakiExec('mkdir hakiapp', cwd: '/home/haki', callback)
+	(callback) -> hakiExec('git init', cwd: '/home/haki/hakiapp', callback)
+	(callback) -> hakiExec("git remote add origin #{state.gitUrl}", cwd: '/home/haki/hakiapp', callback)
+	# done
 	(callback) -> console.log('Bootstrapped') ; callback()
 ]

 updateRepo = (callback) ->
 	tasks1 = [
-		(callback) -> exec('git pull origin master', cwd: 'hakiapp', callback)
-		(stdout, stderr, callback) -> exec('git rev-parse HEAD', cwd: 'hakiapp', callback)
+		(callback) -> hakiExec('git pull origin master', cwd: '/home/haki/hakiapp', callback)
+		(stdout, stderr, callback) -> hakiExec('git rev-parse HEAD', cwd: '/home/haki/hakiapp', callback)
 		(stdout, stderr, callback) -> callback(null, stdout.trim())
 	]

@@ -114,7 +126,6 @@
 	)

 stage2Tasks = [
-	setHakiEnv
 	(callback) -> async.forever(updateRepo, callback)
 ]


handle SIGTERM in filestore

handle SIGTERM in filestore

--- app.coffee
+++ app.coffee
@@ -126,3 +126,7 @@

 server.listen port, host, ->
 	logger.log("filestore store listening on #{host}:#{port}")
+
+process.on 'SIGTERM', () ->
+	logger.log("filestore got SIGTERM, shutting down gracefully")
+	beginShutdown()


karma

karma

--- bot.coffee
+++ bot.coffee
@@ -95,6 +95,7 @@
       async.series [
         (cb) -> db.run "CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT)", cb
         (cb) -> db.run "CREATE TABLE factoids (key TEXT PRIMARY KEY, value TEXT, last_edit TEXT)", cb
+        (cb) -> db.run "CREATE TABLE karma (key TEXT PRIMARY KEY, value INTEGER)", cb
         (cb) -> db.run "INSERT INTO metadata VALUES($key, $value)", {$key: key, $value: value}, cb
         (cb) -> db.run "INSERT INTO metadata VALUES('direct', 'no')", cb
         (cb) -> db.run "INSERT INTO metadata VALUES('ambient', 'yes')", cb
@@ -156,6 +157,23 @@
     log.info "Set factoid key #{key} for team #{team}"
     return cb null

+getKarma = (team, key, cb) ->
+  db = getDatabase team
+  if not db? then return cb "Couldn't get database for team #{team}"
+
+  db.get "SELECT value FROM karma WHERE key = $key", {$key: key}, (err, row) ->
+    if err then return cb "Couldn't get karma for team #{team}: #{err}"
+    return cb null, row?.value or null
+
+setKarma = (team, key, value, cb) ->
+  db = getDatabase team
+  if not db? then return cb "Couldn't get database for team #{team}"
+
+  db.run "INSERT OR REPLACE INTO karma VALUES($key, $value)", {$key: key, $value: value}, (err) ->
+    if err then return cb "Couldn't update karma for team #{team}: #{err}"
+    log.info "Set factoid key #{key} for team #{team}"
+    return cb null
+

 # BOTS ---------------------------------------------------------------------#{{{1

@@ -260,13 +278,13 @@
   reply = (text) -> bot.reply {channel: channel}, {text: text}

   parseAndReply = (value) ->
-    value = oneOf(value.split(/\s+or\s+/i)).trim()
+    value = oneOf(value.split(/\s+|\s+/i)).trim()

     isReply = (/^<reply>\s*/i).test(value)
     value = value.replace(/^<reply>\s*/i, '') if isReply

-    isEmote = (/^\/me\s+/i).test(value)
-    value = value.replace(/^\/me\s+/i, '') if isEmote
+    isEmote = (/^<action>\s+/i).test(value)
+    value = value.replace(/^<action>\s+/i, '') if isEmote

     value = value.replace(/\$who/ig, sender)

@@ -350,7 +368,10 @@
         update key, value

       else if current and isAppending
-        value = "#{current} or #{value}"
+        if /^|/.test value
+          value = "#{current}#{value}"
+        else
+          value = "#{current} or #{value}"
         update key, value

       else if current == value
@@ -362,6 +383,51 @@
       else
         update key, value

+    return
+
+  # Karma query
+  else if (/^karma\s+for\s+/i).test(msg)
+    key = msg.replace(/^karma\s+for\s+/i, '').replace(/\?+$/, '')
+    getKarma team, key, (err, current) ->
+      current or= 0
+      if err
+        log.error err
+        if isVerbose then reply "There was an error getting the karma. Please try again."
+      else
+        reply "Karma for #{key} is #{current}"
+      return
+    return
+
+  # Karma increment/decrement
+  else if /\+\+(\s#.+)?$/.test(msg)
+    if isDirect then return reply "You cannot secretly change the karma for something!"
+    key = msg.split(/\+\+/)[0]
+    getKarma team, key, (err, current) ->
+      if err then return log.error(err)
+      value = Number(current or 0)
+      value++
+      setKarma team, key, value, (err) ->
+        if err
+          log.error err
+          if isVerbose then reply "There was an error changing the karma. Please try again."
+        return
+      return
+    return
+
+  # Karma decrement
+  else if /\-\-(\s#.+)?$/.test(msg)
+    if isDirect then return reply "You cannot secretly change the karma for something!"
+    key = msg.split(/\-\-/)[0]
+    getKarma team, key, (err, current) ->
+      if err then return log.error(err)
+      value = Number(current or 0)
+      value--
+      setKarma team, key, value, (err) ->
+        if err
+          log.error err
+          if isVerbose then reply "There was an error changing the karma. Please try again."
+        return
+      return
     return

   # Getting regular factoids, last chance {{{3


Fixed activity graph interpolation

Fixed activity graph interpolation

--- static/js/activity_scores.coffee
+++ static/js/activity_scores.coffee
@@ -14,11 +14,11 @@
 zerofiller = (xrng) -> (items) ->
     for x, i in xrng
         y = 0
-        ix = items[0]?.x
+        ix = items[0]?[0]
         if ix <= x and not (ix >= xrng[i+1])
-            y = items[0].y
+            y = items[0][1]
             items = items[1..]
-        x: x, y: y
+        [x, y]

@@ -240,7 +240,7 @@

         filler = zerofiller getMonths @time_range.start, @time_range.end
         mangle = (data) ->
-            for [x, y] in data
+            for [x, y] in filler(data)
                 x: dateToEpoch(x), y: y
         @member_series.data = mangle @plot_series[0].data


Use View's native subscribeEvent method

Use View's native subscribeEvent method

I will always read documentation
I will always read documentation
I will always read documentation

--- coffee/views/menu.coffee
+++ coffee/views/menu.coffee
@@ -18,7 +18,7 @@

     initialize: ->
       super
-      Chaplin.mediator.subscribe "!router:changeURL", (url) =>
+      @subscribeEvent "!router:changeURL", (url) ->
         $("#common_menu>li").removeClass("active")
         a = $("#common_menu a[href='/#{url}']")
         if a.length


improved view

improved view

--- views/coffeescript/main.coffee
+++ views/coffeescript/main.coffee
@@ -79,10 +79,19 @@
   constructor: (@sys) ->
     @nodes = {}
     @rels = {}
+    @preferredKeys = [/name/i, /type/i]
     @view = (data) ->
-      for key, value of data
-        console.log "#{key}: #{value}"
-        "#{key}: #{value}"
+      text = []
+      for regex in @preferredKeys
+        for key, value of data
+          if key.match(regex)
+            text.push "#{key}: #{value}"
+      if text.length < 1
+        for key, value of data
+          text.push "#{key}: #{value}"
+        text = text[0..1]
+      console.log text.join('\n'), text.length
+      text.join('\n')

   setView: (@view) ->

@@ -111,11 +120,7 @@

   data = {
     "nodes": [
-      {"data": {
-        "_neo_id":0,
-        "gemFile": "source :rubygems\ngem",
-        "id":0
-      }},
+      {"data": { "_neo_id":0, "gemFile": "source :rubygems\ngem"}, "id":0 },
       {"data":{"_count__all__classname":53, "_neo_id":5}, "id":5},
       {"data":{"_count__all__classname":148, "_neo_id":1}, "id":1},
       {"data":{"_count__all__classname":10, "_neo_id":2}, "id":2},


avoid shadowing parameters

avoid shadowing parameters

--- app/assets/javascripts/angular/directives/modal-dialog.coffee
+++ app/assets/javascripts/angular/directives/modal-dialog.coffee
@@ -8,8 +8,8 @@
       replace: true
       scope: true
       templateUrl: "views/partials/modal-dialog.html"
-      link: (scope, element) ->
-        element = $(element)
+      link: (scope, theelement) ->
+        element = $(theelement)
         retVal = undefined

         modalService.on("openDialog", (event, options) ->
@@ -32,7 +32,6 @@
             )
           )
         )
-
         modalService.on("closeDialog", () ->
           retVal = false
           element.modal("hide")


Update password reset integration tests

Update password reset integration tests

To look for quickcode.io, not scraperwiki.com.

--- test/integration/password_reset.coffee
+++ test/integration/password_reset.coffee
@@ -133,5 +133,5 @@
         it 'it tells me something unexpected went wrong', (done) ->
           wd40.waitForText 'Something went wrong', done

-        it 'it tells me to email hello@scraperwiki.com', (done) ->
-          wd40.waitForText 'hello@scraperwiki.com', done
+        it 'it tells me to email hello@quickcode.io', (done) ->
+          wd40.waitForText 'hello@quickcode.io', done


add rendered markdown routes

add rendered markdown routes

--- server/app/routes.coffee
+++ server/app/routes.coffee
@@ -3,6 +3,7 @@
 jwt = require 'jsonwebtoken'
 nodemailer = require 'nodemailer'
 crypto = require 'crypto'
+markdown = require('markdown').markdown
 utils = require './utils'
 User = require('./models/user').User
 Page = require('./models/page').Page
@@ -72,6 +73,14 @@
       if err
        res.send(500).end()
       else
+        res.json data
+
+  app.get '/data/blog/post/render/:route.json', (req, res) ->
+    Post.findOne {'id': req.params.id}, '-_id -__v', (err, data) ->
+      if err
+       res.send(500).end()
+      else
+        data.body = markdown.toHTML data.body
         res.json data

   app.post '/data/blog/post/', expressJwt({secret: secret}), (req, res) ->
@@ -244,6 +253,14 @@
       if err
        res.send(500).end()
       else
+        res.json data
+
+  app.get '/data/page/render/:route.json', (req, res) ->
+    Page.findOne {route: req.params.route}, '-_id -__v', (err, data) ->
+      if err
+       res.send(500).end()
+      else
+        data.body = markdown.toHTML data.body
         res.json data

   app.post '/data/page/', expressJwt({secret: secret}), (req, res) ->


allow graceful shutdown of server

allow graceful shutdown of server

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -575,8 +575,20 @@
   fs.unlinkSync port

 # Start Server
-app.listen port, ->
+server = app.listen port, ->
   if existsSync(port)
     fs.chmodSync port, 0o600
     child_process.exec "chown www-data #{port}"
   console.log "Listening on #{port}\nPress CTRL-C to stop server."
+
+# Wait for all connections to finish before quitting
+process.on 'SIGTERM', ->
+  console.log "Gracefully stopping..."
+  server.close ->
+    console.log "All connections finished, exiting"
+    process.exit()
+
+  setTimeout ->
+    console.error "Could not close connections in time, forcefully shutting down"
+    process.exit 1
+  , 30*1000


Sim: Shorten timer to 3m, +15s per move

Sim: Shorten timer to 3m, +15s per move

--- server/conditions.coffee
+++ server/conditions.coffee
@@ -218,9 +218,9 @@
         @tell(Protocol.PAUSE_TIMER, @getPlayerIndex(id))

   extend:
-    DEFAULT_TIMER: 5 * 60 * 1000  # five minutes
-    TIMER_PER_TURN_INCREASE: 20 * 1000  # twenty seconds
-    TIMER_CAP: 5 * 60 * 1000  # five minutes
+    DEFAULT_TIMER: 3 * 60 * 1000  # three minutes
+    TIMER_PER_TURN_INCREASE: 15 * 1000  # fifteen seconds
+    TIMER_CAP: 3 * 60 * 1000  # three minutes
     TEAM_PREVIEW_TIMER: 1.5 * 60 * 1000  # 1 minute and 30 seconds

     startTimer: (msecs) ->


More class conversion

More class conversion

--- resources/assets/coffee/react/profile-page/main.coffee
+++ resources/assets/coffee/react/profile-page/main.coffee
@@ -19,10 +19,17 @@
 {div, h2, li, ul} = React.DOM
 el = React.createElement

-ProfilePage.Main = React.createClass
-  mixins: [StickyTabsMixin, ScrollingPageMixin]
-
-  getInitialState: ->
+pages = document.getElementsByClassName("js-switchable-mode-page--scrollspy")
+pagesOffset = document.getElementsByClassName("js-switchable-mode-page--scrollspy-offset")
+
+currentLocation = ->
+  "#{document.location.pathname}#{document.location.search}"
+
+
+class ProfilePage.Main extends React.PureComponent
+  constructor: (props) ->
+    super props
+
     savedStateString = document.body.dataset.profilePageState
     if savedStateString?
       return JSON.parse(savedStateString)
@@ -30,24 +37,26 @@
     optionsHash = ProfilePageHash.parse location.hash
     @initialPage = optionsHash.page

-    currentMode: @validMode(optionsHash.mode ? @props.user.playmode)
-    user: @props.user
-    userPage:
-      html: @props.userPage.html
-      initialRaw: @props.userPage.raw
-      raw: @props.userPage.raw
-      editing: false
-      selection: [0, 0]
-    tabsSticky: false
-    profileOrder: @props.user.profileOrder[..]
-
-
-  componentDidMount: ->
+    @state =
+      currentMode: @validMode(optionsHash.mode ? props.user.playmode)
+      user: props.user
+      userPage:
+        html: props.userPage.html
+        initialRaw: props.userPage.raw
+        raw: props.userPage.raw
+        editing: false
+        selection: [0, 0]
+      tabsSticky: false
+      profileOrder: props.user.profileOrder[..]
+
+
+  componentDidMount: =>
     $.subscribe 'user:update.profilePage', @userUpdate
     $.subscribe 'user:page:update.profilePage', @userPageUpdate
     $.subscribe 'playmode:set.profilePage', @setCurrentMode
     $.subscribe 'profile:page:jump.profilePage', @pageJump
     $.subscribe 'stickyHeader.profilePage', @_tabsStick
+    $(window).on 'throttled-scroll.profilePage', @pageScan

     $(@refs.pages).sortable
       cursor: 'move'
@@ -67,11 +76,13 @@

     osu.pageChange()

+    @modeScrollUrl = currentLocation()
+
     Timeout.set 0, =>
       @pageJump null, @initialPage

-  componentWillUnmount: ->
+  componentWillUnmount: =>
     $.unsubscribe '.profilePage'
     $(window).off '.profilePage'

@@ -80,8 +91,11 @@

     document.body.dataset.profilePageState = JSON.stringify(@state)

-
-  render: ->
+    $(window).stop()
+    Timeout.clear @modeScrollTimeout
+
+
+  render: =>
     rankHistories = @props.allRankHistories[@state.currentMode]
     stats = @props.allStats[@state.currentMode]
     scores = @props.allScores[@state.currentMode]
@@ -180,7 +194,12 @@
             @extraPage name, extraPageParams[name]

-  extraPage: (name, {extraClass, props, component}) ->
+  _tabsStick: (_e, target) =>
+    newState = (target == 'page-extra-tabs')
+    @setState(tabsSticky: newState) if newState != @state.tabsSticky
+
+
+  extraPage: (name, {extraClass, props, component}) =>
     topClassName = 'js-switchable-mode-page--scrollspy js-switchable-mode-page--page'
     props.withEdit = @props.withEdit
     props.name = name
@@ -192,12 +211,78 @@
       el component, props

-  setCurrentMode: (_e, {mode}) ->
+  pageJump: (_e, page) =>
+    if page == 'main'
+      @setCurrentPage null, page
+      return
+
+    target = $(".js-switchable-mode-page--page[data-page-id='#{page}']")
+
+    # if invalid page is specified, scan current position
+    if target.length == 0
+      @pageScan()
+      return
+
+    # Don't bother scanning the current position.
+    # The result will be wrong when target page is too short anyway.
+    @scrolling = true
+    Timeout.clear @modeScrollTimeout
+
+    $(window).stop().scrollTo target, 500,
+      onAfter: =>
+        # Manually set the mode to avoid confusion (wrong highlight).
+        # Scrolling will obviously break it but that's unfortunate result
+        # from having the scrollspy marker at middle of page.
+        @setCurrentPage null, page, =>
+          # Doesn't work:
+          # - part of state (callback, part of mode setting)
+          # - simple variable in callback
+          # Both still change the switch too soon.
+          @modeScrollTimeout = Timeout.set 100, => @scrolling = false
+      # count for the tabs height
+      offset: pagesOffset[0].getBoundingClientRect().height * -1
+
+
+  pageScan: =>
+    return if @modeScrollUrl != currentLocation()
+
+    return if @scrolling
+    return if pages.length == 0
+
+    anchorHeight = pagesOffset[0].getBoundingClientRect().height
+
+    if osu.bottomPage()
+      @setCurrentPage null, _.last(pages).dataset.pageId
+      return
+
+    for page in pages
+      pageDims = page.getBoundingClientRect()
+      pageBottom = pageDims.bottom - Math.min(pageDims.height * 0.75, 200)
+      continue unless pageBottom > anchorHeight
+
+      @setCurrentPage null, page.dataset.pageId
+      return
+
+    @setCurrentPage null, page.dataset.pageId
+
+
+  setCurrentMode: (_e, {mode}) =>
     return if @state.currentMode == mode
     @setState currentMode: @validMode(mode)

-  updateOrder: (event) ->
+  setCurrentPage: (_e, page, extraCallback) =>
+    callback = =>
+      extraCallback?()
+      @setHash?()
+
+    if @state.currentPage == page
+      return callback()
+
+    @setState currentPage: page, callback
+
+
+  updateOrder: (event) =>
     $elems = $(event.target)

     newOrder = $elems.sortable('toArray', attribute: 'data-page-id')
@@ -225,17 +310,17 @@
       .always LoadingOverlay.hide

-  userUpdate: (_e, user) ->
+  userUpdate: (_e, user) =>
     return if !user?
     @setState user: user

-  userPageUpdate: (_e, newUserPage) ->
+  userPageUpdate: (_e, newUserPage) =>
     currentUserPage = _.cloneDeep @state.userPage
     @setState userPage: _.extend(currentUserPage, newUserPage)

-  validMode: (mode) ->
+  validMode: (mode) =>
     modes = BeatmapHelper.modes

     if _.includes(modes, mode)


:bangbang::rice_cracker: Updated in browser at strd6.github.io/editor

--- pixie.cson
+++ pixie.cson
@@ -1,4 +1,4 @@
-version: "0.4.4-pre.0"
+version: "0.5.0-pre.0"
 entryPoint: "main"
 remoteDependencies: [
   "https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.7.1/coffee-script.min.js"


ComponentLibrary: Also include name string

Refs #63

--- microflo.coffee
+++ microflo.coffee
@@ -87,7 +87,8 @@

 generateFactory = (name) ->
     return """static Component *create() { return new #{name}(); }
-    static const MicroFlo::ComponentId id = DefaultComponentLibrary.add(create);
+    static const char * const name = "#{name}";
+    static const MicroFlo::ComponentId id = DefaultComponentLibrary.add(create, name);
     """

 generateComponent = (lib, name, sourceFile) ->


fix: quick fix to correct build

fix: quick fix to correct build

--- frontend/controllers/SeasonManageController.coffee
+++ frontend/controllers/SeasonManageController.coffee
@@ -9,8 +9,9 @@
     $scope.seasonOwner = ->
       season?.owner?.username is User.username
     $scope.createSeasonGrouping = (groupingName) ->
-      $http.post("/api/match/seasons/#{season.id}/match-groups", { name: groupingName })
-        .success (newGroup) ->
+      $http.post("/api/match/seasons/#{season.id}/match-groups", {
+        name: groupingName
+      }).success (newGroup) ->
           toastr.success "Grouping successfully created"
         .error ->
     $scope.addSeasonMember = (newMember) ->


Remove unused NormalizePolymorphic mixin.

--- app/assets/javascripts/mixins/normalize_polymorphic.js.coffee
+++ app/assets/javascripts/mixins/normalize_polymorphic.js.coffee
@@ -1,9 +0,0 @@
-ETahi.NormalizePolymorphic = Ember.Mixin.create
-  normalize: (type, hash, prop) ->
-    if ETahi.taskTypes
-      taskObjs = []
-      for taskId in hash.task_ids
-        taskObjs.push ETahi.taskTypes[taskId]
-      hash.tasks = taskObjs
-      delete hash.task_ids
-    @_super(type, hash, prop)


would returning promises in beforeModel hook help with properly binding shipment associations?

--- app/assets/javascripts/trade/routes/shipments_route.js.coffee
+++ app/assets/javascripts/trade/routes/shipments_route.js.coffee
@@ -1,11 +1,19 @@
 Trade.ShipmentsRoute = Ember.Route.extend

   beforeModel: ->
-    @controllerFor('geoEntities').load()
-    @controllerFor('terms').set('content', Trade.Term.find())
-    @controllerFor('units').set('content', Trade.Unit.find())
-    @controllerFor('sources').set('content', Trade.Source.find())
-    @controllerFor('purposes').set('content', Trade.Purpose.find())
+    (new Ember.RSVP.Promise((resolve) =>
+      resolve(@controllerFor('geoEntities').load())
+    )).then(new Ember.RSVP.Promise((resolve) =>
+      resolve(@controllerFor('terms').set('content', Trade.Term.find()))
+    )).then(new Ember.RSVP.Promise((resolve) =>
+      resolve(@controllerFor('units').set('content', Trade.Unit.find()))
+    )).then(new Ember.RSVP.Promise((resolve) =>
+      resolve(@controllerFor('sources').set('content', Trade.Source.find()))
+    )).then(new Ember.RSVP.Promise((resolve) =>
+      resolve(@controllerFor('purposes').set('content', Trade.Purpose.find()))
+    )).then(new Ember.RSVP.Promise((resolve) =>
+      resolve(Ember.run.sync())
+    ))

   model: (params, queryParams, transition) ->
     queryParams.page = 1 unless queryParams.page


parent prop isn't used

parent prop isn't used

--- resources/assets/coffee/react/comments-show/main.coffee
+++ resources/assets/coffee/react/comments-show/main.coffee
@@ -42,7 +42,6 @@
         div className: 'osu-page osu-page--comment',
           el Comment,
             comment: comment
-            parent: store.comments.get(comment.parent_id)
             showCommentableMeta: true
             depth: 0
             linkParent: true


Only try scrolling if target is one of the pages

--- resources/assets/coffee/react/profile-page/extra.coffee
+++ resources/assets/coffee/react/profile-page/extra.coffee
@@ -62,7 +62,7 @@
     anchorHeight = window.innerHeight * 0.5

     if osu.bottomPage()
-      @_setMode _.last(pages).id
+      @_setMode _.last(pages).dataset.id
       return

     # FIXME: I don't remember why this one scans from bottom while
@@ -71,10 +71,10 @@
       pageTop = page.getBoundingClientRect().top
       continue unless pageTop <= anchorHeight

-      @_setMode page.id
+      @_setMode page.dataset.id
       return

-    @_setMode page.id
+    @_setMode page.dataset.id

   _modeSwitch: (_e, mode) =>
@@ -82,7 +82,11 @@
     # The result will be wrong when target page is too short anyway.
     @_scrolling = true

-    $(window).stop().scrollTo "##{mode}", 500,
+    target = @refs["page-#{mode}"]
+
+    return unless target
+
+    $(window).stop().scrollTo target, 500,
       onAfter: =>
         # Manually set the mode to avoid confusion (wrong highlight).
         # Scrolling will obviously break it but that's unfortunate result
@@ -114,7 +118,7 @@

   updateOrder: (element) =>
     oldOrder = @state.profileOrder
-    newOrder = $(@refs.pages).sortable('toArray')
+    newOrder = $(@refs.pages).sortable('toArray', attribute: 'data-id')

     id = element.attr 'id'

@@ -219,6 +223,7 @@

           div
             key: m
-            id: m
+            ref: "page-#{m}"
+            'data-id': m
             className: topClassName
             el elem, props


Update zh-HANS.coffee (#5736)

Update zh-HANS.coffee (#5736)

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -1,9 +1,9 @@
 module.exports = nativeDescription: "简体中文", englishDescription: "Chinese (Simplified)", translation:

   new_home:
-    title: "CodeCombat - 硅谷原创AP官方权威认证青少年编程平台"
-    meta_keywords: "青少年编程, 少儿编程, CodeCombat, CodeCombat中国, steam教育, python, javascript, 游戏化编程, 美国AP/CSP认证, AI世青赛技术支持平台"
-    meta_description: "CodeCombat中国官方教学版，美国大学理事会认证，国际等级证书，AI世青赛全球独家技术伙伴。"
+    title: "CodeCombat官方｜信息学奥赛｜AI世青赛｜编程等级认证"
+    meta_keywords: "青少年编程, 少儿编程, CodeCombat, CodeCombat中国, steam教育, python, javascript, 游戏化编程, 美国AP/CSP认证,AI世青赛,信息学奥赛"
+    meta_description: "CodeCombat中国唯一官方网站，支持Python, C++, JavaScript, 美国大学理事会（AP)认证，AI世青赛全球独家技术平台，支持中美信息学奥赛(NOI, CSP-J/S, USACO)、电子学会考试和国际青少年编程认证。"
     meta_og_url: "https://koudashijie.com"
     built_for_teachers_title: "一个以教师为中心的课程体系"
     built_for_teachers_blurb: "教孩子们写代码往往会觉得无从下手。 CodeCombat 旨在帮助所有的教育工作者传授学生学习和掌握当下最流行的两种计算机语言，JavaScript 和 Python。整个体系包含六门计算机科学课程，以及通过基于项目的游戏开发和网页开发单元巩固学习，教会孩子们一步步从基本语法到高级递归前进！"


Fixed ticket reload on server change.

Fixed ticket reload on server change.

--- app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
@@ -34,12 +34,16 @@
     @bind(
       'Ticket:update'
       (data) =>
-        update = =>
-          if data.id.toString() is @ticket_id.toString()
-            @log 'notice', 'TRY', new Date(data.updated_at), new Date(@ticketUpdatedAtLastCall)
-            if !@ticketUpdatedAtLastCall || ( new Date(data.updated_at).toString() isnt new Date(@ticketUpdatedAtLastCall).toString() )
-              @fetch( @ticket_id, false )
-        @delay( update, 1800, 'ticket-zoom-' + @ticket_id )
+
+        # check if current ticket has changed
+        if data.id.toString() is @ticket_id.toString()
+
+          # check if we already have the request queued
+          #@log 'notice', 'TRY', @ticket_id, new Date(data.updated_at), new Date(@ticketUpdatedAtLastCall)
+          update = =>
+            @fetch( @ticket_id, false )
+          if !@ticketUpdatedAtLastCall || ( new Date(data.updated_at).toString() isnt new Date(@ticketUpdatedAtLastCall).toString() )
+            @delay( update, 1800, 'ticket-zoom-' + @ticket_id )
     )

   meta: =>
@@ -264,7 +268,6 @@
           )

         show = (ticket) =>
-          console.log('SHOW', ticket.id)
           el.find('.edit').html('')

           defaults   = ticket.attributes()


Character doesn't come free

Character doesn't come free

--- app/assets/javascripts/backbone/views/channel_tabs.js.coffee
+++ app/assets/javascripts/backbone/views/channel_tabs.js.coffee
@@ -7,7 +7,7 @@
     @

   createChannel: (event)->
-    channelName = prompt("What's the channel name?", @randomChannelName())
+    channelName = prompt("What's the channel name?", Kandan.Views.ChannelTabs.randomChannelName())
     channelName = channelName.replace(/^\s+|\s+$/g, '')
     if channelName
       channel = new Kandan.Models.Channel({name: channelName})