Course: Fix self closing assignment/folder adder

--- src/smc-webapp/course/common.cjsx
+++ src/smc-webapp/course/common.cjsx
@@ -9,6 +9,8 @@
 {Button, ButtonToolbar, ButtonGroup, Input, Row, Col} = require('react-bootstrap')

 {ErrorDisplay, Icon, Space, TimeAgo, Tip, SearchInput} = require('../r_misc')
+
+immutable = require('immutable')

 # Move these to funcs file
 exports.STEPS = (peer) ->
@@ -77,21 +79,6 @@
             return ' for peer grading'
         when 'peer_collect'
             return ' who should have peer graded it'
-
-# I don't think this is used anywhere...
-exports.DirectoryLink = rclass
-    displayName : "DirectoryLink"
-
-    propTypes :
-        project_id : rtypes.string.isRequired
-        path       : rtypes.string.isRequired
-        redux      : rtypes.object.isRequired
-
-    open_path : ->
-        @props.redux.getProjectActions(@props.project_id).open_directory(@props.path)
-
-    render : ->
-        e.preventDefault(); @open_path()}>{@props.path}

 exports.BigTime = BigTime = rclass
     displayName : "CourseEditor-BigTime"
@@ -374,31 +361,34 @@
 # Coupled with Assignments Panel and Handouts Panel
 exports.MultipleAddSearch = MultipleAddSearch = rclass
     propTypes :
-        add_selected     : rtypes.func.isRequired   # Submit user selected results
+        add_selected     : rtypes.func.isRequired   # Submit user selected results add_selected(['paths', 'of', 'folders'])
         do_search        : rtypes.func.isRequired   # Submit search query
+        clear_search     : rtypes.func.isRequired
         is_searching     : rtypes.bool.isRequired   # whether or not it is asking the backend for the result of a search
-        err              : rtypes.object            # Search error
-        search_results   : rtypes.array             # contents to put in the selection box after getting search result back
-        real_time        : rtypes.bool              # Controls whether add_selected is called on submit or on change
+        search_results   : rtypes.object #Immutable.List TODO: Turn into rtypes.immutable.List after react rewrite           # contents to put in the selection box after getting search result back
         item_name        : rtypes.string

     getDefaultProps : ->
-        real_time        : false
         item_name        : 'result'

     getInitialState : ->
         selected_items : '' # currently selected options
         show_selector : false

+    shouldComponentUpdate : (newProps, newState) ->
+        return newProps.search_results != @props.search_results or
+            newProps.item_name != @props.item_name or
+            newState.selected_items != @state.selected_items
+
     componentWillReceiveProps : (newProps) ->
         @setState
-            show_selector : newProps.search_results? and newProps.search_results != @props.search_results
+            show_selector : newProps.search_results? and newProps.search_results.size > 0

     clear_and_focus_search_input : ->
-        @setState(show_selector : false)
+        @props.clear_search()
+        @setState(selected_items:'')
         @refs.search_input.clear_and_focus_search_input()

-    # TODO: Check if this works
     search_button : ->
         if @props.is_searching
             # Currently doing a search, so show a spinner
@@ -422,8 +412,10 @@
         @clear_and_focus_search_input()

     render_results_list : ->
-        for item in @props.search_results
-            <option key={item} value={item} label={item}>{item}</option>
+        v = []
+        @props.search_results.map (item) =>
+            v.push(<option key={item} value={item} label={item}>{item}</option>)
+        return v

     render_add_selector : ->

@@ -439,15 +431,15 @@

     render_add_selector_button : ->
-        num_items_selected = @state.selected_items?.length ? 0
-        btn_text = switch @props.search_results.length
+        num_items_selected = @state.selected_items.length ? 0
+        btn_text = switch @props.search_results.size
             when 0 then "No #{@props.item_name} found"
             when 1 then "Add #{@props.item_name}"
             else switch num_items_selected
                 when 0 then "Select #{@props.item_name} above"
                 when 1 then "Add selected #{@props.item_name}"
                 else "Add #{num_items_selected} #{@props.item_name}s"
-        disabled = @props.search_results.length == 0 or (@props.search_results.length >= 2 and num_items_selected == 0)
+        disabled = @props.search_results.size == 0 or (@props.search_results.size >= 2 and num_items_selected == 0)
         <Button disabled={disabled} onClick={@add_button_clicked}><Icon name="plus" /> {btn_text}</Button>

     render : ->
@@ -484,6 +476,7 @@

     getInitialState : ->
         add_is_searching : false
+        add_search_results : immutable.List([])

     do_add_search : (search) ->
         if @state.add_is_searching
@@ -497,7 +490,11 @@
                     @setState(add_is_searching:false, err:err, add_search_results:undefined)
                 else
                     filtered_results = @filter_results(resp.directories, search, @props.items)
-                    @setState(add_is_searching:false, add_search_results:filtered_results)
+                    if filtered_results.length == @state.add_search_results.size
+                        merged = @state.add_search_results.merge(filtered_results)
+                    else
+                        merged = immutable.List(filtered_results)
+                    @setState(add_is_searching:false, add_search_results:merged)

     # Filter directories based on contents of all_items
     filter_results : (directories, search, all_items) ->
@@ -521,6 +518,13 @@
             directories = directories.filter (x) => not should_omit(x)
             directories.sort()
         return directories
+
+    submit_selected : (path_list) ->
+        @props.add_folders(path_list)
+        @clear_add_search()
+
+    clear_add_search : ->
+        @setState(add_search_results:immutable.List([]))

     render : ->
         <Row>
@@ -536,8 +540,9 @@
             </Col>
             <Col md=5>
                 <MultipleAddSearch
-                    add_selected   = {@props.add_folders}
+                    add_selected   = {@submit_selected}
                     do_search      = {@do_add_search}
+                    clear_search   = {@clear_add_search}
                     is_searching   = {@state.add_is_searching}
                     item_name      = {@props.item_name}
                     err            = {undefined}


redux: rewrite course editor

redux: rewrite course editor

--- src/smc-webapp/editor_course.cjsx
+++ src/smc-webapp/editor_course.cjsx
@@ -50,7 +50,7 @@
 schema = require('smc-util/schema')

 # React libraries
-{React, ReactDOM, rclass, rtypes, FluxComponent, Actions, Store}  = require('./r')
+{React, ReactDOM, rclass, rtypes, Redux, Actions, Store}  = require('./smc-react')

 {Alert, Button, ButtonToolbar, ButtonGroup, Input, Row, Col,
     Panel, Popover, Tabs, Tab, Well} = require('react-bootstrap')
@@ -64,7 +64,7 @@

 PARALLEL_LIMIT = 3  # number of async things to do in parallel

-flux_name = (project_id, course_filename) ->
+redux_name = (project_id, course_filename) ->
     return "editor-#{project_id}-#{course_filename}"

 primary_key =
@@ -115,21 +115,18 @@
             return ' whose work you have graded'

 syncdbs = {}
-exports.init_flux = init_flux = (flux, course_project_id, course_filename) ->
-    the_flux_name = flux_name(course_project_id, course_filename)
-    get_actions = ->flux.getActions(the_flux_name)
-    get_store = -> flux.getStore(the_flux_name)
+exports.init_redux = init_redux = (redux, course_project_id, course_filename) ->
+    the_redux_name = redux_name(course_project_id, course_filename)
+    get_actions = ->redux.getActions(the_redux_name)
+    get_store = -> redux.getStore(the_redux_name)
     if get_actions()?
         # already initalized
         return
     syncdb = undefined

-    user_store = flux.getStore('users')
+    user_store = redux.getStore('users')
     class CourseActions extends Actions
         # INTERNAL API
-        _set_to: (payload) =>
-            payload
-
         _loaded: =>
             if not syncdb?
                 @set_error("attempt to set syncdb before loading")
@@ -139,7 +136,7 @@
         _store_is_initialized: =>
             store = get_store()
             return if not store?
-            if not (store.state.students? and store.state.assignments? and store.state.settings?)
+            if not (store.get('students')? and store.get('assignments')? and store.get('settings')?)
                 @set_error("store must be initialized")
                 return false
             return true
@@ -150,55 +147,60 @@
             @save()

         set_tab: (tab) =>
-            @_set_to(tab:tab)
+            @setState(tab:tab)

         save: () =>
             store = get_store()
             return if not store?  # e.g., if the course store object already gone due to closing course.
-            if store.state.saving
+            if store.get('saving')
                 return # already saving
             id = @set_activity(desc:"Saving...")
-            @_set_to(saving:true)
+            @setState(saving:true)
             syncdb.save (err) =>
                 @clear_activity(id)
-                @_set_to(saving:false)
+                @setState(saving:false)
                 if err
                     @set_error("Error saving -- #{err}")
-                    @_set_to(show_save_button:true)
+                    @setState(show_save_button:true)
                 else
-                    @_set_to(show_save_button:false)
+                    @setState(show_save_button:false)

         _syncdb_change: (changes) =>
             store = get_store()
             return if not store?
-            t = misc.copy(store.state)
+            cur = t = store.getState()
+
             remove = (x.remove for x in changes when x.remove?)
             insert = (x.insert for x in changes when x.insert?)
             # first remove, then insert (or we could loose things!)
-            if not t[x.table]?
-                t[x.table] = immutable.Map()
+            if not t.get(x.table)?
+                t = t.set(x.table, immutable.Map())
             for x in remove
                 if x.table != 'settings'
                     y = misc.copy_without(x, 'table')
-                    t[x.table] = t[x.table].remove(x[primary_key[x.table]])
+                    a = t.get(x.table).delete(x[primary_key[x.table]])
+                    t = t.set(x.table, a)
+
             for x in insert
                 if x.table == 'settings'
+                    s = t.get('settings')
                     for k, v of misc.copy_without(x, 'table')
-                        t.settings = t.settings.set(k, immutable.fromJS(v))
+                        s = s.set(k, immutable.fromJS(v))
+                    t = s.set('settings', s)
                 else
                     y = immutable.fromJS(misc.copy_without(x, 'table'))
-                    t[x.table] = t[x.table].set(x[primary_key[x.table]], y)
-            for k, v of t
-                if not immutable.is(v, store.state[k])
-                    @_set_to("#{k}":v)
+                    a = t.get(x.table).set(x[primary_key[x.table]], y)
+                    t = t.set(x.table, a)
+            if cur != t  # something changed
+                @setState(t)

         # PUBLIC API

         set_error: (error) =>
             if error == ''
-                @_set_to(error:error)
+                @setState(error:error)
             else
-                @_set_to(error:((get_store()?.state.error ? '') + '\n' + error).trim())
+                @setState(error:((get_store()?.get('error') ? '') + '\n' + error).trim())

         set_activity: (opts) =>
             opts = defaults opts,
@@ -212,21 +214,21 @@
             store = get_store()
             if not store?  # course was closed
                 return
-            x = store.get_activity()
+            x = store.get_activity()?.toJS()
             if not x?
                 x = {}
             if not opts.desc?
                 delete x[opts.id]
             else
                 x[opts.id] = opts.desc
-            @_set_to(activity: x)
+            @setState(activity: x)
             return opts.id

         clear_activity: (id) =>
             if id?
                 @set_activity(id:id)  # clears for this id
             else
-                @_set_to(activity:{})
+                @setState(activity:{})

         # Settings
         set_title: (title) =>
@@ -327,7 +329,7 @@
         create_student_project: (student) =>
             store = get_store()
             return if not store?
-            if not store.state.students? or not store.state.settings?
+            if not store.get('students')? or not store.get('settings')?
                 @set_error("attempt to create when stores not yet initialized")
                 return
             if not @_create_student_project_queue?
