trying to create better responsive experience around adding new points

Former-commit-id: 20a9738ec7d4367b23718e3feaa8ed5d2dd40e43 [formerly bd4287563d3b56252410311d45c36cb901cd557c] [formerly 255d984d4998aa881155462b2d44dfab9f72a7cc [formerly d8db2422dc76ddf29413512045182297c891ab52]] [formerly f9418e8a2abae0724a74f9d148d6bf8b8769a832 [formerly b02077c9d73d82a8bbe458464d3136e2a8e07aaf] [formerly 260acd8182a8c1295aa5020a7040bdd02510a819 [formerly 260acd8182a8c1295aa5020a7040bdd02510a819 [formerly 1422b3a0601a7f21399b27a67cdb060ddd5e44bd]]]]
Former-commit-id: 39bda995ed6a8a0555c78c5b5655dae8eea2b15e [formerly aaac7d6c3367cc1ee7a50e905f6b08aa17aa90e3] [formerly e1221e7589c6c9f7d4e4be88672702961f142fae [formerly 9861f23efaf812d2bc192215b02b63db16b4b993]]
Former-commit-id: 6b3bced9f8eb64fea2a986f26c89977152b1ef15 [formerly dbcd687a6424fff76a601101b0e9d49e7f92d330]
Former-commit-id: 4e775f63a5e4d2351cfef3dc7863ca7696b89e9c
Former-commit-id: ea958c9533e8b2b866c5e5ca698efe98501020e7

--- @client/franklin.coffee
+++ @client/franklin.coffee
@@ -169,13 +169,19 @@
     current_user = fetch('/current_user')
     subdomain = fetch '/subdomain'

+
+    point_cols = ['your_con_points', 'your_pro_points', 'community_cons', 'community_pros']
+    edit_mode = false
+    for pc in point_cols
+      col = fetch(pc)
+      if col.adding_new_point || col.editing_points?.length > 0
+        edit_mode = pc
+        break
+
     has_focus = \
       if get_selected_point()
         'point'
-      else if fetch('your_con_points').adding_new_point ||
-              fetch('your_pro_points').adding_new_point ||
-              fetch('your_con_points').editing_points?.length > 0 ||
-              fetch('your_pro_points').editing_points?.length > 0
+      else if edit_mode
         'edit point'
       else
         "opinion"
@@ -367,10 +373,23 @@
               (if mode == 'results' then 'score' else 'last_inclusion'), \
               mode == 'crafting'

-
-
-      SaveYourOpinionFooter()
-      if mode == 'results' &&
+      if edit_mode && browser.is_mobile
+        # full screen edit point mode for mobile
+        valence = if edit_mode in ['community_pros', 'your_pro_points']
+                    'pros'
+                  else
+                    'cons'
+        pc = fetch edit_mode
+        EditPoint
+          key: if edit_mode.adding_new_point then "new_point_#{valence}" else pc.editing_points[0]
+          fresh: edit_mode.adding_new_point
+          valence: valence
+          your_points_key: edit_mode
+
+      else if !edit_mode
+        SaveYourOpinionFooter()
+
+      else if mode == 'results' &&
           your_opinion.published &&
           customization('ThanksForYourOpinion', @proposal)
         customization('ThanksForYourOpinion', @proposal)()
@@ -1003,6 +1022,7 @@
         fontSize: 24

       'Your opinion hasn’t been added yet! '
+
       SPAN
         style:
           fontWeight: 700
@@ -1678,7 +1698,6 @@
       borderWidth: 3
       borderStyle: 'solid'
       borderColor: 'transparent'
-      left: -3
       top: -3
       position: 'relative'
       zIndex: 1
@@ -1693,7 +1712,15 @@
         padding: 8
         borderRadius: 8
         top: point_content_style.top - 8
-        left: point_content_style.left - 8
+        left: -11
+        #width: point_content_style.width + 16
+
+    else if @props.rendered_as == 'community_point'
+      _.extend point_content_style,
+        padding: 8
+        borderRadius: if TWO_COL() then "16px 16px 0 0" else 16
+        top: point_content_style.top - 8
+        #left: point_content_style.left - 8
         #width: point_content_style.width + 16

     else if @props.rendered_as == 'under_review'
@@ -1783,9 +1810,9 @@
           style:
             position: 'absolute'
             left: 0
-            top: -3
-
-          if @data().is_pro then '+' else '–'
+            top: 0
+
+          if @data().is_pro then '•' else '•'

       else
         DIV
@@ -1869,9 +1896,9 @@
               @props.rendered_as == 'decision_board_point' || TWO_COL()
             A
               style:
