express = require 'express'
stylus = require 'stylus'
assets = require 'connect-assets'
mongoose = require 'mongoose'

#### Basic application initialization
# Create app instance.
app = express()

# Define Port
app.port = process.env.PORT or process.env.VMC_APP_PORT or 3000

# Config module exports has `setEnvironment` function that sets app settings depending on environment.
config = require "./config"
app.configure 'production', 'development', 'testing', ->
	config.setEnvironment app.settings.env

# TODO: move to `config`
mongoose.connect 'mongodb://localhost/example'

#### View initialization
# Add Connect Assets.
app.use assets()
# Set the public folder as static assets.
app.use express.static(process.cwd() + '/public')

# Set View Engine.
app.set 'view engine', 'jade'

# [Body parser middleware](http://www.senchalabs.org/connect/middleware-bodyParser.html) parses JSON or XML bodies into `req.body` object
app.use express.bodyParser()

#### Finalization
# Initialize routes
routes = require './routes'
routes(app)

# Export application object
module.exports = app

express = require 'express'
stylus = require 'stylus'
assets = require 'connect-assets'
mongoose = require 'mongoose'

#### Basic application initialization
# Create app instance.
app = express()

# Define Port
app.port = process.env.PORT or process.env.VMC_APP_PORT or 3000

# Config module exports has `setEnvironment` function that sets app settings depending on environment.
config = require "./config"
app.configure 'production', 'development', 'testing', ->
	config.setEnvironment app.settings.env

db_config = "mongodb://#{config.DB_USER}:#{config.DB_PASS}@#{config.DB_HOST}:#{config.DB_PORT}/#{config.DB_NAME}"

mongoose.connect db_config

#### View initialization
# Add Connect Assets.
app.use assets()
# Set the public folder as static assets.
app.use express.static(process.cwd() + '/public')

# Set View Engine.
app.set 'view engine', 'jade'

# [Body parser middleware](http://www.senchalabs.org/connect/middleware-bodyParser.html) parses JSON or XML bodies into `req.body` object
app.use express.bodyParser()

#### Finalization
# Initialize routes
routes = require './routes'
routes(app)

# Export application object
module.exports = app

Read mongodb connection from config.

Read mongodb connection from config.