@@ -349,11 +351,11 @@
             @_update(set:{create_project:new Date()}, where:{table:'students',student_id:student_id})
             id = @set_activity(desc:"Create project for #{store.get_student_name(student_id)}.")
             token = misc.uuid()
-            flux.getActions('projects').create_project
-                title       : store.state.settings.get('title')
-                description : store.state.settings.get('description')
+            redux.getActions('projects').create_project
+                title       : store.getIn(['settings', 'title'])
+                description : store.getIn(['settings', 'description'])
                 token       : token
-            flux.getStore('projects').wait_until_project_created token, 30, (err, project_id) =>
+            redux.getStore('projects').wait_until_project_created token, 30, (err, project_id) =>
                 @clear_activity(id)
                 if err
                     @set_error("error creating student project -- #{err}")
@@ -371,21 +373,21 @@
         configure_project_users: (student_project_id, student_id, do_not_invite_student_by_email) =>
             # Add student and all collaborators on this project to the project with given project_id.
             # users = who is currently a user of the student's project?
-            users = flux.getStore('projects').get_users(student_project_id)  # immutable.js map
+            users = redux.getStore('projects').get_users(student_project_id)  # immutable.js map
             # Define function to invite or add collaborator
             s = get_store()
             body = s.get_email_invite()
             invite = (x) ->
                 if '@' in x
                     if not do_not_invite_student_by_email
-                        title = s.state.settings.get('title')
+                        title = s.getIn(['settings', 'title'])
                         subject = "SageMathCloud Invitation to Course #{title}"
-                        name  = flux.getStore('account').get_fullname()
+                        name  = redux.getStore('account').get_fullname()
                         body  = body.replace(/{title}/g,title).replace(/{name}/g, name)
                         body = require('./markdown').markdown_to_html(body).s
-                        flux.getActions('projects').invite_collaborators_by_email(student_project_id, x, body, subject, true)
+                        redux.getActions('projects').invite_collaborators_by_email(student_project_id, x, body, subject, true)
                 else
-                    flux.getActions('projects').invite_collaborator(student_project_id, x)
+                    redux.getActions('projects').invite_collaborator(student_project_id, x)
             # Make sure the student is on the student's project:
             student = s.get_student(student_id)
             student_account_id = student.get('account_id')
@@ -394,7 +396,7 @@
             else if not users?.get(student_account_id)?   # users might not be set yet if project *just* created
                 invite(student_account_id)
             # Make sure all collaborators on course project are on the student's project:
-            target_users = flux.getStore('projects').get_users(course_project_id)
+            target_users = redux.getStore('projects').get_users(course_project_id)
             if not target_users?
                 return  # projects store isn't sufficiently initialized, so we can't do this yet...
             target_users.map (_, account_id) =>
@@ -402,25 +404,25 @@
                     invite(account_id)

         configure_project_visibility: (student_project_id) =>
-            users_of_student_project = flux.getStore('projects').get_users(student_project_id)
+            users_of_student_project = redux.getStore('projects').get_users(student_project_id)
             if not users_of_student_project?  # e.g., not defined in admin view mode
                 return
             # Make project not visible to any collaborator on the course project.
-            users = flux.getStore('projects').get_users(course_project_id)
+            users = redux.getStore('projects').get_users(course_project_id)
             if not users? # TODO: should really wait until users is defined, which is a supported thing to do on stores!
                 return
             users.map (_, account_id) =>
                 x = users_of_student_project.get(account_id)
                 if x? and not x.get('hide')
-                    flux.getActions('projects').set_project_hide(account_id, student_project_id, true)
+                    redux.getActions('projects').set_project_hide(account_id, student_project_id, true)

         configure_project_title: (student_project_id, student_id) =>
             store = get_store()
-            title = "#{store.get_student_name(student_id)} - #{store.state.settings.get('title')}"
-            flux.getActions('projects').set_project_title(student_project_id, title)
+            title = "#{store.get_student_name(student_id)} - #{store.getIn(['settings', 'title'])}"
+            redux.getActions('projects').set_project_title(student_project_id, title)

         set_all_student_project_titles: (title) =>
-            actions = flux.getActions('projects')
+            actions = redux.getActions('projects')
             get_store()?.get_students().map (student, student_id) =>
                 student_project_id = student.get('project_id')
                 project_title = "#{get_store().get_student_name(student_id)} - #{title}"
@@ -428,13 +430,13 @@
                     actions.set_project_title(student_project_id, project_title)

         configure_project_description: (student_project_id, student_id) =>
-            flux.getActions('projects').set_project_description(student_project_id, get_store().state.settings.get('description'))
+            redux.getActions('projects').set_project_description(student_project_id, get_store().get(['settings', 'description']))

         set_all_student_project_descriptions: (description) =>
             get_store()?.get_students().map (student, student_id) =>
                 student_project_id = student.get('project_id')
                 if student_project_id?
-                    flux.getActions('projects').set_project_description(student_project_id, description)
+                    redux.getActions('projects').set_project_description(student_project_id, description)

         configure_project: (student_id, do_not_invite_student_by_email) =>
             # Configure project for the given student so that it has the right title,
@@ -444,7 +446,7 @@
             # - Set the title to [Student name] + [course title] and description to course description.
             store = get_store()
             return if not store?
-            student_project_id = store.state.students?.get(student_id)?.get('project_id')
+            student_project_id = store.getIn(['students', student_id, 'project_id'])
             if not student_project_id?
                 @create_student_project(student_id)
             else
@@ -456,16 +458,16 @@
         delete_project: (student_id) =>
             store = get_store()
             return if not store?
-            student_project_id = store.state.students?.get(student_id)?.get('project_id')
+            student_project_id = store.getIn(['students', student_id, 'project_id'])
             if student_project_id?
-                flux.getActions('projects').delete_project(student_project_id)
+                redux.getActions('projects').delete_project(student_project_id)
                 @_update
                     set   : {create_project:undefined, project_id:undefined}
                     where : {table:'students', student_id:student_id}

         configure_all_projects: =>
             id = @set_activity(desc:"Configuring all projects")
-            @_set_to(configure_projects:'Configuring projects')
+            @setState(configure_projects:'Configuring projects')
             store = get_store()
             if not store?
                 @set_activity(id:id)
@@ -492,7 +494,7 @@
                 @set_activity(id:id)
                 return
             for project_id in store.get_student_project_ids()
-                flux.getActions('projects').apply_upgrades_to_project(project_id, upgrades)
+                redux.getActions('projects').apply_upgrades_to_project(project_id, upgrades)
             @set_activity(id:id)

         set_student_note: (student, note) =>
@@ -916,26 +918,20 @@
                 @set_error("no such project")
                 return
             # Now open it
-            flux.getProjectActions(proj).open_directory(path)
-
-    flux.createActions(the_flux_name, CourseActions)
+            redux.getProjectActions(proj).open_directory(path)
+
+    redux.createActions(the_redux_name, CourseActions)

     class CourseStore extends Store
-        constructor: (flux) ->
-            super()
-            ActionIds = flux.getActionIds(the_flux_name)
-            @register(ActionIds._set_to, @_set_to)
-            @state = {}
-
-        _set_to: (payload) => @setState(payload)
-
         get_email_invite: =>
             host = window.location.hostname
-            @state.settings?.get('email_invite') ? "We will use [SageMathCloud](https://#{host}) for the course *{title}*.  \n\nPlease sign up!\n\n--\n\n{name}"
-
-        get_activity: => @state.activity
-
-        get_students: => @state.students
+            @getIn(['settings', 'email_invite']) ? "We will use [SageMathCloud](https://#{host}) for the course *{title}*.  \n\nPlease sign up!\n\n--\n\n{name}"
+
+        get_activity: =>
+            @get('activity')
+
+        get_students: =>
+            @get('students')

         get_student_name: (student) =>
             student = @get_student(student)
@@ -946,27 +942,29 @@
         get_student_ids: (opts) =>
             opts = defaults opts,
                 deleted : false
-            if not @state.students?
+            if not @get('students')?
                 return
             v = []
-            @state.students.map (val, student_id) =>
+            @get('students').map (val, student_id) =>
                 if !!val.get('deleted') == opts.deleted
                     v.push(student_id)
             return v

         # return list of all non-deleted created student projects (or undefined if not loaded)
         get_student_project_ids: =>
-            if not @state.students?
+            if not @get('students')?
                 return
             v = []
-            @state.students.map (val, student_id) =>
+            @get('students').map (val, student_id) =>
                 if not val.get('deleted')
                     v.push(val.get('project_id'))
             return v

         get_student: (student) =>
             # return student with given id if a string; otherwise, just return student (the input)
-            if typeof(student)=='string' then @state.students?.get(student) else @state.students?.get(student?.get('student_id'))
+            if typeof(student) != 'string'
+                student = student?.get('student_id')
+            return @getIn(['students', student])

         get_student_note: (student) =>
             return @get_student(student)?.get('note')
@@ -976,7 +974,7 @@

         get_sorted_students: =>
             v = []
-            @state.students.map (student, id) =>
+            @get('students').map (student, id) =>
                 if not student.get('deleted')
                     v.push(student)
             v.sort (a,b) => misc.cmp(@get_student_name(a), @get_student_name(b))
@@ -991,11 +989,12 @@
         get_assignment_note: (assignment) =>
             return @get_assignment(assignment)?.get('note')

-        get_assignments: => @state.assignments
+        get_assignments: =>
+            return @get('assignments')

         get_sorted_assignments: =>
             v = []
-            @state.assignments.map (assignment, id) =>
+            @get_assignments().map (assignment, id) =>
                 if not assignment.get('deleted')
                     v.push(assignment)
             f = (a) -> [a.get('due_date') ? 0, a.get('path')?.toLowerCase()]   # note: also used in compute_assignment_list
@@ -1004,15 +1003,17 @@

         get_assignment: (assignment) =>
             # return assignment with given id if a string; otherwise, just return assignment (the input)
-            if typeof(assignment) == 'string' then @state.assignments?.get(assignment) else @state.assignments?.get(assignment?.get('assignment_id'))
+            if typeof(assignment) != 'string'
+                assignment = assignment?.get('assignment_id')
+            return @getIn(['assignments', assignment])

         get_assignment_ids: (opts) =>
             opts = defaults opts,
                 deleted : false   # if true return only deleted assignments
-            if not @state.assignments?
+            if not @get_assignments()
                 return
             v = []
-            @state.assignments.map (val, assignment_id) =>
+            @get_assignments().map (val, assignment_id) =>
                 if !!val.get('deleted') == opts.deleted
                     v.push(assignment_id)
             return v
@@ -1106,7 +1107,7 @@
             @_assignment_status.cache = info
             return info

-    flux.createStore(the_flux_name, CourseStore)
+    redux.createStore(the_redux_name, CourseStore)

     synchronized_db
         project_id : course_project_id
@@ -1115,7 +1116,7 @@
             if err
                 get_actions().set_error("unable to open #{@filename}")
             else
-                syncdbs[the_flux_name] = syncdb = _db
+                syncdbs[the_redux_name] = syncdb = _db
                 i = course_filename.lastIndexOf('.')
                 t = {settings:{title:course_filename.slice(0,i), description:'No description'}, assignments:{}, students:{}}
                 for x in syncdb.select()
@@ -1127,11 +1128,11 @@
                         t.assignments[x.assignment_id] = misc.copy_without(x, 'table')
                 for k, v of t
                     t[k] = immutable.fromJS(v)
-                get_actions()._set_to(t)
+                get_actions().setState(t)
                 syncdb.on('change', (changes) -> get_actions()._syncdb_change(changes))

                 # Wait until the projects store has data about users of our project before configuring anything.
