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, Input, Row, Col, Panel, Table} = require('react-bootstrap')
30
31
# CoCalc and course components
32
util = require('./util')
33
styles = require('./styles')
34
{BigTime, FoldersToolbar} = require('./common')
35
{ErrorDisplay, Icon, Tip, MarkdownInput} = require('../r_misc')
36
37
# Could be merged with steps system of assignments.
38
# Probably not a good idea mixing the two.
39
# Could also be coded into the components below but steps could be added in the future?
40
STEPS = () ->
41
['handout']
42
43
previous_step = (step, peer) ->
44
switch step
45
when 'handout'
46
return
47
else
48
console.warn("BUG! previous_step('#{step}')")
49
50
step_direction = (step) ->
51
switch step
52
when 'handout'
53
return 'to'
54
else
55
console.warn("BUG! step_direction('#{step}')")
56
57
step_verb = (step) ->
58
switch step
59
when 'handout'
60
return 'distribute'
61
else
62
console.warn("BUG! step_verb('#{step}')")
63
64
step_ready = (step, n) ->
65
switch step
66
when 'handout'
67
return ''
68
69
past_tense = (word) ->
70
if word[word.length-1] == 'e'
71
return word + 'd'
72
else
73
return word + 'ed'
74
75
exports.HandoutsPanel = rclass ({name}) ->
76
77
displayName : 'Course-editor-HandoutsPanel'
78
79
reduxProps :
80
"#{name}":
81
expanded_handouts : rtypes.immutable.Set
82
83
propTypes :
84
project_id : rtypes.string.isRequired
85
all_handouts : rtypes.immutable.Map.isRequired # handout_id -> handout
86
students : rtypes.immutable.Map.isRequired # student_id -> student
87
user_map : rtypes.object.isRequired
88
actions : rtypes.object.isRequired
89
store_object : rtypes.object
90
project_actions : rtypes.object.isRequired
91
92
getInitialState: ->
93
show_deleted : false
94
search : '' # Search value for filtering handouts
95
96
# Update on different students, handouts, or filter parameters
97
shouldComponentUpdate: (nextProps, nextState) ->
98
if nextProps.all_handouts != @props.all_handouts or nextProps.students != @props.students or @props.expanded_handouts != nextProps.expanded_handouts
99
return true
100
if nextState.search != @state.search or nextState.show_deleted != @state.show_deleted
101
return true
102
return false
103
104
compute_handouts_list: ->
105
list = util.immutable_to_list(@props.all_handouts, 'handout_id')
106
107
{list, num_omitted} = util.compute_match_list
108
list : list
109
search_key : 'path'
110
search : @state.search.trim()
111
112
{list, deleted, num_deleted} = util.order_list
113
list : list
114
compare_function : (a,b) => misc.cmp(a.path?.toLowerCase(), b.path?.toLowerCase())
115
include_deleted : @state.show_deleted
116
117
return {shown_handouts:list, deleted_handouts:deleted, num_omitted:num_omitted, num_deleted:num_deleted}
118
119
render_show_deleted_button: (num_deleted, num_shown) ->
120
if @state.show_deleted
121
<Button style={styles.show_hide_deleted(needs_margin : num_shown > 0)} onClick={=>@setState(show_deleted:false)}>
122
<Tip placement='left' title="Hide deleted" tip="Handouts are never really deleted. Click this button so that deleted handouts aren't included at the bottom of the list.">
123
Hide {num_deleted} deleted handouts
124
</Tip>
125
</Button>
126
else
127
<Button style={styles.show_hide_deleted(needs_margin : num_shown > 0)} onClick={=>@setState(show_deleted:true, search:'')}>
128
<Tip placement='left' title="Show deleted" tip="Handouts are not deleted forever even after you delete them. Click this button to show any deleted handouts at the bottom of the list of handouts. You can then click on the handout and click undelete to bring the handout back.">
129
Show {num_deleted} deleted handouts
130
</Tip>
131
</Button>
132
133
yield_adder: (deleted_handouts) ->
134
deleted_paths = {}
135
deleted_handouts.map (obj) =>
136
if obj.path
137
deleted_paths[obj.path] = obj.handout_id
138
139
(path) =>
140
if deleted_paths[path]?
141
@props.actions.undelete_handout(deleted_paths[path])
142
else
143
@props.actions.add_handout(path)
144
145
render: ->
146
# Computed data from state changes have to go in render
147
{shown_handouts, deleted_handouts, num_omitted, num_deleted} = @compute_handouts_list()
148
add_handout = @yield_adder(deleted_handouts)
149
150
header =
151
<FoldersToolbar
152
search = {@state.search}
153
search_change = {(value) => @setState(search:value)}
154
num_omitted = {num_omitted}
155
project_id = {@props.project_id}
156
items = {@props.all_handouts}
157
add_folders = {(paths)=>paths.map(add_handout)}
158
item_name = {"handout"}
159
plural_item_name = {"handouts"}
160
/>
161
162
<Panel header={header}>
163
{for handout, i in shown_handouts
164
<Handout backgroundColor={if i%2==0 then "#eee"} key={handout.handout_id}
165
handout={@props.all_handouts.get(handout.handout_id)} project_id={@props.project_id}
166
students={@props.students} user_map={@props.user_map} actions={@props.actions}
167
store_object={@props.store_object} open_directory={@props.project_actions.open_directory}
168
is_expanded={@props.expanded_handouts.has(handout.handout_id)}
169
name={@props.name}
170
/>}
171
{@render_show_deleted_button(num_deleted, shown_handouts.length ? 0) if num_deleted > 0}
172
</Panel>
173
174
exports.HandoutsPanel.Header = rclass
175
propTypes :
176
n : rtypes.number
177
178
render: ->
179
<Tip delayShow=1300
180
title="Handouts"
181
tip="This tab lists all of the handouts associated with your course.">
182
<span>
183
<Icon name="files-o"/> Handouts {if @props.n? then " (#{@props.n})" else ""}
184
</span>
185
</Tip>
186
187
Handout = rclass
188
propTypes :
189
name : rtypes.string
190
handout : rtypes.object
191
backgroundColor : rtypes.string
192
store_object : rtypes.object
193
actions : rtypes.object
194
open_directory : rtypes.func # open_directory(path)
195
is_expanded : rtypes.bool
196
197
getInitialState: ->
198
confirm_delete : false
199
200
open_handout_path: (e)->
201
e.preventDefault()
202
@props.open_directory(@props.handout.get('path'))
203
204
copy_handout_to_all: (step, new_only) ->
205
@props.actions.copy_handout_to_all_students(@props.handout, new_only)
206
207
render_more_header: ->
208
<div>
209
<div style={fontSize:'15pt', marginBottom:'5px'} >
210
{@props.handout.get('path')}
211
</div>
212
<Button onClick={@open_handout_path}>
213
<Icon name="folder-open-o" /> Edit Handout
214
</Button>
215
</div>
216
217
render_handout_notes: ->
218
<Row key='note' style={styles.note}>
219
<Col xs=2>
220
<Tip title="Notes about this handout" tip="Record notes about this handout here. These notes are only visible to you, not to your students. Put any instructions to students about handouts in a file in the directory that contains the handout.">
221
Private Handout Notes<br /><span style={color:"#666"}></span>
222
</Tip>
223
</Col>
224
<Col xs=10>
225
<MarkdownInput
226
persist_id = {@props.handout.get('path') + @props.handout.get('assignment_id') + "note"}
227
attach_to = {@props.name}
228
rows = 6
229
placeholder = 'Private notes about this handout (not visible to students)'
230
default_value = {@props.handout.get('note')}
231
on_save = {(value)=>@props.actions.set_handout_note(@props.handout, value)}
232
/>
233
</Col>
234
</Row>
235
236
render_copy_all: (status) ->
237
steps = STEPS()
238
for step in steps
239
if @state["copy_confirm_#{step}"]
240
@render_copy_confirm(step, status)
241
242
render_copy_confirm: (step, status) ->
243
<span key="copy_confirm_#{step}">
244
{@render_copy_confirm_to_all(step, status) if status[step]==0}
245
{@render_copy_confirm_to_all_or_new(step, status) if status[step]!=0}
246
</span>
247
248
render_copy_cancel: (step) ->
249
cancel = =>
250
@setState("copy_confirm_#{step}":false, "copy_confirm_all_#{step}":false, copy_confirm:false)
251
<Button key='cancel' onClick={cancel}>Cancel</Button>
252
253
copy_handout: (step, new_only) ->
254
# handout to all (non-deleted) students
255
switch step
256
when 'handout'
257
@props.actions.copy_handout_to_all_students(@props.handout, new_only)
258
else
259
console.log("BUG -- unknown step: #{step}")
260
@setState("copy_confirm_#{step}":false, "copy_confirm_all_#{step}":false, copy_confirm:false)
261
262
render_copy_confirm_to_all: (step, status) ->
263
n = status["not_#{step}"]
264
<Alert bsStyle='warning' key="#{step}_confirm_to_all", style={marginTop:'15px'}>
265
<div style={marginBottom:'15px'}>
266
{misc.capitalize(step_verb(step))} this handout {step_direction(step)} the {n} student{if n>1 then "s" else ""}{step_ready(step, n)}?
267
</div>
268
<ButtonToolbar>
269
<Button key='yes' bsStyle='primary' onClick={=>@copy_handout(step, false)} >Yes</Button>
270
{@render_copy_cancel(step)}
271
</ButtonToolbar>
272
</Alert>
273
274
copy_confirm_all_caution: (step) ->
275
switch step
276
when 'handout'
277
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."
278
279
render_copy_confirm_overwrite_all: (step, status) ->
280
<div key="copy_confirm_overwrite_all" style={marginTop:'15px'}>
281
<div style={marginBottom:'15px'}>
282
{@copy_confirm_all_caution(step)}
283
</div>
284
<ButtonToolbar>
285
<Button key='all' bsStyle='danger' onClick={=>@copy_handout(step, false)}>Yes, do it</Button>
286
{@render_copy_cancel(step)}
287
</ButtonToolbar>
288
</div>
289
290
render_copy_confirm_to_all_or_new: (step, status) ->
291
n = status["not_#{step}"]
292
m = n + status[step]
293
<Alert bsStyle='warning' key="#{step}_confirm_to_all_or_new" style={marginTop:'15px'}>
294
<div style={marginBottom:'15px'}>
295
{misc.capitalize(step_verb(step))} this handout {step_direction(step)}...
296
</div>
297
<ButtonToolbar>
298
<Button key='all' bsStyle='danger' onClick={=>@setState("copy_confirm_all_#{step}":true, copy_confirm:true)}
299
disabled={@state["copy_confirm_all_#{step}"]} >
300
{if step=='handout' then 'All' else 'The'} {m} students{step_ready(step, m)}...
301
</Button>
302
{<Button key='new' bsStyle='primary' onClick={=>@copy_handout(step, true)}>The {n} student{if n>1 then 's' else ''} not already {past_tense(step_verb(step))} {step_direction(step)}</Button> if n}
303
{@render_copy_cancel(step)}
304
</ButtonToolbar>
305
{@render_copy_confirm_overwrite_all(step, status) if @state["copy_confirm_all_#{step}"]}
306
</Alert>
307
308
render_handout_button: (handout_count) ->
309
bsStyle = if handout_count == 0 then "primary" else "warning"
310
<Button key='handout'
311
bsStyle = {bsStyle}
312
onClick = {=>@setState(copy_confirm_handout:true, copy_confirm:true)}
313
disabled = {@state.copy_confirm}
314
style = {@outside_button_style}>
315
<Tip title={<span>Handout: <Icon name='user-secret'/> You <Icon name='long-arrow-right' /> <Icon name='users' /> Students </span>}
316
tip="Copy the files for this handout from this project to all other student projects.">
317
<Icon name="share-square-o" /> Distribute...
318
</Tip>
319
</Button>
320
321
delete_handout: ->
322
@props.actions.delete_handout(@props.handout)
323
@setState(confirm_delete:false)
324
325
undelete_handout: ->
326
@props.actions.undelete_handout(@props.handout)
327
328
render_confirm_delete: ->
329
<Alert bsStyle='warning' key='confirm_delete'>
330
Are you sure you want to delete this handout (you can undelete it later)?
331
<br/> <br/>
332
<ButtonToolbar>
333
<Button key='yes' onClick={@delete_handout} bsStyle='danger'>
334
<Icon name="trash" /> Delete
335
</Button>
336
<Button key='no' onClick={=>@setState(confirm_delete:false)}>
337
Cancel
338
</Button>
339
</ButtonToolbar>
340
</Alert>
341
342
render_delete_button: ->
343
if @props.handout.get('deleted')
344
<Tip key='delete' placement='left' title="Undelete handout" tip="Make the handout visible again in the handout list and in student grade lists.">
345
<Button onClick={@undelete_handout} style={@outside_button_style}>
346
<Icon name="trash-o" /> Undelete
347
</Button>
348
</Tip>
349
else
350
<Tip key='delete' placement='left' title="Delete handout" tip="Deleting this handout removes it from the handout list and student grade lists, but does not delete any files off of disk. You can always undelete an handout later by showing it using the 'show deleted handouts' button.">
351
<Button onClick={=>@setState(confirm_delete:true)} disabled={@state.confirm_delete} style={@outside_button_style}>
352
<Icon name="trash" /> Delete...
353
</Button>
354
</Tip>
355
356
render_more: ->
357
<Row key='more'>
358
<Col sm=12>
359
<Panel header={@render_more_header()}>
360
<StudentListForHandout handout={@props.handout} students={@props.students}
361
user_map={@props.user_map} store_object={@props.store_object} actions={@props.actions}/>
362
{@render_handout_notes()}
363
</Panel>
364
</Col>
365
</Row>
366
367
outside_button_style :
368
margin : '4px'
369
paddingTop : '6px'
370
paddingBottom : '4px'
371
372
render: ->
373
status = @props.store_object.get_handout_status(@props.handout)
374
<Row style={if @props.is_expanded then styles.selected_entry else styles.entry}>
375
<Col xs=12>
376
<Row key='summary' style={backgroundColor:@props.backgroundColor}>
377
<Col md=2 style={paddingRight:'0px'}>
378
<h5>
379
<a href='' onClick={(e)=>e.preventDefault();@actions(@props.name).toggle_item_expansion('handout', @props.handout.get('handout_id'))}>
380
<Icon style={marginRight:'10px', float:'left'}
381
name={if @props.is_expanded then 'caret-down' else 'caret-right'} />
382
<div>
383
{misc.trunc_middle(@props.handout.get('path'), 24)}
384
{<b> (deleted)</b> if @props.handout.get('deleted')}
385
</div>
386
</a>
387
</h5>
388
</Col>
389
<Col md=6>
390
<Row style={marginLeft:'8px'}>
391
{@render_handout_button(status.handout)}
392
<span style={color:'#666', marginLeft:'5px'}>
393
({status.handout}/{status.handout + status.not_handout} received)
394
</span>
395
</Row>
396
<Row style={marginLeft:'8px'}>
397
{@render_copy_all(status)}
398
</Row>
399
</Col>
400
<Col md=4>
401
<Row>
402
<span className='pull-right'>
403
{@render_delete_button()}
404
</span>
405
</Row>
406
<Row>
407
{@render_confirm_delete() if @state.confirm_delete}
408
</Row>
409
</Col>
410
</Row>
411
{@render_more() if @props.is_expanded}
412
</Col>
413
</Row>
414
415
StudentListForHandout = rclass
416
propTypes :
417
user_map : rtypes.object
418
students : rtypes.object
419
handout : rtypes.object
420
store_object : rtypes.object
421
actions : rtypes.object
422
423
render_students: ->
424
v = util.immutable_to_list(@props.students, 'student_id')
425
# fill in names, for use in sorting and searching (TODO: caching)
426
v = (x for x in v when not x.deleted)
427
for x in v
428
user = @props.user_map.get(x.account_id)
429
if user?
430
x.first_name = user.get('first_name')
431
x.last_name = user.get('last_name')
432
x.name = x.first_name + ' ' + x.last_name
433
x.sort = (x.last_name + ' ' + x.first_name).toLowerCase()
434
else if x.email_address?
435
x.name = x.sort = x.email_address.toLowerCase()
436
437
v.sort (a,b) ->
438
return misc.cmp(a.sort, b.sort)
439
440
for x in v
441
@render_student_info(x.student_id, x)
442
443
render_student_info: (id, student) ->
444
<StudentHandoutInfo
445
key = {id}
446
actions = {@props.actions}
447
info = {@props.store_object.student_handout_info(id, @props.handout)}
448
title = {misc.trunc_middle(@props.store_object.get_student_name(id), 40)}
449
student = {id}
450
handout = {@props.handout}
451
/>
452
453
render: ->
454
<div>
455
<StudentHandoutInfoHeader
456
key = 'header'
457
title = "Student"
458
/>
459
{@render_students()}
460
</div>
461
462
StudentHandoutInfoHeader = rclass
463
displayName : "CourseEditor-StudentHandoutInfoHeader"
464
465
propTypes :
466
title : rtypes.string.isRequired
467
468
render_col: (step_number, key, width) ->
469
switch key
470
when 'last_handout'
471
title = 'Distribute to Student'
472
tip = 'This column gives the status whether a handout was received by a student and lets you copy the handout to one student at a time.'
473
<Col md={width} key={key}>
474
<Tip title={title} tip={tip}>
475
<b>{step_number}. {title}</b>
476
</Tip>
477
</Col>
478
479
480
render_headers: ->
481
w = 12
482
<Row>
483
{@render_col(1, 'last_handout', w)}
484
</Row>
485
486
render: ->
487
<Row style={borderBottom:'2px solid #aaa'} >
488
<Col md=2 key='title'>
489
<Tip title={@props.title} tip={if @props.title=="Handout" then "This column gives the directory name of the handout." else "This column gives the name of the student."}>
490
<b>{@props.title}</b>
491
</Tip>
492
</Col>
493
<Col md=10 key="rest">
494
{@render_headers()}
495
</Col>
496
</Row>
497
498
StudentHandoutInfo = rclass
499
displayName : "CourseEditor-StudentHandoutInfo"
500
501
propTypes :
502
actions : rtypes.object.isRequired
503
info : rtypes.object.isRequired
504
title : rtypes.oneOfType([rtypes.string,rtypes.object]).isRequired
505
student : rtypes.oneOfType([rtypes.string,rtypes.object]).isRequired # required string (student_id) or student immutable js object
506
handout : rtypes.oneOfType([rtypes.string,rtypes.object]).isRequired # required string (handout_id) or handout immutable js object
507
508
getInitialState: ->
509
{}
510
511
open: (handout_id, student_id) ->
512
@props.actions.open_handout(handout_id, student_id)
513
514
copy: (handout_id, student_id) ->
515
@props.actions.copy_handout_to_student(handout_id, student_id)
516
517
stop: (handout_id, student_id) ->
518
@props.actions.stop_copying_handout(handout_id, student_id)
519
520
render_last_time: (name, time) ->
521
<div key='time' style={color:"#666"}>
522
(<BigTime date={time} />)
523
</div>
524
525
render_open_recopy_confirm: (name, open, copy, copy_tip, open_tip) ->
526
key = "recopy_#{name}"
527
if @state[key]
528
v = []
529
v.push <Button key="copy_confirm" bsStyle="danger" onClick={=>@setState("#{key}":false);copy()}>
530
<Icon name="share-square-o"/> Yes, {name.toLowerCase()} again
531
</Button>
532
v.push <Button key="copy_cancel" onClick={=>@setState("#{key}":false);}>
533
Cancel
534
</Button>
535
return v
536
else
537
<Button key="copy" bsStyle='warning' onClick={=>@setState("#{key}":true)}>
538
<Tip title={name}
539
tip={<span>{copy_tip}</span>}>
540
<Icon name='share-square-o'/> {name}...
541
</Tip>
542
</Button>
543
544
render_open_recopy: (name, open, copy, copy_tip, open_tip) ->
545
<ButtonToolbar key='open_recopy'>
546
{@render_open_recopy_confirm(name, open, copy, copy_tip, open_tip)}
547
<Button key='open' onClick={open}>
548
<Tip title="Open handout" tip={open_tip}>
549
<Icon name="folder-open-o" /> Open
550
</Tip>
551
</Button>
552
</ButtonToolbar>
553
554
render_open_copying: (name, open, stop) ->
555
<ButtonGroup key='open_copying'>
556
<Button key="copy" bsStyle='success' disabled={true}>
557
<Icon name="cc-icon-cocalc-ring" spin /> Working...
558
</Button>
559
<Button key="stop" bsStyle='danger' onClick={stop}>
560
<Icon name="times" />
561
</Button>
562
<Button key='open' onClick={open}>
563
<Icon name="folder-open-o" /> Open
564
</Button>
565
</ButtonGroup>
566
567
render_copy: (name, copy, copy_tip) ->
568
<Tip key="copy" title={name} tip={copy_tip} >
569
<Button onClick={copy} bsStyle={'primary'}>
570
<Icon name="share-square-o" /> {name}
571
</Button>
572
</Tip>
573
574
render_error: (name, error) ->
575
if typeof(error) != 'string'
576
error = misc.to_json(error)
577
if error.indexOf('No such file or directory') != -1
578
error = 'Somebody may have moved the folder that should have contained the handout.\n' + error
579
else
580
error = "Try to #{name.toLowerCase()} again:\n" + error
581
<ErrorDisplay key='error' error={error} style={maxHeight: '140px', overflow:'auto'}/>
582
583
render_last: (name, obj, info, enable_copy, copy_tip, open_tip) ->
584
open = => @open(info.handout_id, info.student_id)
585
copy = => @copy(info.handout_id, info.student_id)
586
stop = => @stop(info.handout_id, info.student_id)
587
obj ?= {}
588
v = []
589
if enable_copy
590
if obj.start
591
v.push(@render_open_copying(name, open, stop))
592
else if obj.time
593
v.push(@render_open_recopy(name, open, copy, copy_tip, open_tip))
594
else
595
v.push(@render_copy(name, copy, copy_tip))
596
if obj.time
597
v.push(@render_last_time(name, obj.time))
598
if obj.error
599
v.push(@render_error(name, obj.error))
600
return v
601
602
render: ->
603
width = 12
604
<Row style={borderTop:'1px solid #aaa', paddingTop:'5px', paddingBottom: '5px'}>
605
<Col md=2 key="title">
606
{@props.title}
607
</Col>
608
<Col md=10 key="rest">
609
<Row>
610
<Col md={width} key='last_handout'>
611
{@render_last('Distribute', @props.info.status, @props.info, true,
612
"Copy the handout from your project to this student's project.",
613
"Open the student's copy of this handout directly in their project. You will be able to see them type, chat with them, answer questions, etc.")}
614
</Col>
615
</Row>
616
</Col>
617
</Row>
618
619