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
# standard non-CoCalc libraries
23
immutable = require('immutable')
24
25
# CoCalc libraries
26
misc = require('smc-util/misc')
27
{webapp_client} = require('../webapp_client')
28
29
# React libraries and Components
30
{React, rclass, rtypes} = require('../smc-react')
31
{Alert, Button, ButtonToolbar, ButtonGroup, Row, Col,
32
Panel, Well, FormGroup, FormControl, Checkbox} = require('react-bootstrap')
33
34
# CoCalc Components
35
{Calendar, Icon, LabeledRow, Loading, MarkdownInput,
36
Space, TextInput, TimeAgo, Tip} = require('../r_misc')
37
38
{StudentProjectUpgrades} = require('./upgrades')
39
{HelpBox} = require('./help_box')
40
{DeleteStudentsPanel} = require('./delete_students')
41
42
StudentProjectsStartStopPanel = rclass ({name}) ->
43
displayName : "CourseEditorSettings-StudentProjectsStartStopPanel"
44
45
reduxProps :
46
"#{name}" :
47
action_all_projects_state : rtypes.string
48
49
propTypes :
50
num_running_projects : rtypes.number
51
num_students : rtypes.number
52
53
getDefaultProps: ->
54
action_all_projects_state : "any"
55
56
getInitialState: ->
57
confirm_stop_all_projects : false
58
confirm_start_all_projects : false
59
60
render_in_progress_action: ->
61
state_name = @props.action_all_projects_state
62
switch state_name
63
when "stopping"
64
if @props.num_running_projects == 0
65
return
66
bsStyle = 'warning'
67
else
68
if @props.num_running_projects == @props.num_students
69
return
70
bsStyle = 'info'
71
72
<Alert bsStyle=bsStyle>
73
{misc.capitalize(state_name)} all projects... <Icon name='cc-icon-cocalc-ring' spin />
74
</Alert>
75
76
render_confirm_stop_all_projects: ->
77
<Alert bsStyle='warning'>
78
Are you sure you want to stop all student projects (this might be disruptive)?
79
<br/>
80
<br/>
81
<ButtonToolbar>
82
<Button bsStyle='warning' onClick={=>@setState(confirm_stop_all_projects:false);@actions(@props.name).action_all_student_projects('stop')}>
83
<Icon name='hand-stop-o'/> Stop all
84
</Button>
85
<Button onClick={=>@setState(confirm_stop_all_projects:false)}>
86
Cancel
87
</Button>
88
</ButtonToolbar>
89
</Alert>
90
91
render_confirm_start_all_projects: ->
92
<Alert bsStyle='info'>
93
Are you sure you want to start all student projects? This will ensure the projects are already running when the students
94
open them.
95
<br/>
96
<br/>
97
<ButtonToolbar>
98
<Button bsStyle='primary' onClick={=>@setState(confirm_start_all_projects:false);@actions(@props.name).action_all_student_projects('start')}>
99
<Icon name='flash'/> Start all
100
</Button>
101
<Button onClick={=>@setState(confirm_start_all_projects:false)}>
102
Cancel
103
</Button>
104
</ButtonToolbar>
105
</Alert>
106
107
render: ->
108
r = @props.num_running_projects
109
n = @props.num_students
110
<Panel header={<h4><Icon name='flash'/> Student projects control</h4>}>
111
<Row>
112
<Col md=9>
113
{r} of {n} student projects currently running.
114
</Col>
115
</Row>
116
<Row style={marginTop:'10px'}>
117
<Col md=12>
118
<ButtonToolbar>
119
<Button onClick={=>@setState(confirm_start_all_projects:true)}
120
disabled={n==0 or n==r or @state.confirm_start_all_projects or @props.action_all_projects_state == "starting"}
121
>
122
<Icon name="flash"/> Start all...
123
</Button>
124
<Button onClick={=>@setState(confirm_stop_all_projects:true)}
125
disabled={n==0 or r==0 or @state.confirm_stop_all_projects or @props.action_all_projects_state == "stopping"}
126
>
127
<Icon name="hand-stop-o"/> Stop all...
128
</Button>
129
</ButtonToolbar>
130
</Col>
131
</Row>
132
<Row style={marginTop:'10px'}>
133
<Col md=12>
134
{@render_confirm_start_all_projects() if @state.confirm_start_all_projects}
135
{@render_confirm_stop_all_projects() if @state.confirm_stop_all_projects}
136
{@render_in_progress_action() if @props.action_all_projects_state != "any"}
137
</Col>
138
</Row>
139
<hr/>
140
<span style={color:'#666'}>
141
Start all projects associated with this course so they are immediately ready for your students to use. For example, you might do this before a computer lab. You can also stop all projects in order to ensure that they do not waste resources or are properly upgraded when next used by students.
142
</span>
143
</Panel>
144
145
DisableStudentCollaboratorsPanel = rclass ->
146
propTypes:
147
checked : rtypes.bool
148
on_change : rtypes.func
149
150
render: ->
151
<Panel header={<h4><Icon name='envelope'/> Collaborator policy</h4>}>
152
<div style={border:'1px solid lightgrey', padding: '10px', borderRadius: '5px'}>
153
<Checkbox
154
checked = {@props.checked}
155
onChange = {(e)=>@props.on_change(e.target.checked)}>
156
Allow arbitrary collaborators
157
</Checkbox>
158
</div>
159
<hr/>
160
<span style={color:'#666'}>
161
Every collaborator on the project that contains this course is automatically added
162
to every student project (and the shared project). In addition, each student is
163
a collaborator on their project. If students add additional collaborators, by default
164
they will be allowed. If you uncheck the above box, then collaborators
165
will be automatically removed from projects; in particular, students may
166
not add arbitrary collaborators to their projects.
167
</span>
168
</Panel>
169
170
exports.SettingsPanel = rclass
171
displayName : "CourseEditorSettings"
172
173
propTypes :
174
redux : rtypes.object.isRequired
175
name : rtypes.string.isRequired
176
path : rtypes.string.isRequired
177
project_id : rtypes.string.isRequired
178
settings : rtypes.immutable.Map.isRequired
179
project_map : rtypes.immutable.Map.isRequired
180
181
getInitialState: ->
182
show_students_pay_dialog : false
183
184
###
185
# Editing title/description
186
###
187
render_title_desc_header: ->
188
<h4>
189
<Icon name='header' /> Title and description
190
</h4>
191
192
render_title_description: ->
193
if not @props.settings?
194
return <Loading />
195
<Panel header={@render_title_desc_header()}>
196
<LabeledRow label="Title">
197
<TextInput
198
text={@props.settings.get('title') ? ''}
199
on_change={(title)=>@actions(@props.name).set_title(title)}
200
/>
201
</LabeledRow>
202
<LabeledRow label="Description">
203
<MarkdownInput
204
persist_id = {@props.name + "course-description"}
205
attach_to = {@props.name}
206
rows = 6
207
type = "textarea"
208
default_value = {@props.settings.get('description')}
209
on_save = {(desc)=>@actions(@props.name).set_description(desc)}
210
/>
211
</LabeledRow>
212
<hr/>
213
<span style={color:'#666'}>
214
Set the course title and description here.
215
When you change the title or description, the corresponding
216
title and description of each student project will be updated.
217
The description is set to this description, and the title
218
is set to the student name followed by this title.
219
Use the description to provide additional information about
220
the course, e.g., a link to the main course website.
221
</span>
222
</Panel>
223
224
###
225
# Grade export
226
###
227
render_grades_header: ->
228
<h4>
229
<Icon name='table' /> Export grades
230
</h4>
231
232
path: (ext) ->
233
p = @props.path
234
i = p.lastIndexOf('.')
235
return p.slice(0,i) + '.' + ext
236
237
open_file: (path) ->
238
@actions(project_id : @props.project_id).open_file(path:path,foreground:true)
239
240
write_file: (path, content) ->
241
actions = @actions(@props.name)
242
id = actions.set_activity(desc:"Writing #{path}")
243
webapp_client.write_text_file_to_project
244
project_id : @props.project_id
245
path : path
246
content : content
247
cb : (err) =>
248
actions.set_activity(id:id)
249
if not err
250
@open_file(path)
251
else
252
actions.set_error("Error writing '#{path}' -- '#{err}'")
253
254
save_grades_to_csv: ->
255
store = @props.redux.getStore(@props.name)
256
assignments = store.get_sorted_assignments()
257
students = store.get_sorted_students()
258
# CSV definition: http://edoceo.com/utilitas/csv-file-format
259
# i.e. double quotes everywhere (not single!) and double quote in double quotes usually blows up
260
timestamp = (webapp_client.server_time()).toISOString()
261
content = "# Course '#{@props.settings.get('title')}'\n"
262
content += "# exported #{timestamp}\n"
263
content += "Name,Email,"
264
content += ("\"#{assignment.get('path')}\"" for assignment in assignments).join(',') + '\n'
265
for student in store.get_sorted_students()
266
grades = ("\"#{store.get_grade(assignment, student) ? ''}\"" for assignment in assignments).join(',')
267
name = "\"#{store.get_student_name(student)}\""
268
email = "\"#{store.get_student_email(student) ? ''}\""
269
line = [name, email, grades].join(',')
270
content += line + '\n'
271
@write_file(@path('csv'), content)
272
273
save_grades_to_py: ->
274
###
275
example:
276
course = 'title'
277
exported = 'iso date'
278
assignments = ['Assignment 1', 'Assignment 2']
279
students=[
280
{'name':'Foo Bar', 'email': 'foo@bar.com', 'grades':[85,37]},
281
{'name':'Bar None', 'email': 'bar@school.edu', 'grades':[15,50]},
282
]
283
###
284
timestamp = (webapp_client.server_time()).toISOString()
285
store = @props.redux.getStore(@props.name)
286
assignments = store.get_sorted_assignments()
287
students = store.get_sorted_students()
288
content = "course = '#{@props.settings.get('title')}'\n"
289
content += "exported = '#{timestamp}'\n"
290
content += "assignments = ["
291
content += ("'#{assignment.get('path')}'" for assignment in assignments).join(',') + ']\n'
292
293
content += 'students = [\n'
294
for student in store.get_sorted_students()
295
grades = (("'#{store.get_grade(assignment, student) ? ''}'") for assignment in assignments).join(',')
296
name = store.get_student_name(student)
297
email = store.get_student_email(student)
298
email = if email? then "'#{email}'" else 'None'
299
line = " {'name':'#{name}', 'email':#{email}, 'grades':[#{grades}]},"
300
content += line + '\n'
301
content += ']\n'
302
@write_file(@path('py'), content)
303
304
render_save_grades: ->
305
<Panel header={@render_grades_header()}>
306
<div style={marginBottom:'10px'}>Save grades to... </div>
307
<ButtonToolbar>
308
<Button onClick={@save_grades_to_csv}><Icon name='file-text-o'/> CSV file...</Button>
309
<Button onClick={@save_grades_to_py}><Icon name='file-code-o'/> Python file...</Button>
310
</ButtonToolbar>
311
<hr/>
312
<span style={color:"#666"}>
313
Export all the grades you have recorded
314
for students in your course to a csv or Python file.
315
</span>
316
</Panel>
317
318
###
319
# Custom invitation email body
320
###
321
322
render_email_invite_body: ->
323
template_instr = ' Also, {title} will be replaced by the title of the course and {name} by your name.'
324
<Panel header={<h4><Icon name='envelope'/> Customize email invitation</h4>}>
325
<div style={border:'1px solid lightgrey', padding: '10px', borderRadius: '5px'}>
326
<MarkdownInput
327
persist_id = {@props.name + "email-invite-body"}
328
attach_to = {@props.name}
329
rows = 6
330
type = "textarea"
331
default_value = {@props.redux.getStore(@props.name).get_email_invite()}
332
on_save = {(body)=>@actions(@props.name).set_email_invite(body)}
333
/>
334
</div>
335
<hr/>
336
<span style={color:'#666'}>
337
If you add a student to this course using their email address, and they do not
338
have a CoCalc account, then they will receive an email invitation. {template_instr}
339
</span>
340
</Panel>
341
342
render_start_all_projects: ->
343
r = @props.redux.getStore(@props.name).num_running_projects(@props.project_map)
344
n = @props.redux.getStore(@props.name).num_students()
345
<StudentProjectsStartStopPanel
346
name = {@props.name}
347
num_running_projects = {r}
348
num_students = {n}
349
/>
350
351
###
352
Students pay
353
###
354
get_student_pay_when: ->
355
date = @props.settings.get('pay')
356
if date
357
return date
358
else
359
return misc.days_ago(-7)
360
361
click_student_pay_button: ->
362
@setState(show_students_pay_dialog : true)
363
364
render_students_pay_button: ->
365
<Button bsStyle='primary' onClick={@click_student_pay_button}>
366
<Icon name='arrow-circle-up' /> {if @state.students_pay then "Adjust settings" else "Require students to pay"}...
367
</Button>
368
369
render_require_students_pay_desc: ->
370
date = @props.settings.get('pay')
371
if date > webapp_client.server_time()
372
<span>
373
Your students will see a warning until <TimeAgo date={date} />. They will then be required to upgrade for a one-time fee of $9.
374
</span>
375
else
376
<span>
377
Your students are required to upgrade their project.
378
</span>
379
380
render_require_students_pay_when: ->
381
if not @props.settings.get('pay')
382
return <span/>
383
<div style={marginBottom:'1em'}>
384
<div style={width:'50%', marginLeft:'3em', marginBottom:'1ex'}>
385
<Calendar
386
value = {@props.settings.get('pay')}
387
on_change = {(date)=>@actions(@props.name).set_course_info(date)}
388
/>
389
</div>
390
{@render_require_students_pay_desc() if @props.settings.get('pay')}
391
</div>
392
393
render_students_pay_submit_buttons: ->
394
<Button onClick={=>@setState(show_students_pay_dialog:false)}>
395
Close
396
</Button>
397
398
handle_students_pay_checkbox: (e) ->
399
if e.target.checked
400
@actions(@props.name).set_course_info(@get_student_pay_when())
401
else
402
@actions(@props.name).set_course_info('')
403
404
render_students_pay_checkbox_label: ->
405
if @props.settings.get('pay')
406
if webapp_client.server_time() >= @props.settings.get('pay')
407
<span>Require that students upgrade immediately:</span>
408
else
409
<span>Require that students upgrade by <TimeAgo date={@props.settings.get('pay')} />: </span>
410
else
411
<span>Require that students upgrade...</span>
412
413
render_students_pay_checkbox: ->
414
<span>
415
<Checkbox checked = {!!@props.settings.get('pay')}
416
key = 'students_pay'
417
ref = 'student_pay'
418
onChange = {@handle_students_pay_checkbox}
419
>
420
{@render_students_pay_checkbox_label()}
421
</Checkbox>
422
</span>
423
424
render_students_pay_dialog: ->
425
<Alert bsStyle='info'>
426
<h3><Icon name='arrow-circle-up' /> Require students to upgrade</h3>
427
<hr/>
428
<span>Click the following checkbox to require that all students in the course pay a <b>one-time $9</b> fee to move their projects to members-only computers and enable full internet access, for four months. Members-only computers are not randomly rebooted constantly and have far fewer users. Student projects that are already on members-only hosts will not be impacted. <em>You will not be charged.</em></span>
429
430
{@render_students_pay_checkbox()}
431
{@render_require_students_pay_when() if @props.settings.get('pay')}
432
{@render_students_pay_submit_buttons()}
433
</Alert>
434
435
render_student_pay_desc: ->
436
if @props.settings.get('pay')
437
<span><span style={fontSize:'18pt'}><Icon name="check"/></span> <Space />{@render_require_students_pay_desc()}</span>
438
else
439
<span>Require that all students in the course pay a one-time $9 fee to move their projects to members only hosts and enable full internet access, for four months. This is optional, but will ensure that your students have a better experience and receive priority support.</span>
440
441
442
render_require_students_pay: ->
443
<Panel header={<h4><Icon name='dashboard' /> Require students to upgrade (students pay)</h4>}>
444
{if @state.show_students_pay_dialog then @render_students_pay_dialog() else @render_students_pay_button()}
445
<hr/>
446
<div style={color:"#666"}>
447
{@render_student_pay_desc()}
448
</div>
449
</Panel>
450
451
render: ->
452
<div>
453
<Row>
454
<Col md=6>
455
{@render_require_students_pay()}
456
<StudentProjectUpgrades name={@props.name} redux={@props.redux} upgrade_goal={@props.settings?.get('upgrade_goal')} />
457
{@render_save_grades()}
458
{@render_start_all_projects()}
459
<DeleteStudentsPanel
460
delete = {@actions(@props.name).delete_all_student_projects}
461
/>
462
</Col>
463
<Col md=6>
464
<HelpBox/>
465
{@render_title_description()}
466
{@render_email_invite_body()}
467
<DisableStudentCollaboratorsPanel
468
checked = {!!@props.settings.get('allow_collabs')}
469
on_change = {@actions(@props.name).set_allow_collabs}
470
/>
471
</Col>
472
</Row>
473
</div>
474
475
exports.SettingsPanel.Header = rclass
476
render: ->
477
<Tip delayShow=1300 title="Settings"
478
tip="Configure various things about your course here, including the title and description. You can also export all grades in various formats from this page.">
479
<span>
480
<Icon name="wrench"/> Settings
481
</span>
482
</Tip>
483