-                flux.getStore('projects').wait
+                redux.getStore('projects').wait
                     until   :  (store) -> store.get_users(course_project_id)?
                     timeout : 30
                     cb      : ->
@@ -1167,7 +1168,7 @@
     displayName : "CourseEditorStudent"

     propTypes :
-        flux        : rtypes.object.isRequired
+        redux       : rtypes.object.isRequired
         name        : rtypes.string.isRequired
         student     : rtypes.object.isRequired
         user_map    : rtypes.object.isRequired
@@ -1200,17 +1201,17 @@
         return <a href="mailto:#{email}">{email}</a>

     open_project : ->
-        @props.flux.getActions('projects').open_project(project_id:@props.student.get('project_id'))
+        @props.redux.getActions('projects').open_project(project_id:@props.student.get('project_id'))

     create_project : ->
-        @props.flux.getActions(@props.name).create_student_project(@props.student_id)
+        @props.redux.getActions(@props.name).create_student_project(@props.student_id)

     render_last_active : ->
         student_project_id = @props.student.get('project_id')
         if not student_project_id?
             return
         # get the last time the student edited this project somehow.
-        last_active = @props.flux.getStore('projects').get_last_active(student_project_id)?.get(@props.student.get('account_id'))
+        last_active = @props.redux.getStore('projects').get_last_active(student_project_id)?.get(@props.student.get('account_id'))
         if last_active   # could be 0 or undefined
             return <span style={color:"#666"}>(last used project <TimeAgo date={last_active} />)</span>
         else
@@ -1246,11 +1247,11 @@
             </Tip>

     delete_student : ->
-        @props.flux.getActions(@props.name).delete_student(@props.student)
+        @props.redux.getActions(@props.name).delete_student(@props.student)
         @setState(confirm_delete:false)

     undelete_student : ->
-        @props.flux.getActions(@props.name).undelete_student(@props.student)
+        @props.redux.getActions(@props.name).undelete_student(@props.student)

     render_confirm_delete : ->
         if @state.confirm_delete
@@ -1291,13 +1292,13 @@
         </span>

     render_assignments_info_rows : ->
-        store = @props.flux.getStore(@props.name)
+        store = @props.redux.getStore(@props.name)
         for assignment in store.get_sorted_assignments()
             grade = store.get_grade(assignment, @props.student)
             <StudentAssignmentInfo
                   key={assignment.get('assignment_id')}
                   title={@render_title(assignment)}
-                  name={@props.name} flux={@props.flux}
+                  name={@props.name} redux={@props.redux}
                   student={@props.student} assignment={assignment}
                   grade={grade} />

@@ -1316,7 +1317,7 @@
                     rows        = 6
                     placeholder = 'Notes about student (not visible to student)'
                     default_value = {@props.student.get('note')}
-                    on_save     = {(value)=>@props.flux.getActions(@props.name).set_student_note(@props.student, value)}
+                    on_save     = {(value)=>@props.redux.getActions(@props.name).set_student_note(@props.student, value)}
                 />
             </Col>
         </Row>
@@ -1382,7 +1383,7 @@

     propTypes :
         name        : rtypes.string.isRequired
-        flux        : rtypes.object.isRequired
+        redux       : rtypes.object.isRequired
         project_id  : rtypes.string.isRequired
         students    : rtypes.object.isRequired
         user_map    : rtypes.object.isRequired
@@ -1418,7 +1419,7 @@
                     @setState(add_searching:false, err:err, add_select:undefined)
                     return
                 # Get the current collaborators/owners of the project that contains the course.
-                users = @props.flux.getStore('projects').get_users(@props.project_id)
+                users = @props.redux.getStore('projects').get_users(@props.project_id)
                 # Make a map with keys the email or account_id is already part of the course.
                 already_added = users.toJS()  # start with collabs on project
                 # For each student in course add account_id and/or email_address:
@@ -1454,7 +1455,7 @@
                     email_address : emails[y]
             else
                 students.push({email_address:y})
-        @props.flux.getActions(@props.name).add_students(students)
+        @props.redux.getActions(@props.name).add_students(students)
         @setState(err:undefined, add_select:undefined, add_search:'')

     render_add_selector_options : ->
@@ -1558,7 +1559,7 @@
         for x,i in students
             <Student background={if i%2==0 then "#eee"} key={x.student_id}
                      student_id={x.student_id} student={@props.students.get(x.student_id)}
-                     user_map={@props.user_map} flux={@props.flux} name={@props.name}
+                     user_map={@props.user_map} redux={@props.redux} name={@props.name}
                      project_map={@props.project_map}
                      assignments={@props.assignments}
                      />
@@ -1590,10 +1591,10 @@
     propTypes :
         project_id : rtypes.string.isRequired
         path       : rtypes.string.isRequired
-        flux       : rtypes.object.isRequired
+        redux      : rtypes.object.isRequired

     open_path : ->
-        @props.flux.getProjectActions(@props.project_id).open_directory(@props.path)
+        @props.redux.getProjectActions(@props.project_id).open_directory(@props.path)

     render : ->
         <a href="" onClick={(e)=>e.preventDefault(); @open_path()}>{@props.path}</a>
@@ -1657,7 +1658,7 @@

     propTypes :
         name       : rtypes.string.isRequired
-        flux       : rtypes.object.isRequired
+        redux      : rtypes.object.isRequired
         title      : rtypes.oneOfType([rtypes.string,rtypes.object]).isRequired
         student    : rtypes.oneOfType([rtypes.string,rtypes.object]).isRequired # required string (student_id) or student immutable js object
         assignment : rtypes.oneOfType([rtypes.string,rtypes.object]).isRequired # required string (assignment_id) or assignment immutable js object
@@ -1667,17 +1668,17 @@
         editing_grade : false

     open : (type, assignment_id, student_id) ->
-        @props.flux.getActions(@props.name).open_assignment(type, assignment_id, student_id)
+        @props.redux.getActions(@props.name).open_assignment(type, assignment_id, student_id)

     copy : (type, assignment_id, student_id) ->
-        @props.flux.getActions(@props.name).copy_assignment(type, assignment_id, student_id)
+        @props.redux.getActions(@props.name).copy_assignment(type, assignment_id, student_id)

     stop : (type, assignment_id, student_id) ->
-        @props.flux.getActions(@props.name).stop_copying_assignment(type, assignment_id, student_id)
+        @props.redux.getActions(@props.name).stop_copying_assignment(type, assignment_id, student_id)

     save_grade : (e) ->
         e?.preventDefault()
-        @props.flux.getActions(@props.name).set_grade(@props.assignment, @props.student, @state.grade)
+        @props.redux.getActions(@props.name).set_grade(@props.assignment, @props.student, @state.grade)
         @setState(editing_grade:false)

     edit_grade : ->
@@ -1801,7 +1802,7 @@
         return v

     render : ->
-        info = @props.flux.getStore(@props.name).student_assignment_info(@props.student, @props.assignment)
+        info = @props.redux.getStore(@props.name).student_assignment_info(@props.student, @props.assignment)
         <Row style={borderTop:'1px solid #aaa', paddingTop:'5px', paddingBottom: '5px'}>
             <Col md=2 key="title">
                 {@props.title}
@@ -1835,19 +1836,19 @@

     propTypes :
         name       : rtypes.string.isRequired
-        flux       : rtypes.object.isRequired
+        redux      : rtypes.object.isRequired
         assignment : rtypes.object.isRequired
         students   : rtypes.object.isRequired
         user_map   : rtypes.object.isRequired
         background : rtypes.string

     render_student_info : (student_id) ->
-        store = @props.flux.getStore(@props.name)
+        store = @props.redux.getStore(@props.name)
         <StudentAssignmentInfo
               key     = {student_id}
               title   = {misc.trunc_middle(store.get_student_name(student_id), 40)}
               name    = {@props.name}
-              flux    = {@props.flux}
+              redux   = {@props.redux}
               student = {student_id}
               assignment = {@props.assignment}
               grade   = {store.get_grade(@props.assignment, student_id)} />
@@ -1885,7 +1886,7 @@
         name       : rtypes.string.isRequired
         assignment : rtypes.object.isRequired
         project_id : rtypes.string.isRequired
-        flux       : rtypes.object.isRequired
+        redux      : rtypes.object.isRequired
         students   : rtypes.object.isRequired
         user_map   : rtypes.object.isRequired
         background : rtypes.string
@@ -1920,7 +1921,7 @@
     date_change : (date) ->
         if not date
             date = @props.assignment.get('due_date') ? new Date()
-        @props.flux.getActions(@props.name).set_due_date(@props.assignment, date)
+        @props.redux.getActions(@props.name).set_due_date(@props.assignment, date)

     render_note : ->
         <Row key='note' style={note_style}>
@@ -1934,13 +1935,13 @@
                     rows          = 6
                     placeholder   = 'Private notes about this assignment (not visible to students)'
                     default_value = {@props.assignment.get('note')}
-                    on_save       = {(value)=>@props.flux.getActions(@props.name).set_assignment_note(@props.assignment, value)}
+                    on_save       = {(value)=>@props.redux.getActions(@props.name).set_assignment_note(@props.assignment, value)}
                 />
             </Col>
         </Row>

     render_more_header : ->
-        status = @props.flux.getStore(@props.name).get_assignment_status(@props.assignment)
+        status = @props.redux.getStore(@props.name).get_assignment_status(@props.assignment)
         if not status?
             return <Loading key='loading_more'/>
         v = []
@@ -1973,7 +1974,7 @@
         <Row key='more'>
             <Col sm=12>
                 <Panel header={@render_more_header()}>
-                    <StudentListForAssignment flux={@props.flux} name={@props.name}
+                    <StudentListForAssignment redux={@props.redux} name={@props.name}
                         assignment={@props.assignment} students={@props.students}
                         user_map={@props.user_map} />
                     {@render_note()}
@@ -1983,10 +1984,10 @@

     assign_assignment : ->
         # assign assignment to all (non-deleted) students
-        @props.flux.getActions(@props.name).copy_assignment_to_all_students(@props.assignment)
+        @props.redux.getActions(@props.name).copy_assignment_to_all_students(@props.assignment)

     open_assignment_path : ->
-        @props.flux.getProjectActions(@props.project_id).open_directory(@props.assignment.get('path'))
+        @props.redux.getProjectActions(@props.project_id).open_directory(@props.assignment.get('path'))

     render_open_button : ->
         <Tip key='open' title={<span><Icon name='folder-open-o'/> Open assignment</span>}
@@ -2026,7 +2027,7 @@

     copy_assignment : (step, new_only) ->
         # assign assignment to all (non-deleted) students
-        actions = @props.flux.getActions(@props.name)
+        actions = @props.redux.getActions(@props.name)
         switch step
             when 'assignment'
                 actions.copy_assignment_to_all_students(@props.assignment, new_only)
@@ -2090,7 +2091,7 @@

     collect_assignment : ->
         # assign assignment to all (non-deleted) students
-        @props.flux.getActions(@props.name).copy_assignment_from_all_students(@props.assignment)
+        @props.redux.getActions(@props.name).copy_assignment_from_all_students(@props.assignment)

     render_collect_tip : (warning) ->
         <span key='normal'>
@@ -2119,7 +2120,7 @@

     return_assignment : ->
         # Assign assignment to all (non-deleted) students.
-        @props.flux.getActions(@props.name).return_assignment_to_all_students(@props.assignment)
+        @props.redux.getActions(@props.name).return_assignment_to_all_students(@props.assignment)

     render_return_button : (status) ->
         # Disable the button if nothing collected.