FactlinkApp.module "Overlay", (Overlay, FactlinkApp, Backbone, Marionette, $, _) ->
  $overlay_elements = $('

')
    .appendTo('body')

  $focus_el = null

  Overlay.show = ($el) ->
    $overlay_elements.fadeIn 'fast'

    $focus_el = $el
    $focus_el?.addClass 'overlay-focus'

  Overlay.hide = ->
    $overlay_elements.fadeOut 'fast'
    $focus_el?.removeClass 'overlay-focus'

FactlinkApp.module "Overlay", (Overlay, FactlinkApp, Backbone, Marionette, $, _) ->
  $overlay_elements = $('

')
    .appendTo('body')

  $focus_el = null

  Overlay.show = ($el) ->
    $overlay_elements.fadeIn 'fast'

    Overlay.removeFocus()
    $focus_el = $el
    $focus_el.addClass 'overlay-focus'

  Overlay.hide = ->
    Overlay.removeFocus()
    $overlay_elements.fadeOut 'fast'

  Overlay.removeFocus = ->
    $focus_el?.removeClass 'overlay-focus'

Remove focus when calling 'show' twice

Remove focus when calling 'show' twice


App.BudgetEditAssociations =

  set_text: (response)->
    $(".js-budget-show-administrators-list").text(response["administrators"])
    $(".js-budget-show-valuators-list").text(response["valuators"])
    $(".js-budget-show-trackers-list").text(response["trackers"])

  initialize: ->
    $(".js-budget-list-checkbox-user").on
      click: ->
        admin_count = $(".js-budget-list-checkbox-administrators:checkbox:checked").length
        valuator_count = $(".js-budget-list-checkbox-valuators:checkbox:checked").length
        tracker_count = $(".js-budget-list-checkbox-trackers:checkbox:checked").length
        budget = $(".js-budget-id").attr("id")
        url = "/admin/budgets/" + budget + "/assigned_users_translation.json"
        params = {administrators: admin_count, valuators: valuator_count, trackers: tracker_count}
        $.get(url, params, (response) -> App.BudgetEditAssociations.set_text response, "json")

    $(".js-budget-show-users-list").on
      click: ->
        div_id = $(this).data().toggle
        $(".js-budget-users-list").each ->
          if this.id != div_id
            $(this).addClass("is-hidden") if !$(this).hasClass("is-hidden")

App.BudgetEditAssociations =

  set_text: (response)->
    $(".js-budget-show-administrators-list").text(response["administrators"])
    $(".js-budget-show-valuators-list").text(response["valuators"])
    $(".js-budget-show-trackers-list").text(response["trackers"])

  initialize: ->
    $(".js-budget-list-checkbox-user").on
      click: ->
        admin_count = $(".js-budget-list-checkbox-administrators:checkbox:checked").length
        valuator_count = $(".js-budget-list-checkbox-valuators:checkbox:checked").length
        tracker_count = $(".js-budget-list-checkbox-trackers:checkbox:checked").length
        budget = $(".js-budget-id").attr("id")
        url = "/admin/budgets/" + budget + "/assigned_users_translation.json"
        params = {administrators: admin_count, valuators: valuator_count, trackers: tracker_count}
        $.get(url, params, (response) -> App.BudgetEditAssociations.set_text response, "json")

    $(".js-budget-show-users-list").on
      click: ->
        div_id = $(this).data().toggle
        $(".js-budget-users-list").each ->
          if this.id != div_id && !$(this).hasClass("is-hidden")
            $(this).addClass("is-hidden")

Make conditions easier to read

Make conditions easier to read

Combining both the inline and "traditional" `if` styles made the code
difficult to read, particularly when compiled to JavaScript.


moduleFor 'view:paperEdit', 'Unit: paperEditView',
  teardown: ->
    ETahi.VisualEditorService.create.restore()
    ETahi.reset()

  setup: ->
    paper = Ember.Object.create
      title: ''
      shortTitle: 'Does not matter'
      body: 'hello'

    sinon.stub(ETahi.VisualEditorService, 'create').returns
      enable: ->
      disable: ->

    controller = ETahi.__container__.lookup 'controller:paperEdit'
    @subject().set 'controller', controller
    controller.set 'content', paper

    sinon.stub @subject(), 'updateVisualEditor'
    @subject().setupVisualEditor()

test 'when the paper is being edited, do not update editor on body change', ->
  @subject().set('isEditing', true)

  @subject().updateVisualEditor.reset()
  @subject().set('controller.body', 'foo')

  ok !@subject().updateVisualEditor.called

test 'when the paper is not being edited, update editor on body change', ->
  @subject().set('isEditing', false)

  @subject().updateVisualEditor.reset()
  @subject().set('controller.body', 'foo')

  ok @subject().updateVisualEditor.called

moduleFor 'view:paperEdit', 'Unit: paperEditView',
  teardown: ->
    ETahi.VisualEditorService.create.restore()
    ETahi.reset()

  setup: ->
    paper = Ember.Object.create
      id: 5
      title: ''
      shortTitle: 'Does not matter'
      body: 'hello'

    sinon.stub(ETahi.VisualEditorService, 'create').returns
      enable: ->
      disable: ->

    controller = ETahi.__container__.lookup 'controller:paperEdit'
    @subject().set 'controller', controller
    controller.set 'content', paper

    sinon.stub @subject(), 'updateVisualEditor'
    @subject().setupVisualEditor()

test 'when the paper is being edited, do not update editor on body change', ->
  @subject().set('isEditing', true)

  @subject().updateVisualEditor.reset()
  @subject().set('controller.body', 'foo')

  ok !@subject().updateVisualEditor.called

test 'when the paper is not being edited, update editor on body change', ->
  @subject().set('isEditing', false)

  @subject().updateVisualEditor.reset()
  @subject().set('controller.body', 'foo')

  ok @subject().updateVisualEditor.called

Add missing id to paper test data.

Add missing id to paper test data.

The associated tests were throwing an error when the paper id
was undefined.


crypto = require 'crypto'

module.exports = ProjectTokenGenerator =

	readOnlyToken: () ->
		length = 12
		tokenAlpha = 'bcdfghjkmnpqrstvwxyz'
		result = ''
		crypto.randomBytes(length).map( (a) -> result += tokenAlpha[a % tokenAlpha.length] )
		return result

	readAndWriteToken: () ->
		numerics = Math.random().toString().slice(2, 12)
		token = ProjectTokenGenerator.readOnlyToken()
		return "#{numerics}#{token}"

crypto = require 'crypto'

# This module mirrors the token generation in Overleaf (`random_token.rb`),
# for the purposes of implementing token-based project access, like the
# 'unlisted-projects' feature in Overleaf

module.exports = ProjectTokenGenerator =

	# (From Overleaf `random_token.rb`)
  #   Letters (not numbers! see generate_token) used in tokens. They're all
  #   consonsants, to avoid embarassing words (I can't think of any that use only
  #   a y), and lower case "l" is omitted, because in many fonts it is
  #   indistinguishable from an upper case "I" (and sometimes even the number 1).
	TOKEN_ALPHA: 'bcdfghjkmnpqrstvwxyz'

	# Generate a 12-char token with only characters from TOKEN_ALPHA,
	# suitable for use as a read-only token for a project
	readOnlyToken: () ->
		length = 12
		tokenAlpha = ProjectTokenGenerator.TOKEN_ALPHA
		result = ''
		crypto.randomBytes(length).map( (a) -> result += tokenAlpha[a % tokenAlpha.length] )
		return result

	# Generate a longer token, with a numeric prefix,
	# suitable for use as a read-and-write token for a project
	readAndWriteToken: () ->
		numerics = Math.random().toString().slice(2, 12)
		token = ProjectTokenGenerator.readOnlyToken()
		return "#{numerics}#{token}"

Add commentary to token-generator, and move token-alpha to top level


"*":
  "exception-reporting":
    userId: "1f6d8ded-d27b-39e5-b84e-95714f424379"
  welcome:
    showOnStartup: false
  core:
    projectHome: "/Users/Richard/Dropbox/Projects"
    themes: [
      "one-dark-ui"
      "base16-tomorrow-dark-theme"
    ]
    disabledPackages: [
      "relative-numbers"
      "timecop"
      "vim-mode"
    ]
    excludeVcsIgnoredPaths: false
    packagesWithKeymapsDisabled: [
      "emmet"
    ]
  editor:
    invisibles: {}
    softWrap: true
    softWrapAtPreferredLineLength: true
    fontSize: 15
  "one-dark-ui":
    layoutMode: "Compact"
  "relative-line-numbers":
    showNormalLineNumbers: false
  "tree-view": {}
  "vim-mode":
    useClipboardAsDefaultRegister: true
    useSmartcaseForSearch: true
  pigments:
    groupPaletteColors: "by file"
    mergeColorDuplicates: true
  linter: {}
  "linter-rubocop":
    executablePath: "/Users/Richard/.rbenv/shims/rubocop"
  "linter-scss-lint":
    executablePath: "/Users/Richard/.rbenv/shims/scss-lint"
  whitespace:
    ignoreWhitespaceOnCurrentLine: false
  react: {}
  "linter-eslint": {}
  emmet: {}

"*":
  "exception-reporting":
    userId: "1f6d8ded-d27b-39e5-b84e-95714f424379"
  welcome:
    showOnStartup: false
  core:
    projectHome: "/Users/Richard/Dropbox/Projects"
    themes: [
      "one-dark-ui"
      "base16-tomorrow-dark-theme"
    ]
    disabledPackages: [
      "relative-numbers"
      "timecop"
      "vim-mode"
    ]
    packagesWithKeymapsDisabled: [
      "emmet"
    ]
  editor:
    invisibles: {}
    softWrap: true
    softWrapAtPreferredLineLength: true
    fontSize: 15
  "one-dark-ui":
    layoutMode: "Compact"
  "relative-line-numbers":
    showNormalLineNumbers: false
  "tree-view": {}
  "vim-mode":
    useClipboardAsDefaultRegister: true
    useSmartcaseForSearch: true
  pigments:
    groupPaletteColors: "by file"
    mergeColorDuplicates: true
  linter: {}
  "linter-rubocop":
    executablePath: "/Users/Richard/.rbenv/shims/rubocop"
  "linter-scss-lint":
    executablePath: "/Users/Richard/.rbenv/shims/scss-lint"
  whitespace:
    ignoreWhitespaceOnCurrentLine: false
  react: {}
  "linter-eslint": {}
  emmet: {}

Exclude VCS ignored paths in Atom

Exclude VCS ignored paths in Atom


"*":
  core:
    disabledPackages: [
      "background-tips"
      "exception-reporting"
      "metrics"
      "welcome"
    ]
  editor:
    invisibles: {}
    showIndentGuide: true
    showInvisibles: true
    fontFamily: "Source Code Pro"

"*":
  core:
    disabledPackages: [
      "background-tips"
      "exception-reporting"
      "metrics"
      "styleguide"
      "timecop"
      "welcome"
    ]
  editor:
    invisibles: {}
    showIndentGuide: true
    showInvisibles: true
    fontFamily: "Source Code Pro"

Disable some Atom packages by default

Disable some Atom packages by default


Backbone = require 'backbone'
mediator = require '../../../lib/mediator.coffee'
template = -> require('../templates/partials/_channel_groups.jade') arguments...

class ProfileView extends Backbone.View
  loading: false
  disabled: false
  threshold: -500
  page: 2

  initialize: ->
    @timer = setInterval @maybeLoad, 150

  maybeLoad: =>
    return false if @loading or
      @disabled or
      mediator.shared.state.get 'lightbox'

    total = document.body.scrollHeight
    scrollPos = (document.documentElement.scrollTop || document.body.scrollTop)
    progress = scrollPos + window.innerHeight * 4

    if (total - progress < @threshold)
      @loadNextPage()

  loadNextPage: ->
    console.log('loadNextPage')
    @loading = true
    $.ajax
      data:
        page: @page
      url: "/api/#{sd.USER.slug}/profile"
      success: (response) =>
        @page++
        @loading = false

        $('.profile').append template
          channels: response.channels

module.exports.init = ->
  new ProfileView
    el: $('.profile')

Backbone = require 'backbone'
mediator = require '../../../lib/mediator.coffee'
template = -> require('../templates/partials/_channel_groups.jade') arguments...

class ProfileView extends Backbone.View
  loading: false
  disabled: false
  threshold: -500
  page: 2

  initialize: ->
    @timer = setInterval @maybeLoad, 150

  maybeLoad: =>
    return false if @loading or
      @disabled or
      mediator.shared.state.get 'lightbox'

    total = document.body.scrollHeight
    scrollPos = (document.documentElement.scrollTop || document.body.scrollTop)
    progress = scrollPos + window.innerHeight * 4

    if (total - progress < @threshold)
      @loadNextPage()

  loadNextPage: ->
    console.log('loadNextPage')
    @loading = true
    $.ajax
      data:
        page: @page
      url: "/api/#{sd.USER.slug}/profile"
      success: (response) =>
        @page++
        @loading = false

        if response.channels.length
          $('.profile').append template
            channels: response.channels
        else
          @disabled = true

module.exports.init = ->
  new ProfileView
    el: $('.profile')

Disable loading after no more results

Disable loading after no more results


$(document)
  .on("click", ".flash-bar-show", ->
    $(this).removeClass("flash-bar-show")
  )

$(document)
  .on("click touchstart", ".flash-bar-show", ->
    $(this).removeClass("flash-bar-show")
  )

Allow touch devices to dismiss flash bar


_ = require "underscore"
Transform = require "./transform"

class Jitter extends Transform.Model
  initialize: (attrs, options) ->
    super(attrs, options)

  defaults: ->
    return _.extend({}, super(), {
      width: 1
    })

  compute: (x) ->
    # Apply the transform to a single value
    return(x + ((Math.random() - 0.5) * @get('width')))

  v_compute: () ->
    # Apply the tranform to a vector of values
    pass

#  map_to_target: (x) ->
#    # do step transform for one value using @get('points')
#    return(x + ((Math.random() - 0.5) * width))

#  v_map_to_target(xs) ->
#    # do step transform for an array of values using @get('points')

module.exports =
  Model: Jitter

_ = require "underscore"
Transform = require "./transform"

class Jitter extends Transform.Model
  initialize: (attrs, options) ->
    super(attrs, options)

  defaults: ->
    return _.extend({}, super(), {
      width: 1
    })

  compute: (x) ->
    # Apply the transform to a single value
    return(x + ((Math.random() - 0.5) * @get('width')))

  v_compute: () ->
    # Apply the tranform to a vector of values
    pass

module.exports =
  Model: Jitter

Remove unessessary old commented code

Remove unessessary old commented code


define [
    './cilantro/core'
    './cilantro/changelog'
    './cilantro/models'
    './cilantro/structs'
    './cilantro/ui'
], (c, changelog, models, structs, ui) ->

    c.changelog = changelog
    c.models = models
    c.structs = structs
    c.ui = ui

    # Defines the minimum version of Serrano that this version of Cilantro is
    # 100% compatible with. While Cilantro will attempt to run normally despite
    # the version number received from the server, the user will be warned if
    # no version number is found or if it is less than this minimum to
    # prepare them in the case of missing or broken functionality.
    c.minimumSerranoVersion = [2, 1, 0]

    c.data =
        concepts: new models.ConceptCollection
        fields: new models.FieldCollection
        contexts: new models.ContextCollection
        views: new models.ViewCollection
        results: new models.Results
        exporters: new models.ExporterCollection

    if c.getOption('autoload')
        c.openSession()

    return (@cilantro = c)

define [
    './cilantro/core'
    './cilantro/changelog'
    './cilantro/models'
    './cilantro/structs'
    './cilantro/ui'
], (c, changelog, models, structs, ui) ->

    c.changelog = changelog
    c.models = models
    c.structs = structs
    c.ui = ui

    # Defines the minimum version of Serrano that this version of Cilantro is
    # 100% compatible with. While Cilantro will attempt to run normally despite
    # the version number received from the server, the user will be warned if
    # no version number is found or if it is less than this minimum to
    # prepare them in the case of missing or broken functionality.
    c.minimumSerranoVersion = [2, 0, 24]

    c.data =
        concepts: new models.ConceptCollection
        fields: new models.FieldCollection
        contexts: new models.ContextCollection
        views: new models.ViewCollection
        results: new models.Results
        exporters: new models.ExporterCollection

    if c.getOption('autoload')
        c.openSession()

    return (@cilantro = c)

Drop the minimum Serrano version to >= 2.0.24


"*":
  autosave:
    enabled: true
  core:
    audioBeep: false
    themes: [
      "one-light-ui"
      "one-light-syntax"
    ]
    projectHome: "/Users/randy/src"
  editor:
    fontFamily: "Hack"
    fontSize: 19
    invisibles: {}
    showIndentGuide: true
    tabType: "soft"
  emmet: {}
  "exception-reporting":
    userId: "30e6f2d0-42d3-ab77-4984-dde4c431f019"
  "expand-region": {}
  "git-blame": {}
  "git-diff":
    showIconsInEditorGutter: true
    rspecAllCommand: "bundle exec rspec --tty spec"
    rspecFileCommand: "bundle exec rspec --tty {relative_path}"
    rspecSingleCommand: "bundle exec rspec --tty {relative_path}:{line_number}"
  "git-plus": {}
  linter:
    errorPanelHeight: 26
  "linter-eslint": {}
  "linter-sass-lint":
    noConfigDisable: true
  "markdown-deluxe":
    fontFamily: "Hack"
  "package-sync":
    forceOverwrite: true
  "ruby-test":
    specFramework: "rspec"
    testFramework: "minitest"
  tabs:
    enableVcsColoring: true
  "terminal-plus":
    core: {}
    style: {}
  "toggle-quotes": {}
  "vim-mode":
    startInInsertMode: true
  welcome:
    showOnStartup: false
"source.gfm":
  editor:
    softWrap: true

"*":
  autosave:
    enabled: true
  core:
    audioBeep: false
    ]
    projectHome: "/Users/randy/src"
  editor:
    fontFamily: "Hack"
    fontSize: 19
    invisibles: {}
    showIndentGuide: true
    tabType: "soft"
  emmet: {}
  "exception-reporting":
    userId: "30e6f2d0-42d3-ab77-4984-dde4c431f019"
  "expand-region": {}
  "git-blame": {}
  "git-diff":
    showIconsInEditorGutter: true
    rspecAllCommand: "bundle exec rspec --tty spec"
    rspecFileCommand: "bundle exec rspec --tty {relative_path}"
    rspecSingleCommand: "bundle exec rspec --tty {relative_path}:{line_number}"
  "git-plus": {}
  linter:
    errorPanelHeight: 26
  "linter-eslint": {}
  "linter-sass-lint":
    noConfigDisable: true
  "markdown-deluxe":
    fontFamily: "Hack"
  "package-sync":
    forceOverwrite: true
  "ruby-test":
    specFramework: "rspec"
    testFramework: "minitest"
  tabs:
    enableVcsColoring: true
  "terminal-plus":
    core: {}
    style: {}
  "toggle-quotes": {}
  "vim-mode":
    startInInsertMode: true
  welcome:
    showOnStartup: false
"source.gfm":
  editor:
    softWrap: true

Switch back to dark themes

Switch back to dark themes


class TerminalPane extends Pane

  constructor: (options = {}, data) ->

    options.cssClass  = "terminal-pane terminal"
    options.delay    ?= 0

    super options, data

    @createWebTermView()
    @webterm.on "WebTermConnected", (@remote) =>
      @emit "WebtermCreated"
      @onWebTermConnected()

  createWebTermView: ->
    @webterm           = new WebTermView
      cssClass         : "webterm"
      advancedSettings : no
      delegate         : this
      mode             : @getMode()

    @webterm.connectToTerminal()
    # WebTermView.setTerminalTimeout null, 15000, handler, handler

  getMode: -> "create"

  onWebTermConnected: ->
    {command} = @getOptions()
    @runCommand command if command

  runCommand: (command, callback) ->
    return unless command
    if @remote
      if callback
        @webterm.once "WebTermEvent", callback
        command += ";echo $?|kdevent"
      return @remote.input "#{command}\n"

    if not @remote and not @triedAgain
      @utils.wait 2000, =>
        @runCommand command
        @triedAgain = yes

  pistachio: ->
    """
      {{> @header}}
      {{> @webterm}}
    """

class TerminalPane extends Pane

  constructor: (options = {}, data) ->

    options.cssClass  = "terminal-pane terminal"
    options.delay    ?= 0

    super options, data

    @createWebTermView()

  createWebTermView: ->

    KD.singletons.vmController.fetchDefaultVm (err, vm)=>

      @webterm           = new WebTermView {
        cssClass         : "webterm"
        advancedSettings : no
        delegate         : this
        mode             : @getMode()
        vm
      }

      @addSubView @header
      @addSubView @webterm

      @webterm.connectToTerminal()

      @webterm.on "WebTermConnected", (@remote) =>
        @emit "WebtermCreated"
        @onWebTermConnected()

      # WebTermView.setTerminalTimeout null, 15000, handler, handler

  getMode: -> "create"

  onWebTermConnected: ->
    {command} = @getOptions()
    @runCommand command if command

  runCommand: (command, callback) ->
    return unless command
    if @remote
      if callback
        @webterm.once "WebTermEvent", callback
        command += ";echo $?|kdevent"
      return @remote.input "#{command}\n"

    if not @remote and not @triedAgain
      @utils.wait 2000, =>
        @runCommand command
        @triedAgain = yes

  # pistachio: ->
  #   """
  #     {{> @header}}
  #     {{> @webterm}}
  #   """

Fix TerminalPane until we support for multiple vms


deviceTypesCommon = require '@resin.io/device-types/common'
{ networkOptions, commonImg, instructions } = deviceTypesCommon

module.exports =
	version: 1
	slug: 'raspberry-pi2'
	aliases: [ 'raspberrypi2' ]
	name: 'Raspberry Pi 2'
	arch: 'armv7hf'
	state: 'released'

	instructions: commonImg.instructions
	gettingStartedLink:
		windows: 'https://docs.resin.io/raspberrypi2/nodejs/getting-started/#adding-your-first-device'
		osx: 'https://docs.resin.io/raspberrypi2/nodejs/getting-started/#adding-your-first-device'
		linux: 'https://docs.resin.io/raspberrypi2/nodejs/getting-started/#adding-your-first-device'
	supportsBlink: true

	options: [ networkOptions.group ]

	yocto:
		machine: 'raspberrypi2'
		image: 'resin-image'
		fstype: 'resinos-img'
		version: 'yocto-warrior'
		deployArtifact: 'resin-image-raspberrypi2.resinos-img'
		compressed: true

	configuration:
		config:
			partition:
				primary: 1
			path: '/config.json'

	initialization: commonImg.initialization

deviceTypesCommon = require '@resin.io/device-types/common'
{ networkOptions, commonImg, instructions } = deviceTypesCommon

module.exports =
	version: 1
	slug: 'raspberry-pi2'
	aliases: [ 'raspberrypi2' ]
	name: 'Raspberry Pi 2'
	arch: 'armv7hf'
	state: 'released'

	imageDownloadAlerts: [
		{
			type: 'warning'
			message: 'The Raspberry Pi 2 is not capable of connecting to WiFi networks without an external adapter.'
		}
	]

	instructions: commonImg.instructions
	gettingStartedLink:
		windows: 'https://docs.resin.io/raspberrypi2/nodejs/getting-started/#adding-your-first-device'
		osx: 'https://docs.resin.io/raspberrypi2/nodejs/getting-started/#adding-your-first-device'
		linux: 'https://docs.resin.io/raspberrypi2/nodejs/getting-started/#adding-your-first-device'
	supportsBlink: true

	options: [ networkOptions.group ]

	yocto:
		machine: 'raspberrypi2'
		image: 'resin-image'
		fstype: 'resinos-img'
		version: 'yocto-warrior'
		deployArtifact: 'resin-image-raspberrypi2.resinos-img'
		compressed: true

	configuration:
		config:
			partition:
				primary: 1
			path: '/config.json'

	initialization: commonImg.initialization

Add warning advising of no WiFi on Pi 2

Add warning advising of no WiFi on Pi 2

Changelog-entry: Add warning advising of no WiFi on Pi 2
Change-type: patch
Signed-off-by: Chris Crocker-White <55144d225ecaa64cbddbc62fe6b6637fa9671c59@balena.io>


class @Breakpoints
  instance = null;

  class BreakpointInstance
    BREAKPOINTS = ["xs", "sm", "md", "lg"]

    constructor: ->
      @setup()

    setup: ->
      allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
        ".device-#{breakpoint}"

      return if $(allDeviceSelector.join(",")).length

      # Create all the elements
      els = $.map BREAKPOINTS, (breakpoint) ->
        "

"
      $("body").append els.join('')

    getBreakpointSize: ->
      allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
        ".device-#{breakpoint}"

      $visibleDevice = $(allDeviceSelector.join(",")).filter(":visible")

      return $visibleDevice.attr("class").split("visible-")[1]

  @get: ->
    return instance ?= new BreakpointInstance

$ =>
  @bp = Breakpoints.get()

class @Breakpoints
  instance = null;

  class BreakpointInstance
    BREAKPOINTS = ["xs", "sm", "md", "lg"]

    constructor: ->
      @setup()

    setup: ->
      allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
        ".device-#{breakpoint}"
      return if $(allDeviceSelector.join(",")).length

      # Create all the elements
      els = $.map BREAKPOINTS, (breakpoint) ->
        "

"
      $("body").append els.join('')

    visibleDevice: ->
      allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
        ".device-#{breakpoint}"
      $(allDeviceSelector.join(",")).filter(":visible")

    getBreakpointSize: ->
      $visibleDevice = @visibleDevice
      # the page refreshed via turbolinks
      if not $visibleDevice().length
        @setup()
      $visibleDevice = @visibleDevice()
      return $visibleDevice.attr("class").split("visible-")[1]

  @get: ->
    return instance ?= new BreakpointInstance

$ =>
  @bp = Breakpoints.get()

Fix bug with JS error from dropdowns in filter area


OFNShared.directive "questionMarkWithTooltip", ($tooltip)->
  # We use the $tooltip service from Angular foundation to give us boilerplate
  # Subsequently we patch the scope, template and restrictions
  tooltip = $tooltip 'questionMarkWithTooltip', 'questionMarkWithTooltip', 'click'
  tooltip.scope =
    variant: "="
    key: "="
  tooltip.templateUrl = "shared/question_mark_with_tooltip_icon.html"
  tooltip.replace = true
  tooltip.restrict = 'E'
  tooltip

# This is automatically referenced via naming convention in $tooltip
OFNShared.directive 'questionMarkWithTooltipPopup', ->
  restrict: 'EA'
  replace: true
  templateUrl: 'shared/question_mark_with_tooltip.html'
  scope: false

OFNShared.directive "questionMarkWithTooltip", ($tooltip)->
  # We use the $tooltip service from Angular foundation to give us boilerplate
  # Subsequently we patch the scope, template and restrictions
  tooltip = $tooltip 'questionMarkWithTooltip', 'questionMarkWithTooltip', 'click'
  tooltip.scope =
    context: "="
    key: "="
  tooltip.templateUrl = "shared/question_mark_with_tooltip_icon.html"
  tooltip.replace = true
  tooltip.restrict = 'E'
  tooltip

# This is automatically referenced via naming convention in $tooltip
OFNShared.directive 'questionMarkWithTooltipPopup', ->
  restrict: 'EA'
  replace: true
  templateUrl: 'shared/question_mark_with_tooltip.html'
  scope: false

Add context inside scope: used to specify display

 - Context is used to customize some CSS specifications as sometimes display can be different (depending on the context)
 - variant variable was not used: copy/paste error


Wheelmap.StatusFilterButtonComponent = Wheelmap.WheelchairPopoverComponent.extend
  tagName: 'button'
  classNameBindings: [':btn', 'isActive:active', ':btn-info', 'wheelchair']
  wheelchair: null
  activeFilters: null
  template: Ember.Handlebars.compile('')

  popoverOptions:
    trigger: 'hover'
    placement: () ->
      if window.innerWidth <= 767 then 'top' else 'bottom'
    delay: { show: 400 }
    container: '#toolbar .status-filter' # Need for not having little spaces between status buttons

  isActive: (()->
    @get('activeFilters').contains(@get('wheelchair'))
  ).property('wheelchair', 'activeFilters.@each')

  click: ()->
    @sendAction('didClick', @get('wheelchair'))

Ember.Handlebars.helper('status-filter-button', Wheelmap.StatusFilterButtonComponent)

Wheelmap.StatusFilterButtonComponent = Wheelmap.WheelchairPopoverComponent.extend
  tagName: 'button'
  classNameBindings: [':btn', 'isActive:active', ':btn-info', 'wheelchair']
  wheelchair: null
  activeFilters: null
  template: Ember.Handlebars.compile('')

  popoverOptions:
    trigger: 'hover'
    placement: () ->
      if window.innerWidth <= 767 then 'top' else 'bottom'
    delay: { show: 400 }
    container: '#toolbar .status-filter' # Need for not having little spaces between status buttons

  isActive: (()->
    @get('activeFilters').contains(@get('wheelchair'))
  ).property('wheelchair', 'activeFilters.@each')

  click: (event)->
    event.preventDefault();

    @sendAction('didClick', @get('wheelchair'))

Ember.Handlebars.helper('status-filter-button', Wheelmap.StatusFilterButtonComponent)

Add first draft for better presentation of info popups in responsive toolbar.


".autocomplete-plus input.hidden-input":
  "tab": "autocomplete-plus:confirm"
  "down": "autocomplete-plus:select-next"
  "up": "autocomplete-plus:select-previous"
  "escape": "autocomplete-plus:cancel"

".editor":
  "ctrl-shift-space": "autocomplete-plus:activate"

".autocomplete-plus input.hidden-input":
  "tab": "autocomplete-plus:confirm"
  "down": "autocomplete-plus:select-next"
  "ctrl-n": "autocomplete-plus:select-next"
  "up": "autocomplete-plus:select-previous"
  "ctrl-p": "autocomplete-plus:select-previous"
  "escape": "autocomplete-plus:cancel"

".editor":
  "ctrl-shift-space": "autocomplete-plus:activate"

Add ctrl-n,p emacs bindings for next, previous

Emacs binds are supported through most of the rest of Atom. Plus it's rough having to drop down to the arrow keys in the midst of typing :)


class MainChatPanel extends JView

  constructor:->
    super
      cssClass : 'main-chat-panel'

    @registerSingleton "chatPanel", @, yes

    @conversationList = new ChatConversationListView
    @conversationListController = new ChatConversationListController
      view : @conversationList

  viewAppended:->
    @addSubView @conversationList
    @conversationListController.loadItems()

  show:->
    @setClass 'visible'

  hide:->
    @unsetClass 'visible'

  toggle:->
    @toggleClass 'visible'

  isVisible:->
    @hasClass 'visible'

class MainChatPanel extends JView

  constructor:->
    super
      cssClass : 'main-chat-panel visible'

    @registerSingleton "chatPanel", @, yes

    @conversationList = new ChatConversationListView
    @conversationListController = new ChatConversationListController
      view : @conversationList

  viewAppended:->
    @addSubView @conversationList
    @conversationListController.loadItems()

  show:->
    @setClass 'visible'

  hide:->
    @unsetClass 'visible'

  toggle:->
    @toggleClass 'visible'

  isVisible:->
    @hasClass 'visible'

Make it visible by default for testing fastly


sys = require 'sys'
fs = require 'fs'
xml2js = require 'xml2js'
_ = require 'underscore'
URL = require 'url'
mongoose = require 'mongoose'

parser = new xml2js.Parser()

parser.addListener 'end', (result) ->
    Camera = new mongoose.Schema
        name: String
        url: String
        loc:
            x: Number
            y: Number
    Camera.index loc: '2d'
    mongoose.model 'Camera', Camera
    CameraModel = mongoose.model 'Camera'
    mongoose.connect 'mongodb://localhost/cams'

    _(result.Document.Folder.Placemark).each (placemark, i) ->
        url = URL.parse placemark.description.split('"')[3]
        coords = placemark.Point.coordinates.split ','

        aCamera = new CameraModel
            name: placemark.name
            url: url.href
            loc:
                x: parseFloat coords[0], 10
                y: parseFloat coords[1], 10

        aCamera.save (err) ->
            if err then console.log """Could not save camera #{i}:

#{aCamera}

Error: #{err}"""

fs.readFile 'data/raw/trafficcameraswithfeed.kml', (err, data) ->
    parser.parseString data

sys = require 'sys'
fs = require 'fs'
xml2js = require 'xml2js'
_ = require 'underscore'
URL = require 'url'
mongoose = require 'mongoose'

parser = new xml2js.Parser()

parser.addListener 'end', (result) ->
    # Define the mongoose schema
    Camera = new mongoose.Schema
        name: String
        url: String
        loc:
            x: Number
            y: Number
    Camera.index loc: '2d'

    # Create the mongoose model
    mongoose.model 'Camera', Camera
    CameraModel = mongoose.model 'Camera'

    # Connect to mongodb
    mongoose.connect 'mongodb://localhost/cams'

    # Remove all previously saved documents
    aCamera = new CameraModel()
    aCamera.collection.drop()

    # Parse each Placemark into a mongoose model and save each
    _(result.Document.Folder.Placemark).each (placemark, i) ->
        url = URL.parse placemark.description.split('"')[3]
        coords = placemark.Point.coordinates.split ','

        aCamera = new CameraModel
            name: placemark.name
            url: url.href
            loc:
                x: parseFloat coords[0], 10
                y: parseFloat coords[1], 10

        aCamera.save (err) ->
            if err then console.log """Could not save camera #{i}:

#{aCamera}

Error: #{err}"""

    console.log "If you haven't seen any errors, we've probably been successful."

fs.readFile 'data/raw/trafficcameraswithfeed.kml', (err, data) ->
    parser.parseString data

Add some comments and logging to the cam parser.


#= require jquery

$(document).on 'uic:domchange', (e) ->
  $(e.target).find('[data-toggle="markdown-readonly"]').each (_, el) ->
    $el = $(el)
    $el.html(marked(decodeURIComponent($el.data('markdown'))))
    $el.show()

Remove require directive from component JS


util = require 'lib/util'
network = require 'lib/network'

methodMap =
  create: 'POST'
  update: 'POST'
  delete: 'POST'
  read: 'GET'

getValue = (object, prop) ->
  if val = object?[prop]
    if _.isFunction(val) then val() else val
  else
    null

module.exports = class Sync

  constructor: (@options = {}) ->

    @options.parse ?= (response) ->
      response

  buildUrl: (url) =>

    if urlRoot = @options.urlRoot

      divider = if urlRoot.match(/\/$/) then '' else '/'

      url = url.replace /^\//, ''

      url = [urlRoot, url].join divider

    url

  sync: (method, model, options) =>

    url = @buildUrl options.url ? getValue model, 'url'

    if not options.data and model and method in ['create', 'update']

      data = JSON.stringify model.toJSON()

    if data
      contentType = 'application/json'

    network.request url,

      type: methodMap[method]

      data: data

      contentType: contentType

      timeout: options.timeout

      beforeSend: @options.beforeSend

      success: (data, status, client) =>

        data = @options.parse data

        options.success? data, status, client

util = require 'lib/util'
network = require 'lib/network'

methodMap =
  create: 'POST'
  update: 'POST'
  delete: 'POST'
  read: 'GET'

module.exports = class Sync

  constructor: (@options = {}) ->

    @options.parse ?= (response) ->
      response

  buildUrl: (url) =>

    if urlRoot = @options.urlRoot

      divider = if urlRoot.match(/\/$/) then '' else '/'

      url = url.replace /^\//, ''

      url = [urlRoot, url].join divider

    url

  sync: (method, model, options) =>

    url = @buildUrl options.url ? _.result model, 'url'

    if not options.data and model and method in ['create', 'update']

      data = JSON.stringify model.toJSON()

    if data
      contentType = 'application/json'

    network.request url,

      type: methodMap[method]

      data: data

      contentType: contentType

      timeout: options.timeout

      beforeSend: @options.beforeSend

      success: (data, status, client) =>

        data = @options.parse data

        options.success? data, status, client

Replace 'getValue' with underscore's _.result method


class Fustrate.GenericTable extends Fustrate.GenericPage
  @blankRow: null
  table: null

  initialize: =>
    super

    @reloadTable()

  reloadTable: ->

  sortRows: (rows, sortFunction = ->) ->
    ([sortFunction(row), row] for row in rows)
      .sort (x, y) -> if x[0] == y[0] then 0 else x[0] > y[0]
      .map (row) -> row[1]

  createRow: (item) =>
    @updateRow @constructor.blankRow.clone(), item

  updateRow: (row, item) ->
    row

  reloadRows: (rows, {sort} = { sort: null }) =>
    tbody = $ 'tbody', @table

    $('tr.loading', tbody).hide()

    if rows
      $('tr:not(.no-records):not(.loading)', tbody).remove()

      tbody.append if sort then @sortRows(rows, sort) else rows

    @updated()

  addRow: (row) =>
    $('tbody', @table).append row
    @updated()

  removeRow: (row) =>
    row.fadeOut =>
      row.remove()
      @updated()

  updated: =>
    $('tbody tr.no-records', @table)
      .toggle $('tbody tr:not(.no-records):not(.loading)', @table).length < 1

  # This should be fed a response from a JSON request for a paginated
  # collection.
  updatePagination: (response) =>
    $('.pagination', @root)
      .replaceWith (new Fustrate.Components.Pagination response).generate()

Add method to update pagination

Add method to update pagination

Signed-off-by: Steven Hoffman <f5bcacb8ce1651b7d1e56f2693431f20e085fdeb@valenciamgmt.com>


# See https://atom.io/docs/latest/creating-a-package#menus for more details
# 'context-menu':
#   '.overlayer':
#       'Open terminal here': 'term:open'

'menu': [
  'label': 'Packages'
  'submenu': [
    'label': 'Term 2'
    'submenu': [
      {'label': 'Open New Terminal in New Tab', 'command': 'term3:open'}
      {'label': 'Open New Terminal in Right Pane', 'command': 'term3:open-split-right'}
      {'label': 'Open New Terminal in Bottom Pane', 'command': 'term3:open-split-down'}
      {'label': 'Open New Terminal in Top Pane', 'command': 'term3:open-split-up'}
      {'label': 'Open New Terminal in Left Pane', 'command': 'term3:open-split-left'}
    ]
  ]
]
'context-menu':
  '.term': [
    {
      'label': 'Copy'
      'command': 'term3:copy'
    }
    {
      'label': 'Paste'
      'command': 'term3:paste'
    }
  ]

  '.overlayer': [
    {
      'label': 'Term3: Pipe selection to last Terminal session'
      'command': 'term3:pipe-selection'
    }
    {
      'label': 'Term3: Pipe file path to the Terminal session'
      'command': 'term3:pipe-path'
    }
  ]

# See https://atom.io/docs/latest/creating-a-package#menus for more details
# 'context-menu':
#   '.overlayer':
#       'Open terminal here': 'term:open'

'menu': [
  'label': 'Packages'
  'submenu': [
    'label': 'Term 3'
    'submenu': [
      {'label': 'Open New Terminal in New Tab', 'command': 'term3:open'}
      {'label': 'Open New Terminal in Right Pane', 'command': 'term3:open-split-right'}
      {'label': 'Open New Terminal in Bottom Pane', 'command': 'term3:open-split-down'}
      {'label': 'Open New Terminal in Top Pane', 'command': 'term3:open-split-up'}
      {'label': 'Open New Terminal in Left Pane', 'command': 'term3:open-split-left'}
    ]
  ]
]
'context-menu':
  '.term': [
    {
      'label': 'Copy'
      'command': 'term3:copy'
    }
    {
      'label': 'Paste'
      'command': 'term3:paste'
    }
  ]

  '.overlayer': [
    {
      'label': 'Term3: Pipe selection to last Terminal session'
      'command': 'term3:pipe-selection'
    }
    {
      'label': 'Term3: Pipe file path to the Terminal session'
      'command': 'term3:pipe-path'
    }
  ]

Correct package menu label to show Term 3 instead of Term 2


Ember.Application.initializer
  name: 'session'
  after: 'ember-auth-load'

  initialize: (container, application) ->
    Dashboard.Session = Ember.Object.extend(
       init: ->
        auth = container.lookup 'auth:main'

        auth.addHandler 'signInSuccess', ->
          sessionStorage.setItem('authToken', auth.get('authToken'))
          sessionStorage.setItem('authUserId', auth.get('userId'))

        authToken = sessionStorage.getItem('authToken')
        authUserId = sessionStorage.getItem('authUserId')

        if authToken != null && authUserId != null
          auth.createSession '{"access_token": "' + authToken + '", "user_id": "' + authUserId + '"}'
    ).create()

Ember.Application.initializer
  name: 'session'
  after: 'ember-auth-load'

  initialize: (container, application) ->
    Dashboard.Session = Ember.Object.extend(
       init: ->
        auth = container.lookup 'auth:main'

        auth.addHandler 'signInSuccess', ->
          sessionStorage.setItem('authToken', auth.get('authToken'))
          sessionStorage.setItem('authUserId', auth.get('userId'))

        auth.addHandler 'signOutSuccess', ->
          sessionStorage.removeItem('authToken')
          sessionStorage.removeItem('authUserId')

        authToken = sessionStorage.getItem('authToken')
        authUserId = sessionStorage.getItem('authUserId')

        if authToken != null && authUserId != null
          auth.createSession '{"access_token": "' + authToken + '", "user_id": "' + authUserId + '"}'
    ).create()

Add auth handler to remove session data when sign out


asar = require 'asar'
fs = require 'fs'
path = require 'path'

module.exports = (grunt) ->
  {cp, rm} = require('./task-helpers')(grunt)

  grunt.registerTask 'generate-asar', 'Generate asar archive for the app', ->
    done = @async()

    unpack = [
      '*.node'
      'ctags-darwin'
      'ctags-linux'
      'ctags-win32.exe'
      '\\.ctags'
    ]
    unpack = "{#{unpack.join(',')}}"

    appDir = grunt.config.get('atom.appDir')
    unless fs.existsSync(appDir)
      grunt.log.error 'The app has to be built before generating asar archive.'
      return done(false)

    asar.createPackageWithOptions appDir, path.resolve(appDir, '..', 'app.asar'), {unpack}, (err) ->
      return done(err) if err?

      rm appDir
      fs.renameSync path.resolve(appDir, '..', 'new-app'), appDir

      ctagsFolder = path.join("#{appDir}.asar.unpacked", 'node_modules', 'symbols-view', 'vendor')
      for ctagsFile in fs.readdirSync(ctagsFolder)
        fs.chmodSync(path.join(ctagsFolder, ctagsFile), "755")

      done()

asar = require 'asar'
fs = require 'fs'
path = require 'path'

module.exports = (grunt) ->
  {cp, rm} = require('./task-helpers')(grunt)

  grunt.registerTask 'generate-asar', 'Generate asar archive for the app', ->
    done = @async()

    unpack = [
      '*.node'
      '*.ctags'
      'ctags-darwin'
      'ctags-linux'
      'ctags-win32.exe'
    ]
    unpack = "{#{unpack.join(',')}}"

    appDir = grunt.config.get('atom.appDir')
    unless fs.existsSync(appDir)
      grunt.log.error 'The app has to be built before generating asar archive.'
      return done(false)

    asar.createPackageWithOptions appDir, path.resolve(appDir, '..', 'app.asar'), {unpack}, (err) ->
      return done(err) if err?

      rm appDir
      fs.renameSync path.resolve(appDir, '..', 'new-app'), appDir

      ctagsFolder = path.join("#{appDir}.asar.unpacked", 'node_modules', 'symbols-view', 'vendor')
      for ctagsFile in fs.readdirSync(ctagsFolder)
        fs.chmodSync(path.join(ctagsFolder, ctagsFile), "755")

      done()

Update .ctags pattern for unpacking

Update .ctags pattern for unpacking


kd   = require 'kd'

nick    = require 'app/util/nick'
Tracker = require 'app/util/tracker'

module.exports = (machine) ->

  return  unless analytics
  return  unless typeof analytics.user is 'function'

  { initialTurnOn } = analytics.user().traits()
  return  if initialTurnOn

  initialTurnOn = yes
  analytics.identify nick(), { initialTurnOn }

  track Tracker.BUTTON_CLICKED

  kd.singletons.computeController.once 'MachineBuilt', (event) ->

    { machineId } = event

    return  if machineId isnt machine._id

    track Tracker.MODAL_DISPLAYED

track = (action) ->
  Tracker.track action, { category: Tracker.CATEGORY_TURN_ON_VM }

kd   = require 'kd'

nick    = require 'app/util/nick'
Tracker = require 'app/util/tracker'

module.exports = (machine) ->

  return  unless analytics
  return  unless typeof analytics.user is 'function'

  { initialTurnOn } = analytics.user().traits()
  return  if initialTurnOn

  initialTurnOn = yes
  analytics.identify nick(), { initialTurnOn }
  execute machine

execute = (machine) ->

  track Tracker.BUTTON_CLICKED

  kd.singletons.computeController.once 'MachineBuilt', (event) ->

    { machineId } = event

    return  if machineId isnt machine._id

    track Tracker.MODAL_DISPLAYED

track = (action) ->
  Tracker.track action, { category: Tracker.CATEGORY_TURN_ON_VM }

Split track initial turn on function body


class @MixingpanelCookies
  constructor: ()->
    @_loadCookies()
    @cookieDefOpts =
      domain: null
      path: null
      expires_at: null

  _loadCookies: ->
    for cookie_token in document.cookie.split("; ")
      [key, value] = cookie_token.split("=")
      @[key] = value

  set: (key, value, opts = {})->
    opts  = $.extend(@cookieDefOpts, opts)
    value = value.replace(";","\\;")

    keyValue = "#{key}=#{value}"
    domain   = if opts.domain? then "; domain=#{opts.domain}" else ""
    path     = if opts.path? then "; path=#{opts.path}" else ""
    expires  = if opts.expires_at? then "; expires=#{opts.expires_at.toGMTString()}" else ""
    session  = if opts.session? then "; session=#{opts.session}" else ""

    document.cookie = (keyValue + expires + path + domain + session)
    @[key] = value

  delete: (key)->
    keyValue = "#{key}="
    expires  = "; expires=#{(new Date(0)).toGMTString()}"

    document.cookie = (keyValue + expires)
    delete @[key]

class @MixingpanelCookies
  constructor: ()->
    @_loadCookies()
    @cookieDefOpts =
      domain: null
      path: null
      expires_at: null

  _loadCookies: ->
    for cookie_token in document.cookie.split("; ")
      [key, value] = cookie_token.split("=")
      @[key] = value

  set: (key, value, opts = {})->
    opts  = $.extend(@cookieDefOpts, opts)
    value = value.replace(";","\\;")

    keyValue = "#{key}=#{value}"
    domain   = if opts.domain? then "; domain=#{opts.domain}" else ""
    path     = if opts.path? then "; path=#{opts.path}" else ""
    expires  = if opts.expires_at? then "; expires=#{opts.expires_at.toGMTString()}" else ""

    document.cookie = (keyValue + expires + path + domain + session)
    @[key] = value

  delete: (key)->
    keyValue = "#{key}="
    expires  = "; expires=#{(new Date(0)).toGMTString()}"

    document.cookie = (keyValue + expires)
    delete @[key]

Remove session from cookie set option

Remove session from cookie set option


BibtexProvider = require "./provider"

module.exports =
  config:
    bibtex:
      type: 'string'
      default: ''
    scope:
      type: 'string'
      default: '.source.gfm'
    resultTemplate:
      type: 'string'
      default: '@[key]'

  activate: ->
    @provider = new BibtexProvider()

  deactivate: ->
    @provider.registration.dispose()

  provide: ->
    return { providers: [@provider] }

BibtexProvider = require "./provider"

module.exports =
  config:
    bibtex:
      type: 'array'
      default: []
      items:
        type: 'string'
    scope:
      type: 'string'
      default: '.source.gfm'
    resultTemplate:
      type: 'string'
      default: '@[key]'

  activate: ->
    @provider = new BibtexProvider()

  deactivate: ->
    @provider.registration.dispose()

  provide: ->
    return { providers: [@provider] }

Update configuration schema to reflect that we expect an array of BibTeX file names.


root = exports ? this

root.User = class User
  @findOne: (idOrName) ->
    data = Meteor.users.findOne(idOrName)
    if not data
      data = Meteor.users.findOne({'profile.name': new RegExp('^' + idOrName + '$', 'i')})
    new User(data)

  @current: ->
    Meteor.userId() and new User(Meteor.user())

  constructor: (@data) ->

  name: -> @data.profile.name

  photoUrl: (height) ->
    if @data.services
      if @data.services.google
        picture = @data.services.google.picture
        if picture and height
          picture = "#{picture}?sz=#{height}"
      else if @data.services.github
        picture = @data.services.github.picture
        if picture and height
          picture = "#{picture}&s=#{height}"
      else if @data.services.facebook
        picture = "http://graph.facebook.com/#{@data.services.facebook.id}/picture/"
        if height
          picture = "#{picture}?height=#{height/2}"
      else if @data.services.twitter
        picture = @data.services.twitter.profile_image_url
    if not picture
      if height
        picture = Cdn.cdnify("/img/user-#{height}x#{height}.png")
      else
        picture = Cdn.cdnify('/img/user.png')
    picture

root = exports ? this

root.User = class User
  @findOne: (idOrName) ->
    data = Meteor.users.findOne(idOrName)
    if not data
      data = Meteor.users.findOne({'profile.name': new RegExp('^' + idOrName + '$', 'i')})
    new User(data)

  @current: ->
    Meteor.userId() and new User(Meteor.user())

  constructor: (@data) ->

  name: -> @data.profile.name

  photoUrl: (height) ->
    if @data.services
      if @data.services.google
        picture = @data.services.google.picture
        if picture and height
          picture = "#{picture}?sz=#{height}"
      else if @data.services.github
        picture = @data.services.github.picture
        if picture and height
          picture = "#{picture}&s=#{height}"
      else if @data.services.facebook
        picture = "http://graph.facebook.com/#{@data.services.facebook.id}/picture/"
        if height
          picture = "#{picture}?height=#{height/2}&width=#{height/2}"
      else if @data.services.twitter
        picture = @data.services.twitter.profile_image_url
    if not picture
      if height
        picture = Cdn.cdnify("/img/user-#{height}x#{height}.png")
      else
        picture = Cdn.cdnify('/img/user.png')
    picture

Fix facebook images to make sure they are squares


define [
  "base"
], (App) ->
  App.directive "toggleSwitch", () ->
    restrict: "E"
    scope:
      description: "@"
      labelFalse: "@"
      labelTrue: "@"
      ngModel: "="
    template: """
  <fieldset class="toggle-switch">
    <legend class="sr-only">{{description}}</legend>

    <input
      type="radio"
      name="editor-mode"
      class="toggle-switch-input"
      id="toggle-switch-false-{{$id}}"
      ng-value="false"
      ng-model="ngModel"
    >
    {{labelFalse}}

    <input
      type="radio"
      class="toggle-switch-input"
      name="editor-mode"
      id="toggle-switch-true-{{$id}}"
      ng-value="true"
      ng-model="ngModel"
    >
    {{labelTrue}}

  </fieldset>
"""

Mark visible toggle switch as hidden from screen readers


define('base_layer', [], ->
  class BaseLayer
    @render: (map, opts={}) ->
      terrain = L.mapbox.tileLayer('unepwcmc.l8gj1ihl')
      satellite = L.mapbox.tileLayer('unepwcmc.lac5fjl1')

      L.control.layers({
        "Terrain": terrain,
        "Satellite": satellite
      }, null, {
        position: opts.controlPosition || 'topleft'
      }).addTo(map)

      layers = {
        "Terrain": "Terrain",
        "Satellite": "Satellite"
      }

      map.on('baselayerchange', (e) ->
        for layerName, layerId of layers
          layerControl = $("span[data-layer='#{layerId}']")

          if layerName == e.name
            layerControl.addClass("is-selected")
          else
            layerControl.removeClass("is-selected")
      )

      terrainControl = $("span[data-layer='Terrain']")
      terrainControl.addClass("is-selected")

  return BaseLayer
)

define('base_layer', [], ->
  class BaseLayer
    @render: (map, opts={}) ->
      terrain = L.mapbox.tileLayer('unepwcmc.l8gj1ihl')
      satellite = L.mapbox.tileLayer('unepwcmc.lac5fjl1')
      marine = L.mapbox.styleLayer('mapbox://styles/unepwcmc/cj63iq5m04i142rl0481raoxj')

      # add Terrain/Satellite button
      if !opts.hideControl
        L.control.layers({
          "Terrain": terrain,
          "Satellite": satellite
        }, null, {
          position: opts.controlPosition || 'topleft'
        }).addTo(map)

        layers = {
          "Terrain": "Terrain",
          "Satellite": "Satellite"
        }

        map.on('baselayerchange', (e) ->
          for layerName, layerId of layers
            layerControl = $("span[data-layer='#{layerId}']")

            if layerName == e.name
              layerControl.addClass("is-selected")
            else
              layerControl.removeClass("is-selected")
        )

        terrainControl = $("span[data-layer='Terrain']")
        terrainControl.addClass("is-selected")

      # add marine style
      if opts.style == 'marine'
        marine.addTo(map)

  return BaseLayer
)

Add option to set a style for the map and hide the terrain options


angular.module("admin.subscriptions").controller "SubscriptionController", ($scope, Subscription, SubscriptionForm, customers, schedules, paymentMethods, shippingMethods) ->
  $scope.subscription = new Subscription()
  $scope.errors = null
  $scope.save = null
  $scope.customers = customers
  $scope.schedules = schedules
  $scope.paymentMethods = paymentMethods
  $scope.shippingMethods = shippingMethods
  $scope.distributor_id = $scope.subscription.shop_id # variant selector requires distributor_id
  $scope.view = if $scope.subscription.id? then 'review' else 'details'
  $scope.nextCallbacks = {}
  $scope.backCallbacks = {}
  $scope.creditCards = []
  $scope.setView = (view) -> $scope.view = view
  $scope.stepTitleFor = (step) -> t("admin.subscriptions.steps.#{step}")
  $scope.registerNextCallback = (view, callback) => $scope.nextCallbacks[view] = callback
  $scope.registerBackCallback = (view, callback) => $scope.backCallbacks[view] = callback
  $scope.next = -> $scope.nextCallbacks[$scope.view]()
  $scope.back = -> $scope.backCallbacks[$scope.view]()

  $scope.shipAddressFromBilling = =>
    angular.extend($scope.subscription.ship_address, $scope.subscription.bill_address)

  $scope.$watch 'subscription_form', ->
    form = new SubscriptionForm($scope.subscription_form, $scope.subscription)
    $scope.errors = form.errors
    $scope.save = form.save

angular.module("admin.subscriptions").controller "SubscriptionController", ($scope, Subscription, SubscriptionForm, Customers, Schedules, PaymentMethods, ShippingMethods) ->
  $scope.subscription = new Subscription()
  $scope.errors = null
  $scope.save = null
  $scope.customers = Customers.all
  $scope.schedules = Schedules.all
  $scope.paymentMethods = PaymentMethods.all
  $scope.shippingMethods = ShippingMethods.all
  $scope.distributor_id = $scope.subscription.shop_id # variant selector requires distributor_id
  $scope.view = if $scope.subscription.id? then 'review' else 'details'
  $scope.nextCallbacks = {}
  $scope.backCallbacks = {}
  $scope.creditCards = []
  $scope.setView = (view) -> $scope.view = view
  $scope.stepTitleFor = (step) -> t("admin.subscriptions.steps.#{step}")
  $scope.registerNextCallback = (view, callback) => $scope.nextCallbacks[view] = callback
  $scope.registerBackCallback = (view, callback) => $scope.backCallbacks[view] = callback
  $scope.next = -> $scope.nextCallbacks[$scope.view]()
  $scope.back = -> $scope.backCallbacks[$scope.view]()

  $scope.shipAddressFromBilling = =>
    angular.extend($scope.subscription.ship_address, $scope.subscription.bill_address)

  $scope.$watch 'subscription_form', ->
    form = new SubscriptionForm($scope.subscription_form, $scope.subscription)
    $scope.errors = form.errors
    $scope.save = form.save

Use services to access injected data arrays


Marbles.Views.ProfileAvatar = class ProfileAvatarView extends Marbles.Views.ProfileComponent
  @template_name: '_profile_avatar'
  @view_name: 'profile_avatar'

  constructor: ->
    super

    @on 'ready', @checkImageMortality

    @render()

  checkImageMortality: =>
    img = Marbles.DOM.querySelector('img', @el)
    unless img.complete
      return setTimeout @checkImageMortality, 10

    # Fallback to default avatar if image fails to load
    if img.naturalHeight == 0 && (url = TentStatus.config.defaultAvatarURL(@get('entity')))
      img.src = url

Marbles.Views.ProfileAvatar = class ProfileAvatarView extends Marbles.Views.ProfileComponent
  @template_name: '_profile_avatar'
  @view_name: 'profile_avatar'

  constructor: ->
    super

    @on 'ready', @checkImageMortality

  checkImageMortality: =>
    img = Marbles.DOM.querySelector('img', @el)
    unless img.complete
      return setTimeout @checkImageMortality, 10

    # Fallback to default avatar if image fails to load
    if img.naturalHeight == 0 && (url = TentStatus.config.defaultAvatarURL(@get('entity')))
      img.src = url

Fix profile avatar component: Don't render until profile fetch has been attempted


DiffLine = require './diff-line'
ListItem = require '../list-item'
{git} = require '../../git'
{File} = require 'pathwatcher'

_ = require 'underscore'

##
# A DiffChunk represents one consecutive block of altered lines. The end-goal of
# breaking them out separately is to be able to stage them individually.
#
module.exports =
class DiffChunk extends ListItem
  initialize: (options) ->
    chunk = @deleteFirstLine options.chunk
    chunk = @deleteInitialWhitespace chunk
    chunk = @deleteTrailingWhitespace chunk
    @lines = _.map @splitIntoLines(chunk), (line) ->
      new DiffLine line: line

  deleteTrailingWhitespace: (chunk) ->
    chunk.replace /\s*$/, ""

  deleteFirstLine: (chunk) ->
    chunk.replace /.*?\n/, ""

  deleteInitialWhitespace: (chunk) ->
    chunk.replace /^(\s*?\n)*/, ""

  splitIntoLines: (chunk) ->
    chunk.split /\n/g

  patch: ->
    @get("header") + @get("chunk") + "\n"

  kill: ->
    (new File @patchPath()).write(@patch())
    git.git "apply --reverse #{@patchPath()}"

  stage: ->
    (new File @patchPath()).write(@patch())
    git.git "apply --cached #{@patchPath()}"

  unstage: ->
    (new File @patchPath()).write(@patch())
    git.git "apply --cached --reverse #{@patchPath()}"

  patchPath: ->
    atom.project.getRepo().getWorkingDirectory() + ".git/atomatigit_diff_patch"

path = require 'path'

DiffLine = require './diff-line'
ListItem = require '../list-item'
{git} = require '../../git'
{File} = require 'pathwatcher'

_ = require 'underscore'

##
# A DiffChunk represents one consecutive block of altered lines. The end-goal of
# breaking them out separately is to be able to stage them individually.
#
module.exports =
class DiffChunk extends ListItem
  initialize: (options) ->
    chunk = @deleteFirstLine options.chunk
    chunk = @deleteInitialWhitespace chunk
    chunk = @deleteTrailingWhitespace chunk
    @lines = _.map @splitIntoLines(chunk), (line) ->
      new DiffLine line: line

  deleteTrailingWhitespace: (chunk) ->
    chunk.replace /\s*$/, ""

  deleteFirstLine: (chunk) ->
    chunk.replace /.*?\n/, ""

  deleteInitialWhitespace: (chunk) ->
    chunk.replace /^(\s*?\n)*/, ""

  splitIntoLines: (chunk) ->
    chunk.split /\n/g

  patch: ->
    @get("header") + @get("chunk") + "\n"

  kill: ->
    (new File @patchPath()).write(@patch())
    git.git "apply --reverse #{@patchPath()}"

  stage: ->
    (new File @patchPath()).write(@patch())
    git.git "apply --cached #{@patchPath()}"

  unstage: ->
    (new File @patchPath()).write(@patch())
    git.git "apply --cached --reverse #{@patchPath()}"

  patchPath: ->
    path.join git.getPath(), ".git/atomatigit_diff_patch"

Fix junks folders created for patch file


path = require('path')

class HubotSensor
  constructor: (@freq, active = true) ->
    @_id = null
    @room = process.env.HUBOT_SENSORS_ROOM

    @setup() if active

  setup: () ->
    @enable()

  check: () ->

  fire: () ->

  notify: (text) ->
    @robot.messageRoom @room, text

  name: () ->
    @constructor.name

  info: () ->
    "Info of #{@constructor.name}"

  enable: () ->
    @_id = setInterval @_loop, @freq

  disable: () ->
    clearInterval @_id
    @_id = null

  isActive: () ->
    @_id?

  _loop: () =>
    @fire() if @check()

module.exports = HubotSensor

path = require('path')

class HubotSensor
  constructor: (@freq, active = true) ->
    @_id = null
    @room = process.env.HUBOT_SENSORS_ROOM

    @setup() if active

  setup: () ->
    @enable()

  check: () ->

  fire: () ->

  notify: (text, rooms = [@room]) ->
    @robot.messageRoom(room, text) for room in [].concat rooms

  name: () ->
    @constructor.name

  info: () ->
    "Info of #{@constructor.name}"

  enable: () ->
    @_id = setInterval @_loop, @freq

  disable: () ->
    clearInterval @_id
    @_id = null

  isActive: () ->
    @_id?

  _loop: () =>
    @fire() if @check()

module.exports = HubotSensor

Add a rooms param to notify method to notify in multiple rooms


Form = require('multiparty').Form
createWriteStream = require('fs').createWriteStream
hash = require('crypto').createHash
rand = require('crypto').pseudoRandomBytes

JFUM = module.exports = (opts) ->
  opts ?= {}

  @tmpDir = opts.tmpDir or require('os').tmpdir()
  @minFileSize = opts.minFileSize or 1
  @maxFileSize = opts.maxFileSize or 10000000000
  @acceptFileTypes = opts.acceptFileTypes or /\.(gif|jpe?g|pg)$/i

  @

JFUM.prototype.optionsHandler = (req, res, next) ->
  res.set
    'Access-Control-Allow-Origin': '*'
    'Access-Control-Allow-Methods': 'OPTIONS, POST'
    'Access-Control-Allow-Headers': 'Content-Type'
  next()

JFUM.prototype.postHandler = (req, res, next) ->
  res.set 'Access-Control-Allow-Origin', '*'

  req.jfum = null

  form = new Form()
  form.on 'error', next
  form.on 'close', next
  form.on 'part', (part) =>
    return next() if not part.filename or not @acceptFileTypes.test part.filename

    req.jfum =
      name: part.filename
      file: @tmpDir + '/jfum-' + hash('sha1').update(rand(128)).digest('hex')
      size: part.byteCount
      type: part.headers['content-type']

    write = createWriteStream req.jfum.file
    write.on 'error', next
    part.pipe write

  form.parse req

Form = require('multiparty').Form
createWriteStream = require('fs').createWriteStream
hash = require('crypto').createHash
rand = require('crypto').pseudoRandomBytes

JFUM = module.exports = (opts) ->
  opts ?= {}

  @tmpDir = opts.tmpDir or require('os').tmpdir()
  @minFileSize = opts.minFileSize or 1
  @maxFileSize = opts.maxFileSize or 10000000000
  @acceptFileTypes = opts.acceptFileTypes or /\.(gif|jpe?g|png)$/i

  @

JFUM.prototype.optionsHandler = (req, res, next) ->
  res.set
    'Access-Control-Allow-Origin': '*'
    'Access-Control-Allow-Methods': 'OPTIONS, POST'
    'Access-Control-Allow-Headers': 'Content-Type'
  next()

JFUM.prototype.postHandler = (req, res, next) ->
  res.set 'Access-Control-Allow-Origin', '*'

  req.jfum = null

  form = new Form()
  form.on 'error', next
  form.on 'close', next
  form.on 'part', (part) =>
    return next() if not part.filename or not @acceptFileTypes.test part.filename

    req.jfum =
      name: part.filename
      file: @tmpDir + '/jfum-' + hash('sha1').update(rand(128)).digest('hex')
      size: part.byteCount
      type: part.headers['content-type']

    write = createWriteStream req.jfum.file
    write.on 'error', next
    part.pipe write

  form.parse req

Fix default supported file types

Fix default supported file types


#= require gravtastic

angular.module('kassa').directive('gravatar', [
  '$parse'
  ($parse)->
    linker = ($scope, $element, $attrs)->
      email = size = null
      setGravatarSrc = (email, size)->
        if email?
          $element.attr('src', Gravtastic(email, default: 'mm', size: (size ||= 22)))

      updateEmail = (newEmail)->
        return if newEmail == email
        email = newEmail
        setGravatarSrc(email, size)
        email

      updateSize = (newSize)->
        return if newSize == size
        size = newSize
        setGravatarSrc(email, size)
        size

      $scope.$watch($attrs.gravatar, updateEmail)
      $scope.$watch($attrs.gravatarSize, updateSize)

    restrict: 'A', terminal: true, link: linker, scope: false
])

#= require gravtastic

angular.module('kassa').directive('gravatar', [
  '$parse'
  ($parse)->
    linker = ($scope, $element, $attrs)->
      email = size = null
      setGravatarSrc = (email, size)->
        email = "" unless email?
        $element.attr('src', Gravtastic(email, default: 'mm', size: (size ||= 22)))

      updateEmail = (newEmail)->
        return if newEmail == email
        email = newEmail
        setGravatarSrc(email, size)
        email

      updateSize = (newSize)->
        return if newSize == size
        size = newSize
        setGravatarSrc(email, size)
        size

      $scope.$watch($attrs.gravatar, updateEmail)
      $scope.$watch($attrs.gravatarSize, updateSize)

    restrict: 'A', terminal: true, link: linker, scope: false
])

Fix gravatar to show mystery man if no email is present


angular.module('loomioApp').factory 'MessageChannelService', ($http, Records, CommentModel, EventModel) ->
  new class MessageChannelService
    subscribeSuccess: (subscription, onMessageReceived) ->
      PrivatePub.sign(subscription)
      console.log 'subscribed to channel:', subscription.channel
      PrivatePub.subscribe subscription.channel, (data, channel) =>
        @messageReceived(data, onMessageReceived)

    subscribeTo: (channel, onMessageReceived) ->
      $http.post('/api/v1/faye/subscribe', channel: channel).then (response) =>
          subscription = response.data
          PrivatePub.sign(subscription)
          console.log 'subscribed response:', response
          PrivatePub.subscribe subscription.channel, (data, channel) =>
            @messageReceived(data, onMessageReceived)

    messageReceived: (data, onMessageReceived) ->
      console.log 'message received: ', data
      if data.memo?
        console.log 'new memo!', data.memo
        memo = data.memo
        if memo.kind == 'comment_destroyed'
          if comment = Records.comments.get(memo.data.comment_id)
            comment.destroy()
        if memo.kind == 'comment_updated'
          Records.comments.new(memo.data.comment)
          Records.import(memo.data)
        if memo.kind == 'comment_unliked'
          if comment = Records.comments.get(memo.data.comment_id)
            comment.removeLikerId(memo.data.user_id)

      if data.event?
        console.log 'new event', data.event
        Records.events.new(data.event)

      # maybe indent this one
      Records.import(data)
      onMessageReceived(data) if onMessageReceived?

angular.module('loomioApp').factory 'MessageChannelService', ($http, Records, CommentModel, EventModel) ->
  new class MessageChannelService
    subscribeSuccess: (subscription, onMessageReceived) ->
      PrivatePub.sign(subscription)
      console.log 'subscribed to channel:', subscription.channel
      PrivatePub.subscribe subscription.channel, (data, channel) =>
        @messageReceived(data, onMessageReceived)

    subscribeTo: (channel, onMessageReceived) ->
      $http.post('/api/v1/faye/subscribe', channel: channel).then (response) =>
          subscription = response.data
          PrivatePub.sign(subscription)
          console.log 'subscribed response:', response
          PrivatePub.subscribe subscription.channel, (data, channel) =>
            @messageReceived(data, onMessageReceived)

    messageReceived: (data, onMessageReceived) ->
      if data.memo?
        switch data.memo.kind
          when 'comment_destroyed'
            if comment = Records.comments.find(memo.data.comment_id)
              comment.destroy()
          when 'comment_updated'
            Records.comments.initialize(memo.data.comment)
            Records.import(memo.data)
          when 'comment_unliked'
            if comment = Records.comments.find(memo.data.comment_id)
              comment.removeLikerId(memo.data.user_id)

      if data.event?
        Records.events.initialize(data.event)

      # maybe indent this one
      Records.import(data)
      onMessageReceived(data) if onMessageReceived?

Fix live updating for discussions

Fix live updating for discussions


###
Analytics Payload Loader

This is the `analytics.js` file that loads the actual Analytics payload. It
utilizes the Friendly iFrames (FiF) technique in order to load the JavaScript
library without blocking the `window.onload` event.

@see http://goo.gl/VLDc3F More information on FiF technique
###
((url)->
  # Section 1
  iframe = document.createElement('iframe')
  iframe.src = "javascript:false"
  iframe.title = ""
  # iframe.role = "presentation" # a11y

  (iframe.frameElement or iframe).style.cssText = "width: 0; height: 0; border: 0"
  where = document.getElementsByTagName('script')
  where = where[where.length - 1]
  where.parentNode.insertBefore(iframe, where)

  # Section 2
  try
    doc = iframe.contentWindow.document
  catch err
    dom = document.domain
    iframe.src = "javascript:var d=document.open();d.domain='#{dom}';void(0);"
    doc = iframe.contentWindow.document

  doc.open()._l = ->
    js = this.createElement("script")

    this.domain = dom if(dom)
    js.id = "js-iframe-async"
    js.src = "@@base/#{url}"
    this.body.appendChild(js)

  doc.write('<body onload="window.inDapIF=true; document._l();">')
  doc.close()
)('@@payload_hash')

Add documentation for Analytics Payload Loader


init = ->
  $("[data-provider='summernote']").summernote
    height: 300
    maximumImageFileSize: 262144 # 256KB
    toolbar: [
      ['style', ['style']],
      ['font', ['bold', 'italic', 'underline', 'clear']],
      ['fontname', ['fontname']],
      ['color', ['color']],
      ['para', ['ul', 'ol', 'paragraph']],
      ['table', ['table']],
      ['insert', ['link', 'picture', 'video']],
      ['view', ['fullscreen', 'codeview', 'help']],
    ]

$(document).ready(init)

init = ->
  $("[data-provider='summernote']").summernote
    height: 300
    maximumImageFileSize: 262144 # 256KB
    callbacks: onBlurCodeview: -> $(this).val $(this).summernote('code') # Fix content not saved from codeview issue
    toolbar: [
      ['style', ['style']],
      ['font', ['bold', 'italic', 'underline', 'clear']],
      ['fontname', ['fontname']],
      ['color', ['color']],
      ['para', ['ul', 'ol', 'paragraph']],
      ['table', ['table']],
      ['insert', ['link', 'picture', 'video']],
      ['view', ['fullscreen', 'codeview', 'help']],
    ]

$(document).ready(init)

Fix content not saved from codeview

Fix content not saved from codeview


goog.provide 'app.Routes'

goog.require 'este.Routes'

class app.Routes extends este.Routes

  ###*
    @param {app.user.Store} userStore
    @constructor
    @extends {este.Routes}
  ###
  constructor: (userStore) ->
    @home = @route '/'
    @about = @route '/about'
    @songs = @route '/songs'
    @recentlyUpdatedSongs = @route '/songs/recently-updated'
    @song = @route '/:urlArtist/:urlName'

    @me = @route '/@me'
    @newSong = @route '/@me/songs/new'
    @trash = @route '/@me/songs/trash'
    @editSong = @route '/@me/songs/:id/edit'
    @mySong = @route '/@me/songs/:id'

    @api = @routes '/api',
      clientErrors: '/client-errors'
      songs: @routes '/songs',
        byUrl: '/:urlArtist/:urlName'
        id: '/:id'
        recentlyUpdated: '/recently-updated'
        search: '/search'

Add api search song route.

Add api search song route.


Flow.Browser = (_) ->
  _docs = signals []
  _hasDocs = lift _docs, (docs) -> docs.length > 0

  createDocView = ([ type, id, doc ]) ->
    _title = signal doc.title
    _date = signal new Date doc.modifiedDate
    _fromNow = lift _date, Flow.Util.fromNow

    load = ->
      _.loadNotebook id, doc

    purge = ->
      _.requestDeleteObject type, id, (error) ->
        if error
        else
          _docs.remove self

    self =
      id: id
      title: _title
      doc: doc
      date: _date
      fromNow: _fromNow
      load: load
      purge: purge

  storeNotebook = (id, doc, go) ->
    if id
      _.requestPutObject 'notebook', id, doc, (error) ->
        if error
          go error
        else
          for source, index in _docs() when source.id is id
            break
          _docs.splice index, 1, createDocView [ 'notebook', id, doc ]
          go null, id
    else
      id = uuid()
      _.requestPutObject 'notebook', id, doc, (error) ->
        if error
          go error
        else
          _docs.push createDocView [ 'notebook', id, doc ]
          go null, id

  loadNotebooks = ->
    _.requestObjects 'notebook', (error, objs) ->
      if error
        debug error
      else
        #XXX sort
        _docs map objs, createDocView

  link _.ready, ->
    loadNotebooks()

  link _.storeNotebook, storeNotebook

  docs: _docs
  hasDocs: _hasDocs
  loadNotebooks: loadNotebooks

Flow.Browser = (_) ->
  _docs = signals []

  _sortedDocs = lift _docs, (docs) ->
    sortBy docs, (doc) -> -doc.date().getTime()

  _hasDocs = lift _docs, (docs) -> docs.length > 0

  createDocView = ([ type, id, doc ]) ->
    _title = signal doc.title
    _date = signal new Date doc.modifiedDate
    _fromNow = lift _date, Flow.Util.fromNow

    load = ->
      _.loadNotebook id, doc

    purge = ->
      _.requestDeleteObject type, id, (error) ->
        if error
          debug error
        else
          _docs.remove self

    self =
      id: id
      title: _title
      doc: doc
      date: _date
      fromNow: _fromNow
      load: load
      purge: purge

  storeNotebook = (id, doc, go) ->
    if id
      _.requestPutObject 'notebook', id, doc, (error) ->
        if error
          go error
        else
          for source, index in _docs() when source.id is id
            break
          _docs.splice index, 1, createDocView [ 'notebook', id, doc ]
          go null, id
    else
      id = uuid()
      _.requestPutObject 'notebook', id, doc, (error) ->
        if error
          go error
        else
          _docs.push createDocView [ 'notebook', id, doc ]
          go null, id

  loadNotebooks = ->
    _.requestObjects 'notebook', (error, objs) ->
      if error
        debug error
      else
        #XXX sort
        _docs map objs, createDocView

  link _.ready, ->
    loadNotebooks()

  link _.storeNotebook, storeNotebook

  docs: _sortedDocs
  hasDocs: _hasDocs
  loadNotebooks: loadNotebooks

Sort files by modification time

Sort files by modification time


module.exports = ->
  # Project configuration
  @initConfig
    pkg: @file.readJSON 'package.json'

    # BDD tests on Node.js
    mochaTest:
      nodejs:
        src: ['spec/*.coffee']
        options:
          reporter: 'spec'

    # Coding standards
    coffeelint:
      components:
        files:
          src: ['spec/*.coffee', 'src/*.coffee', 'src/runtimes/*.coffee', 'src/utils/*.coffee']
        options:
          max_line_length:
            value: 100
            level: 'warn'

    # Protocol tests
    shell:
      msgflo:
        command: 'node bin/msgflo'
        options:
          async: true
      fbp_test:
        command: 'fbp-test --colors'

  # Grunt plugins used for building

  # Grunt plugins used for testing
  @loadNpmTasks 'grunt-mocha-test'
  @loadNpmTasks 'grunt-coffeelint'
  @loadNpmTasks 'grunt-shell-spawn'

  # Our local tasks
  @registerTask 'fbp-test', [
    'shell:msgflo'
    'shell:fbp_test'
    'shell:msgflo:kill'
  ]

  @registerTask 'build', 'Build the chosen target platform', (target = 'all') =>
    # nothing

  @registerTask 'test', 'Build and run automated tests', (target = 'all') =>
    @task.run 'coffeelint'
    @task.run 'build'
    @task.run 'mochaTest'
#    @task.run 'fbp-test'

  @registerTask 'default', ['test']

module.exports = ->
  # Project configuration
  @initConfig
    pkg: @file.readJSON 'package.json'

    # BDD tests on Node.js
    mochaTest:
      nodejs:
        src: ['spec/*.coffee']
        options:
          reporter: 'spec'
          grep: process.env.TESTS

    # Coding standards
    coffeelint:
      components:
        files:
          src: ['spec/*.coffee', 'src/*.coffee', 'src/runtimes/*.coffee', 'src/utils/*.coffee']
        options:
          max_line_length:
            value: 100
            level: 'warn'

    # Protocol tests
    shell:
      msgflo:
        command: 'node bin/msgflo'
        options:
          async: true
      fbp_test:
        command: 'fbp-test --colors'

  # Grunt plugins used for building

  # Grunt plugins used for testing
  @loadNpmTasks 'grunt-mocha-test'
  @loadNpmTasks 'grunt-coffeelint'
  @loadNpmTasks 'grunt-shell-spawn'

  # Our local tasks
  @registerTask 'fbp-test', [
    'shell:msgflo'
    'shell:fbp_test'
    'shell:msgflo:kill'
  ]

  @registerTask 'build', 'Build the chosen target platform', (target = 'all') =>
    # nothing

  @registerTask 'test', 'Build and run automated tests', (target = 'all') =>
    @task.run 'coffeelint'
    @task.run 'build'
    @task.run 'mochaTest'
#    @task.run 'fbp-test'

  @registerTask 'default', ['test']

Grunt: Allow to grep for tests using TESTS envvar


Pancakes.ApplicationRoute = Ember.Route.extend
  renderTemplate: (controller, model) ->
    @_super(controller, model)

    @render 'study_locations',
      into: 'application',
      outlet: 'studyLocations',
      controller: 'studyLocations'

  setupController: (controller) ->
    @_super(controller)

    @controllerFor('studyLocations').setProperties
      editable: false
      available: Pancakes.StudyLocation.find()

  redirect: ->
    @transitionTo 'event_searches.new'

# vim:ts=2:sw=2:et:tw=78

Pancakes.ApplicationRoute = Ember.Route.extend
  renderTemplate: (controller, model) ->
    @_super(controller, model)

    @render 'study_locations',
      into: 'application',
      outlet: 'studyLocations',
      controller: 'studyLocations'

  setupController: (controller) ->
    @_super(controller)

    @controllerFor('studyLocations').setProperties
      editable: false
      available: Pancakes.StudyLocation.find()

# vim:ts=2:sw=2:et:tw=78

Revert "Redirect / to /event_searches/new."

This reverts commit 0de8612155705207d54facc388020df1ceb82141.

The scope of the hook is too broad; it applies to _every_ route below
the application route.  A different approach is needed.


{open} = require 'io/file'

class Downloader
  constructor: (@url, @file, @callback) ->
    @_downloader = Cc["@mozilla.org/network/downloader;1"].createInstance()
    @_ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)

    @_downloader.QueryInterface Ci.nsIDownloader
    @_downloader.init this, @file

  @::__defineGetter__ 'file', ->
    @_file

  @::__defineSetter__ 'file', (file) ->
    if file.path?
      @_file = file
    else
      @_file = open file

  #
  # Starts the download. This is the only public method for this class.
  #
  download: ->
    @_httpChannel = ioService.newChannel @url, '', null
    @_httpChannel.QueryInterface Ci.nsIHttpChannel
    @_httpChannel.asyncOpen @_downloader, null

  @download: (url, file, callback) ->
    (new Downloader url, file, callback).download()

  #
  # nsIDownloader delegate callback function.
  #
  onDownloadComplete: (dl, req, ctxt, status, file) ->
     try
       match = @_httpChannel.getResponseHeader('Content-Disposition').match(/filename="([^"]+)"/i)
       suggestedFilename = match?[1]
     catch err
       match = url.match(/.+\/([^\/\?]+)/)
       suggestedFilename = match?[1]

     wesabe.callback @callback, req.status is 0, [file, suggestedFilename]

     delete @_httpChannel
     delete @_downloader
     delete @_ioService
     delete @callback

