View.rowDetail refactoring. Code deduplication

--- jsapp/xlform_model_view/view.rowDetail.coffee
+++ jsapp/xlform_model_view/view.rowDetail.coffee
@@ -104,6 +104,23 @@
     insertInDOM: (rowView)->
       @_insertInDOM rowView.defaultRowDetailParent

+  viewRowDetail.Templates = {
+    textbox: (cid, key, key_label = key, input_class = '') ->
+      @field """<input type="text" name="#{key}" id="#{cid}" class="#{input_class}" />""", cid, key_label
+
+    checkbox: (cid, key, key_label = key, input_label = 'Yes') ->
+      @field """<input type="checkbox" name="#{key}" id="#{cid}"/> #{input_label}""", cid, key_label
+
+    field: (input, cid, key_label) ->
+      """
+

+        #{key_label}:
+
+          #{input}
+
+

+      """
+
   viewRowDetail.DetailViewMixins = {}

   viewRowDetail.DetailViewMixins.type =
@@ -127,28 +144,14 @@
   viewRowDetail.DetailViewMixins.hint = viewRowDetail.DetailViewMixins.default =
     html: ->
       @$el.addClass("card__settings__fields--active")
-      """
-

-        #{@model.key}:
-
-          <input type="text" name="#{@model.key}" id="#{@cid}" class="text" />
-
-

-      """
+      viewRowDetail.Templates.textbox @cid, @model.key, @model.key, 'text'
     afterRender: ->
       @listenForInputChange()

   viewRowDetail.DetailViewMixins.constraint_message =
     html: ->
       @$el.addClass("card__settings__fields--active")
-      """
-

-        Error Message:
-
-          <input type="text" name="#{@model.key}" id="#{@cid}" class="text" />
-
-

-      """
+      viewRowDetail.Templates.textbox @cid, @model.key, 'Error Message', 'text'
     insertInDOM: (rowView)->
       @_insertInDOM rowView.cardSettingsWrap.find('.card__settings__fields--validation-criteria').eq(0)
     afterRender: ->
@@ -172,17 +175,7 @@
   viewRowDetail.DetailViewMixins.constraint =
     html: ->
       @$el.addClass("card__settings__fields--active")
-      # Validation logic (i.e. constraint):
-      # #{@model.get("value")}
-      fldUid = _.uniqueId("row-detail-field")
-      """
-

-        Validation logic:
-
-          <input type="text" name="#{@model.key}" id="#{@cid}" />
-
-

-      """
+      viewRowDetail.Templates.textbox @cid, @model.key, 'Validation logic'
     afterRender: ->
       @listenForInputChange()
     insertInDOM: (rowView) ->
@@ -191,47 +184,20 @@
   viewRowDetail.DetailViewMixins.name =
     html: ->
       @fieldTab = "active"
-      # @listenTo @model, "change:value", ()=>
-      #   @render()
-      #   @afterRender()
       @$el.addClass("card__settings__fields--#{@fieldTab}")
-      """
-

-        #{@model.key}:
-
-          <input type="text" name="#{@model.key}" id="#{@cid}" class="text" />
-
-

-      """
+      viewRowDetail.Templates.textbox @cid, @model.key, @model.key, 'text'
     afterRender: ->
       @$el.find('input').eq(0).val(@model.get("value"))
       @listenForInputChange(transformFn: (value)-> $modelUtils.sluggify(value, lowerCase:false))
-    # insertInDom: (rowView)->
-    #   # default behavior...
-    #   rowView.defaultRowDetailParent.append(@el)

   viewRowDetail.DetailViewMixins.default =
     html: ->
       @fieldTab = "active"
-      # @listenTo @model, "change:value", ()=>
-      #   @render()
-      #   @afterRender()
       @$el.addClass("card__settings__fields--#{@fieldTab}")
-      """
-

-        #{@model.key}:
-
-          <input type="text" name="#{@model.key}" id="#{@cid}" class="text" />
-
-

-      """
+      viewRowDetail.Templates.textbox @cid, @model.key, @model.key, 'text'
     afterRender: ->
       @$el.find('input').eq(0).val(@model.get("value"))
       @listenForInputChange()
-
-    # insertInDom: (rowView)->
-    #   # default behavior...
-    #   rowView.defaultRowDetailParent.append(@el)

   viewRowDetail.DetailViewMixins.calculation =
     html: -> false
@@ -240,66 +206,30 @@
   viewRowDetail.DetailViewMixins._isRepeat =
     html: ->
       @$el.addClass("card__settings__fields--active")
-      """
-

-        Repeat:
-
-          <input type="checkbox" name="#{@model.key}" id="#{@cid}"/> Yes
-
-

-      """
+      viewRowDetail.Templates.checkbox @cid, @model.key, 'Repeat'
     afterRender: ->
       @listenForCheckboxChange()

   viewRowDetail.DetailViewMixins.required =
     html: ->
       @$el.addClass("card__settings__fields--active")
-      """
-

-        Required:
-
-          <input type="checkbox" name="#{@model.key}" id="#{@cid}"/> Yes
-
-

-      """
+      viewRowDetail.Templates.checkbox @cid, @model.key, 'Required'
     afterRender: ->
       @listenForCheckboxChange()

-      # inp = @$el.find("input")
-      # # to be moved into the model when XLF.configs.truthyValues is refactored
-      # isTrueValue = (@model.get("value") || "").toLowerCase() in $configs.truthyValues
-      # inp.prop("checked", isTrueValue)
-      # inp.change ()=> @model.set("value", inp.prop("checked"))
-
   viewRowDetail.DetailViewMixins.appearance =
     html: ->
       @$el.addClass("card__settings__fields--active")
       if @model._parent.constructor.key == 'group'
-        """
-

-          Appearance:
-
-            <input type="checkbox" name="#{@model.key}" id="#{@cid}"/>Show all questions in this groups on the same screen
-
-

-        """
+        viewRowDetail.Templates.checkbox @cid, @model.key, 'Appearance', 'Show all questions in this groups on the same screen'
       else
-        """
-

-          #{@model.key}:
-
-            <input type="text" name="#{@model.key}" id="#{@cid}" class="text" />
-
-

-        """
+        viewRowDetail.Templates.textbox @cid, @model.key, 'Appearance', 'text'
     afterRender: ->
       if @model._parent.constructor.key == 'group'
         $checkbox = @$('input[type=checkbox]').eq(0)
+        value_lookup = ['', 'field-list']
         $checkbox.on 'change', () =>
-          if $checkbox.prop('checked')
-            @model.set('value', 'field-list')
-          else
-            @model.set('value', '')
+          @model.set('value', value_lookup[+$checkbox.prop('checked')])
       else
         @listenForInputChange()


Fixed JS error in effective_price

Fixed JS error in effective_price

--- app/assets/javascripts/effective_price/initialize.js.coffee
+++ app/assets/javascripts/effective_price/initialize.js.coffee
@@ -15,7 +15,7 @@
 $(document).on 'keydown', "input[type='text'].effective_price", (event) ->
   allowed = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', ',', '.']

-  if event.key.length == 1 && event.metaKey == false && allowed.indexOf(event.key) == -1
+  if event.key && event.key.length == 1 && event.metaKey == false && allowed.indexOf(event.key) == -1
     event.preventDefault()


Call ErrorHandler on window

Call ErrorHandler on window

--- app/assets/javascripts/initializers/error_handler.js.coffee
+++ app/assets/javascripts/initializers/error_handler.js.coffee
@@ -22,7 +22,7 @@
     # The global error handler
     Ember.onerror = (error) ->
       logError("\n" + error.message + "\n" + error.stack + "\n")
-      ErrorNotifier.notify(error.message, error.stack)
+      window.ErrorNotifier.notify(error.message, error.stack)
       if ETahi.environment == 'development'
         throw error
       else


keep polling on content migrations page when pre-processing

for some reason a course copy can sometimes start in this state

closes #CNVS-27464

Change-Id: Ifd277763bee377c35bc303a42feace9d5ec43ef6
Reviewed-on: https://gerrit.instructure.com/74852
Tested-by: Jenkins
Reviewed-by: Jeremy Stanley <b3f594e10a9edcf5413cf1190121d45078c62290@instructure.com>
Product-Review: James Williams  <3a5f4ed991dbfa5c9ef76ad729d240ca12f62180@instructure.com>
QA-Review: James Williams  <3a5f4ed991dbfa5c9ef76ad729d240ca12f62180@instructure.com>

--- app/coffeescripts/models/ProgressingContentMigration.coffee
+++ app/coffeescripts/models/ProgressingContentMigration.coffee
@@ -2,32 +2,32 @@
   'Backbone'
   'compiled/models/ContentMigrationProgress'
   'compiled/collections/ContentMigrationIssueCollection'
-], (Backbone, ProgressModel, IssuesCollection) ->
+], (Backbone, ProgressModel, IssuesCollection) ->

   # Summary