@@ -2143,11 +2144,11 @@
             </Button>

     delete_assignment : ->
-        @props.flux.getActions(@props.name).delete_assignment(@props.assignment)
+        @props.redux.getActions(@props.name).delete_assignment(@props.assignment)
         @setState(confirm_delete:false)

     undelete_assignment : ->
-        @props.flux.getActions(@props.name).undelete_assignment(@props.assignment)
+        @props.redux.getActions(@props.name).undelete_assignment(@props.assignment)

     render_confirm_delete : ->
         if @state.confirm_delete
@@ -2223,7 +2224,7 @@
     propTypes :
         name        : rtypes.string.isRequired
         project_id  : rtypes.string.isRequired
-        flux        : rtypes.object.isRequired
+        redux       : rtypes.object.isRequired
         assignments : rtypes.object.isRequired
         students    : rtypes.object.isRequired
         user_map    : rtypes.object.isRequired
@@ -2300,7 +2301,7 @@
             </Button>

     add_selected_assignment : ->
-        @props.flux.getActions(@props.name).add_assignment(@state.add_selected)
+        @props.redux.getActions(@props.name).add_assignment(@state.add_selected)
         @setState(err:undefined, add_select:undefined, add_search:'', add_selected:'')

     render_add_selector_options : ->
@@ -2395,7 +2396,7 @@
     render_assignments : (assignments) ->
         for x,i in assignments
             <Assignment background={if i%2==0 then "#eee"}  key={x.assignment_id} assignment={@props.assignments.get(x.assignment_id)}
-                    project_id={@props.project_id}  flux={@props.flux}
+                    project_id={@props.project_id}  redux={@props.redux}
                     students={@props.students} user_map={@props.user_map}
                     name={@props.name}
                     />
@@ -2425,7 +2426,7 @@
     displayName : "CourseEditorSettings"

     propTypes :
-        flux        : rtypes.object.isRequired
+        redux       : rtypes.object.isRequired
         name        : rtypes.string.isRequired
         path        : rtypes.string.isRequired
         settings    : rtypes.object.isRequired
@@ -2450,7 +2451,7 @@
             <LabeledRow label="Title">
                 <TextInput
                     text={@props.settings.get('title')}
-                    on_change={(title)=>@props.flux.getActions(@props.name).set_title(title)}
+                    on_change={(title)=>@props.redux.getActions(@props.name).set_title(title)}
                 />
             </LabeledRow>
             <LabeledRow label="Description">
@@ -2458,7 +2459,7 @@
                     rows    = 6
                     type    = "textarea"
                     default_value = {@props.settings.get('description')}
-                    on_save ={(desc)=>@props.flux.getActions(@props.name).set_description(desc)}
+                    on_save ={(desc)=>@props.redux.getActions(@props.name).set_description(desc)}
                 />
             </LabeledRow>
             <hr/>
@@ -2490,10 +2491,10 @@
         return p.slice(0,i) + '.' + ext

     open_file : (path) ->
-        @props.flux.getProjectActions(@props.project_id).open_file(path:path,foreground:true)
+        @props.redux.getProjectActions(@props.project_id).open_file(path:path,foreground:true)

     write_file : (path, content) ->
-        actions = @props.flux.getActions(@props.name)
+        actions = @props.redux.getActions(@props.name)
         id = actions.set_activity(desc:"Writing #{path}")
         salvus_client.write_text_file_to_project
             project_id : @props.project_id
@@ -2507,7 +2508,7 @@
                     actions.set_error("Error writing '#{path}' -- '#{err}'")

     save_grades_to_csv : ->
-        store = @props.flux.getStore(@props.name)
+        store = @props.redux.getStore(@props.name)
         assignments = store.get_sorted_assignments()
         students = store.get_sorted_students()
         # TODO: actually learn CSV format... (e.g., what if comma in path)
@@ -2521,7 +2522,7 @@

     save_grades_to_py : ->
         content = "assignments = ['Assignment 1', 'Assignment 2']\nstudents=[\n    {'name':'Foo Bar', 'grades':[85,37]},\n    {'name':'Bar None', 'grades':[15,50]}\n]\n"
-        store = @props.flux.getStore(@props.name)
+        store = @props.redux.getStore(@props.name)
         assignments = store.get_sorted_assignments()
         students = store.get_sorted_students()
         # TODO: actually learn CSV format... (e.g., what if comma in path)
@@ -2580,8 +2581,8 @@
                 <MarkdownInput
                     rows    = 6
                     type    = "textarea"
-                    default_value = {@props.flux.getStore(@props.name).get_email_invite()}
-                    on_save ={(body)=>@props.flux.getActions(@props.name).set_email_invite(body)}
+                    default_value = {@props.redux.getStore(@props.name).get_email_invite()}
+                    on_save ={(body)=>@props.redux.getActions(@props.name).set_email_invite(body)}
                 />
             </div>
             <hr/>
@@ -2596,7 +2597,7 @@
     ###

     delete_all_student_projects: ->
-        @props.flux.getActions(@props.name).delete_all_student_projects()
+        @props.redux.getActions(@props.name).delete_all_student_projects()

     render_confirm_delete_student_projects: ->
         <Well style={marginTop:'10px'}>
@@ -2633,7 +2634,7 @@
                 upgrades[quota] = val / display_factor
         @setState(upgrade_quotas: false)
         if misc.len(upgrades) > 0
-            @props.flux.getActions(@props.name).upgrade_all_student_projects(upgrades)
+            @props.redux.getActions(@props.name).upgrade_all_student_projects(upgrades)

     upgrade_quotas_submittable: ->
         if @_upgrade_is_invalid
@@ -2760,10 +2761,10 @@
             @render_upgrade_row(quota, available, current, yours, num_projects)

     render_upgrade_quotas: ->
-        flux = @props.flux
+        redux = @props.redux

         # Get available upgrades that instructor has to apply
-        account_store = flux.getStore('account')
+        account_store = redux.getStore('account')
         if not account_store?
             return <Loading/>
         purchased_upgrades = account_store.get_total_upgrades()
@@ -2771,7 +2772,7 @@
             # user has no upgrades on their account
             return <NoUpgrades cancel={=>@setState(upgrade_quotas:false)} />

-        course_store = flux.getStore(@props.name)
+        course_store = redux.getStore(@props.name)
         if not course_store?
             return <Loading/>

@@ -2784,7 +2785,7 @@
             return <span>There are no student projects yet.<br/><br/>{@render_upgrade_submit_buttons()}</span>

         # Get remaining upgrades
-        projects_store = flux.getStore('projects')
+        projects_store = redux.getStore('projects')
         if not projects_store?
             return <Loading/>
         applied_upgrades = projects_store.get_total_upgrades_you_have_applied()
@@ -2863,35 +2864,41 @@
             </Row>
         </div>

-CourseEditor = rclass
+CourseEditor = (name) -> rclass
     displayName : "CourseEditor"

+    reduxProps :
+        "#{name}" :
+            error       : rtypes.string
+            tab         : rtypes.string
+            activity    : rtypes.object    # status messages about current activity happening (e.g., things being assigned)
+            students    : rtypes.immutable
+            assignments : rtypes.immutable
+            settings    : rtypes.immutable
+        users :
+            user_map    : rtypes.immutable
+        projects :
+            project_map : rtypes.immutable  # gets updated when student is active on their project
+
     propTypes :
-        error       : rtypes.string
-        activity    : rtypes.object   # status messages about current activity happening (e.g., things being assigned)
+        redux       : rtypes.object
         name        : rtypes.string.isRequired
+        project_id  : rtypes.string.isRequired
         path        : rtypes.string.isRequired
-        project_id  : rtypes.string.isRequired
-        flux        : rtypes.object
-        settings    : rtypes.object
-        students    : rtypes.object
-        assignments : rtypes.object
-        user_map    : rtypes.object
-        project_map : rtypes.object  # gets updated when student is active on their project

     render_activity : ->
         if @props.activity?
             <ActivityDisplay activity={misc.values(@props.activity)} trunc=80
-                on_clear={=>@props.flux.getActions(@props.name).clear_activity()} />
+                on_clear={=>@props.redux.getActions(@props.name).clear_activity()} />

     render_error : ->
         if @props.error
             <ErrorDisplay error={@props.error}
-                          onClose={=>@props.flux.getActions(@props.name).set_error('')} />
+                          onClose={=>@props.redux.getActions(@props.name).set_error('')} />

     render_students : ->
-        if @props.flux? and @props.students? and @props.user_map? and @props.project_map?
-            <Students flux={@props.flux} students={@props.students}
+        if @props.redux? and @props.students? and @props.user_map? and @props.project_map?
+            <Students redux={@props.redux} students={@props.students}
                       name={@props.name} project_id={@props.project_id}
                       user_map={@props.user_map} project_map={@props.project_map}
                       assignments={@props.assignments}
@@ -2900,22 +2907,22 @@
             return <Loading />

     render_assignments : ->
-        if @props.flux? and @props.assignments? and @props.user_map? and @props.students?
-            <Assignments flux={@props.flux} assignments={@props.assignments}
+        if @props.redux? and @props.assignments? and @props.user_map? and @props.students?
+            <Assignments redux={@props.redux} assignments={@props.assignments}
                 name={@props.name} project_id={@props.project_id} user_map={@props.user_map} students={@props.students} />
         else
             return <Loading />

     render_settings : ->
-        if @props.flux? and @props.settings?
-            <Settings flux={@props.flux} settings={@props.settings}
+        if @props.redux? and @props.settings?
+            <Settings redux={@props.redux} settings={@props.settings}
                       name={@props.name} project_id={@props.project_id}
                       path={@props.path} />
         else
             return <Loading />

     render_student_header : ->
-        n = @props.flux.getStore(@props.name)?.num_students()
+        n = @props.redux.getStore(@props.name)?.num_students()
         <Tip delayShow=1300
              title="Students" tip="This tab lists all students in your course, along with their grades on each assignment.  You can also quickly find students by name on the left and add new students on the right.">
             <span>
@@ -2924,7 +2931,7 @@
         </Tip>

     render_assignment_header : ->
-        n = @props.flux.getStore(@props.name)?.num_assignments()
+        n = @props.redux.getStore(@props.name)?.num_assignments()
         <Tip delayShow=1300
              title="Assignments" tip="This tab lists all of the assignments associated to your course, along with student grades and status about each assignment.  You can also quickly find assignments by name on the left.   An assignment is a directory in your project, which may contain any files.  Add an assignment to your course by searching for the directory name in the search box on the right.">
             <span>
@@ -2942,10 +2949,10 @@

     render_save_button : ->
         if @props.show_save_button
-            <SaveButton saving={@props.saving} unsaved={true} on_click={=>@props.flux.getActions(@props.name).save()}/>
+            <SaveButton saving={@props.saving} unsaved={true} on_click={=>@props.redux.getActions(@props.name).save()}/>

     show_files : ->
-        @props.flux?.getProjectActions(@props.project_id).set_focused_page('project-file-listing')
+        @props.redux?.getProjectActions(@props.project_id).set_focused_page('project-file-listing')

     render_files_button : ->
         <Button className='smc-small-only' style={float:'right', marginLeft:'15px'}
@@ -2961,7 +2968,7 @@
             {@render_activity()}
             {@render_files_button()}
             {@render_title()}
-            <Tabs animation={false} activeKey={@props.tab} onSelect={(key)=>@props.flux?.getActions(@props.name).set_tab(key)}>
+            <Tabs animation={false} activeKey={@props.tab} onSelect={(key)=>@props.redux?.getActions(@props.name).set_tab(key)}>
                 <Tab eventKey={'students'} title={@render_student_header()}>
                     <div style={marginTop:'8px'}></div>
                     {@render_students()}
