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
# CoCalc libraries
23
misc = require('smc-util/misc')
24
{defaults, required} = misc
25
{webapp_client} = require('../webapp_client')
26
27
# React libraries
28
{React, rclass, rtypes} = require('../smc-react')
29
{Alert, Button, ButtonToolbar, ButtonGroup, FormControl, FormGroup, Checkbox, Row, Col, Panel} = require('react-bootstrap')
30
31
# CoCalc and course components
32
util = require('./util')
33
styles = require('./styles')
34
{DateTimePicker, ErrorDisplay, Icon, LabeledRow, Loading, MarkdownInput, Space, Tip, NumberInput} = require('../r_misc')
35
{STEPS, step_direction, step_verb, step_ready} = util
36
{BigTime, FoldersToolbar, StudentAssignmentInfo, StudentAssignmentInfoHeader} = require('./common')
37
38
39
exports.AssignmentsPanel = rclass ({name}) ->
40
displayName : "CourseEditorAssignments"
41
42
reduxProps :
43
"#{name}":
44
expanded_assignments : rtypes.immutable.Set
45
active_assignment_sort : rtypes.object
46
active_student_sort : rtypes.immutable.Map
47
expanded_peer_configs : rtypes.immutable.Set
48
49
propTypes :
50
name : rtypes.string.isRequired
51
project_id : rtypes.string.isRequired
52
redux : rtypes.object.isRequired
53
actions : rtypes.object.isRequired
54
all_assignments : rtypes.object.isRequired
55
students : rtypes.object.isRequired
56
user_map : rtypes.object.isRequired
57
58
getInitialState: ->
59
err : undefined # error message to display at top.
60
search : '' # search query to restrict which assignments are shown.
61
show_deleted : false # whether or not to show deleted assignments on the bottom
62
63
compute_assignment_list: ->
64
list = util.immutable_to_list(@props.all_assignments, 'assignment_id')
65
66
{list, num_omitted} = util.compute_match_list
67
list : list
68
search_key : 'path'
69
search : @state.search.trim()
70
71
if @props.active_assignment_sort.column_name == "due_date"
72
f = (a) -> [a.due_date ? 0, a.path?.toLowerCase()]
73
else if @props.active_assignment_sort.column_name == "dir_name"
74
f = (a) -> [a.path?.toLowerCase(), a.due_date ? 0]
75
76
{list, deleted, num_deleted} = util.order_list
77
list : list
78
compare_function : (a,b) => misc.cmp_array(f(a), f(b))
79
reverse : @props.active_assignment_sort.is_descending
80
include_deleted : @state.show_deleted
81
82
return {shown_assignments:list, deleted_assignments:deleted, num_omitted:num_omitted, num_deleted:num_deleted}
83
84
render_sort_link: (column_name, display_name) ->
85
<a href=''
86
onClick={(e)=>e.preventDefault();@actions(@props.name).set_active_assignment_sort(column_name)}>
87
{display_name}
88
<Space/>
89
{<Icon style={marginRight:'10px'}
90
name={if @props.active_assignment_sort.is_descending then 'caret-up' else 'caret-down'}
91
/> if @props.active_assignment_sort.column_name == column_name}
92
</a>
93
94
render_assignment_table_header: ->
95
# HACK: -10px margin gets around ReactBootstrap's incomplete access to styling
96
<Row style={marginTop:'-10px', marginBottom:'3px'}>
97
<Col md=6>
98
{@render_sort_link("dir_name", "Assignment Name")}
99
</Col>
100
<Col md=6>
101
{@render_sort_link("due_date", "Due Date")}
102
</Col>
103
</Row>
104
105
render_assignments: (assignments) ->
106
for x,i in assignments
107
<Assignment
108
key = {x.assignment_id}
109
assignment = {@props.all_assignments.get(x.assignment_id)}
110
background = {if i%2==0 then "#eee"}
111
project_id = {@props.project_id}
112
redux = {@props.redux}
113
students = {@props.students}
114
user_map = {@props.user_map}
115
name = {@props.name}
116
is_expanded = {@props.expanded_assignments.has(x.assignment_id)}
117
active_student_sort = {@props.active_student_sort}
118
expand_peer_config = {@props.expanded_peer_configs.has(x.assignment_id)}
119
/>
120
121
render_show_deleted: (num_deleted, num_shown) ->
122
if @state.show_deleted
123
<Button style={styles.show_hide_deleted(needs_margin : num_shown > 0)} onClick={=>@setState(show_deleted:false)}>
124
<Tip placement='left' title="Hide deleted" tip="Assignments are never really deleted. Click this button so that deleted assignments aren't included at the bottom of the list. Deleted assignments are always hidden from the list of grades for a student.">
125
Hide {num_deleted} deleted assignments
126
</Tip>
127
</Button>
128
else
129
<Button style={styles.show_hide_deleted(needs_margin : num_shown > 0)} onClick={=>@setState(show_deleted:true,search:'')}>
130
<Tip placement='left' title="Show deleted" tip="Assignments are not deleted forever even after you delete them. Click this button to show any deleted assignments at the bottom of the list of assignments. You can then click on the assignment and click undelete to bring the assignment back.">
131
Show {num_deleted} deleted assignments
132
</Tip>
133
</Button>
134
135
yield_adder: (deleted_assignments) ->
136
deleted_paths = {}
137
deleted_assignments.map (obj) =>
138
if obj.path
139
deleted_paths[obj.path] = obj.assignment_id
140
141
(path) =>
142
if deleted_paths[path]?
143
@props.actions.undelete_assignment(deleted_paths[path])
144
else
145
@props.actions.add_assignment(path)
146
147
render: ->
148
{shown_assignments, deleted_assignments, num_omitted, num_deleted} = @compute_assignment_list()
149
add_assignment = @yield_adder(deleted_assignments)
150
151
header =
152
<FoldersToolbar
153
search = {@state.search}
154
search_change = {(value) => @setState(search:value)}
155
num_omitted = {num_omitted}
156
project_id = {@props.project_id}
157
items = {@props.all_assignments}
158
add_folders = {(paths)=>paths.map(add_assignment)}
159
item_name = {"assignment"}
160
plural_item_name = {"assignments"}
161
/>
162
163
<Panel header={header}>
164
{@render_assignment_table_header() if shown_assignments.length > 0}
165
{@render_assignments(shown_assignments)}
166
{@render_show_deleted(num_deleted, shown_assignments.length) if num_deleted}
167
</Panel>
168
169
exports.AssignmentsPanel.Header = rclass
170
propTypes :
171
n : rtypes.number
172
173
render: ->
174
<Tip delayShow=1300
175
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.">
176
<span>
177
<Icon name="share-square-o"/> Assignments {if @props.n? then " (#{@props.n})" else ""}
178
</span>
179
</Tip>
180
181
Assignment = rclass
182
displayName : "CourseEditor-Assignment"
183
184
propTypes :
185
name : rtypes.string.isRequired
186
assignment : rtypes.immutable.Map.isRequired
187
project_id : rtypes.string.isRequired
188
redux : rtypes.object.isRequired
189
students : rtypes.object.isRequired
190
user_map : rtypes.object.isRequired
191
background : rtypes.string
192
is_expanded : rtypes.bool
193
active_student_sort : rtypes.immutable.Map
194
expand_peer_config : rtypes.bool
195
196
shouldComponentUpdate: (nextProps, nextState) ->
197
return @state != nextState or @props.assignment != nextProps.assignment or @props.students != nextProps.students or @props.user_map != nextProps.user_map or @props.background != nextProps.background or @props.is_expanded != nextProps.is_expanded or @props.active_student_sort != nextProps.active_student_sort or @props.expand_peer_config != nextProps.expand_peer_config
198
199
getInitialState: ->
200
confirm_delete : false
201
202
_due_date: ->
203
due_date = @props.assignment.get('due_date') # a string
204
if not due_date?
205
return webapp_client.server_time()
206
else
207
return new Date(due_date)
208
209
render_due: ->
210
<Row>
211
<Col xs=1 style={marginTop:'8px', color:'#666'}>
212
<Tip placement='top' title="Set the due date"
213
tip="Set the due date for the assignment. This changes how the list of assignments is sorted. Note that you must explicitly click a button to collect student assignments when they are due -- they are not automatically collected on the due date. You should also tell students when assignments are due (e.g., at the top of the assignment).">
214
Due
215
</Tip>
216
</Col>
217
<Col xs=11>
218
<DateTimePicker
219
value = {@_due_date()}
220
on_change = {@date_change}
221
/>
222
</Col>
223
</Row>
224
225
date_change: (date) ->
226
date ?= @_due_date()
227
@props.redux.getActions(@props.name).set_due_date(@props.assignment, date?.toISOString())
228
229
render_note: ->
230
<Row key='note' style={styles.note}>
231
<Col xs=2>
232
<Tip title="Notes about this assignment" tip="Record notes about this assignment here. These notes are only visible to you, not to your students. Put any instructions to students about assignments in a file in the directory that contains the assignment.">
233
Private Assignment Notes<br /><span style={color:"#666"}></span>
234
</Tip>
235
</Col>
236
<Col xs=10>
237
<MarkdownInput
238
persist_id = {@props.assignment.get('path') + @props.assignment.get('assignment_id') + "note"}
239
attach_to = {@props.name}
240
rows = 6
241
placeholder = 'Private notes about this assignment (not visible to students)'
242
default_value = {@props.assignment.get('note')}
243
on_save = {(value)=>@props.redux.getActions(@props.name).set_assignment_note(@props.assignment, value)}
244
/>
245
</Col>
246
</Row>
247
248
render_more_header: ->
249
status = @props.redux.getStore(@props.name).get_assignment_status(@props.assignment)
250
if not status?
251
return <Loading key='loading_more'/>
252
v = []
253
254
bottom =
255
borderBottom : '1px solid grey'
256
paddingBottom : '15px'
257
marginBottom : '15px'
258
v.push <Row key='header3' style={bottom}>
259
<Col md=2>
260
{@render_open_button()}
261
</Col>
262
<Col md=10>
263
<Row>
264
<Col md=6 style={fontSize:'14px'} key='due'>
265
{@render_due()}
266
</Col>
267
<Col md=6 key='delete'>
268
<Row>
269
<Col md=7>
270
{@render_peer_button()}
271
</Col>
272
<Col md=5>
273
<span className='pull-right'>
274
{@render_delete_button()}
275
</span>
276
</Col>
277
</Row>
278
</Col>
279
</Row>
280
</Col>
281
</Row>
282
283
if @props.expand_peer_config
284
v.push <Row key='header2-peer' style={bottom}>
285
<Col md=10 mdOffset=2>
286
{@render_configure_peer()}
287
</Col>
288
</Row>
289
if @state.confirm_delete
290
v.push <Row key='header2-delete' style={bottom}>
291
<Col md=10 mdOffset=2>
292
{@render_confirm_delete()}
293
</Col>
294
</Row>
295
296
peer = @props.assignment.get('peer_grade')?.get('enabled')
297
if peer
298
width = 2
299
else
300
width = 3
301
buttons = []
302
for name in STEPS(peer)
303
b = @["render_#{name}_button"](status)
304
if b?
305
if name == 'return_graded'
306
buttons.push(<Col md={width} key='filler'></Col>)
307
buttons.push(<Col md={width} key={name}>{b}</Col>)
308
309
v.push <Row key='header-control'>
310
<Col md=10 mdOffset=2 key='buttons'>
311
<Row>
312
{buttons}
313
</Row>
314
</Col>
315
</Row>
316
317
v.push <Row key='header2-copy'>
318
<Col md=10 mdOffset=2>
319
{@render_copy_confirms(status)}
320
</Col>
321
</Row>
322
323
return v
324
325
render_more: ->
326
<Row key='more'>
327
<Col sm=12>
328
<Panel header={@render_more_header()}>
329
<StudentListForAssignment
330
redux = {@props.redux}
331
name = {@props.name}
332
assignment = {@props.assignment}
333
students = {@props.students}
334
user_map = {@props.user_map}
335
active_student_sort = {@props.active_student_sort}
336
/>
337
{@render_note()}
338
</Panel>
339
</Col>
340
</Row>
341
342
open_assignment_path: ->
343
@props.redux.getProjectActions(@props.project_id).open_directory(@props.assignment.get('path'))
344
345
render_open_button: ->
346
<Tip key='open' title={<span><Icon name='folder-open-o'/> Open assignment</span>}
347
tip="Open the folder in the current project that contains the original files for this assignment. Edit files in this folder to create the content that your students will see when they receive an assignment.">
348
<Button onClick={@open_assignment_path}>
349
<Icon name="folder-open-o" /> Open
350
</Button>
351
</Tip>
352
353
render_assignment_button: ->
354
bsStyle = if (@props.assignment.get('last_assignment')?.size ? 0) == 0 then "primary" else "warning"
355
<Button key='assign'
356
bsStyle = {bsStyle}
357
onClick = {=>@setState(copy_confirm_assignment:true, copy_confirm:true)}
358
disabled = {@state.copy_confirm}>
359
<Tip title={<span>Assign: <Icon name='user-secret'/> You <Icon name='long-arrow-right' /> <Icon name='users' /> Students </span>}
360
tip="Copy the files for this assignment from this project to all other student projects.">
361
<Icon name="share-square-o" /> Assign...
362
</Tip>
363
</Button>
364
365
render_copy_confirms: (status) ->
366
steps = STEPS(@props.assignment.get('peer_grade')?.get('enabled'))
367
for step in steps
368
if @state["copy_confirm_#{step}"]
369
@render_copy_confirm(step, status)
370
371
render_copy_confirm: (step, status) ->
372
<span key="copy_confirm_#{step}">
373
{@render_copy_confirm_to_all(step, status) if status[step]==0}
374
{@render_copy_confirm_to_all_or_new(step, status) if status[step]!=0}
375
</span>
376
377
render_copy_cancel: (step) ->
378
cancel = =>
379
@setState("copy_confirm_#{step}":false, "copy_confirm_all_#{step}":false, copy_confirm:false)
380
<Button key='cancel' onClick={cancel}>Cancel</Button>
381
382
copy_assignment: (step, new_only) ->
383
# assign assignment to all (non-deleted) students
384
actions = @props.redux.getActions(@props.name)
385
switch step
386
when 'assignment'
387
actions.copy_assignment_to_all_students(@props.assignment, new_only)
388
when 'collect'
389
actions.copy_assignment_from_all_students(@props.assignment, new_only)
390
when 'peer_assignment'
391
actions.peer_copy_to_all_students(@props.assignment, new_only)
392
when 'peer_collect'
393
actions.peer_collect_from_all_students(@props.assignment, new_only)
394
when 'return_graded'
395
actions.return_assignment_to_all_students(@props.assignment, new_only)
396
else
397
console.log("BUG -- unknown step: #{step}")
398
@setState("copy_confirm_#{step}":false, "copy_confirm_all_#{step}":false, copy_confirm:false)
399
400
render_copy_confirm_to_all: (step, status) ->
401
n = status["not_#{step}"]
402
<Alert bsStyle='warning' key="#{step}_confirm_to_all", style={marginTop:'15px'}>
403
<div style={marginBottom:'15px'}>
404
{misc.capitalize(step_verb(step))} this homework {step_direction(step)} the {n} student{if n>1 then "s" else ""}{step_ready(step, n)}?
405
</div>
406
<ButtonToolbar>
407
<Button key='yes' bsStyle='primary' onClick={=>@copy_assignment(step, false)} >Yes</Button>
408
{@render_copy_cancel(step)}
409
</ButtonToolbar>
410
</Alert>
411
412
copy_confirm_all_caution: (step) ->
413
switch step
414
when 'assignment'
415
return "This will recopy all of the files to them. CAUTION: if you update a file that a student has also worked on, their work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots."
416
when 'collect'
417
return "This will recollect all of the homework from them. CAUTION: if you have graded/edited a file that a student has updated, your work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots."
418
when 'return_graded'
419
return "This will rereturn all of the graded files to them."
420
when 'peer_assignment'
421
return 'This will recopy all of the files to them. CAUTION: if there is a file a student has also worked on grading, their work will get copied to a backup file ending in a tilde, or possibly be only available in snapshots.'
422
when 'peer_collect'
423
return 'This will recollect all of the peer-graded homework from the students. CAUTION: if you have graded/edited a previously collected file that a student has updated, your work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.'
424
425
render_copy_confirm_overwrite_all: (step, status) ->
426
<div key="copy_confirm_overwrite_all" style={marginTop:'15px'}>
427
<div style={marginBottom:'15px'}>
428
{@copy_confirm_all_caution(step)}
429
</div>
430
<ButtonToolbar>
431
<Button key='all' bsStyle='danger' onClick={=>@copy_assignment(step, false)}>Yes, do it</Button>
432
{@render_copy_cancel(step)}
433
</ButtonToolbar>
434
</div>
435
436
render_copy_confirm_to_all_or_new: (step, status) ->
437
n = status["not_#{step}"]
438
m = n + status[step]
439
<Alert bsStyle='warning' key="#{step}_confirm_to_all_or_new" style={marginTop:'15px'}>
440
<div style={marginBottom:'15px'}>
441
{misc.capitalize(step_verb(step))} this homework {step_direction(step)}...
442
</div>
443
<ButtonToolbar>
444
<Button key='all' bsStyle='danger' onClick={=>@setState("copy_confirm_all_#{step}":true, copy_confirm:true)}
445
disabled={@state["copy_confirm_all_#{step}"]} >
446
{if step=='assignment' then 'All' else 'The'} {m} students{step_ready(step, m)}...
447
</Button>
448
{<Button key='new' bsStyle='primary' onClick={=>@copy_assignment(step, true)}>The {n} student{if n>1 then 's' else ''} not already {step_verb(step)}ed {step_direction(step)}</Button> if n}
449
{@render_copy_cancel(step)}
450
</ButtonToolbar>
451
{@render_copy_confirm_overwrite_all(step, status) if @state["copy_confirm_all_#{step}"]}
452
</Alert>
453
454
render_collect_tip: (warning) ->
455
<span key='normal'>
456
Collect an assignment from all of your students.
457
(There is currently no way to schedule collection at a specific time; instead, collection happens when you click the button.)
458
</span>
459
460
render_collect_button: (status) ->
461
if status.assignment == 0
462
# no button if nothing ever assigned
463
return
464
if status.collect > 0
465
# Have already collected something
466
bsStyle = 'warning'
467
else
468
bsStyle = 'primary'
469
<Button key='collect'
470
onClick = {=>@setState(copy_confirm_collect:true, copy_confirm:true)}
471
disabled = {@state.copy_confirm}
472
bsStyle={bsStyle} >
473
<Tip
474
title={<span>Collect: <Icon name='users' /> Students <Icon name='long-arrow-right' /> <Icon name='user-secret'/> You</span>}
475
tip = {@render_collect_tip(bsStyle=='warning')}>
476
<Icon name="share-square-o" rotate={"180"} /> Collect...
477
</Tip>
478
</Button>
479
480
render_peer_assign_tip: (warning) ->
481
<span key='normal'>
482
Send copies of collected homework out to all students for peer grading.
483
</span>
484
485
render_peer_assignment_button: (status) ->
486
# Render the "Peer Assign..." button in the top row, for peer assigning to all
487
# students in the course.
488
if not status.peer_assignment?
489
# not peer graded
490
return
491
if status.not_collect + status.not_assignment > 0
492
# collect everything before peer grading
493
return
494
if status.collect == 0
495
# nothing to peer assign
496
return
497
if status.peer_assignment == 0
498
# haven't peer-assigned anything yet
499
bsStyle = 'primary'
500
else
501
# warning, since we have assigned already and this may overwrite
502
bsStyle = 'warning'
503
<Button key='peer-assign'
504
onClick = {=>@setState(copy_confirm_peer_assignment:true, copy_confirm:true)}
505
disabled = {@state.copy_confirm}
506
bsStyle = {bsStyle} >
507
<Tip
508
title={<span>Peer Assign: <Icon name='users' /> You <Icon name='long-arrow-right' /> <Icon name='user-secret'/> Students</span>}
509
tip = {@render_peer_assign_tip(bsStyle=='warning')}>
510
<Icon name="share-square-o" /> Peer Assign...
511
</Tip>
512
</Button>
513
514
render_peer_collect_tip: (warning) ->
515
<span key='normal'>
516
Collect the peer grading that your students did.
517
</span>
518
519
render_peer_collect_button: (status) ->
520
# Render the "Peer Collect..." button in the top row, for collecting peer grading from all
521
# students in the course.
522
if not status.peer_collect?
523
return
524
if status.peer_assignment == 0
525
# haven't even peer assigned anything -- so nothing to collect
526
return
527
if status.not_peer_assignment > 0
528
# everybody must have received peer assignment, or collecting isn't allowed
529
return
530
if status.peer_collect == 0
531
# haven't peer-collected anything yet
532
bsStyle = 'primary'
533
else
534
# warning, since we have already collected and this may overwrite
535
bsStyle = 'warning'
536
<Button key='peer-collect'
537
onClick = {=>@setState(copy_confirm_peer_collect:true, copy_confirm:true)}
538
disabled = {@state.copy_confirm}
539
bsStyle = {bsStyle} >
540
<Tip
541
title={<span>Peer Collect: <Icon name='users' /> Students <Icon name='long-arrow-right' /> <Icon name='user-secret'/> You</span>}
542
tip = {@render_peer_collect_tip(bsStyle=='warning')}>
543
<Icon name="share-square-o" rotate="180"/> Peer Collect...
544
</Tip>
545
</Button>
546
547
return_assignment: ->
548
# Assign assignment to all (non-deleted) students.
549
@props.redux.getActions(@props.name).return_assignment_to_all_students(@props.assignment)
550
551
render_return_graded_button: (status) ->
552
if status.collect == 0
553
# No button if nothing collected.
554
return
555
if status.peer_collect? and status.peer_collect == 0
556
# Peer grading enabled, but we didn't collect anything yet
557
return
558
if status.not_return_graded == 0 and status.return_graded == 0
559
# Nothing unreturned and ungraded yet and also nothing returned yet
560
return
561
if status.return_graded > 0
562
# Have already returned some
563
bsStyle = "warning"
564
else
565
bsStyle = "primary"
566
<Button key='return'
567
onClick = {=>@setState(copy_confirm_return_graded:true, copy_confirm:true)}
568
disabled = {@state.copy_confirm}
569
bsStyle = {bsStyle} >
570
<Tip title={<span>Return: <Icon name='user-secret'/> You <Icon name='long-arrow-right' /> <Icon name='users' /> Students </span>}
571
tip="Copy the graded versions of files for this assignment from this project to all other student projects.">
572
<Icon name="share-square-o" /> Return...
573
</Tip>
574
</Button>
575
576
delete_assignment: ->
577
@props.redux.getActions(@props.name).delete_assignment(@props.assignment)
578
@setState(confirm_delete:false)
579
580
undelete_assignment: ->
581
@props.redux.getActions(@props.name).undelete_assignment(@props.assignment)
582
583
render_confirm_delete: ->
584
<Alert bsStyle='warning' key='confirm_delete'>
585
Are you sure you want to delete this assignment (you can undelete it later)?
586
<br/> <br/>
587
<ButtonToolbar>
588
<Button key='yes' onClick={@delete_assignment} bsStyle='danger'>
589
<Icon name="trash" /> Delete
590
</Button>
591
<Button key='no' onClick={=>@setState(confirm_delete:false)}>
592
Cancel
593
</Button>
594
</ButtonToolbar>
595
</Alert>
596
597
render_delete_button: ->
598
if @props.assignment.get('deleted')
599
<Tip key='delete' placement='left' title="Undelete assignment" tip="Make the assignment visible again in the assignment list and in student grade lists.">
600
<Button onClick={@undelete_assignment}>
601
<Icon name="trash-o" /> Undelete
602
</Button>
603
</Tip>
604
else
605
<Tip key='delete' placement='left' title="Delete assignment" tip="Deleting this assignment removes it from the assignment list and student grade lists, but does not delete any files off of disk. You can always undelete an assignment later by showing it using the 'show deleted assignments' button.">
606
<Button onClick={=>@setState(confirm_delete:true)} disabled={@state.confirm_delete}>
607
<Icon name="trash" /> Delete
608
</Button>
609
</Tip>
610
611
set_peer_grade: (config) ->
612
@props.redux.getActions(@props.name).set_peer_grade(@props.assignment, config)
613
614
render_configure_peer_checkbox: (config) ->
615
<div>
616
<Checkbox checked = {config.enabled ? false}
617
key = 'peer_grade_checkbox'
618
ref = 'peer_grade_checkbox'
619
onChange = {(e)=>@set_peer_grade(enabled:e.target.checked)}
620
style = {display:'inline-block', verticalAlign:'middle'}
621
/>
622
Enable Peer Grading
623
</div>
624
625
_peer_due: (date) ->
626
date ?= @props.assignment.getIn(['peer_grade', 'due_date'])
627
if date?
628
return new Date(date)
629
else
630
return misc.server_days_ago(-7)
631
632
peer_due_change: (date) ->
633
@set_peer_grade(due_date : @_peer_due(date)?.toISOString())
634
635
render_configure_peer_due: (config) ->
636
label = <Tip placement='top' title="Set the due date"
637
tip="Set the due date for grading this assignment. Note that you must explicitly click a button to collect graded assignments when -- they are not automatically collected on the due date. A file is included in the student peer grading assignment telling them when they should finish their grading.">
638
Due
639
</Tip>
640
<LabeledRow label_cols=6 label={label}>
641
<DateTimePicker
642
value = {@_peer_due(config.due_date)}
643
on_change = {@peer_due_change}
644
/>
645
</LabeledRow>
646
647
render_configure_peer_number: (config) ->
648
store = @props.redux.getStore(@props.name)
649
<LabeledRow label_cols=6 label='Number of students who will grade each assignment'>
650
<NumberInput
651
on_change = {(n) => @set_peer_grade(number : n)}
652
min = 1
653
max = {(store?.num_students() ? 2) - 1}
654
number = {config.number ? 1}
655
/>
656
</LabeledRow>
657
658
render_configure_grading_guidelines: (config) ->
659
store = @props.redux.getStore(@props.name)
660
<div style={marginTop:'10px'}>
661
<LabeledRow label_cols=6 label='Grading guidelines, which will be made available to students in their grading folder in a file GRADING_GUIDE.md. Tell your students how to grade each problem. Since this is a markdown file, you might also provide a link to a publicly shared file or directory with guidelines.'>
662
<div style={background:'white', padding:'10px', border:'1px solid #ccc', borderRadius:'3px'}>
663
<MarkdownInput
664
persist_id = {@props.assignment.get('path') + @props.assignment.get('assignment_id') + "grading-guidelines"}
665
attach_to = {@props.name}
666
rows = 16
667
placeholder = 'Enter your grading guidelines for this assignment...'
668
default_value = {config.guidelines}
669
on_save = {(x) => @set_peer_grade(guidelines : x)}
670
/>
671
</div>
672
</LabeledRow>
673
</div>
674
675
render_configure_peer: ->
676
config = @props.assignment.get('peer_grade')?.toJS() ? {}
677
<Alert bsStyle='warning'>
678
<h3><Icon name="users"/> Peer grading</h3>
679
680
<div style={color:'#666'}>
681
Use peer grading to randomly (and anonymously) redistribute
682
collected homework to your students, so that they can grade
683
it for you.
684
</div>
685
686
{@render_configure_peer_checkbox(config)}
687
{@render_configure_peer_number(config) if config.enabled}
688
{@render_configure_peer_due(config) if config.enabled}
689
{@render_configure_grading_guidelines(config) if config.enabled}
690
691
<Button onClick={=>@actions(@props.name).toggle_item_expansion('peer_config', @props.assignment.get('assignment_id'))}>
692
Close
693
</Button>
694
695
</Alert>
696
697
render_peer_button: ->
698
if @props.assignment.get('peer_grade')?.get('enabled')
699
icon = 'check-square-o'
700
else
701
icon = 'square-o'
702
<Button disabled={@props.expand_peer_config } onClick={=>@actions(@props.name).toggle_item_expansion('peer_config', @props.assignment.get('assignment_id'))}>
703
<Icon name={icon} /> Peer Grading...
704
</Button>
705
706
render_summary_due_date: ->
707
due_date = @props.assignment.get('due_date')
708
if due_date
709
<div style={marginTop:'12px'}>Due <BigTime date={due_date} /></div>
710
711
render_assignment_name: ->
712
<span>
713
{misc.trunc_middle(@props.assignment.get('path'), 80)}
714
{<b> (deleted)</b> if @props.assignment.get('deleted')}
715
</span>
716
717
render_assignment_title_link: ->
718
<a href='' onClick={(e)=>e.preventDefault();@actions(@props.name).toggle_item_expansion('assignment', @props.assignment.get('assignment_id'))}>
719
<Icon style={marginRight:'10px'}
720
name={if @props.is_expanded then 'caret-down' else 'caret-right'} />
721
{@render_assignment_name()}
722
</a>
723
724
render_summary_line: () ->
725
<Row key='summary' style={backgroundColor:@props.background}>
726
<Col md=6>
727
<h5>
728
{@render_assignment_title_link()}
729
</h5>
730
</Col>
731
<Col md=6>
732
{@render_summary_due_date()}
733
</Col>
734
</Row>
735
736
render: ->
737
<Row style={if @props.is_expanded then styles.selected_entry else styles.entry}>
738
<Col xs=12>
739
{@render_summary_line()}
740
{@render_more() if @props.is_expanded}
741
</Col>
742
</Row>
743
744
StudentListForAssignment = rclass
745
displayName : "CourseEditor-StudentListForAssignment"
746
747
propTypes :
748
name : rtypes.string.isRequired
749
redux : rtypes.object.isRequired
750
assignment : rtypes.object.isRequired
751
students : rtypes.object.isRequired
752
user_map : rtypes.object.isRequired
753
background : rtypes.string
754
active_student_sort : rtypes.immutable.Map
755
756
render_student_info: (student_id) ->
757
store = @props.redux.getStore(@props.name)
758
<StudentAssignmentInfo
759
key = {student_id}
760
title = {misc.trunc_middle(store.get_student_name(student_id), 40)}
761
name = {@props.name}
762
student = {student_id}
763
assignment = {@props.assignment}
764
grade = {store.get_grade(@props.assignment, student_id)}
765
info = {store.student_assignment_info(student_id, @props.assignment)} />
766
767
render_students: ->
768
v = util.parse_students(@props.students, @props.user_map, @props.redux)
769
# fill in names, for use in sorting and searching (TODO: caching)
770
v = (x for x in v when not x.deleted)
771
v.sort(util.pick_student_sorter(@props.active_student_sort.toJS()))
772
if @props.active_student_sort.get('is_descending')
773
v.reverse()
774
775
for x in v
776
@render_student_info(x.student_id)
777
778
render: ->
779
<div>
780
<StudentAssignmentInfoHeader
781
key = 'header'
782
title = "Student"
783
peer_grade = {!!@props.assignment.get('peer_grade')?.get('enabled')}
784
/>
785
{@render_students()}
786
</div>
787
788