module.exports = Downloader

{open} = require 'io/file'

class Downloader
  constructor: (@url, @file, @callback) ->
    @_downloader = Cc["@mozilla.org/network/downloader;1"].createInstance()
    @_ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)

    @_downloader.QueryInterface Ci.nsIDownloader
    @_downloader.init this, @file

  @::__defineGetter__ 'file', ->
    @_file

  @::__defineSetter__ 'file', (file) ->
    if file.path?
      @_file = file
    else
      @_file = open file

  #
  # Starts the download. This is the only public method for this class.
  #
  download: ->
    @_httpChannel = @_ioService.newChannel @url, '', null
    @_httpChannel.QueryInterface Ci.nsIHttpChannel
    @_httpChannel.asyncOpen @_downloader, null

  @download: (url, file, callback) ->
    (new Downloader url, file, callback).download()

  #
  # nsIDownloader delegate callback function.
  #
  onDownloadComplete: (dl, req, ctxt, status, file) ->
     try
       match = @_httpChannel.getResponseHeader('Content-Disposition').match(/filename="([^"]+)"/i)
       suggestedFilename = match?[1]
     catch err
       match = url.match(/.+\/([^\/\?]+)/)
       suggestedFilename = match?[1]

     wesabe.callback @callback, req.status is 0, [file, suggestedFilename]

     delete @_httpChannel
     delete @_downloader
     delete @_ioService
     delete @callback