@@ -2977,27 +2984,28 @@
             </Tabs>
         </div>

-render = (flux, project_id, path) ->
-    name = flux_name(project_id, path)
+render = (redux, project_id, path) ->
+    name = redux_name(project_id, path)
     # dependence on account below is for adjusting quotas
-    <FluxComponent flux={flux} connectToStores={[name, 'users', 'projects', 'account']} >
-        <CourseEditor name={name} project_id={project_id} path={path} />
-    </FluxComponent>
-
-exports.render_editor_course = (project_id, path, dom_node, flux) ->
-    init_flux(flux, project_id, path)
-    ReactDOM.render(render(flux, project_id, path), dom_node)
-
-exports.hide_editor_course = (project_id, path, dom_node, flux) ->
+    CourseEditor_connected = CourseEditor(name)
+    <Redux redux={redux}>
+        <CourseEditor_connected redux={redux} name={name} project_id={project_id} path={path} />
+    </Redux>
+
+exports.render_editor_course = (project_id, path, dom_node, redux) ->
+    init_redux(redux, project_id, path)
+    ReactDOM.render(render(redux, project_id, path), dom_node)
+
+exports.hide_editor_course = (project_id, path, dom_node, redux) ->
     #console.log("hide_editor_course")
     ReactDOM.unmountComponentAtNode(dom_node)

-exports.show_editor_course = (project_id, path, dom_node, flux) ->
+exports.show_editor_course = (project_id, path, dom_node, redux) ->
     #console.log("show_editor_course")
-    ReactDOM.render(render(flux, project_id, path), dom_node)
-
-exports.free_editor_course = (project_id, path, dom_node, flux) ->
-    fname = flux_name(project_id, path)
+    ReactDOM.render(render(redux, project_id, path), dom_node)
+
+exports.free_editor_course = (project_id, path, dom_node, redux) ->
+    fname = redux_name(project_id, path)
     db = syncdbs[fname]
     if not db?
         return
@@ -3006,10 +3014,10 @@
     ReactDOM.unmountComponentAtNode(dom_node)
     # It is *critical* to first unmount the store, then the actions,
     # or there will be a huge memory leak.
-    store = flux.getStore(fname)
+    store = redux.getStore(fname)
     delete store.state
-    flux.removeStore(fname)
-    flux.removeActions(fname)
+    redux.removeStore(fname)
+    redux.removeActions(fname)

 immutable_to_list = (x, primary_key) ->
     if not x?


remove an incorrect reference to error that would cause a stack trace

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -191,10 +191,9 @@
                 #winston.debug("serving a blob: #{misc.to_json(query)}")
                 if not query.uuid?
                     res.writeHead(500, {'Content-Type':'text/plain'})
-                    res.end("internal error: #{error}")
+                    res.end("internal error")
                     return
                 get_blob uuid:query.uuid, cb:(error, data) ->
-                    #winston.debug("query got back: #{error}, #{misc.to_json(data)}")
                     if error
                         res.writeHead(500, {'Content-Type':'text/plain'})
                         res.end("internal error: #{error}")


number_extensions

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -41,12 +41,6 @@
 ###*
 Returns the mathematical floor of this number.

-@name floor
-@methodOf Number#
-
-@type Number
-@returns The number truncated to the nearest integer of less than or equal value.
-

    4.9.floor()
 => 4
@@ -57,6 +51,12 @@
    (-1.2).floor()
 => -2

+
+@name floor
+@methodOf Number#
+
+@type Number
+@returns The number truncated to the nearest integer of less than or equal value.
 ###
 Number::floor = ->
   Math.floor(this)
@@ -64,12 +64,6 @@
 ###*
 Returns this number rounded to the nearest integer.

-@name round
-@methodOf Number#
-
-@type Number
-@returns The number rounded to the nearest integer.
-

    4.5.round()
 => 5
@@ -77,28 +71,33 @@
    4.4.round()
 => 4

+
+@name round
+@methodOf Number#
+
+@type Number
+@returns The number rounded to the nearest integer.
 ###
 Number::round = ->
   Math.round(this)

 ###*
 Returns a number whose value is limited to the given range.
-
-@name clamp
-@methodOf Number#
-
-@param {Number} min The lower boundary of the output range
-@param {Number} max The upper boundary of the output range
-
-@type Number
-@returns A number in the range [min, max]
-

    # limit the output of this computation to between 0 and 255
    (2 * 255).clamp(0, 255)
 => 255

+
+@name clamp
+@methodOf Number#
+
+@param {Number} min The lower boundary of the output range
+@param {Number} max The upper boundary of the output range
+
+@type Number
+@returns A number in the range [min, max]
 ###
 Number::clamp = (min, max) ->
   Math.min(Math.max(this, min), max)
@@ -107,18 +106,17 @@
 A mod method useful for array wrapping. The range of the function is
 constrained to remain in bounds of array indices.

-@name mod
-@methodOf Number#
-
-@param {Number} base
-@type Number
-@returns An integer between 0 and (base - 1) if base is positive.
-
-

    (-1).mod(5)
 => 4

+
+@name mod
+@methodOf Number#
+
+@param {Number} base
+@type Number
+@returns An integer between 0 and (base - 1) if base is positive.
 ###
 Number::mod = (base) ->
   result = this % base;
@@ -131,12 +129,6 @@
 ###*
 Get the sign of this number as an integer (1, -1, or 0).

-@name sign
-@methodOf Number#
-
-@type Number
-@returns The sign of this number, 0 if the number is 0.
-

    (-5).sign()
 => -1
@@ -147,6 +139,12 @@
    5.sign()
 => 1

+
+@name sign
+@methodOf Number#
+
+@type Number
+@returns The sign of this number, 0 if the number is 0.
 ###
 Number::sign = ->
   if this > 0
@@ -159,22 +157,22 @@
 ###*
 Returns true if this number is even (evenly divisible by 2).

+

+   2.even()
+=> true
+
+   3.even()
+=> false
+
+   0.even()
+=> true
+

+
 @name even
 @methodOf Number#

 @type Boolean
 @returns true if this number is an even integer, false otherwise.
-
-

-   2.even()
-=> true
-
-   3.even()
-=> false
-
-   0.even()
-=> true
-

 ###
 Number::even = ->
   this % 2 == 0
@@ -182,22 +180,22 @@
 ###*
 Returns true if this number is odd (has remainder of 1 when divided by 2).

+

+   2.odd()
+=> false
+
+   3.odd()
+=> true
+
+   0.odd()
+=> false
+

+
 @name odd
 @methodOf Number#

 @type Boolean
 @returns true if this number is an odd integer, false otherwise.
-
-

-   2.odd()
-=> false
-
-   3.odd()
-=> true
-
-   0.odd()
-=> false
-

 ###
 Number::odd = ->
   if this > 0
@@ -209,6 +207,16 @@
 Calls iterator the specified number of times, passing in the number of the
 current iteration as a parameter: 0 on first call, 1 on the second call, etc.

+

+   output = []
+
+   5.times (n) ->
+     output.push(n)
+
+   output
+=> [0, 1, 2, 3, 4]
+

+
 @name times
 @methodOf Number#

@@ -219,16 +227,6 @@

 @type Number
 @returns The number of times the iterator was called.
-
-

-   output = []
-
-   5.times (n) ->
-     output.push(n)
-
-   output
-=> [0, 1, 2, 3, 4]
-

 ###
 Number::times = (iterator, context) ->
   i = -1


allow flashing of frame in all platforms

--- src/ui/views/notifications.coffee
+++ src/ui/views/notifications.coffee
@@ -106,11 +106,10 @@
             #  and mute option is not set
             if (!notifierSupportsSound || viewstate.forceCustomSound) && !viewstate.muteSoundNotification && audioEl.paused
                 audioEl.play()
-        if process.platform is 'win32'
-            #if not mainWindow.isVisible()
-            #    mainWindow.showInactive()
-            #    mainWindow.minimize()
-            mainWindow.flashFrame(true)
+        #if not mainWindow.isV  isible()
+        #    mainWindow.showInactive()
+        #    mainWindow.minimize()
+        mainWindow.flashFrame(true) # uncommented in #1206

 textMessage = (cont, proxied, showMessage = true) ->
     if cont?.segment?


Added in event

Added in event


Ensure we have input before firing

Ensure we have input before firing


Updated a test that should have been modified along with #543

--- tests/batman/view/filter_parsing_test.coffee
+++ tests/batman/view/filter_parsing_test.coffee
@@ -126,7 +126,7 @@
   node = helpers.render '

',
     foo: 'foo'
   , (node) ->
-    equal node.html(), "(foo)undefined"
+    equal node.html(), "(foo)"
     QUnit.start()

 asyncTest 'should update bindings with the filtered value if they change', 1, ->


wss rollback for ws at heroku

wss rollback for ws at heroku

--- app/assets/javascripts/SumWebSocket.coffee
+++ app/assets/javascripts/SumWebSocket.coffee
@@ -1,8 +1,9 @@
 define () ->
   class SumWebSocket
     constructor: (password) ->
-      @ws = new WebSocket("ws://localhost:9000/sum/" + password)
-      @ws.onerror = () ->
+      try
+        @ws = new WebSocket("ws://localhost:9000/sum/" + password)
+      catch e
         @ws = new WebSocket("wss://localhost:9000/sum/" + password)

     onConnected: (callback) ->


Hype++

Hype++

Suck it, nerds.

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -90,7 +90,7 @@
   if /arbo/i.test(name)
     # Bouncers are hard or something, whatever.
     name = 'arbo'
-
+
   if name == 'sflios'
     # He visited this one time
     # http://pixxx.wtf.cat/image/0Q3B34330v0c/12188116_10205398142892807_4244989619067012464_o.jpg
@@ -99,14 +99,14 @@
   # has multiple connections because computers Я hard
   if /justinw/i.test(name)
     name = 'justinw2'
-
+
   if /blah64/i.test(name)
     name = 'Blah64'

   # All hosts are dongs
   if /(dev|dong|dodge)host/i.test(name)
     name = 'dong'
-
+
   if /meowth/i.test(name)
     name = 'o_rly'

@@ -119,7 +119,7 @@

   # Emails 'R' Secret
   text = text.replace(/([^@\s]+@)[^@\s]+\.[^@\s]+/g, "$1[redacted]")
-
+
   # Let users redact shit too
   text = text.replace(/\[[^\]]*\]/g, "[redacted]")

@@ -156,7 +156,7 @@
     )

   # Ask for what currently serves as "output"
-  robot.respond /ayp(\s+(me)?)?\s*$/i, (msg) ->
+  robot.respond /(?:nmp|ayp)(\s+(me)?)?\s*$/i, (msg) ->
     buffer.get 6, (err, lines) ->
       # Build a strip of AYP
       new AYPStrip lines, (err, image, strip) ->


Honor options passed through when overridding #parse for embeddedResources

--- app/assets/javascripts/exo/model.js.coffee
+++ app/assets/javascripts/exo/model.js.coffee
@@ -73,10 +73,10 @@

       super(attrs, options)

-    parse: (resp) ->
+    parse: (resp, options) ->
       if resp # PATCH requests return an empty response
         for attr, klass of (@embeddedResources || [])
           data = resp[attr]
-          resp[attr] = new klass(data, parse: true)
+          resp[attr] = new klass(data, options)

       super(resp)


OX-120 debug

OX-120 debug

--- app/assets/javascripts/model/binary.coffee
+++ app/assets/javascripts/model/binary.coffee
@@ -159,7 +159,7 @@
       unless lastPosition
         lastPosition = [0, 0, 0]

