Clean out some warnings

Clean out some warnings

--- app/classifier/tasks/survey.cjsx
+++ app/classifier/tasks/survey.cjsx
@@ -87,7 +87,7 @@
         else
           for choiceID, i in filteredChoices
             choice = @props.task.choices[choiceID]
-            <button key={choiceID + i} type="button" className="survey-task-chooser-choice" onClick={@props.onChoose.bind this, choiceID}>
+            <button key={choiceID + i} type="button" className="survey-task-chooser-choice" onClick={@props.onChoose.bind null, choiceID}>
               {unless choice.images.length is 0
                   }

{choice.label}

@@ -131,7 +131,7 @@

         {for index in [0...@props.images.length]
-
+
             <button type="button" className="survey-task-image-flipper-pip" disabled={index is @state.frame} onClick={@handleFrameChange.bind this, index}>{index + 1}</button>
             {' '}
           }
@@ -141,7 +141,7 @@
   renderPreload: ->

       {for image in @props.images
-        }
+        }

   handleFrameChange: (frame) ->
@@ -182,7 +182,7 @@
           Often confused with
           {' '}
           {for otherChoiceID in choice.confusionsOrder
-
+
               <DropdownForm label={

                   {@props.task.choices[otherChoiceID].label}
@@ -190,7 +190,7 @@
               }>
                 <Markdown content={choice.confusions[otherChoiceID]} />

-                  <button type="button" className="standard-button" onClick={@props.onSwitch.bind this, otherChoiceID}>I think it’s this</button>
+                  <button type="button" className="standard-button" onClick={@props.onSwitch.bind null, otherChoiceID}>I think it’s this</button>
                   {' '}
                   <button type="submit" className="major-button">Dismiss</button>


removed unused component

removed unused component

--- imports/ui/instance/steps.cjsx
+++ imports/ui/instance/steps.cjsx
@@ -1,30 +0,0 @@
-{ Meteor }          = require 'meteor/meteor'
-{ createContainer } = require 'meteor/react-meteor-data'
-React               = require 'react'
-
-renderStep = (step) ->
-  switch step.type
-    when 'pull' then <PullStep image={step.image} />
-    when 'service' then <ServiceStep name={step.name} />
-
-PullStep = React.createClass
-  displayName: 'PullStep'
-  render: ->
-    Image {@props.image} is pulled
-
-ServiceStep = React.createClass
-  displayName: 'ServiceStep'
-  render: ->
-    Service {@props.name} is running
-
-module.exports = React.createClass
-  displayName: 'Steps'
-  render: ->
-

-      {@props.items.map (step) ->
-

-
-          {renderStep step}
-

-      }
-


Disconnect only if already connected

Disconnect only if already connected

--- app/components/vnc-viewer.coffee
+++ app/components/vnc-viewer.coffee
@@ -64,7 +64,8 @@

     disconnect: ->
       rfb = @get "rfb"
-      rfb.disconnect()
+      if rfb._rfb_connection_state is 'connected'
+        rfb.disconnect()

     dynamicScan: ->
       file = @get "file"


Remove workspaceView references

Remove workspaceView references

--- spec/find-view-spec.coffee
+++ spec/find-view-spec.coffee
@@ -1,18 +1,24 @@
 _ = require 'underscore-plus'
 {$} = require 'atom-space-pen-views'
-{WorkspaceView} = require 'atom'

 path = require 'path'

 describe 'FindView', ->
-  [editorView, editor, findView, activationPromise, workspaceElement] = []
+  [workspaceElement, editorView, editor, findView, activationPromise] = []

   getFindAtomPanel = ->
     workspaceElement.querySelector('.find-and-replace').parentNode

+  getResultDecorations = (editor, clazz) ->
+    markerIdForDecorations = editor.decorationsForScreenRowRange(0, editor.getLineCount())
+    resultDecorations = []
+    for markerId, decorations of markerIdForDecorations
+      for decoration in decorations
+        resultDecorations.push decoration if decoration.getProperties().class is clazz
+    resultDecorations
+
   beforeEach ->
     spyOn(atom, 'beep')
-    atom.workspaceView = new WorkspaceView()
     workspaceElement = atom.views.getView(atom.workspace)
     atom.project.setPaths([path.join(__dirname, 'fixtures')])

@@ -20,9 +26,9 @@
       atom.workspace.open('sample.js')

     runs ->
-      atom.workspaceView.attachToDom()
-      editorView = atom.workspaceView.getActiveView()
-      editor = editorView.getModel()
+      jasmine.attachToDOM(workspaceElement)
+      editor = atom.workspace.getActiveEditor()
+      editorView = atom.views.getView(editor)

       activationPromise = atom.packages.activatePackage("find-and-replace").then ({mainModule}) ->
         mainModule.createViews()
@@ -30,7 +36,7 @@

   describe "when find-and-replace:show is triggered", ->
     it "attaches FindView to the root view", ->
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -40,7 +46,7 @@

     it "populates the findEditor with selection when there is a selection", ->
       editor.setSelectedBufferRange([[2, 8], [2, 13]])
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -52,25 +58,25 @@
         findView.findEditor.setText('')

         editor.setSelectedBufferRange([[2, 14], [2, 20]])
-        editorView.trigger 'find-and-replace:show'
+        atom.commands.dispatch editorView, 'find-and-replace:show'
         expect(getFindAtomPanel()).toBeVisible()
         expect(findView.findEditor.getText()).toBe('length')

     it "does not change the findEditor text when there is no selection", ->
       editor.setSelectedBufferRange([[2, 8], [2, 8]])
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise

       runs ->
         findView.findEditor.setText 'kitten'
-        editorView.trigger 'find-and-replace:show'
+        atom.commands.dispatch editorView, 'find-and-replace:show'
         expect(findView.findEditor.getText()).toBe('kitten')

     it "does not change the findEditor text when there is a multiline selection", ->
       editor.setSelectedBufferRange([[2, 8], [3, 12]])
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -84,7 +90,7 @@
       atom.config.set('find-and-replace.caseSensitive', true)
       atom.config.set('find-and-replace.inCurrentSelection', true)

-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -96,30 +102,30 @@

   describe "when find-and-replace:toggle is triggered", ->
     it "toggles the visibility of the FindView", ->
-      atom.workspaceView.trigger 'find-and-replace:toggle'
+      atom.commands.dispatch workspaceElement, 'find-and-replace:toggle'

       waitsForPromise ->
         activationPromise

       runs ->
         expect(getFindAtomPanel()).toBeVisible()
-        atom.workspaceView.trigger 'find-and-replace:toggle'
+        atom.commands.dispatch workspaceElement, 'find-and-replace:toggle'
         expect(getFindAtomPanel()).not.toBeVisible()

   describe "when FindView's replace editor is visible", ->
     it "keeps the replace editor visible when find-and-replace:show is triggered", ->
-      editorView.trigger 'find-and-replace:show-replace'
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show-replace'
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        atom.commands.dispatch editorView, 'find-and-replace:show'
         expect(findView.replaceEditor).toBeVisible()

   describe "core:cancel", ->
     beforeEach ->
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'
       waitsForPromise ->
         activationPromise

@@ -134,9 +140,9 @@
         expect(getFindAtomPanel()).not.toBeVisible()

       it "removes highlighted matches", ->
-        findResultsView = editorView.find('.search-results')
+        expect(workspaceElement).toHaveClass 'find-visible'
         atom.commands.dispatch(document.activeElement, 'core:cancel')
-        expect(findResultsView.parent()).not.toExist()
+        expect(workspaceElement).not.toHaveClass 'find-visible'

     describe "when core:cancel is triggered on an empty pane", ->
       it "hides the find panel", ->
@@ -151,7 +157,7 @@
           atom.workspace.open()

         runs ->
-          atom.workspaceView.getActiveView().trigger 'core:cancel'
+          atom.commands.dispatch editorView, 'core:cancel'
           expect(getFindAtomPanel()).not.toBeVisible()

     describe "when core:cancel is triggered on a mini editor", ->
@@ -165,7 +171,7 @@

   describe "serialization", ->
     it "serializes find and replace history", ->
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -189,7 +195,7 @@
           mainModule.createViews()
           {findView} = mainModule

-        editorView.trigger 'find-and-replace:show'
+        atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -208,7 +214,7 @@
         expect(findView.replaceEditor.getText()).toBe 'cat'

     it "serializes find options ", ->
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -235,7 +241,7 @@
           mainModule.createViews()
           {findView} = mainModule

-        editorView.trigger 'find-and-replace:show'
+        atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -250,7 +256,7 @@
     beforeEach ->
       atom.config.set('find-and-replace.focusEditorAfterSearch', false)
       editor.setCursorBufferPosition([2,0])
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -263,11 +269,11 @@
       it "runs a search", ->
         findView.findEditor.setText 'notinthefile'
         atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:confirm')
-        expect(editorView.find('.find-result')).toHaveLength 0
+        expect(getResultDecorations(editor, 'find-result')).toHaveLength 0

         findView.findEditor.setText 'items'
         atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:confirm')
-        expect(editorView.find('.find-result')).toHaveLength 5
+        expect(getResultDecorations(editor, 'find-result')).toHaveLength 5

     describe "when the find string contains an escaped char", ->
       beforeEach ->
@@ -413,7 +419,7 @@

     it "selects the next match when the 'find-and-replace:find-next' event is triggered and correctly focuses the editor", ->
       expect(findView).toHaveFocus()
-      editorView.trigger('find-and-replace:find-next')
+      atom.commands.dispatch editorView,('find-and-replace:find-next')
       expect(findView.resultCounter.text()).toEqual('3 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[2, 34], [2, 39]]
       expect(editorView).toHaveFocus()
@@ -446,7 +452,7 @@

     it "selects the previous match when the 'find-and-replace:find-previous' event is triggered and correctly focuses the editor", ->
       expect(findView).toHaveFocus()
-      editorView.trigger('find-and-replace:find-previous')
+      atom.commands.dispatch editorView,('find-and-replace:find-previous')
       expect(findView.resultCounter.text()).toEqual('1 of 6')
       expect(editor.getSelectedBufferRange()).toEqual [[1, 27], [1, 22]]
       expect(editorView).toHaveFocus()
@@ -480,20 +486,17 @@

     it "places the selected text into the find editor when find-and-replace:set-find-pattern is triggered", ->
       editor.setSelectedBufferRange([[1,6],[1,10]])
-      atom.workspaceView.trigger 'find-and-replace:use-selection-as-find-pattern'
+      atom.commands.dispatch workspaceElement, 'find-and-replace:use-selection-as-find-pattern'

       expect(findView.findEditor.getText()).toBe 'sort'
       expect(editor.getSelectedBufferRange()).toEqual [[1,6],[1,10]]

-      atom.workspaceView.trigger 'find-and-replace:find-next'
+      atom.commands.dispatch workspaceElement, 'find-and-replace:find-next'
       expect(editor.getSelectedBufferRange()).toEqual [[8,11],[8,15]]

     it "does not highlight the found text when the find view is hidden", ->
       atom.commands.dispatch(findView.findEditor[0], 'core:cancel')
       atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:find-next')
-
-      findResultsView = editorView.find('.search-results')
-      expect(findResultsView.parent()).not.toExist()

     describe "when the active pane item changes", ->
       beforeEach ->
@@ -510,17 +513,18 @@
             expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]]

         it "initially highlights the found text in the new edit session", ->
-          expect(editorView.find('.highlight.find-result')).toHaveLength 6
+          expect(getResultDecorations(editor, 'find-result')).toHaveLength 6

           waitsForPromise ->
             atom.workspace.open('sample.coffee')

           runs ->
             # old editor has no more results
-            expect(editorView.find('.highlight.find-result')).toHaveLength 0
+            expect(getResultDecorations(editor, 'find-result')).toHaveLength 0

             # new one has 7 results
-            expect(atom.workspaceView.getActiveView().find('.highlight.find-result')).toHaveLength 7
+            newEditor = atom.workspace.getActiveEditor()
+            expect(getResultDecorations(newEditor, 'find-result')).toHaveLength 7

         it "highlights the found text in the new edit session when find next is triggered", ->
           waitsForPromise ->