-  #   Represents a model that is progressing through its
-  #   workflow_state steps.
-
+  #   Represents a model that is progressing through its
+  #   workflow_state steps.
+
   class ProgressingContentMigration extends Backbone.Model
-    initialize: (attr, options) ->
+    initialize: (attr, options) ->
       super
       @course_id = @collection?.course_id || options?.course_id || @get('course_id')
       @buildChildren()
       @pollIfRunning()
       @syncProgressUrl()

-    # Create child associations for this model. Each
+    # Create child associations for this model. Each
     # ProgressingMigration should have a ProgressModel
     # and an IssueCollection
-    #
-    # Creates:
+    #
+    # Creates:
     #   @progressModel
     #   @issuesCollection
     #
     # @api private

-    buildChildren: ->
-      @progressModel     = new ProgressModel
+    buildChildren: ->
+      @progressModel     = new ProgressModel
                              url: @get('progress_url')
                              course_id: @course_id

@@ -36,19 +36,19 @@
                              content_migration_id: @get('id')

     # Logic to determin if we need to start polling progress. Progress
-    # shouldn't need to be polled unless this migration is in a running
+    # shouldn't need to be polled unless this migration is in a running
     # state.
     #
     # @api private

-    pollIfRunning: -> @progressModel.poll() if @get('workflow_state') == 'running'
+    pollIfRunning: -> @progressModel.poll() if @get('workflow_state') == 'running' || @get('workflow_state') == 'pre_processing'

-    # Sometimes the progress url for this progressing migration might change or
+    # Sometimes the progress url for this progressing migration might change or
     # be added after initialization. If this happens, the @progressModel's url needs
     # to be updated to reflect the change.
     #
     # @api private

-    syncProgressUrl: ->
+    syncProgressUrl: ->
       @on 'change:progress_url', => @progressModel.set('url', @get('progress_url'))


fix disappearing file bug

fix disappearing file bug

test plan:
 - have two folders and one file in a folder
 - preview a file
 - when you close the preview, both files should still be there

fixes CNVS-22597
refs CNVS-22758

Change-Id: Idc3320b089b8e3cb6bce340999b10f8f8bb45ad9
Reviewed-on: https://gerrit.instructure.com/62376
Reviewed-by: Ryan Shaw <ea3cd978650417470535f3a4725b6b5042a6ab59@instructure.com>
Tested-by: Jenkins
QA-Review: Charles Kimball <46fea98154686b8132aa2745574cc8021812cb41@instructure.com>
Product-Review: Jeremy Stanley <b3f594e10a9edcf5413cf1190121d45078c62290@instructure.com>

--- app/coffeescripts/react_files/components/FilePreview.coffee
+++ app/coffeescripts/react_files/components/FilePreview.coffee
@@ -62,7 +62,7 @@
                 props.currentFolder.files.models
       folders = props.currentFolder.folders.models

-      items = _.extend files, folders
+      items = files.concat folders
       otherItems =  items.filter (item) ->
                       return true unless onlyIdsToPreview
                       item.id in onlyIdsToPreview


Updates the switch spec to point to new plugin directory

--- spec/javascripts/plugins/tbg-switch_spec.js.coffee
+++ spec/javascripts/plugins/tbg-switch_spec.js.coffee
@@ -1,4 +1,4 @@
-#=require plugins//tbg-switch
+#=require styleguide/plugins/tbg-switch

 describe 'Switch:', ->
   activeClass = "is-active"


Allow fs-plus exported object to be modified and spied upon

--- src/fs-plus.coffee
+++ src/fs-plus.coffee
@@ -652,4 +652,7 @@
 module.exports = new Proxy({}, {
   get: (target, key) ->
     fsPlus[key] ? fs[key]
+
+  set: (target, key, value) ->
+    fsPlus[key] = value
 })