-      unless lastDirection
+      unless @lastDirection
         @lastDirection = [1, 0, 0]

       @lastPosition = position


Fixed form validation

Fixed form validation

--- scripts/job.coffee
+++ scripts/job.coffee
@@ -1,7 +1,15 @@
-validateForm = (max_pay) ->
-	alert max_pay
+$(document).ready ->
+	$('#bid-form').submit ->
+		max_pay = ~~$('#bid-form').data 'maxpay'
+		amount = ~~$('input[name="amount"]').val()
+		if amount < 5
+			alert('Minimum amount is 5 gold')
+			return false
+		else if amount > max_pay
+			alert('Amount is over maximum')
+			return false
+		return true

-$(document).ready ->
 	$('.bid-accept').click (e) ->
 		elem = $ e.currentTarget
 		gold = elem.data 'gold'


disable the terminal button when the underlying folder does not exist

--- app/assets/javascripts/workflows.js.coffee
+++ app/assets/javascripts/workflows.js.coffee
@@ -29,6 +29,7 @@
         if missing_data_path()
           # If the template folder does not exist we need to disable certain buttons.
           update_open_dir_button()
+          update_terminal_button()
           update_edit_button()
           update_submit_button()
           update_copy_button()


Fix an error with the close button of the lightbox and reset the form

--- app/coffee/modules/admin/lightboxes.coffee
+++ app/coffee/modules/admin/lightboxes.coffee
@@ -37,7 +37,7 @@
             <option value="<%- role.id %>"><%- role.name %></option>
             <% }); %>
         </select>
