Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
| Download
Views: 39598
1
##############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
# React libraries
23
{Actions, Store} = require('../smc-react')
24
25
# SMC libraries
26
misc = require('smc-util/misc')
27
{defaults, required} = misc
28
29
# Course Library
30
{STEPS, previous_step, step_direction, step_verb, step_ready} = require('./util')
31
32
# Upgrades
33
project_upgrades = require('./project-upgrades')
34
35
exports.CourseStore = class CourseStore extends Store
36
any_assignment_uses_peer_grading: =>
37
# Return true if there are any non-deleted assignments that use peer grading
38
has_peer = false
39
@get_assignments().forEach (assignment, _) =>
40
if assignment.getIn(['peer_grade', 'enabled']) and not assignment.get('deleted')
41
has_peer = true
42
return false # stop looping
43
return has_peer
44
45
get_peers_that_student_will_grade: (assignment, student) =>
46
# Return the peer assignment for grading of the given assignment for the given student,
47
# if such an assignment has been made. If not, returns undefined.
48
# In particular, this returns a Javascript array of student_id's.
49
assignment = @get_assignment(assignment)
50
student = @get_student(student)
51
return assignment.getIn(['peer_grade', 'map'])?.get(student.get('student_id'))?.toJS()
52
53
get_peers_that_graded_student: (assignment, student) =>
54
# Return Javascript array of the student_id's of the students
55
# that graded the given student, or undefined if no relevant assignment.
56
assignment = @get_assignment(assignment)
57
map = assignment.getIn(['peer_grade', 'map'])
58
if not map?
59
return
60
student = @get_student(student)
61
id = student.get('student_id')
62
return (student_id for student_id, who_grading of map.toJS() when id in who_grading)
63
64
get_shared_project_id: =>
65
# return project_id (a string) if shared project has been created, or undefined or empty string otherwise.
66
return @getIn(['settings', 'shared_project_id'])
67
68
get_pay: =>
69
return @getIn(['settings', 'pay']) ? ''
70
71
get_allow_collabs: =>
72
return @getIn(['settings', 'allow_collabs']) ? true
73
74
get_email_invite: =>
75
{SITE_NAME, DOMAIN_NAME} = require('smc-util/theme')
76
@getIn(['settings', 'email_invite']) ? "We will use [#{SITE_NAME}](#{DOMAIN_NAME}) for the course *{title}*. \n\nPlease sign up!\n\n--\n\n{name}"
77
78
get_activity: =>
79
@get('activity')
80
81
get_students: =>
82
@get('students')
83
84
# Get the student's name.
85
# Uses an instructor-given name if it exists.
86
get_student_name: (student, include_email=false) =>
87
student = @get_student(student)
88
if not student?
89
return 'student'
90
email = student.get('email_address')
91
account_id = student.get('account_id')
92
first_name = student.get('first_name') ? @redux.getStore('users').get_first_name(account_id)
93
last_name = student.get('last_name') ? @redux.getStore('users').get_last_name(account_id)
94
if first_name? and last_name?
95
full_name = first_name + ' ' + last_name
96
else if first_name?
97
full_name = first_name
98
else if last_name?
99
full_name = last_name
100
else
101
full_name = email ? 'student'
102
if include_email and full_name? and email?
103
full = full_name + " <#{email}>"
104
else
105
full = full_name
106
if full_name == 'Unknown User' and email?
107
full_name = email
108
if not include_email
109
return full_name
110
return {simple:full_name.replace(/\W/g, ' '), full:full}
111
112
get_student_email: (student) =>
113
student = @get_student(student)
114
if not student?
115
return 'student'
116
return student.get('email_address')
117
118
get_student_ids: (opts) =>
119
opts = defaults opts,
120
deleted : false
121
if not @get('students')?
122
return
123
v = []
124
@get('students').map (val, student_id) =>
125
if !!val.get('deleted') == opts.deleted
126
v.push(student_id)
127
return v
128
129
# return list of all student projects (or undefined if not loaded)
130
get_student_project_ids: (opts) =>
131
{include_deleted, deleted_only, map} = defaults opts,
132
include_deleted : false
133
deleted_only : false
134
map : false # return as map to true/false instead of array
135
# include_deleted = if true, also include deleted projects
136
# deleted_only = if true, only include deleted projects
137
if not @get('students')?
138
return
139
if map
140
v = {}
141
include = (x) -> v[x] = true
142
else
143
v = []
144
include = (x) -> v.push(x)
145
@get('students').map (val, student_id) =>
146
id = val.get('project_id')
147
if deleted_only
148
if include_deleted and val.get('deleted')
149
include(id)
150
else if include_deleted
151
include(id)
152
else if not val.get('deleted')
153
include(id)
154
return v
155
156
get_student: (student) =>
157
# return student with given id if a string; otherwise, just return student (the input)
158
if typeof(student) != 'string'
159
student = student?.get('student_id')
160
return @getIn(['students', student])
161
162
get_student_note: (student) =>
163
return @get_student(student)?.get('note')
164
165
get_student_project_id: (student) =>
166
return @get_student(student)?.get('project_id')
167
168
get_sorted_students: =>
169
v = []
170
@get('students').map (student, id) =>
171
if not student.get('deleted')
172
v.push(student)
173
v.sort (a,b) => misc.cmp(@get_student_name(a), @get_student_name(b))
174
return v
175
176
get_grade: (assignment, student) =>
177
return @get_assignment(assignment)?.get('grades')?.get(@get_student(student)?.get('student_id'))
178
179
get_due_date: (assignment) =>
180
due_date = @get_assignment(assignment)?.get('due_date')
181
if due_date?
182
return new Date(due_date)
183
184
get_assignment_note: (assignment) =>
185
return @get_assignment(assignment)?.get('note')
186
187
get_assignments: =>
188
return @get('assignments')
189
190
get_sorted_assignments: =>
191
v = []
192
@get_assignments().map (assignment, id) =>
193
if not assignment.get('deleted')
194
v.push(assignment)
195
f = (a) -> [a.get('due_date') ? 0, a.get('path')?.toLowerCase()] # note: also used in compute_assignment_list
196
v.sort (a,b) -> misc.cmp_array(f(a), f(b))
197
return v
198
199
get_assignment: (assignment) =>
200
# return assignment with given id if a string; otherwise, just return assignment (the input)
201
if typeof(assignment) != 'string'
202
assignment = assignment?.get('assignment_id')
203
return @getIn(['assignments', assignment])
204
205
get_assignment_ids: (opts) =>
206
opts = defaults opts,
207
deleted : false # if true return only deleted assignments
208
if not @get_assignments()
209
return
210
v = []
211
@get_assignments().map (val, assignment_id) =>
212
if !!val.get('deleted') == opts.deleted
213
v.push(assignment_id)
214
return v
215
216
_num_nondeleted: (a) =>
217
if not a?
218
return
219
n = 0
220
a.map (val, key) =>
221
if not val.get('deleted')
222
n += 1
223
return n
224
225
# number of non-deleted students
226
num_students: => @_num_nondeleted(@get_students())
227
228
# number of student projects that are currently running
229
num_running_projects: (project_map) =>
230
n = 0
231
@get_students()?.map (student, student_id) =>
232
if not student.get('deleted')
233
if project_map.getIn([student.get('project_id'), 'state', 'state']) == 'running'
234
n += 1
235
return n
236
237
# number of non-deleted assignments
238
num_assignments: => @_num_nondeleted(@get_assignments())
239
240
# number of non-deleted handouts
241
num_handouts: => @_num_nondeleted(@get_handouts())
242
243
# get info about relation between a student and a given assignment
244
student_assignment_info: (student, assignment) =>
245
assignment = @get_assignment(assignment)
246
student = @get_student(student)
247
student_id = student.get('student_id')
248
status = @get_assignment_status(assignment)
249
info = # RHS -- important to be undefined if no info -- assumed in code
250
last_assignment : assignment.get('last_assignment')?.get(student_id)?.toJS()
251
last_collect : assignment.get('last_collect')?.get(student_id)?.toJS()
252
last_peer_assignment : assignment.get('last_peer_assignment')?.get(student_id)?.toJS()
253
last_peer_collect : assignment.get('last_peer_collect')?.get(student_id)?.toJS()
254
last_return_graded : assignment.get('last_return_graded')?.get(student_id)?.toJS()
255
student_id : student_id
256
assignment_id : assignment.get('assignment_id')
257
peer_assignment : (status.not_collect + status.not_assignment == 0) and status.collect != 0
258
peer_collect : status.not_peer_assignment? and status.not_peer_assignment == 0
259
return info
260
261
262
# Return the last time the assignment was copied to/from the
263
# student (in the given step of the workflow), or undefined.
264
# Even an attempt to copy with an error counts.
265
last_copied: (step, assignment, student_id, no_error) =>
266
x = @get_assignment(assignment)?.get("last_#{step}")?.get(student_id)
267
if not x?
268
return
269
if no_error and x.get('error')
270
return
271
return x.get('time')
272
273
has_grade: (assignment, student_id) =>
274
return @get_assignment(assignment)?.get("grades")?.get(student_id)
275
276
get_assignment_status: (assignment) =>
277
#
278
# Compute and return an object that has fields (deleted students are ignored)
279
#
280
# assignment - number of students who have received assignment
281
# not_assignment - number of students who have NOT received assignment
282
# collect - number of students from whom we have collected assignment
283
# not_collect - number of students from whom we have NOT collected assignment but we sent it to them
284
# peer_assignment - number of students who have received peer assignment
285
# (only present if peer grading enabled; similar for peer below)
286
# not_peer_assignment - number of students who have NOT received peer assignment
287
# peer_collect - number of students from whom we have collected peer grading
288
# not_peer_collect - number of students from whome we have NOT collected peer grading
289
# return_graded - number of students to whom we've returned assignment
290
# not_return_graded - number of students to whom we've NOT returned assignment
291
# but we collected it from them *and* assigned a grade
292
#
293
# This function caches its result and only recomputes values when the store changes,
294
# so it should be safe to call in render.
295
#
296
if not @_assignment_status?
297
@_assignment_status = {}
298
@on 'change', => # clear cache on any change to the store
299
@_assignment_status = {}
300
assignment = @get_assignment(assignment)
301
if not assignment?
302
return undefined
303
304
assignment_id = assignment.get('assignment_id')
305
if @_assignment_status[assignment_id]?
306
return @_assignment_status[assignment_id]
307
308
students = @get_student_ids(deleted:false)
309
if not students?
310
return undefined
311
312
# Is peer grading enabled?
313
peer = assignment.get('peer_grade')?.get('enabled')
314
315
info = {}
316
for t in STEPS(peer)
317
info[t] = 0
318
info["not_#{t}"] = 0
319
for student_id in students
320
previous = true
321
for t in STEPS(peer)
322
x = assignment.get("last_#{t}")?.get(student_id)
323
if x? and not x.get('error')
324
previous = true
325
info[t] += 1
326
else
327
# add one only if the previous step *was* done (and in
328
# the case of returning, they have a grade)
329
if previous and (t!='return_graded' or @has_grade(assignment, student_id))
330
info["not_#{t}"] += 1
331
previous = false
332
333
@_assignment_status[assignment_id] = info
334
return info
335
336
get_handout_note: (handout) =>
337
return @get_handout(handout)?.get('note')
338
339
get_handouts: =>
340
return @get('handouts')
341
342
get_handout: (handout) =>
343
# return handout with given id if a string; otherwise, just return handout (the input)
344
if typeof(handout) != 'string'
345
handout = handout?.get('handout_id')
346
return @getIn(['handouts', handout])
347
348
get_handout_ids: (opts) =>
349
opts = defaults opts,
350
deleted : false # if true return only deleted handouts
351
if not @get_handouts()
352
return undefined
353
v = []
354
@get_handouts().map (val, handout_id) =>
355
if !!val.get('deleted') == opts.deleted
356
v.push(handout_id)
357
return v
358
359
student_handout_info: (student, handout) =>
360
handout = @get_handout(handout)
361
student = @get_student(student)
362
student_id = student.get('student_id')
363
status = @get_handout_status(handout)
364
info = # RHS -- important to be undefined if no info -- assumed in code
365
status : handout.get('status')?.get(student_id)?.toJS()
366
student_id : student_id
367
handout_id : handout.get('handout_id')
368
return info
369
370
# Return the last time the handout was copied to/from the
371
# student (in the given step of the workflow), or undefined.
372
# Even an attempt to copy with an error counts.
373
# ???
374
handout_last_copied: (handout, student_id) =>
375
x = @get_handout(handout)?.get("status")?.get(student_id)
376
if not x?
377
return undefined
378
if x.get('error')
379
return undefined
380
return x.get('time')
381
382
get_handout_status: (handout) =>
383
#
384
# Compute and return an object that has fields (deleted students are ignored)
385
#
386
# handout - number of students who have received handout
387
# not_handout - number of students who have NOT received handout
388
# This function caches its result and only recomputes values when the store changes,
389
# so it should be safe to call in render.
390
#
391
if not @_handout_status?
392
@_handout_status = {}
393
@on 'change', => # clear cache on any change to the store
394
@_handout_status = {}
395
handout = @get_handout(handout)
396
if not handout?
397
return undefined
398
399
handout_id = handout.get('handout_id')
400
if @_handout_status[handout_id]?
401
return @_handout_status[handout_id]
402
403
students = @get_student_ids(deleted:false)
404
if not students?
405
return undefined
406
407
info =
408
handout : 0
409
not_handout : 0
410
411
for student_id in students
412
x = handout.get("status")?.get(student_id)
413
if x? and not x.get('error')
414
info.handout += 1
415
else
416
info.not_handout += 1
417
418
@_handout_status[handout_id] = info
419
return info
420
421
get_upgrade_plan: (upgrade_goal) =>
422
account_store = @redux.getStore('account')
423
plan = project_upgrades.upgrade_plan
424
account_id : account_store.get_account_id()
425
purchased_upgrades : account_store.get_total_upgrades()
426
project_map : @redux.getStore('projects').get('project_map')
427
student_project_ids : @get_student_project_ids(include_deleted:true, map:true)
428
deleted_project_ids : @get_student_project_ids(include_deleted:true, deleted_only:true, map:true)
429
upgrade_goal : upgrade_goal
430
return plan
431
432