[#78944478] run observer on init ot observer computed property

--- app/assets/javascripts/components/participant-selector-component.js.coffee
+++ app/assets/javascripts/components/participant-selector-component.js.coffee
@@ -15,7 +15,7 @@
   updateParticipantsList: (->
     Ember.run =>
       @set('availableParticipantsList', @get('availableParticipants'))
-  ).observes('availableParticipants.@each')
+  ).observes('availableParticipants').on('init')

   remoteUrl: (->
     "/filtered_users/non_participants/#{@get('taskId')}/%QUERY"


fix admin tools logging type dropdown a11y sadness

fixes CNVS-35656

test plan:
- navigate to the admin tools page and select the logging tab
- select a logging type from the dropdown
- note that the focus does not change to the form that appears and
  instead stays on the dropdown
- note that the number of select options does not change after the
  initial selection
- note that this is true with all 3 SRs we support

Change-Id: I98889fff221bd74145bcb56c16959290b6868eee
Reviewed-on: https://gerrit.instructure.com/106093
Reviewed-by: Clay Diffrient <9dff2e5c98626d20d2848250c411e8009465bb87@instructure.com>
Tested-by: Jenkins
QA-Review: Dan Sasaki
Product-Review: Felix Milea-Ciobanu <9384aa226ac9d3237ed0bc52e18ffcb251ee0bf7@instructure.com>

--- app/coffeescripts/views/accounts/admin_tools/LoggingContentPaneView.coffee
+++ app/coffeescripts/views/accounts/admin_tools/LoggingContentPaneView.coffee
@@ -40,8 +40,7 @@
       $target = $(e.target)
       value = $target.val()
       @$el.find(".loggingTypeContent").hide()
-      @$el.find(value).show().find("input").first().focus()
-      $target.find('[value=default]').remove()
+      @$el.find(value).show()

     initAuthLogging: ->
       unless @permissions.authentication
@@ -61,4 +60,4 @@
       unless @permissions.course
         return new Backbone.View

-      return new CourseLoggingContentView+      return new CourseLoggingContentView


Fix incorrect/misleading comment.

Fix incorrect/misleading comment.

--- background_scripts/completion.coffee
+++ background_scripts/completion.coffee
@@ -50,9 +50,10 @@
   pushMatchingRanges: (string,term,ranges) ->
     textPosition = 0
     # Split `string` into a (flat) list of pairs:
-    #   - splits[i%2] is unmatched text
-    #   - splits[(i%2)+1] is the following matched text (matching `term`)
-    #     (except for the final element, for which there is no following matched text).
+    #   - for i=0,2,4,6,...
+    #     - splits[i] is unmatched text
+    #     - splits[i+1] is the following matched text (matching `term`)
+    #       (except for the final element, for which there is no following matched text).
     # Example:
     #   - string = "Abacab"
     #   - term = "a"


add atom ignore files

add atom ignore files

--- atom/config.cson
+++ atom/config.cson
@@ -24,6 +24,10 @@
       "._*"
       "Thumbs.db"
       "docs/*"
+      "node_modules/*"
+      "gui/*"
+      "minifrac/*"
+      "core/*"
     ]
     packagesWithKeymapsDisabled: [
       "highlight-selected"


Exclude images from Fuzzy Finder

Exclude images from Fuzzy Finder

--- atom/config.cson
+++ atom/config.cson
@@ -33,6 +33,8 @@
       ".DS_Store"
       "._*"
       "Thumbs.db"
+      "*.png"
+      "*.jpg"
     ]
     followSymlinks: false
   editor:


atom: Update config file slightly

atom: Update config file slightly

--- atom/config.cson
+++ atom/config.cson
@@ -1,6 +1,9 @@
 "*":
   autoprefixer: {}
   core:
+    disabledPackages: [
+      "react"
+    ]
     themes: [
       "one-dark-ui"
       "cobalt2-syntax"
@@ -16,6 +19,7 @@
     showInvisibles: true
     softWrap: true
     softWrapAtPreferredLineLength: true
+    tabType: "soft"
   emmet: {}
   "exception-reporting":
     userId: "0f4e16a3-5040-01ba-b000-a0793a69fb44"
@@ -29,6 +33,10 @@
     useGitHubStyle: true
   minimap:
     plugins:
+      "highlight-selected": true
+      "highlight-selectedDecorationsZIndex": 0
+      pigments: true
+      pigmentsDecorationsZIndex: 0
       selection: true
       selectionDecorationsZIndex: 0
   "minimap-selection":
@@ -37,11 +45,19 @@
   "one-light-ui": {}
   pigments: {}
   react: {}
-  "tree-view":
-    hideVcsIgnoredFiles: false
+  "tree-view": {}
   welcome:
     showOnStartup: false
 ".angular.html.text":
+  editor:
+    preferredLineLength: 120
+".cake.source":
+  editor:
+    preferredLineLength: 120
+".cs.source":
+  editor:
+    preferredLineLength: 120
+".csx.source":
   editor:
     preferredLineLength: 120
 ".git-commit.text":
@@ -56,3 +72,18 @@
 ".js.jsx.react.source":
   editor:
     showInvisibles: true
+".js.regexp.replacement.source":
+  editor:
+    preferredLineLength: 120
+".js.regexp.source":
+  editor:
+    preferredLineLength: 120
+".js.source":
+  editor:
+    preferredLineLength: 120
+".json.source":
+  editor:
+    preferredLineLength: 120
+".nant-build.source":
+  editor:
+    preferredLineLength: 120


Add Atom terminal toggle keyamp

Add Atom terminal toggle keyamp

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -22,8 +22,9 @@
     # file operations
     'ctrl-o': 'fuzzy-finder:toggle-file-finder'

-    # toggle option bar
+    # toggle snap-on panes
     'cmd-o': 'outline-view:toggle'
+    'cmd-t': 'atom-ide-terminal:toggle'

 # vim-mode-plus
 # ----------------------------------------


feat(atom.keymap.cson): add keymaps to move upwards and downwards

Add keymaps to move upwards and downwards using ctrl+k and ctrl+j
respectively inside the Command Palette and Fuzzy Finder windows.

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -34,6 +34,13 @@
   'ctrl-cmd-o': 'cursor-history:prev-within-editor'

 'atom-text-editor[mini]':
+
+  # The keymaps below allow the user to move the cursor up and down when
+  # Command Palette and Fuzzy Finder are toggled:
+  'ctrl-k': 'core:move-up'
+  'ctrl-j': 'core:move-down'
+  'ctrl-p': 'core:move-up'
+  'ctrl-n': 'core:move-down'

   'ctrl-u': 'editor:delete-to-beginning-of-line'
   'ctrl-w': 'editor:delete-to-beginning-of-word'


Better names handling

Better names handling

--- 404.coffee
+++ 404.coffee
@@ -1,6 +1,7 @@
 FIREBASE_URL = "https://whereareyou.firebaseio.com/v1"
 POSITION = "position"

+myKey = undefined
 myPosition = undefined
 map = undefined
 markers = {}
@@ -12,7 +13,6 @@
 initialize = ->
   initFirebase()
   initMap()
-  initGeoloc()
   showAndroidAd()

 initFirebase = ->
@@ -29,8 +29,9 @@
   else
     myHandle = room.push()
     myHandle.child("name").set("Web")
+    myKey = myHandle.name()
     if localStorage?
-      localStorage[roomName] = myHandle.name()
+      localStorage[roomName] = myKey

   myPosition = myHandle.child(POSITION)

@@ -67,9 +68,12 @@
 childAdded = (snapshot) ->
   person = snapshot.val()
   personName = person.name
+
   positionRef = snapshot.ref().child(POSITION)
+  positionRef.on("value", positionCallback(personName))

-  positionRef.on("value", positionCallback(personName))
+  if snapshot.ref().name() == myKey
+    initGeoloc()

 positionCallback = (personName) ->
   (snapshot) ->


change type of transpose column

change type of transpose column

--- lib/slickgrid/transpose.coffee
+++ lib/slickgrid/transpose.coffee
@@ -32,7 +32,7 @@
             rerenderOnResize :true
             resizable:false
             sortable:false
-            type:"bigint"
+            type:"text"
             type_code: 20
             width: 175
         ].concat(@rows.map (row, index) =>
@@ -46,7 +46,7 @@
             rerenderOnResize :true
             resizable:false
             sortable:false
-            type:"bigint"
+            type:"text"
             type_code: 20
             width: 300
         )