module.exports = Downloader

Fix regression when downloading a specific URL.

Copy-paste restructuring error. It used to be all in one function, now it's an ivar.


@XBlock =
  runtime: {}

  initializeBlock: (element) ->
    $element = $(element)
    children = @initializeBlocks($element)
    version = $element.data("runtime-version")
    initFnName = $element.data("init")
    if version? and initFnName?
      runtime = @runtime["v#{version}"](element, children)
      initFn = window[initFnName]
      block = initFn(runtime, element) ? {}
    else
      elementTag = $('<div>').append($element.clone()).html();
      console.log("Block #{elementTag} is missing data-runtime-version or data-init, and can't be initialized")
      block = {}

    block.element = element
    block.name = $element.data("name")

    block

  initializeBlocks: (element) ->
    $(element).immediateDescendents(".xblock").map((idx, elem) =>
      @initializeBlock elem
    ).toArray()

@XBlock =
  runtime: {}

  initializeBlock: (element) ->
    $element = $(element)
    children = @initializeBlocks($element)
    version = $element.data("runtime-version")
    initFnName = $element.data("init")
    if version? and initFnName?
      runtime = @runtime["v#{version}"](element, children)
      initFn = window[initFnName]
      block = initFn(runtime, element) ? {}
    else
      elementTag = $('<div>').append($element.clone()).html();
      console.log("Block #{elementTag} is missing data-runtime-version or data-init, and can't be initialized")
      block = {}

    block.element = element
    block.name = $element.data("name")

    $element.trigger("xblock-initialized")
    $element.data("initialized", true)
    block

  initializeBlocks: (element) ->
    $(element).immediateDescendents(".xblock").map((idx, elem) =>
      @initializeBlock elem
    ).toArray()

