コメント行の削除など

コメント行の削除など

--- gitlab.coffee
+++ gitlab.coffee
@@ -85,7 +85,7 @@

   handler = (mode, req, res) ->
     query = querystring.parse(url.parse(req.url).query)
-    hook = req.body
+    hook  = req.body

     switch mode
       when "system"
@@ -106,13 +106,11 @@
       when "web"
         message = ""
         branch = hook.ref.split("/")[2..].join("/")
-        console.log "branch: #{branch}"
         # if the ref before the commit is 00000, this is a new branch
         if /^0+$/.test(hook.before)
             message = "#{bold(hook.user_name)} pushed a new branch (#{bold(branch)}) to #{bold(hook.repository.name)} (#{underline(hook.repository.homepage)})"
         else
             message = "#{bold(hook.user_name)} pushed #{bold(hook.total_commits_count)} commits to #{bold(branch)} in #{bold(hook.repository.name)} (#{underline(hook.repository.homepage + '/compare/' + hook.before.substr(0,9) + '...' + hook.after.substr(0,9))})"
-        # robot.send user, message
         send_msg type, dst['target'], message

   robot.router.post "/gitlab/system", (req, res) ->


nginx: allow 127.0.0.0 block

nginx: allow 127.0.0.0 block

--- deployment/nginx.coffee
+++ deployment/nginx.coffee
@@ -25,7 +25,7 @@
       auth_basic_user_file  /etc/nginx/conf.d/.htpasswd;"""

 allowInternal = """
-  allow                 127.0.0.1;
+  allow                 127.0.0.0/8;
         allow                 192.168.0.0/16;
         allow                 172.16.0.0/12;
         deny                  all;


statusLED: socialworker error fix

statusLED: socialworker error fix

--- client/landing/app/MainApp/monitor.coffee
+++ client/landing/app/MainApp/monitor.coffee
@@ -87,7 +87,7 @@
             if item.indexOf(key.replace('*','')) isnt -1
               @socialWorkers++
               socialWorker = @monitorData[item]
-          socialWorker.count = @socialWorkers
+          socialWorker?.count = @socialWorkers or 0
           return socialWorker

   getOnlineServices:->


select field requires a model

select field requires a model

--- client/skr/components/SystemSettings.cjsx
+++ client/skr/components/SystemSettings.cjsx
@@ -18,6 +18,7 @@

             <SC.BankAccountFinder selectField
                 sm=3 label="Default Bank Account"
+                model={Lanes.config.system_settings}
                 choices={this.banks.models}
                 setSelection={@setBankAccount}
                 getSelection={@getBankAccount}


cleanup, use functional style as exercise


pizza.coffee: Add missing onConsoleMessage.

--- examples/pizza.coffee
+++ examples/pizza.coffee
@@ -1,6 +1,9 @@
 # Find pizza in New York using Google Local

 page = new WebPage()
+
+page.onConsoleMessage = (msg) ->
+  console.log(msg);

 page.open 'http://www.google.com/m/local?site=local&q=pizza+in+new+york',
   (status) ->


As it turned out replaceAll is a bit too new

--- resources/assets/coffee/osu_common.coffee
+++ resources/assets/coffee/osu_common.coffee
@@ -244,7 +244,7 @@
     # Wrapping the string with quotes and escaping the used quotes inside
     # is sufficient. Use double quote as it's easy to figure out with
     # encodeURI (it doesn't escape single quote).
-    if osu.present(url) then "url(\"#{String(url).replaceAll('"', '%22')}\")" else null
+    if osu.present(url) then "url(\"#{String(url).replace(/"/g, '%22')}\")" else null

   navigate: (url, keepScroll, {action = 'advance'} = {}) ->


trace

trace


Allow for feedback open to be triggered through the mediator

--- components/main_layout/footer/view.coffee
+++ components/main_layout/footer/view.coffee
@@ -1,14 +1,18 @@
 _             = require 'underscore'
 Backbone      = require 'backbone'
 FeedbackView  = require '../../feedback/view.coffee'
+mediator      = require '../../../lib/mediator.coffee'

 module.exports = class FooterView extends Backbone.View
   events:
     'click .mlf-feedback': 'feedback'

+  initialize: (options) ->
+    mediator.on 'open:feedback', @openFeedback, this
+
   feedback: (e) ->
     e.preventDefault()
+    mediator.trigger 'open:feedback'

-    # FeedbackView inherits ModalView which auto-renders
-    # and disposes of itself on close
+  openFeedback: ->
     new FeedbackView(width: '500px')


Get port number from environent variable

--- server/index.coffee
+++ server/index.coffee
@@ -2,4 +2,4 @@
 search = require("./config/search")

 search ->
-  server.listen 3000
+  server.listen process.env.PORT || 3000


add examples for CSV reading and writing

--- src/smc-webapp/buttonbar.coffee
+++ src/smc-webapp/buttonbar.coffee
@@ -522,6 +522,33 @@
             insert : "[1, 2, 5, 6, 10]"
         list_comprehension :
             insert : "[n+1 for n in range(10) if n%2==0]"
+        read_csv : """
+                        import csv
+                        import sys
+
+                        f = open('example.csv', 'rt')
+                        try:
+                            reader = csv.reader(f)
+                            for row in reader:
+                                print row
+                        finally:
+                            f.close()
+                        """
+        write_csv : """
+                        import csv
+                        import sys
+
+                        f = open('example.csv', 'wt')
+                        try:
+                            writer = csv.writer(f)
+                            writer.writerow( ('Title 1', 'Title 2', 'Title 3') )
+                            for i in range(10):
+                                writer.writerow( (i+1, chr(ord('a') + i), '08/%02d/07' % (i+1)) )
+                        finally:
+                            f.close()
+
+                        print open('example.csv', 'rt').read()
+                        """
         dict :
             insert : "{'sage':'math', 3:7}"
         set :
@@ -1349,7 +1376,10 @@
             ["Set", "#set"],
             ["Tuple", "#tuple"],
             ["Properties"],
-            ["Length", "#len"]
+            ["Length", "#len"],
+            ["CSV"],
+            ["Read CSV file", "#read_csv_file"],
+            ["Write CSV file", "#write_csv_file"]
            ]]
     add_menu(pybar, py_control)


webapp: take care of missing src in images when processing smc links -- fixes #1770

--- src/smc-webapp/misc_page.coffee
+++ src/smc-webapp/misc_page.coffee
@@ -160,6 +160,8 @@
                 for x in e.find(tag)
                     y = $(x)
                     src = y.attr(attr)
+                    if not src?
+                        continue
                     if src.indexOf('://') != -1
                         continue
                     {join} = require('path')


Add arrow key controls to project log

Add arrow key controls to project log

--- src/smc-webapp/project_log.cjsx
+++ src/smc-webapp/project_log.cjsx
@@ -26,7 +26,7 @@

 {React, ReactDOM, Actions, Store, Table, rtypes, rclass, Redux}  = require('./smc-react')
 {Col, Row, Button, ButtonGroup, ButtonToolbar, FormControl, FormGroup, InputGroup, Panel, Well} = require('react-bootstrap')
-{Icon, Loading, TimeAgo, FileLink, r_join, Space, Tip} = require('./r_misc')
+{Icon, Loading, TimeAgo, FileLink, r_join, SearchInput, Space, Tip} = require('./r_misc')
 {User} = require('./users')
 {file_action_buttons} = require('./project_files')
 {ProjectTitleAuto} = require('./projects')
@@ -47,19 +47,10 @@
         search           : rtypes.string
         actions          : rtypes.object.isRequired
         selected         : rtypes.object
-
-    clear_and_focus_input: ->
-        ReactDOM.findDOMNode(@refs.project_log_search).focus()
-        @props.actions.setState(search:'', page:0)
-
-    render_clear_button: ->
-        s = if @props.search?.length > 0 then 'warning' else "default"
-        <Button onClick={@clear_and_focus_input} bsStyle={s}>
-            <Icon name='times-circle' />
-        </Button>
-
-    do_open_selected: (e) ->
-        e.preventDefault()
+        increment_cursor : rtypes.func
+        decrement_cursor : rtypes.func
+
+    open_selected: (value, info) ->
         e = @props.selected?.event
         if not e?
             return
@@ -67,38 +58,27 @@
             when 'open'
                 target = e.filename
                 if target?
-                    @props.actions.open_file(path:target, foreground:true)
+                    @props.actions.open_file
+                        path       : target
+                        foreground : not info.ctrl_down
             when 'set'
                 @props.actions.set_active_tab('settings')

-    keydown: (e) ->
-        if e.keyCode == 27
-            @props.actions.setState(search:'', page:0)
-
-    on_change: (e) ->
-        e.preventDefault()
-        x = ReactDOM.findDOMNode(@refs.project_log_search).value
-        @props.actions.setState(search : x, page : 0)
+    on_change: (value) ->
+        @props.actions.setState(search : value, page : 0)

     render: ->
-        <form onSubmit={@do_open_selected}>
-            <FormGroup>
-                <InputGroup>
-                    <FormControl
-                        autoFocus
-                        type        = 'search'
-                        value       = @props.search
-                        ref         = 'project_log_search'
-                        placeholder = 'Search log...'
-                        onChange    = {@on_change}
-                        onKeyDown   = {@keydown}
-                        />
-                        <InputGroup.Button>
-                            {@render_clear_button()}
-                        </InputGroup.Button>
-                </InputGroup>
-            </FormGroup>
-        </form>
+        <SearchInput
+            autoFocus   = {true}
+            autoSelect  = {true}
+            placeholder = 'Search log...'
+            value       = {@props.search}
+            on_change   = {@on_change}
+            on_submit   = {@open_selected}
+            on_up       = {@props.decrement_cursor}
+            on_down     = {@props.increment_cursor}
+            on_escape   = {=> @props.actions.setState(search:'', page:0)}
+        />

 selected_item =
     backgroundColor : '#08c'
@@ -337,7 +317,12 @@
         search : ''   # search that user has requested
         page   : 0

-    shouldComponentUpdate: (nextProps) ->
+    getInitialState: ->
+        cursor_index : 0
+
+    shouldComponentUpdate: (nextProps, nextState) ->
+        if @state.cursor_index != nextState.cursor_index
+            return true
         if @props.search != nextProps.search
             return true
         if @props.page != nextProps.page
@@ -432,6 +417,16 @@
             log = (x for x in log when matches(x.search, words))
         return log

+    increment_cursor: ->
+        if @state.cursor_index == Math.min(PAGE_SIZE - 1, @displayed_log_size - 1)
+            return
+        @setState(cursor_index : @state.cursor_index + 1)
+
+    decrement_cursor: ->
+        if @state.cursor_index == 0
+            return
+        @setState(cursor_index : @state.cursor_index - 1)
+
     render_paging_buttons: (num_pages, cur_page) ->
         <ButtonGroup>
             <Button onClick={@previous_page} disabled={@props.page<=0} >
@@ -450,10 +445,10 @@
         num_pages = Math.ceil(log.length / PAGE_SIZE)
         page = @props.page
         log = log.slice(PAGE_SIZE*page, PAGE_SIZE*(page+1))
-        # make first visible entry appear "selected" (FUTURE: implement cursor to move)
+        @displayed_log_size = log.length
         if log.length > 0
-            cursor = log[0].id
-            selected = log[0]
+            cursor = log[@state.cursor_index].id
+            selected = log[@state.cursor_index]
         else
             cursor = undefined
             selected = undefined
@@ -461,7 +456,13 @@
         <Panel>
             <Row>
                 <Col sm=4>
-                    <LogSearch actions={@actions(name)} search={@props.search} selected={selected} />
+                    <LogSearch
+                        actions          = {@actions(name)}
+                        search           = {@props.search}
+                        selected         = {selected}
+                        increment_cursor = {@increment_cursor}
+                        decrement_cursor = {@decrement_cursor}
+                    />
                 </Col>
                 <Col sm=4>
                     {@render_paging_buttons(num_pages, @props.page)}


Fix subject viewer to render correctly

Fix subject viewer to render correctly