Added a wConnect helper

Added a wConnect helper

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -19,6 +19,9 @@
   wEmit: (sel, args...)->
     $.wApp.wEmit(sel, args...)

+  wConnect: (signal, slot)->
+	  signal.listen(slot)
+
   wCall: (qid, fn)->
     $.wApp.wCall(qid, fn)


KF-81 - Inputs now using $.editable

KF-81 - Inputs now using $.editable

--- dkobo/koboform/static/js/xlform/view_xlformbuilder.coffee
+++ dkobo/koboform/static/js/xlform/view_xlformbuilder.coffee
@@ -381,17 +381,15 @@
       new XlfRowSelector(el: @$el.find(".expanding-spacer-between-rows").get(0), action: "click-add-row", survey: @survey)

     # .form-name maps to settings.form_title
-    @$(".form-name").editInPlace
-      callback: (u, ent)=>
+    @$(".form-name").editable
+      success: (u, ent)=>
         val = if ent then XLF.sluggify(ent) else ""
         @survey.settings.set("form_title", val)
-        if val then val else "..."

     # .display-title maps to first line of settings.description
-    @$(".display-title").editInPlace
-      callback: (u, ent)=>
+    @$(".display-title").editable
+      success: (u, ent)=>
         @survey.set("displayTitle", ent)
-        if ent then ent else "..."

     #.display-description maps to remaining lines of settings.description
     @$(".display-description").editInPlace


Removed 'Hello world'

Removed 'Hello world'

--- node-red.coffee
+++ node-red.coffee
@@ -6,7 +6,6 @@
   class NodeRed extends env.plugins.Plugin

     init: (@app, @framework, @config) =>