Add information to xblocks about when they complete initialization that's visible in javascript


request   = require 'supertest'
assert    = require 'assert'

req = request require '../../coffee/server'

describe '/CloudHealthCheck', ->
  it 'should return OK for MongoDB and Redis', (done) ->
    req.get '/CloudHealthCheck'
      .expect 200
      .expect (res) ->
        assert.deepEqual res.body, message: 'System OK'
      .end done

request   = require 'supertest'
assert    = require 'assert'

req = request require '../../coffee/server'

describe '/CloudHealthCheck', ->
  it 'should return 200 OK for GET request', (done) ->
    req.get '/CloudHealthCheck'
      .expect 200
      .expect (res) ->
        assert.deepEqual res.body, message: 'System OK'
      .end done

  it 'should return 200 OK for HEAD request', (done) ->
    req.get('/CloudHealthCheck').expect(200).end done

Add integration test for HEAD /CloudHealthCheck


expect = require('chai').expect
fs     = require('fs')

Parser = require('../lib/parser')

describe 'Parser', ->
  dummy  = fs.createReadStream('./test/fixtures/dummy.html')
  parser = new Parser(dummy)

  it 'lists available years', (done) ->
    parser.getYears (err, years) ->
      expect(err).to.be.null
      expect(years).to.be.eql years: ['2001', '2002', '2003', '2004']
      done()