-                fontSize: 12
-                color: '#999'
-                paddingRight: 8
+                fontSize: if browser.is_mobile then 18 else 14
+                color: focus_blue
+                padding: '3px 12px 3px 0'

               onClick: ((e) =>
                 e.stopPropagation()
@@ -1885,9 +1912,9 @@
             A
               'data-action': 'delete-point'
               style:
-                fontSize: 12
-                color: '#999'
-                paddingRight: 8
+                fontSize: if browser.is_mobile then 18 else 14
+                color: focus_blue
+                padding: '3px 8px'
               onClick: (e) =>
                 e.stopPropagation()
                 if confirm('Delete this point forever?')
@@ -1898,13 +1925,11 @@
         included = @included()
         DIV
           style:
-            border: "1px solid #{ if included || @local.hover_important then focus_blue else '#aaa'}"
+            border: "1px solid #{ if included || @local.hover_important then focus_blue else '#414141'}"
             borderTopColor: if included then focus_blue else 'transparent'
-            color: if included then 'white' else if @local.hover_important then focus_blue else "#888"
-            width: point_content_style.width - 24
+            color: if included then 'white' else if @local.hover_important then focus_blue else "#414141"
             position: 'relative'
-            left: 12
-            top: -3
+            top: -13
             padding: '8px 5px'
             textAlign: 'center'
             borderRadius: '0 0 16px 16px'
@@ -1935,7 +1960,7 @@
               display: 'inline-block'
               marginRight: 10

-          "Important point"
+          "Important point#{if included then '' else '?'}"

       if is_selected
@@ -2687,132 +2712,69 @@
       sign_name : if @props.fresh then true else !@data().hide_name
       add_details : false

+    mobile = browser.is_mobile
+
     textarea_style =
       width: '100%'
       overflow: 'hidden'
-      fontSize: 14
+      fontSize: if mobile then 30 else 14
       padding: '4px 6px'

-    guidelines_w = 230
-    guidelines_h = 238
-
-    singular =  if @props.valence == 'pros'
-                  customization('point_labels.pro', @proposal)
-                else
-                  customization('point_labels.con', @proposal)
-
-    plural =  if @props.valence == 'pros'
-                customization('point_labels.pros', @proposal)
-              else
-                customization('point_labels.cons', @proposal)
-
-
-    DIV
-      className: 'edit_point'
-      style:
+
+    if mobile
+      # full page adding a point if we're on mobile
+      style =
+        position: 'fixed'
+        top: 0
+        left: 0
+        height: '100%'
+        width: WINDOW_WIDTH()
+        backgroundColor: 'white'
+        fontSize: 16
+        zIndex: 99999999
+        padding: if mobile then '20px 50px'
+
+    else
+      style =
         position: 'relative'
         fontSize: 14
         zIndex: 1
         marginTop: if TWO_COL() then 40
-
-      if !@props.fresh
-        LABEL
-          htmlFor:'nutshell'
-          'Your point'
-      else
-        DIV
-          style:
-            color: focus_blue
-            position: 'absolute'
-            top: -22
-          "Write a new "
-          capitalize \
-            if @props.valence == 'pros'
-              customization('point_labels.pro', @proposal)
-            else
-              customization('point_labels.con', @proposal)
+        marginBottom: 15
+
+
+    DIV
+      className: 'edit_point'
+      style: style
+
+      onTouchMove: (e) ->
+        e.preventDefault()
+
+
+      if !mobile
+        if !@props.fresh
+          LABEL
+            htmlFor:'nutshell'
+            'Your point'
+        else
+          DIV
+            style:
+              color: focus_blue
+              position: 'absolute'
+              top: -22
+            "Write a new "
+            capitalize \
+              if @props.valence == 'pros'
+                customization('point_labels.pro', @proposal)
+              else
+                customization('point_labels.con', @proposal)

       DIV
         style:
           position: 'relative'

-        # guidelines/tips for good points
-        DIV
-          style:
-            position: 'absolute'
-            left: if @props.valence == 'pros' then -280 else 280
-            width: guidelines_w
-            color: focus_blue
-            zIndex: 1
-
-
-          SVG
-            width: guidelines_w + 28
-            height: guidelines_h
-            viewBox: "-4 0 #{guidelines_w+20 + 9} #{guidelines_h}"
-            style: css.crossbrowserify
-              position: 'absolute'
-              transform: if @props.valence == 'cons' then 'scaleX(-1)'
-              left: if @props.valence == 'cons' then -20
-
-            DEFS null,
-              svg.dropShadow
-                id: "guidelines-shadow"
-                dx: '0'
-                dy: '2'
-                stdDeviation: "3"
-                opacity: .5
-
-            PATH
-              stroke: focus_blue #'#ccc'
-              strokeWidth: 1
-              fill: "#FFF"
-              filter: 'url(#guidelines-shadow)'
-
-              d: """
-                  M#{guidelines_w},33
-                  L#{guidelines_w},0
-                  L1,0
-                  L1,#{guidelines_h}
-                  L#{guidelines_w},#{guidelines_h}
-                  L#{guidelines_w},58
-                  L#{guidelines_w + 20},48
-                  L#{guidelines_w},33
-                  Z
-                 """
-          DIV
-            style:
-              padding: '14px 18px'
-              position: 'relative'
-              marginLeft: 5
-
-
-            SPAN
-              style:
-                fontWeight: 600
-              "Write a "
-              capitalize singular
-              ' (or question) for this proposal'
-
-            UL
-              style:
-                listStylePosition: 'outside'
-                marginLeft: 16
-                marginTop: 5
-
-              do ->
-                tips = ["Make one coherent point. Add multiple #{capitalize(plural)} if you have more.",
-                        "Be direct. The summary is your main point.",
-                        "Review your language. Don’t be careless.",
-                        "No personal attacks."
-                       ]
-
-                for tip in tips
-                  LI
-                    style:
-                      paddingBottom: 3
-                    tip
+        @drawTips()

         CharacterCountTextInput
@@ -2825,6 +2787,7 @@
           defaultValue: if @props.fresh then null else @data().nutshell
           style: _.extend {}, textarea_style,
             minHeight: 75
+
         INPUT
           id:'is_pro'
           name: 'is_pro'
@@ -2833,24 +2796,6 @@

       DIV null,
-        # A
-        #   onClick: =>
-        #     @local.add_details = !@local.add_details
-        #     save(@local)
-        #   title: 'Provide background and/or back your point up with evidence.'
-        #   style: { fontSize: 14 }
-
-        #   I className: if @local.add_details
-        #                  'fa-caret-down fa'
-        #                else
-        #                  'fa-caret-right fa'
-        #   SPAN
-        #     style: {paddingLeft: 6}
-        #     'Add details'
-
-        # DIV
-        #   style:
-        #     display: if @local.add_details then 'block' else 'none'

         AutoGrowTextArea
           id:'text'
@@ -2880,7 +2825,6 @@

       DIV
         style:
-          textAlign: 'right'
           marginTop: 3
           marginBottom: '.5em'

@@ -2894,28 +2838,37 @@
             'data-action': 'submit-point'
             onClick: @savePoint
             style:
-              marginTop: '.5em'
-              width: '100%'
-              fontSize: 24
-              padding: 4
+              marginTop: 0
+              display: 'inline-block'
+              fontSize: if mobile then 36 else 24
+              padding: '4px 30px'
+              float: 'left'
             'Done'

         A
+          onTouchEnd: @done
           onClick: @done
           style:
             display: 'inline-block'
             color: '#888888'
             cursor: 'pointer'
+            zIndex: 1
+            top: if mobile then 5 else 12
+            fontSize: if mobile then 30 else 16
+            right: 20
             position: 'relative'
-            zIndex: 1
-            top: 5
+            float: 'right'
           'cancel'
+
+        DIV
+          style:
+            clear: 'both'

       if @proposal.active
         DIV
           style:
             position: 'relative'
-            top: -20
+
           INPUT
             className: 'newpoint-anonymous'
             type:      'checkbox'
@@ -2935,6 +2888,103 @@
       $el = $(@getDOMNode())
       $el.find('#nutshell').focus()
       $el.find('[data-action="submit-point"]').ensureInView {scroll: false, position: 'bottom'}
+
+  drawTips : ->
+    # guidelines/tips for good points
+    mobile = browser.is_mobile
+
+    guidelines_w = if mobile then 'auto' else 230
+    guidelines_h = 238
+
+    singular =  if @props.valence == 'pros'
+                  customization('point_labels.pro', @proposal)
+                else
+                  customization('point_labels.con', @proposal)
+
+    plural =  if @props.valence == 'pros'
+                customization('point_labels.pros', @proposal)
+              else
+                customization('point_labels.cons', @proposal)
+
+
+    DIV
+      style:
+        position: if mobile then 'relative' else 'absolute'
+        left: if !mobile then (if @props.valence == 'pros' then -265 else 265)
+        width: guidelines_w
+        color: focus_blue
+        zIndex: 1
+        marginBottom: if mobile then 20
+
+
+      if !mobile
+        SVG
+          width: guidelines_w + 28
+          height: guidelines_h
+          viewBox: "-4 0 #{guidelines_w+20 + 9} #{guidelines_h}"
+          style: css.crossbrowserify
+            position: 'absolute'
+            transform: if @props.valence == 'cons' then 'scaleX(-1)'
+            left: if @props.valence == 'cons' then -20
+
+          DEFS null,
+            svg.dropShadow
+              id: "guidelines-shadow"
+              dx: '0'
+              dy: '2'
+              stdDeviation: "3"
+              opacity: .5
+
+          PATH
+            stroke: focus_blue #'#ccc'
+            strokeWidth: 1
+            fill: "#FFF"
+            filter: 'url(#guidelines-shadow)'
+
+            d: """
+                M#{guidelines_w},33
+                L#{guidelines_w},0
+                L1,0
+                L1,#{guidelines_h}
+                L#{guidelines_w},#{guidelines_h}
+                L#{guidelines_w},58
+                L#{guidelines_w + 20},48
+                L#{guidelines_w},33
+                Z
+               """
+      DIV
+        style:
+          padding: if !mobile then '14px 18px'
+          position: 'relative'
+          marginLeft: 5
+
+        SPAN
+          style:
+            fontWeight: 600
+            fontSize: if mobile then 36
+          "Write a "
+          capitalize singular
+          ' (or question) for this proposal'
+
+        UL
+          style:
+            listStylePosition: 'outside'
+            marginLeft: 16
+            marginTop: 5
+
+          do ->
+            tips = ["Make one coherent point. Add multiple #{capitalize(plural)} if you have more.",
+                    "Be direct. The summary is your main point.",
+                    "Review your language. Don’t be careless.",
+                    "No personal attacks."
+                   ]
+
+            for tip in tips
+              LI
+                style:
+                  paddingBottom: 3
+                  fontSize: if mobile then 14
+                tip

   done : ->


histogram responds to mouseenters/leaves in addition to click selects

Former-commit-id: adc216410b8c55dc026f2c5472d48984e15ddc66 [formerly bc5f97fa1485eb5dd9e613667117f100da6f4d23] [formerly 6afcaafe46fb02ae72bf6467c15887c94eaae8e3 [formerly d9e573a845f423402fd8999f58629b943047e279]] [formerly 578e7cd5b5958dc92be9d9bb5676cf5a5cb5e74b [formerly 09920c779c46d1389a34b54461e200a6aebe5f54] [formerly c84763118fe07db7a212c327c22546f48383df58 [formerly c84763118fe07db7a212c327c22546f48383df58 [formerly 03869a1d4fc38d96380ec8f55c4f2e2264d19546]]]]
Former-commit-id: fb89c3e22fd8f17205d11ce19ca7a8d1fd8e2eef [formerly bb4e71fe98a912d80b7417b12d3c20856bef7c48] [formerly dec5a6e5531723fb8f878be421a23653938192f1 [formerly e098bb77151c8ea326f2b4562edfd3fe4a0ae737]]
Former-commit-id: 2ac64ad034c128ca408c55f2365d45c6fb658048 [formerly 23840feae078d8f358850ebdf570cedf7fb55c4e]
Former-commit-id: 8592e385c6c1cfb266dfd3c81fe777a1541bd5d3
Former-commit-id: 70ade6703c536a802b4d1ba35973dcc11398fedf

--- @client/franklin.coffee
+++ @client/franklin.coffee
@@ -112,7 +112,7 @@

   getInitialState : ->
     users_to_highlight_in_histogram : []
-    selected_segment_in_histogram : null
+    selected_segment_in_histogram : [null, null]

   # TODO: add prop types here

@@ -252,9 +252,10 @@
     route = if @props.state == 'results' then Routes.new_opinion_proposal_path(@props.data.proposal.long_id) else Routes.proposal_path(@props.data.proposal.long_id)
     app_router.navigate route, {trigger : true}

-  onSelectSegment : ( segment ) ->
+  onSelectSegment : ( segment, click_select ) ->
     @setState
-      selected_segment_in_histogram : segment
+      selected_segment_in_histogram : [segment, click_select]
+

   ##
   # Make this thing!
@@ -403,8 +404,16 @@
   onSelectSegment : (ev) ->
     if @props.state == 'results'
       segment = $(ev.currentTarget).data('segment')
-      # if clicking on already selected segment, then we'll deselect
-      @props.onSelectSegment if @props.selected_segment_in_histogram == segment then null else segment
+
+
+      # If clicking on already hard-selected segment, then we'll deselect.
+      segment = if @props.selected_segment_in_histogram[0] == segment && @props.selected_segment_in_histogram[1] then null else segment
+      click_select = ev.type == 'click'
+
+      #ignore mouseEnter and mouseLeave events if selection was via click
+      return if @props.selected_segment_in_histogram && @props.selected_segment_in_histogram[1] && !click_select
+
+      @props.onSelectSegment(segment, click_select && segment)

   render : ->
     [num_columns, segments, avatar_size] = @buildHistogram() #todo: memoize
@@ -415,7 +424,7 @@
     R.table className: 'histogram', style: { width: effective_histogram_width, marginLeft: margin_adjustment },
       R.tr null,
         for bars, segment in segments.reverse()
-          R.td className:"histogram_segment", key:segment, onClick: @onSelectSegment, 'data-segment':segment, style : { opacity: if !@props.selected_segment_in_histogram? || @props.selected_segment_in_histogram == segment then '1' else '.2' },
+          R.td className:"histogram_segment", key:segment, onClick: @onSelectSegment, onMouseLeave: @onSelectSegment, onMouseEnter: @onSelectSegment, 'data-segment':segment, style : { opacity: if !@props.selected_segment_in_histogram[0]? || @props.selected_segment_in_histogram[0] == segment then '1' else '.2' },
             R.table null,
               R.tr null,
                 for bar in bars
@@ -634,13 +643,13 @@
       has_not_been_included = @props.state == 'results' || !_.contains(@props.included_points, pnt.id)
       is_correct_valence && has_not_been_included

-    if @props.selected_segment_in_histogram?
+    if @props.selected_segment_in_histogram[0]?
       # If there is a histogram segment selected, we'll have to filter down
       # to the points that users in this segment think are important, and
       # order them by resonance to those users. I'm doing this quite inefficiently.
       point_inclusions_per_point_for_segment = {} # map of points to including users
       _.each @props.opinions, (opinion) =>
-        if opinion.stance_segment == 6 - @props.selected_segment_in_histogram && opinion.point_inclusions
+        if opinion.stance_segment == 6 - @props.selected_segment_in_histogram[0] && opinion.point_inclusions
           for point_id in opinion.point_inclusions
             if !_.has(point_inclusions_per_point_for_segment, point_id)
               point_inclusions_per_point_for_segment[point_id] = 1
@@ -657,7 +666,7 @@

     R.div className:"points_by_community #{@props.valence}s_by_community",
       R.h1 className:'points_heading_label', if @props.state == 'results' then "Top #{label}s" else "Others' #{label}s"
-      R.p className:'points_segment_label', if !@props.selected_segment_in_histogram? then '' else "for #{@props.stance_names[@props.selected_segment_in_histogram]}s"
+      R.p className:'points_segment_label', if !@props.selected_segment_in_histogram[0]? then '' else "for #{@props.stance_names[@props.selected_segment_in_histogram[0]]}s"

       R.ul null,
         for point in points


updated form validator to use new isValid method

--- assets/scripts/coffee/hoarder/validator/form_validator.coffee
+++ assets/scripts/coffee/hoarder/validator/form_validator.coffee
@@ -12,10 +12,10 @@

   validateForm: (form)->
     @validateElement(element) for element in form.elements()
-    form.checkValidity()
+    form.isValid()

   validateElement: (element)->
-    clearValidationErrorsOn element
+    clearCustomErrorOn element

     type = element.getAttribute("type")
     for constraint in @constraints
@@ -26,7 +26,7 @@

   # private

-  clearValidationErrorsOn = (element)-> markValidityAs element, ""
+  clearCustomErrorOn = (element)-> markValidityAs element, ""

   markValidityAs = (element, message)-> element.setCustomValidity message


namespaced unbind

namespaced unbind

--- src/bindable.coffee
+++ src/bindable.coffee
@@ -44,7 +44,7 @@
   is triggered.
   ###
   bind: (events, callback) ->
-    events.split(" ")
+    events.split(" ").each (namespacedEvent) ->
     eventCallbacks[event] = eventCallbacks[event] || []

     eventCallbacks[event].push(callback)


Use preview tab like sublime

Use preview tab like sublime

--- config.cson
+++ config.cson
@@ -3,6 +3,7 @@
     userId: "53c5f869-8a29-d7f5-ef48-bc598612bb8d"
   welcome:
     showOnStartup: false
-  core: {}
   editor:
     invisibles: {}
+  tabs:
+    usePreviewTabs: true


Add karma test to webhook header

Add karma test to webhook header

--- karma/flows/test_controllers.coffee
+++ karma/flows/test_controllers.coffee
@@ -426,6 +426,7 @@
         scope.ruleset.ruleset_type = 'webhook'
         scope.formData.webhook = 'http://www.nyaruka.com'
         scope.formData.webhook_action = 'POST'
+        scope.formData.webhook_header = {"Authorization": "Token 12345"}

       ruleset = flowService.flow.rule_sets[0]
       expect(ruleset.ruleset_type).toBe('webhook')
@@ -436,6 +437,7 @@
       # our config should have a url
       expect(ruleset.config.webhook).toBe('http://www.nyaruka.com')
       expect(ruleset.config.webhook_action).toBe('POST')
+      expect(ruleset.config.webhook_header['Authorization']).toBe('Token 12345')

       # do it again, make sure we have the right number of rules
       editRules ruleset, (scope) ->


added classes for more highlighting types

--- lib/utils/highlighter.coffee
+++ lib/utils/highlighter.coffee
@@ -9,6 +9,10 @@
 decorToClasses = (decor) ->
   switch decor
     when ':type' then 'storage type'
+    when ':function' then 'entity name function'
+    when ':data' then 'constant'
+    when ':keyword' then 'keyword'
+    when ':bound' then ''
     else ''

 highlightWord = (word, info) ->


Fix number of lines in library constant

Fix number of lines in library constant

--- src/compiler.coffee
+++ src/compiler.coffee
@@ -6277,6 +6277,8 @@

 eval immutable

+completeLibrary = library + '\n' + immutable + '\n'
+
 # JS Reserved words

 reservedInJs = newSetWith ("abstract arguments boolean break byte case catch char class " +
@@ -6409,13 +6411,13 @@
     compiledDefinitions, exportDictionary)

 compileModuleWithDependencies = (typedModulePath = defaultTypedModulePath) ->
-  js: library + immutable + (listOfLines map lookupJs,
+  js: completeLibrary + (listOfLines map lookupJs,
     (setToArray runtimeDependencies (modulePathToName typedModulePath.names)))

 compileExpression = (source, typedModulePath = defaultTypedModulePath) ->
   {js} = parsed = parseExpression source, typedModulePath
   extend parsed,
-    (js:  library + immutable + (listOfLines map lookupJs,
+    (js:  completeLibrary + (listOfLines map lookupJs,
       (setToArray runtimeDependencies (modulePathToName typedModulePath.names))) + '\n;' + js)

 parseTopLevel = (source, typedModulePath = defaultTypedModulePath) ->
@@ -6836,7 +6838,7 @@
   types: ctx._scope()
   subs: ctx.substitution.fails
   ast: ast
-  compiled: library + immutable + compiledDefinitions.js + '\n;' + compiledExpression.js
+  compiled: completeLibrary + compiledDefinitions.js + '\n;' + compiledExpression.js

 typeEnumaration = (ctx) ->
   values mapMap _type, ctx._scope()
@@ -8385,7 +8387,7 @@
 exports.prettyPrint = prettyPrint
 exports.plainPrettyPrint = plainPrettyPrint
 exports.labelDocs = labelDocs
-exports.builtinLibraryNumLines = library.split('\n').length + immutable.split('\n').length
+exports.builtinLibraryNumLines = completeLibrary.split('\n').length - 1

 exports.library = library
 exports.immutable = immutable


syncstring: added MAX_LENGTH cap on size of syncstring.

--- salvus/syncstring.coffee
+++ salvus/syncstring.coffee
@@ -43,6 +43,10 @@
 INIT_POLL_INTERVAL     = 3000
 MAX_POLL_INTERVAL      = 20000   # TODO: for testing -- for deploy make longer!
 POLL_DECAY_RATIO       = 1.4
+
+# Maximum allowed syncstring size -- we keep this manageable since we have to be
+# able to load the entire string within a few seconds from the database.
+MAX_STRING_LENGTH      = 5000000

 # We grab patches that are up to TIMESTAMP_OVERLAP old from db each time polling.
 # We are assuming a write to the database propogates to
@@ -134,11 +138,24 @@
         @push_edits_to_browser(undefined, cb)

 # A string that is synchronized amongst all connected clients.
+# IMPORTANT: anything in the string after @max_size
+# will be automatically truncated -- i.e., if any operation results
+# in the string getting longer than that, it is truncated at the end.  It is
+# the responsibility of the client code to properly deal with this, e.g.,
+# by putting a sentinel character at the end of the string and checking for
+# it to see if the string was truncated as a result of a sync.
+
+# x={};require('bup_server').global_client(cb:(e,c)->x.c=c; x.s = require('syncstring'); x.s.init_syncstring_db(x.c.database); x.ss=x.s.get_syncstring_db(); x.s.get_syncstring(string_id:'4bd8cb98-506c-45a5-8042-5c6bd8fddff0',max_length:50,cb:(e,t)->x.t=t))
 class SynchronizedString
-    constructor: (@string_id) ->
+    constructor: (@string_id, @max_length) ->
         misc.call_lock(obj:@)
         @clients = {}
         @head = ''
+        if not @max_length?
+            @max_length = MAX_STRING_LENGTH
+        else
+            # no matter what, we never allow syncstrings to exceed the MAX_STRING_SIZE
+            @max_length = Math.min(@max_length, MAX_STRING_LENGTH)

     new_browser_client: (opts) =>
         opts = defaults opts,
@@ -159,6 +176,8 @@
         client = new diffsync.DiffSync(doc:@head)
         client.id = opts.session_id
         client.sync = (cb) =>
+            if client.live.length > @max_length
+                client.live = client.live.slice(0,@max_length)
             client.shadow    = client.live
             client.last_sync = client.live
             client.emit('sync')
@@ -181,13 +200,15 @@
                 if err
                     opts.cb(err)
                 else
+                    if client.live.length > @max_length
+                        client.live = client.live.slice(0,@max_length)
                     @clients[opts.session_id] = client
                     opts.cb(undefined, client)

     sync: (cb) =>
         @_call_with_lock(@_sync, cb)

-    _sync: (cb) =>
+    _sync: (cb, retry) =>
         last = @head
         #winston.debug("sync: last='#{last}'")
         all = {}
@@ -202,10 +223,15 @@

         v = []
         for _, client of @clients
+            t0 = misc.mswalltime()
+            #winston.debug("local_client_sync: start...")
             v.push(client)
             patch = diffsync.dmp.patch_make(last, client.live)
+            #winston.debug("local_client_sync: computing patch in #{misc.mswalltime(t0)}ms"); t0 = misc.mswalltime()
             @head = diffsync.dmp.patch_apply(patch, @head)[0]
+            #winston.debug("local_client_sync: applied patch in #{misc.mswalltime(t0)}ms"); t0 = misc.mswalltime()
             #winston.debug("sync: new head='#{@head}' from patch=#{misc.to_json(patch)}")
+
         for _, client of @clients
             client.live = @head

@@ -228,21 +254,29 @@
             else
                 cb()
         async.map v, f, () =>
-            if misc.len(successful_live) > 1 # if not stable (with ones with no err), do again.
+            if not retry and misc.len(successful_live) > 0
+                # if not stable (with ones with no err), do again; but not more than once.
                 #winston.debug("syncing again")
-                @_sync(cb)
+                @_sync(cb, true)
             else
                 #winston.debug("not syncing again since successful_live='#{misc.to_json(successful_live)}'")
                 cb?()

 sync_strings = {}

-exports.get_syncstring = get_syncstring = (string_id, cb) ->
-    S = sync_strings[string_id]
+exports.get_syncstring = get_syncstring = (opts) ->
+    opts = defaults opts,
+        string_id  : required
+        max_length : MAX_STRING_LENGTH   # max length of string; anything more gets silently truncated!
+        cb         : required
+    if opts.max_length > MAX_STRING_LENGTH
+        opts.cb("max_length of string may be at most #{MAX_STRING_LENGTH}")
+        return
+    S = sync_strings[opts.string_id]
     if S?
-        cb(undefined, S); return
-
-    S = new SynchronizedString(string_id)
+        opts.cb(undefined, S); return
+
+    S = new SynchronizedString(opts.string_id, opts.max_length)
     async.series([
         (cb) =>
             S.new_database_client
@@ -267,10 +301,10 @@
     ], (err) =>
         if err
             # TODO: undo any other harm
-            cb(err)
+            opts.cb(err)
         else
-            sync_strings[string_id] = S
-            cb(undefined, S)
+            sync_strings[opts.string_id] = S
+            opts.cb(undefined, S)
     )

 exports.syncstring = (opts) ->
@@ -278,19 +312,23 @@
         string_id      : required
         session_id     : required
         push_to_client : required    # function that allows for sending a JSON message to remote client
+        max_length     : undefined
         cb             : required
-    get_syncstring opts.string_id, (err, S) =>
-        if err
-            opts.cb(err)
-        else
-            S.new_browser_client
-                session_id     : opts.session_id
-                push_to_client : opts.push_to_client
-                cb             : (err, client) ->
-                    if err
-                        opts.cb(err)
-                    else
-                        opts.cb(undefined, client)
+    get_syncstring
+        string_id  : opts.string_id
+        max_length : opts.max_length
+        cb         : (err, S) =>
+            if err
+                opts.cb(err)
+            else
+                S.new_browser_client
+                    session_id     : opts.session_id
+                    push_to_client : opts.push_to_client
+                    cb             : (err, client) ->
+                        if err
+                            opts.cb(err)
+                        else
+                            opts.cb(undefined, client)

 # The hub has to call this on startup in order for syncstring to work.
 syncstring_db = undefined
@@ -484,14 +522,20 @@
                 #dbg("nothing to do for #{string_id}")
                 cb() # nothing to do
             else
+                t0 = misc.mswalltime()
+                #dbg("starting patch make from length #{string.last_sync.length} to #{string.live.length}...")
                 patch = diffsync.dmp.patch_make(string.last_sync, string.live)
+                #dbg("made patch for #{string_id} in #{misc.mswalltime(t0)}ms"); t0 = misc.mswalltime()
                 #dbg("patch for #{string_id} = #{misc.to_json(patch)}")
                 timestamp = cass.now() - 0
+                patch_as_string = @patch_to_string(patch)
+                #dbg("converted patch to string in #{misc.mswalltime(t0)}ms"); t0 = misc.mswalltime()
                 @db.update
                     table : 'syncstrings'
-                    set   : {patch:@patch_to_string(patch)}
+                    set   : {patch:patch_as_string}
                     where : {string_id:string_id, timestamp:timestamp}
                     cb    : (err) =>
+                        #dbg("wrote patch to database in #{misc.mswalltime(t0)}ms"); t0 = misc.mswalltime()
                         if err
                             cb(err)
                         else
@@ -794,24 +838,31 @@
 # There is a corresponding implementation run by clients.
 #---------------------------------------------------------------------

+###
+x={};require('bup_server').global_client(cb:(e,c)->x.c=c; x.s = require('syncstring'); x.s.init_syncstring_db(x.c.database); x.ss=x.s.syncdb(string_id:'4bd8cb98-506c-45a5-8042-5c6bd8fddff1', cb:(e,t)->x.t=t))
+###
 _syncdb_cache = {}
 exports.syncdb = (opts) ->
     opts = defaults opts,
         string_id      : required
+        max_length     : MAX_STRING_LENGTH
         cb             : required
     d = _syncdb_cache[opts.string_id]
     if d?
         opts.cb(undefined, d)
         return
-    get_syncstring opts.string_id, (err, S) =>
-        if err
-            opts.cb(err)
-        else
-            doc = new diffsync.SynchronizedDB_DiffSyncWrapper(S.in_memory_client)
-            S.db_client.on 'changed', () =>
-                doc.emit("sync")
-            d = _syncdb_cache[opts.string_id] = new diffsync.SynchronizedDB(doc)
-            d.string_id = opts.string_id
-            opts.cb(undefined, d)
-
-
+    get_syncstring
+        string_id  : opts.string_id
+        max_length : opts.max_length
+        cb         : (err, S) =>
+            if err
+                opts.cb(err)
+            else
+                doc = new diffsync.SynchronizedDB_DiffSyncWrapper(S.in_memory_client)
+                S.db_client.on 'changed', () =>
+                    doc.emit("sync")
+                d = _syncdb_cache[opts.string_id] = new diffsync.SynchronizedDB(doc)
+                d.string_id = opts.string_id
+                opts.cb(undefined, d)
+
+


reset the artwork grid view when there are no results

--- desktop/components/commercial_filter/models/filter.coffee
+++ desktop/components/commercial_filter/models/filter.coffee
@@ -112,6 +112,8 @@
         .then ({ filter_artworks }) =>
           if filter_artworks.hits.length
             @artworks.reset filter_artworks.hits
+          else
+            @artworks.reset []
           @set loading: false
           @set total: filter_artworks.total
           @set followed_artists_total: filter_artworks.followed_artists_total


error message now nested inside error like nodejs style

--- routes/game_routes.coffee
+++ routes/game_routes.coffee
@@ -57,7 +57,7 @@
     else
       if req.body.deviceToken isnt game.nextTurn
         status_code = 500
-        json_response = JSON.stringify {error: "It is not your turn, no cheatin!!!"}
+        json_response = JSON.stringify {error: {message: "It is not your turn, no cheatin!!!"} }
         res.writeHead status_code, {"Content-Type": "application/json"}
         console.log "Response: #{json_response}"
         res.end json_response


Fix preview

Fix preview

--- client/apps/edit/components/section_image_set/index.coffee
+++ client/apps/edit/components/section_image_set/index.coffee
@@ -105,11 +105,13 @@
     $('.esis-preview-image-container').each (i, value) ->
       allowedPixels = 560.0 - 100 # min-width + margins
       totalPixels = 0.0
-      $(value).find('img').each (i, value) ->
-        _.defer ->
-          totalPixels = totalPixels + value.width
-          return if totalPixels > allowedPixels
-          $(value).css('display', 'inline-block')
+      $(value).find('img').each (i, img) ->
+        _.defer -> _.defer ->
+          totalPixels = totalPixels + img.width
+          if totalPixels > allowedPixels
+            $(img).css('display', 'none')
+          else
+            $(img).css('display', 'inline-block')

   render: ->
     section {


Move away from deprecated Electron require syntax

Required for atom/atom#10983

--- lib/github-file.coffee
+++ lib/github-file.coffee
@@ -1,4 +1,5 @@
-Shell = require 'shell'
+# TODO: Remove the catch once Atom 1.7.0 is released
+try {shell} = require 'electron' catch then shell = require 'shell'
 {Range} = require 'atom'
 parseUrl = require('url').parse

@@ -124,7 +125,7 @@

   # Internal
   openUrlInBrowser: (url) ->
-    Shell.openExternal url
+    shell.openExternal url

   # Internal
   blobUrl: ->


deployer working correctly

deployer working correctly

--- lib/deployers/gh-pages/index.coffee
+++ lib/deployers/gh-pages/index.coffee
@@ -25,6 +25,7 @@
     .then(build_tree)
     .then(create_commit)
     .then(update_gh_pages_branch)
+    .catch(console.error)
     .done(d.resolve, d.reject)

   return d.promise
@@ -88,7 +89,7 @@
 ###

 build_tree = ->
-  file_map(@root, { ignore_files: @config.ignore })
+  file_map(@root, { file_ignores: @config.ignore })
     .then (tree) => format_tree.call(@, path: '', children: tree)

 ###*
@@ -101,30 +102,20 @@
 ###

 format_tree = (root) ->
-  dirs = find_all_of_type(root.children, 'directory')
-  files = find_all_of_type(root.children, 'file')
+  dirs = _.where(root.children, { type: 'directory' })
+  files = _.where(root.children, { type: 'file' })

   if dirs.length
     W.map(dirs, format_tree.bind(@))
-      .then (res) => res.concat(W.map(files, create_blob.bind(@)))
+      .then (dir_objects) =>
+        W.map(files, create_blob.bind(@))
+        .then (file_objects) -> dir_objects.concat(file_objects)
       .then(create_tree.bind(@, root))
   else
     W.map(files, create_blob.bind(@))
       .then(create_tree.bind(@, root))

 ###*
- * Filters an array of objects for objects that have the given type key.
- *
- * @param  {Array} arr - an array containing objects which have a type property
- * @param  {String} type - desired value of the 'type' key in the objects
- * @return {Array} array of results
-###
-
-find_all_of_type = (arr, type) ->
-  res = _.find(arr, { type: type }) or []
-  Array::concat(res)
-
-###*
  * Creates a blob through github's API, given a file.
  *
  * @param  {Object} file - file object via file-map
@@ -134,7 +125,7 @@
 create_blob = (file) ->
   nodefn.call(fs.readFile, file.full_path, 'utf8')
   .then(get_blob_sha.bind(@))
-  .then (sha) -> { path: file.path, mode: '100644', type: 'blob', sha: sha }
+  .then (sha) -> { path: path.basename(file.path), mode: '100644', type: 'blob', sha: sha }

 ###*
  * Creates a tree through github's API, given an array of contents.
@@ -146,7 +137,7 @@

 create_tree = (root, tree) ->
   get_tree_sha.call(@, tree)
-  .then (sha) -> { path: root.path, mode: '040000', type: 'tree', sha: sha }
+  .then (sha) -> { path: path.basename(root.path), mode: '040000', type: 'tree', sha: sha }

 ###*
  * Given a file's content, creates a blob through github and returns the sha.


loading from HTML5 local storage works once more


:bug: Handle an error case while rendering

--- lib/linter-view.coffee
+++ lib/linter-view.coffee
@@ -4,6 +4,7 @@

   render: ->
     return unless @Linter.ActiveEditor # When we don't have any editor
+    return unless @Linter.ActiveEditor.getPath() # When we have an invalid text editor
     @Messages = @renderMessages(@Linter.MessagesProject.values()).concat(@renderMessages(@Linter.getActiveEditorLinter().Messages.values()))
     if not @Messages.length
       @Linter.PanelModal.hide() if @Linter.PanelModal.isVisible()


fix upgrade test

fix upgrade test

--- test/integration/upgrade.coffee
+++ test/integration/upgrade.coffee
@@ -18,21 +18,15 @@
         should.exist span
         done()

-    it 'it shows I can upgrade to the medium plan', (done) ->
-      wd40.elementByCss '.account-medium .cta', (err, span) ->
-        span.text (err, text) ->
-          text.should.include 'Upgrade'
-          done()
-
     it 'it shows I can upgrade to the large plan', (done) ->
       wd40.elementByCss '.account-large .cta', (err, span) ->
         span.text (err, text) ->
           text.should.include 'Upgrade'
           done()

-    context 'when I click on the medium upgrade button', ->
+    context 'when I click on the large upgrade button', ->
       before (done) ->
-        wd40.click '.account-medium a', done
+        wd40.click '.account-large a', done

       it 'it lets me subscribe without creating a new account', (done) ->
         wd40.elementByCss '#recurly-subscribe', (err, div) ->
@@ -53,11 +47,6 @@
   context 'when I go to the pricing page', ->
     before (done) ->
       browser.get "#{base_url}/pricing/", done
-
-    it 'it shows I am on the medium plan', (done) ->
-      wd40.elementByCss '.account-medium .currentPlan', (err, span) ->
-        should.exist span
-        done()

     it 'it shows I can upgrade to the large plan', (done) ->
       wd40.elementByCss '.account-large .cta', (err, span) ->
@@ -89,28 +78,3 @@
           wd40.elementByCss '.account-large .currentPlan', (err, span) ->
             should.exist span
             done()
-
-    context 'when I downgrade again', ->
-      before (done) ->
-        wd40.click '.account-medium a', done
-
-      it 'it opens a modal window checking I\'m sure', (done) ->
-        wd40.elementByCss '.modal', (err, modal) ->
-          should.exist modal
-          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
-
-        it 'it closes the modal window', (done) ->
-          wd40.waitForInvisibleByCss '.modal', done
-
-        it 'it shows I am on the medium plan', (done) ->
-          wd40.elementByCss '.account-medium .currentPlan', (err, span) ->
-            should.exist span
-            done()


Implemented PasswordEditor

Implemented PasswordEditor

--- lib/subviews/password-editor.coffee
+++ lib/subviews/password-editor.coffee
@@ -5,3 +5,31 @@
   constructor: (editorOrParams, props) ->
     editorOrParams.mini = true
     super editorOrParams, props
+
+    # TODO: support pasting (now it will show plain text)
+    # TODO: work with multiple selections
+
+    @hiddenInput.on 'keypress', (e) =>
+      editor = @getEditor()
+      selection = editor.getSelectedBufferRange()
+      cursor = editor.getCursorBufferPosition()
+      if !selection.isEmpty()
+        @originalText = _s.splice(@originalText, selection.start.column, selection.end.column - selection.start.column, String.fromCharCode(e.which))
+      else
+        @originalText = _s.splice(@originalText, cursor.column, 0, String.fromCharCode(e.which))
+      @insertText '*'
+      false
+
+    @hiddenInput.on 'keydown', (e) =>
+      if e.which == 8
+        editor = @getEditor()
+        selection = editor.getSelectedBufferRange()
+        cursor = editor.getCursorBufferPosition()
+        if !selection.isEmpty()
+          @originalText = _s.splice(@originalText, selection.start.column, selection.end.column - selection.start.column)
+        else
+          @originalText = _s.splice(@originalText, cursor.column - 1, 1)
+        @backspace
+      else if e.which == 13
+        @trigger 'core:confirm'
+      true


Update tests

Update tests

--- test/tests/BaseClassTest.coffee
+++ test/tests/BaseClassTest.coffee
@@ -203,5 +203,5 @@
 			testPropA: 'a'
 			testPropB: true

-		instance.props.testPropA.should.equal "a"
-		instance.props.testPropB.should.equal "value"
+		instance.testPropA.should.equal "a"
+		instance.testPropB.should.equal "value"


Make sure to return after the close

Make sure to return after the close

--- lib/script-view.coffee
+++ lib/script-view.coffee
@@ -23,11 +23,12 @@
   start: ->
     # Get current editor
     editor = atom.workspace.getActiveEditor()
-    if not editor? then @close()
+    if not editor?
+      @close()
+      return

     @resetView()
     @setup(editor)
-

   resetView: ->
     # Display window and load message
@@ -83,9 +84,6 @@

     filename = editor.getTitle()

-    # Set up header
-    @heading.text(lang + " - " + filename)
-
     # Get selected text
     selectedText = editor.getSelectedText()
     filepath = editor.getPath()
@@ -117,7 +115,9 @@
             "</a> or send your own Pull Request"
       @handleError(err)
       return false
-
+
+    # Set up header
+    @heading.text(lang + " - " + filename)
     @run(command, args)

   handleError: (err) ->


Fix triggering of change event after file resurrection

--- src/app/file.coffee
+++ src/app/file.coffee
@@ -46,7 +46,6 @@

   handleNativeChangeEvent: (eventType, path) ->
     if eventType is "remove"
-      @cachedContents = null
       @detectResurrectionAfterDelay()
     else if eventType is "move"
       @setPath(path)
@@ -65,6 +64,7 @@
       @subscribeToNativeChangeEvents()
       @handleNativeChangeEvent("contents-change", @getPath())
     else
+      @cachedContents = null
       @unsubscribeFromNativeChangeEvents()
       @trigger "remove"


FinderController: Mount home directory instead of root.

--- client/finder/filetree/controllers/findercontroller.coffee
+++ client/finder/filetree/controllers/findercontroller.coffee
@@ -98,7 +98,7 @@

     { uid } = machine
     mRoots  = (@appStorage.getValue 'machineRoots') or {}
-    path    = mRoots[uid] or "/"
+    path    = mRoots[uid] or "/home/#{KD.nick()}"

     if @getMachineNode uid
       return warn "Machine #{machine.getName()} is already mounted!"


app.coffee

app.coffee

--- demo/public/js/app.coffee
+++ demo/public/js/app.coffee
@@ -38,7 +38,6 @@
     , 400

   $('#qrcode_home').scan ->
-    console.log 'home'
     setTimeout ->
       $('body, html').animate(scrollTop: $('#div_home').height())
     , 2400
@@ -64,7 +63,7 @@
   # ### MARKDOWN2BUTTONS
   template_buttons = $('#template_div_buttons').html()
   do sync_buttons = ->
-    console.log Mustache.render template_buttons, markdown_2_json $('#textarea_markdown').val()
+    # console.log Mustache.render template_buttons, markdown_2_json $('#textarea_markdown').val()
     json = markdown_2_json $('#textarea_markdown').val()
     $('#div_buttons').html(Mustache.render template_buttons, json)
     $('#div_edit_button_demo_code code').html(JSON.stringify json, null, '  ')


Remove unused requires

Remove unused requires

--- lib/theme-panel.coffee
+++ lib/theme-panel.coffee
@@ -1,11 +1,11 @@
 _ = require 'underscore-plus'
-{$, $$, View} = require 'atom'
+{$$, View} = require 'atom'

 module.exports =
 class ThemeConfigPanel extends View
   @content: ->
     @div class: 'section themes', =>
-      @div class: 'section-heading theme-heading icon icon-device-desktop', 'Pick a Theme'
+      @div class: 'section-heading theme-heading icon icon-device-desktop', 'Choose a Theme'
       @div class: 'text padded', """
         Atom supports two types of themes, UI and syntax. UI themes style
         elements such as the tabs, status bar, and tree view.  Syntax themes


Minor cleanup

Minor cleanup

--- tests-src/TMXServer_test.coffee
+++ tests-src/TMXServer_test.coffee
@@ -3,12 +3,11 @@
 net = require '../lib/util/net-tools'
 http = require 'http'
 {PNG} = require 'pngjs'
-{assert, assertions, refute} = buster
+{assert, refute} = buster

 TMXServer = require '../lib/TMXServer'
-#assertions.throwOnFailure = false

-describe "=>TMX Server", ->
+describe "TMX Server", ->
   tmxServer = null

   beforeAll (done) ->
@@ -29,14 +28,13 @@
       assert.equals res.headers['access-control-allow-origin'],
         'http://'+req.connection.localAddress+':'+req.connection.localPort
       res.pipe new PNG filterType: 4
-        .on 'parsed', ->
+        .on 'parsed', done ->
           assert.isNumber @width
           assert.isNumber @height
           assert @readable
-          done()

     req.on 'error', done (e) ->
-      assertions.fail()
+      refute true

     req.end()


stream combiner: clarify comment

stream combiner: clarify comment

--- lib/understream.coffee
+++ lib/understream.coffee
@@ -19,7 +19,8 @@
   new F()

 # Based on: http://stackoverflow.com/questions/17471659/creating-a-node-js-stream-from-two-piped-streams
-# The version there was broken and needed some changes
+# The version there was broken and needed some changes, we just kept the concept of using the 'pipe'
+# event and overriding the pipe method
 class StreamCombiner extends PassThrough
   constructor: (@streams...) ->
     super objectMode: true


Fixed a few more instructions.

Fixed a few more instructions.

--- chip-8.coffee
+++ chip-8.coffee
@@ -71,6 +71,11 @@
 #     |---|---|---|---|
 #     | A | 0 | B | F |
 #     |---|---|---|---|
+
+# TODO: Figure out how to implement keyboard.
+getKeyPresses = () -> [3,15]
+
+waitForKeyPress = () -> 5

 # ## Display

@@ -422,7 +427,7 @@
 # Checks the keyboard, and if the key corresponding to the value of Vx is
 # currently in the down position, PC is increased by 2.
 inst_Ex9E_SKP = (x) ->
-  # todo
+  if registers[x] in getKeyPresses() then PC += 2

 # #### ExA1 - SKNP Vx

@@ -432,7 +437,7 @@
 # currently in the up position, PC is increased by 2.
 #/
 inst_ExA1_SKNP = (x) ->
-  # todo
+  if registers[x] not in getKeyPresses() then PC += 2

 # #### Fx07 - LD Vx, DT

@@ -449,7 +454,7 @@
 # All execution stops until a key is pressed, then the value of that key is
 # stored in Vx.
 inst_Fx0A_LD = (x, key) ->
-  # todo
+  key = waitForKeyPress()
   registers[x] = key

 # #### Fx15 - LD DT, Vx
@@ -484,7 +489,7 @@
 # corresponding to the value of Vx. See section 2.4, Display, for more
 # information on the Chip-8 hexadecimal font.
 inst_Fx29_LD = (x) ->
-  I = x * 5
+  I = x * 5 # as each sprite is 5 bytes and stored at address 0

 # #### Fx33 - LD B, Vx


Muda paleta de cores do gráfico

Muda paleta de cores do gráfico

--- app/assets/javascripts/graph.js.coffee
+++ app/assets/javascripts/graph.js.coffee
@@ -18,7 +18,7 @@
          data = processData(autorizado, pago)
          chart = nv.models.multiChart()
                .x((d,i) -> parseInt(d.x))
-               .color(d3.scale.category10().range())
+               .color(d3.scale.category20c().range())

          chart.xAxis.showMaxMin(true).tickFormat((d) ->
            index = d - 1


Select just the first matched element

Select just the first matched element

This a generic selector. It was also affecting spinner icon of the tabs

--- app/assets/javascripts/pager.js.coffee
+++ app/assets/javascripts/pager.js.coffee
@@ -1,6 +1,7 @@
 @Pager =
   init: (@limit = 0, preload, @disable = false) ->
-    @loading = $(".loading")
+    @loading = $('.loading').first()
+
     if preload
       @offset = 0
       @getOld()


refactor

refactor

--- client/app/Applications/StartTab.kdapplication/AppView.coffee
+++ client/app/Applications/StartTab.kdapplication/AppView.coffee
@@ -304,10 +304,10 @@
       endTitle    : "Your session end, logging out."
       content     : "You can use Koding for 20 minutes without registering. Register now."
       callback    : =>
+        return  unless KD.isGuest()
         {defaultVmName} = KD.getSingleton "vmController"
-        if KD.isGuest()
-          KD.remote.api.JVM.removeByHostname defaultVmName, (err)->
-            KD.getSingleton("finderController").unmountVm defaultVmName
-            KD.getSingleton("vmController").emit 'VMListChanged'
-            $.cookie "clientId", erase: yes
-            $.cookie "guestForFirstTime", erase: yes+        KD.remote.api.JVM.removeByHostname defaultVmName, (err)->
+          KD.getSingleton("finderController").unmountVm defaultVmName
+          KD.getSingleton("vmController").emit 'VMListChanged'
+          $.cookie "clientId", erase: yes
+          $.cookie "guestForFirstTime", erase: yes


Better commenting

Better commenting

--- app/assets/javascripts/votelist.coffee
+++ app/assets/javascripts/votelist.coffee
@@ -1,7 +1,9 @@
-VoteList = Em.Application.create()
+# Ember.js Application
+VoteList = Ember.Application.create()

-VoteList.Candidate = Em.Object.extend
+# Model object indicating one potential vote recipient
+VoteList.Candidate = Ember.Object.extend
   name: 'somebody'
   score: 0

@@ -12,9 +14,10 @@
   # Decrement the score by one
   downvote: ->
     this.set 'score', this.get('score') - 1
-
-
-VoteList.CandidateController = Em.ArrayController.create
+
+
+# Controller for candidate models
+VoteList.CandidateController = Ember.ArrayController.create
   content: []

   # Computed property to keep content sorted
@@ -22,7 +25,7 @@
     content = this.get('content')

     if content
-      result = Em.copy content
+      result = Ember.copy content

       result.sort ( a, b ) ->
         b.get('score') - a.get('score')
@@ -36,7 +39,8 @@
     this.pushObject(cand)

-VoteList.CreateCandidateView = Em.TextField.extend
+# View for creating a new candidate
+VoteList.CreateCandidateView = Ember.TextField.extend
   # Automate having new items inserted into the
   # list of candidates when enter is pressed
   insertNewline: ->
@@ -47,7 +51,8 @@
       this.set 'value', ''

-VoteList.CandidateView = Em.View.extend
+# View for displaying an individual candidate
+VoteList.CandidateView = Ember.View.extend
   person: null

   # View helper to pass calls down to the model


Dropbox: Minor MR fixes.

Dropbox: Minor MR fixes.

--- client/landing/app/CommonViews/dropboxdownloaditemview.coffee
+++ client/landing/app/CommonViews/dropboxdownloaditemview.coffee
@@ -14,19 +14,16 @@
         src      : fileData.thumbnails["64x64"] or fileData.icon

     @fileName    = new KDCustomHTMLView
-      tagName    : "div"
       cssClass   : "file-name"
       partial    : fileData.name

     @fileSize    = new KDCustomHTMLView
-      tagName    : "div"
       cssClass   : "file-size"
       partial    : KD.utils.formatBytesToHumanReadable fileData.bytes

     @loader      = new KDLoaderView
       size       :
         width    : 24
-        height   : 24

     @success     = new KDCustomHTMLView
       cssClass   : "done"


Catch server errors in edit task form

Catch server errors in edit task form

--- app/coffee/modules/tasks/detail.coffee
+++ app/coffee/modules/tasks/detail.coffee
@@ -138,14 +138,25 @@
             if not form.validate()
                 return

-            $tgrepo.save($scope.task).then ->
+            onSuccess = ->
                 $confirm.notify("success")
                 $location.path("/project/#{$scope.project.slug}/tasks/#{$scope.task.ref}")

+            onError = ->
+                $confirm.notify("error")
+
+            $tgrepo.save($scope.task).then(onSuccess, onError)
+
         $el.on "click", ".add-comment a.button-green", (event) ->
             event.preventDefault()
-            $tgrepo.save($scope.task).then ->
+
+            onSuccess = ->
                 $ctrl.loadHistory()
+
+            onError = ->
+                $confirm.notify("error")
+
+            $tgrepo.save($scope.task).then(onSuccess, onError)

         $el.on "focus", ".add-comment textarea", (event) ->
             $(this).addClass('active')


Add grading spec tests.

Add grading spec tests.

--- cms/static/coffee/spec/models/settings_grading_spec.coffee
+++ cms/static/coffee/spec/models/settings_grading_spec.coffee
@@ -1,4 +1,4 @@
-define ["js/models/settings/course_grading_policy"], (CourseGradingPolicy) ->
+define ["underscore", "js/models/settings/course_grading_policy"], (_, CourseGradingPolicy) ->
     describe "CourseGradingPolicy", ->
         beforeEach ->
             @model = new CourseGradingPolicy()
@@ -23,3 +23,14 @@
                 expect(@model.parseGracePeriod("asdf")).toBe(null)
                 expect(@model.parseGracePeriod("7:19")).toBe(null)
                 expect(@model.parseGracePeriod("1000:00")).toBe(null)
+
+        describe "validate", ->
+            it "enforces that the passing grade is <= the minimum grade to receive credit if credit is enabled", ->
+                @model.set({minimum_grade_credit: 0.8, grace_period: '01:00', is_credit_course: true})
+                @model.set('grade_cutoffs', [0.9], validate: true)
+                expect(_.keys(@model.validationError)).toContain('minimum_grade_credit')
+
+            it "does not enforce the passing grade limit in non-credit courses", ->
+                @model.set({minimum_grade_credit: 0.8, grace_period: '01:00', is_credit_course: false})
+                @model.set({grade_cutoffs: [0.9]}, validate: true)
+                expect(@model.validationError).toBe(null)


Handle in-app notifications

Handle in-app notifications

--- lib/integrated-learn-environment.coffee
+++ lib/integrated-learn-environment.coffee
@@ -70,6 +70,9 @@
       notif.onclick = ->
         notif.close()

+    ipc.on 'in-app-notification', (notifData) =>
+      atom.notifications['add' + notifData.type.charAt(0).toUpperCase() + notifData.type.slice(1)] notifData.message, {detail: notifData.detail, dismissable: notifData.dismissable}
+
     @fsViewEmitter.on 'toggleTerminal', (focus) =>
       @termView.toggle(focus)


Succinct graph pull (#47)

Succinct graph pull (#47)

* Adding option for more succinct mood graph

--- scripts/awesome.team.coffee
+++ scripts/awesome.team.coffee
@@ -34,6 +34,15 @@
   , null, true, 'America/New_York'

   robot.hear /what is the team's mood in the past (.*) days/i, (msg) ->
+    if robot.brain.data._private.team is undefined or robot.brain.data._private.team[msg.message.room] is undefined
+      msg.reply "You haven't picked a team yet!"
+    else
+      msg.http($baseurl+"/teams/"+robot.brain.data._private.team[msg.message.room]._id+"/moods/" +msg.match[1]+ "/trend/image")
+      .get() (err, res, body) ->
+        image = JSON.parse body
+        msg.reply image.imageUrl
+
+  robot.hear /(.*)-day mood/i, (msg) ->
     if robot.brain.data._private.team is undefined or robot.brain.data._private.team[msg.message.room] is undefined
       msg.reply "You haven't picked a team yet!"
     else


Store raw schema

Store raw schema

--- lib/schema.coffee
+++ lib/schema.coffee
@@ -8,7 +8,7 @@
 # @private
 module.exports = class Schema
   constructor: (@model_type) ->
-
+    @raw = _.result(@model_type, 'schema') or {}
     @fields ={}; @relations ={}; @ids_accessor = {}
     @_parse()

@@ -78,9 +78,7 @@
     return result

   _parse: ->
-    schema = _.result(@model_type, 'schema') or {}
-
-    for key, options of schema
+    for key, options of @raw
       options = @_parseFieldOptions(options)

       # a relationship (defered parse)


CUI.parseLocation: Recoginize "http://" again.

--- src/base/CUI.coffee
+++ src/base/CUI.coffee
@@ -725,7 +725,7 @@
 	@urlRegex: new RegExp(
 		"^" +
 		# protocol identifier
-		"(?:(?:(ftp|ftps|https)?)://|)" +
+		"(?:(?:(ftp|ftps|https|http))://|)" +
 		# user:pass authentication
 		"(?:(\\S+?)(?::(\\S*))?@)?" +
 		"((?:(?:" +


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/active_bounds.coffee
+++ src/active_bounds.coffee
@@ -1,7 +1,7 @@
 ###*
 The ActiveBounds module automatically destroys objects that
 are outside of the specified bounds. The default bounds are
-the dimensions of your game.
+the dimensions of your game. Useful for bull

     player = Core
       x: 10


OHM-125: Remove redundant code

OHM-125: Remove redundant code

--- src/api/heartbeat.coffee
+++ src/api/heartbeat.coffee
@@ -1,11 +1,7 @@
 utils = require "../utils"
-logger = require 'winston'
 server = require "../server"
 Q = require 'q'
-moment = require 'moment'

-#############
-#############

 exports.getHeartbeat = (next) ->
   try


Test if documents are correct instances.

--- tests.coffee
+++ tests.coffee
@@ -5083,3 +5083,248 @@
       test.isTrue intercepted.message.lastIndexOf('PeerDB exception: Error: Test exception', 0) is 0, intercepted.message
       test.equal intercepted.level, 'error'
 ]
+
+testAsyncMulti 'meteor-peerdb - instances', [
+  (test, expect) ->
+    testDefinition test
+
+    Person.documents.insert
+      username: 'person1'
+      displayName: 'Person 1'
+    ,
+      expect (error, person1Id) =>
+        test.isFalse error, error?.toString?() or error
+        test.isTrue person1Id
+        @person1Id = person1Id
+
+    Person.documents.insert
+      username: 'person2'
+      displayName: 'Person 2'
+    ,
+      expect (error, person2Id) =>
+        test.isFalse error, error?.toString?() or error
+        test.isTrue person2Id
+        @person2Id = person2Id
+
+    Person.documents.insert
+      username: 'person3'
+      displayName: 'Person 3'
+    ,
+      expect (error, person3Id) =>
+        test.isFalse error, error?.toString?() or error
+        test.isTrue person3Id
+        @person3Id = person3Id
+
+    Meteor.setTimeout expect(), WAIT_TIME
+,
+  (test, expect) ->
+    @person1 = Person.documents.findOne @person1Id
+    @person2 = Person.documents.findOne @person2Id
+    @person3 = Person.documents.findOne @person3Id
+
+    test.instanceOf @person1, Person
+    test.equal @person1.username, 'person1'
+    test.equal @person1.displayName, 'Person 1'
+    test.instanceOf @person2, Person
+    test.equal @person2.username, 'person2'
+    test.equal @person2.displayName, 'Person 2'
+    test.instanceOf @person3, Person
+    test.equal @person3.username, 'person3'
+    test.equal @person3.displayName, 'Person 3'
+
+    Post.documents.insert
+      author:
+        _id: @person1._id
+      subscribers: [
+        _id: @person2._id
+      ,
+        _id: @person3._id
+      ]
+      reviewers: [
+        _id: @person2._id
+      ,
+        _id: @person3._id
+      ]
+      subdocument:
+        person:
+          _id: @person2._id
+        persons: [
+          _id: @person2._id
+        ,
+          _id: @person3._id
+        ]
+        body: 'SubdocumentFooBar'
+      nested: [
+        required:
+          _id: @person2._id
+        optional:
+          _id: @person3._id
+        body: 'NestedFooBar'
+      ]
+      body: 'FooBar'
+    ,
+      expect (error, postId) =>
+        test.isFalse error, error?.toString?() or error
+        test.isTrue postId
+        @postId = postId
+
+    # Sleep so that observers have time to update documents
+    Meteor.setTimeout expect(), WAIT_TIME
+,
+  (test, expect) ->
+    @post = Post.documents.findOne @postId
+
+    test.instanceOf @post, Post
+
+    plainObject = _.object _.pairs @post
+
+    test.equal plainObject,
+      _id: @postId
+      _schema: '1.0.0'
+      author:
+        _id: @person1._id
+        username: @person1.username
+      # subscribers have only ids
+      subscribers: [
+        _id: @person2._id
+      ,
+        _id: @person3._id
+      ]
+      # But reviewers have usernames as well
+      reviewers: [
+        _id: @person2._id
+        username: @person2.username
+      ,
+        _id: @person3._id
+        username: @person3.username
+      ]
+      subdocument:
+        person:
+          _id: @person2._id
+          username: @person2.username
+        persons: [
+          _id: @person2._id
+          username: @person2.username
+        ,
+          _id: @person3._id
+          username: @person3.username
+        ]
+        slug: 'subdocument-prefix-foobar-subdocumentfoobar-suffix'
+        body: 'SubdocumentFooBar'
+      nested: [
+        required:
+          _id: @person2._id
+          username: @person2.username
+        optional:
+          _id: @person3._id
+          username: @person3.username
+        slug: 'nested-prefix-foobar-nestedfoobar-suffix'
+        body: 'NestedFooBar'
+      ]
+      body: 'FooBar'
+      slug: 'prefix-foobar-subdocumentfoobar-suffix'
+      tags: [
+        'tag-0-prefix-foobar-subdocumentfoobar-suffix'
+        'tag-1-prefix-foobar-nestedfoobar-suffix'
+      ]
+
+    SpecialPost.documents.insert
+      author:
+        _id: @person1._id
+      subscribers: [
+        _id: @person2._id
+      ,
+        _id: @person3._id
+      ]
+      reviewers: [
+        _id: @person2._id
+      ,
+        _id: @person3._id
+      ]
+      subdocument:
+        person:
+          _id: @person2._id
+        persons: [
+          _id: @person2._id
+        ,
+          _id: @person3._id
+        ]
+        body: 'SubdocumentFooBar'
+      nested: [
+        required:
+          _id: @person2._id
+        optional:
+          _id: @person3._id
+        body: 'NestedFooBar'
+      ]
+      body: 'FooBar'
+      special:
+        _id: @person1._id
+    ,
+      expect (error, postId) =>
+        test.isFalse error, error?.toString?() or error
+        test.isTrue postId
+        @postId = postId
+
+    # Sleep so that observers have time to update documents
+    Meteor.setTimeout expect(), WAIT_TIME
+,
+  (test, expect) ->
+    @post = SpecialPost.documents.findOne @postId
+
+    test.instanceOf @post, SpecialPost
+
+    plainObject = _.object _.pairs @post
+
+    test.equal plainObject,
+      _id: @postId
+      _schema: '1.0.0'
+      author:
+        _id: @person1._id
+        username: @person1.username
+      # subscribers have only ids
+      subscribers: [
+        _id: @person2._id
+      ,
+        _id: @person3._id
+      ]
+      # But reviewers have usernames as well
+      reviewers: [
+        _id: @person2._id
+        username: @person2.username
+      ,
+        _id: @person3._id
+        username: @person3.username
+      ]
+      subdocument:
+        person:
+          _id: @person2._id
+          username: @person2.username
+        persons: [
+          _id: @person2._id
+          username: @person2.username
+        ,
+          _id: @person3._id
+          username: @person3.username
+        ]
+        slug: 'subdocument-prefix-foobar-subdocumentfoobar-suffix'
+        body: 'SubdocumentFooBar'
+      nested: [
+        required:
+          _id: @person2._id
+          username: @person2.username
+        optional:
+          _id: @person3._id
+          username: @person3.username
+        slug: 'nested-prefix-foobar-nestedfoobar-suffix'
+        body: 'NestedFooBar'
+      ]
+      body: 'FooBar'
+      slug: 'prefix-foobar-subdocumentfoobar-suffix'
+      tags: [
+        'tag-0-prefix-foobar-subdocumentfoobar-suffix'
+        'tag-1-prefix-foobar-nestedfoobar-suffix'
+      ]
+      special:
+        _id: @person1._id
+]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/bindable.coffee
+++ src/bindable.coffee
@@ -60,7 +60,7 @@

       #  removes the handler coolEventHandler from the event
       # "someCustomEvent" while leaving the other events intact.
-      yourObject.unbind "someCustomEvent", coolEventHandler
+      yourObject.off "someCustomEvent", coolEventHandler

       # removes all handlers attached to "anotherCustomEvent"
       yourObject.unbind "anotherCustomEvent"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/bindable.coffee
+++ src/bindable.coffee
@@ -27,7 +27,7 @@

       # this will call coolEventHandler after
       # yourObject.trigger "someCustomEvent" is called.
-      yourObject.bind "someCustomEvent", coolEventHandler
+      yourObject.on "someCustomEvent", coolEventHandler

       #or
       yourObject.on "anotherCustomEvent", ->


Consolidate price buckets & update label copy


Added more route matching tests.

Added more route matching tests.

This time covering the extraction of the hash and query parameters.

--- tests/match-routes-tests.coffee
+++ tests/match-routes-tests.coffee
@@ -26,6 +26,8 @@
 		]

 	beforeEach () -> router.current null
+
+	# Route Matching

 	it 'matches the default route', () ->
 		router.go '/'
@@ -62,3 +64,16 @@
 	it 'sets current to null if unable to match a route', () ->
 		router.go '/unknown'
 		expect(router.current()).toBe(null)
+
+	# Hash extraction
+
+	it 'extracts the hash into a property of the current route', () -> expect(true).toBe(false)
+	it 'sets the hash property to be null when there is no hash in the URL', () -> expect(true).toBe(false)
+
+	# Query string extraction
+
+	it 'extracts a query parameter into the query object of the current route', () -> expect(true).toBe(false)
+	it 'extracts a query parameter into the query object of the current route when there is also a hash', () -> expect(true).toBe(false)
+	it 'extracts multiple query parameters into the query object of the current route', () -> expect(true).toBe(false)
+	it 'extracts multiple query parameters of the same name into an array on the query object of the current route', () -> expect(true).toBe(false)
+	it 'extracts multiple query parameters with a mix of distinct and multiple of the same name into the query object of the current route', () -> expect(true).toBe(false)


Remove test data from bootstrap.

Remove test data from bootstrap.

--- src/bootstrap-elm.coffee
+++ src/bootstrap-elm.coffee
@@ -364,27 +364,6 @@

   notes = new DreamNotes sync

-  [{
-    title: 'One-liners from The Matrix',
-    body: 'If music be the food of love, play on'
-  }, {
-    title: 'Note to self',
-    body: 'useful note'
-  }, {
-    title: 'Another note',
-    body: 'also useful'
-  }, {
-    title: 'A note again',
-    body: 'pretty cool'
-  }, {
-    title: 'How come Neo gets to be The One?',
-    body: 'pretty cool'
-  }, {
-    title: 'Thoughts on green code rain',
-    body: 'One two three!'
-  }].forEach ({title, body}) ->
-    notes.save {title}, body
-
   # Initialize the app based on the stored currentDocId
   sync.getCurrentDocId().done (id) ->
     if id?


Add event follow command at peatix.coffee


debuggin

debuggin


Add an option to deny the same user to modify their own karma.

--- src/scripts/karma.coffee
+++ src/scripts/karma.coffee
@@ -5,7 +5,7 @@
 #   None
 #
 # Configuration:
-#   None
+#   KARMA_ALLOW_SELF
 #
 # Commands:
 #   <thing>++ - give thing some karma
@@ -55,6 +55,13 @@
   decrementResponse: ->
      @decrement_responses[Math.floor(Math.random() * @decrement_responses.length)]

+  selfDeniedResponses: (name) ->
+    @self_denied_responses = [
+      "Hey everyone! #{name} is a narcissist!",
+      "I might just allow that next time, but no.",
+      "I can't do that #{name}."
+    ]
+
   get: (thing) ->
     k = if @cache[thing] then @cache[thing] else 0
     return k
@@ -75,20 +82,31 @@

 module.exports = (robot) ->
   karma = new Karma robot
+  allow_self = process.env.KARMA_ALLOW_SELF or "true"
+
   robot.hear /(\S+[^+:\s])[: ]*\+\+(\s|$)/, (msg) ->
     subject = msg.match[1].toLowerCase()
-    karma.increment subject
-    msg.send "#{subject} #{karma.incrementResponse()} (Karma: #{karma.get(subject)})"
+    if allow_self is true or msg.message.user.name.toLowerCase() != subject
+      karma.increment subject
+      msg.send "#{subject} #{karma.incrementResponse()} (Karma: #{karma.get(subject)})"
+    else
+      msg.send msg.random karma.selfDeniedResponses(msg.message.user.name)

   robot.hear /(\S+[^-:\s])[: ]*--(\s|$)/, (msg) ->
     subject = msg.match[1].toLowerCase()
-    karma.decrement subject
-    msg.send "#{subject} #{karma.decrementResponse()} (Karma: #{karma.get(subject)})"
+    if allow_self is true or msg.message.user.name.toLowerCase() != subject
+      karma.decrement subject
+      msg.send "#{subject} #{karma.decrementResponse()} (Karma: #{karma.get(subject)})"
+    else
+      msg.send msg.random karma.selfDeniedResponses(msg.message.user.name)

   robot.respond /karma empty ?(\S+[^-\s])$/i, (msg) ->
     subject = msg.match[1].toLowerCase()
-    karma.kill subject
-    msg.send "#{subject} has had its karma scattered to the winds."
+    if allow_self is true or msg.message.user.name.toLowerCase() != subject
+      karma.kill subject
+      msg.send "#{subject} has had its karma scattered to the winds."
+    else
+      msg.send msg.random karma.selfDeniedResponses(msg.message.user.name)

   robot.respond /karma( best)?$/i, (msg) ->
     verbiage = ["The Best"]


Fix typo which prevented log messages from showing up in log divs.

--- src/code/log.coffee
+++ src/code/log.coffee
@@ -10,7 +10,7 @@
   writeCustomLogDom: (message,severity="warn") ->
     # Possibly update a Dom element with message
     $log = $(@logDiv)
-    if $log and $log.size > 0
+    if $log and $log.length > 0
       $msg = $ "#{message}
"
       $log.append($msg)
       $log[0].scrollTop = $log[0].scrollHeight


Modified tests for backwards.indexOf

Modified tests for backwards.indexOf


remove SIP prefix from callers

remove SIP prefix from callers


Remove outdated comments, re-order functions in source file.

--- src/TinyMockJS.coffee
+++ src/TinyMockJS.coffee
@@ -1,22 +1,3 @@
-#
-# Expectation represents a mocked method that we expect to be called.
-# It stores the method name, the arguments that we expect to be called
-# with, a value to return or throw, and whether it has actually been
-# called.  For example, the following:
-#
-#   my_mock = new Mock()
-#   expectation = my_mock.expects("my_method").args(1,2,3).returns(42)
-#
-# would result in a Expectation with @method_name = "my_method",
-# @_args = [1,2,3], @_returns = 42, @_throws = undefined, and @called =
-# false.  Doing:
-#
-#   my_mock.my_method(1,2,3)
-#
-# would result in @called = true.  Note that a mocked method can
-# either return a value or throw an error but not both, so either
-# @returns or @throws (or both) must be undefined.
-#
 class Expectation

   @_all_expectations: []
@@ -158,99 +139,10 @@
     throw expectation._throws if expectation._throws?
     expectation._returns

-_build_errors = (expectation) ->
-  # TODO: use a mapping function?
-  errors = ""
-  for expectation in expectation._all_expectations when expectation.called == false
-    errors += "'#{expectation.method_name}(#{expectation._args})' was never called\n"
-  errors
-
-_throw_args_usage = ->
-  throw "you need to supply at least one argument to args(), e.g. my_mock.expects('my_method').args(42)"
-
-_throw_args_called_more_than_once = ->
-  throw new Error("you called args() more than once, e.g. my_mock.expects('my_method').args(1).args(2); call it just once")
-
-_throw_returns_usage = ->
-  throw "you need to supply an argument to returns(), e.g. my_mock.expects('my_method').returns(123)"
-
-_throw_returns_called_more_than_once = ->
-  throw new Error("you called returns() more than once, e.g. my_mock.expects('my_method').returns(1).returns(2); call it just once")
-
-_throw_throws_usage = ->
-  throw "you need to supply an argument to throws(), e.g. my_mock.expects('my_method').throws('an error')"
-
-_throw_throws_called_more_than_once = ->
-  throw new Error("you called throws() more than once, e.g. my_mock.expects('my_method').throws('something').throws('something else'); call it just once")
-
-_throw_returns_and_throws_both_called = ->
-  throw new Error("you called returns() and throws() on the same expectation; use one or the other but not both")
-
-
-
-#
-# Mock represents the mock of some object. @expectations is a list of
-# mocked methods, which are added with the expects(), args(),
-# returns(), and throws() functions.  These must be called in a
-# specific order, expects then args then returns or throws, so:
-#
-#   my_mock.expects("my_method").args(123).returns(456)
-#
-# is legal, whereas:
-#
-#   my_mock.returns(456).args(123).expects("my_method")
-#
-# is not.  Note that args, returns and throws are optional.  Also
-# note that either returns or throws can be used, but not both on
-# the same expectation.
-#
-# @expectations is used as a stack only in so far as the method expectation
-# at the front of the list is the most recently defined expectation and is
-# the one to which args and returns would be applied.  For example:
-#
-#   my_mock = new Mock()    # @expectations = []
-#   my_mock.expects("m1")   # [ { "m1" } ]
-#   my_mock.args(1,2,3)     # [ { "m1", [1,2,3] } ]
-#   my_mock.returns(42)     # [ { "m1", [1,2,3], 42 } ]
-#   my_mock.expects("m2")   # [ { "m2" }, { "m1", [1,2,3], 42 } ]
-#   my_mock.args(4,5,6)     # [ { "m2", [4,5,6] }, { "m1", [1,2,3], 42 } ]
-#   my_mock.returns(43)     # [ { "m2", [4,5,6], 43 }, { "m1", [1,2,3], 42 } ]
-#
-# @expectations is just an array, not a hash.  It will not grow very large
-# so a linear search for a particular expectation is fine.
-#
-# @expectations is added to the mock object by the various mock methods when
-# needed (i.e. lazily); if we mock an existing class then we only need to
-# worry about adding the mock methods to that class, not monkeying with its
-# contructor to also add @expectations.
-#
-#class Mock
-
-#
-# my_mock = new Mock()
-# my_mock.expects("my_method")
-# my_mock.my_method()
-#
-expects = (method_name) ->
-  _check_expects_usage(method_name)
-  if @[ method_name ]? and typeof @[ method_name ] != 'function'
-    throw new Error("'#{method_name}' is an existing property; you can only mock functions")
-  if typeof @ == 'function' and not @.prototype[ method_name ]?
-    throw new Error("'#{method_name}' is not an existing method; you can only mock existing methods on classes")
-  _start_new_expectation(@, method_name)
-
-
-
-#
-# private
-#
-
-_is_reserved_word = (word) ->
-  word in [ "expects", "args", "returns", "check" ]
-
-_check_expects_usage = (method_name) ->
-  _throw_expects_usage() unless method_name?
-  _throw_reserved_word(method_name) if _is_reserved_word(method_name)
+_find_expectation = (object, method_name, args...) ->
+  for expectation in Expectation._all_expectations when expectation.matches(object, method_name, args...)
+    return expectation
+  undefined

 _check_for_duplicate_expectations = (mock) ->
   # TODO: use each with index and slice to avoid last element
@@ -261,25 +153,65 @@
       if expectations[outer].equals( expectations[inner] )
         _throw_duplicate_expectation("#{expectations[outer].method_name}(#{expectations[outer]._args})")

-_find_expectation = (object, method_name, args...) ->
-  for expectation in Expectation._all_expectations when expectation.matches(object, method_name, args...)
-    return expectation
-  undefined
+_build_errors = (expectation) ->
+  # TODO: use a mapping function?
+  errors = ""
+  for expectation in expectation._all_expectations when expectation.called == false
+    errors += "'#{expectation.method_name}(#{expectation._args})' was never called\n"
+  errors
+
+_throw_args_usage = ->
+  throw "you need to supply at least one argument to args(), e.g. my_mock.expects('my_method').args(42)"
+
+_throw_args_called_more_than_once = ->
+  throw new Error("you called args() more than once, e.g. my_mock.expects('my_method').args(1).args(2); call it just once")
+
+_throw_returns_usage = ->
+  throw "you need to supply an argument to returns(), e.g. my_mock.expects('my_method').returns(123)"
+
+_throw_returns_called_more_than_once = ->
+  throw new Error("you called returns() more than once, e.g. my_mock.expects('my_method').returns(1).returns(2); call it just once")
+
+_throw_throws_usage = ->
+  throw "you need to supply an argument to throws(), e.g. my_mock.expects('my_method').throws('an error')"
+
+_throw_throws_called_more_than_once = ->
+  throw new Error("you called throws() more than once, e.g. my_mock.expects('my_method').throws('something').throws('something else'); call it just once")
+
+_throw_returns_and_throws_both_called = ->
+  throw new Error("you called returns() and throws() on the same expectation; use one or the other but not both")
+
+_throw_duplicate_expectation = (expectation) ->
+  throw "#{expectation} is a duplicate expectation"
+
+_throw_unknown_expectation = (expectation) ->
+  throw "#{expectation} does not match any expectations"
+
+
+
+expects = (method_name) ->
+  _check_expects_usage(method_name)
+  if @[ method_name ]? and typeof @[ method_name ] != 'function'
+    throw new Error("'#{method_name}' is an existing property; you can only mock functions")
+  if typeof @ == 'function' and not @.prototype[ method_name ]?
+    throw new Error("'#{method_name}' is not an existing method; you can only mock existing methods on classes")
+  _start_new_expectation(@, method_name)
+
+_check_expects_usage = (method_name) ->
+  _throw_expects_usage() unless method_name?
+  _throw_reserved_word(method_name) if _is_reserved_word(method_name)
+
+_is_reserved_word = (word) ->
+  word in [ "expects", "args", "returns", "check" ]
+
+_throw_expects_usage = ->
+  throw "you need to supply a method name to expects(), e.g. my_mock.expects('my_method')"
+
+_throw_reserved_word = (reserved) ->
+  throw "you cannot do my_mock.expects('#{reserved}'); '#{reserved}' is a reserved method name"

 _start_new_expectation = (object, method_name) ->
   new Expectation(object, method_name)
-
-_throw_expects_usage = ->
-  throw "you need to supply a method name to expects(), e.g. my_mock.expects('my_method')"
-
-_throw_reserved_word = (reserved) ->
-  throw "you cannot do my_mock.expects('#{reserved}'); '#{reserved}' is a reserved method name"
-
-_throw_duplicate_expectation = (expectation) ->
-  throw "#{expectation} is a duplicate expectation"
-
-_throw_unknown_expectation = (expectation) ->
-  throw "#{expectation} does not match any expectations"


BW: Correct Toxic damage.

BW: Correct Toxic damage.

--- server/bw/attachment.coffee
+++ server/bw/attachment.coffee
@@ -1273,7 +1273,7 @@
   endTurn: ->
     @counter = Math.min(@counter + 1, 15)
     return  if @pokemon.hasAbility("Poison Heal")
-    if @pokemon.damage(Math.floor(@pokemon.stat('hp') * @counter / 16))
+    if @pokemon.damage(Math.max(Math.floor(@pokemon.stat('hp') / 16), 1) * @counter)
       @battle.message "#{@pokemon.name} was hurt by poison!"

 class @Status.Sleep extends @StatusAttachment


#225 Fix center Active node

#225 Fix center Active node

--- app/assets/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.coffee
+++ app/assets/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.coffee
@@ -59,7 +59,7 @@
       "b" : => @model.skeletonTracing.pushBranch()
       "j" : => @popBranch()

-      "s" : @centerActiveNode
+      "s" : @skeletonTracingController.centerActiveNode

       #Comments
       "n" : => @skeletonTracingController.setActiveNode(


cooldown finilazation

cooldown finilazation

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -1,5 +1,5 @@
 ###*
-The Cooldown module provide
+The Cooldown module provides a dec
 GameObjects based on the velocity and acceleration. It does not check
 collisions so is probably best suited to particle effect like things.


0.8a (Correction)

0.8a (Correction)

Silly me.

--- Eva.widget/index.coffee
+++ Eva.widget/index.coffee
@@ -1,4 +1,4 @@
-# Version: 0.71a
+# Version: 0.8a
 ## If you do not want a transparent widget, please adjust the opacity setting under STYLE
 ## If you do not know how to write HTML/CSS, it is best for you to learn it first before
 ## attempting to customise the UI. Or you can contact me.


del: Some uneeded helper functions. #24

del: Some uneeded helper functions. #24

--- src/js/core/utils.coffee
+++ src/js/core/utils.coffee
@@ -36,14 +36,6 @@
         fixed

     $.extend utils, {
-        isEmpty: (obj) ->
-            unless obj? then return true
-            if $.isArray(obj) or @isString(obj)
-                return obj.length is 0
-            for key in obj
-                if @has(obj, key) then return false
-            true
-
         isBoolean: (obj) ->
             obj is true or obj is false or toString.call(obj) is '[object Boolean]'

@@ -64,10 +56,6 @@
                 shuffled[i - 1] = shuffled[rand]
                 shuffled[rand] = item
             shuffled
-
-        clone: (obj) ->
-            unless $.isPlainObject(obj) then obj
-            if $.isArray(obj) then obj.slice() else $.extend({}, obj)

         time2str: (time) ->
             r = []


Regex tweak to avoid Fleep adapter bugs and strengthen self-abuse avoidance.

--- src/tucker.coffee
+++ src/tucker.coffee
@@ -83,7 +83,7 @@
     if person is "me"
       quote = res.random thirdPerson
       res.send "Malcolm was talking about you earlier, he said \"#{quote}\""
-    else if person.search(/malc(\s+|olm)|tucker|himself/i) > -1
+    else if person.search(/malc|malcolm|tucker|himself/i) > -1
       res.send "I'm watching you, you cheeky fucker."
     else
       quote = res.random secondPerson
@@ -99,7 +99,7 @@
         res.send "\"#{quote}\""

   unless process.env.HUBOT_LESS_MALCOLM
-    robot.hear /malc(\s+|olm)/i, (res) ->
+    robot.hear /malc(\s*|olm)/i, (res) ->
       message = res.message.text
       if message.search(/tucker/i) is -1 and message.search(/Malcolm was talking/i) is -1 and message.search(/heard Malcolm talking/i) is -1
         quote = res.random allQuotes


WIP: gearjson work version

WIP: gearjson work version

--- src/gearjson.coffee
+++ src/gearjson.coffee
@@ -1,46 +1,130 @@
-dateClass = '[object Date]'
+VERSION = 'developing'
+
+undefinedStr = 'undefined'
 objectStr = 'object'

-toString = Object.prototype.toString
+{toString} = Object::
 {isArray} = Array
-isDate = (value)->
-  value instanceof Date or toString.call(value) is dateClass or no

-isBlob = (value)->
-  value instanceof Buffer or value.constructor.name is 'Buffer'
+getType = (object)->
+  type = typeof object
+  return undefinedStr if type is undefinedStr
+  if object
+    type = object.constructor.name
+  else
+    type = toString.call(object).slice 8, -1
+  type.toLowerCase()
+
+
+defaultTypesMap =
+  buffer:
+    type: 'blob'
+    callback: (v)-> new Buffer v
+  date:
+    type: 'date'
+    callback: (v)-> new Date v
+
+
+getset = (obj, path, cb)->
+  stopped = no
+  last = path.length - 1
+  path.reduce ((obj, key, idx)->
+    return if stopped
+    if idx is last
+      obj[key] = cb obj[key]
+    else
+      return stopped = yes unless obj.hasOwnProperty(key)
+      obj[key]
+  ), obj
+

 each = (obj, cb)->
   if isArray obj
-    for v, k in obj
-      cb v, k
+    for value, key in obj
+      cb key, value
   else
-    for k in Object.keys obj
-      cb obj[k], k
+    for key in Object.keys obj
+      cb key, obj[key]
   return

-class Element
-  constructor: (@source, @types)->
-    @determineTypes() unless @types
-
-  determineTypes: ->
-    @types = {}
-    path = []
-    runner = (obj)=>
-      each obj, (v, k)=>
-        tn = typeof v
-        if tn is objectStr
-          if isDate v
-            @types[path.concat(k).join('.')] = 'date'
-          else if isBlob v
-            @types[path.concat(k).join('.')] = 'blob'
-          else
-            path.push k
-            runner v
-            path.pop()
-    runner @source
+typesO2A = (obj)-> [path.split('.'), type] for path, type of obj

-class Collection
-  constructor: ->

-module.exports = {Element, Collection}+class GearJson
+  constructor: (options = {})->
+    {@typesMap} = options
+    @typesMap = defaultTypesMap unless @typesMap
+
+  determineTypes: (item, pathesSkip)->
+    if isArray pathesSkip
+      pathesSkip = pathesSkip.reduce ((o, key)->
+        key = key.join('.') if isArray key
+        o[key] = yes
+        o
+      ), {}
+    path = []
+    types = []
+    addKey = (k, type)->
+      pathArr = path.concat(k)
+      unless pathesSkip and pathesSkip[pathArr.join('.')]
+        types.push [pathArr, type]
+    runner = (obj)=>
+      each obj, (key, value)=>
+        if objectStr is typeof value
+          valueType = getType value
+          if @typesMap[valueType]
+            addKey key, @typesMap[valueType].type
+          else
+            path.push key
+            runner value
+            path.pop()
+    runner item
+    types
+
+  wrapItem: (item, options = {})->
+    {types, pathesSkip} = options
+    types = @determineTypes(item, pathesSkip) unless types
+    result = {item}
+    if types.length
+      if options.hashTypes
+        result.types = types.reduce ((o, [pathArr, type])->
+          o[pathArr.join('.')] = type
+          o
+        ), {}
+      else
+        result.types = types
+    result
+
+  wrapCollection: (items, options = {})->
+    {types, extItems, hashTypes} = options
+    if not types or extItems
+      itemOpts = {hashTypes}
+      if types
+        itemOpts.pathesSkip = Object.keys types
+      items = items.map (item)=> @wrapItem item, itemOpts
+      extItems = yes
+    result = {items}
+    result.types = types if types
+    result.extItems = yes if extItems
+    result
+
+  unwrapItem: (item, types)->
+    types = typesO2A types unless isArray types
+    for [path, type] in types
+      getset item, path, @typesMap[type].callback
+    item
+
+  unwrapCollection: ({items, types, extItems})->
+    if types
+      if extItems
+        items.forEach (item)=> @unwrapItem item.item, types
+      else
+        items.forEach (item)=> @unwrapItem item, types
+    if extItems
+      items.forEach (item)=> @unwrapItem item.item, item.types
+      items = items.map (item)-> item.item
+    items
+
+
+module.exports = GearJson


Fix display page

Fix display page

--- src/coffee/flight.coffee
+++ src/coffee/flight.coffee
@@ -198,11 +198,8 @@
   #
   # Returns nothing.
   displayPage: (targetPage, currentPage) ->
-    #TODO: fix this as technique for moving the DOM has changed - see slide()
     targetPage.style.display = "-webkit-box"
-    currentPage.style.display = "-webkit-box"
-    targetPage.style.left = "0%"
-    currentPage.style.left = "100%"
+    currentPage.style.display = "none"

   # Private: Hide DOM that has just been transitioned
   #


[➠] Improved “scrolly”.

[➠] Improved “scrolly”.

--- vendor/assets/javascripts/jquery/plugins/scrollify/scrolly.js.coffee
+++ vendor/assets/javascripts/jquery/plugins/scrollify/scrolly.js.coffee
@@ -21,7 +21,9 @@
   # === Events ===

   onWindowScroll: (event) =>
-    scrollTop = do $(window).scrollTop
+    window = $ event.target
+
+    scrollTop = do window.scrollTop

     if scrollTop > 0
       minimizeRoof.call @


switch to midi note numbers

switch to midi note numbers

--- intervals.coffee
+++ intervals.coffee
@@ -40,18 +40,20 @@
 StringNumbers = [1..6]
 StringCount = StringNumbers.length

-FretNumbers = [0..4]  # includes nuNumbert
-FretCount = FretNumbers.length - 1  # doesn't include nuNumbert
-
+FretNumbers = [0..4]  # includes nut
+FretCount = FretNumbers.length - 1  # doesn't include nut
+
+# FIXME these are backwards from the string numbers
 StringIntervals = [5, 5, 5, 4, 5]
-StringNoteNumbers = (->
-  numbers = [0]
+
+OpenStringNoteNumbers = do (numbers=[]) ->
+  numbers.push 44
   for interval, i in StringIntervals
     numbers.push numbers[i] + interval
-  numbers.reverse())()
+  numbers.reverse()

 fingering_note_number = ({string, fret}) ->
-  StringNoteNumbers[string - 1] + fret
+  OpenStringNoteNumbers[string - 1] + fret

 finger_positions_each = (fn) ->
   for string in StringNumbers
@@ -358,7 +360,7 @@
         draw_fingerboard fingerings

 optimize_fingers = (fingerings) ->
-  bystring = ([] for _ in StringsNumber)
+  bystring = ([] for _ in OpenStringNoteNumbers)
   bynote = ([] for _ in [0..10])
   for fingering in fingerings
     bystring[fingering.string - 1].push fingering


Fixed isoparse local zone compensation

Fixed isoparse local zone compensation

--- src/datetime.coffee
+++ src/datetime.coffee
@@ -762,7 +762,7 @@
   dt.isoparse = (s) ->
     d = @strptime(s, dt.ISO_FORMAT)
     # Account for the timezone difference since original date is in UTC
-    d.setMinutes d.getMinutes() + d.getTimezoneOffset()
+    d.setMinutes d.getMinutes() - d.getTimezoneOffset()
     d

   dt


Ensure building is lowercase in URL.

Ensure building is lowercase in URL.

--- routes/room.coffee
+++ routes/room.coffee
@@ -64,7 +64,7 @@
       # Split into building and room.
       req.query.room = req.query.room.split(/\s/).join('')
       roomParts = req.query.room.match(/([A-Za-z]*)([0-9]*)/)
-      building = roomParts[1]
+      building = roomParts[1].toLowerCase()
       room = roomParts[2]

       if building.length is 1 or building.toUpperCase() is 'EV'


cleanup

cleanup

--- api/index.coffee
+++ api/index.coffee
@@ -19,9 +19,6 @@
 app.use morgan 'dev'

 # Apps
-app.use (req, res, next) ->
-  console.log req.headers
-  next()
 app.use '/__gravity', require('antigravity').server if NODE_ENV is 'test'
 app.use setUser
 app.post '/articles', authenticated


Added velocity limit

Added velocity limit

--- jarkle/client/webgl_synth.coffee
+++ jarkle/client/webgl_synth.coffee
@@ -30,7 +30,7 @@
     @webGLVis.stopAll()

   handleDrumMidiMessage: (noteInfo) =>
-    if noteInfo.note == RIDE_CYMBAL_1
+    if noteInfo.note == RIDE_CYMBAL_1 and noteInfo.vel >= 120
       @nextPlayer()

   nextPlayer: ->


Removed password from public data.

Removed password from public data.

--- api/teams.coffee
+++ api/teams.coffee
@@ -29,4 +29,5 @@

 formatTeam = (team) ->
     result = team
+    result.password = ''
     return result


Missing change.

Missing change.

--- server/publication.coffee
+++ server/publication.coffee
@@ -20,7 +20,7 @@
   process: (pdf, initCallback, textCallback, pageImageCallback, progressCallback) =>
     pdf ?= Storage.open @filename()
     initCallback ?= (numberOfPages) ->
-    textCallback ?= (pageNumber, x, y, width, height, direction, text) ->
+    textCallback ?= (pageNumber, left, top, width, height, direction, text) ->
     pageImageCallback ?= (pageNumber, canvasElement) ->
     progressCallback ?= ->


Just a test commit to test wakatime

Just a test commit to test wakatime

--- app/enums.coffee
+++ app/enums.coffee
@@ -62,7 +62,7 @@
     STORE: 1
     SCM: 2

-# Populate `CHOICES`
+# Populate `CHOICES`.
 for enumName, enumValues of ENUMS
   choices = []
   enumValues['UNKNOWN'] = -1


Use proxy_pass'd api

Use proxy_pass'd api

--- app/index.coffee
+++ app/index.coffee
@@ -47,14 +47,14 @@
     ExperimentServer.resetExperimentalFlags()
     Geordi.logEvent 'logout'

-Api.init
-  host: if location.origin is 'http://preview.zooniverse.org'
-    'https://dev.zooniverse.org'
-  else if +location.port < 1024
-    'https://api.zooniverse.org'
-  else
-    'https://dev.zooniverse.org'
-    #"#{location.protocol}//#{location.hostname}:3000"
+[host, proxyPath] = if location.origin is 'http://preview.zooniverse.org'
+  ['https://dev.zooniverse.org', '/proxy']
+else if +location.port < 1024
+  [window.location.origin, '/_ouroboros_api/proxy']
+else
+  ['https://dev.zooniverse.org', '/proxy']
+
+Api.init {host, proxyPath}

 # TODO: Don't count on the proxy frame to have no loaded yet.


Use proxy_pass'd api

Use proxy_pass'd api

--- app/index.coffee
+++ app/index.coffee
@@ -11,7 +11,10 @@
 User = require 'zooniverse/models/user'
 Recent = require 'zooniverse/models/recent'

-api = new Api project: 'spacewarp'
+api = if window.location.hostname is 'spacewarps.org'
+  new Api project: 'spacewarp', host: 'http://spacewarps.org', path: '/_ouroboros_api/proxy'
+else
+  new Api project: 'spacewarp'

 # Initialize Counter model
 new Counter({classified: 0, potentials: 0, favorites: 0}).save()


zIndexin

zIndexin

--- src/drawable.coffee
+++ src/drawable.coffee
@@ -21,6 +21,7 @@
   $.reverseMerge I,
     color: "#196"
     spriteName: null
+    zIndex: 0

   if I.spriteName
     I.sprite = Sprite(I.spriteName, (sprite) ->