-      env.logger.info("Hello World")
       settings = {
         httpAdminRoot:"/red",
         httpNodeRoot: "/api",
@@ -20,7 +19,6 @@
       appie.use("/",express.static("public"));
       server = http.createServer(appie);

-      env.logger.info "RED init"
       RED.init(server,settings)
       appie.use(settings.httpAdminRoot,RED.httpAdmin)
       appie.use(settings.httpNodeRoot,RED.httpNode)
@@ -28,7 +26,6 @@

       @framework.on 'server listen', (context)=>
         finished = true
-        env.logger.info "RED start"
         RED.start()
         return
   return new NodeRed


Arch - Fix gulp config

Arch - Fix gulp config

--- gulp/tasks/build.coffee
+++ gulp/tasks/build.coffee
@@ -7,7 +7,7 @@
 gulp.task 'build',
   "Build project to prepare it for a deployment. Minify CSS & JS files and pack
   Python dependencies into #{paths.py.lib_file}.",
-  $.sequence 'clean:min', 'install_dependencies', 'ext', ['script', 'style', 'html', 'zip']
+  $.sequence 'clean:min', 'install_dependencies', 'ext', ['script', 'style', 'zip']


more syntax conv changes

more syntax conv changes

--- library/nucleus/restful.coffee
+++ library/nucleus/restful.coffee
@@ -74,7 +74,7 @@
         assert _.isFunction(limitation), noLimitation
         assert limitation.length is 3, wrongSignature
         assert _.isArray inherted = @$condition or []
-        @$condition = inherted.concat
+        return @$condition = inherted.concat
             limitation: limitation
             synopsis: synopsis

@@ -104,7 +104,7 @@
         identify = @constructor?.identify().underline
         p = (i, c) -> i.limitation request, response, c
         fails = "service #{identify} fails some conditions"
-        super request, response, (decision) =>
+        return super request, response, (decision) =>
             return decide no unless decision
             async.every conditions, p, (confirms) ->
                 return decide yes if confirms
@@ -160,8 +160,8 @@
         isContent = content isnt undefined
         noContent = "No valid content supplied"
         throw new Error noContent unless isContent
-        @emit "push", this, response, content
-        uploader = -> response.send content
+        try @emit "push", this, response, content
+        uploader = -> return response.send content
         prestreamer = @upstreamAsync "prepushing", =>
             poststreamer = @upstreamAsync "postpushing"
             uploader(); poststreamer response, content


1. Attribute `wildcards` became absolete 2. Added "free" function on query 3. Enhanced stop method (now deletes its ephemeral storage) 4. Set some cb to optional 5. Fixed a bug in `free` method

--- src/RedisPort.coffee
+++ src/RedisPort.coffee
@@ -41,9 +41,6 @@
 	# @property [Object] Subscription registry
 	subscriptions:    null

-	# @property [Object] Wildcard queries
-	wildcards:        null
-
 	# Constructor
 	#
 	# @param options [Object] Main options
@@ -82,7 +79,6 @@

 		@ephemerals    = {}
 		@subscriptions = {}
-		@wildcards     = {}

 		@subscriber = redis.createClient @redisPort, @redisHost, retry_max_delay: 10000

@@ -91,24 +87,32 @@

 			switch key
 				when "set"
+					sub = @subscriptions[pattern]
+					return log.debug "#{@id}: no subscription for #{pattern}" unless sub
+
 					if pattern.length - 1 is pattern.indexOf "*"
-						wildcard    = @wildcards[pattern]
 						serviceRole = channel.replace "__keyspace@0__:", ""

 						@get serviceRole, (error, service) ->
 							return log.error "get error #{error.message}" if error

-							wildcard.fn service
+							sub.regFn? service

 					else
-						sub = @subscriptions[pattern]
-						return log.debug "#{@id}: no subscription for #{pattern}" unless sub
-
 						@get sub.path, (error, service) =>
 							return log.error "get error #{error.message}" if error

-							sub.fn service
-				# when "expired"
+							sub.regFn? service
+
+				when "del"
+					sub = @subscriptions[pattern]
+					return log.debug "#{@id}: no subscription for #{pattern}" unless sub
+
+					if pattern.length - 1 is pattern.indexOf "*"
+						serviceRole = channel.replace "__keyspace@0__:", ""
+						sub.freeFn? serviceRole.replace "#{@rootPath}/services/", ""
+					else
+						sub.freeFn? sub.role

 		@subscriber.on "error", (error) =>
 			log.warn "Redis client error: #{error.message}"
@@ -130,13 +134,15 @@
 	stop: ->
 		log.debug "#{@id}: stopping"

-		@client.end()
-		@subscriber.end()
-
-		clearTimeout timeout for p, timeout of @ephemerals
-
-		@emit "stopped"
-		log.debug "#{@id}: stopped"
+		async.each (Object.keys @ephemerals), ((key, cb) =>
+			clearTimeout @ephemerals[key]
+			@del key, cb
+		), =>
+			@client.end()
+			@subscriber.end()
+
+			@emit "stopped"
+			log.debug "#{@id}: stopped"

 	# Persistantly set a key-value
 	#
@@ -148,8 +154,7 @@
 		p = @_cleanPath p
 		@client.set p, JSON.stringify(data), (error, result) ->
 			log.debug "#{@id}: set", p, result
-			return cb error if error
-			cb()
+			cb? error

 	# Set array of persistant key-values
 	#
@@ -157,9 +162,10 @@
 	# @param [Function] Callback function
 	#
 	mpset: (arr, cb) ->
-		return cb new Error "No 2D array" unless Array.isArray arr
-		return cb new Error "No 2D array" if arr.length and not Array.isArray arr[0]
-		async.each arr, ((a, cb) => @pset a[0], a[1], cb), cb
+		return cb? new Error "No 2D array" unless Array.isArray arr
+		return cb? new Error "No 2D array" if arr.length and not Array.isArray arr[0]
+		async.each arr, ((a, cb) => @pset a[0], a[1], cb), (error) ->
+			cb? error

 	# Get a value by key
 	#
@@ -171,8 +177,7 @@

 		@client.get p, (error, data) =>
 			log.debug "#{@id}: get", p
-			return cb error if error
-			cb null, JSON.parse data
+			cb error, JSON.parse data

 	# Delete by key
 	#
@@ -183,19 +188,17 @@
 		p = @_cleanPath p

 		@client.keys "#{p}*", (error, keys) =>
-			log.debug "#{@id}: del keys", "#{p}*", keys
-			return cb error if error
+			log.debug "#{@id}: keys", "#{p}*", keys
+			return cb? error if error

 			async.eachSeries keys, ((k, cb) =>
 				clearTimeout @ephemerals[k]

 				@client.del k, (error, result) =>
 					log.debug "#{@id}: del", k, result
-					return cb error if error
-					cb()
+					cb error
 			), (error) =>
-				return cb error if error
-				cb()
+				cb? error

 	# Delete multiple paths
 	#
@@ -203,8 +206,9 @@
 	# @param [Function] Callback function
 	#
 	mdel: (keys, cb) ->
-		return cb new Error "No array" unless Array.isArray keys
-		async.each keys, ((key, cb) => @del key, cb), cb
+		return cb? new Error "No array" unless Array.isArray keys
+		async.each keys, ((key, cb) => @del key, cb), (error) ->
+			cb? error

 	# Ephemeral set a key-value
 	#
@@ -217,7 +221,7 @@

 		@client.psetex p, @ephemeralExpire, JSON.stringify(data), (error, result) =>
 			log.debug "#{@id}: setex", p, @ephemeralExpire
-			return cb error if error
+			return cb? error if error

 			updateExpire = =>
 				@client.pexpire p, @ephemeralExpire, (error, result) =>
@@ -227,7 +231,7 @@

 			updateExpire()

-			cb()
+			cb?()

 	# Set multiple key-values
 	#
@@ -235,10 +239,11 @@
 	# @param [Function] Callback function
 	#
 	mset: (arr, cb) ->
-		return cb new Error "No 2D array" unless Array.isArray arr
-		return cb new Error "No 2D array" if arr.length and not Array.isArray arr[0]
-
-		async.each arr, ((a, cb) => @set a[0], a[1], cb), cb
+		return cb? new Error "No 2D array" unless Array.isArray arr
+		return cb? new Error "No 2D array" if arr.length and not Array.isArray arr[0]
+
+		async.each arr, ((a, cb) => @set a[0], a[1], cb), (error) ->
+			cb? error

 	# List all value by a path
 	#
@@ -288,21 +293,22 @@
 	# @param [Function] Callback function
 	#
 	free: (role, cb) ->
+		log.debug "#{@id}: free", role
+
 		p = @_cleanPath "services/#{role}"

 		@get p, (error, service) =>
-			return cb error if error
-
-			@del role, (error) ->
-				return cb error if error
-				cb()
+			return cb? error if error
+
+			@del p, (error) ->
+				cb? error, service

 	# Set a watch function on a role
 	#
 	# @param [String] Role name
 	# @param [Function] Watch function, is executed when the role is set
 	#
-	query: (role, fn) ->
+	query: (role, regFn, freeFn) ->
 		log.debug "#{@id}: query", role

 		p = @_cleanPath "services/#{role}"
@@ -310,17 +316,21 @@
 		subscriptionKey = "__keyspace@0__:#{p}"
 		@subscriber.psubscribe subscriptionKey

+		@subscriptions[subscriptionKey] =
+			path:   p
+			role:   role
+			regFn:  regFn
+			freeFn: freeFn
+
 		if role.length - 1 is role.indexOf "*"
-			@wildcards[subscriptionKey] = path: p, fn: fn, role: role
-
 			@getServices role, (error, services) ->
-				fn service for service in services
+				return log.error "#{@id}: Error get services: #{error.message}" if error
+				regFn service for service in services

 		else
-			@subscriptions[subscriptionKey] = path: p, fn: fn, role: role
-
 			@get p, (error, service) =>
-				fn service if service
+				return log.error "#{@id}: Error get: #{error.message}" if error
+				regFn service if service

 	# Get current known services
 	#


debug buttons hiding

debug buttons hiding

--- client/app/views/reset_view.coffee
+++ client/app/views/reset_view.coffee
@@ -49,8 +49,8 @@
         @buttons = $ "#buttons"
         @buttons.hide()

-        @passwordField1 = $("#reset-password1-field")
-        @passwordField2 = $("#reset-password2-field")
+        @passwordField1 = $ "#reset-password1-field"
+        @passwordField2 = $ "#reset-password2-field"

         @resetDataButton = $ "#reset-form-button"
         @resetDataButton.click @onDataSubmit


Publish incidents not deleted and filter unaccepted in doc details

--- client/controllers/articles.coffee
+++ client/controllers/articles.coffee
@@ -67,7 +67,9 @@
     source.title or source.url or (source.content?.slice(0,30) + "...")

   incidentsForSource: (source) ->
-    Incidents.find(articleId: Template.instance().selectedSourceId.get())
+    Incidents.find
+      articleId: Template.instance().selectedSourceId.get()
+      accepted: true

   locationsForSource: (source) ->
     locations = {}


Leave out the 'Available Options' bit.

Leave out the 'Available Options' bit.

--- optparse.coffee
+++ optparse.coffee
@@ -47,7 +47,7 @@
   # Return the help text for this **OptionParser**, listing and describing all
   # of the valid options, for `--help` and such.
   help: ->
-    lines = ['Available options:']
+    lines = []
     lines.unshift "#{@banner}\n" if @banner
     for rule in @rules
       spaces  = 15 - rule.longFlag.length


Refactor URL launching from Vomnibar.

Refactor URL launching from Vomnibar.

It's slightly strange how the classes and objects are structured in the
Vomnibar.

However, this refactors the code for launching URLs (including
Javascript URLs) such that we're not repeating the logix in two separate
places.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -166,11 +166,7 @@
         # text than that which is included in the URL associated with the primary suggestion.  Therefore, to
         # avoid a race condition, we construct the query from the actual contents of the input (query).
         query = Utils.createSearchUrl query, @lastReponse.engine.searchUrl if isCustomSearchPrimarySuggestion
-        @hide ->
-          openInNewTab &&= not Utils.hasJavascriptPrefix query
-          chrome.runtime.sendMessage
-            handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
-            url: query
+        @hide -> Vomnibar.getCompleter().launchUrl query, openInNewTab
       else
         completion = @completions[@selection]
         @hide -> completion.performAction openInNewTab
@@ -326,15 +322,18 @@
   # These are the actions we can perform when the user selects a result.
   completionActions:
     navigateToUrl: (url) -> (openInNewTab) ->
-      # If the URL is a bookmarklet (so, prefixed with "javascript:"), then we always open it in the current
-      # tab.
-      openInNewTab &&= not Utils.hasJavascriptPrefix url
-      chrome.runtime.sendMessage
-        handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
-        url: url
+      Vomnibar.getCompleter().launchUrl url, openInNewTab

     switchToTab: (tabId) -> ->
       chrome.runtime.sendMessage handler: "selectSpecificTab", id: tabId
+
+  launchUrl: (url, openInNewTab) ->
+    # If the URL is a bookmarklet (so, prefixed with "javascript:"), then we always open it in the current
+    # tab.
+    openInNewTab &&= not Utils.hasJavascriptPrefix url
+    chrome.runtime.sendMessage
+      handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
+      url: url

 UIComponentServer.registerHandler (event) ->
   switch event.data.name ? event.data


Find .auto-complete-search-list in own element

--- core/app/backbone/views/channels/auto_complete_search_channels_view.coffee
+++ core/app/backbone/views/channels/auto_complete_search_channels_view.coffee
@@ -9,7 +9,7 @@

   template: "channels/auto_complete_search_channel"

-  scrollIntoView: -> scrollIntoViewWithinContainer @$el, $('.auto-complete-search-list')
+  scrollIntoView: -> scrollIntoViewWithinContainer @$el, @$('.auto-complete-search-list')

   templateHelpers: ->
     query = @options.query


fix typos

fix typos

--- src/brain.coffee
+++ src/brain.coffee
@@ -27,13 +27,13 @@
   #
   # Returns int
   llen: (key) ->
-    Q(@_private[@key key].length)
+    Q(@data._private[@key key].length)

   # Public: set the list value at the specified index
   #
   # Returns promise
   lset: (key, index, value) ->
-    @_private[@key key][index] = @serialize value
+    @data._private[@key key][index] = @serialize value
     Q @

   # Public: insert a value into the list before or after the pivot element.
@@ -41,11 +41,11 @@
   # Returns promise
   linsert: (key, placement, pivot, value) ->
     key = @key key
-    if @_private[key] isnt undefined
-      index = _.find @_private[key], pivot
+    if @data._private[key] isnt undefined
+      index = _.find @data._private[key], pivot

       if index
-        @_private[key].splice 0, placement is 'AFTER' ? index + 1 : index, @serialize(value)
+        @data._private[key].splice 0, placement is 'AFTER' ? index + 1 : index, @serialize(value)

     Q @

@@ -54,10 +54,10 @@
   # Returns promise
   lpush: (key, value) ->
     key = @key key
-    if @_private[key] is undefined
-      @_private[key] = []
-
-    @_private[key].unshift(@serialize value)
+    if @data._private[key] is undefined
+      @data._private[key] = []
+
+    @data._private[key].unshift(@serialize value)
     Q @

   # Public: push a new value onto the right-side of the list
@@ -65,29 +65,29 @@
   # Returns promise
   rpush: (key, value) ->
     key = @key key
-    if @_private[key] is undefined
-      @_private[key] = []
-
-    @_private[key].push(@serialize value)
+    if @data._private[key] is undefined
+      @data._private[key] = []
+
+    @data._private[key].push(@serialize value)
     Q @

   # Public: pop a value off of the left-side of the list
   #
   # Returns promise for list item
   lpop: (key) ->
-    Q(@deserialize(@_private[@key key].shift()))
+    Q(@deserialize(@data._private[@key key].shift()))

   # Public: pop a value off of the right-side of the list
   #
   # Returns promise for list item
   rpop: (key) ->
-    Q(@deserialize(@_private[@key key].pop()))
+    Q(@deserialize(@data._private[@key key].pop()))

   # Public: get a list item by index
   #
   # Returns promise for list item
   lindex: (key, index) ->
-    Q(@deserialize(@_private[@key key][index]))
+    Q(@deserialize(@data._private[@key key][index]))

   # Public: get a slice of the list
   #
@@ -96,21 +96,21 @@
     key = @key key

     if end < 0
-      end = @_private[key].length + end
-
-    Q(_.map(@_private[key].slice(start, end + 1), @deserialize.bind(@)))
+      end = @data._private[key].length + end
+
+    Q(_.map(@data._private[key].slice(start, end + 1), @deserialize.bind(@)))

   # Public: Add a member to the set specified by `key`
   #
   # Returns promise
   sadd: (key, value) ->
     key = @key key
-    if @_private[key] is undefined
-      @_private[key] = []
+    if @data._private[key] is undefined
+      @data._private[key] = []

     #TODO behavior with objects may not be what's expected. maybe make JSON.stringify the default serializer?
-    if not _.contains @private[key], value
-      @_private[key].push(@serialize value)
+    if not _.contains @data._private[key], value
+      @data._private[key].push(@serialize value)

     Q @

@@ -118,16 +118,16 @@
   #
   # Returns promise for boolean
   sismember: (key, value) ->
-    Q(_.contains @_private[key], value)
+    Q(_.contains @data._private[key], value)

   # Public: Remove a member from the set
   #
   # Returns promise
   srem: (key, value) ->
     key = @key key
-    index = _.findIndex @_private[key], value
+    index = _.findIndex @data._private[key], value
     if index
-      @_private[key].splice(1, index)
+      @data._private[key].splice(1, index)

     Q @

@@ -135,17 +135,17 @@
   #
   # Returns promise for int
   scard: (key) ->
-    Q @_private[@key key].length
+    Q @data._private[@key key].length

   # Public: Get and remove a random member from the set
   #
   # Returns promise for a set member
   spop: (key) ->
     key = @key key
-    index = _.random 0, @_private[key].length - 1
-    item = @_private[key][index]
-
-    @_private[key].splice(1, index)
+    index = _.random 0, @data._private[key].length - 1
+    item = @data._private[key][index]
+
+    @data._private[key].splice(1, index)

     Q item

@@ -154,13 +154,13 @@
   # Returns promise for a set member
   srandmember: (key) ->
     key = @key key
-    Q @_private[key][_.random 0, @_private[key].length - 1]
+    Q @data._private[key][_.random 0, @data._private[key].length - 1]

   # Public: Get all the members of the set
   #
   # Returns promise for array
   smembers: (key) ->
-    Q @_private[key]
+    Q @data._private[key]

   # Public: get all the keys, optionally restricted to keys prefixed with `searchKey`
   #
@@ -213,41 +213,41 @@
   #
   # Returns promise for array.
   hkeys: (table) ->
-    Q(_.keys(@_private[@key table] or {}))
+    Q(_.keys(@data._private[@key table] or {}))

   # Public: Get all the values for the given hash table name
   #
   # Returns promise for array.
   hvals: (table) ->
-    Q(_.mapValues(@_private[@key table] or {}, @deserialize.bind(@)))
+    Q(_.mapValues(@data._private[@key table] or {}, @deserialize.bind(@)))

   # Public: Set a value in the specified hash table
   #
   # Returns promise for the value.
   hset: (table, key, value) ->
     table = @key table
-    @_private[table] = @_private[table] or {}
-    @_private[table][key] = @serialize value
+    @data._private[table] = @data._private[table] or {}
+    @data._private[table][key] = @serialize value
     Q @

   # Public: Get a value from the specified hash table.
   #
   # Returns: promise for the value.
   hget: (table, key) ->
-    Q(@deserialize @_private[@key table][key])
+    Q(@deserialize @data._private[@key table][key])

   # Public: Delete a field from a hash table
   #
   # Returns promise
   hdel: (table, key) ->
-    delete @_private[@key table][key]
+    delete @data._private[@key table][key]
     Q @

   # Public: Get the whole hash table as an object.
   #
   # Returns: promise for object.
   hgetall: (table) ->
-    Q(_.clone(_.mapValues(@_private[@key table], @deserialize.bind @)))
+    Q(_.clone(_.mapValues(@data._private[@key table], @deserialize.bind @)))

   # Public: increment the hash value by num atomically
   #


Preview comment as plain text when not rendered in backend yet

--- core/app/backbone/views/sub_comments/react_sub_comment.coffee
+++ core/app/backbone/views/sub_comments/react_sub_comment.coffee
@@ -6,6 +6,15 @@
         ]

 React.defineBackboneClass('ReactSubComment')