expect = require('chai').expect
fs     = require('fs')

Parser = require('../lib/parser')

describe 'Parser', ->
  beforeEach ->
    @dummy  = fs.createReadStream('./test/fixtures/dummy.html')
    @parser = new Parser(@dummy)

  it 'lists available years', (done) ->
    @parser.getYears (err, years) ->
      expect(err).to.be.null
      expect(years).to.be.eql years: ['2001', '2002', '2003', '2004']
      done()

Use properties instead of local variables


try
  require '../src/window'
  Atom = require '../src/atom'
  window.atom = new Atom()
  window.atom.show() unless atom.getLoadSettings().exitWhenDone
  {runSpecSuite} = require './jasmine-helper'

  document.title = "Spec Suite"
  runSpecSuite './spec-suite'
catch error
  unless atom.getLoadSettings().exitWhenDone
    atom.getCurrentWindow().setSize(800, 600)
    atom.getCurrentWindow().center()
    atom.openDevTools()

  console.error(error.stack ? error)
  atom.exit(1) if atom.getLoadSettings().exitWhenDone

try
  require '../src/window'
  Atom = require '../src/atom'
  window.atom = new Atom()
  window.atom.show() unless atom.getLoadSettings().exitWhenDone
  {runSpecSuite} = require './jasmine-helper'

  document.title = "Spec Suite"
  runSpecSuite './spec-suite'