@@ -528,12 +532,13 @@

           runs ->
             atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:find-next')
-            expect(atom.workspaceView.getActiveView().find('.highlight.find-result')).toHaveLength 6
-            expect(atom.workspaceView.getActiveView().find('.highlight.current-result')).toHaveLength 1
+            newEditor = atom.workspace.getActiveEditor()
+            expect(getResultDecorations(newEditor, 'find-result')).toHaveLength 6
+            expect(getResultDecorations(newEditor, 'current-result')).toHaveLength 1

       describe "when all active pane items are closed", ->
         it "updates the result count", ->
-          editorView.trigger 'core:close'
+          atom.commands.dispatch editorView, 'core:close'
           expect(findView.resultCounter.text()).toEqual('no results')

       describe "when the active pane item is not an edit session", ->
@@ -553,18 +558,9 @@
           runs ->
             expect(findView.resultCounter.text()).toEqual('no results')

-        it "removes all highlights", ->
-          findResultsView = editorView.find('.search-results')
-
-          waitsForPromise ->
-            atom.workspace.open "another"
-
-          runs ->
-            expect(findResultsView.children()).toHaveLength 0
-
       describe "when a new edit session is activated on a different pane", ->
         it "initially highlights all the sample.js results", ->
-          expect(editorView.find('.find-result')).toHaveLength 6
+          expect(getResultDecorations(editor, 'find-result')).toHaveLength 6

         it "reruns the search on the new editor", ->
           newEditor = null
@@ -573,50 +569,52 @@
             atom.project.open('sample.coffee').then (o) -> newEditor = o

           runs ->
-            newEditorView = editorView.getPaneView().splitRight(newEditor).activeView
+            newEditor = atom.workspace.paneForItem(editor).splitRight(items: [newEditor]).getActiveItem()
+            expect(getResultDecorations(newEditor, 'find-result')).toHaveLength 7
+
             expect(findView.resultCounter.text()).toEqual('7 found')
-            expect(newEditorView.getModel().getSelectedBufferRange()).toEqual [[0, 0], [0, 0]]
+            expect(newEditor.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]]

             atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:find-next')
             expect(findView.resultCounter.text()).toEqual('1 of 7')
-            expect(newEditorView.getModel().getSelectedBufferRange()).toEqual [[1, 9], [1, 14]]
+            expect(newEditor.getSelectedBufferRange()).toEqual [[1, 9], [1, 14]]

         it "highlights the found text in the new edit session (and removes the highlights from the other)", ->
           [newEditor, newEditorView] = []

           waitsForPromise ->
-            atom.project.open('sample.coffee').then (o) -> newEditor = o
-
-          runs ->
-            newEditorView = editorView.getPaneView().splitRight(newEditor).activeView
+            atom.workspace.open('sample.coffee').then (o) -> newEditor = o

           runs ->
             # old editor has no more results
-            expect(editorView.find('.find-result')).toHaveLength 0
+            expect(getResultDecorations(editor, 'find-result')).toHaveLength 0

             # new one has 7 results
-            expect(newEditorView.find('.find-result')).toHaveLength 7
+            expect(getResultDecorations(newEditor, 'find-result')).toHaveLength 7

         it "will still highlight results after the split pane has been destroyed", ->
           [newEditor, newEditorView] = []

           waitsForPromise ->
-            atom.project.open('sample.coffee').then (o) -> newEditor = o
+            atom.workspace.open('sample.coffee').then (o) -> newEditor = o

           runs ->
-            newEditorView = editorView.getPaneView().splitRight(newEditor).activeView
-            expect(newEditorView.find('.find-result')).toHaveLength 7
-
-            newEditorView.focus()
-            newEditorView.trigger('core:close')
+            originalPane = atom.workspace.paneForItem(editor)
+            splitPane = atom.workspace.paneForItem(editor).splitRight()
+            originalPane.moveItemToPane(newEditor, splitPane, 0)
+            expect(getResultDecorations(newEditor, 'find-result')).toHaveLength 7
+
+            newEditorView = atom.views.getView(editor)
+            atom.commands.dispatch newEditorView, 'core:close'
             editorView.focus()

+            expect(atom.workspace.getActiveEditor()).toBe editor
+
           runs ->
-            expect(editorView.find('.find-result')).toHaveLength 6
+            expect(getResultDecorations(editor, 'find-result')).toHaveLength 6

     describe "when the buffer contents change", ->
       it "re-runs the search", ->
-        findResultsView = editorView.find('.search-results')
         editor.setSelectedBufferRange([[1, 26], [1, 27]])
         editor.insertText("")

@@ -728,57 +726,46 @@
         expect(editor.getSelectedBufferRange()).toEqual [[2, 0], [2, 5]]

     describe "highlighting search results", ->
-      resultPosition = (result) ->
-        {top: result.children()[0].style.top, left: result.children()[0].style.left}
+      getResultDecorationMarker = (clazz) ->
+        getResultDecorations(editor, clazz)[0]?.getMarker()

       it "only highlights matches", ->
-        expect(editorView.find('.find-result')).toHaveLength 5
+        expect(getResultDecorations(editor, 'find-result')).toHaveLength 5

         findView.findEditor.setText 'notinthefilebro'
         atom.commands.dispatch(findView.findEditor[0], 'core:confirm')

         runs ->
-          expect(editorView.find('.find-result')).toHaveLength 0
+          expect(getResultDecorations(editor, 'find-result')).toHaveLength 0

       it "adds a class to the current match indicating it is the current match", ->
-        position = (result) ->
-          {top: result.children()[0].style.top, left: result.children()[0].style.left}
-
-        firstResult = editorView.find('.current-result')
-        firstPosition = resultPosition(firstResult)
-        expect(firstResult).toHaveLength 1
-        expect(editorView.find('.find-result')).toHaveLength 5
-
-        atom.commands.dispatch(findView.findEditor[0], 'core:confirm')
-        atom.commands.dispatch(findView.findEditor[0], 'core:confirm')
-
-        nextResult = editorView.find('.current-result')
-        nextPosition = resultPosition(nextResult)
-        expect(nextResult).toHaveLength 1
-        expect(nextPosition).not.toEqual firstPosition
+        firstResultMarker = getResultDecorationMarker('current-result')
+        expect(getResultDecorations(editor, 'find-result')).toHaveLength 5
+
+        atom.commands.dispatch(findView.findEditor[0], 'core:confirm')
+        atom.commands.dispatch(findView.findEditor[0], 'core:confirm')
+
+        nextResultMarker = getResultDecorationMarker('current-result')
+        expect(nextResultMarker).not.toEqual firstResultMarker

         atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:find-previous')
         atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:find-previous')

-        originalResult = editorView.find('.current-result')
-        originalPosition = resultPosition(originalResult)
-        expect(originalResult).toHaveLength 1
-        expect(originalPosition).toEqual firstPosition
+        originalResultMarker = getResultDecorationMarker('current-result')
+        expect(originalResultMarker).toEqual firstResultMarker

       it "adds a class to the result when the current selection equals the result's range", ->
-        originalResult = editorView.find('.current-result')
-        originalPosition = resultPosition(originalResult)
-        expect(originalResult).toHaveLength 1
+        originalResultMarker = getResultDecorationMarker('current-result')
+        expect(originalResultMarker).toBeDefined()

         editor.setSelectedBufferRange([[5, 16], [5, 20]])

-        expect(editorView.find('.current-result')).toHaveLength 0
+        expect(getResultDecorationMarker('current-result')).toBeUndefined()
         editor.setSelectedBufferRange([[5, 16], [5, 21]])

-        newResult = editorView.find('.current-result')
-        newPosition = resultPosition(newResult)
-        expect(newResult).toHaveLength 1
-        expect(newPosition).not.toBe originalPosition
+        newResultMarker = getResultDecorationMarker('current-result')
+        expect(newResultMarker).toBeDefined()
+        expect(newResultMarker).not.toBe originalResultMarker

     describe "when user types in the find editor", ->
       advance = ->
@@ -827,7 +814,7 @@
   describe "replacing", ->
     beforeEach ->
       editor.setCursorBufferPosition([2,0])
-      editorView.trigger 'find-and-replace:show-replace'
+      atom.commands.dispatch editorView, 'find-and-replace:show-replace'

       waitsForPromise ->
         activationPromise
@@ -903,7 +890,7 @@

       describe "when the 'find-and-replace:replace-next' event is triggered", ->
         it "replaces the match after the cursor and selects the next match", ->
-          editorView.trigger 'find-and-replace:replace-next'
+          atom.commands.dispatch editorView, 'find-and-replace:replace-next'
           expect(findView.resultCounter.text()).toEqual('2 of 5')
           expect(editor.lineTextForBufferRow(2)).toBe "    if (cats.length <= 1) return items;"
           expect(editor.getSelectedBufferRange()).toEqual [[2, 33], [2, 38]]
@@ -933,7 +920,7 @@

       describe "when the 'find-and-replace:replace-all' event is triggered", ->
         it "replaces all matched text", ->
-          editorView.trigger 'find-and-replace:replace-all'
+          atom.commands.dispatch editorView, 'find-and-replace:replace-all'
           expect(findView.resultCounter.text()).toEqual('no results')
           expect(editor.getText()).not.toMatch /items/
           expect(editor.getText().match(/\bcats\b/g)).toHaveLength 6
@@ -945,7 +932,7 @@
           atom.commands.dispatch(findView.findEditor[0], 'find-and-replace:toggle-regex-option')
           findView.findEditor.setText('(items)([\\.;])')
           findView.replaceEditor.setText('$2$1')
-          editorView.trigger 'find-and-replace:replace-all'
+          atom.commands.dispatch editorView, 'find-and-replace:replace-all'
           expect(editor.getText()).toMatch /;items/
           expect(editor.getText()).toMatch /\.items/

@@ -953,13 +940,13 @@
         it "replaces the matches with without any regex subsitions", ->
           findView.findEditor.setText('items')
           findView.replaceEditor.setText('$&cats')
-          editorView.trigger 'find-and-replace:replace-all'
+          atom.commands.dispatch editorView, 'find-and-replace:replace-all'
           expect(editor.getText()).not.toMatch /items/
           expect(editor.getText().match(/\$&cats\b/g)).toHaveLength 6

   describe "history", ->
     beforeEach ->
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'

       waitsForPromise ->
         activationPromise
@@ -979,7 +966,7 @@
       [oneRange, twoRange, threeRange] = []

       beforeEach ->
-        editorView.trigger 'find-and-replace:show'
+        atom.commands.dispatch editorView, 'find-and-replace:show'
         editor.setText("zero\none\ntwo\nthree\n")
         findView.findEditor.setText('one')
         atom.commands.dispatch(findView.findEditor[0], 'core:confirm')
@@ -1068,7 +1055,7 @@

   describe "panel focus", ->
     beforeEach ->
-      editorView.trigger 'find-and-replace:show'
+      atom.commands.dispatch editorView, 'find-and-replace:show'
       waitsForPromise -> activationPromise

     it "focuses the find editor when the panel gets focus", ->


improve tests

improve tests

--- spec/find-view-spec.coffee
+++ spec/find-view-spec.coffee
@@ -829,17 +829,25 @@
       beforeEach ->
         findView.findEditor.focus()

-      it "scrolls to the first match depending on the settings", ->
+      it "scrolls to the first match if the settings scrollToResultOnLiveSearch is true", ->
+        atom.config.set('find-and-replace.scrollToResultOnLiveSearch', true)
+        editor.setHeight(3)
         editor.moveToTop()
-        atom.config.set('find-and-replace.scrollToResultOnLiveSearch', true)
-        findView.findEditor.setText 'sort'
+        originalScrollPosition = editor.getScrollTop()
+        findView.findEditor.setText 'Array'
         advance()
-        expect(editor.getSelectedBufferRange()).toEqual [[0, 9], [0, 13]]
+        expect(editor.getScrollTop()).not.toEqual originalScrollPosition
+        expect(editor.getSelectedBufferRange()).toEqual [[11, 14], [11, 19]]
         expect(findView.findEditor).toHaveFocus()