--- src/ubret/subject_viewer.coffee
+++ src/ubret/subject_viewer.coffee
@@ -3,19 +3,22 @@
 class SubjectViewer extends BaseTool
   name: 'Subject Viewer'

-  constructor: (opts) ->
-    super
-    @count = 0
+  constructor: (selector) ->
+    super selector

   start: =>
-    if @opts.selectedIds.length is 0
-      @selectIds [@opts.data[0].uid]
+    super
+
     subjects = _(@opts.data).filter (d) =>
       d.uid in @opts.selectedIds
-    @render(subjects)
+
+    if _.isEmpty subjects
+      @selectIds [@opts.data[0].uid]
+    else
+      @render(subjects)

   render: (subjects) =>
-    @div = d3.select(@selector)
+    @div = @opts.selector
     @div.selectAll('div.subject').remove()

     subject = @div.selectAll('div.subject')
@@ -24,7 +27,9 @@
         .attr('class', 'subject')

     subject.append('img')
-        .attr('src', (d) -> d.image)
+        .attr('src', (d) ->
+          console.log d.image
+          d.image)

     subject.append('ul').selectAll('ul')
       .data((d) => @toArray(d)).enter()


Update context menu format

Update context menu format

--- menus/autocomplete.cson
+++ menus/autocomplete.cson
@@ -1,3 +1,3 @@
 'context-menu':
   '.overlayer':
-    'Autocomplete': 'autocomplete:toggle'
+    [{label: 'Autocomplete', command: 'autocomplete:toggle'}]


fix duplicate article detection bug

fix duplicate article detection bug

--- methods/articles.coffee
+++ methods/articles.coffee
@@ -10,6 +10,7 @@
           title: source.title
           userEventId: source.userEventId
         existingArticle = Articles.find(
+          userEventId: source.userEventId
           url: source.url
           deleted:
             $in: [null, false]


remove script tag from content.

remove script tag from content.

--- app/assets/javascripts/api/crud_comment.js.coffee
+++ app/assets/javascripts/api/crud_comment.js.coffee
@@ -120,13 +120,9 @@
     abaa.className = "juvia-comment-pure-content juvia-comment-editable-content"
   else
     abaa.className = "juvia-comment-pure-content"
-
-  stringOfHtml = comment_text;
-  wrappedString = '<div>' + stringOfHtml + '</div>';
-  noScript = wrappedString.replace(/script/g, "THISISNOTASCRIPTREALLY");
-  html = $(noScript);
-  html.find('THISISNOTASCRIPTREALLY').remove();
-  $(abaa).html html
+
+  comment_content = $(comment_text).clone().find("script,noscript,style").remove().end().html();
+  $(abaa).html comment_content

   aba.appendChild abaa
   abab = document.createElement("div")


add help modal icon

add help modal icon

--- app/assets/javascripts/components/mark/index.cjsx
+++ app/assets/javascripts/components/mark/index.cjsx
@@ -239,6 +239,10 @@

+

+
+

           { if @getCurrentTask()? && @getCurrentSubject()?

                 <TaskComponent
@@ -250,33 +254,35 @@
                   subject={@getCurrentSubject()}
                 />

-
-                  { if false
-                    <button type="button" className="back minor-button" disabled={onFirstAnnotation} onClick={@destroyCurrentAnnotation}>Back</button>
-                  }
-                  { if @getNextTask() and not @state.badSubject?
-                      <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@advanceToNextTask}>Next</button>
-                    else
-                      if @state.taskKey == "completion_assessment_task"
-                        if @getCurrentSubject() == @getCurrentSubjectSet().subjects[@getCurrentSubjectSet().subjects.length-1]
-                          <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@completeSubjectAssessment}>Next</button>
+

+
+                    { if false
+                      <button type="button" className="back minor-button" disabled={onFirstAnnotation} onClick={@destroyCurrentAnnotation}>Back</button>
+                    }
+                    { if @getNextTask() and not @state.badSubject?
+                        <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@advanceToNextTask}>Next</button>
+                      else
+                        if @state.taskKey == "completion_assessment_task"
+                          if @getCurrentSubject() == @getCurrentSubjectSet().subjects[@getCurrentSubjectSet().subjects.length-1]
+                            <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@completeSubjectAssessment}>Next</button>
+                          else
+                            <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@completeSubjectAssessment}>Next Page</button>
                         else
-                          <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@completeSubjectAssessment}>Next Page</button>
-                      else
-                        <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@completeSubjectSet}>Done</button>
-                  }
-
-
-

-                  { if @getCurrentTask().help?
-                    <HelpButton onClick={@toggleHelp} label="" className="task-help-button" />
-                  }
-                  { if onFirstAnnotation
-                    <BadSubjectButton class="bad-subject-button" label={"Bad " + @props.project.term('subject')} active={@state.badSubject} onClick={@toggleBadSubject} />
-                  }
-                  { if @state.badSubject
-
You've marked this {@props.project.term('subject')} as BAD. Thanks for flagging the issue! Press DONE to continue.

-                  }
+                          <button type="button" className="continue major-button" disabled={waitingForAnswer} onClick={@completeSubjectSet}>Done</button>
+                    }
+
+
+

+                    { if @getCurrentTask().help?
+                      <HelpButton onClick={@toggleHelp} label="" className="task-help-button" />
+                    }
+                    { if onFirstAnnotation
+                      <BadSubjectButton class="bad-subject-button" label={"Bad " + @props.project.term('subject')} active={@state.badSubject} onClick={@toggleBadSubject} />
+                    }
+                    { if @state.badSubject
+
You've marked this {@props.project.term('subject')} as BAD. Thanks for flagging the issue! Press DONE to continue.

+                    }
+

           }


fixed dataset undifined while loading mappings. ##1351

--- app/assets/javascripts/oxalis/model/binary.coffee
+++ app/assets/javascripts/oxalis/model/binary.coffee
@@ -47,7 +47,7 @@
     @pullQueue = new PullQueue(datasetName, @cube, @layer, @boundingBox, @connectionInfo, datastoreInfo)
     @pushQueue = new PushQueue(datasetName, @cube, @layer, @tracing.id, updatePipeline)
     @cube.initializeWithQueues(@pullQueue, @pushQueue)