catch error
  if atom?.getLoadSettings().exitWhenDone
    atom.exit(1)
  else
    throw error

Remove window handling in catch block

Remove window handling in catch block

The catch block in index.html takes care of this


{CompositeDisposable} = require 'atom'
RestructuredPreviewView = require './restructured-preview-view'
url = require 'url'

module.exports = RestructuredPreview =
  restructuredPreviewView: null
  modalPanel: null
  subscriptions: null

  activate: (state) ->
    @subscriptions = new CompositeDisposable
    @subscriptions.add atom.commands.add 'atom-workspace', 'restructured-preview:show-preview': => @toggle()

    atom.workspace.addOpener( (uri) ->
      if /^restructured-preview:.*.rst$/.test(uri)
        view = new RestructuredPreviewView(uri)
        result = url.parse(uri)
        child = require('child_process').exec(
          "python #{__dirname}/rst2html.py #{result.pathname}", (error, stdout, stderr) ->
            view.setText(stdout)
        )

        console.log "#{view}"
        return view
    )

  deactivate: ->
    @subscriptions.dispose()

  serialize: ->

  toggle: ->
    editor = atom.workspace.getActiveTextEditor()
    path = editor.getPath()
    atom.workspace.open("restructured-preview://#{path}")

{CompositeDisposable} = require 'atom'
RestructuredPreviewView = require './restructured-preview-view'
url = require 'url'