+
+      it "doesn't scroll to the first match if the settings scrollToResultOnLiveSearch is false", ->
+        atom.config.set('find-and-replace.scrollToResultOnLiveSearch', false)
+        editor.setHeight(3)
         editor.moveToTop()
-        atom.config.set('find-and-replace.scrollToResultOnLiveSearch', false)
-        findView.findEditor.setText 'concat'
+        originalScrollPosition = editor.getScrollTop()
+        findView.findEditor.setText 'Array'
         advance()
+        expect(editor.getScrollTop()).toEqual originalScrollPosition
         expect(editor.getSelectedBufferRange()).toEqual []
         expect(findView.findEditor).toHaveFocus()


Don't remove ci.log files for debugging

Don't remove ci.log files for debugging

--- spec/jasmine-helper.coffee
+++ spec/jasmine-helper.coffee
@@ -25,7 +25,6 @@
         timeReporter.logLongestSuites 10, (line) -> process.stdout.write("#{line}\n")
         process.stdout.write('\n')
         timeReporter.logLongestSpecs 10, (line) -> process.stdout.write("#{line}\n")
-        fs.closeSync(logStream) if logStream?
         atom.exit(runner.results().failedCount > 0 ? 1 : 0)
   else
     AtomReporter = require './atom-reporter'


add another test

add another test

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -1608,6 +1608,18 @@
       expect(editor.getText()).toBe "abcabc"
       expect(editor.getCursorScreenPosition()).toEqual [0, 5]

+    it "stores for repeating only the last batch of characters, repeats as insert", ->
+      keydown 'a'
+      editor.insertText("abc")
+      atom.commands.dispatch editorElement, 'vim-mode:move-left-insert'
+      editor.insertText("de")
+      keydown 'escape'
+      expect(editor.getText()).toBe "abdec"
+      expect(editor.getCursorScreenPosition()).toEqual [0, 3]
+      keydown '.'
+      expect(editor.getText()).toBe "abddeec"
+      expect(editor.getCursorScreenPosition()).toEqual [0, 4]
+
   describe "the ctrl-a/ctrl-x keybindings", ->
     beforeEach ->
       atom.config.set 'vim-mode.numberRegex', settings.config.numberRegex.default


Suppress deprecations on deprecated code path

--- spec/pane-view-spec.coffee
+++ spec/pane-view-spec.coffee
@@ -1,7 +1,7 @@
 PaneContainer = require '../src/pane-container'
 PaneView = require '../src/pane-view'
 fs = require 'fs-plus'
-{Emitter} = require 'event-kit'
+{Emitter, Disposable} = require 'event-kit'
 {$, View} = require '../src/space-pen-extensions'
 path = require 'path'
 temp = require 'temp'
@@ -21,6 +21,7 @@
       @emitter.emit 'did-change-title', 'title'
     onDidChangeTitle: (callback) ->
       @emitter.on 'did-change-title', callback
+    onDidChangeModified: -> new Disposable(->)

   beforeEach ->
     deserializerDisposable = atom.deserializers.add(TestView)
@@ -155,13 +156,18 @@
       expect(view1.is(':visible')).toBe true

   describe "when the title of the active item changes", ->
-    describe 'when there is no onDidChangeTitle method', ->
+    describe 'when there is no onDidChangeTitle method (deprecated)', ->
       beforeEach ->
+        jasmine.snapshotDeprecations()
+
         view1.onDidChangeTitle = null
         view2.onDidChangeTitle = null

         pane.activateItem(view2)
         pane.activateItem(view1)
+
+      afterEach ->
+        jasmine.restoreDeprecationsSnapshot()

       it "emits pane:active-item-title-changed", ->
         activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler")


add missing ?

add missing ?

--- shared/src/api/action-adapter.coffee
+++ shared/src/api/action-adapter.coffee
@@ -1,7 +1,6 @@
 {Promise} = require 'es6-promise'
 interpolate = require 'interpolate'
 {METHODS_TO_ACTIONS} = require './collections'
-{ extendObservable, observable } = require 'mobx'

 partial   = require 'lodash/partial'
 map       = require 'lodash/map'
@@ -146,7 +145,7 @@
       this.api?.requestCounts[action] += 1
       this.api?.requestsInProgress.delete(action)
       reply
-    ).catch( => this.apiRequestsInProgress.delete(action))
+    ).catch( => this.api?.requestsInProgress.delete(action))

   klass.prototype[method] = wrap(klass.prototype[method] or emptyFn, handler)


fix for showing wrong tab issue after unlocking wallet

--- app/js/controllers/market.coffee
+++ app/js/controllers/market.coffee
@@ -12,18 +12,17 @@
     current_market = null
     price_decimals = 4

-    # tabs
-    $scope.tabs = tabs_basic = []
-    tabs_basic.push { heading: "market.buy", route: "market.buy", active: true, class: "tab-buy" }
-    tabs_basic.push { heading: "market.sell", route: "market.sell", active: false, class: "tab-sell" }
-    tabs_basic.push { heading: "market.short", route: "market.short", active: false, class: "tab-short" }
+    $scope.tabs = [
+        { heading: "market.buy", route: "market.buy", active: true, class: "tab-buy" },
+        { heading: "market.sell", route: "market.sell", active: false, class: "tab-sell" },
+        { heading: "market.short", route: "market.short", active: false, class: "tab-short" }
+    ]

     $scope.goto_tab = (route) -> $state.go route
     $scope.active_tab = (route) -> $state.is route
     $scope.$on "$stateChangeSuccess", ->
-        #$scope.state_name = $state.current.name
         $scope.tabs.forEach (tab) -> tab.active = $scope.active_tab(tab.route)
-
+
     Wallet.get_account(account.name).then (acct) ->
         Wallet.set_current_account(acct)

@@ -66,6 +65,7 @@
     market_name = $stateParams.name
     promise = MarketService.init(market_name)
     promise.then (market) ->
+        $scope.tabs.forEach (tab) -> tab.active = $scope.active_tab(tab.route)
         $scope.market = current_market = market
         $scope.actual_market = market.get_actual_market()
         $scope.market_inverted_url = MarketService.inverted_url


Added unit tests to check if return values a pure

--- specs/somewhereSpec.coffee
+++ specs/somewhereSpec.coffee
@@ -12,6 +12,7 @@
       username: 'dreyacosta'
       name: 'David'
       blog: 'dreyacosta.com'
+      source: 'twitter'
     item = db.save 'users', user
     expect(item.username).to.equal 'dreyacosta'

@@ -24,9 +25,38 @@
       username: 'drey'
       name: 'David'
       blog: 'drey.com'
+      source: 'twitter'
     item = db.save 'users', user
     items = db.find 'users', { name: 'David' }
     expect(items.length).to.equal 2
+
+  it "should return a pure object when find one", ->
+    item = db.findOne 'users', { username: 'dreyacosta' }
+    expect(item.name).to.equal 'David'
+    item.name = 'Mike'
+    item = db.findOne 'users', { username: 'dreyacosta' }
+    expect(item.name).to.equal 'David'
+
+  it "should return a pure array when find all", ->
+    user =
+      username: 'pirish'
+      name: 'Paul'
+      blog: 'pirish.com'
+      source: 'twitter'
+    item = db.save 'users', user
+    items = db.find 'users', { source: 'twitter' }
+    expect(items.length).to.equal 3
+    items[0].source = 'facebook'
+    items = db.find 'users', { source: 'twitter' }
+    expect(items.length).to.equal 3
+
+  it "should return a pure object when update", ->
+    item = db.findOne 'users', { username: 'dreyacosta' }
+    item = db.update 'users', item.id, { name: 'David Rey' }
+    expect(item.name).to.equal 'David Rey'
+    item.name = 'Mike'
+    item = db.findOne 'users', { username: 'dreyacosta' }
+    expect(item.name).to.equal 'David Rey'

   it "should update item", ->
     data =
@@ -38,10 +68,11 @@
   it "should remove item from a collection", ->
     item = db.findOne 'users', { username: 'drey' }
     expect(item.username).to.equal 'drey'
-
-    db.remove 'users', item.id
+    result = db.remove 'users', item.id
+    expect(result).to.equal true
     item = db.findOne 'users', { id: item.id }
-    expect(item).to.equal undefined
+    expect(typeof item).to.equal 'object'
+    expect(Object.keys(item).length).to.equal 0

   it "should clear the database file", ->
     db.clear()


Fix example

Fix example

--- examples/index.coffee
+++ examples/index.coffee
@@ -8,8 +8,8 @@
   _ "body", (_)->
     _ "h1", page_title
     _ "#main", (_)->
-      url = "http://k2lab.net/"
-      _ "a[href=#{url}][name=link]", url
+      url = "/"
+      _ "a[href=#{url}][name=link]", "Top"
       _ "p#id_p", {style: "color:red"}, "Hello"
       _ "hr.class_hr"
       _ "p", "World"


Update project-controller.coffee

Update project-controller.coffee

Fix detecting platform

--- lib/controllers/project-controller.coffee
+++ lib/controllers/project-controller.coffee
@@ -5,7 +5,6 @@
 RegExpPattern = require '../pattern/regexp-pattern'
 ProjectView = require '../views/project-view'
 {Directory, File, BufferedProcess, CompositeDisposable} = require 'atom'
-os = require 'os';

 module.exports =
 class ProjectController
@@ -196,12 +195,12 @@

   getPathExpression: (hasLine) ->
     if hasLine
-      if os.platform == "win32"
+      if process.platform == "win32"
         return "(?:[a-z]:\\\\|\\\\)?[\\w\\.\\-]+[\\\\[\\w\\.\\-]+]*";
       else
         return "(?:~\\/|\\/?)[\\w\\.\\-]+[\\/[\\w\\.\\-]+]*";

-    if os.platform == "win32"
+    if process.platform == "win32"
       return "(?:[a-z]:\\\\|\\\\)?(?:[\\w\\.\\-]+\\\\)+[\\w\\.\\-]+";

     return "(?:~\\/|\\/?)(?:[\\w\\.\\-]+\\/)+[\\w\\.\\-]+";


Get classification ready to send

Get classification ready to send

--- app/models/classification.coffee
+++ app/models/classification.coffee
@@ -1,19 +1,24 @@
 Api = require 'zooniverse/lib/api'

 class Classification
+  subject: null
   started: 0
   agent: ''
   marks: null
+  favorite: false

-  constructor: ->
+  constructor: (params = {}) ->
+    @[propety] = value for own propety, value of params
     @started = (new Date).toUTCString()
     @agent = window.navigator.userAgent
     @marks = []

   toJSON: ->
-    annotations: (mark.toJSON() for mark in @marks when not mark.destroyed).concat [{@started}, {@agent}]
+    annotations = (mark.toJSON() for mark in @marks when not mark.destroyed).concat [{@started}, {@agent}]
+    {subject_ids: [@subject.id], annotations, @favorite}

   send: ->
     console?.info 'Saving classification', @toJSON()
+    window.lastClassification = @toJSON()

 module.exports = Classification


Adjust gallery

Adjust gallery

--- source/javascripts/justified-gallery.coffee
+++ source/javascripts/justified-gallery.coffee
@@ -1,10 +1,13 @@
+# See also http://miromannino.github.io/Justified-Gallery/
 #= require './vendor/jquery.justifiedGallery'

 $ ->
   lineHeight = parseInt($('body').css('lineHeight'))
   numOfLines = 6
+  margins = 1
   $(".picture").justifiedGallery
-    rowHeight: numOfLines * lineHeight
+    rowHeight: numOfLines * lineHeight - margins
     fixedHeight: yes
     randomize: yes
     lastRow: 'justify'
+    margins: margins


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -118,44 +118,44 @@
   If no arguments are given, defaults to transparent.