-    @mappings = new Mappings(@model.dataSetName, @layer)
+    @mappings = new Mappings(datasetName, @layer)
     @activeMapping = null

     @pingStrategies = [


Make removing the progress bar after the download optional

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -18,6 +18,7 @@
     path: ''
     additional_data: null
     before_add: null
+    remove_completed_progress_bar: true

   $.extend settings, options

@@ -60,7 +61,7 @@
           content[$uploadForm.data('as')] = content.url
           $.post(to, content)

-        data.context.remove() if data.context # remove progress bar
+        data.context.remove() if data.context && settings.remove_completed_progress_bar # remove progress bar
         $uploadForm.trigger("s3_upload_complete", [content])

         current_files.splice($.inArray(data, current_files), 1) # remove that element from the array


Add method to get addtional data.

Add method to get addtional data.

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -170,4 +170,7 @@
   @additional_data = (new_data) ->
     settings.additional_data = new_data

+  @get_additional_data = () ->
+    settings.additional_data
+
   @initialize()


adjusted positioning

adjusted positioning

--- drm-tooltips.coffee
+++ drm-tooltips.coffee
@@ -1,14 +1,13 @@
 ###############################################################################
 # Displays tooltips on click
 ###############################################################################
+"use strict"

-( ($) ->
-
-    drmTooltips = {
-        config: {
+( ($) ->
+    drmTooltips =
+        config:
             tooltip: $ '.drm-has-tooltip'
             speed: 300
-        }

         init: (config) ->
             $.extend @config, config
@@ -19,10 +18,10 @@
         addTooltip: ->
             that = $ @
             content = that.data 'title'
-            oldTooltip = $ ".drm-tooltip-#{position}:contains(#{content})"
+            oldTooltip = $ "div.drm-tooltip-#{position}:contains(#{content})"
             position = that.data 'position'

-            if oldTooltip.length == 0
+            unless oldTooltip.length isnt 0
                 newTooltip = $ '

', {
                     text: content,
                     class: "drm-tooltip-#{position}"
@@ -37,40 +36,43 @@
             that = $ @
             positionTop = that.offset().top + parseInt(that.css('padding-top'), 10)
             positionLeft = that.offset().left + parseInt(that.css('padding-left'), 10)
-            height = parseInt(newTooltip.css('height'), 10) + parseInt(newTooltip.css('padding-top'), 10) + parseInt(newTooltip.css('padding-bottom'), 10)
-            width = parseInt(newTooltip.css('width'), 10) + parseInt(newTooltip.css('padding-left'), 10) + parseInt(newTooltip.css('padding-right'), 10)
-            elWidth = parseInt(that.css('width'), 10) + parseInt(that.css('padding-left'), 10) + parseInt(that.css('padding-right'), 10)
-            elHeight = parseInt(that.css('height'), 10) + parseInt(that.css('padding-top'), 10) + parseInt(that.css('padding-bottom'), 10)
+            height = parseInt(newTooltip.outerHeight(), 10)
+            width = parseInt(newTooltip.outerWidth(), 10)
+            elWidth = parseInt(that.outerWidth(), 10)
+            elHeight = parseInt(that.outerHeight(), 10)

-            if position == 'left'
-                tooltipTop = positionTop - (elHeight / 2)
-                tooltipLeft = positionLeft - width + 10
-            else if position == 'right'
-                tooltipTop = positionTop - (elHeight / 2)
-                tooltipLeft = positionLeft + elWidth + 15
-            else if position == 'bottom'
-                tooltipTop = positionTop
-                tooltipLeft = positionLeft
-            else
-                tooltipTop = positionTop - ((elHeight + height) / 2) - 10
-                tooltipLeft = positionLeft + ((elWidth + width) / 2)
+            switch position
+                when 'left'
+                    tooltipTop = positionTop - (elHeight / 2)
+                    tooltipLeft = positionLeft - width + 10
+                when 'right'
+                    tooltipTop = positionTop - (elHeight / 2)
+                    tooltipLeft = positionLeft + elWidth + 15
+                when 'bottom'
+                    tooltipTop = positionTop
+                    tooltipLeft = positionLeft
+                else # default to top
+                    tooltipTop = positionTop - ((elHeight + height) / 2) - 10
+                    tooltipLeft = positionLeft + ((elWidth + width) / 2)

             console.log "Top: #{tooltipTop} #{positionTop}, Left: #{tooltipLeft} #{positionLeft}"
             console.log "Height: #{height} #{elHeight}, Width: #{width} #{elWidth}"
-            tooltipCSS = {'top': "#{tooltipTop}px", 'left': "#{tooltipLeft}px"}
+            tooltipCSS = {
+                'top': "#{tooltipTop}px"
+                'left': "#{tooltipLeft}px"
+            }

-            return tooltipCSS
+            tooltipCSS

         removeTooltip: ->
             that = $ @
             content = that.data 'title'
             position = that.data 'position'
-            oldTooltip = $ ".drm-tooltip-#{position}:contains(#{content})"
+            oldTooltip = $ "div.drm-tooltip-#{position}:contains(#{content})"

-            if oldTooltip.length > 0
+            unless oldTooltip.length is 0
                 oldTooltip.fadeOut drmTooltips.config.speed, ->
                     $(@).remove()
-    }

     drmTooltips.init()


Use jquery for upload

Use jquery for upload

--- app/javascript/src/legacy/post-upload-form.coffee
+++ app/javascript/src/legacy/post-upload-form.coffee
@@ -41,8 +41,7 @@
   if !key
     key = e.keyCode

-  ### Opera ###
-
+  # Opera
   if key != Event.KEY_ESC
     return
   @cancel()
@@ -54,9 +53,7 @@
   return

 PostUploadForm::form_submit_event = (e) ->
-
-  ### This submit may have been stopped by User.run_login_onsubmit. ###
-
+  # This submit may have been stopped by User.run_login_onsubmit.
   if e.stopped
     return
   if @current_request?
@@ -64,76 +61,72 @@
   $('post-exists').hide()
   $('post-upload-error').hide()

-  ### If the files attribute isn't supported, or we have no file (source upload), use regular
-  # form submission.
-  ###
-
+  # If the files attribute isn't supported, or we have no file (source upload), use regular
+  # form submission.
   post_file = $('post_file')
   if !post_file.files? or post_file.files.length == 0
     return
   e.stop()
   @set_progress 0
   @request_starting()
-  form_data = new FormData(@form_element)
-  onprogress = ((e) ->
+  formData = new FormData(@form_element)
+
+  onprogress = (e) =>
+    console.log 'hi'
     done = e.loaded
     total = e.total
-    @set_progress if total then done / total else 1
-    return
-  ).bind(this)
-  @current_request = new (Ajax.Request)('/post/create.json',
-    contentType: null
-    method: 'post'
-    postBody: form_data
-    onCreate: (resp) ->
-      xhr = resp.request.transport
-      xhr.upload.onprogress = onprogress
+    progress = if total > 0 then done / total else 1
+
+    @set_progress progress
+
+  @current_request = jQuery.ajax '/post/create.json',
+    contentType: false
+    data: formData
+    dataType: 'json'
+    method: 'POST'
+    processData :false
+    xhr: =>
+      xhr = new XMLHttpRequest
+      xhr.upload.addEventListener('progress', onprogress)
+      xhr
+  .always =>
+    @current_request = null
+    @request_ending()
+  .done (json) =>
+    # If a post/similar link was given and similar results exists, go to them.  Otherwise,
+    # go to the new post.
+    window.location.href =
+      if json.similar_location && json.has_similar_hits
+        json.similar_location
+      else
+        json.location
+  .fail (xhr) =>
+    json = xhr.responseJSON
+
+    if json? && json.location
+      a = document.querySelector('#post-exists-link')
+      a.text = "post ##{json.post_id}"
+      a.href = json.location
+      document.querySelector('#post-exists').style.display = ''
       return
-    onComplete: ((resp) ->
-      @current_request = null
-      @request_ending()
-      json = resp.responseJSON
-      if !json
-        return
-      if !json.success
-        if json.location
-          a = $('post-exists-link')
-          a.setTextContent 'post #' + json.post_id
-          a.href = json.location
-          $('post-exists').show()
-          return
-        $('post-upload-error').setTextContent json.reason
-        $('post-upload-error').show()
-        return

-      ### If a post/similar link was given and similar results exists, go to them.  Otherwise,
-      # go to the new post.
-      ###
+    errorLabel = document.querySelector('#post-upload-error')
+    errorLabel.text = json?.reason ? 'unknown error'
+    errorLabel.style.display = ''

-      target = json.location
-      if json.similar_location and json.has_similar_hits
-        target = json.similar_location
-      window.location.href = target
-      return
-    ).bind(this))
   return

-### Cancel the running request, if any. ###
-
+# Cancel the running request, if any.
 PostUploadForm::cancel = ->
   if !@current_request?
     return

-  ### Don't clear this.current_request; it'll be done by the onComplete callback. ###
-
-  @current_request.transport.abort()
+  # Don't clear this.current_request; it'll be done by the onComplete callback.
+  @current_request.abort()
   return

-###
 # When file_field is changed to an image, run an image search and put a summary in
 # results.
-###
-
 window.UploadSimilarSearch = (file_field, results) ->
   if !ThumbnailUserImage
     return
@@ -155,49 +148,39 @@
 UploadSimilarSearch::thumbnail_complete = (result) ->
   if !result.success
     @results.innerHTML = 'Image load failed.'
-    @results.show()
+    @results.style.display = ''
     return

-  ### Grab a data URL from the canvas; this is what we'll send to the server. ###
+  jQuery.ajax '/post/similar.json',
+    data:
+      url: result.canvas.toDataURL()
+    dataType: 'json'
+    method: 'POST'
+  .always =>
+    @results.innerHTML = ''
+    @results.style.display = ''
+  .done (json) =>
+    if json.posts.length > 0
+      posts = []
+      shownPosts = 3
+      makeUrl =
+          if User.get_use_browser()
+            (post) => "/post/browse##{post.id}"
+          else
+            (post) => "/post/show/#{post.id}"
+      posts = json.posts.slice(0, shownPosts).map (post) =>
+        "post ##{post.id}"
+      seeAll = "(see all)"
+      html = "Similar posts #{seeAll}: #{posts.join(', ')}"

-  data_url = result.canvas.toDataURL()
+      if json.posts.length > shownPosts
+        remainingPosts = json.posts.length - shownPosts
+        html += " (#{remainingPosts} more)"

-  ### Create the FormData containing the thumbnail image we're sending. ###
+      message = html
+    else
+      message = 'No similar posts found.'

-  form_data = new FormData
-  form_data.append 'url', data_url
-  req = new (Ajax.Request)('/post/similar.json',
-    method: 'post'
-    postBody: form_data
-    contentType: null
-    onComplete: ((resp) ->
-      @results.innerHTML = ''
-      @results.show()
-      json = resp.responseJSON
-      if !json.success
-        @results.innerHTML = json.reason
-        return
-      if json.posts.length > 0
-        posts = []
-        shown_posts = 3
-        json.posts.slice(0, shown_posts).each (post) ->
-          url = undefined
-          if User.get_use_browser()
-            url = '/post/browse#' + post.id
-          else
-            url = '/post/show/' + post.id
-          s = 'post #' + post.id + ''
-          posts.push s
-          return
-        post_links = posts.join(', ')
-        see_all = '(see all)'
-        html = 'Similar posts ' + see_all + ': ' + post_links
-        if json.posts.length > shown_posts
-          remaining_posts = json.posts.length - shown_posts
-          html += ' (' + remaining_posts + ' more)'
-        @results.innerHTML = html
-      else
-        @results.innerHTML = 'No similar posts found.'
-      return
-    ).bind(this))
-  return
+    @results.innerHTML = message
+  .fail (xhr) =>
+    @results.innerHTML = xhr.responseJSON?.reason ? 'unknown error'


Stop test suite from crashing when this one fails.

--- test/server/functional/prepaid.spec.coffee
+++ test/server/functional/prepaid.spec.coffee
@@ -50,6 +50,7 @@
             url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
             redeemer = { userID: otherUser.id }
             request.post {uri: url, json: redeemer }, (err, res, body) ->
+              expect(body.redeemers).toBeDefined()
               expect(body.redeemers.length).toBe(1)
               expect(res.statusCode).toBe(200)
               prepaid = Prepaid.findById body._id, (err, prepaid) ->


Add clean task into default task in gulpfile.coffee

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -39,4 +39,4 @@
   gulp.src './css/hmms.css', {read:false}
     .pipe clean()

-gulp.task 'default', ['coffee', 'sass', 'jade']
+gulp.task 'default', ['clean', 'coffee', 'sass', 'jade']


feat(bowerSwitch): add switch to turn off bower component retrieval

Usage:
gulp --bower=false
or
gulp --no-bower

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -136,6 +136,11 @@
 	description : 'Use your own backend.  No backendless.'
 	type        : 'boolean'

+yargs.options 'bower',
+	default     : true
+	description : 'Force retrieve of Bower components'
+	type        : 'boolean'
+
 yargs.options 'help',
 	default     : false
 	description : 'Show help'
@@ -163,6 +168,7 @@

 appUrl         = "http://localhost:#{PORT}"
 env            = gutil.env
+getBower       = getSwitchOption 'bower'
 isProd         = getSwitchOption 'prod'
 isWindows      = /^win/.test(process.platform)
 manifest       = {}
@@ -339,7 +345,7 @@

 # Clean working directories
 gulp.task 'clean:working', ->
-	sources = [COMPONENTS_DIRECTORY, TEMP_DIRECTORY, DIST_DIRECTORY, BOWER_FILE]
+	sources = [TEMP_DIRECTORY, DIST_DIRECTORY].concat(if getBower then [COMPONENTS_DIRECTORY, BOWER_FILE] else [])

 	gulp
 		.src sources, {read: false}
@@ -971,7 +977,7 @@
 		.on 'error', onError

 # Prepare for compilation
-gulp.task 'prepare', ['normalizeComponents', 'clean:working']
+gulp.task 'prepare', ['clean:working'].concat(if getBower then ['normalizeComponents'] else [])

 # Reload the app in the default browser
 gulp.task 'reload', ['build'], ->


fix watch

fix watch

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -31,7 +31,7 @@
     destination: './infokala/static/infokala/'
     filename: 'infokala.html'
     source: './infokala/static_src/infokala/infokala.stylus'
-    watch: './infokala/static_Src/infokala/*.stylus'
+    watch: './infokala/static_src/infokala/*.stylus'

 handleError = (err) ->


Remove homePath from gulpfile.

Remove homePath from gulpfile.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -6,7 +6,6 @@
 $ = require('gulp-load-plugins')()

 remotePluginPath = 'razor-x/vimrc'
-pluginPath = "#{homePath}/.vim/bundle/vimrc"
 vimCmd = (cmd) ->
   "vim -N -u #{homePath}/.vimrc -c \"try | #{cmd} $* | finally | qall | endtry\" -U NONE -i NONE -V1 -e -s"


Remove unnecessary comments.

Remove unnecessary comments.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -1,4 +1,3 @@
-# Find plugins at https://npmjs.org/browse/keyword/gulpplugin
 gulp = require 'gulp'

 coffee = require 'gulp-coffee'
@@ -24,11 +23,9 @@
 		.pipe(uglify())
 		.pipe(gulp.dest('./dist/'))

-# The default task (called when you run `gulp`)
 gulp.task 'default', ->
 	gulp.run 'build'

-	# Watch files and run tasks if they change
 	gulp.watch [
 		'lib/base64-binary.js',
 		'src/validateSSH.coffee'


disable advanced css minification

disable advanced css minification

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -186,7 +186,7 @@

     gulp.src(_paths)
         .pipe(concat("main.css"))
-        .pipe(gulpif(isDeploy, minifyCSS()))
+        .pipe(gulpif(isDeploy, minifyCSS({noAdvanced: true})))
         .pipe(gulp.dest(paths.dist + "styles/"))

 gulp.task "styles", ["delete-tmp-styles"], ->


MakeNSIS fails without ./dist folder. on win32

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -69,6 +69,7 @@

 # Create a nsis installer for win32; must have `makensis` installed
 gulp.task 'pack:win32', ['build:win32'], ->
+   shelljs.mkdir '-p', './dist'            # makensis fails if ./dist doesn't exist
    shelljs.exec 'makensis ./assets-windows/installer.nsi'

 # Create packages for linux


don't clean datalist fallback classes

don't clean datalist fallback classes

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -329,6 +329,8 @@
                 new RegExp('^.Blog-nav.*')
                 new RegExp('^.loading.*')
                 new RegExp('^.parsley-.*')
+                new RegExp('^.datalist.*')
+                new RegExp('^.active.*')
                 ]
                 ))
     .pipe(gulp.dest('build/css'))


Test [up-observe-event] parsing

Test [up-observe-event] parsing

--- spec/unpoly/form_spec.js.coffee
+++ spec/unpoly/form_spec.js.coffee
@@ -1395,26 +1395,65 @@

       it 'runs the JavaScript code in the attribute value when a change is observed in any contained field', asyncSpec (next) ->
         window.observeCallbackSpy = jasmine.createSpy('observe callback')
-        $form = $fixture('form[up-observe="window.observeCallbackSpy(value, name)"]')
-        $field1 = $form.affix('input[name="field1"][value="field1-old-value"]')
-        $field2 = $form.affix('input[name="field2"][value="field2-old-value"]')
-        up.hello($form)
-        $field1.val('field1-new-value')
-        Trigger.change($field1)
+        form = fixture('form[up-observe="window.observeCallbackSpy(value, name)"]')
+        field1 = e.affix(form, 'input[name="field1"][value="field1-old-value"]')
+        field2 = e.affix(form, 'input[name="field2"][value="field2-old-value"]')
+        up.hello(form)
+        field1.value = 'field1-new-value'
+        Trigger.change(field1)

         next =>
           expect(window.observeCallbackSpy.calls.allArgs()).toEqual [
             ['field1-new-value', 'field1']
           ]

-          $field2.val('field2-new-value')
-          Trigger.change($field2)
+          field2.value = 'field2-new-value'
+          Trigger.change(field2)

         next =>
           expect(window.observeCallbackSpy.calls.allArgs()).toEqual [
             ['field1-new-value', 'field1'],
             ['field2-new-value', 'field2']
           ]
+
+      describe 'with [up-observe-event] modifier', ->
+
+        it 'allows to set a different event to observe', asyncSpec (next) ->
+          window.observeCallbackSpy = jasmine.createSpy('observe callback')
+          form = fixture('form[up-observe="window.observeCallbackSpy(value, name)"][up-observe-event="foo"]')
+          field1 = e.affix(form, 'input[name="field1"][value="field1-old-value"]')
+          up.hello(form)
+          field1.value = 'field1-new-value'
+          Trigger.change(field1)
+
+          next ->
+            expect(window.observeCallbackSpy).not.toHaveBeenCalled()
+
+            up.emit(field1, 'foo')
+
+          next ->
+            expect(window.observeCallbackSpy.calls.allArgs()).toEqual [
+              ['field1-new-value', 'field1'],
+            ]
+
+        it 'allows to override the custom event at individual inputs', asyncSpec (next) ->
+          window.observeCallbackSpy = jasmine.createSpy('observe callback')
+          form = fixture('form[up-observe="window.observeCallbackSpy(value, name)"][up-observe-event="foo"]')
+          field1 = e.affix(form, 'input[name="field1"][value="field1-old-value"][up-observe-event="bar"]')
+          up.hello(form)
+          field1.value = 'field1-new-value'
+          Trigger.change(field1)
+          up.emit(field1, 'foo')
+
+          next ->
+            expect(window.observeCallbackSpy).not.toHaveBeenCalled()
+
+            up.emit(field1, 'bar')
+
+          next ->
+            expect(window.observeCallbackSpy.calls.allArgs()).toEqual [
+              ['field1-new-value', 'field1'],
+            ]

     describe 'input[up-validate]', ->


upgrade.coffee - wait for modal windows to fade in before clicking

--- test/integration/upgrade.coffee
+++ test/integration/upgrade.coffee
@@ -75,6 +75,10 @@
           done()

       context 'when I click the only button on the modal', ->
+        # wait a little while for the modal fade effect to finish
+        before (done) ->
+          setTimeout done, 500
+
         before (done) ->
           wd40.click '.modal .btn', done

@@ -96,6 +100,10 @@
           done()

       context 'when I click the only button on the modal', ->
+        # wait a little while for the modal fade effect to finish
+        before (done) ->
+          setTimeout done, 500
+
         before (done) ->
           wd40.click '.modal .btn', done


Update consolidate task for production

Update consolidate task for production

--- grunt/consolidate.coffee
+++ grunt/consolidate.coffee
@@ -5,23 +5,15 @@
     options:
       local:
         livereload: true,
-        #scripts: templateScripts.development,
-        #styles: templateStyles.development,
         mainModule: '<%= package.name %>',
         title: '<%= package.name %> - Development build',
-        #description: lyriaConfig.description,
-        #author: lyriaConfig.author
     files:
       'build/debug/index.html': 'template/index.html'
   production:
     options:
       local:
         livereload: false,
-      #scripts: templateScripts.development,
-      #styles: templateStyles.development,
-        #mainModule: pkgFile.name,
-        #title: '<%= lyriaConfig.title %> (Development build <%= buildVersion %>)',
-  #description: lyriaConfig.description,
-  #author: lyriaConfig.author
+        mainModule: '<%= package.name %>'
+        title: '<%= package.name %>'
     files:
-      'build/production/index.html': 'template/index.html'+      'build/production/index.html': 'template/index.html'


Remove old commented out test

Remove old commented out test

were testing behaviour that we intentionally removed a while back

--- test/tests/BaseClassTest.coffee
+++ test/tests/BaseClassTest.coffee
@@ -108,30 +108,6 @@

 		testClass = new TestClass3()
 		testClass.keys().should.eql ["testA", "testB"]
-
-	# it "should create getters/setters", ->
-
-	# 	class TestClass4 extends Framer.BaseClass
-	# 		@define "testA", @simpleProperty "testA", 100
-
-	# 	testClass = new TestClass4()
-	# 	testClass.setTestA(500)
-	# 	testClass.getTestA().should.equal 500
-	# 	testClass.testA.should.equal 500
-
-	# it "should override getters/setters", ->
-
-	# 	class TestClass5 extends Framer.BaseClass
-	# 		@define "testA", @simpleProperty "testA", 100
-
-	# 	class TestClass6 extends TestClass5
-	# 		setTestA: (value) ->
-	# 			super value * 10
-
-	# 	testClass = new TestClass6()
-	# 	testClass.setTestA(500)
-	# 	testClass.getTestA().should.equal 5000
-	# 	testClass.testA.should.equal 5000

 	it "should work with proxyProperties", ->


Implement more robust checks for trivial context conditions

--- src/coffee/cilantro/ui/concept/form.coffee
+++ src/coffee/cilantro/ui/concept/form.coffee
@@ -118,6 +118,24 @@

             @_renderFooter(message, className, false)

+        isContextNonTrivial: ->
+            # If the context has no children then we can consider it trivial
+            # as it has no filter conditions.
+            if not (model = @context.children?.models[0])?
+                return false
+
+            # If the operator is not present or the operator is not the 'in'
+            # operator then the context is considered non-trivial since we
+            # don't have a good clasiffier for those cases yet.
+            if not (operator = model.get('operator'))? or operator != 'in'
+                return true
+
+            # Now that we are sure the operator is 'in', if there are no
+            # values in the list of values to match against the context is
+            # considered trivial since it will never match anything and always
+            # end up returning 0 results.
+            return (value = model.get('value'))? and value.length > 0
+
         renderApplied: ->
             @ui.apply.hide()
             @ui.update.show()
@@ -127,26 +145,16 @@
                 className = 'alert-warning'
                 message = 'Heads up! The filter has been changed. Revert'

-            # If we are using the 'in' operator and there are now values in
-            # the list of values to match against, then we should disable the
-            # update filter button because it makes no sense to update an 'in'
-            # filter with an empty search list as it will never match anything.
-            enabled = enabled and @context.children.models[0]?.get('value')?.length > 0
+            enabled = enabled and @isContextNonTrivial()

             @_renderFooter(message, className, enabled)

         renderNew: ->
             @ui.apply.show()

-            # If we are using the 'in' operator and there are now values in
-            # the list of values to match against, then we should disable the
-            # apply filter button because it makes no sense to apply an 'in'
-            # filter with an empty search list as it will never match anything.
-            enabled = @context.children.models[0]?.get('value')?.length > 0
-
             @ui.update.hide()
             @ui.remove.hide()
-            @_renderFooter(null, null, enabled)
+            @_renderFooter(null, null, @isContextNonTrivial())

         # Saves the current state of the context which enables it to be
         # synced with the server.


Use better context/view json accessor and remove old comment

--- src/coffee/cilantro/ui/query/dialog.coffee
+++ src/coffee/cilantro/ui/query/dialog.coffee
@@ -32,10 +32,6 @@
             @ui.nameHelp.hide()
             @ui.nameGroup.removeClass('error')

-            # This class is not the actual modal window, only the contents
-            # of the modal itself. The parent is a ModalRegion which is what
-            # contains the modal wrapper so we need to hide the modal via this
-            # class's parent.
             @$el.modal('hide')

             @collection.add(@model)
@@ -43,8 +39,8 @@
                 name: @ui.nameText.val(),
                 description: @ui.descriptionText.val(),
                 usernames_or_emails: @ui.emailText.val(),
-                context_json: c.data.contexts.getSession().attributes.json,
-                view_json: c.data.views.getSession().attributes.json
+                context_json: c.data.contexts.getSession().toJSON().json,
+                view_json: c.data.views.getSession().toJSON().json
             })

         onShow: =>