module.exports = RestructuredPreview =
  restructuredPreviewView: null
  modalPanel: null
  subscriptions: null

  activate: (state) ->
    @subscriptions = new CompositeDisposable
    @subscriptions.add atom.commands.add 'atom-workspace', 'restructured-preview:show-preview': => @toggle()

    atom.workspace.addOpener( (uri) ->
      if /^restructured-preview:.*.rst$/.test(uri)
        view = new RestructuredPreviewView(uri)
        result = url.parse(uri)
        child = require('child_process').exec(
          "python #{__dirname}/rst2html.py #{result.pathname}", (error, stdout, stderr) ->
            view.setText(stdout)
        )

        console.log "#{view}"
        return view
    )

  deactivate: ->
    @subscriptions.dispose()

  serialize: ->

  toggle: ->
    editor = atom.workspace.getActiveTextEditor()
    path = editor.getPath()
    options =
      split: 'right'
      searchAllPanes: true
    atom.workspace.open("restructured-preview://#{path}", options)

Split to the right when opening preview

Split to the right when opening preview


App.Forms =

  disableEnter: ->
    $('form.js-enter-disabled').on('keyup keypress', (event) ->
      if event.which == 13
        e.preventDefault()
    )

  submitOnChange: ->
    $('.js-submit-on-change').unbind('change').on('change', ->
      $(this).closest('form').submit()
      false
    )

  toggleLink: ->
    $('.js-toggle-link').unbind('click').on('click', ->
      $($(this).data('toggle-selector')).toggle("down")
      if $(this).data('toggle-text') isnt undefined
        toggle_txt = $(this).text()
        $(this).text( $(this).data('toggle-text')  )
        $(this).data('toggle-text', toggle_txt)

      false
    )

  synchronizeInputs: ->
    $("[name='progress_bar[percentage]']").on
      input: ->
        $("[name='#{this.name}']").val($(this).val())

  hideOrShowFieldsAfterSelection: ->
    $("[name='progress_bar[kind]']").on
      change: ->
        title_field = $("[name^='progress_bar'][name$='[title]']").parent()

        if this.value == "primary"
          title_field.hide()
        else
          title_field.show()

    $("[name='progress_bar[kind]']").change()

  initialize: ->
    App.Forms.disableEnter()
    App.Forms.submitOnChange()
    App.Forms.toggleLink()
    App.Forms.synchronizeInputs()
    App.Forms.hideOrShowFieldsAfterSelection()
    false

App.Forms =

  disableEnter: ->
    $('form.js-enter-disabled').on('keyup keypress', (event) ->
      if event.which == 13
        e.preventDefault()
    )

  submitOnChange: ->
    $('.js-submit-on-change').unbind('change').on('change', ->
      $(this).closest('form').submit()
      false
    )

  toggleLink: ->
    $('.js-toggle-link').unbind('click').on('click', ->
      $($(this).data('toggle-selector')).toggle("down")
      if $(this).data('toggle-text') isnt undefined
        toggle_txt = $(this).text()
        $(this).text( $(this).data('toggle-text')  )
        $(this).data('toggle-text', toggle_txt)

      false
    )

  synchronizeInputs: ->
    $("[name='progress_bar[percentage]']").on
      input: ->
        $("[name='#{this.name}']").val($(this).val())

    $("[name='progress_bar[percentage]'][type='range']").trigger("input")

  hideOrShowFieldsAfterSelection: ->
    $("[name='progress_bar[kind]']").on
      change: ->
        title_field = $("[name^='progress_bar'][name$='[title]']").parent()

        if this.value == "primary"
          title_field.hide()
        else
          title_field.show()

    $("[name='progress_bar[kind]']").change()

  initialize: ->
    App.Forms.disableEnter()
    App.Forms.submitOnChange()
    App.Forms.toggleLink()
    App.Forms.synchronizeInputs()
    App.Forms.hideOrShowFieldsAfterSelection()
    false

Synchronize percentage for new progress bars

According to the HTML specification:

> The default value is the minimum plus half the difference between the
> minimum and the maximum, unless the maximum is less than the minimum,
> in which case the default value is the minimum.

So for new progress bars, we had a numeric value of `nil` and a range
value of `50`, meaning the input fields weren't in sync.

Manually triggering the event on the progress, while not an ideal
solution (ideally we would be able to define `0` as default), sets the
value of the numeric field.


jQuery ->
  $(".clickable").live "click", (e) ->
    window.location.href = $(this).find("a.primary-link").attr("href")

  $(".collapsible")
    .hover ->
      $(this).find(".collapse").slideDown()
    , ->
      $(this).find(".collapse").slideUp()
    .find(".collapse").hide()

  $(":input.date").dateinput
    format: "dddd, dd mmmm yyyy"

jQuery ->
  # Crude way to make large blocks .clickable by definiting a.primary-link in them
  $(".clickable").live "click", (e) ->
    window.location.href = $(this).find("a.primary-link").attr("href")

  # When .collapsible item is hovered in/out the .collapse elements inside
  # expand and collapse
  $(".collapsible")
    .hover ->
      $(this).find(".collapse").slideDown()
    , ->
      $(this).find(".collapse").slideUp()
    .find(".collapse").hide()

  # Apply date selector to all date inputs
  $(":input.date").dateinput
    format: "dddd, dd mmmm yyyy"

  # When a select box is changed search for other selects that
  # are linked via the autoset and autoset-param data attributes
  # and update them with the new value.
  $(document).on "change", "select", ->
    source_select = $ this
    $("select[data-autoset='##{this.id}']").each ->
      target_select = $ this
      param = target_select.data("autoset-param")
      new_value = source_select.find("option:selected").data(param)
      console.info source_select, target_select, param, new_value
      target_select.val(new_value)

  # When a select box is changed find any dependent elements and
  # hide or show based on whether the new value is blank or not.
  $(document).on "change", "select", ->
    source_select = $ this
    $("*[data-dependent='##{this.id}']").each ->
      target = $ this
      if source_select.val() != ""
        target.show()
      else
        target.hide()

Add autoset and dependent methods to select inputs.

* Autoset allows one select to influence another.
* Dependent shows or hides elements based on the value of a select.


".source.ruby":
  "Hashrocket":
    prefix: "="
    body: " => "

  "Context block":
    prefix: "context"
    body: """
      context "$1" do
        $2
      end
    """

  "Test block":
    prefix: "test"
    body: """
      test "$1" do
        $2
      end
    """

".source.coffee":
  "Describe block":
    prefix: "de"
    body: """
      describe "${1:description}", ->
        ${2:body}
    """
  "It block":
    prefix: "i"
    body: """
      it "$1", ->
        $2
    """
  "Before each":
    prefix: "be"
    body: """
      beforeEach ->
        $1
    """
  "After each":
    prefix: "af"
    body: """
      afterEach ->
        $1
    """
  "Expectation":
    prefix: "ex"
    body: "expect($1).to$2"
  "Console log":
    prefix: "log"
    body: "console.log $1"
  "Range array":
    prefix: "ra"
    body: "[[$1, $2], [$3, $4]]"
  "Point array":
    prefix: "pt"
    body: "[$1, $2]"
  "Key-value pair":
    prefix: ":"
    body: '${1:"${2:key}"}: ${3:value}'
  "Create Jasmine spy":
    prefix: "spy"
    body: 'jasmine.createSpy("${1:description}")$2'

".source.ruby":
  "Hashrocket":
    prefix: "="
    body: " => "

  "Curly braces block":
    prefix: "{"
    body: "{ |${1:variable}| $2 "

  "Context block":
    prefix: "context"
    body: """
      context "$1" do
        $2
      end
    """

  "Test block":
    prefix: "test"
    body: """
      test "$1" do
        $2
      end
    """

".source.coffee":
  "Describe block":
    prefix: "de"
    body: """
      describe "${1:description}", ->
        ${2:body}
    """
  "It block":
    prefix: "i"
    body: """
      it "$1", ->
        $2
    """
  "Before each":
    prefix: "be"
    body: """
      beforeEach ->
        $1
    """
  "After each":
    prefix: "af"
    body: """
      afterEach ->
        $1
    """
  "Expectation":
    prefix: "ex"
    body: "expect($1).to$2"
  "Console log":
    prefix: "log"
    body: "console.log $1"
  "Range array":
    prefix: "ra"
    body: "[[$1, $2], [$3, $4]]"
  "Point array":
    prefix: "pt"
    body: "[$1, $2]"
  "Key-value pair":
    prefix: ":"
    body: '${1:"${2:key}"}: ${3:value}'
  "Create Jasmine spy":
    prefix: "spy"
    body: 'jasmine.createSpy("${1:description}")$2'

Add Atom snippet for Ruby curly braces block


React = require 'react'
BS = require 'react-bootstrap'

ChapterSectionType = require './chapter-section-type'

module.exports = React.createClass
  displayName: 'LearningGuideProgressBar'

  propTypes:
    section:  ChapterSectionType.isRequired
    onPractice: React.PropTypes.func

  render: ->
    {section, chapter, onPractice} = @props
    level = section.current_level
    percent = Math.round((level / 1) * 100)
    color = switch
      when percent >  75 then 'high'
      when percent >= 50 then 'medium'
      else 'low'

    bar = <BS.ProgressBar className={color} now={percent} />
    if onPractice
      <BS.Button onClick={-> onPractice(section)} block>{bar}</BS.Button>
    else
      bar

React = require 'react'
BS = require 'react-bootstrap'

ChapterSectionType = require './chapter-section-type'

module.exports = React.createClass
  displayName: 'LearningGuideProgressBar'

  propTypes:
    section:  ChapterSectionType.isRequired
    onPractice: React.PropTypes.func

  render: ->
    {section, chapter, onPractice} = @props
    level = section.current_level
    percent = Math.round((level / 1) * 100)
    color = switch
      when percent >  75 then 'high'
      when percent >= 50 then 'medium'
      else 'low'

    bar = <BS.ProgressBar className={color} now={percent} />
    if onPractice
      tooltip = <BS.Tooltip>Click to practice</BS.Tooltip>
      <BS.OverlayTrigger placement='bottom' overlay={tooltip}>
        <BS.Button onClick={-> onPractice(section)} block>{bar}</BS.Button>
      </BS.OverlayTrigger>
    else
      bar

Add "Click to practice" tooltip to practice buttons