-  individualRgb = Color(23, 56, 49, 0.4)
-
-  arrayRgb = Color([59, 100, 230])
-
-  hex = Color('#ff0000')
-
-  rgb = Color('rgb(0, 255, 0)')
-
-  hsl = Color('hsl(180, 1, 0.5)')
-
-  anotherColor = Color('blue')
-
-  Color(anotherColor)
-  # => a new color with the same r, g, b, and alpha values as `anotherColor`
-
-  # You have access to all sorts of weird colors.
-  # We give you all the named colors the browser recognizes
-  # and the ones from this survey
-  # http://blog.xkcd.com/2010/05/03/color-survey-results/
-  namedBrown = Color('Fuzzy Wuzzy Brown')
-
-  # Uutput color in Hex format
-  namedBrown.toHex()
-  # => '#c45655'
-
-  # Default behavior
-  transparent = Color()
-
-  transparent.toString()
-  # => 'rgba(0, 0, 0, 0)'
-
-  # let's print out the colors on a canvas to see what they look like
-  canvas.font('14px Helvetica')
-  for color, index in ['individualRgb', 'arrayRgb', 'hex', 'rgb', 'hsl', 'anotherColor', 'namedBrown']
-    canvas.centerText
-      color: eval(color)
-      text: color
-      y: 20 * (index + 1)
+      individualRgb = Color(23, 56, 49, 0.4)
+
+      arrayRgb = Color([59, 100, 230])
+
+      hex = Color('#ff0000')
+
+      rgb = Color('rgb(0, 255, 0)')
+
+      hsl = Color('hsl(180, 1, 0.5)')
+
+      anotherColor = Color('blue')
+
+      Color(anotherColor)
+      # => a new color with the same r, g, b, and alpha values as `anotherColor`
+
+      # You have access to all sorts of weird colors.
+      # We give you all the named colors the browser recognizes
+      # and the ones from this survey
+      # http://blog.xkcd.com/2010/05/03/color-survey-results/
+      namedBrown = Color('Fuzzy Wuzzy Brown')
+
+      # Uutput color in Hex format
+      namedBrown.toHex()
+      # => '#c45655'
+
+      # Default behavior
+      transparent = Color()
+
+      transparent.toString()
+      # => 'rgba(0, 0, 0, 0)'
+
+      # let's print out the colors on a canvas to see what they look like
+      canvas.font('14px Helvetica')
+      for color, index in ['individualRgb', 'arrayRgb', 'hex', 'rgb', 'hsl', 'anotherColor', 'namedBrown']
+        canvas.centerText
+          color: eval(color)
+          text: color
+          y: 20 * (index + 1)

   @name Color
   @param {Array|Number|String|Color} args... An Array, r, g, b values,


CurrentUser now a fxn returning the current user

--- app/services/current-user.coffee
+++ app/services/current-user.coffee
@@ -1,11 +1,6 @@
 null

 ### @ngInject ###
-global.cobudgetApp.factory 'CurrentUser', (Records, ipCookie) ->
-
-  setId: (id) ->
-    ipCookie('currentUserId', id)
-
-  get: ->
-    id = ipCookie('currentUserId')
-    Records.users.find(id)+global.cobudgetApp.factory 'CurrentUser', (Records) ->
+  ->
+    Records.users.find(global.cobudgetApp.currentUserId)


added get_closest_level_by_extent test

added get_closest_level_by_extent test

--- bokehjs/test/test_renderer/test_geo/test_tile_layer.coffee
+++ bokehjs/test/test_renderer/test_geo/test_tile_layer.coffee
@@ -60,7 +60,7 @@

   describe "Grid Layer", ->
     url = 'http://c.tiles.mapbox.com/v3/examples.map-szwdot65/{Z}/{X}/{Y}.png'
-    provider = new Geo.GridLayer(url, 256)
+    provider = new Geo.TileProvider(url, 256)

     it "should remove tile and add back to image pool ", ->
       expect(provider.pool.images.length).to.be.equal(0)
@@ -157,15 +157,20 @@
       expect(provider.get_resolution(12)).to.be.closeTo(38.2185, tol)

     it "should convert tile x,y,z into cache key", ->
-      expect(provider.tile_xyz_to_key(1, 1, 1)).to.be.equal "1:1:1"
+      expect(provider.tile_xyz_to_key(1, 1, 1)).to.be.equal("1:1:1")

     it "should convert cache key into tile x,y,z", ->
-      expect(provider.key_to_tile_xyz("1:1:1")).to.be.eql [1,1,1]
+      expect(provider.key_to_tile_xyz("1:1:1")).to.be.eql([1,1,1])

     it "should get best zoom level based on extent and height/width", ->
-      expect(provider.get_level_by_extent(T.MERCATOR_BOUNDS, 256, 256)).to.be.equal 0
-      expect(provider.get_level_by_extent(T.MERCATOR_BOUNDS, 512, 512)).to.be.equal 1
-      expect(provider.get_level_by_extent(T.MERCATOR_BOUNDS, 1024, 1024)).to.be.equal 2
+      expect(provider.get_level_by_extent(T.MERCATOR_BOUNDS, 256, 256)).to.be.equal(0)
+      expect(provider.get_level_by_extent(T.MERCATOR_BOUNDS, 512, 512)).to.be.equal(1)
+      expect(provider.get_level_by_extent(T.MERCATOR_BOUNDS, 1024, 1024)).to.be.equal(2)
+
+    it "should get closest zoom level based on extent and height/width", ->
+      expect(provider.get_closest_level_by_extent(T.MERCATOR_BOUNDS, 256, 256)).to.be.equal 0
+      expect(provider.get_closest_level_by_extent(T.MERCATOR_BOUNDS, 513, 512)).to.be.equal 1
+      expect(provider.get_closest_level_by_extent(T.MERCATOR_BOUNDS, 1024, 1024)).to.be.equal 2

     it "should convert pixel x/y to tile x/y", ->
       expect(provider.pixels_to_tile(1, 1)).to.be.eql [0,0]


toJson() allows arg {omit: key} or {omit: [keys]}

--- src/core/element.coffee
+++ src/core/element.coffee
@@ -1,4 +1,5 @@
 {pjson, d} = require 'lightsaber/lib/log'
+_ = require 'lodash'

 class Element

@@ -23,6 +24,9 @@
   #
   toJson: (args = {}) ->
     {replacer, space} = args
-    JSON.stringify @data(), replacer, space
+    omit = args.omit
+    data = @data()
+    data = _.omit data, omit if omit
+    JSON.stringify data, replacer, space

 module.exports = Element


Handle decorations added with a class param

In that case the class param is transformed in the corresponding
selector and scoped into the `.minimap` class.

--- lib/mixins/decoration-management.coffee
+++ lib/mixins/decoration-management.coffee
@@ -32,6 +32,10 @@

   decorateMarker: (marker, decorationParams) ->
     marker = @getMarker(marker.id)
+
+    if !decorationParams.scope? and decorationParams.class?
+      cls = decorationParams.class.split(' ').join('.')
+      decorationParams.scope = ".minimap .#{cls}"

     @decorationMarkerDestroyedSubscriptions[marker.id] ?= @subscribe marker, 'destroyed', =>
       @removeAllDecorationsForMarker(marker)


Testing newpie on Support

Testing newpie on Support

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -275,6 +275,7 @@
             totalTickets += pd.value
         totalLabel.append('text')
             .text(totalTickets)
+            .attr('font-size',radiusi + 'px')
             .attr('text-anchor', 'middle')
             .attr('alignment-baseline', 'mathematical')
             .attr('filter','url(#dropshadow)')


Return a Disposable from mocked method to avoid exception it specs

--- spec/text-editor-element-spec.coffee
+++ spec/text-editor-element-spec.coffee
@@ -1,5 +1,6 @@
 TextEditorElement = require '../src/text-editor-element'
 TextEditor = require '../src/text-editor'
+{Disposable} = require 'event-kit'

 # The rest of text-editor-component-spec will be moved to this file when React
 # is eliminated. This covers only concerns related to the wrapper element for now
@@ -159,6 +160,7 @@
         initialThemeLoadComplete
       spyOn(atom.themes, 'onDidChangeActiveThemes').andCallFake (fn) ->
         themeReloadCallback = fn
+        new Disposable

       atom.config.set("editor.useShadowDOM", false)


Agent pie: test globalizing variables

Agent pie: test globalizing variables

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -3,6 +3,13 @@
     @accessor 'data'

     #@oldPieData: ''
+    @container: ''
+
+    ready: ->
+        # Remove any previous svg
+        $(@node).children('svg').remove()
+        # Define the container
+        @container = $(@node).parent()
     #
     # CONFIG ZONE
     #


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -229,20 +229,16 @@
         #        return arc b
         #)
         path.transition().duration(2000).attrTween('d', (d,i) ->
-            console.log('window.oldPieData[myInstanceDataId]',window.oldPieData[myInstanceDataId])
-            opd = window.oldPieData[myInstanceDataId]
-            if opd.length = 0
-                opd = d
-            console.log('opd',opd)
+            window.oldPieData[myInstanceDataId][i] = window.oldPieData[myInstanceDataId][i] ? []
+            window.oldPieData[myInstanceDataId][i] = d
+
             if myInstanceDataId = 'resolved' and i = 0
                 console.log('in tween - OLD DATA - ' + myInstanceDataId + '[' + i + ']: ',window.oldPieData[myInstanceDataId][i])
             #console.log('1. in tween - d: ',d)

-            #myInterpolater = d3.interpolate(window.oldPieData[myInstanceDataId][i] ? d, d)
-            myInterpolater = d3.interpolate(opd[i])
-
-            #console.log('myInterpolater(0): ',myInterpolater(0))
-            window.oldPieData[myInstanceDataId][i] = myInterpolater(0) ? d
+            myInterpolater = d3.interpolate(window.oldPieData[myInstanceDataId][i], d)
+
+            #window.oldPieData[myInstanceDataId][i] = myInterpolater(0) ? d
             if myInstanceDataId = 'resolved' and i = 0
                 console.log('in tween - NEW DATA - ' + myInstanceDataId + '[' + i + ']: ',window.oldPieData[myInstanceDataId][i])
             #console.log('2. in tween - d: ',d)


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -98,7 +98,7 @@
         c = arc.centroid(d)
         x = c[0]
         y = c[1]
-        mx = 1.2
+        mx = 1.1
         my = 1.3
         h = Math.sqrt(x*x + y*y)
         r = labelRadius


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -279,6 +279,7 @@
             .attr("class", "pie")
             .attr("fill",(d, i) -> return color i)
         #path.data(piedata)
+        console.log("d",d)
         path.transition()
             .duration(1500)
             #.attrTween("d", pieTween)


change username: working prototype

change username: working prototype

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -30,6 +30,10 @@
                           ]
       instance          : []
     schema              :
+      type              :
+        type            : String
+        enum            : ['invalid vm type', ['user', 'group']]
+      uid               : Number
       ip                :
         type            : String
         default         : -> null
@@ -69,6 +73,11 @@
         regYears      : 0
       ).save (err)-> console.log err  if err?

+  @removeDomains = (domains) ->
+    JDomain = require './domain'
+    JDomain.remove domain: $in: domains, (err) ->
+      console.error err  if err?
+
   @createAliases = ({nickname, type, uid, groupSlug})->
     domain       = 'kd.io'
     aliases      = []