bugging style 1

bugging style 1

--- apis/v0.coffee
+++ apis/v0.coffee
@@ -157,19 +157,7 @@
   res.send {
     success: true
     newsHTML: "ATTENTION!
THIS VERSION WILL STOP FUNCTIONING VERY SOON. UPGRADE NOW!
Click Here to Download Last Version.
The new version shortcut is labeled SVU Helper, while the old (this) one is labeled Login.
For technical reasons, the new version does not replace this one. Instead, you will have to un-install this one.
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
-
Download Last Version
+
Download Last Version
     "
   }


Add spec keyword definitions.

Add spec keyword definitions.

--- docs/_compile_docs.cjsx
+++ docs/_compile_docs.cjsx
@@ -76,21 +76,82 @@

                 <GoogleAnalytics id=google_analytics_id />
                 <BuildInfo />
-                <Asset path='script.coffee' />

+
+# http://tools.ietf.org/html/rfc2119
+RFC2119_KEYWORD_DEFINITIONS =
+    MUST: """
+        The definition is an absolute requirement of the specification.
+    """
+    MUST_NOT: """
+        The definition is an absolute prohibition of the specification.
+    """
+    SHOULD: """
+        There may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.
+    """
+    SHOULD_NOT: """
+        There may exist valid reasons in particular circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood and the case carefully weighed before implementing any behavior described with this label.
+    """
+    MAY: """
+        The item is truly optional.  One vendor may choose to include the item because a particular marketplace requires it or because the vendor feels that it enhances the product while another vendor may omit the same item. An implementation which does not include a particular option MUST be prepared to interoperate with another implementation which does include the option, though perhaps with reduced functionality. In the same vein an implementation which does include a particular option MUST be prepared to interoperate with another implementation which does not include the option (except, of course, for the feature the option provides.)
+    """
+
+RFC2119_KEYWORDS =
+    'MUST':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MUST
+        regex       : new RegExp('MUST(?! NOT)', 'g')
+    'REQUIRED':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MUST
+        regex       : new RegExp('REQUIRED', 'g')
+    'SHALL':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MUST
+        regex       : new RegExp('SHALL(?! NOT)', 'g')
+    'MUST NOT':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MUST_NOT
+        regex       : new RegExp('MUST NOT', 'g')
+    'SHALL NOT':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MUST_NOT
+        regex       : new RegExp('SHALL NOT', 'g')
+    'SHOULD':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.SHOULD
+        regex       : new RegExp('SHOULD(?! NOT)', 'g')
+    'RECOMMENDED':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.SHOULD
+        regex       : new RegExp('RECOMMENDED', 'g')
+    'SHOULD NOT':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.SHOULD_NOT
+        regex       : new RegExp('SHOULD NOT', 'g')
+    'MAY':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MAY
+        regex       : new RegExp('MAY', 'g')
+    'OPTIONAL':
+        definition  : RFC2119_KEYWORD_DEFINITIONS.MAY
+        regex       : new RegExp('OPTIONAL', 'g')
+
+SpecKeyword = React.createClass
+    render: ->
+        { @props.word }
+
+ReactDOMServer = require 'react-dom/server'
+
 MarkdownPage = React.createClass
     render: ->
         output_content = fs.readFileSync(@props.file).toString()