+  renderContent: ->
+    if @model().get('formatted_comment_content')
+      _span ["subcomment-content spec-subcomment-content",
+        dangerouslySetInnerHTML: {__html: @model().get('formatted_comment_content')}]
+    else
+      _span ["subcomment-content spec-subcomment-content"],
+        @model().get('content')
+
+
   render: ->
     creator = @model().creator()

@@ -21,9 +30,7 @@
           ' '
           Factlink.Global.t.ago

-      R.span
-        className: "subcomment-content spec-subcomment-content",
-        dangerouslySetInnerHTML: {__html: @model().get('formatted_comment_content')}
+      @renderContent()

       if @model().can_destroy()
         window.ReactDeleteButton


fix(generators/app/templates): fix comment

--- generators/app/templates/@Gruntfile.coffee
+++ generators/app/templates/@Gruntfile.coffee
@@ -1065,8 +1065,8 @@
                     linkNatives:        true
                     tabtospace:         4

-                    ##              NOTE:   The list of YUIDoc documentation sets, as bundled with installed packages will be dynamically established
-                    ##                      when the 'doc' task is run.
+                    ##                  NOTE:   The list of external YUIDoc documentation sets, as bundled with installed packages will be dynamically
+                    ##                          established when the 'doc' task is run.
                     ##
                     external:           {}