@@ -125,7 +134,9 @@
           JVM.createDomains hostnameAlias

           vm = new JVM {
-            name        : "#{name}#{uid}"
+            type
+            uid
+            name        : "#{name}~#{uid}"
             planCode    : planCode
             planOwner   : planOwner
             users       : [{ id: user.getId(), sudo: yes, owner: yes }]
@@ -293,6 +304,8 @@
       JVM.createDomains hostnameAlias

       vm = new JVM {
+        type
+        uid
         name      : name
         planCode  : planCode
         planOwner : planOwner
@@ -321,15 +334,31 @@
         else user.update { $set: { uid } }, handleError

     JAccount.on 'UsernameChanged', (oldUsername, newUsername) ->
-      # JVM.each name: ///^\w+~#{oldUsername}~///, (err, vm) ->
-      #   groupName = vm.name.substr 0, vm.name.indexOf '~'
-      #   hostnameAlias = vm.hostnameAlias.map (alias)->
-      #     edges = alias.split '.'
-      #     console.log edges
+      re = ///^\w+~#{oldUsername}~///
+      JVM.each name: re, {}, (err, vm) ->
+        return callback err  if err?
+
+        if vm?
+          [groupSlug] = vm.name.split '~'
+
+          name = "#{groupSlug}~#{newUsername}~#{vm.uid}"
+
+          JVM.removeDomains vm.hostnameAlias  if vm.hostnameAlias?
+
+          hostnameAlias = JVM.createAliases {
+            nickname  : newUsername
+            type      : vm.type
+            uid       : vm.uid
+            groupSlug
+          }
+
+          JVM.createDomains hostnameAlias
+
+          vm.update $set: { name, hostnameAlias }, (err) ->
+            console.error err  if err?

     JGroup.on 'GroupCreated', ({group, creator})->
       group.fetchBundle (err, bundle)->
-        console.log err, bundle
         if err then handleError err
         else if bundle and bundle.sharedVM
           creator.fetchUser (err, user)->


King of New York easter egg

King of New York easter egg

--- src/leaderboard.coffee
+++ src/leaderboard.coffee
@@ -69,6 +69,8 @@
 module.exports = (robot) ->
   leaderboards = new Leaderboards robot

+  String::startsWith ?= (s) -> @[...s.length] is s
+
   robot.respond /(.*) (climbs|rises|ascends)( up)?( the)? (.*) leaderboard/i, (msg) ->
     name = msg.match[1]
     event = msg.match[5]
@@ -80,7 +82,7 @@
         name = users[0].name

       leaderboards.increment event, name
-      if event.toLowerCase() is "king of tokyo"
+      if event.toLowerCase().startsWith "king of"
         msg.send "We have a new king"
       else
         msg.send "We have a winner"


Relax restriction on trailing period

Relax restriction on trailing period

--- src/legal-eagle.coffee
+++ src/legal-eagle.coffee
@@ -121,12 +121,15 @@
 readIfExists = (path) ->
   readFileSync(path, 'utf8') if existsSync(path)

+normalizeLicenseText = (licenseText) ->
+  licenseText.replace(/\s+/gm, ' ').replace(/\s+$/m, '').replace(/\.$/, '').trim()
+
 MITLicenseText = """
   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

-  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
 """.replace(/\s+/gm, ' ')

 isMITLicense = (licenseText) ->
@@ -135,7 +138,7 @@
   else
     startIndex = licenseText.indexOf('Permission is hereby granted')
     if startIndex > -1
-      normalizedLicenseText = licenseText[startIndex..].replace(/\s+/gm, ' ').replace(/\s+$/m, '')
+      normalizedLicenseText = normalizeLicenseText(licenseText[startIndex..])
       normalizedLicenseText is MITLicenseText
     else
       false
@@ -147,7 +150,7 @@

   Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

-  THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+  THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
 """.replace(/\s+/gm, ' ')

 isBSDLicense = (licenseText) ->
@@ -156,7 +159,7 @@
   else
     startIndex = licenseText.indexOf('Redistribution and use')
     if startIndex > -1
-      normalizedLicenseText = licenseText[startIndex..].replace(/\s+/gm, ' ').replace(/\s+$/m, '')
+      normalizedLicenseText = normalizeLicenseText(licenseText[startIndex..])
       normalizedLicenseText is BSD3LicenseText
     else
       false
@@ -179,7 +182,7 @@
   else
     startIndex = licenseText.indexOf('This is free and unencumbered software')
     if startIndex > -1
-      normalizedLicenseText = licenseText[startIndex..].replace(/\s+/gm, ' ').replace(/\s+$/m, '')
+      normalizedLicenseText = normalizeLicenseText(licenseText[startIndex..])
       normalizedLicenseText is UnlicenseText
     else
       false


Normalize 'WTFPL' to 'WTF'

Normalize 'WTFPL' to 'WTF'

--- src/legal-eagle.coffee
+++ src/legal-eagle.coffee
@@ -44,6 +44,7 @@
     license = 'BSD' if license.match /^BSD-.*/
     license = 'MIT' if license is 'MIT/X11'
     license = 'Apache' if license.match /^Apache.*/
+    license = 'WTF' if license is 'WTFPL'
     {license, source: 'package.json'}
   else
     extractLicenseFromReadme(readme) ? extractLicenseFromDirectory(path) ? {license: 'UNKNOWN'}


Missing return

Missing return

--- src/lib/Helpers.coffee
+++ src/lib/Helpers.coffee
@@ -65,6 +65,7 @@
       func.call component, data, groups, outProxy
       do errorHandler
       output.done()
+      return

     # Async WirePattern will call the output.done callback itself
     errorHandler = setupErrorHandler component, config, output, ->
@@ -154,6 +155,7 @@
     return false unless component.inPorts[p].hasDefault()
     true
   component.sendDefaults = ->
+    platform.deprecated 'noflo.helpers.WirePattern sendDefaults method is deprecated. Please start with a Network'
     portsWithDefaults.forEach (port) ->
       tempSocket = InternalSocket.createSocket()
       component.inPorts[port].attach tempSocket


mocha: Output error in loading/parsing suites

JS-YAML is very picky about yaml formatting for instance...

--- src/mocha.coffee
+++ src/mocha.coffee
@@ -37,7 +37,12 @@
 # it is responsible for setting up the "describe", and "it" functions
 exports.run = (rt, tests, options) ->
   runner = new Runner rt
-  suites = testsuite.getSuitesSync tests
+  console.log 'trying suites'
+  try
+    suites = testsuite.getSuitesSync tests
+  catch e
+    console.log 'Unable to get suites:', e
+    throw e
   process = null

   start = (callback) ->


added helper methods for Number

added helper methods for Number

--- src/scripts/EMI.coffee
+++ src/scripts/EMI.coffee
@@ -37,3 +37,18 @@
 Math.round = (number, decimals) -> # optional, default 0
   multiplier = Math.pow(10, decimals)
   _round(number * multiplier) / multiplier
+
+Number::round = (decimals=0) ->
+  Math.round this, decimals
+
+Number::format_rupees = () ->
+  x = @toString()
+  afterPoint = ""
+  afterPoint = x.substring(x.indexOf("."), x.length)  if x.indexOf(".") > 0
+  x = Math.floor(x)
+  x = x.toString()
+  lastThree = x.substring(x.length - 3)
+  otherNumbers = x.substring(0, x.length - 3)
+  lastThree = "," + lastThree  unless otherNumbers is ""
+  res = otherNumbers.replace(/\B(?=(\d{2})+(?!\d))/g, ",") + lastThree + afterPoint
+


Revert this, Sinan fixed this in framework level

--- client/Framework/components/tree/treeviewcontroller.coffee
+++ client/Framework/components/tree/treeviewcontroller.coffee
@@ -240,7 +240,6 @@
       # self remove
       @listControllers[parentId].getListView().removeItem @nodes[id]
       # remove reference
-      @nodes[id].destroy?()
       delete @nodes[id]

   removeNodeView:(nodeView)->
@@ -264,7 +263,6 @@
       @removeNode childNodeId

     @listControllers[id]?.getView().destroy()
-    @listControllers[id]?.destroy()
     delete @listControllers[id]
     delete @listData[id]


Groups: your groups double loading fixed

--- client/Main/avatararea/avatarareagroupswitcherpopup.coffee
+++ client/Main/avatararea/avatarareagroupswitcherpopup.coffee
@@ -178,7 +178,7 @@

   populateGroups:->
-    return  unless KD.isLoggedIn() or @isLoading
+    return  if not KD.isLoggedIn() or @isLoading

     @listController.removeAllItems()


removed unnecessary object properties

removed unnecessary object properties

--- client/app/CommonViews/opinions/opinionlistitemview.coffee
+++ client/app/CommonViews/opinions/opinionlistitemview.coffee
@@ -260,20 +260,8 @@
     </div>
     """

-class OpinionBodyView extends KDView
-  constructor:(options,data)->
-    super options, data
-
-  viewAppended:->
-    @setTemplate @pistachio()
-    @template.update()
-
-  render:(force=no)->
-    if force
-      super
-    else
-      no
-
+class OpinionBodyView extends JView
+
   pistachio:->
     """
       {{@utils.expandUsernames(@utils.applyMarkdown(#(body)),"pre")}}


deprecated @data.content

deprecated @data.content

--- modules/http/lib/Context.coffee
+++ modules/http/lib/Context.coffee
@@ -31,6 +31,8 @@
                 config.data.route = ()=>
                     route.apply @, arguments

+            @_content = config.content
+
             return config

@@ -160,8 +162,10 @@
         ##
         ##
         _createContent: (req, res, fn)->
-
-            if typeof (content = @data.content) is 'function'
+            if @data.content
+                @logger.warn 'config.data.content is deprecated! use config.content instead'
+
+            if typeof (content = @_content) is 'function'
                 content.apply @, [req, res, fn]

             else


Better handling of IDB bogus NotFoundError

--- assets/javascripts/app/db.coffee
+++ assets/javascripts/app/db.coffee
@@ -84,9 +84,11 @@
         return
       txn.onerror = (event) =>
         event.preventDefault()
-        if txn.error.name is 'NotFoundError' and _retry
+        if txn.error?.name is 'NotFoundError' and _retry
           @migrate()
-          @store(doc, data, onSuccess, onError, false)
+          setTimeout =>
+            @store(doc, data, onSuccess, onError, false)
+          , 0
         else
           onError(event)
         return
@@ -113,9 +115,11 @@
         return
       txn.onerror = (event) ->
         event.preventDefault()
-        if txn.error.name is 'NotFoundError' and _retry
+        if txn.error?.name is 'NotFoundError' and _retry
           @migrate()
-          @unstore(doc, onSuccess, onError, false)
+          setTimeout =>
+            @unstore(doc, onSuccess, onError, false)
+          , 0
         else
           onError(event)
         return


Fix error while opening file

Fix error while opening file

--- src/main/window.coffee
+++ src/main/window.coffee
@@ -41,7 +41,7 @@

   onContentsDidFinishLoad: =>
     @browserWindow.webContents.executeJavaScript js
-    # @start 'README.md'
+    @browserWindow.setTitle 'README.md'
     @render readMe

   onMoved: => @registerBounds()
@@ -85,7 +85,7 @@
   load: (filename) ->
     readFile filename, 'utf8', (err, data) =>
       throw err if err?
-      @render md
+      @render data

   render: (md) ->
     @browserWindow.webContents.send 'call', 'render', md


didn't mean that change

didn't mean that change

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -338,7 +338,6 @@
         };
       }
     """
-    loadContracts = loadContracts.replace("\n", "")

     return code if o.bare
     if o.contracts


Clean up logging

Clean up logging

--- src/backbone.sharedcollection.coffee
+++ src/backbone.sharedcollection.coffee
@@ -55,7 +55,7 @@

   captureError: (model, method) => (err) =>
     if err
-      log "Sync error!", err
+      log "Sync error! #{ method }:", err
       @trigger "syncerror", model, method, err

   create: (model, options={}) ->
@@ -147,18 +147,12 @@
   _bindSendOperations: ->

     @bind "change", (model, options) =>
-      if options?.local
-        console.log "LOCAL change"
       @_sendModelChange model unless options?.local

     @bind "add", (model, collection, options) =>
-      if options?.local
-        console.log "LOCAL add"
       @_sendModelAdd model unless options?.local

     @bind "destroy", (model, collection, options) =>
-      if options?.local
-        console.log "LOCAL destroy"
       @_sendModelDestroy model unless options?.local


fixed endpoints

fixed endpoints

--- src/api/statsd.coffee
+++ src/api/statsd.coffee
@@ -45,9 +45,10 @@
     var render_url = "/render?target=transformNull(summarize(stats.counters." + domain + ".channels." + allowedIds[j]
     for (i = 0; i < status_array.length; i++) {
       path = render_url + ".statuses." + status_array[i] + ".count,'1week'))&from=-1days&format=json";
-      results[status_array[i]] = yield exports.fetchData(path);
+      results[status_array[i]] = co(function* () {
+        yield exports.fetchData(path)
+      });
       final[status_array[i]] = 'data' in results[status_array[i]] ? results[status_array[i]].data[0][0] : 0;
-
     }

     data.push({
@@ -90,6 +91,7 @@
       completed = 'data' in results.Completed ? results.Completed.data[0][0]  : 0,
       successful = 'data' in  results.Successful ? results.Successful.data[0][0] : 0,
       completedWErrors = 'data' in results['Completed with error(s)'] ? results['Completed with error(s)'].data[0][0] : 0;
+
     data.push({
       _id: {"channelID": channelId},
       failed: failed,
@@ -151,7 +153,7 @@
   switch requiredFormat
     when "retrieveAverageLoadTimePerHour" then _.forEach raw.data, (item) ->
       data.push
-        load: item[0]
+        avgResp: item[0]
         timestamp: moment.unix item[1]

     when "retrieveChannelMetrics", "transactionsPerChannelPerHour" then _.forEach raw.data, (item) ->


making the packageDirPaths order back to the way it was

--- src/app/config.coffee
+++ src/app/config.coffee
@@ -22,7 +22,7 @@
 class Config
   configDirPath: configDirPath
   themeDirPaths: [userThemesDirPath, bundledThemesDirPath, vendoredThemesDirPath]
-  packageDirPaths: [userPackagesDirPath, bundledPackagesDirPath, vendoredPackagesDirPath]
+  packageDirPaths: [userPackagesDirPath, vendoredPackagesDirPath, bundledPackagesDirPath]
   defaultSettings: null
   settings: null


Display name of file that didn't match a spec

--- test/config/test-runner.coffee
+++ test/config/test-runner.coffee
@@ -44,7 +44,7 @@
         testPath.replace(/\.(\w+)$/, ".spec.cjsx"), testPath.replace(/\.(\w+)$/, ".spec.coffee")
       ], fileExists)
       if _.isEmpty(existingSpecs)
-        gutil.log("[change]", gutil.colors.red("no spec was found"))
+        gutil.log("[change]", gutil.colors.red("no spec was found for #{change.relative}"))
       else
         @pendingSpecs.push(existingSpecs...)
     else


Separate functions for id->msg and for msg->msg

--- src/i18n.coffee
+++ src/i18n.coffee
@@ -1,20 +1,13 @@
-I18n = (id_or_object, args...) ->
-  switch typeof id_or_object
-    when "string"
-      m = chrome.i18n.getMessage(id_or_object, [args...])
-      return escapeHtml(m) if m != ""
-      m = id_or_object.replace(
-        /(.)([A-Z][a-z])/g,
-        (m, p1, p2) -> "#{p1} #{p2.toLowerCase()}"
-      )
-      m = m.replace(/_/g, ' ')
-      escapeHtml(m)
-    when "object"
-      m = id_or_object[chrome.i18n.getUILanguage()]
-      return m if m
-      id_or_object["en"]
-    else
-      undefined
+I18n = (id, args...) ->
+  m = chrome.i18n.getMessage(id, [args...])
+  return escapeHtml(m) if m != ""
+  m = id.replace(/(.)([A-Z][a-z])/g, (m, p1, p2) -> "#{p1} #{p2.toLowerCase()}")
+  m = m.replace(/_/g, ' ')
+  escapeHtml(m)
+
+I18nS = (dict) ->
+  return dict if typeof dict == "string"
+  dict[chrome.i18n.getUILanguage()] or dict["en"]

 $(->
   pat = /__MSG_([A-Za-z0-9]+)__/


only encode the query string if it isn't already encoded

--- src/share_utils.coffee
+++ src/share_utils.coffee
@@ -65,6 +65,9 @@
   is_encoded: (str) ->
     decodeURIComponent(str) isnt str

+  encode: (str)->
+    if @is_encoded(str) then str else encodeURIComponent(str)
+
   popup: (url, params = {}) ->
     popup =
       width:  500
@@ -73,7 +76,7 @@
     popup.top  = (screen.height/2) - (popup.height/2)
     popup.left = (screen.width/2)  - (popup.width/2)

-    qs = ("#{k}=#{encodeURIComponent(v)}" for k, v of params).join('&')
+    qs = ("#{k}=#{@encode(v)}" for k, v of params).join('&')
     if qs then qs = "?#{qs}"

     window.open(url+qs, 'targetWindow', "toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,left=#{popup.left},top=#{popup.top},width=#{popup.width},height=#{popup.height}")


log search: display timestamp for each search result.

--- scripts/logging.coffee
+++ scripts/logging.coffee
@@ -36,6 +36,23 @@
           else
             buf = "#{items.length} matches:\n"
             for m in items
-              buf += "#{m["user"]}: #{m["text"]}\n"
+              date = m["time"]
+              month = date.getMonth()
+              day = date.getDate()
+              hour = date.getHours()
+              min = date.getMinutes()
+              sec = date.getSeconds()
+              if month < 10
+                month = "0" + month
+              if day < 10
+                day = "0" + day
+              if hour < 10
+                hour = "0" + hour
+              if min < 10
+                min = "0" + min
+              if sec < 10
+                sec = "0" + sec
+              date_str = "#{date.getFullYear()}-#{month}-#{day} #{hour}:#{min}:#{sec}"
+              buf += "#{m["user"]}:#{date_str}: #{m["text"]}\n"
             msg.send buf
           db.close


githubのmasterが自動的にherokuにプッシュされるのかテスト

githubのmasterが自動的にherokuにプッシュされるのかテスト

--- scripts/lottery.coffee
+++ scripts/lottery.coffee
@@ -14,7 +14,6 @@
       for name in names
         user = robot.adapter.client.rtm.dataStore.getUserByName(name)
         if user? then targets.push user.real_name else targets.push name
-      console.log targets
       target_name = msg.random targets
     else
       if channel.getType() is "dm"


Fix linting

Fix linting

--- server/lib/indexer.coffee
+++ server/lib/indexer.coffee
@@ -80,7 +80,8 @@
                 if err
                     if err.type is 'NotFoundError'
                         log.warn """
-Last index sequence was not found. If no document is indexed yet, it's ok. In other cases, there is probably an error with your indexes.
+Last index sequence was not found. If no document is indexed yet, it's ok.
+In other cases, there is probably an error with the data-system indexer.
 """
                         callback()
                     else


Verify parameters when checking.

Verify parameters when checking.

--- server/publication.coffee
+++ server/publication.coffee
@@ -215,16 +215,19 @@
           library:
             _id: publication._id

-  verifyPublication: (id, samplesData) ->
-    throw new Meteor.Error 401, "User is not signed in." unless Meteor.personId()
+  verifyPublication: (publicationId, samplesData) ->
+    check publicationId, String
+    check samplesData, [Uint8Array]
+
+    throw new Meteor.Error 401, "User not signed in." unless Meteor.personId()

     publication = Publications.findOne
-      _id: id
+      _id: publicationId
       cached:
         $exists: true

-    throw new Meteor.Error 403, "No publication importing." unless publication
-    throw new Meteor.Error 403, "Number of samples does not match." unless samplesData?.length == NUMBER_OF_VERIFICATION_SAMPLES
+    throw new Meteor.Error 400, "Error verifying file. Please retry." unless publication
+    throw new Meteor.Error 400, "Invalid number of samples." unless samplesData?.length == NUMBER_OF_VERIFICATION_SAMPLES

     publicationFile = Storage.open publication.filename()
     serverSamples = publication._verificationSamples Meteor.personId()
@@ -232,20 +235,17 @@
     verified = _.every _.map serverSamples, (serverSample, index) ->
       clientSampleData = samplesData[index]
       serverSampleData = new Uint8Array publicationFile.slice serverSample.offset, serverSample.offset + serverSample.size
-      return _.isEqual clientSampleData, serverSampleData
-
-    if verified
-      # Samples were verified, so add it to person's library
-      Persons.update
-        '_id': Meteor.personId()
-      ,
-        $addToSet:
-          library:
-            _id: publication._id
-
-      return true
-    else
-      throw new Meteor.Error 403, "Verification failed."
+      _.isEqual clientSampleData, serverSampleData
+
+    throw new Meteor.Error 403, "Verification failed." unless verified
+
+    # Samples were verified, so add it to person's library
+    Persons.update
+      '_id': Meteor.personId()
+    ,
+      $addToSet:
+        library:
+          _id: publication._id

 Meteor.publish 'publications-by-author-slug', (slug) ->
   return unless slug


update deploy

update deploy

--- core/deploy.coffee
+++ core/deploy.coffee
@@ -100,6 +100,17 @@
                                                     callback()
                             else
                                 callback()
+
+initIgnoreProfile = (location,config,callback)->
+    if config.ignores and Array.isArray(config.ignores)
+        ignoreFileContent = ""
+        for ignore in config.ignores
+            ignoreFileContent = "#{ignoreFileContent}\n#{ignore}"
+        ignoreFile = getLocationFile(location,".gitignore")
+        Fs.writeFile ignoreFile,ignoreFileContent,callback
+    else
+        callback()
+

 initLocationProfile = (location,config,callback)->
     if config.profile
@@ -198,7 +209,9 @@
                                         initLocationProfile location,config,(err)=>
                                             return callback(err) if err
                                             initLocationDataCopy @mew,location,config,(err)=>
-                                                callback(err)
+                                                return callback(err) if err
+                                                initIgnoreProfile location,config,(err)=>
+                                                    callback(err)
                         else
                             callback("location is not a directory")
         else


Adds hierachical property support to leafy

--- src/base/leafy.coffee
+++ src/base/leafy.coffee
@@ -7,11 +7,36 @@

 module.exports = T.object {
+  proto: () ->
+    THIS   = this
+    result = Object.create Object.prototype, T({
+      installProperty: (name) ->
+        descr = {
+          enumerable: true
+          configurable: false
+          get: () ->
+            here = this
+            while here
+              log.info "here #{here}"
+              val  = here._values[name]
+              log.info "val #{val}"
+              return val if val
+              here = here.parent
+            undefined
+          set: (newValue) ->
+            this._values[name] = newValue
+        }
+        Object.defineProperty this.proto, name, descr
+    })
+    result
+
   trait: (parent, childs) ->
     THIS   = this
+    proto  = if parent then parent.proto else THIS.proto()
     result = T {
       _childs: objset.create childs
       _parent: parent
+      _values: {}

       isBelow: (other) ->
         p = this.parent
@@ -23,10 +48,21 @@
       hasChild: (c) ->
         this._childs.has(c)

+      newRoot: () ->
+        trait = THIS.trait(null)
+        leaf  = Object.create this.proto, trait
+        leaf
+
       newChild: () ->
-        child = Object.create Object.prototype, THIS.trait(this)
-        this._childs.add child
-        child
+        trait = THIS.trait(this)
+        leaf  = Object.create this.proto, trait
+        this._childs.add leaf
+        leaf
+    }
+    result.proto   = {
+      enumerable: false,
+      get: () ->
+        proto
     }
     result.root    = {
       enumerable: false
@@ -50,7 +86,9 @@
     result.childs = { enumerable: false, get: () -> this._childs.elems }
     result

-  create: (childs = []) -> Object.create Object.prototype, this.trait(null, childs)
+  create: (childs = []) ->
+    trait = this.trait(null, childs)
+    Object.create trait.proto.get(), trait
 }


add selector for convert to html

add selector for convert to html

--- src/base_model.coffee
+++ src/base_model.coffee
@@ -320,9 +320,9 @@

   # Generate a new model instance from form
   # @note not support a relations
-  @from_html: () ->
+  @from_html: (selector) ->
     #FIXME
-    form_name = @.form_name || @.name.replace(/([A-Z])/g, '_$1').replace(/^_/,"").toLowerCase()
-
-    @.from_json(SiriusApplication.adapter.form_to_json("form[name='#{form_name}'"))
-
+    form_name = selector || @.form_name || @.name.replace(/([A-Z])/g, '_$1').replace(/^_/,"").toLowerCase()
+
+    @.from_json(SiriusApplication.adapter.form_to_json(form_name))
+


Inspect the error loudly

Inspect the error loudly

--- scripts/twitter.coffee
+++ scripts/twitter.coffee
@@ -1,4 +1,5 @@
 t = require '../lib/twitter'
+util = require 'util'

 module.exports = (robot) ->
   robot.respond /(?:retweet|rt|retwat)\s+(.+$)/i, (msg) ->
@@ -10,7 +11,7 @@

     return msg.reply "That's not a fucking tweet, moron" if not tid
     t.client.post "statuses/retweet/#{tid}", {}, (err, tweet, response) ->
-      return msg.reply "No, because #{err}" if err
+      return msg.reply "No, because #{util.inspect err}" if err
       msg.reply "Fine, did it."


weather script

weather script

--- scripts/weather.coffee
+++ scripts/weather.coffee
@@ -6,7 +6,8 @@

 module.exports = (robot) ->
   robot.respond /weather (.*)/i, (msg) ->
-    location = escape(msg.match[1]) or "Kansas City"
+    location = escape(msg.match[1]) or 'Kansas City'
+    console.log(location)
     locationOptions =
       uri: 'http://maps.googleapis.com/maps/api/geocode/json'
       qs:


Pass test with naked SVG tag

Pass test with naked SVG tag

--- test/spec/test_reactive.coffee
+++ test/spec/test_reactive.coffee
@@ -98,7 +98,7 @@
       console.log(child)
       console.log(child.is)
       expect(cont.length).toBe(1)
-      expect(cont.last().is('button')).toBe(true)
+      expect(cont.last().is('animatetransform')).toBe(true)
       expect(cont.last().text()).toBe('click me')

   describe 'attribute id and class parsing', ->


Removed event suspenders, cleanup

Removed event suspenders, cleanup

--- muleview/app/controller/KeysTree.coffee
+++ muleview/app/controller/KeysTree.coffee
@@ -57,28 +57,20 @@
   # Event handling:

   setMultiMode: (multi) ->
-    console.log("SETMULTIMODE: ", multi)
     return if !!multi == @isMulti
     @isMulti = !!multi

     # Find current selection
     selectedNode = @getSelectedNode()

-    @store.suspendEvents(false) #TODO: check
-
     @forAllNodes (node) ->
       checked = if multi then (node == selectedNode) else null
-      node.suspendEvents(false) #TODO: check
       node.set("checked", checked)
-      node.resumeEvents(true) #TODO: check
-
-    @store.resumeEvents() #TODO: check

     @getMultiModeBtn().setVisible(!multi)
     @getNormalModeBtn().setVisible(multi)

   createViewChangeEvent: ->
-    console.log("CREATE VIEW CHANGE")
     chosenKeys = []
     if @isMulti
       # keys are chosen by their checked value:
@@ -89,27 +81,21 @@
     Muleview.event "viewChange", chosenKeys , Muleview.currentRetention

   receiveViewChangeEvent: (keys) ->
-    console.log("RECEIVE VIEW CHANGE EVENT - ", keys)
     keysArr = Ext.Array.from(keys)
     @setMultiMode(@isMulti or keysArr.length > 1)
-
-    @store.suspendEvents(false) #TODO: check
-    @tree.suspendEvents(false) #TODO: check

     if @isMulti
       @tree.getSelectionModel().deselectAll()
       @forAllNodes (node) =>
         checked = Ext.Array.contains(keysArr, node.get("fullname"))
-        node.suspendEvents(false) #TODO: check
         node.set("checked", checked)
-        node.resumeEvents() #TODO: check
+        @expandKey(node) if checked

     else
       chosenNode = @store.getById(keysArr[0])
-      @tree.getSelectionModel().select(chosenNode)
-
-    @store.resumeEvents() #TODO: check
-    @tree.resumeEvents() #TODO: check
+      if chosenNode
+        @tree.getSelectionModel().select(chosenNode)
+        @expandKey(chosenNode)

   # ================================================================
   # Data fetching and displaying:


removed console.log call

removed console.log call

--- test/test-chain-builder.coffee
+++ test/test-chain-builder.coffee
@@ -113,7 +113,6 @@
       it 'should return an noop function', ->
         fn = builder.chain []
         noop = /function\s?\(\)\s?{\n\s*return true;\n\s*}/
-        console.log 'noop:',fn
         assert.equal noop.test(''+fn), true

     describe 'test empty pipeline', ->


Send time units to CODAP

Send time units to CODAP

Note that if you change time units, you will not see this in the table
until a new table is opened.

[#112305963]

--- src/code/models/codap-connect.coffee
+++ src/code/models/codap-connect.coffee
@@ -1,6 +1,8 @@
 IframePhoneRpcEndpoint = (require 'iframe-phone').IframePhoneRpcEndpoint
 tr = require '../utils/translate'
-CodapActions = require '../actions/codap-actions'
+CodapActions    = require '../actions/codap-actions'
+SimulationStore = require '../stores/simulation-store'
+TimeUnits       = require '../utils/time-units'
 module.exports = class CodapConnect

   name: 'Building Models Tool'
@@ -24,7 +26,6 @@
     GraphStore = require '../stores/graph-store'
     @graphStore = GraphStore.store

-    SimulationStore = require '../stores/simulation-store'
     SimulationStore.actions.simulationStarted.listen       @_openNewCase.bind(@)
     SimulationStore.actions.simulationFramesCreated.listen @_sendSimulationData.bind(@)

@@ -59,9 +60,10 @@
     @currentCaseID = null

     # First column definition is the time index
+    timeUnit = TimeUnits.toString SimulationStore.store.settings.stepUnits, true
     sampleDataAttrs = [
       {
-        name: "time"
+        name: timeUnit
         type: "numeric"
       }
     ]


take out adding and removing event listeners on updates

--- src/components/bind-store-mixin.cjsx
+++ src/components/bind-store-mixin.cjsx
@@ -28,9 +28,3 @@

   componentWillMount:   -> @_addListener()
   componentWillUnmount: -> @_removeListener()
-
-  # TODO do we deprecate these?
-  # I imagine we don't need these now
-  # The following fixs an invariant violation when switching screens
-  componentDidUpdate:   -> @_addListener()
-  componentWillUpdate:  -> @_removeListener()


Fix for stray `class=`

Fix for stray `class=`

--- internal_packages/account-sidebar/lib/account-sidebar.cjsx
+++ internal_packages/account-sidebar/lib/account-sidebar.cjsx
@@ -21,7 +21,7 @@
     @unsubscribe() if @unsubscribe

   render: ->
-

+

         {@_sections()}


Final level name

Final level name

--- games/tutorial.coffee
+++ games/tutorial.coffee
@@ -273,7 +273,7 @@
       ]
     }
     {
-      name: 'Create and Share Your Own Level'
+      name: 'Create, Test, and Share Your Own Level'
       comment: '
       Encourage students to express themselves.
       Track the solution and use that for the new goal metrics.


Wrap long error output correctly

Wrap long error output correctly

Adds "overflow-x: auto;" style to messages generated by safely.coffee

--- bokehjs/src/coffee/safely.coffee
+++ bokehjs/src/coffee/safely.coffee
@@ -38,6 +38,7 @@
   # Make message
   message = document.createElement("pre")
   message.style["white-space"] = "unset"
+  message.style["overflow-x"] = "auto"
   message.appendChild(document.createTextNode(error.message ? error))

   # Add pieces to box


Update sync token only at the end of a run.

--- server/packages.coffee
+++ server/packages.coffee
@@ -21,12 +21,6 @@
   loop
     syncToken = SyncToken.findOne().syncToken
     result = connection.call 'syncNewPackageData', syncToken
-
-    SyncToken.update
-      _id: 'syncToken'
-    ,
-      $set:
-        syncToken: result.syncToken

     if result.resetData
       Packages.remove {}
@@ -106,6 +100,12 @@
     console.log "Builds - all: #{Builds.find().count()}, new: #{newBuilds}, updated: #{updatedBuilds}" if newBuilds or updatedBuilds
     console.log "ReleaseTracks - all: #{ReleaseTracks.find().count()}, new: #{newReleaseTracks}, updated: #{updatedReleaseTracks}" if newReleaseTracks or updatedReleaseTracks
     console.log "ReleaseVersions - all: #{ReleaseVersions.find().count()}, new: #{newReleaseVersions}, updated: #{updatedReleaseVersions}" if newReleaseVersions or updatedReleaseVersions
+
+    SyncToken.update
+      _id: 'syncToken'
+    ,
+      $set:
+        syncToken: result.syncToken

     return if result.upToDate


Deprecate usesSoftTabs

Deprecate usesSoftTabs

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -915,13 +915,9 @@
         return row unless @isRowBlank(row)
     null

-  # Public: Determine if the buffer uses soft tabs.
-  #
-  # Returns `true` if the first line with leading whitespace starts with a
-  # space character. Returns `false` if it starts with a hard tab (`\t`).
-  #
-  # Returns a {Boolean},
+  # Deprecate
   usesSoftTabs: ->
+    Grim.deprecate("Use Editor::usesSoftTabs instead. TextBuffer doesn't have enough context to determine this.")
     for row in [0..@getLastRow()]
       if match = @lineForRow(row).match(/^\s/)
         return match[0][0] != '\t'


:art: Use prettier emptiness check

:art: Use prettier emptiness check

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -132,7 +132,7 @@
   subscribeToBuffer: ->
     @buffer.retain()
     @subscribe @buffer.onDidChangePath =>
-      unless atom.project.getPaths()[0]?
+      unless atom.project.getPaths().length > 0
         atom.project.setPaths([path.dirname(@getPath())])
       @emit "title-changed"
       @emitter.emit 'did-change-title', @getTitle()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/text_screen.coffee
+++ src/text_screen.coffee
@@ -24,8 +24,8 @@
     </pre></code>

     @name centerText
-    @methodOf TextScr#
-    @param {GameObject} obj The object you want to follow
+    @methodOf TextScreen#
+    @param {GameObject} canvas The object you want to follow
     ###
     centerText: (canvas, text, options={}) ->
       font = options.font || I.font


Fix broken level 2

Fix broken level 2

--- games/tutorial.coffee
+++ games/tutorial.coffee
@@ -52,7 +52,7 @@
       bricks: 2
       sprites: [
         { kind: 'gate', actions: {interrupt: ['_victory']}, position: [5,0] }
-        { kind: 'turtle', actions: {run: ['forward', 'right']}, editable: false }
+        { kind: 'turtle', actions: {run: ['forward', 'right']} }
       ]
     }
     # TODO: add a level explaining how/why to rearrange bricks


cleanup tutorial doc

cleanup tutorial doc

--- games/tutorial.coffee
+++ games/tutorial.coffee
@@ -1,15 +1,27 @@
+#
+# tutorial.coffee
+#
+# The Primary Hour of Node Tutorial
+#
+# Role: explain how to use the interface to program turtles to reach the exit
+#
+# Key Interface Elements:
+# * run button
+# * action bricks (forward, right, left)
+# * run program (run loop)
+# * bump program (interrupt handler)
+
+# TODO:
 # Gate by default should send _victory action when interrupted
 # Turtle defaults to reverse on interrupt, to avoid invisible loop
-
-# TODO:
 # Implicitly add: assume, per-level turtle and gate (if absent)
 # Check and send 'done -1' if infinite loop (e.g., no interrupt)

 # COMMENT
 # Scores for each level are based on analogs of real-world metrics
-# - clicks (programmer hours)
-# - bricks (lines of code)
-# - ticks (execution time)
+# - Clicks (programmer hours)
+# - Bricks (lines of code)
+# - Ticks (execution time)
 # [with apologies to Dr. Seuss and "Fox in Socks"]
 #
 # Advanced players seeking to optimize their score will need to make
@@ -19,7 +31,7 @@
 exports.game = {
   name: 'The Hour of NODE'
   assume: 'baseline'
-  comment: "The world's first Hour of NODE game"
+  comment: "The original Hour of NODE tutorial"
   author: {name: 'David Huffman', url: 'mailto:david%40theswanfactory.com'}
   license: {
     name: 'Creative Commons Attribution 4.0 International'


成功なのか確認

成功なのか確認

--- scripts/line-beacon-listener.coffee
+++ scripts/line-beacon-listener.coffee
@@ -7,6 +7,6 @@

     data = if req.body.payload? then JSON.parse req.body.payload else req.body
     robot.logger.error data
-    robot.logger.error data.events[0].type
-    robot.send {room: "C4LEAQHPW"}, "ビーコンに反応あり"
+    robot.logger.error data.events[0].beacon.type
+    robot.send {room: "C4LEAQHPW"}, "ビーコンに反応あり 反応の種類#{data.events[0].beacon.type}"
     res.end ""


EmailInput: Fixed regexp.

EmailInput: Fixed regexp.

--- src/elements/Input/EmailInput.coffee
+++ src/elements/Input/EmailInput.coffee
@@ -20,6 +20,6 @@
 		else
 			return false

-	@regexp: new RegExp("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$", "i")
+	@regexp: new RegExp("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", "i")

 EmailInput = CUI.EmailInput


stop grouping same user message after a few time

--- src/frontend/modules/textChat.coffee
+++ src/frontend/modules/textChat.coffee
@@ -74,7 +74,7 @@
     newMessageElem : (message) =>
         d             = new Date
         jqNewMetadata = ($ '<div>', { class : 'chatmetadata' })
-        jqNewAuthor   = ($ '<span>', { class : 'fontlightblue' }).text message.from
+        jqNewAuthor   = ($ '<span>', { class : 'fontlightblue', rel : do d.getTime }).text message.from
         jqNewTime     = ($ '<span>', { class : 'time' }).text ' at ' + ((do d.toTimeString).substr 0, 5)
         (jqNewMetadata.append jqNewAuthor).append jqNewTime
         jqNewMessage  = ($ '<div>', { class : 'chatmessage' }).html message.text
@@ -85,16 +85,19 @@
         message.text  = @markdown.makeHtml message.text
         jqLastMessage = do (@jqMessageBox.find 'li').last
         if jqLastMessage[0]?
+            d          = new Date
             lastAuthor = do ((jqLastMessage.children '.chatmetadata').children 'span').first
-            if do lastAuthor.text is message.from
+            diffTime   = do d.getTime - lastAuthor.attr 'rel'
+            if do lastAuthor.text is message.from and diffTime < 30000
                 (jqLastMessage.children '.chatmessage').append ($ '<br>')
                 (jqLastMessage.children '.chatmessage').append message.text
+                lastAuthor.attr 'rel', do d.getTime
             else
                 @newMessageElem message
         else
             @newMessageElem message
         do @scrollPane.reinitialise
-        #@scrollPane.scrollToPercentY 100, no
+        @scrollPane.scrollToPercentY 100, no

 if window?
     window.TextChat     = TextChat


fix createAccount bug

fix createAccount bug

--- server/methods/createAccount.coffee
+++ server/methods/createAccount.coffee
@@ -18,7 +18,7 @@

 				project = Projects.findOne projectId, fields: 'tags._id': 1
 				for tag in project.tags
-					Meteor.call 'changeTagRole', tagId, userId, 'participant'
+					Meteor.call 'changeTagRole', tag._id, userId, 'participant'

 				Meteor.users.update userId, $set: state: 'created'

@@ -38,11 +38,11 @@
 		for existingUser in existingUsers
 			user = Meteor.users.find({'profile.email': existingUser.email}, {fields: _id: 1}).fetch()[0]

-			if not Roles.userIsInRole user._id, ['admin', 'shiftAdmin', 'depotAdmin', 'member'], projectId
+			if !Roles.userIsInRole user._id, Permissions.member, projectId
 				Meteor.call 'changeProjectRole', projectId, user._id, 'member'

 				project = Projects.findOne projectId, fields: 'tags._id': 1
 				for tag in project.tags
-					Meteor.call 'changeTagRole', tagId, user._id, 'participant'
+					Meteor.call 'changeTagRole', tag._id, user._id, 'participant'

 				Meteor.call 'sendJoinProject', projectId, user._id


Mark a model as a draft if it has no version

--- src/scripts/models/contents/node.coffee
+++ src/scripts/models/contents/node.coffee
@@ -179,6 +179,8 @@

     isBook: () -> @get('mediaType') is 'application/vnd.org.cnx.collection'

-    isDraft: () -> @get('version') is 'draft' or /@draft$/.test(@id)
+    isDraft: () ->
+      version = @get('version')
+      return version is 'draft' or /@draft$/.test(@id) or not version

     isSaveable: () -> !!@get('mediaType')


feat(snippet): add course snippet

feat(snippet): add course snippet

--- snippets/language-coursescript.cson
+++ snippets/language-coursescript.cson
@@ -601,7 +601,7 @@
         coordinate: x=$10,y=$11,w=$12,h=$13
         coordinate: x=$14,y=$15,w=$16,h=$17
     """
-   'MCQx':
+  'MCQx':
     'prefix': 'MCQx'
     'body': """
       [TYPE MCQx]
@@ -614,14 +614,14 @@
       Text: $6
       Text: $7
       """
-   'explanation':
+  'explanation':
     'prefix': 'explanation'
     'body': """
       \"""
       explanation: "$1"
       \"""
     """
-   'PreTextTeaching':
+  'PreTextTeaching':
     'prefix': 'pretextteaching'
     'body':"""
       [TYPE PreTextTeaching]
@@ -636,3 +636,335 @@
         - $7
       \"""
     """
+  'SentenceQuiz':
+    'prefix': 'SentenceQuiz'
+    'body': """
+      [TYPE SentenceQuiz]
+      ID: $1
+      Audio: $2
+      \"""
+      category: $3
+      \"""
+    """
+  'WordQuiz':
+    'prefix': 'WordQuiz'
+    'body': """
+      [TYPE WordQuiz]
+      ID: $1
+      Audio: $2
+      Text: $3
+      \"""
+      category: $4
+      \"""
+    """
+  'TeachingVideo':
+    'prefix': 'TeachingVideo'
+    'body': """
+      [TYPE TeachingVideo]
+      ID: $1
+      Video: $2
+      VideoClip: $3
+      VideoClip: $4
+      VideoClip: $5
+      VideoClip: $6
+      VideoClip: $7
+      \"""
+      category: $8
+      \"""
+    """
+  'PhonemePractice':
+    'prefix': 'PhonemePractice'
+    'body': """
+      [TYPE PhonemePractice]
+      ID: $1
+      Video: $2
+      Audio: $3
+      Text: $4
+      Text: $5
+      \"""
+      category: $6
+      \"""
+    """
+  'WordPronoun':
+    'prefix': 'WordPronoun'
+    'body': """
+      [TYPE WordPronoun]
+      ID: $1
+      Audio: $2
+      Text: $3
+      \"""
+      category: $4
+      \"""
+      """
+  'SentencePronoun':
+    'prefix': 'SentencePronoun'
+    'body': """
+      [TYPE SentencePronoun]
+      ID: $1
+      Audio: $2
+      """
+  'MPListeningPractice':
+    'prefix': 'MPListeningPractice'
+    'body': """
+      [TYPE MPListeningPractice]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Audio: $4
+      Text: $5
+      \"""
+      category: $6
+      tag: $7
+      \"""
+      """
+  'MPTeachingVideo':
+    'prefix': 'MPTeachingVideo'
+    'body': """
+      [TYPE MPTeachingVideo]
+      ID: $1
+      Video: $2
+      Audio: $3
+      Audio: $4
+      Audio: $5
+      \"""
+      category: $6
+      tag: $7
+      \"""
+      """
+  'MPPronounPractice':
+    'prefix': 'MPPronounPractice'
+    'body': """
+      [TYPE MPPronounPractice]
+      ID: $1
+      Audio: $2
+      Text: $3
+      \"""
+      category: $4
+      tag: $5
+      \"""
+      """
+  'RimePronoun':
+    'prefix': 'RimePronoun'
+    'body': """
+      [TYPE RimePronoun]
+      ID: $1
+      Audio: $2
+      \"""
+      category: $3
+      tag: $4
+      \"""
+      """
+  'ConsonantPractice':
+    'prefix': 'ConsonantPractice'
+    'body': """
+      [TYPE ConsonantPractice]
+      ID: $1
+      Video: $2
+      Audio: $3
+      Text: $4
+      Text: $5
+      """
+  'SyllablePractice':
+    'prefix': 'SyllablePractice'
+    'body': """
+      [TYPE SyllablePractice]
+      ID: $1
+      Audio: $2
+      Text: $3
+      \"""
+      category: $4
+      \"""
+      """
+  'SyllableStress':
+    'prefix': 'SyllableStress'
+    'body': """
+      [TYPE SyllableStress]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      \"""
+      category: $5
+      vocab: $6
+      - word: $7
+        desc: $8
+        pos: $9
+      \"""
+      """
+  'RhythmInGroup':
+    'prefix': 'RhythmInGroup'
+    'body': """
+      [TYPE RhythmInGroup]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      \"""
+      category: $5
+      \"""
+      """
+  'RhythmInGroupS':
+    'prefix': 'RhythmInGroupS'
+    'body': """
+      [TYPE RhythmInGroupS]
+      ID: $1
+      Audio: $2
+      Text: $3
+      QuestionPart:
+      Audio: $4
+      Text: $5
+      Text: $6
+      QuestionPart:
+      Audio: $7
+      Text: $8
+      Text: $9
+      Text: $10
+      \"""
+      category: $11
+      \"""
+      """
+  'WordIntonation':
+    'prefix': 'WordIntonation'
+    'body': """
+      [TYPE WordIntonation]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      """
+  'IntonationInGroup':
+    'prefix': 'IntonationInGroup'
+    'body': """
+      [TYPE IntonationInGroup]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      """
+  'IntonationInGroupS':
+    'prefix': 'IntonationInGroupS'
+    'body': """
+      [TYPE IntonationInGroupS]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      QuestionPart:
+      Audio: $5
+      Text: $6
+      QuestionPart:
+      Audio: $7
+      Text: $8
+      QuestionPart:
+      Audio: $9
+      Text: $10
+      QuestionPart:
+      Audio: $11
+      Text: $12
+      """
+  'LinkingCVDF1':
+    'prefix': 'LinkingCVDF1'
+    'body': """
+      [TYPE LinkingCV]
+      ID: $1
+      DF1:
+      Audio: $2
+      Text: $3
+      Text: $4
+      Text: $5
+      """
+  'LinkingCVDF2':
+    'prefix': 'LinkingCVDF2'
+    'body': """
+      [TYPE LinkingCV]
+      ID: $1
+      DF2:
+      Audio: $2
+      Text: $3
+      Text: $4
+      Text: $5
+      """
+  'LinkingCVDF3':
+    'prefix': 'LinkingCVDF3'
+    'body': """
+      [TYPE LinkingCV]
+      ID: $1
+      DF3:
+      Audio: $2
+      Text: $3
+      Text: $4
+      Text: $5
+      """
+  'LossOfPlosion':
+    'prefix': 'LossOfPlosion'
+    'body': """
+      [TYPE LossOfPlosion]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      """
+  'ReducingPronoun':
+    'prefix': 'ReducingPronoun'
+    'body': """
+      [TYPE ReducingPronoun]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      Text: $5
+      """
+  'ConsecutiveLinkings':
+    'prefix': 'ConsecutiveLinkings'
+    'body': """
+      [TYPE ConsecutiveLinkings]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      Text: $5
+      Text: $6
+      """
+  'MultipleLinkings':
+    'prefix': 'MultipleLinkings'
+    'body': """
+      [TYPE MultipleLinkings]
+      ID: $1
+      Audio: $2
+      Text: $3
+      QuestionPart:
+      Audio: $4
+      Text: $5
+      Text: $6
+      Text: $7
+      Text: $8
+      QuestionPart:
+      Audio: $9
+      Text: $10
+      Text: $11
+      QuestionPart:
+      Audio: $12
+      Text: $13
+      Text: $14
+      Text: $15
+      """
+  'T2A':
+    'prefix': 'T2A'
+    'body': """
+      [TYPE T2A]
+      ID: $1
+      Text: $2
+      Opts:
+      Audio: $3
+      Audio: $4
+      Audio: $5
+      """
+  'ORBell':
+    'prefix': 'ORBell'
+    'body': """
+      [TYPE ORBell]
+      ID: $1
+      Audio: $2
+      Text: $3
+      Text: $4
+      """