-        # http://tools.ietf.org/html/rfc2119
-        KEYWORDS = ['MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'MAY', 'OPTIONAL']
-        KEYWORDS.forEach (word) ->
-            exp = new RegExp(word,'g')
-            output_content = output_content.replace(exp, """
-                #{ word }
-            """.trim())
+        for word, spec of RFC2119_KEYWORDS
+            output_content = output_content.replace(
+                spec.regex,
+                ReactDOMServer.renderToStaticMarkup(
+                    <SpecKeyword word=word />
+                )
+            )
         output_content = marked(output_content)


Redirect to tournament after created

Redirect to tournament after created

--- client/tournament/create/controller.coffee
+++ client/tournament/create/controller.coffee
@@ -2,9 +2,9 @@
 .controller 'createTournamentController', ($scope, Tournament, $mdToast, $state) ->
   $scope.create = (tournament) ->
     Tournament.create tournament
-    , ->
+    , (tournament) ->
       $mdToast.showSimple 'Tournoi créé avec succès'
-      $state.go 'tournamentList'
+      $state.go 'tournamentDetail', id: tournament.id
     , (err) =>
       $mdToast.showSimple 'Erreur lors de la création'


sizes based on settings

sizes based on settings

--- hexaflip.coffee
+++ hexaflip.coffee
@@ -66,6 +66,9 @@
     @_cubes[setsKeys[setsKeys.length - 1]].el.style.marginRight = '0'

     @el.classList.add @className
+    @el.style.height = @size + 'px'
+    @el.style.width = ((@size + @margin * 2) * setsLength) - @margin * 2 + 'px'
+    @el.style[css.perspective] = @perspective + 'px'
     @el.appendChild cubeFragment

@@ -80,6 +83,7 @@

     cube.el.className = "#{ @className }-cube #{ @className }-cube-#{ set }"
     cube.el.style.margin = "0 #{ @margin }px"
+    cube.el.style.width = cube.el.style.height = @size + 'px'

     for side in @_faceNames
       cube[side] = document.createElement 'div'
@@ -161,7 +165,7 @@

   _getTransform: (deg) ->
-    "translate(0, 0) translateZ(-280px) rotate3d(1, 0, 0, #{ deg }deg)"
+    "translate(0, 0) translateZ(-#{ @size }px) rotate3d(1, 0, 0, #{ deg }deg)"

   _onMouseDown: (e, cube) ->


--- packages/rocketchat-file/file.server.coffee
+++ packages/rocketchat-file/file.server.coffee
@@ -4,6 +4,9 @@
 path = Npm.require('path')
 mkdirp = Npm.require('mkdirp')
 gm = Npm.require('gm')
+
+# Fix problem with usernames being converted to object id
+Grid.prototype.tryParseObjectId = -> false

 RocketChatFile =
 	gm: gm


Removing pictures when they are uploaded.

--- lib/client/autoform-slingshot.coffee
+++ lib/client/autoform-slingshot.coffee
@@ -134,13 +134,11 @@
           console.error err
         else
           # Patch cache upload.
-          SlingshotAutoformFileCache.upsert {
+          SlingshotAutoformFileCache.remove {
             template: instance.data.atts.id
             field: name
             directive: directiveName
-          }, {$unset: {
-            tmp: ''
-          }}
+          }

       # Wait for authorization and for transfer to start.
       statusTracking = Tracker.autorun =>


Add lint

Add lint

--- gulp/tasks/browserify.coffee
+++ gulp/tasks/browserify.coffee
@@ -57,7 +57,7 @@
     .pipe(bundleApp())

-gulp.task 'browserify:watch', [ 'clean-js' ], ->
+gulp.task 'browserify:watch', [ 'clean-js', 'coffeelint', 'eslint' ], ->
   gulp
     .src([
       'app/scripts/*.js'


Add support for upcoming react minimap

Add support for upcoming react minimap

--- lib/minimap-find-results-view.coffee
+++ lib/minimap-find-results-view.coffee
@@ -1,4 +1,4 @@
-{$, EditorView} = require 'atom'
+{$} = require 'atom'

 # HACK The exports is a function here because we are not sure that the
 # `find-and-replace` and `minimap` packages will be available when this
@@ -31,9 +31,13 @@
       @css '-webkit-transform', "scale3d(#{minimapInstance.getCharWidthRatio()},1,1)"
       @adjusted = true

+    getEditor: ->
+      activeView = atom.workspaceView.getActiveView()
+      if activeView?.hasClass('editor') then activeView else null
+
     getMinimap: ->
       editorView = @getEditor()
-      if editorView instanceof EditorView
+      if editorView.hasClass('editor')
         return minimapInstance.minimapForEditorView(editorView)

     # HACK We don't want the markers to disappear when they're not


test refactoring/cleanup

test refactoring/cleanup

--- tests/V1.JsonQuery.Tests.coffee
+++ tests/V1.JsonQuery.Tests.coffee
@@ -30,41 +30,35 @@

   describe 'converting a backbone model to a query', ->

+    expectedQuery = (expected) ->
+       (url, data) ->
+          expect(data).to.equal(expected)
+
     describe 'a single query', ->

       it 'generate a simple query', ->
-        Member = Backbone.Model.extend
-          assetType: "Member"
-          schema: ["Name"]

-        Members = Backbone.Model.extend(model:Member)
+        Members = Backbone.Model.extend
+          model: Backbone.Model.extend
+            assetType: "Member"
+            schema: ["Name"]

-        results = []
+        query = expectedQuery '{"from":"Member","select":["Name"]}'

-        queryChecker = (url, data) ->
-          expect(data).to.equal '{"from":"Member","select":["Name"]}'
-
-
-        query = new JsonQuery(url:"url", fetcher: queryChecker)
+        query = new JsonQuery(url:"url", fetcher: query)
         query.for(Members)
         query.exec()

       it 'query with an alias', ->
-
         alias = JsonQuery.alias

-        Expression = Backbone.Model.extend
-          assetType: "Expression"
-          schema: [ alias("Author.Name").as("Speaker") ]
+        Expressions = Backbone.Model.extend
+          model: Backbone.Model.extend
+            assetType: "Expression"
+            schema: [ alias("Author.Name").as("Speaker") ]

-        Expressions = Backbone.Model.extend(model:Expression)
+        query = expectedQuery '{"from":"Expression","select":["Author.Name"]}'

-        results = []
-
-        queryChecker = (url, data) ->
-          expect(data).to.equal '{"from":"Expression","select":["Author.Name"]}'
-
-
-        query = new JsonQuery(url:"url", fetcher: queryChecker)
+        query = new JsonQuery(url:"url", fetcher: query)
         query.for(Expressions)
         query.exec()


ApiNodeTargetOwner: spell fix.

ApiNodeTargetOwner: spell fix.

--- src/Mixins/ApiNodeTargetOwner.coffee
+++ src/Mixins/ApiNodeTargetOwner.coffee
@@ -12,7 +12,7 @@
   #
   # Sets current target.
   #
-  # If target is a function the it considered as a constructor of the response
+  # If target is a function then it considered as a constructor of the response
   # subject.
   #
   # If target is a string then it tries convert it to API node constructor


add hostname to cobalt app name

add hostname to cobalt app name

--- code/serv.coffee
+++ code/serv.coffee
@@ -24,7 +24,7 @@
 if process.env.NODETIME_KEY?
   require('nodetime').profile
     accountKey: process.env.NODETIME_KEY
-    appName: process.env.CO_NODETIME_APP
+    appName: "#{process.env.CO_NODETIME_APP} #{require('os').hostname()}"

 app = express()


updated limit

updated limit

--- coffeeApp.coffee
+++ coffeeApp.coffee
@@ -93,9 +93,9 @@
 # Array of re-tweeted screen_names
 twitterUsernameArray = []
 # 350 per hour, 50 per min, we'll do 45 so we avoid hitting the limit
-limit = 0
+limit = 25
 setInterval resetLimit, 60000
-resetLimit = -> limit = 0
+resetLimit = -> limit = 25


Change the beautifier to conform to the expectations of elm-format

--- src/beautifiers/elm-format.coffee
+++ src/beautifiers/elm-format.coffee
@@ -1,9 +1,12 @@
 ###
 Requires https://github.com/avh4/elm-format
 ###
-
 "use strict"
 Beautifier = require('./beautifier')
+Promise = require("bluebird")
+fs = require("fs")
+readFile = Promise.promisify(fs.readFile)
+rename = Promise.promisify(fs.rename)

 module.exports = class ElmFormat extends Beautifier
   name: "elm-format"
@@ -13,6 +16,14 @@
   }

   beautify: (text, language, options) ->
-    @run("elm-format", [
-      @tempFile("input", text)
-      ])
+    tempfile = @tempFile("input", text)
+    .then (name) =>
+      newName = name + ".elm"
+      rename name, newName
+      .then () =>
+        @run("elm-format", [
+          '--yes',
+          newName
+          ])
+        .then () =>
+          readFile newName, {encoding: 'utf-8'}


Add allRows in TG

Add allRows in TG

--- src/access/table-gateway.coffee
+++ src/access/table-gateway.coffee
@@ -59,7 +59,7 @@
     fromJS: (values) ->
         for key, value of values
             property = @schema.columnsByProperty[key]
-
+
             converter = property?.jsType?.convert
             continue if (!_.isFunction(converter))

@@ -73,7 +73,7 @@

         unless keyValues?
             F.throw("You must provide key values as arguments to #{opName}One()")
-
+
         queryArgument = @fromJS(queryArgument)
         if _.isObject(keyValues)
             covered = @schema.coversSomeKey(keyValues)
@@ -177,6 +177,7 @@
     count: (cb = null) -> @newSelect().select(sql.count(1)).tryCall('scalar', cb)

     all: (cb) -> @newSelect().tryCall('allObjects', cb)
+    allRows: (cb) -> @newSelect().tryCall('allRows', cb)

     where: (clause, cb) -> @newSelect().where(clause).tryCall('allObjects', cb)


Only notify CODAP once for batched undoable actions

[#96197680]

--- src/code/utils/undo-redo.coffee
+++ src/code/utils/undo-redo.coffee
@@ -32,8 +32,10 @@
   createAndExecuteCommand: (name, methods) ->
     result = @execute (new Command name, methods)

-    codapConnect = CodapConnect.instance DEFAULT_CONTEXT_NAME
-    codapConnect.sendUndoableActionPerformed()
+    # Only notify CODAP of an undoable action on the first command of a batched command
+    if (not @currentBatch) or (@currentBatch.commands.length is 1)
+      codapConnect = CodapConnect.instance DEFAULT_CONTEXT_NAME
+      codapConnect.sendUndoableActionPerformed()

     result


Use @props as defaults for currentParams

--- tutor/src/components/assignment-links.cjsx
+++ tutor/src/components/assignment-links.cjsx
@@ -13,7 +13,7 @@
   bindStore: ReferenceBookStore

   componentWillMount: ->
-    {courseId} = _.defaults(@props, Router.currentParams() )
+    {courseId} = _.defaults(Router.currentParams(), @props)

     course = CourseStore.get(courseId)
     @setState(ecosystem_id: course.ecosystem_id)


Restore focus after select list detaches

Previously if the select list was cancelled with no open
editors the mini editor would be given focus before being
detached causing nothing to have focus after detach completed.

Now the select list tracks the previously focused elements and
restores focus to that element at the end of the cancel.

--- src/command-palette-view.coffee
+++ src/command-palette-view.coffee
@@ -23,7 +23,8 @@
     super

   attach: ->
-    @previouslyFocusedElement = $(':focus')
+    super
+
     @keyBindings = _.losslessInvert(keymap.bindingsForElement(@previouslyFocusedElement))

     events = []
@@ -53,4 +54,3 @@
     @previouslyFocusedElement.trigger(eventName)

   cancelled: ->
-    @previouslyFocusedElement.focus() if @miniEditor.isFocused


updated the javascript task so that you can compile multiple coffee script files as oppose to just the index.coffee

--- gulp-tasks/js.coffee
+++ gulp-tasks/js.coffee
@@ -1,41 +1,36 @@
 # TODO: Find a way to have index.coffee update on watch
+# TODO: Use the underscore pattern to manage which files should be outputted like with the SASS

 # Compile all coffeescripts into javascript
 gulp.task "js", (cb) ->
   helper.out "Running Javascript task"

-  gulp.src "#{config.js.coffee}/**/*.coffee"
+  # Compile all coffeescript files into the js folders
+  gulp.src ["#{config.js.coffee}/**/*.coffee", "!#{config.js.coffee}/index.coffee"]
     .pipe coffee(bare: true)
     .pipe gulp.dest(config.js.src)

+
+  # Get the list of javascript files
+  coffeeFiles = require("#{config.js.coffee}/index.coffee")
+
   # Concat and minify all javascript files
-  js_files = ("#{config.js.src}/#{file_name}.js" for file_name in require("#{config.js.coffee}/index.coffee"))
+  for fileName,fileNames of coffeeFiles

-  gulp.src js_files
-    # Concat all the Javascript files
-    .pipe concat("index.js")
+    # Append the correct path and js extension
+    javascriptFileNames = ("#{config.js.src}/#{javascriptFileName}.js" for javascriptFileName in fileNames)

-    # Optimize the javascript
-    .pipe gulpIf(args.production, uglify())
+    gulp.src javascriptFileNames
+      # Concat all the Javascript files
+      .pipe concat("#{fileName}.js")

-    .pipe gulp.dest(config.wordpress.theme.dest)
+      # Minify the javascript
+      .pipe gulpIf(args.production, uglify())

-    # Generate a size report
-    .pipe sizeReport(gzip: true, total: false)
+      .pipe gulp.dest(config.wordpress.theme.dest)

-    # Live reload hook
-    .pipe liveReload()
+      # Generate a size report
+      .pipe sizeReport(gzip: true, total: false)

-  # Internet explorer javascript
-  gulp.src ["#{config.js.src}/ie.js"]
-    .pipe gulpIf(args.production, uglify())
-    .pipe gulp.dest(config.wordpress.theme.dest)
-
-
-gulp.task "js_minify", ->
-
-  # TODO: List file sizes
-  gulp.src "#{config.js.src}/*.js"
-    .pipe uglify()
-    .pipe gulp.dest("#{config.js.src}/../min")
-
+      # Live reload hook
+      .pipe liveReload()


Add some likne breaks

Add some likne breaks

--- app/coffee/modules/project.coffee
+++ app/coffee/modules/project.coffee
@@ -1,5 +1,6 @@
 taiga = @.taiga
 module = angular.module("taigaProject", [])
+

 class ProjectsController extends taiga.Controller
     @.$inject = ["$scope", "$tgResources"]
@@ -14,6 +15,7 @@
             @.projects = {'recents': projects.slice(0, 8), 'all': projects.slice(8)}

 module.controller("ProjectsController", ProjectsController)
+

 class ProjectController extends taiga.Controller
     @.$inject = ["$scope", "$tgResources", "$tgRepo", "$routeParams", "$q"]


Adds x-source headers.

Adds x-source headers.

--- grammars/eml.cson
+++ grammars/eml.cson
@@ -66,15 +66,16 @@
     # X-Headers
     { 'match': '^(?i)msmail-priority.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)priority.*?:', 'name': 'constant.other.eml' },
-    { 'match': '^(?i)X-Auto-Response-Suppress.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-antiabuse.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-auto-response-suppress.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-bulkmail.*?:', 'name': 'constant.other.eml' },
-    { 'match': '^(?i)X-Campaign.*?:', 'name': 'constant.other.eml' },
-    { 'match': '^(?i)X-campaignid.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-campaign.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-campaignid.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-cmae-analysis.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-cmae-score.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-eopattributedmessage.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-exchange-antispam-report-cfa-test.*?:', 'name': 'constant.other.eml' },
-    { 'match': '^(?i)X-Feedback-ID.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-feedback-id.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-forefront-antispam-report.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-forefront-prvs.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-gm-message-state.*?:', 'name': 'constant.other.eml' },
@@ -85,7 +86,7 @@
     { 'match': '^(?i)x-mailing-list.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-mandrill-user.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-mc-unique.*?:', 'name': 'constant.other.eml' },
-    { 'match': '^(?i)X-MC-User.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-mc-user.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-mcda.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-microsoft-antispam.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-mimeole.*?:', 'name': 'constant.other.eml' },
@@ -108,6 +109,9 @@
     { 'match': '^(?i)x-report-abuse.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-report-abuse.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-sender.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-source-args.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-source-dir.*?:', 'name': 'constant.other.eml' },
+    { 'match': '^(?i)x-source.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-spam-checker-version.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-spam-level.*?:', 'name': 'constant.other.eml' },
     { 'match': '^(?i)x-spam-status.*?:', 'name': 'constant.other.eml' },


Removed reference to undefined function

Removed reference to undefined function

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -128,7 +128,14 @@

       @subjectButton.removeClass 'disabled'

-      @informationVM.$set 'subject', subject.toJSON()
+      subjectJSON = {
+        metadata: {
+          collector: subject.metadata.collector
+          date: subject.metadata.date
+          comments: subject.metadata.comments
+        }
+      }
+      @informationVM.$set 'subject', subjectJSON
       @surface.enable()

   onClickRestartTutorial: ->


improved /help text

improved /help text

--- src/chat/user_command_handler.coffee
+++ src/chat/user_command_handler.coffee
@@ -47,7 +47,7 @@
         @conn.irc.doCommand 'JOIN', @channel

     @_addCommand 'win',
-      description: 'switch windows'
+      description: 'switches windows'
       params: ['windowNum']
       parseArgs: ->
         num = parseInt @windowNum
@@ -56,7 +56,7 @@
         @chat.switchToWindow @window

     @_addCommand 'say',
-      description: 'displays text in the current channel'
+      description: 'sends text to the current channel'
       params: ['text...']
       requires: ['connection', 'channel', 'connected']
       run: ->
@@ -64,7 +64,7 @@
         @displayMessage 'privmsg', @conn.irc.nick, @text

     @_addCommand 'me',
-      description: 'displays text in the current channel, spoken in the 3rd person'
+      description: 'sends text to the current channel, spoken in the 3rd person'
       extends: 'say'
       parseArgs: ->
         @text = "\u0001ACTION #{@text}\u0001"
@@ -79,11 +79,12 @@
         @conn?.irc.doCommand 'NICK', @nick

     @_addCommand 'server',
-      description: 'connects to a sevrer, the default port is 6667, ' +
-          "if no arguments are given it tries to reconnect to the current server"
+      description: 'connects to the sevrer, port 6667 is used by default, ' +
+          "reconnects to the current server if server is specified"
       params: ['opt_server', 'opt_port']
       parseArgs: ->
-        @port = parseInt(@port ? 6667)
+        @port ?= parseInt @server
+        @port = parseInt(@port) ? 6667
         @server ?= @conn?.name
         return @port and @server
       run: ->
@@ -104,7 +105,7 @@
         @chat.removeWindow @chat.winList.get @conn.name

     @_addCommand 'names',
-      description: 'lists the nicks on the current channel'
+      description: 'lists nicks in the current channel'
       requires: ['connection', 'channel', 'connected']
       run: ->
         names = (v for k,v of @conn.irc.channels[@chan].names).sort()
@@ -112,8 +113,8 @@
         @chat.currentWindow.message '*', msg, 'notice names'

     @_addCommand 'help',
-      description: "displays information about a command, if no command is " +
-          "given then lists all possible commands."
+      description: "displays information about a command, lists all commands " +
+          "when no command is specified"
       params: ["opt_command"]
       run: ->
         @command = @chat.userCommands.getCommand @command
@@ -124,7 +125,7 @@
           @chat.currentWindow.displayHelp commands

     @_addCommand 'part',
-      description: "closes the current window and leaves the channel if connected"
+      description: "closes the current window and disconnects from the channel"
       params: ['opt_reason...']
       requires: ['connection', 'channel']
       run: ->
@@ -143,20 +144,21 @@
         @conn.irc.doCommand command...

     @_addCommand 'load',
-      description: "loads a script from the file system"
+      description: "loads a script by opening a file browser dialog"
       run: ->
         script.loader.createScriptFromFileSystem (script) =>
           @chat.emit 'script_loaded', script

     @_addCommand 'topic',
-      description: "sets the topic of the current channel, if there are no arguments then it displays the current topic"
+      description: "sets the topic of the current channel, displays the " +
+          "current topic if no topic is specified"
       params: ['opt_topic...']
       requires: ['connection', 'channel']
       run: ->
         @conn.irc.doCommand 'TOPIC', @chan, @topic

     @_addCommand 'kick',
-      description: "removes a user from the current channel"
+      description: "removes the nick from the current channel"
       params: ['nick', 'opt_reason...']
       requires: ['connection', 'channel']
       run: ->
@@ -164,8 +166,8 @@

     @_addCommand 'mode',
       # TODO when used with no args, display current modes
-      description: "sets the mode for the given user, if no nick is provides " +
-          "then your nick is used"
+      description: "sets the mode for the given user, your nick is used if " +
+          "no nick is specified"
       params: ['opt_nick', 'mode']
       requires: ['connection']
       parseArgs: ->
@@ -179,32 +181,32 @@
           @conn.irc.doCommand 'MODE', @mode, @nick

     @_addCommand 'op',
-      description: "gives a user operator status"
+      description: "gives operator status"
       params: ['nick']
       extends: 'mode'
       parseArgs: -> @mode = '+o'

     @_addCommand 'deop',
-      description: "removes operator status from a user"
+      description: "removes operator status"
       params: ['nick']
       extends: 'mode'
       parseArgs: -> @mode = '-o'

     @_addCommand 'voice',
-      description: "gives a user voice"
+      description: "gives voice"
       params: ['nick']
       extends: 'mode'
       parseArgs: -> @mode = '+v'

     @_addCommand 'devoice',
-      description: "removes voice from a user"
+      description: "removes voice"
       params: ['nick']
       extends: 'mode'
       parseArgs: -> @mode = '-v'

     @_addCommand 'away',
-      description: "sets your current status to away, when people /msg you " +
-          "or do a WHOIS on you, they will get an automatic response"
+      description: "sets your status to away, a response is " +
+          "automatically sent when people /msg or WHOIS you"
       params: ['response...']
       requires: ['connection']
       parseArgs: ->
@@ -220,7 +222,7 @@
       run: -> @conn.irc.doCommand 'AWAY', @response

     @_addCommand 'msg',
-      description: "closes the current window and leaves the channel if connected"
+      description: "sends a private message"
       params: ['nick', 'message...']
       requires: ['connection']
       run: ->


Re-use existing method.

Re-use existing method.

--- src/spec/integration/channels.spec.coffee
+++ src/spec/integration/channels.spec.coffee
@@ -67,7 +67,7 @@
       done _u.prettify(error)

   it 'should create a new channel with given role and return it', (done) ->
-    key = "channel-#{new Date().getTime()}"
+    key = uniqueId "channel"
     @client.channels.byKeyOrCreate(key, ROLE_ORDER_EXPORT)
     .then (result) ->
       channels.push result.body


Remove obsolete qiDateline directive.

Remove obsolete qiDateline directive.

--- app/javascripts/directives.coffee
+++ app/javascripts/directives.coffee
@@ -52,92 +52,3 @@
         scope.config = VisitDateline.configure_chart(data)
   templateUrl: '/templates/visit_dateline_chart.html'
 ]
-#
-#
-# # Draws a dateline.
-# directives.directive 'qiDateline', ['Helpers', (Helpers) ->
-#   # Formats the dateline from the given data.
-#   format = (scope, element, attrs) ->
-#     # Reformats the chart.
-#     # @param chart the SVG chart element
-#     # @param scales
-#     # @param config the {x : {scale, axes: [{element, axis}, ...},
-#     #                    y : ...} data structure
-#     resize = (chart, config) ->
-#       chartNode = chart.node()
-#       # The parent element.
-#       parent = chartNode.parentNode
-#       # The chart height and width.
-#       height = $(parent).height()
-#       width = $(parent).width()
-#       # Resize the chart.
-#       config.x.scale.range([0, width])
-#       for {element: elt, axis: axis} in config.x.axes
-#         elt.attr('transform', "translate(0, #{ height })")
-#         elt.call(axis)
-#       if config.y
-#         config.y.scale.range([height, 0])
-#         for {element: elt, axis: axis} in config.y.axes
-#           elt.call(axis)
-#
-#     # The element which contains the chart.
-#     parent = d3.select(element[0])
-#     # The visits to display.
-#     data = scope.data
-#     # The visit dates.
-#     dates =
-#         obj[attrs.attribute] for obj in data
-#
-#     # The x axis scale.
-#     x_scale = d3.scale.linear()
-#     domain = [_.min(dates), _.max(dates)]
-#     x_scale.domain(domain)
-#
-#     # The SVG chart.
-#     chart = parent.append('svg')
-#
-#     # The dates are displayed along the x axis bottom.
-#     bot_axis = d3.svg.axis()
-#     bot_axis.scale(x_scale).orient('bottom').tickValues(dates)
-#       .tickFormat(Helpers.dateFormat)
-#     bottom = chart.append('g').attr('class', 'x axis')
-#
-#     # The visit numbers are displayed along the x axis top.
-#     top_axis = d3.svg.axis()
-#     top_axis.scale(x_scale).orient('top').tickValues(dates)
-#       .tickFormat(Helpers.indexFormatter(dates, 1))
-#     top = chart.append('g').attr('class', 'x axis')
-#
-#     # The chart data structure.
-#     config =
-#       x:
-#         scale: x_scale
-#         axes: [
-#           {element: bottom, axis: bot_axis}
-#           {element: top, axis: top_axis}
-#         ]
-#
-#     # The visit hyperlinks.
-#     hrefs =
-#       scope.href(obj) for obj in data
-#     # Replace the visit text with hyperlinks.
-#     Helpers.d3Hyperlink(top, hrefs)
-#
-#     # Make the chart responsive.
-#     d3.select(window).on('resize', resize(chart, config))
-#
-#     # Set the initial chart size.
-#     resize(chart, config)
-#
-#   # Make the directive.
-#   restrict: 'E'
-#   scope:
-#     data: '='
-#     href: '='
-#     width: '@'
-#     height: '@'
-#   link: (scope, element, attrs) ->
-#     scope.$watch 'data', (data) ->
-#       if data
-#         format(scope, element, attrs)
-# ]


dont need validators anymore...

dont need validators anymore...

--- src/coffee/services/directive.coffee
+++ src/coffee/services/directive.coffee
@@ -7,7 +7,7 @@
 ##

 angular.module('CoolFormServices').
-  factory('directivesService', (validators) ->
+  factory('directivesService', ->

     return ->


Avoid some unnecessary overhead in AI choice function.

--- basicAI.coffee
+++ basicAI.coffee
@@ -82,13 +82,15 @@
       choiceSet = {}
       for choice in choices
         choiceSet[choice] = choice
+
+      nullable = null in choices

       # Get the priority list.
-      priority = priorityfunc.bind(this)(state, my)
+      priority = priorityfunc.call(this, state, my)
       # Now look up all the preferences in that list. The moment we encounter
       # a valid choice, we can return it.
       for preference in priority
-        if preference is null and null in choices
+        if preference is null and nullable
           return null
         if choiceSet[preference]?
           return choiceSet[preference]
@@ -103,7 +105,7 @@
         if (choice is null) or (choice is no)
           value = 0
         else
-          value = valuefunc.bind(this)(state, choice, my)
+          value = valuefunc.call(this, state, choice, my)
         if value > bestValue
           bestValue = value
           bestChoice = choice


working on PutOnDeckPrio again

working on PutOnDeckPrio again

--- basicAI.coffee
+++ basicAI.coffee
@@ -403,20 +403,29 @@
     #    Take card from hand which are actions, sort them by ActionPriority
     #
     if my.countPlayableTerminals(state) == 0
-      putBack = (card for card in my.hand when card?.isAction).sort( (y, x) -> my.ai.choiceToValue('action', state, x) - my.ai.choiceToValue('action', state, y) )
+      # take actions from hand
+      # and sort them by actionPriority (highest first)
+      putBack = (card for card in my.hand when card?.isAction)
+      putBack = putBack.sort( (y, x) -> my.ai.choiceToValue('action', state, x) - my.ai.choiceToValue('action', state, y) )

     # 2) If not enough actions left, put back best Terminal you can't play
     #    Take cards from hand which are Actions and Terminals, sort them by ActionPriority
     #    Then, ignore as many terminals as you can play this turn, return the others
     #
     else
-      putBack = (tmp=( card for card in my.hand when (card?.isAction and card?.getActions(state)>0) ).sort( (y, x) -> my.ai.choiceToValue('action', state, x) - my.ai.choiceToValue('action', state, y) ) )[my.countPlayableTerminals(state) ... tmp.length]
-
+      # take terminals from hand
+      # and sort them by actionPriority (highest first)
+      # just take the once you can not play by the actions you have
+      putBack = (card for card in my.hand when (card?.isAction and card?.getActions(state)==0))
+      putBack = putBack.sort( (y, x) -> my.ai.choiceToValue('action', state, x) - my.ai.choiceToValue('action', state, y) )
+      putBack = putBack[my.countPlayableTerminals(state) ... putBack.length]
+
     # 3) Put back as much money as you can
     #    First get a pessimistic estimate of the avaiable money this turn
     #    then find take the cost in coins of the highest priority card you can afford
     #    then put the treasures which are not needed in 'putBack' for discardPriority, most valuable first, Potion = 2.5 Coins
     #
+    state.log("PutBack: #{putBack}")
     if putBack.length==0
       coinEstimate = my.ai.pessimisticMoneyInHand(state)
       potionEstimate = my.countInHand(state.cardInfo["Potion"])
@@ -436,7 +445,7 @@
         potionTarget = Math.max potionTarget, 1

       putBack = (card for card in my.hand when (card.isTreasure and (card.getCoins(state) <= (coinEstimate - coinTarget)) and (card.getPotion(state) <= (potionEstimate - potionTarget)) ) ).sort( (b, a) -> (state.cardInfo[a].getCoins(state) + 2.5*state.cardInfo[a].getPotion(state) - state.cardInfo[b].getCoins(state) - 2.5*state.cardInfo[b].getPotion(state)) )
-
+
     # 4) Put back the worst card (take priority for discard)
     #
     if putBack.length==0
@@ -662,6 +671,9 @@
         state.phase = 'treasure'
       else if state.phase == 'treasure'
         state.phase = 'buy'
+
+    state.log("")
+    state.log("HYPOTHETICAL start. Ignore everything...")

     [hypothesis, hypothetically_my] = state.hypothetical(this)
     hypothetically_my.draw = []
@@ -670,6 +682,9 @@
     while hypothesis.phase != 'buy'
       hypothesis.doPlay()

+    state.log("HYPOTHTICAL end. ... until here!")
+    state.log("")
+
     return hypothesis

   pessimisticCardsGained: (state) ->


Add the correct match reference

Add the correct match reference

--- src/github-event-announcer.coffee
+++ src/github-event-announcer.coffee
@@ -37,8 +37,8 @@
       robot.messageRoom(room, message)

   robot.respond /listen for GitHub events on ([^/]+)\/(.+)$/i, (msg) ->
-    user = match[1]
-    repo = match[2]
+    user = msg.match[1]
+    repo = msg.match[2]

     robot.logger.info "Request to add GitHub events hook to #{user}/#{repo} received"


MEH: vain attempt to get iOS safari to behave properly for a fullscreen app

--- source/Art/Engine/FullScreenApp.coffee
+++ source/Art/Engine/FullScreenApp.coffee
@@ -92,6 +92,8 @@

     nameContentMetas = merge
       "viewport": "user-scalable=no, width=device-width, initial-scale=1.0"
+      "mobile-web-app-capable": "yes"
+      "apple-touch-fullscreen": "yes"
       "apple-mobile-web-app-capable": "yes"
       "apple-mobile-web-app-status-bar-style": "black" #"black-translucent"
       "format-detection": "telephone=no"


fix host alias options bug

fix host alias options bug

--- component.coffee
+++ component.coffee
@@ -327,7 +327,7 @@
       describe: "test server port, overrides $PORT env variable\n(cmd: test)"

     .option "host",
-      alias: "h"
+      alias: "H"
       default: process.argv.HOST || "localhost"
       describe: "test server host, overrides $HOST env variable\n(cmd: test)"


[api] Echo back all incoming socket messages ( for now )

--- lib/api.coffee
+++ lib/api.coffee
@@ -38,6 +38,9 @@
       ws.on 'message', (message) ->
         logger.info 'api server ' + process.pid + ' received message: ' + message

+        # Echo back websocket message ( for now )
+        ws.send(message)
+
       ws.send 'i am api server ' + process.pid

     callback null, wss


Use lowercase 'optional' keyword used by docs

The docs generator expects 'optional' starting with a lowercase 'o' to
mark arguments as optional. Lowercase 'O' to properly generate the docs.

--- src/context-menu-manager.coffee
+++ src/context-menu-manager.coffee
@@ -83,25 +83,25 @@
   #
   # * `itemsBySelector` An {Object} whose keys are CSS selectors and whose
   #   values are {Array}s of item {Object}s containing the following keys:
-  #   * `label` (Optional) A {String} containing the menu item's label.
-  #   * `command` (Optional) A {String} containing the command to invoke on the
+  #   * `label` (optional) A {String} containing the menu item's label.
+  #   * `command` (optional) A {String} containing the command to invoke on the
   #     target of the right click that invoked the context menu.
-  #   * `enabled` (Optional) A {Boolean} indicating whether the menu item
+  #   * `enabled` (optional) A {Boolean} indicating whether the menu item
   #     should be clickable. Disabled menu items typically appear grayed out.
   #     Defaults to `true`.
-  #   * `submenu` (Optional) An {Array} of additional items.
-  #   * `type` (Optional) If you want to create a separator, provide an item
+  #   * `submenu` (optional) An {Array} of additional items.
+  #   * `type` (optional) If you want to create a separator, provide an item
   #      with `type: 'separator'` and no other keys.
-  #   * `visible` (Optional) A {Boolean} indicating whether the menu item
+  #   * `visible` (optional) A {Boolean} indicating whether the menu item
   #     should appear in the menu. Defaults to `true`.
-  #   * `created` (Optional) A {Function} that is called on the item each time a
+  #   * `created` (optional) A {Function} that is called on the item each time a
   #     context menu is created via a right click. You can assign properties to
   #    `this` to dynamically compute the command, label, etc. This method is
   #    actually called on a clone of the original item template to prevent state
   #    from leaking across context menu deployments. Called with the following
   #    argument:
   #     * `event` The click event that deployed the context menu.
-  #   * `shouldDisplay` (Optional) A {Function} that is called to determine
+  #   * `shouldDisplay` (optional) A {Function} that is called to determine
   #     whether to display this item on a given context menu deployment. Called
   #     with the following argument:
   #     * `event` The click event that deployed the context menu.


Changing some default theme colors.

Changing some default theme colors.

--- styles/default_theme/default_theme.coffee
+++ styles/default_theme/default_theme.coffee
@@ -29,12 +29,15 @@
         @colors = colors =
             t1: '#5f5f5f'
             light: '#efefef'
+            gray: '#d5d5d5'
             dark: '#838383'
-            yellow: '#FFD000'
-            green: '#BCE346'
-            blue: '#00BEFF'
+            very_dark: '#3f3f3f'
+            purple: '#c188cf'
+            blue: '#94bdec'
+            orange: '#db8851'
+            green: '#b1c151'
+            yellow: '#eec151'
             red: '#f16b5c'
-            purple: '#c188cf'

         @socketColors = socketColors =
             basic: 'white'


Added function to get src urls without html markup

--- lib/nap.coffee
+++ lib/nap.coffee
@@ -62,26 +62,40 @@

   @

-# Run js pre-processors & output the packages in dev.
-#
+
+# Returns the script / css urls
+#
+# @param {String} type Must be 'js' or 'css'
 # @param {String} pkg The name of the package to output
 # @return {String} Script tag(s) pointing to the ouput package(s)
+module.exports.getSrcUrls = getSrcUrls = (type, pkg, gzip) =>
+  throw new Error "Cannot find package '#{pkg}'" unless @assets[type][pkg]?
+
+  if @mode is 'production'
+    fingerprint = '-' + fingerprintForPkg(type, pkg)
+    extension = "#{type}"
+    extension += ".#{type[0]}gz" if gzip
+    src = (@cdnUrl ? @_assetsDir) + '/' + "#{pkg}#{fingerprint ? ''}.#{extension}"
+    return [src]
+
+  expandAssetGlobs()
+
+  urls = []
+  for filename, contents of preprocessPkg pkg, type
+    writeFile filename, contents unless @usingMiddleware
+    urls.push "#{@_assetsDir}/#{filename}"
+  return urls
+
+# Run js pre-processors & output the packages in dev.
+#
+# @param {String} pkg The name of the package to output
+# @return {String} Script tag(s) pointing to the ouput package(s)

 module.exports.js = (pkg, gzip = @gzip) =>
-  throw new Error "Cannot find package '#{pkg}'" unless @assets.js[pkg]?
-
-  if @mode is 'production'
-    fingerprint = '-' + fingerprintForPkg('js', pkg) if @mode is 'production'
-    src = (@cdnUrl ? @_assetsDir) + '/' + "#{pkg}#{fingerprint ? ''}.js"
-    src += '.jgz' if gzip
-    return "<script src='#{src}' type='text/javascript'></script>"
-
-  expandAssetGlobs()
-
+  urls = getSrcUrls('js', pkg, gzip)
   output = ''
-  for filename, contents of preprocessPkg pkg, 'js'
-    writeFile filename, contents unless @usingMiddleware
-    output += "<script src='#{@_assetsDir}/#{filename}' type='text/javascript'></script>"
+  for url in urls
+    output += "<script src='#{url}' type='text/javascript'></script>"
   output

 # Run css pre-processors & output the packages in dev.
@@ -90,20 +104,10 @@
 # @return {String} Link tag(s) pointing to the ouput package(s)

 module.exports.css = (pkg, gzip = @gzip) =>
-  throw new Error "Cannot find package '#{pkg}'" unless @assets.css[pkg]?
-
-  if @mode is 'production'
-    fingerprint = '-' + fingerprintForPkg('css', pkg) if @mode is 'production'
-    src = (@cdnUrl ? @_assetsDir) + '/' + "#{pkg}#{fingerprint ? ''}.css"
-    src += '.cgz' if gzip
-    return "<link href='#{src}' rel='stylesheet' type='text/css'>"
-
-  expandAssetGlobs()
-
+  urls = getSrcUrls('css', pkg, gzip)
   output = ''
-  for filename, contents of preprocessPkg pkg, 'css'
-    writeFile filename, contents unless @usingMiddleware
-    output += "<link href='#{@_assetsDir}/#{filename}' rel='stylesheet' type='text/css'>"
+  for url in urls
+    output += "<link href='#{url}' rel='stylesheet' type='text/css'>"
   output

 # Compile the templates into JST['file/path'] : functionString pairs in dev


Fix issue #80 by adding a callback which is invoked at the end of writeFile so that done() can be passed as a callback when using grunt.

--- lib/nap.coffee
+++ lib/nap.coffee
@@ -148,9 +148,9 @@
       contents = uglify contents if @mode is 'production' and @minify
       fingerprint = '-' + fingerprintForPkg('js', pkg)
       filename = "#{pkg}#{fingerprint ? ''}.js"
-      writeFile filename, contents
-      if @gzip then gzipPkg(contents, filename, callback) else callback()
-      total++
+      writeFile filename, contents, (err) =>
+        if @gzip then gzipPkg(contents, filename, callback) else callback()
+        total++

   if @assets.css?
     for pkg, files of @assets.css
@@ -160,9 +160,9 @@
       contents = sqwish.minify contents if @mode is 'production'
       fingerprint = '-' + fingerprintForPkg('css', pkg)
       filename = "#{pkg}#{fingerprint ? ''}.css"
-      writeFile filename, contents
-      if @gzip then gzipPkg(contents, filename, callback) else callback()
-      total++
+      writeFile filename, contents, (err) =>
+        if @gzip then gzipPkg(contents, filename, callback) else callback()
+        total++

   if @assets.jst?
     for pkg, files of @assets.jst
@@ -171,9 +171,9 @@
       contents = uglify contents if @mode is 'production' and @minify
       fingerprint = '-' + fingerprintForPkg('jst', pkg)
       filename = "#{pkg}#{fingerprint ? ''}.jst.js"
-      writeFile filename , contents
-      if @gzip then gzipPkg(contents, filename, callback) else callback()
-      total++
+      writeFile filename , contents, (err) =>
+        if @gzip then gzipPkg(contents, filename, callback) else callback()
+        total++

 # Instead of compiling & writing the packages to disk, nap will compile and serve the files in
 # memory per request.
@@ -365,11 +365,12 @@
 # @param {String} contents Contents of the file to be output
 # @return {String} The new full directory of the output file

-writeFile = (filename, contents) =>
+writeFile = (filename, contents, callback) =>
   file = path.join(@_outputDir, filename)
   dir = path.dirname file
   mkdirp.sync dir, '0755' unless fs.existsSync dir
   fs.writeFileSync file, contents ? ''
+  callback() if callback?

 # Runs uglify js on a string of javascript
 #


removed fugue

removed fugue


Optimize the directive

Optimize the directive

The step calculation is separated to improve the performances

--- src/angular-moment-timepicker.coffee
+++ src/angular-moment-timepicker.coffee
@@ -113,7 +113,6 @@
       -steps[scope.view].line

     scope.keyPress = ($event) ->
-
       selected = moment(scope.selected)

       switch $event.keyCode
@@ -187,10 +186,38 @@
       scope.position = date.toDate()

-    ###
-      Helper method that split the steps by week
-    ###
+    #
+    # Compute a boolean value that indicate if
+    # switching to the next view would put the
+    # cursor on a date after maxDate.
+    #
+    # Returns true if maxDate is not defined.
+    #
+    canNextView = ->
+      if not scope.maxDate?
+        return true
+      else
+        return moment(scope.selected).add(steps[scope.view].next).isBefore(scope.maxDate)
+
+    #
+    # Compute a boolean value that indicate if
+    # switching to the previous view would put the
+    # cursor on a date before minDate.
+    #
+    # Returns true if minDate is not defined.
+    #
+    canPreviousView = ->
+      if not scope.minDate?
+        return true
+      else
+        return moment(scope.selected).add(steps[scope.view].previous).isAfter(scope.minDate)
+
+    #
+    #  Helper method that split the steps by week
+    #
     splitByWeeks = (steps = []) ->
+      return if not scope.steps?
+
       lastWeek = null
       stepsByWeek = []
       for step in steps
@@ -199,79 +226,64 @@
           stepsByWeek.push []
           lastWeek = week
         stepsByWeek[stepsByWeek.length - 1].push step
+
       return stepsByWeek

-    scope.$watch('activeBefore + activeAfter + view + position', ->
-      # need to update the steps.
-      return if not scope.steps? or not (scope.activeBefore? or scope.activeAfter?)
-
+    scope.$watch('minDate + maxDate + steps.length', ->
+      return if not scope.steps?
       for step in scope.steps
-        date = moment(step.value)
-        isBefore = if scope.activeBefore? then date.isBefore(scope.activeBefore) else true
-        isAfter = if scope.activeAfter? then date.isAfter(scope.activeAfter) else true
-        step.active = isBefore and isAfter
+        stepDate = moment(step.value)
+        afterMinimum = if scope.minDate? then stepDate.isAfter(scope.minDate, scope.view) else true
+        beforeMaximum = if scope.maxDate? then stepDate.isBefore(scope.maxDate, scope.view) else true
+        step.selectable = afterMinimum and beforeMaximum
     )

-    scope.$watch('view + position + minDate + maxDate + selected', ->
-      # Get the starting date.
+    scope.$watch('activeBefore + activeAfter + steps.length', ->
+      return if not scope.steps?
+      for step in scope.steps
+        stepDate = moment(step.value)
+        if not scope.activeBefore? and not scope.activeAfter?
+          step.active = false
+        else
+          isBefore = if scope.activeBefore? then stepDate.isBefore(scope.activeBefore, scope.view) or stepDate.isSame(scope.activeBefore, scope.view) else true
+          isAfter = if scope.activeAfter? then stepDate.isAfter(scope.activeAfter, scope.view) or stepDate.isSame(scope.activeAfter, scope.view) else true
+          step.active = isBefore and isAfter
+    )
+
+    scope.$watch('selected + steps.length', ->
+      return if not scope.steps?
+
+      for step in scope.steps
+        step.selected = moment(step.value).isSame(scope.selected, scope.view)
+
+      scope.$canNext = canNextView()
+      scope.$canPrevious = canPreviousView()
+    )
+
+    scope.$watch('position + view', ->
+      # Position only.
       step = steps[scope.view]
       stepDate = step.first(moment(scope.position))
       period = step.step
       amount = step.amount

+      # TODO: Fix this!
       lowerDate = moment(scope.position).startOf(scope.view)
       upperDate = moment(scope.position).add(1, scope.view)
-      currentDate = moment(scope.selected)
-
-      scope.steps = for i in [1..amount]
-
-        # TODO: Use the step to make the comparison!
-        before = if scope.minDate? then stepDate.isBefore(scope.minDate, scope.view) else true
-        after = if scope.maxDate? then stepDate.isAfter(scope.maxDate, scope.view) else true
-        same = stepDate.isSame(currentDate, scope.view)
-        newDate = {
-          selectable: not (before and after)
-          active: same #
-          past: stepDate.isBefore(lowerDate)
-          future: stepDate.isBefore(upperDate)
-
-          formatted: stepDate.format(step.format)
-          value: stepDate.toDate()
+      console.log "#{ lowerDate.format() } -> #{ lowerDate.format() } (#{  scope.view })"
+
+      scope.steps = for i in [0..amount] by 1
+        stepDate.add(period)
+        {
+        past: moment(stepDate).isBefore(lowerDate)
+        future: moment(stepDate).isAfter(upperDate)
+        formatted: moment(stepDate).format(step.format)
+        value: moment(stepDate).toDate()
         }
-        stepDate = moment(stepDate).add(period)
-        newDate # Important!
-
-      scope.stepsByWeek = splitByWeeks(scope.steps)
-
-      scope.$canNext = canNextView()
-      scope.$canPrevious = canPreviousView()
+      scope.byWeeks = splitByWeeks(scope.steps) if scope.view == 'day'
+      return
     )

-    #
-    # Compute a boolean value that indicate if
-    # switching to the next view would put the
-    # cursor on a date after maxDate.
-    #
-    # Returns true if maxDate is not defined.
-    #
-    canNextView = ->
-      if not scope.maxDate?
-        return true
-      else
-        return moment(scope.selected).add(steps[scope.view].next).isBefore(scope.maxDate)
-
-    #
-    # Compute a boolean value that indicate if
-    # switching to the previous view would put the
-    # cursor on a date before minDate.
-    #
-    # Returns true if minDate is not defined.
-    #
-    canPreviousView = ->
-      if not scope.minDate?
-        return true
-      else
-        return moment(scope.selected).add(steps[scope.view].previous).isAfter(scope.minDate)

     #
     # Return the next view value.
@@ -355,13 +367,13 @@
                        data-ng-repeat="step in steps"
                        data-ng-class="{active: step.active, past: step.past, future: step.future, disabled: !step.selectable}"
                        data-ng-click="select(step,$event)"
-                       data-ng-mouseover="hover(step,$event)">{{ step.formatted }}
+                       data-ng-mouseenter="hover(step,$event)">{{ step.formatted }}

-

-           <td data-ng-repeat="step in week"
+

+           {{ step.formatted }}


Make tz selection test non-system-specific

--- frontend/registration_tz_selection.coffee
+++ frontend/registration_tz_selection.coffee
@@ -18,7 +18,14 @@
   tz_value = @evaluate ->
     document.querySelector('#tz_date').value
   # Should match system time zone
-  @test.assertMatch tz_value, /^GMT-05:00/
+  # System time zone is our time zone
+  offset_hours = (new Date()).getTimezoneOffset() / -60
+  sign = if offset_hours < 0 then '-' else '+'
+  offset = Math.abs(offset_hours)
+  if offset < 10
+    offset = '0' + offset
+  # XXX does not handle fractional hour offsets
+  @test.assertMatch tz_value, new RegExp("^GMT#{sign}#{offset}:00")

 casper.run ->
   @test.done()