-
+
     </fieldset>
     """) # i18n

@@ -46,11 +46,15 @@
             ctx = {roleList: $scope.roles}
             return template(ctx)

-        $scope.$on "membersform:new",  ->
+        resetForm = ->
+            $el.find("form > fieldset").remove()
+
             title = $el.find("h2")
             fieldSet = createFieldSet()
             title.after(fieldSet)

+        $scope.$on "membersform:new",  ->
+            resetForm()
             $el.removeClass("hidden")

         $scope.$on "$destroy", ->
@@ -61,19 +65,19 @@
             event.preventDefault()
             $el.addClass("hidden")

-        $el.on "click", ".icon-delete", (event) ->
+        $el.on "click", ".delete-fieldset", (event) ->
             event.preventDefault()
             target = angular.element(event.currentTarget)
             fieldSet = target.parent()

             fieldSet.remove()

-        $el.on "click", ".icon-plus", (event) ->
+        $el.on "click", ".add-fieldset", (event) ->
             event.preventDefault()
             target = angular.element(event.currentTarget)
             fieldSet = target.parent()

-            target.removeClass("icon-plus").addClass("icon-delete")
+            target.removeClass("icon-plus add-fieldset").addClass("icon-delete delete-fieldset")
             newFieldSet = createFieldSet()
             fieldSet.after(newFieldSet)


Schedules & overrides

Schedules & overrides

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -362,7 +362,7 @@
       overflow: 'true'
     }

-    thing = 'entries'
+    thing = ''
     if msg.match[3] && msg.match[3].match /overrides/
       thing = 'overrides'
       query['editable'] = 'true'
@@ -387,7 +387,7 @@
           robot.emit 'error', err, msg
           return

-        entries = json.entries || json.overrides
+        entries = json?.schedule?.final_schedule?.rendered_schedule_entries || json.overrides
         if entries
           sortedEntries = entries.sort (a, b) ->
             moment(a.start).unix() - moment(b.start).unix()
@@ -397,7 +397,7 @@
             startTime = moment(entry.start).tz(timezone).format()
             endTime   = moment(entry.end).tz(timezone).format()
             if entry.id
-              buffer += "* (#{entry.id}) #{startTime} - #{endTime} #{entry.user.name}\n"
+              buffer += "* (#{entry.id}) #{startTime} - #{endTime} #{entry.user.summary}\n"
             else
               buffer += "* #{startTime} - #{endTime} #{entry.user.name}\n"
           if buffer == ""


Adding support for the most recent version of Font Awesome.

--- src/plugins/image.coffee
+++ src/plugins/image.coffee
@@ -104,7 +104,7 @@
       buttonHolder = jQuery ''
       buttonHolder.hallobutton
         label: 'Images'
-        icon: 'icon-picture'
+        icon: 'fa-picture-o'
         editable: @options.editable
         command: null
         queryState: false


Fixed bug.

Fixed bug.

--- app/lib/collections/schemas/company.coffee
+++ app/lib/collections/schemas/company.coffee
@@ -20,6 +20,8 @@
     type: String
     label: 'Owner'
     denyUpdate: true
+    autoform:
+      type: 'hidden'
     autoValue: () ->
       @userId if @isInsert
   customers:


Removed debug info

Removed debug info

--- js-library/app/js/publisher_poc/nu.nl.coffee
+++ js-library/app/js/publisher_poc/nu.nl.coffee
@@ -16,7 +16,6 @@
   factlink_publisher.addFactlinkButtons()

   FACTLINK.on 'factlink.factsLoaded', (facts) ->
-    console.info "FACTS: ", facts
     ui = new UI
     ui.updateFactsCount facts.length
     ui.bindActions()


probably tricky

probably tricky

--- src/ipso.coffee
+++ src/ipso.coffee
@@ -303,6 +303,7 @@

                 #
                 # * TODO: enable require 'username/componentname' to handle name collisions
+                #                (this could prove to be very !!TRICKY!!)
                 #

                 component = JSON.parse readFileSync componentFile
@@ -321,7 +322,6 @@
             when 3  then console.log "ipso: could not access directory: #{compomnentsRoot}"
             when 34 then console.log "ipso: expected directory: #{compomnentsRoot}"
             else         console.log "ipso: unexpected error reading directory: #{compomnentsRoot}"
-

     ipso.define list, aliases
     return ipso


Only hound if presence changes to active

--- src/scripts/hound.coffee
+++ src/scripts/hound.coffee
@@ -122,8 +122,9 @@
       channel = { private: null, name: '' }
     hound user, channel
   robot.adapter.client.on 'presence_change', (res) ->
-    user = robot.adapter.client.rtm.dataStore.getUserById res.user
-    hound user, { private: null, name: '' }
+    if res.presence is 'active'
+      user = robot.adapter.client.rtm.dataStore.getUserById res.user
+      hound user, { private: null, name: '' }

   # Every five minutes, attempt to hound non-salaried users
   autoHoundJob = schedule.scheduleJob '*/5 * * * *', ->


movable velocity and acceleration getters


Test that bid confirmation amount passes through on mobile.

--- src/mobile/apps/feature/test/routes.test.coffee
+++ src/mobile/apps/feature/test/routes.test.coffee
@@ -70,6 +70,26 @@
       @res.render.args[0][1].hasQualifiedCreditCard.should.be.true
       done()

+  it 'converts to dollars and renders the bid confirmation amount from the query', (done) ->
+    @req = session: {}, params: {}, query: { bid: 20000 }, get: sinon.stub()
+    @metaphysics.returns Promise.resolve
+      artwork: sale_artwork: bid_increments: [100, 200]
+      me: {
+        has_qualified_credit_cards: true
+        bidders: [{ id: 'foo', qualified_for_bidding: false }]
+        lot_standing: null
+      }
+
+    routes.bid @req, @res, done
+
+    Backbone.sync.args[0][2].success fabricate 'artwork'
+    Backbone.sync.args[1][2].success [fabricate 'sale', is_auction: true]
+    Backbone.sync.args[2][2].success fabricate 'sale_artwork'
+
+    _.defer =>
+      @res.render.args[0][1].maxBid.should.equal(200)
+      done()
+
   it 'handles users who are logged in but not registered and do not have a qualified credit card', (done) ->
     @metaphysics.returns Promise.resolve
       artwork: sale_artwork: bid_increments: [100, 200]


set cookie when auction is clicked

set cookie when auction is clicked

--- components/auction_reminder/index.coffee
+++ components/auction_reminder/index.coffee
@@ -14,7 +14,7 @@

   events: ->
     'click .modal-close': 'close'
-    'click .auction-reminder-container': 'trackAnalytics'
+    'click .auction-reminder-container': 'clickAuctionReminder'

   initialize: ({ @auction, @auctionImage }) ->

@@ -63,7 +63,8 @@
     @$Clock.addClass 'is-fade-in'
     @clock.start()

-  trackAnalytics: ->
+  clickAuctionReminder: ->
+    Cookies.set('closeAuctionReminder', true)
     analytics.snowplowStruct 'auction_reminder', 'click', @auction.get('id'), 'feature'

 class AuctionClock extends ClockView


Render after compositionend

Render after compositionend

--- src/trix/controllers/input_controller.coffee
+++ src/trix/controllers/input_controller.coffee
@@ -23,7 +23,9 @@
       updateSelectionForEvent(event)

     compositionend: (event) ->
-      @composing = false
+      if @composing
+        @composing = false
+        @requestRender()

   inputTypes:
     deleteByComposition: (event) ->


Use internal attachment selector so styles persist when configurable class names change

--- src/trix/elements/trix_editor_element.coffee
+++ src/trix/elements/trix_editor_element.coffee
@@ -2,7 +2,8 @@
 #= require trix/controllers/editor_controller

 {makeElement, triggerEvent, handleEvent, handleEventOnce, defer} = Trix
-{classNames} = Trix.config.css
+
+{attachmentSelector} = Trix.AttachmentView

 Trix.registerElement "trix-editor", do ->
   id = 0
@@ -51,11 +52,11 @@
       height: auto;
     }

-    %t .#{classNames.attachment.captionEditor} {
+    %t #{attachmentSelector} figcaption textarea {
       resize: none;
     }

-    %t .#{classNames.attachment.captionEditor}.trix-autoresize-clone {
+    %t #{attachmentSelector} figcaption textarea.trix-autoresize-clone {
       position: absolute;
       left: -9999px;
       max-height: 0px;


Fix copy in apm list

Fix copy in apm list

--- src/list.coffee
+++ src/list.coffee
@@ -82,7 +82,7 @@
     if options.argv.themes
       console.log "#{'Built-in Atom themes'.cyan} (#{packages.length})"
     else
-      console.log "#{'Built-in Atom themes'.cyan} (#{packages.length})"
+      console.log "#{'Built-in Atom packages'.cyan} (#{packages.length})"
     @logPackages(packages)

   run: (options) ->


bigger icon in linuxgit diff

bigger icon in linuxgit diff

--- src/main.coffee
+++ src/main.coffee
@@ -113,7 +113,7 @@
         height: 590
         "min-width": 620
         "min-height": 420
-        icon: path.join __dirname, 'icons', 'icon.png'
+        icon: path.join __dirname, 'icons', 'icon@32.png'
         show: false
         titleBarStyle: 'hidden-inset' if process.platform is 'darwin'
         # autoHideMenuBar : true unless process.platform is 'darwin'


Cleanup SVG tags

Cleanup SVG tags

--- src/main.coffee
+++ src/main.coffee
@@ -2,7 +2,7 @@

 _.mixin(_.str.exports())
 bind = rx.bind
-rxt.importTags()
+T = rxt.tags

 # Dependencies

@@ -12,66 +12,26 @@
 # Import SVG Tags for reactive-coffee 1.2.2; should be upstreamed in 1.2.4 or so

 tags = ['svg','g','rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path', 'marker', 'text', 'tspan', 'tref', 'textpath', 'switch', 'image', 'a', 'defs', 'symbol', 'use', 'animateTransform', 'stop', 'linearGradient', 'radialGradient', 'pattern', 'clipPath', 'mask', 'filter', 'feMerge', 'feOffset', 'feGaussianBlur', 'feMergeNode']
-T = _.object([tag, rxt.mktag(tag)] for tag in tags)
-
+S = _.object([tag, rxt.mktag(tag)] for tag in tags)

 # Define our main view

 main = ->

-  # Reactive cell to track currently selected item
-  # Default is the first item
-  currentItem = rx.cell(items.at(0))
-
   $('body').append(
-    div {class: 'item-manager'}, [
-      h1 bind -> "#{items.length()} Items"
-
-      # Display list of existing items
-
-      ul {class: 'items'}, items.map (item) ->
-        li {
-          class: 'item'
-          # Animate creation with JQuery efffect
-          init: -> _.defer => @slideDown('fast')
-        }, [
-          span {
-            class: 'data'
-          }, bind -> "#{item.data.get()} "
-          a {
-            href: 'javascript: void 0'
-            click: -> currentItem.set(item)
-          }, 'Edit'
-        ]
-
-      # Create a new item
-
-      button {
-        click: ->
-          title = "Item ##{items.length()+1}"
-          items.push(new Item(title))
-      }, 'Add Item'
-
-      # Edit selected item
-
-      editor {
-        item: bind -> currentItem.get()
-        onSubmit: (data) ->
-          currentItem.get().data.set(data)
-      }
-    ]
-    T.svg {class: 'graphics'}, bind ->[
-     T.rect {x:10, y:20, height:100, width:100, fill:"blue", stroke: "red"}, bind -> [
-       T.animateTransform({
-        attributeName: "transform"
-        begin: "0s"
-        dur: "20s"
-        type: "rotate"
-        from: "0 60 60"
-        to: "360 60 60"
-        repeatCount: "indefinite"
-       })
-     ]
+    T.h1 "Rohan's Teenage Robot Turtles"
+    S.svg {class: 'graphics'}, bind ->[
+      S.rect {x:10, y:20, height:100, width:100, fill:"blue", stroke:"red"}, bind -> [
+        S.animatetransform {
+          attributeName: "transform"
+          begin: "0s"
+          dur: "20s"
+          type: "rotate"
+          from: "0 60 60"
+          to: "360 60 60"
+          repeatCount: "indefinite"
+        }
+      ]
     ]
   )


加入统一的分页方法，并改善find

加入统一的分页方法，并改善find

--- biz/BaseEntity.coffee
+++ biz/BaseEntity.coffee
@@ -1,4 +1,5 @@
 _store = require './store'
+_async = require 'async'

 class BaseEntity
   constructor: (@member)->
@@ -21,7 +22,7 @@
     return _store.database()(this.schema.name)

   #简单的搜索
-  find: (condition, options, callback)->
+  find: (condition, options, cb)->
     if typeof options is 'function'
       callback = options
       options = {}
@@ -31,33 +32,64 @@
     for key, value of condition
       delete condition[key] if value is undefined

-    exec = this.entity()
-    .where condition
+    queue = []
+    self = @
+    #查询总记录数
+    queue.push(
+      (done)->
+        #如果有匹配id，不需要分页
+        return done null, null if condition.id

-    #如果存在
-    options.beforeQuery?(exec)
-    if typeof options.fields is 'function'
-      options.fields exec
-    else
-      exec.select(options.fields || '*')
+        exec = self.entity().where condition
+        options.beforeQuery?(exec, true)
+        exec.select exec.knex.raw('count(*)')
+        console.log exec.toString()

-    sql = exec.toString()
-    console.log sql
+        #汇总统计
+        self.scalar exec.toString(), (err, count)-> done null, count
+    )

-    exec.then((items)->
-      #如果存在id，则表示查找单条数据
-      if condition.id
-        result = if items.length == 0 then null else items[0]
-      else
-        result =
-          items: items
-          pagination:
-            page_index: 1,
-            page_size: 10
+    queue.push(
+      (count, done)->
+        exec = self.entity().where condition

-      callback null, result
+        #如果存在
+        options.beforeQuery?(exec)
+        if typeof options.fields is 'function'
+          options.fields exec
+        else
+          exec.select(options.fields || '*')
+
+        #加入排序
+        exec.orderBy key, value for key, value of options.orderBy || {}
+
+        #如果有使用分页
+        page = options.pagination
+        if page
+          #整理数据，防止提交的数据不对
+          page.limit = page.limit || 10
+          page.offset = page.offset || 0
+          page.count = count
+
+          exec.limit page.limit
+          exec.offset page.offset
+
+        sql = exec.toString()
+        console.log sql
+
+        exec.then (items)->
+          #如果存在id，则表示查找单条数据
+          if condition.id
+            result = if items.length == 0 then null else items[0]
+          else
+            result =
+              items: items
+              pagination: page
+          done null, result
     )
-    #console.log sql
+
+
+    _async.waterfall queue, cb

   #简单的存储
   save: (data, callback)->


Run copy-i18n-tags (#6422)

Run copy-i18n-tags (#6422)

Co-authored-by: CoCo CI <5df1af3d7600b97ee69552caffeccf9dfd29ab8f@codecombat.com>

--- app/locale/rot13.coffee
+++ app/locale/rot13.coffee
@@ -2045,7 +2045,7 @@
     hoc_programming: "WninFpevcg/Clguba Cebtenzzvat"
     hoc_teacher_led: "Grnpure-Yrq Yrffbaf"
     hoc_teacher_led_desc1: "Qbjaybnq bhe"
-    hoc_teacher_led_link: "Ubhe bs Pbqr yrffba cynaf"  # {punatr}
+    hoc_teacher_led_link: "Ubhe bs Pbqr yrffba cynaf"
     hoc_teacher_led_desc2: "gb vagebqhpr lbhe fghqragf gb cebtenzzvat pbaprcgf hfvat bssyvar npgvivgvrf"
     hoc_group: "Tebhc Tnzrcynl"
     hoc_group_desc_1: "Grnpuref pna hfr gur yrffbaf va pbawhapgvba jvgu bhe Vagebqhpgvba gb Pbzchgre Fpvrapr pbhefr gb genpx fghqrag cebterff. Frr bhe"


Pass tunnel identifier to webdriver so it actually uses the tunnel. Send other build metadata to Sauce.

--- smoke-test.coffee
+++ smoke-test.coffee
@@ -17,6 +17,19 @@
   logger: console.log
 }

+# http://docs.travis-ci.com/user/ci-environment/#Environment-variables
+# http://docs.drone.io/env.html  (Jenkins compatible)
+# https://www.codeship.io/documentation/continuous-integration/set-environment-variables/
+env = process.env
+build = env.BUILD_ID || env.TRAVIS_BUILD_ID || env.CI_BUILD_NUMBER
+buildUrl = env.BUILD_URL || env.CI_BUILD_URL || build
+commit = env.GIT_COMMIT || env.TRAVIS_COMMIT
+tunnelId = build
+tags = []
+tags.push('travis') if env.TRAVIS
+tags.push('drone') if env.DRONE
+tags.push(env.CI_NAME) if env.CI_NAME
+
 browser = wd.remote('ondemand.saucelabs.com', 80, sauceUser, sauceKey)

 browser.on 'status', (info) ->
@@ -31,15 +44,21 @@
   version: '8'
   platform: 'Windows XP'
   name: 'smoke test'
+  build: "commit #{commit} build #{buildUrl}"
+  tags: tags
+  # Most my tests timeout a lot due crashing without cleanup (see below);
+  # this will waste less Sauce resources than default 90s.
+  'idle-timeout': 30
 }

 # TODO: Cleanup even if there were errors.  Use promises for sanity?
+#  Use a test runner with guaranteed pre/post methods?

 test = (cb) ->
   # TODO: use current source (via Sauce Connect?)
   browser.get 'http://localhost:8000/?doc=_mathdown_test_smoke', (err) ->
     assert.ifError(err)
-    browser.waitFor wd.asserters.jsCondition('document.title.match(/smoke test/)', 2000), (err, value) ->
+    browser.waitFor wd.asserters.jsCondition('document.title.match(/smoke test/)', 10000), (err, value) ->
       assert.ifError(err)
       browser.waitForElementByCss '.MathJax_Display', 15000, (err, el) ->
         assert.ifError(err)
@@ -59,10 +78,13 @@
 server.listen(8000)
 console.log('Server up, e.g. http://localhost:8000/?doc=_mathdown_test_smoke')

-tunnel = new sauceTunnel(sauceUser, sauceKey, undefined, true, ['--verbose'])
+tunnel = new sauceTunnel(sauceUser, sauceKey, tunnelId, true, ['--verbose'])
+console.log('Creating tunnel...')
 tunnel.start (status) ->
   assert(status, 'tunnel creation failed')
   console.log('tunnel created')
+  desired['tunnel-identifier'] = tunnel.identifier
+  console.log(desired)
   browser.init desired, (err) ->
     assert.ifError(err)
     test ->


Add bubbles on click.

Add bubbles on click.

--- app/scripts/main.coffee
+++ app/scripts/main.coffee
@@ -1,17 +1,33 @@
 setup = ->
-  aPaper = Raphael("a-timestream", 500, 50)
-  line = aPaper.path("M0 25L500 25").attr("stroke-width:": 3, "stroke": "blue")
-  addBubble = (text, paper) ->
-    circle = paper.circle(15, 25, 15).attr("stroke-width": 0, "fill": "blue")
-    label = paper.text(15, 25, text).attr("font-size": 10, "stroke": "white")
+  setupTimeline = (id, options = {}) ->
+    paper = Raphael(id, 500, 50)
+    line = paper.path("M0 25L500 25").attr
+      "stroke-width": 3
+      "stroke": options.fillColor
+    paper
+
+  addBubble = (text, paper, options = {}) ->
+    circle = paper.circle(15, 25, 15).attr
+      "stroke-width": 0
+      "fill": options.fillColor
+    label = paper.text(15, 25, text).attr
+      "font-size": 10
+      "stroke": options.textColor
     group = paper.set()
     group.push(circle, label)
     end = 500 - 15
     group.animate(cx: end, x: end, 3000, 'linear', -> group.remove())

-  aClicks = $("#button-a").asEventStream('click')
-  aPapers = Bacon.constant(aPaper)
-  aLabels = Bacon.constant("A")
+  aColors = { fillColor: "blue", textColor: "white" }
+  aPaper = setupTimeline "a-timestream", aColors
+  bColors = { fillColor: "red", textColor: "white" }
+  bPaper = setupTimeline "b-timestream", bColors

-  # TODO: finish this
+  # streams
+  aClick = $('#button-a').asEventStream('click')
+  bClick = $('#button-b').asEventStream('click')
+
+  # side effects
+  aClick.map(["A", aPaper, aColors]).onValues(addBubble)
+  bClick.map(["B", bPaper, bColors]).onValues(addBubble)
 $(setup)


Move to inside the main Graph describe

Move to inside the main Graph describe

--- spec/Graph.coffee
+++ spec/Graph.coffee
@@ -4,12 +4,11 @@
 else
   noflo = require 'noflo'

-describe 'Unnamed graph instance', ->
-  it 'should have an empty name', ->
-    g = new noflo.graph.Graph
-    chai.expect(g.name).to.equal ''
-
-describe 'Graph', ->
+describe 'NoFlo Graph', ->
+  describe 'Unnamed graph instance', ->
+    it 'should have an empty name', ->
+      g = new noflo.graph.Graph
+      chai.expect(g.name).to.equal ''
   describe 'with new instance', ->
     g = null
     it 'should get a name from constructor', ->


update grunt main file

update grunt main file

--- public/Gruntfile.coffee
+++ public/Gruntfile.coffee
@@ -2,12 +2,6 @@

   # Load task configurations.
   @loadTasks "build/tasks"
-
-  # Run JSHint and a quick test.
-  # @registerTask "test", [
-    # "jshint"
-    # "karma:run"
-  # ]

   @registerTask "development", [
     "less:dev"


use the s3 xml response to set the file URL to avoid unicode confusion

--- app/assets/javascripts/s3_direct_upload.js.coffee
+++ app/assets/javascripts/s3_direct_upload.js.coffee
@@ -41,13 +41,13 @@
           data.context.find('.bar').css('width', progress + '%')

       done: (e, data) ->
-        content = build_content_object $uploadForm, data.files[0]
-
+        content = build_content_object $uploadForm, data.files[0], data.result
+
         to = $uploadForm.data('post')
         if to
           content[$uploadForm.data('as')] = content.url
           $.post(to, content)
-
+
         data.context.remove() if data.context && settings.remove_completed_progress_bar # remove progress bar
         $uploadForm.trigger("s3_upload_complete", [content])

@@ -56,7 +56,7 @@
           $(document).trigger("s3_uploads_complete")

       fail: (e, data) ->
-        content = build_content_object $uploadForm, data.files[0]
+        content = build_content_object $uploadForm, data.files[0], data.result
         content.error_thrown = data.errorThrown
         $uploadForm.trigger("s3_upload_failed", [content])

@@ -73,13 +73,15 @@

         data

-  build_content_object = ($uploadForm, file) ->
-    domain = $uploadForm.attr('action')
-    path = settings.path + $uploadForm.find('input[name=key]').val().replace('/${filename}', '')
+  build_content_object = ($uploadForm, file, result) ->
+    domain           = $uploadForm.attr('action')
+    path             = $('Key', result).text()
+    split_path       = path.split('/')
+
     content          = {}
-    content.url      = domain + path + '/' + file.name
-    content.filename = file.name
-    content.filepath = path
+    content.url      = domain + path
+    content.filename = split_path[split_path.length - 1]
+    content.filepath = split_path.slice(0, split_path.length - 1).join('/')
     content.filesize = file.size if 'size' of file
     content.filetype = file.type if 'type' of file
     content = $.extend content, settings.additional_data if settings.additional_data


avoid the blank content of via

avoid the blank content of via

--- app/assets/javascripts/social-share-button.coffee
+++ app/assets/javascripts/social-share-button.coffee
@@ -25,7 +25,8 @@
       when "weibo"
         SocialShareButton.openUrl("http://service.weibo.com/share/share.php?url=#{url}&type=3&pic=#{img}&title=#{title}&appkey=#{appkey}",popup)
       when "twitter"
-        SocialShareButton.openUrl("https://twitter.com/intent/tweet?url=#{url}&text=#{title}&via=#{via}",popup)
+        via_str = via.length > 0 ? "&via=#{via}" : ''
+        SocialShareButton.openUrl("https://twitter.com/intent/tweet?url=#{url}&text=#{title}#{via_str}",popup)
       when "douban"
         SocialShareButton.openUrl("http://shuo.douban.com/!service/share?href=#{url}&name=#{title}&image=#{img}&sel=#{desc}",popup)
       when "facebook"


Use single quotes to avoid coffeescript parsing string as variable

--- lib/js/integrations/slack_integration.coffee
+++ lib/js/integrations/slack_integration.coffee
@@ -3,7 +3,7 @@
 config = require('../../../config.json').slack;

 class SlackIntegration
-  DEFAULT_MESSAGE = "#{player} is playing #{game}. Go join them!"
+  DEFAULT_MESSAGE = '#{player} is playing #{game}. Go join them!'

   constructor: (opts)->
     {
@@ -42,8 +42,8 @@
     notifyChannels

   formatMessage: (message, player, game)->
-    message.replace('#{player}', player) if player
-    message.replace('#{game}', game) if game
+    message.replace('#{player}', player)
+    message.replace('#{game}', game)
     message

   module.exports = SlackIntegration


Add default message if none defined in config

--- lib/js/integrations/slack_integration.coffee
+++ lib/js/integrations/slack_integration.coffee
@@ -3,6 +3,7 @@
 config = require('../../../config.json').slack;

 class SlackIntegration
+  DEFAULT_MESSAGE = "#{player} is playing #{game}. Go join them!"

   constructor: (opts)->
     {
@@ -29,7 +30,9 @@
     return if not @config_channels or not @slack_channels

     channels = @getChannelsToNotify(player)
-    message = config.message.replace('#{player}', player).replace('#{game}', game)
+
+    message = if config.message then formatMessage(config.message, player, game) else formatMessage(DEFAULT_MESSAGE, player, game)
+
     channel.send(message) for channel in channels

   getChannelsToNotify: (player)=>
@@ -38,5 +41,9 @@

     notifyChannels

+  formatMessage: (message, player, game)->
+    message.replace('#{player}', player) if player
+    message.replace('#{game}', game) if game
+    message

   module.exports = SlackIntegration


Fix boss popup

Fix boss popup

--- app/scripts/lazerscripts/stage1/stage1boss.coffee
+++ app/scripts/lazerscripts/stage1/stage1boss.coffee
@@ -275,7 +275,7 @@
     )

   execute: ->
-    @bindSequence 'Hit', @leaveScreen, => @entity.health < 130000
+    @bindSequence 'Hit', @leaveScreen, => @entity.health < 230000
     @inventoryAdd 'item', 'xp', ->
       Crafty.e('PowerUp').powerUp(contains: 'xp', marking: 'X')


first work

first work

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -799,3 +799,47 @@
 exports.import.params = {
   host: { type: 'string' }
 }
+
+exports.diskAdd = ({id, name, size, SR, position}) ->
+  try
+    VM = @getObject id, 'VM'
+    SR = @getObject SR, 'SR'
+  catch
+    @throw 'NO_SUCH_OBJECT'
+
+  xapi = @getXAPI VM
+  VDI_ref = $wait xapi.call 'VDI.create', {
+    name_label: name
+    virtual_size: size
+    type: 'user'
+    SR: SR.ref
+    sharable : false
+    read_only : false
+    other_config: {}
+  }
+
+  VBD_ref = $wait xapi.call 'VBD.create', {
+    VM: VM.ref
+    VDI: VDI_ref
+    mode: 'RW'
+    type: 'Disk'
+    userdevice: position
+    bootable : false
+    empty : false
+    other_config: {}
+    qos_algorithm_type: ''
+    qos_algorithm_params: {}
+  }
+
+  $wait xapi.call 'VBD.plug', VBD_ref
+
+  return true
+
+exports.diskAdd.permission = 'admin'
+exports.diskAdd.params = {
+  id: { type: 'string' }
+  name: { type: 'string' }
+  size: { type: 'string' }
+  SR: { type: 'string' }
+  position: { type: 'string' }
+}


Actually implement `prevent` and `allow` in EventEmitter.

--- src/batman.coffee
+++ src/batman.coffee
@@ -319,13 +319,16 @@
       result = wrappedFunction.apply(this, arguments)
       @event('change').fire(this, this)
       result
-
-for k in ['prevent', 'allow', 'fire', 'isPrevented']
-  do (k) ->
-    Batman.EventEmitter[k] = (key, args...) ->
-      @event(key)[k](args...)
-      @
-
+  prevent: (key) ->
+    @event(key).prevent()
+    @
+  allow: (key) ->
+    @event(key).allow()
+    @
+  isPrevented: (key) ->
+    @event(key).isPrevented()
+  fire: (key, args...) ->
+    @event(key).fire(args...)

 class Batman.PropertyEvent extends Batman.Event
   eachHandler: (iterator) -> @base.eachObserver(iterator)
@@ -364,7 +367,7 @@
   hashKey: ->
     @hashKey = -> key
     key = "<Batman.Property base: #{Batman.Hash::hashKeyFor(@base)}, key: \"#{Batman.Hash::hashKeyFor(@key)}\">"
-
+
   changeEvent: ->
     event = @event('change')
     @changeEvent = -> event
@@ -419,7 +422,7 @@
     handler = => @_handleSourceChange()
     @sourceChangeHandler = -> handler
     handler
-
+
   _handleSourceChange: ->
     if @isIsolated()
       @_needsRefresh = yes


refactor promise accessors to use wrapAccessor macro

--- src/batman.coffee
+++ src/batman.coffee
@@ -947,11 +947,25 @@
   getAccessorObject = (base, accessor) ->
     if typeof accessor is 'function'
       accessor = {get: accessor}
-    else if typeof accessor.promise is 'function'
-      accessor = new Batman.PromiseAccessor(accessor.promise, Batman.Property.defaultAccessorForBase(base))
     accessor

+  promiseWrapper = (fetcher) ->
+    (core) ->
+      get: (key) ->
+        val = core.get.apply(this, arguments)
+        return val if (typeof val isnt 'undefined')
+        returned = false
+        deliver = (err, result) =>
+          @set(key, result) if returned
+          val = result
+        fetcher.call(this, deliver, key)
+        returned = true
+        val
+      cachable: true
+
   @classAccessor: (keys..., accessor) ->
+    if typeof accessor.promise is 'function'
+      return @wrapAccessor(keys..., promiseWrapper(accessor.promise))
     Batman.initializeObject @
     # Create a default accessor if no keys have been given.
     if keys.length is 0
@@ -1004,23 +1018,6 @@
     @classAccessor singletonMethodName,
       get: -> @["_#{singletonMethodName}"] ||= new @

-
-class Batman.PromiseAccessor
-  constructor: (fetcher, wrappedAccessor = Batman.Property.defaultAccessor) ->
-    @[k] = v for k,v of wrappedAccessor
-    @get = (key) ->
-      val = wrappedAccessor.get.apply(this, arguments)
-      return val if (typeof val isnt 'undefined')
-      returned = false
-      deliver = (err, result) =>
-        @set(key, result) if returned
-        val = result
-      fetcher.call(this, deliver, key)
-      returned = true
-      val
-    @cachable = true
-
-
 Batman.Object = BatmanObject

 class Batman.Accessible extends Batman.Object


removed legacy todo comment

removed legacy todo comment

--- src/brunch.coffee
+++ src/brunch.coffee
@@ -71,7 +71,6 @@
   exports.spawnStylus()

 exports.stop = ->
-  # TODO check out SIGHUP signal
   expressProcess.kill 'SIGHUP' unless expressProcess is {}

 # creates a stitch package for app directory and include vendor as dependencies


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -35,7 +35,7 @@
       width: 2 * deadzone.x
       height: 2 * deadzone.y

-    if objectCe
+    if objectCenter.x
     I.scroll = Point(
       I.scroll.x.clamp(centerRect.left, centerRect.right)
       I.scroll.y.clamp(centerRect.top, centerRect.bottom)


fix listeners

fix listeners

--- src/chance.coffee
+++ src/chance.coffee
@@ -17,8 +17,7 @@
 coinValues = ['heads', 'tails']

 module.exports = (robot) ->
-  robot.respond /roll di/i, (msg) ->
-    count = if msg.match[1]? then parseInt(msg.match[1], 10) else 1
+  robot.respond /(roll di)?/i, (msg) ->
     msg.send msg.random diceValues

   robot.respond /roll( \d+)? dice/i, (msg) ->


debounce the html/markdown preview

debounce the html/markdown preview

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -3784,7 +3784,7 @@
         @spell_check()

         cm = @cm()
-        cm.on('change', @update_preview)
+        cm.on('change', _.debounce(@update_preview,500))
         #cm.on 'cursorActivity', @update_preview

         @init_buttons()