async = require('async')
markdownlib = require('../markdown')
misc = require('smc-util/misc')
{defaults, required} = misc
schema = require('smc-util/schema')
{webapp_client} = require('../webapp_client')
{STEPS, previous_step, step_direction, step_verb, step_ready} = require('./util')
{Actions, Store} = require('../smc-react')
PARALLEL_LIMIT = 5
primary_key =
students : 'student_id'
assignments : 'assignment_id'
handouts : 'handout_id'
exports.CourseActions = class CourseActions extends Actions
constructor: (@name, @redux) ->
if not @name?
throw Error("@name must be defined")
if not @redux?
throw Error("@redux must be defined")
@get_store = () => @redux.getStore(@name)
_loaded: =>
if not @syncdb?
@set_error("attempt to set syncdb before loading")
return false
return true
_store_is_initialized: =>
store = @get_store()
return if not store?
if not (store.get('students')? and store.get('assignments')? and store.get('settings')? and store.get('handouts'))
@set_error("store must be initialized")
return false
return true
_set: (obj) =>
if not @_loaded() or @syncdb?.is_closed()
return
@syncdb.set(obj)
_get_one: (obj) =>
if @syncdb?.is_closed()
return
return @syncdb.get_one(obj)?.toJS()
set_tab: (tab) =>
@setState(tab:tab)
save: =>
store = @get_store()
return if not store?
if store.get('saving')
return
id = @set_activity(desc:"Saving...")
@setState(saving:true)
@syncdb.save (err) =>
@clear_activity(id)
@setState(saving:false)
@setState(unsaved:@syncdb?.has_unsaved_changes())
if err
@set_error("Error saving -- #{err}")
@setState(show_save_button:true)
else
@setState(show_save_button:false)
_syncdb_change: (changes) =>
store = @get_store()
return if not store?
cur = t = store.getState()
changes.map (obj) =>
table = obj.get('table')
if not table?
return
x = @syncdb.get_one(obj)
key = primary_key[table]
if not x?
if key?
t = t.set(table, t.get(table).delete(obj.get(key)))
else
if key?
t = t.set(table, t.get(table).set(x.get(key), x))
else if table == 'settings'
t = t.set(table, t.get(table).merge(x.delete('table')))
else
console.warn("unknown table '#{table}'")
return
if not cur.equals(t)
@setState(t)
@setState(unsaved:@syncdb?.has_unsaved_changes())
handle_projects_store_update: (state) =>
store = @get_store()
return if not store?
users = state.getIn(['project_map', store.get('course_project_id'), 'users'])?.keySeq()
if not users?
return
if not @_last_collaborator_state?
@_last_collaborator_state = users
return
if not @_last_collaborator_state.equals(users)
@configure_all_projects()
@_last_collaborator_state = users
set_error: (error) =>
if error == ''
@setState(error:error)
else
@setState(error:((@get_store()?.get('error') ? '') + '\n' + error).trim())
set_activity: (opts) =>
opts = defaults opts,
id : undefined
desc : undefined
if not opts.id? and not opts.desc?
return
if not opts.id?
@_activity_id = (@_activity_id ? 0) + 1
opts.id = @_activity_id
store = @get_store()
if not store?
return
x = store.get_activity()?.toJS()
if not x?
x = {}
if not opts.desc?
delete x[opts.id]
else
x[opts.id] = opts.desc
@setState(activity: x)
return opts.id
clear_activity: (id) =>
if id?
@set_activity(id:id)
else
@setState(activity:{})
set_title: (title) =>
@_set(title:title, table:'settings')
@set_all_student_project_titles(title)
@set_shared_project_title()
set_description: (description) =>
@_set(description:description, table:'settings')
@set_all_student_project_descriptions(description)
@set_shared_project_description()
set_upgrade_goal: (upgrade_goal) =>
@_set(upgrade_goal:upgrade_goal, table:'settings')
set_allow_collabs: (allow_collabs) =>
@_set(allow_collabs:allow_collabs, table:'settings')
@configure_all_projects()
set_email_invite: (body) =>
@_set(email_invite:body, table:'settings')
shared_project_settings: (title) =>
store = @get_store()
return if not store?
x =
title : "Shared Project -- #{title ? store.getIn(['settings', 'title'])}"
description : store.getIn(['settings', 'description']) + "\n---\n This project is shared with all students."
return x
set_shared_project_title: =>
store = @get_store()
shared_id = store?.get_shared_project_id()
return if not store? or not shared_id
title = @shared_project_settings().title
@redux.getActions('projects').set_project_title(shared_id, title)
set_shared_project_description: =>
store = @get_store()
shared_id = store?.get_shared_project_id()
return if not store? or not shared_id
description = @shared_project_settings().description
@redux.getActions('projects').set_project_description(shared_id, description)
action_shared_project: (action) =>
if action not in ['start', 'stop', 'restart']
throw Error("action must be start, stop or restart")
store = @get_store()
return if not store?
shared_project_id = store.get_shared_project_id()
if not shared_project_id
return
@redux.getActions('projects')[action+"_project"]?(shared_project_id)
configure_shared_project: =>
store = @get_store()
return if not store?
shared_project_id = store.get_shared_project_id()
if not shared_project_id
return
@set_shared_project_title()
projects = @redux.getStore('projects')
shared_project_users = projects.get_users(shared_project_id)
if not shared_project_users?
return
course_project_users = projects.get_users(store.get('course_project_id'))
if not course_project_users?
return
student_account_ids = {}
store.get_students().map (student, _) =>
if not student.get('deleted')
account_id = student.get('account_id')
if account_id?
student_account_ids[account_id] = true
actions = @redux.getActions('projects')
if not store.get_allow_collabs()
shared_project_users.map (_, account_id) =>
if not course_project_users.get(account_id) and not student_account_ids[account_id]
actions.remove_collaborator(shared_project_id, account_id)
course_project_users.map (_, account_id) =>
if not shared_project_users.get(account_id)
actions.invite_collaborator(shared_project_id, account_id)
for account_id, _ of student_account_ids
if not shared_project_users.get(account_id)
actions.invite_collaborator(shared_project_id, account_id)
_set_shared_project_id: (project_id) =>
@_set
table : 'settings'
shared_project_id : project_id
create_shared_project: () =>
store = @get_store()
return if not store?
if store.get_shared_project_id()
return
id = @set_activity(desc:"Creating global shared project for everybody.")
x = @shared_project_settings()
x.token = misc.uuid()
@redux.getActions('projects').create_project(x)
@redux.getStore('projects').wait_until_project_created x.token, 30, (err, project_id) =>
@clear_activity(id)
if err
@set_error("error creating shared project -- #{err}")
else
@_set_shared_project_id(project_id)
@configure_shared_project()
set_course_info: (pay='') =>
@_set
pay : pay
table : 'settings'
@set_all_student_project_course_info(pay)
toggle_item_expansion: (item_name, item_id) =>
store = @get_store()
return if not store?
field_name = "expanded_#{item_name}s"
expanded_items = store.get(field_name)
if expanded_items.has(item_id)
adjusted = expanded_items.delete(item_id)
else
adjusted = expanded_items.add(item_id)
@setState("#{field_name}" : adjusted)
add_students: (students) =>
student_ids = []
for x in students
student_id = misc.uuid()
student_ids.push(student_id)
x.table = 'students'
x.student_id = student_id
@syncdb.set(x)
f = (student_id, cb) =>
async.series([
(cb) =>
store = @get_store()
if not store?
cb("store not defined"); return
store.wait
until : (store) => store.get_student(student_id)
timeout : 60
cb : cb
(cb) =>
@create_student_project(student_id)
store = @get_store()
if not store?
cb("store not defined"); return
store.wait
until : (store) => store.get_student(student_id).get('project_id')
timeout : 60
cb : cb
], cb)
id = @set_activity(desc:"Creating #{students.length} student projects (do not close this until done)")
async.mapLimit student_ids, PARALLEL_LIMIT, f, (err) =>
@set_activity(id:id)
if err
@set_error("error creating student projects -- #{err}")
@configure_all_projects()
delete_student: (student) =>
store = @get_store()
return if not store?
student = store.get_student(student)
@redux.getActions('projects').clear_project_upgrades(student.get('project_id'))
@_set
deleted : true
student_id : student.get('student_id')
table : 'students'
@configure_all_projects()
undelete_student: (student) =>
store = @get_store()
return if not store?
student = store.get_student(student)
@_set
deleted : false
student_id : student.get('student_id')
table : 'students'
@configure_all_projects()
lookup_nonregistered_students: =>
store = @get_store()
if not store?
console.warn("lookup_nonregistered_students: store not initialized")
return
v = {}
s = []
store.get_students().map (student, student_id) =>
if not student.get('account_id') and not student.get('deleted')
email = student.get('email_address')
v[email] = student_id
s.push(email)
if s.length > 0
webapp_client.user_search
query : s.join(',')
limit : s.length
cb : (err, result) =>
if err
console.warn("lookup_nonregistered_students: search error -- #{err}")
else
for x in result
@_set
account_id : x.account_id
table : 'students'
student_id : v[x.email_address]
set_active_student_sort: (column_name) =>
store = @get_store()
if not store?
return
current_column = store.getIn(['active_student_sort', 'column_name'])
if current_column == column_name
is_descending = not store.getIn(['active_student_sort', 'is_descending'])
else
is_descending = false
@setState(active_student_sort : {column_name, is_descending})
set_internal_student_info: (student, info) =>
store = @get_store()
return if not store?
student = store.get_student(student)
info = defaults info,
first_name : required
last_name : required
email_address : student.get('email_address')
@_set
first_name : info.first_name
last_name : info.last_name
email_address : info.email_address
student_id : student.get('student_id')
table : 'students'
@configure_all_projects()
create_student_project: (student) =>
store = @get_store()
return if not store?
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?
@_create_student_project_queue = [student]
else
@_create_student_project_queue.push(student)
if not @_creating_student_project
@_process_create_student_project_queue()
_process_create_student_project_queue: () =>
@_creating_student_project = true
queue = @_create_student_project_queue
student = queue[0]
store = @get_store()
return if not store?
student_id = store.get_student(student).get('student_id')
@_set
create_project : webapp_client.server_time()
table : 'students'
student_id : student_id
id = @set_activity(desc:"Create project for #{store.get_student_name(student_id)}.")
token = misc.uuid()
@redux.getActions('projects').create_project
title : store.getIn(['settings', 'title'])
description : store.getIn(['settings', 'description'])
token : token
@redux.getStore('projects').wait_until_project_created token, 30, (err, project_id) =>
@clear_activity(id)
if err
@set_error("error creating student project for #{store.get_student_name(student_id)} -- #{err}")
else
@_set
create_project : null
project_id : project_id
table : 'students'
student_id : student_id
@configure_project(student_id, undefined, project_id)
delete @_creating_student_project
queue.shift()
if queue.length > 0
@_process_create_student_project_queue()
configure_project_users: (student_project_id, student_id, do_not_invite_student_by_email) =>
users = @redux.getStore('projects').get_users(student_project_id)
if not users?
return
s = @get_store()
if not s?
return
body = s.get_email_invite()
invite = (x) =>
account_store = @redux.getStore('account')
name = account_store.get_fullname()
replyto = account_store.get_email_address()
if '@' in x
if not do_not_invite_student_by_email
title = s.getIn(['settings', 'title'])
subject = "SageMathCloud Invitation to Course #{title}"
body = body.replace(/{title}/g, title).replace(/{name}/g, name)
body = markdownlib.markdown_to_html(body).s
@redux.getActions('projects').invite_collaborators_by_email(student_project_id, x, body, subject, true, replyto, name)
else
@redux.getActions('projects').invite_collaborator(student_project_id, x)
student = s.get_student(student_id)
student_account_id = student.get('account_id')
if not student_account_id?
invite(student.get('email_address'))
else if not users?.get(student_account_id)?
invite(student_account_id)
target_users = @redux.getStore('projects').get_users(s.get('course_project_id'))
if not target_users?
return
target_users.map (_, account_id) =>
if not users.get(account_id)?
invite(account_id)
if not s.get_allow_collabs()
users.map (_, account_id) =>
if not target_users.get(account_id)? and account_id != student_account_id
@redux.getActions('projects').remove_collaborator(student_project_id, account_id)
configure_project_visibility: (student_project_id) =>
users_of_student_project = @redux.getStore('projects').get_users(student_project_id)
if not users_of_student_project?
return
users = @redux.getStore('projects').get_users(@get_store().get('course_project_id'))
if not users?
return
users.map (_, account_id) =>
x = users_of_student_project.get(account_id)
if x? and not x.get('hide')
@redux.getActions('projects').set_project_hide(account_id, student_project_id, true)
configure_project_title: (student_project_id, student_id) =>
store = @get_store()
if not store?
return
title = "#{store.get_student_name(student_id)} - #{store.getIn(['settings', 'title'])}"
@redux.getActions('projects').set_project_title(student_project_id, title)
action_all_student_projects: (action) =>
if action not in ['start', 'stop', 'restart']
throw Error("action must be start, stop or restart")
@action_shared_project(action)
act_on_student_projects = () =>
return @get_store()?.get_students()
.filter (student) =>
not student.get('deleted') and student.get('project_id')?
.map (student) =>
@redux.getActions('projects')[action+"_project"](student.get('project_id'))
if not act_on_student_projects()
return
if @prev_interval_id?
window.clearInterval(@prev_interval_id)
if @prev_timeout_id?
window.clearTimeout(@prev_timeout_id)
clear_state = () =>
window.clearInterval(@prev_interval_id)
@setState(action_all_projects_state : "any")
@prev_interval_id = window.setInterval(act_on_student_projects, 30000)
@prev_timeout_id = window.setTimeout(clear_state, 300000)
if action in ['start', 'restart']
@setState(action_all_projects_state : "starting")
else if action == 'stop'
@setState(action_all_projects_state : "stopping")
set_all_student_project_titles: (title) =>
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}"
if student_project_id?
actions.set_project_title(student_project_id, project_title)
configure_project_description: (student_project_id, student_id) =>
@redux.getActions('projects').set_project_description(student_project_id, @get_store()?.getIn(['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?
@redux.getActions('projects').set_project_description(student_project_id, description)
set_all_student_project_course_info: (pay) =>
store = @get_store()
if not store?
return
if not pay?
pay = store.get_pay()
else
@_set
pay : pay
table : 'settings'
store.get_students().map (student, student_id) =>
student_project_id = student.get('project_id')
student_account_id = student.get('account_id')
student_email_address = student.get('email_address')
if student_project_id?
@redux.getActions('projects').set_project_course_info(student_project_id,
store.get('course_project_id'), store.get('course_filename'), pay, student_account_id, student_email_address)
configure_project: (student_id, do_not_invite_student_by_email, student_project_id) =>
store = @get_store()
return if not store?
student_project_id = student_project_id ? store.getIn(['students', student_id, 'project_id'])
if not student_project_id?
@create_student_project(student_id)
else
@configure_project_users(student_project_id, student_id, do_not_invite_student_by_email)
@configure_project_visibility(student_project_id)
@configure_project_title(student_project_id, student_id)
@configure_project_description(student_project_id, student_id)
delete_project: (student_id) =>
store = @get_store()
return if not store?
student_project_id = store.getIn(['students', student_id, 'project_id'])
if student_project_id?
@redux.getActions('projects').delete_project(student_project_id)
@_set
create_project : null
project_id : null
table : 'students'
student_id : student_id
configure_all_projects: =>
id = @set_activity(desc:"Configuring all projects")
@setState(configure_projects:'Configuring projects')
store = @get_store()
if not store?
@set_activity(id:id)
return
for student_id in store.get_student_ids(deleted:false)
@configure_project(student_id, false)
@configure_shared_project()
@set_activity(id:id)
@set_all_student_project_course_info()
delete_all_student_projects: () =>
id = @set_activity(desc:"Deleting all student projects...")
store = @get_store()
if not store?
@set_activity(id:id)
return
for student_id in store.get_student_ids(deleted:false)
@delete_project(student_id)
@set_activity(id:id)
upgrade_all_student_projects: (upgrade_goal) =>
store = @get_store()
if not store?
return
plan = store.get_upgrade_plan(upgrade_goal)
if misc.len(plan) == 0
return
id = @set_activity(desc:"Adjusting upgrades on #{misc.len(plan)} student projects...")
for project_id, upgrades of plan
@redux.getActions('projects').apply_upgrades_to_project(project_id, upgrades, false)
setTimeout((=>@set_activity(id:id)), 5000)
admin_upgrade_all_student_projects: (quotas) =>
if not @redux.getStore('account').get('groups')?.contains('admin')
console.warn("must be an admin to upgrade")
return
store = @get_store()
if not store?
console.warn('unable to get store')
return
f = (project_id, cb) =>
x = misc.copy(quotas)
x.project_id = project_id
x.cb = (err, mesg) =>
if err or mesg.event == 'error'
console.warn("failed to set quotas for #{project_id} -- #{misc.to_json(mesg)}")
else
console.log("set quotas for #{project_id}")
cb(err)
webapp_client.project_set_quotas(x)
async.mapSeries store.get_student_project_ids(), f, (err) =>
if err
console.warn("FAIL -- #{err}")
else
console.log("SUCCESS")
set_student_note: (student, note) =>
store = @get_store()
return if not store?
student = store.get_student(student)
@_set
note : note
table : 'students'
student_id : student.get('student_id')
_collect_path: (path) =>
store = @get_store()
i = store.get('course_filename').lastIndexOf('.')
store.get('course_filename').slice(0,i) + '-collect/' + path
add_assignment: (path) =>
collect_path = @_collect_path(path)
path_parts = misc.path_split(path)
if path_parts.head
beginning = '/graded-'
else
beginning = 'graded-'
graded_path = path_parts.head + beginning + path_parts.tail
target_path = path
@_set
path : path
collect_path : collect_path
graded_path : graded_path
target_path : target_path
table : 'assignments'
assignment_id : misc.uuid()
delete_assignment: (assignment) =>
store = @get_store()
return if not store?
assignment = store.get_assignment(assignment)
@_set
deleted : true
assignment_id : assignment.get('assignment_id')
table : 'assignments'
undelete_assignment: (assignment) =>
store = @get_store()
return if not store?
assignment = store.get_assignment(assignment)
@_set
deleted : false
assignment_id : assignment.get('assignment_id')
table : 'assignments'
set_grade: (assignment, student, grade) =>
store = @get_store()
return if not store?
assignment = store.get_assignment(assignment)
student = store.get_student(student)
obj = {table:'assignments', assignment_id:assignment.get('assignment_id')}
grades = @_get_one(obj).grades ? {}
grades[student.get('student_id')] = grade
obj.grades = grades
@_set(obj)
set_active_assignment_sort: (column_name) =>
store = @get_store()
if not store?
return
current_column = store.getIn(['active_assignment_sort', 'column_name'])
if current_column == column_name
is_descending = not store.getIn(['active_assignment_sort', 'is_descending'])
else
is_descending = false
@setState(active_assignment_sort : {column_name, is_descending})
_set_assignment_field: (assignment, name, val) =>
store = @get_store()
return if not store?
assignment = store.get_assignment(assignment)
@_set
"#{name}" : val
table : 'assignments'
assignment_id : assignment.get('assignment_id')
set_due_date: (assignment, due_date) =>
if not typeof(due_date) == 'string'
due_date = due_date?.toISOString()
@_set_assignment_field(assignment, 'due_date', due_date)
set_assignment_note: (assignment, note) =>
@_set_assignment_field(assignment, 'note', note)
set_peer_grade: (assignment, config) =>
cur = assignment.get('peer_grade')?.toJS() ? {}
for k, v of config
cur[k] = v
@_set_assignment_field(assignment, 'peer_grade', cur)
update_peer_assignment: (assignment) =>
store = @get_store()
return if not store?
assignment = store.get_assignment(assignment)
if assignment.getIn(['peer_grade', 'map'])?
return
N = assignment.getIn(['peer_grade','number']) ? 1
map = misc.peer_grading(store.get_student_ids(), N)
@set_peer_grade(assignment, map:map)
copy_assignment_from_student: (assignment, student) =>
if @_start_copy(assignment, student, 'last_collect')
return
id = @set_activity(desc:"Copying assignment from a student")
finish = (err) =>
@clear_activity(id)
@_finish_copy(assignment, student, 'last_collect', err)
if err
@set_error("copy from student: #{err}")
store = @get_store()
return if not store?
if not @_store_is_initialized()
return finish("store not yet initialized")
if not student = store.get_student(student)
return finish("no student")
if not assignment = store.get_assignment(assignment)
return finish("no assignment")
student_name = store.get_student_name(student)
student_project_id = student.get('project_id')
if not student_project_id?
@clear_activity(id)
else
target_path = assignment.get('collect_path') + '/' + student.get('student_id')
@set_activity(id:id, desc:"Copying assignment from #{student_name}")
async.series([
(cb) =>
webapp_client.copy_path_between_projects
src_project_id : student_project_id
src_path : assignment.get('target_path')
target_project_id : store.get('course_project_id')
target_path : target_path
overwrite_newer : true
backup : true
delete_missing : false
exclude_history : false
cb : cb
(cb) =>
name = store.get_student_name(student, true)
webapp_client.write_text_file_to_project
project_id : store.get('course_project_id')
path : target_path + "/STUDENT - #{name.simple}.txt"
content : "This student is #{name.full}."
cb : cb
], finish)
return_assignment_to_student: (assignment, student) =>
if @_start_copy(assignment, student, 'last_return_graded')
return
id = @set_activity(desc:"Returning assignment to a student")
finish = (err) =>
@clear_activity(id)
@_finish_copy(assignment, student, 'last_return_graded', err)
if err
@set_error("return to student: #{err}")
store = @get_store()
if not store? or not @_store_is_initialized()
return finish("store not yet initialized")
grade = store.get_grade(assignment, student)
if not student = store.get_student(student)
return finish("no student")
if not assignment = store.get_assignment(assignment)
return finish("no assignment")
student_name = store.get_student_name(student)
student_project_id = student.get('project_id')
if not student_project_id?
@clear_activity(id)
else
@set_activity(id:id, desc:"Returning assignment to #{student_name}")
src_path = assignment.get('collect_path')
if assignment.getIn(['peer_grade', 'enabled'])
peer_graded = true
src_path += '-peer-grade/'
else
peer_graded = false
src_path += '/' + student.get('student_id')
async.series([
(cb) =>
content = "Your grade on this assignment:\n\n #{grade}"
if peer_graded
content += "\n\n\nPEER GRADED:\n\nYour assignment was peer graded by other students.\nYou can find the comments they made in the folders below."
webapp_client.write_text_file_to_project
project_id : store.get('course_project_id')
path : src_path + '/GRADE.txt'
content : content
cb : cb
(cb) =>
webapp_client.copy_path_between_projects
src_project_id : store.get('course_project_id')
src_path : src_path
target_project_id : student_project_id
target_path : assignment.get('graded_path')
overwrite_newer : true
backup : true
delete_missing : false
exclude_history : true
cb : cb
(cb) =>
if peer_graded
webapp_client.exec
project_id : student_project_id
command : 'rm ./*/GRADER*.txt'
timeout : 60
bash : true
path : assignment.get('graded_path')
cb : cb
else
cb(null)
], finish)
return_assignment_to_all_students: (assignment, new_only) =>
id = @set_activity(desc:"Returning assignments to all students #{if new_only then 'who have not already received it' else ''}")
error = (err) =>
@clear_activity(id)
@set_error("return to student: #{err}")
store = @get_store()
if not store? or not @_store_is_initialized()
return error("store not yet initialized")
if not assignment = store.get_assignment(assignment)
return error("no assignment")
errors = ''
peer = assignment.get('peer_grade')?.get('enabled')
f = (student_id, cb) =>
if not store.last_copied(previous_step('return_graded', peer), assignment, student_id, true)
cb(); return
if not store.has_grade(assignment, student_id)
cb(); return
if new_only
if store.last_copied('return_graded', assignment, student_id, true) and store.has_grade(assignment, student_id)
cb(); return
n = misc.mswalltime()
@return_assignment_to_student(assignment, student_id)
store.wait
timeout : 60*15
until : => store.last_copied('return_graded', assignment, student_id) >= n
cb : (err) =>
if err
errors += "\n #{err}"
cb()
async.mapLimit store.get_student_ids(deleted:false), PARALLEL_LIMIT, f, (err) =>
if errors
error(errors)
else
@clear_activity(id)
_finish_copy: (assignment, student, type, err) =>
if student? and assignment?
store = @get_store()
if not store?
return
student = store.get_student(student)
assignment = store.get_assignment(assignment)
obj = {table:'assignments', assignment_id:assignment.get('assignment_id')}
x = @_get_one(obj)?[type] ? {}
student_id = student.get('student_id')
x[student_id] = {time: misc.mswalltime()}
if err
x[student_id].error = err
obj[type] = x
@_set(obj)
_start_copy: (assignment, student, type) =>
if student? and assignment?
store = @get_store()
if not store?
return
student = store.get_student(student)
assignment = store.get_assignment(assignment)
obj = {table:'assignments', assignment_id:assignment.get('assignment_id')}
x = @_get_one(obj)?[type] ? {}
y = (x[student.get('student_id')]) ? {}
if y.start? and webapp_client.server_time() - y.start <= 15000
return true
y.start = misc.mswalltime()
x[student.get('student_id')] = y
obj[type] = x
@_set(obj)
return false
_stop_copy: (assignment, student, type) =>
if student? and assignment?
store = @get_store()
if not store?
return
student = store.get_student(student)
assignment = store.get_assignment(assignment)
obj = {table:'assignments', assignment_id:assignment.get('assignment_id')}
x = @_get_one(obj)?[type]
if not x?
return
y = (x[student.get('student_id')])
if not y?
return
if y.start?
delete y.start
x[student.get('student_id')] = y
obj[type] = x
@_set(obj)
copy_assignment_to_student: (assignment, student) =>
if @_start_copy(assignment, student, 'last_assignment')
return
id = @set_activity(desc:"Copying assignment to a student")
finish = (err) =>
@clear_activity(id)
@_finish_copy(assignment, student, 'last_assignment', err)
if err
@set_error("copy to student: #{err}")
store = @get_store()
if not store? or not @_store_is_initialized()
return finish("store not yet initialized")
if not student = store.get_student(student)
return finish("no student")
if not assignment = store.get_assignment(assignment)
return finish("no assignment")
student_name = store.get_student_name(student)
@set_activity(id:id, desc:"Copying assignment to #{student_name}")
student_project_id = student.get('project_id')
student_id = student.get('student_id')
src_path = assignment.get('path')
async.series([
(cb) =>
if not student_project_id?
@set_activity(id:id, desc:"#{student_name}'s project doesn't exist, so creating it.")
@create_student_project(student)
store = @get_store()
if not store?
cb("no store")
return
store.wait
until : => store.get_student_project_id(student_id)
cb : (err, x) =>
student_project_id = x
cb(err)
else
cb()
(cb) =>
due_date = store.get_due_date(assignment)
if not due_date?
cb(); return
webapp_client.write_text_file_to_project
project_id : store.get('course_project_id')
path : src_path + '/DUE_DATE.txt'
content : "This assignment is due\n\n #{due_date.toLocaleString()}"
cb : cb
(cb) =>
@set_activity(id:id, desc:"Copying files to #{student_name}'s project")
webapp_client.copy_path_between_projects
src_project_id : store.get('course_project_id')
src_path : src_path
target_project_id : student_project_id
target_path : assignment.get('target_path')
overwrite_newer : false
delete_missing : false
backup : true
exclude_history : true
cb : cb
], (err) =>
finish(err)
)
copy_assignment: (type, assignment_id, student_id) =>
switch type
when 'assigned'
@copy_assignment_to_student(assignment_id, student_id)
when 'collected'
@copy_assignment_from_student(assignment_id, student_id)
when 'graded'
@return_assignment_to_student(assignment_id, student_id)
when 'peer-assigned'
@peer_copy_to_student(assignment_id, student_id)
when 'peer-collected'
@peer_collect_from_student(assignment_id, student_id)
else
@set_error("copy_assignment -- unknown type: #{type}")
copy_assignment_to_all_students: (assignment, new_only) =>
desc = "Copying assignments to all students #{if new_only then 'who have not already received it' else ''}"
short_desc = "copy to student"
@_action_all_students(assignment, new_only, @copy_assignment_to_student, 'assignment', desc, short_desc)
copy_assignment_from_all_students: (assignment, new_only) =>
desc = "Copying assignment from all students #{if new_only then 'from whom we have not already copied it' else ''}"
short_desc = "copy from student"
@_action_all_students(assignment, new_only, @copy_assignment_from_student, 'collect', desc, short_desc)
peer_copy_to_all_students: (assignment, new_only) =>
desc = "Copying assignments for peer grading to all students #{if new_only then 'who have not already received their copy' else ''}"
short_desc = "copy to student for peer grading"
@_action_all_students(assignment, new_only, @peer_copy_to_student, 'peer_assignment', desc, short_desc)
peer_collect_from_all_students: (assignment, new_only) =>
desc = "Copying peer graded assignments from all students #{if new_only then 'from whom we have not already copied it' else ''}"
short_desc = "copy peer grading from students"
@_action_all_students(assignment, new_only, @peer_collect_from_student, 'peer_collect', desc, short_desc)
_action_all_students: (assignment, new_only, action, step, desc, short_desc) =>
id = @set_activity(desc:desc)
error = (err) =>
@clear_activity(id)
err="#{short_desc}: #{err}"
@set_error(err)
store = @get_store()
if not store? or not @_store_is_initialized()
return error("store not yet initialized")
if not assignment = store.get_assignment(assignment)
return error("no assignment")
errors = ''
peer = assignment.get('peer_grade')?.get('enabled')
prev_step = previous_step(step, peer)
f = (student_id, cb) =>
if prev_step? and not store.last_copied(prev_step, assignment, student_id, true)
cb(); return
if new_only and store.last_copied(step, assignment, student_id, true)
cb(); return
n = misc.mswalltime()
action(assignment, student_id)
store.wait
timeout : 60*15
until : => store.last_copied(step, assignment, student_id) >= n
cb : (err) =>
if err
errors += "\n #{err}"
cb()
async.mapLimit store.get_student_ids(deleted:false), PARALLEL_LIMIT, f, (err) =>
if errors
error(errors)
else
@clear_activity(id)
peer_copy_to_student: (assignment, student) =>
if @_start_copy(assignment, student, 'last_peer_assignment')
return
id = @set_activity(desc:"Copying peer grading to a student")
finish = (err) =>
@clear_activity(id)
@_finish_copy(assignment, student, 'last_peer_assignment', err)
if err
@set_error("copy peer-grading to student: #{err}")
store = @get_store()
if not store? or not @_store_is_initialized()
return finish("store not yet initialized")
if not student = store.get_student(student)
return finish("no student")
if not assignment = store.get_assignment(assignment)
return finish("no assignment")
student_name = store.get_student_name(student)
@set_activity(id:id, desc:"Copying peer grading to #{student_name}")
@update_peer_assignment(assignment)
peers = store.get_peers_that_student_will_grade(assignment, student)
if not peers?
return finish()
student_project_id = student.get('project_id')
guidelines = assignment.getIn(['peer_grade', 'guidelines']) ? 'Please grade this assignment.'
due_date = assignment.getIn(['peer_grade', 'due_date'])
if due_date?
guidelines = "GRADING IS DUE #{new Date(due_date).toLocaleString()} \n\n " + guidelines
target_base_path = assignment.get('path') + "-peer-grade"
f = (student_id, cb) =>
src_path = assignment.get('collect_path') + '/' + student_id
target_path = target_base_path + "/" + student_id
async.series([
(cb) =>
name = store.get_student_name(student_id, true)
webapp_client.exec
project_id : store.get('course_project_id')
command : 'rm'
args : ['-f', src_path + "/STUDENT - #{name.simple}.txt", src_path + "/DUE_DATE.txt", src_path + "/STUDENT - #{name.simple}.txt~", src_path + "/DUE_DATE.txt~"]
cb : cb
(cb) =>
webapp_client.copy_path_between_projects
src_project_id : store.get('course_project_id')
src_path : src_path
target_project_id : student_project_id
target_path : target_path
overwrite_newer : false
delete_missing : false
cb : cb
], cb)
webapp_client.write_text_file_to_project
project_id : student_project_id
path : target_base_path + "/GRADING_GUIDE.md"
content : guidelines
cb : (err) =>
if not err
async.map(peers, f, finish)
else
finish(err)
peer_collect_from_student: (assignment, student) =>
if @_start_copy(assignment, student, 'last_peer_collect')
return
id = @set_activity(desc:"Collecting peer grading of a student")
finish = (err) =>
@clear_activity(id)
@_finish_copy(assignment, student, 'last_peer_collect', err)
if err
@set_error("collecting peer-grading of a student: #{err}")
store = @get_store()
if not store? or not @_store_is_initialized()
return finish("store not yet initialized")
if not student = store.get_student(student)
return finish("no student")
if not assignment = store.get_assignment(assignment)
return finish("no assignment")
student_name = store.get_student_name(student)
@set_activity(id:id, desc:"Collecting peer grading of #{student_name}")
peers = store.get_peers_that_graded_student(assignment, student)
if not peers?
return finish()
our_student_id = student.get('student_id')
f = (student_id, cb) =>
s = store.get_student(student_id)
if s.get('deleted')
cb()
return
path = assignment.get('path')
src_path = "#{path}-peer-grade/#{our_student_id}"
target_path = "#{assignment.get('collect_path')}-peer-grade/#{our_student_id}/#{student_id}"
async.series([
(cb) =>
webapp_client.copy_path_between_projects
src_project_id : s.get('project_id')
src_path : src_path
target_project_id : store.get('course_project_id')
target_path : target_path
overwrite_newer : false
delete_missing : false
cb : cb
(cb) =>
name = store.get_student_name(student_id, true)
webapp_client.write_text_file_to_project
project_id : store.get('course_project_id')
path : target_path + "/GRADER - #{name.simple}.txt"
content : "The student who did the peer grading is named #{name.full}."
cb : cb
(cb) =>
name = store.get_student_name(student, true)
webapp_client.write_text_file_to_project
project_id : store.get('course_project_id')
path : target_path + "/STUDENT - #{name.simple}.txt"
content : "This student is #{name.full}."
cb : cb
], cb)
async.map(peers, f, finish)
stop_copying_assignment: (type, assignment_id, student_id) =>
switch type
when 'assigned'
type = 'last_assignment'
when 'collected'
type = 'last_collect'
when 'graded'
type = 'last_return_graded'
when 'peer-assigned'
type = 'last_peer_assignment'
when 'peer-collected'
type = 'last_peer_collect'
@_stop_copy(assignment_id, student_id, type)
open_assignment: (type, assignment_id, student_id) =>
store = @get_store()
if not store?
return
assignment = store.get_assignment(assignment_id)
student = store.get_student(student_id)
student_project_id = student.get('project_id')
if not student_project_id?
@set_error("open_assignment: student project not yet created")
return
switch type
when 'assigned'
path = assignment.get('target_path')
proj = student_project_id
when 'collected'
path = assignment.get('collect_path') + '/' + student.get('student_id')
proj = store.get('course_project_id')
when 'peer-assigned'
proj = student_project_id
path = assignment.get('path') + '-peer-grade'
when 'peer-collected'
path = assignment.get('collect_path') + '-peer-grade/' + student.get('student_id')
proj = store.get('course_project_id')
when 'graded'
path = assignment.get('graded_path')
proj = student_project_id
else
@set_error("open_assignment -- unknown type: #{type}")
if not proj?
@set_error("no such project")
return
@redux.getProjectActions(proj).open_directory(path)
add_handout: (path) =>
target_path = path
@_set
path : path
target_path : target_path
table : 'handouts'
handout_id : misc.uuid()
delete_handout: (handout) =>
store = @get_store()
return if not store?
handout = store.get_handout(handout)
@_set
deleted : true
handout_id : handout.get('handout_id')
table : 'handouts'
undelete_handout: (handout) =>
store = @get_store()
return if not store?
handout = store.get_handout(handout)
@_set
deleted : false
handout_id : handout.get('handout_id')
table : 'handouts'
_set_handout_field: (handout, name, val) =>
store = @get_store()
return if not store?
handout = store.get_handout(handout)
@_set
"#{name}" : val
table : 'handouts'
handout_id : handout.get('handout_id')
set_handout_note: (handout, note) =>
@_set_handout_field(handout, 'note', note)
_handout_finish_copy: (handout, student, err) =>
if student? and handout?
store = @get_store()
if not store?
return
student = store.get_student(student)
handout = store.get_handout(handout)
obj = {table:'handouts', handout_id:handout.get('handout_id')}
status_map = @_get_one(obj)?.status ? {}
student_id = student.get('student_id')
status_map[student_id] = {time: misc.mswalltime()}
if err
status_map[student_id].error = err
obj.status = status_map
@_set(obj)
_handout_start_copy: (handout, student) =>
if student? and handout?
store = @get_store()
if not store?
return
student = store.get_student(student)
handout = store.get_handout(handout)
obj = {table:'handouts', handout_id:handout.get('handout_id')}
status_map = @_get_one(obj)?.status ? {}
student_status = (status_map[student.get('student_id')]) ? {}
if student_status.start? and webapp_client.server_time() - student_status.start <= 15000
return true
student_status.start = misc.mswalltime()
status_map[student.get('student_id')] = student_status
obj.status = status_map
@_set(obj)
return false
stop_copying_handout: (handout, student) =>
if student? and handout?
store = @get_store()
if not store?
return
student = store.get_student(student)
handout = store.get_handout(handout)
obj = {table:'handouts', handout_id:handout.get('handout_id')}
status = @_get_one(obj)?.status
if not status?
return
student_status = (status[student.get('student_id')])
if not student_status?
return
if student_status.start?
delete student_status.start
status[student.get('student_id')] = student_status
obj.status = status
@_set(obj)
copy_handout_to_student: (handout, student) =>
if @_handout_start_copy(handout, student)
return
id = @set_activity(desc:"Copying handout to a student")
finish = (err) =>
@clear_activity(id)
@_handout_finish_copy(handout, student, err)
if err
@set_error("copy to student: #{err}")
store = @get_store()
if not store? or not @_store_is_initialized()
return finish("store not yet initialized")
if not student = store.get_student(student)
return finish("no student")
if not handout = store.get_handout(handout)
return finish("no handout")
student_name = store.get_student_name(student)
@set_activity(id:id, desc:"Copying handout to #{student_name}")
student_project_id = student.get('project_id')
student_id = student.get('student_id')
src_path = handout.get('path')
async.series([
(cb) =>
if not student_project_id?
@set_activity(id:id, desc:"#{student_name}'s project doesn't exist, so creating it.")
@create_student_project(student)
store = @get_store()
if not store?
cb("no store")
return
store.wait
until : => store.get_student_project_id(student_id)
cb : (err, x) =>
student_project_id = x
cb(err)
else
cb()
(cb) =>
@set_activity(id:id, desc:"Copying files to #{student_name}'s project")
webapp_client.copy_path_between_projects
src_project_id : store.get('course_project_id')
src_path : src_path
target_project_id : student_project_id
target_path : handout.get('target_path')
overwrite_newer : false
delete_missing : false
backup : true
exclude_history : true
cb : cb
], (err) =>
finish(err)
)
copy_handout_to_all_students: (handout, new_only) =>
desc = "Copying handouts to all students #{if new_only then 'who have not already received it' else ''}"
short_desc = "copy to student"
id = @set_activity(desc:desc)
error = (err) =>
@clear_activity(id)
err="#{short_desc}: #{err}"
@set_error(err)
store = @get_store()
if not store? or not @_store_is_initialized()
return error("store not yet initialized")
if not handout = store.get_handout(handout)
return error("no handout")
errors = ''
f = (student_id, cb) =>
if new_only and store.handout_last_copied(handout, student_id, true)
cb(); return
n = misc.mswalltime()
@copy_handout_to_student(handout, student_id)
store.wait
timeout : 60*15
until : => store.handout_last_copied(handout, student_id) >= n
cb : (err) =>
if err
errors += "\n #{err}"
cb()
async.mapLimit store.get_student_ids(deleted:false), PARALLEL_LIMIT, f, (err) =>
if errors
error(errors)
else
@clear_activity(id)
open_handout: (handout_id, student_id) =>
store = @get_store()
if not store?
return
handout = store.get_handout(handout_id)
student = store.get_student(student_id)
student_project_id = student.get('project_id')
if not student_project_id?
@set_error("open_handout: student project not yet created")
return
path = handout.get('target_path')
proj = student_project_id
if not proj?
@set_error("no such project")
return
@redux.getProjectActions(proj).open_directory(path)