allow to only add custom CSS for default theme

--- src/javascripts/theme.coffee
+++ src/javascripts/theme.coffee
@@ -73,12 +73,12 @@
       @loadCss(themeCss)
       @loadHtml(themeHtml)
     else if theme.constructor == String
-      @loadInternalTheme(theme)
+      @loadInternalTheme(theme, themeHtml, themeCss)

-  loadInternalTheme: (name) =>
+  loadInternalTheme: (name, themeHtml, themeCss) =>
     pathPrefix = "themes/#{name}/index"
-    @loadCss("#{pathPrefix}.css")
-    @loadHtml("#{pathPrefix}.html")
+    @loadCss(themeCss || "#{pathPrefix}.css")
+    @loadHtml(themeHtml || "#{pathPrefix}.html")

   loadHtml: (path) =>
     loaded = $.Deferred()


Delete context menu

Delete context menu

--- menus/notational-velocity.cson
+++ menus/notational-velocity.cson
@@ -1,11 +1,4 @@
 # See https://atom.io/docs/latest/creating-a-package#menus for more details
-'context-menu':
-  'atom-text-editor': [
-    {
-      'label': 'Toggle Notational Velocity'
-      'command': 'notational-velocity:toggle'
-    }
-  ]
 'menu': [
   {
     'label': 'Packages'


change email: catch invalid emails and don't pass the error back on invalid requests

--- services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee
+++ services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee
@@ -46,10 +46,14 @@
 			else if err? and err?.message?.indexOf("could not be validated") != -1
 				logger.log {oldEmail, newEmail},
 					"unable to change email in newsletter, user has previously unsubscribed or new email already exist on list"
-				return callback(err)
+				return callback()
 			else if err? and err.message.indexOf("is already a list member") != -1
 				logger.log {oldEmail, newEmail},
 					"unable to change email in newsletter, new email is already on mailing list"
+				return callback()
+			else if err? and err?.message?.indexOf("looks fake or invalid") != -1
+				logger.log {oldEmail, newEmail},
+					"unable to change email in newsletter, email looks fake to mailchimp"
 				return callback()
 			else if err?
 				logger.err {err, oldEmail, newEmail}, "error changing email in newsletter"


don't log doc contents if file is unchanged.

--- services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -217,7 +217,7 @@
 						logger.err "error putting doc #{doc_id} in project #{project_id} #{err}"
 						callback err
 					else if docComparitor.areSame docLines, doc.lines
-						logger.log sl_req_id: sl_req_id, docLines:docLines, project_id:project_id, doc_id:doc_id, rev:doc.rev, "old doc lines are same as the new doc lines, not updating them"
+						logger.log sl_req_id: sl_req_id, project_id:project_id, doc_id:doc_id, rev:doc.rev, "old doc lines are same as the new doc lines, not updating them"
 						callback()
 					else
 						logger.log sl_req_id: sl_req_id, project_id:project_id, doc_id:doc_id, docLines: docLines, oldDocLines: doc.lines, rev:doc.rev, "updating doc lines"


Mark visible toggle switch as hidden from screen readers

--- services/web/public/coffee/ide/editor/directives/toggleSwitch.coffee
+++ services/web/public/coffee/ide/editor/directives/toggleSwitch.coffee
@@ -32,6 +32,6 @@
     >
     {{labelTrue}}

-
+
   </fieldset>
 """


Option for 24hr clocks (/timeformat 24hr)

--- Web/js/classes/chat.coffee
+++ Web/js/classes/chat.coffee
@@ -49,7 +49,7 @@
 		else if ! command.indexOf 'logout'
 			loginMgr.logout()
 		else if ! command.indexOf 'password'
-			pass = text.substr( 10 )
+			pass = text.substr 10
 			if ! pass.length
 				loginMgr.forgetPassword()
 				@statusMessage 'Password removed.'
@@ -69,6 +69,21 @@
 					@statusMessage 'Invalid Regex: ' + res
 				else
 					@statusMessage 'Notification regex saved!'
+		else if ! command.indexOf 'timeformat'
+			fmt = text.substr 12
+			if fmt == '24hr'
+				window.rohStore.set 'clock format', '24hr'
+				@statusMessage 'Clock set to 24hr'
+			else if fmt == '12hr'
+				window.rohStore.set 'clock format', '12hr'
+				@statusMessage 'Clock set to 12hr'
+			else
+				@statusMessage 'Unknown clock format ' + fmt + '! Try 12hr or 24hr.'
+				return
+			@chat.find('time').each( (a,b) =>
+				me = $ b
+				me.text @formatTime new Date me.attr 'datetime'
+			)
 		else
 			return false
 		return true
@@ -100,19 +115,23 @@

 	formatTime: ( date ) ->
 		hours = date.getHours()
-		suffix = 'AM'
-		if hours >= 12
-			suffix = 'PM'
-			hours -= 12
-		if hours == 0
-			hours = 12
+		military = '24hr' == window.rohStore.get 'clock format'
+		unless military
+			suffix = 'AM'
+			if hours >= 12
+				suffix = 'PM'
+				hours -= 12
+			if hours == 0
+				hours = 12
 		minutes = date.getMinutes()
 		if hours < 10
 			hours = ' ' + hours
 		if minutes < 10
 			minutes = '0' + minutes
-
-		return "#{hours}:#{minutes} #{suffix}"
+		if military
+			return "#{hours}:#{minutes}"
+		else
+			return "#{hours}:#{minutes} #{suffix}"

 	linkify: (text) ->
 		# Put spaces infront of <s to stop urlize seizing them as urls


missed a comment.

missed a comment.

--- src/observable/property.coffee
+++ src/observable/property.coffee
@@ -152,7 +152,7 @@

   sourceChangeHandler: ->
     @_sourceChangeHandler ||= @_handleSourceChange.bind(@)
-    #Batman.developer.do => @_sourceChangeHandler.property = @
+    Batman.developer.do => @_sourceChangeHandler.property = @
     @_sourceChangeHandler

   _handleSourceChange: ->