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 and components
28
{React, ReactDOM, rclass, rtypes} = require('../smc-react')
29
{Button, ButtonToolbar, ButtonGroup, FormGroup, FormControl, InputGroup, Row, Col, Panel, Well} = require('react-bootstrap')
30
31
# CoCalc components
32
{User} = require('../users')
33
{ErrorDisplay, Icon, MarkdownInput, SearchInput, Space, TimeAgo, Tip} = require('../r_misc')
34
{StudentAssignmentInfo, StudentAssignmentInfoHeader} = require('./common')
35
util = require('./util')
36
styles = require('./styles')
37
38
exports.StudentsPanel = rclass ({name}) ->
39
displayName: "CourseEditorStudents"
40
41
reduxProps:
42
"#{name}":
43
expanded_students : rtypes.immutable.Set
44
active_student_sort : rtypes.object
45
get_student_name : rtypes.func
46
47
propTypes:
48
name : rtypes.string.isRequired
49
redux : rtypes.object.isRequired
50
project_id : rtypes.string.isRequired
51
students : rtypes.object.isRequired
52
user_map : rtypes.object.isRequired
53
project_map : rtypes.object.isRequired
54
assignments : rtypes.object.isRequired
55
56
getInitialState: ->
57
err : undefined
58
search : ''
59
add_search : ''
60
add_searching : false
61
add_select : undefined
62
existing_students: undefined
63
selected_option_nodes : undefined
64
show_deleted : false
65
66
do_add_search: (e) ->
67
# Search for people to add to the course
68
e?.preventDefault()
69
if not @props.students?
70
return
71
if @state.add_searching # already searching
72
return
73
search = @state.add_search.trim()
74
if search.length == 0
75
@setState(err:undefined, add_select:undefined, existing_students:undefined, selected_option_nodes:undefined)
76
return
77
@setState(add_searching:true, add_select:undefined, existing_students:undefined, selected_option_nodes:undefined)
78
add_search = @state.add_search
79
webapp_client.user_search
80
query : add_search
81
limit : 50
82
cb : (err, select) =>
83
if err
84
@setState(add_searching:false, err:err, add_select:undefined, existing_students:undefined)
85
return
86
# Get the current collaborators/owners of the project that contains the course.
87
users = @props.redux.getStore('projects').get_users(@props.project_id)
88
# Make a map with keys the email or account_id is already part of the course.
89
already_added = users.toJS() # start with collabs on project
90
# also track **which** students are already part of the course
91
existing_students = {}
92
existing_students.account = {}
93
existing_students.email = {}
94
# For each student in course add account_id and/or email_address:
95
@props.students.map (val, key) =>
96
for n in ['account_id', 'email_address']
97
if val.get(n)?
98
already_added[val.get(n)] = true
99
# This function returns true if we shouldn't list the given account_id or email_address
100
# in the search selector for adding to the class.
101
exclude_add = (account_id, email_address) =>
102
aa = already_added[account_id] or already_added[email_address]
103
if aa
104
if account_id?
105
existing_students.account[account_id] = true
106
if email_address?
107
existing_students.email[email_address] = true
108
return aa
109
select2 = (x for x in select when not exclude_add(x.account_id, x.email_address))
110
# Put at the front of the list any email addresses not known to CoCalc (sorted in order) and also not invited to course.
111
# NOTE (see comment on https://github.com/sagemathinc/cocalc/issues/677): it is very important to pass in
112
# the original select list to nonclude_emails below, **NOT** select2 above. Otherwise, we wend up
113
# bringing back everything in the search, which is a bug.
114
select3 = (x for x in noncloud_emails(select, add_search) when not exclude_add(null, x.email_address)).concat(select2)
115
# We are no longer searching, but now show an options selector.
116
@setState(add_searching:false, add_select:select3, existing_students:existing_students)
117
118
student_add_button: ->
119
<Button onClick={@do_add_search}>
120
{if @props.add_searching then <Icon name="cc-icon-cocalc-ring" spin /> else <Icon name="search" />}
121
</Button>
122
123
add_selector_clicked: ->
124
@setState(selected_option_nodes: ReactDOM.findDOMNode(@refs.add_select).selectedOptions)
125
126
add_selected_students: (options) ->
127
emails = {}
128
for x in @state.add_select
129
if x.account_id?
130
emails[x.account_id] = x.email_address
131
students = []
132
selections = []
133
134
# first check, if no student is selected and there is just one in the list
135
if (not @state.selected_option_nodes? or @state.selected_option_nodes?.length == 0) and options?.length == 1
136
selections.push(options[0].key)
137
else
138
for option in @state.selected_option_nodes
139
selections.push(option.getAttribute('value'))
140
141
for y in selections
142
if misc.is_valid_uuid_string(y)
143
students.push
144
account_id : y
145
email_address : emails[y]
146
else
147
students.push({email_address:y})
148
@actions(@props.name).add_students(students)
149
@setState(err:undefined, add_select:undefined, selected_option_nodes:undefined, add_search:'')
150
151
get_add_selector_options: ->
152
v = []
153
seen = {}
154
for x in @state.add_select
155
key = x.account_id ? x.email_address
156
if seen[key]
157
continue
158
seen[key] = true
159
student_name = if x.account_id? then x.first_name + ' ' + x.last_name else x.email_address
160
v.push(<option key={key} value={key} label={student_name}>{student_name}</option>)
161
return v
162
163
render_add_selector: ->
164
if not @state.add_select?
165
return
166
options = @get_add_selector_options()
167
<FormGroup>
168
<FormControl componentClass='select' multiple ref="add_select" rows=10 onClick={@add_selector_clicked}>
169
{options}
170
</FormControl>
171
{@render_add_selector_button(options)}
172
</FormGroup>
173
174
render_add_selector_button: (options) ->
175
nb_selected = @state.selected_option_nodes?.length ? 0
176
_ = require('underscore')
177
es = @state.existing_students
178
if es?
179
existing = _.keys(es.email).length + _.keys(es.account).length > 0
180
else
181
# es not defined when user clicks the close button on the warning.
182
existing = 0
183
btn_text = switch options.length
184
when 0 then (if existing then "Student already added" else "No student found")
185
when 1 then "Add student"
186
else switch nb_selected
187
when 0 then "Select student above"
188
when 1 then "Add selected student"
189
else "Add #{nb_selected} students"
190
disabled = options.length == 0 or (options.length >= 2 and nb_selected == 0)
191
<Button onClick={=>@add_selected_students(options)} disabled={disabled}><Icon name='user-plus' /> {btn_text}</Button>
192
193
render_error: ->
194
ed = null
195
if @state.err
196
ed = <ErrorDisplay error={misc.trunc(@state.err,1024)} onClose={=>@setState(err:undefined)} />
197
else if @state.existing_students?
198
existing = []
199
for email, v of @state.existing_students.email
200
existing.push(email)
201
for account_id, v of @state.existing_students.account
202
user = @props.user_map.get(account_id)
203
existing.push("#{user.get('first_name')} #{user.get('last_name')}")
204
if existing.length > 0
205
if existing.length > 1
206
msg = "Already added students or project collaborators: "
207
else
208
msg = "Already added student or project collaborator: "
209
msg += existing.join(', ')
210
ed = <ErrorDisplay bsStyle='info' error=msg onClose={=>@setState(existing_students:undefined)} />
211
if ed?
212
<Row style={marginTop:'1em', marginBottom:'-10px'}><Col md=5 lgOffset=7>{ed}</Col></Row>
213
214
render_header: (num_omitted) ->
215
<div>
216
<Row style={marginBottom:'-15px'}>
217
<Col md=3>
218
<SearchInput
219
placeholder = "Find students..."
220
default_value = {@state.search}
221
on_change = {(value)=>@setState(search:value)}
222
/>
223
</Col>
224
<Col md=4>
225
{<h6>(Omitting {num_omitted} students)</h6> if num_omitted}
226
</Col>
227
<Col md=5>
228
<form onSubmit={@do_add_search}>
229
<FormGroup>
230
<InputGroup>
231
<FormControl
232
ref = 'student_add_input'
233
type = 'text'
234
placeholder = "Add student by name or email address..."
235
value = {@state.add_search}
236
onChange = {=>@setState(add_select:undefined, add_search:ReactDOM.findDOMNode(@refs.student_add_input).value)}
237
onKeyDown = {(e)=>if e.keyCode==27 then @setState(add_search:'', add_select:undefined)}
238
/>
239
<InputGroup.Button>
240
{@student_add_button()}
241
</InputGroup.Button>
242
</InputGroup>
243
</FormGroup>
244
</form>
245
{@render_add_selector()}
246
</Col>
247
</Row>
248
{@render_error()}
249
</div>
250
251
compute_student_list: ->
252
# TODO: good place to cache something...
253
# turn map of students into a list
254
# account_id : "bed84c9e-98e0-494f-99a1-ad9203f752cb" # Student's CoCalc account ID
255
# email_address : "4@student.com" # Email the instructor signed the student up with.
256
# first_name : "Rachel" # Student's first name they use for CoCalc
257
# last_name : "Florence" # Student's last name they use for CoCalc
258
# project_id : "6bea25c7-da96-4e92-aa50-46ebee1994ca" # Student's project ID for this course
259
# student_id : "920bdad2-9c3a-40ab-b5c0-eb0b3979e212" # Student's id for this course
260
# last_active : 2357025
261
# create_project : True
262
# deleted : False
263
# note : "Is younger sister of Abby Florence (TA)"
264
265
v = util.parse_students(@props.students, @props.user_map, @props.redux)
266
v.sort(util.pick_student_sorter(@props.active_student_sort))
267
268
if @props.active_student_sort.is_descending
269
v.reverse()
270
271
# Deleted students
272
w = (x for x in v when x.deleted)
273
num_deleted = w.length
274
v = (x for x in v when not x.deleted)
275
if @state.show_deleted # but show at the end...
276
v = v.concat(w)
277
278
num_omitted = 0
279
if @state.search
280
words = misc.split(@state.search.toLowerCase())
281
search = (a) -> ((a.last_name ? '') + (a.first_name ? '') + (a.email_address ? '')).toLowerCase()
282
match = (s) ->
283
for word in words
284
if s.indexOf(word) == -1
285
num_omitted += 1
286
return false
287
return true
288
v = (x for x in v when match(search(x)))
289
290
return {students:v, num_omitted:num_omitted, num_deleted:num_deleted}
291
292
render_sort_link: (column_name, display_name) ->
293
<a href=''
294
onClick={(e)=>e.preventDefault();@actions(@props.name).set_active_student_sort(column_name)}>
295
{display_name}
296
<Space/>
297
{<Icon style={marginRight:'10px'}
298
name={if @props.active_student_sort.is_descending then 'caret-up' else 'caret-down'}
299
/> if @props.active_student_sort.column_name == column_name}
300
</a>
301
302
render_student_table_header: ->
303
# HACK: -10px margin gets around ReactBootstrap's incomplete access to styling
304
<Row style={marginTop:'-10px', marginBottom:'3px'}>
305
<Col md=3>
306
<div style={display:'inline-block', width:'50%'}>
307
{@render_sort_link("first_name", "First Name")}
308
</div>
309
<div style={display:"inline-block"}>
310
{@render_sort_link("last_name", "Last Name")}
311
</div>
312
</Col>
313
<Col md=2>
314
{@render_sort_link("email", "Student Email")}
315
</Col>
316
<Col md=4>
317
{@render_sort_link("last_active", "Last Active")}
318
</Col>
319
<Col md=3>
320
{@render_sort_link("hosting", "Hosting Type")}
321
</Col>
322
</Row>
323
324
render_students: (students) ->
325
for x,i in students
326
name =
327
full : @props.get_student_name(x.student_id)
328
first : x.first_name
329
last : x.last_name
330
331
<Student background={if i%2==0 then "#eee"} key={x.student_id}
332
student_id={x.student_id} student={@props.students.get(x.student_id)}
333
user_map={@props.user_map} redux={@props.redux} name={@props.name}
334
project_map={@props.project_map}
335
assignments={@props.assignments}
336
is_expanded={@props.expanded_students.has(x.student_id)}
337
student_name={name}
338
display_account_name={true}
339
/>
340
341
render_show_deleted: (num_deleted, shown_students) ->
342
if @state.show_deleted
343
<Button style={styles.show_hide_deleted(needs_margin : shown_students.length > 0)} onClick={=>@setState(show_deleted:false)}>
344
<Tip placement='left' title="Hide deleted" tip="Students are never really deleted. Click this button so that deleted students aren't included at the bottom of the list of students. Deleted students are always hidden from the list of grades.">
345
Hide {num_deleted} deleted students
346
</Tip>
347
</Button>
348
else
349
<Button style={styles.show_hide_deleted(needs_margin : shown_students.length > 0)} onClick={=>@setState(show_deleted:true,search:'')}>
350
<Tip placement='left' title="Show deleted" tip="Students are not deleted forever, even after you delete them. Click this button to show any deleted students at the bottom of the list. You can then click on the student and click undelete to bring the assignment back.">
351
Show {num_deleted} deleted students
352
</Tip>
353
</Button>
354
355
render: ->
356
{students, num_omitted, num_deleted} = @compute_student_list()
357
<Panel header={@render_header(num_omitted, num_deleted)}>
358
{@render_student_table_header() if students.length > 0}
359
{@render_students(students)}
360
{@render_show_deleted(num_deleted, students) if num_deleted}
361
</Panel>
362
363
exports.StudentsPanel.Header = rclass
364
propTypes:
365
n : rtypes.number
366
367
render: ->
368
<Tip delayShow=1300
369
title="Students"
370
tip="This tab lists all students in your course, along with their grades on each assignment. You can also quickly find students by name on the left and add new students on the right.">
371
<span>
372
<Icon name="users"/> Students {if @props?.n? then " (#{@props.n})" else ""}
373
</span>
374
</Tip>
375
376
###
377
Updates based on:
378
- Expanded/Collapsed
379
- If collapsed: First name, last name, email, last active, hosting type
380
- If expanded: Above +, Student's status on all assignments,
381
382
###
383
Student = rclass
384
displayName: "CourseEditorStudent"
385
386
propTypes:
387
redux : rtypes.object.isRequired
388
name : rtypes.string.isRequired
389
student : rtypes.object.isRequired
390
user_map : rtypes.object.isRequired
391
project_map : rtypes.object.isRequired # here entirely to cause an update when project activity happens
392
assignments : rtypes.object.isRequired # here entirely to cause an update when project activity happens
393
background : rtypes.string
394
is_expanded : rtypes.bool
395
student_name : rtypes.object
396
display_account_name : rtypes.bool
397
398
shouldComponentUpdate: (nextProps, nextState) ->
399
return @state != nextState or @props.student != nextProps.student or @props.assignments != nextProps.assignments or @props.project_map != nextProps.project_map or @props.user_map != nextProps.user_map or @props.background != nextProps.background or @props.is_expanded != nextProps.is_expanded or @props.student_name.full != nextProps.student_name.full
400
401
componentWillReceiveProps: (next) ->
402
if @props.student_name.first != next.student_name.first
403
@setState(edited_first_name : next.student_name.first)
404
if @props.student_name.last != next.student_name.last
405
@setState(edited_last_name : next.student_name.last)
406
if @props.student.get('email_address') != next.student.get('email_address')
407
@setState(edited_email_address : next.student.get('email_address'))
408
409
getInitialState: ->
410
confirm_delete : false
411
editing_student : false
412
edited_first_name : @props.student_name.first ? ""
413
edited_last_name : @props.student_name.last ? ""
414
edited_email_address : @props.student.get('email_address') ? ""
415
416
on_key_down: (e) ->
417
switch e.keyCode
418
when 13
419
@save_student_changes()
420
when 27
421
@cancel_student_edit()
422
423
toggle_show_more: (e) ->
424
e.preventDefault()
425
if @state.editing_student
426
@cancel_student_edit()
427
item_id = @props.student.get('student_id')
428
@actions(@props.name).toggle_item_expansion('student', item_id)
429
430
render_student: ->
431
<a href='' onClick={@toggle_show_more}>
432
<Icon style={marginRight:'10px'}
433
name={if @props.is_expanded then 'caret-down' else 'caret-right'}
434
/>
435
{@render_student_name()}
436
</a>
437
438
render_student_name: ->
439
account_id = @props.student.get('account_id')
440
if account_id?
441
return <User account_id={account_id} user_map={@props.user_map} name={@props.student_name.full} show_original={@props.display_account_name}/>
442
return <span>{@props.student.get("email_address")} (invited)</span>
443
444
render_student_email: ->
445
email = @props.student.get("email_address")
446
return <a href="mailto:#{email}">{email}</a>
447
448
open_project: ->
449
@actions('projects').open_project(project_id:@props.student.get('project_id'))
450
451
create_project: ->
452
@actions(@props.name).create_student_project(@props.student_id)
453
454
render_last_active: ->
455
student_project_id = @props.student.get('project_id')
456
if not student_project_id?
457
return
458
# get the last time the student edited this project somehow.
459
last_active = @props.redux.getStore('projects').get_last_active(student_project_id)?.get(@props.student.get('account_id'))
460
if last_active # could be 0 or undefined
461
return <span style={color:"#666"}>(last used project <TimeAgo date={last_active} />)</span>
462
else
463
return <span style={color:"#666"}>(has never used project)</span>
464
465
render_hosting: ->
466
student_project_id = @props.student.get('project_id')
467
if student_project_id
468
upgrades = @props.redux.getStore('projects').get_total_project_quotas(student_project_id)
469
if not upgrades?
470
# user opening the course isn't a collaborator on this student project yet
471
return
472
if upgrades.member_host
473
<Tip placement='left' title={<span><Icon name='check'/> Members-only hosting</span>} tip='Projects is on a members-only server, which is much more robust and has priority support.'>
474
<span style={color:'#888', cursor:'pointer'}><Icon name='check'/> Members-only</span>
475
</Tip>
476
else
477
<Tip placement='left' title={<span><Icon name='exclamation-triangle'/> Free hosting</span>} tip='Project is hosted on a free server, so it may be overloaded and will be rebooted frequently. Please upgrade in course settings.'>
478
<span style={color:'#888', cursor:'pointer'}><Icon name='exclamation-triangle'/> Free</span>
479
</Tip>
480
481
render_project_access: ->
482
# first check if the project is currently being created
483
create = @props.student.get("create_project")
484
if create?
485
# if so, how long ago did it start
486
how_long = (webapp_client.server_time() - create)/1000
487
if how_long < 120 # less than 2 minutes -- still hope, so render that creating
488
return <div><Icon name="cc-icon-cocalc-ring" spin /> Creating project... (started <TimeAgo date={create} />)</div>
489
# otherwise, maybe user killed file before finished or something and it is lost; give them the chance
490
# to attempt creation again by clicking the create button.
491
492
student_project_id = @props.student.get('project_id')
493
if student_project_id?
494
<ButtonToolbar>
495
<ButtonGroup>
496
<Button onClick={@open_project}>
497
<Tip placement='right'
498
title='Student project'
499
tip='Open the course project for this student.'
500
>
501
<Icon name="edit" /> Open student project
502
</Tip>
503
</Button>
504
</ButtonGroup>
505
{@render_edit_student() if @props.student.get('account_id')}
506
</ButtonToolbar>
507
else
508
<Tip placement='right'
509
title='Create the student project'
510
tip='Create a new project for this student, then add (or invite) the student as a collaborator, and also add any collaborators on the project containing this course.'>
511
<Button onClick={@create_project}>
512
<Icon name="plus-circle" /> Create student project
513
</Button>
514
</Tip>
515
516
student_changed: ->
517
@props.student_name.first != @state.edited_first_name or
518
@props.student_name.last != @state.edited_last_name or
519
@props.student.get('email_address') != @state.edited_email_address
520
521
render_edit_student: ->
522
if @state.editing_student
523
disable_save = not @student_changed()
524
<ButtonGroup>
525
<Button onClick={@save_student_changes} bsStyle='success' disabled={disable_save}>
526
<Icon name='save'/> Save
527
</Button>
528
<Button onClick={@cancel_student_edit} >
529
Cancel
530
</Button>
531
</ButtonGroup>
532
else
533
<Button onClick={@show_edit_name_dialogue}>
534
<Icon name='address-card-o'/> Edit student...
535
</Button>
536
537
cancel_student_edit: ->
538
@setState(@getInitialState())
539
540
save_student_changes: ->
541
@actions(@props.name).set_internal_student_info @props.student,
542
first_name : @state.edited_first_name
543
last_name : @state.edited_last_name
544
email_address : @state.edited_email_address
545
546
@setState(editing_student:false)
547
548
show_edit_name_dialogue: ->
549
@setState(editing_student:true)
550
551
delete_student: ->
552
@actions(@props.name).delete_student(@props.student)
553
@setState(confirm_delete:false)
554
555
undelete_student: ->
556
@actions(@props.name).undelete_student(@props.student)
557
558
render_confirm_delete: ->
559
if @state.confirm_delete
560
<div>
561
Are you sure you want to delete this student (you can always undelete them later)?<Space/>
562
<ButtonToolbar>
563
<Button onClick={@delete_student} bsStyle='danger'>
564
<Icon name="trash" /> YES, Delete
565
</Button>
566
<Button onClick={=>@setState(confirm_delete:false)}>
567
Cancel
568
</Button>
569
</ButtonToolbar>
570
</div>
571
572
render_delete_button: ->
573
if not @props.is_expanded
574
return
575
if @state.confirm_delete
576
return @render_confirm_delete()
577
if @props.student.get('deleted')
578
<Button onClick={@undelete_student} style={float:'right'}>
579
<Icon name="trash-o" /> Undelete
580
</Button>
581
else
582
<Button onClick={=>@setState(confirm_delete:true)} style={float:'right'}>
583
<Icon name="trash" /> Delete...
584
</Button>
585
586
render_title_due: (assignment) ->
587
date = assignment.get('due_date')
588
if date
589
<span>(Due <TimeAgo date={date} />)</span>
590
591
render_title: (assignment) ->
592
<span>
593
<em>{misc.trunc_middle(assignment.get('path'), 50)}</em> {@render_title_due(assignment)}
594
</span>
595
596
render_assignments_info_rows: ->
597
store = @props.redux.getStore(@props.name)
598
for assignment in store.get_sorted_assignments()
599
grade = store.get_grade(assignment, @props.student)
600
info = store.student_assignment_info(@props.student, assignment)
601
<StudentAssignmentInfo
602
key={assignment.get('assignment_id')}
603
title={@render_title(assignment)}
604
name={@props.name}
605
student={@props.student}
606
assignment={assignment}
607
grade={grade}
608
info={info}
609
/>
610
611
render_assignments_info: ->
612
peer_grade = @props.redux.getStore(@props.name).any_assignment_uses_peer_grading()
613
header = <StudentAssignmentInfoHeader key='header' title="Assignment" peer_grade={peer_grade}/>
614
return [header, @render_assignments_info_rows()]
615
616
render_note: ->
617
<Row key='note' style={styles.note}>
618
<Col xs=2>
619
<Tip title="Notes about this student" tip="Record notes about this student here. These notes are only visible to you, not to the student. In particular, you might want to include an email address or other identifying information here, and notes about late assignments, excuses, etc.">
620
Notes
621
</Tip>
622
</Col>
623
<Col xs=10>
624
<MarkdownInput
625
persist_id = {@props.student.get('student_id') + "note"}
626
attach_to = {@props.name}
627
rows = 6
628
placeholder = 'Notes about student (not visible to student)'
629
default_value = {@props.student.get('note')}
630
on_save = {(value)=>@actions(@props.name).set_student_note(@props.student, value)}
631
/>
632
</Col>
633
</Row>
634
635
render_more_info: ->
636
# Info for each assignment about the student.
637
v = []
638
v.push <Row key='more'>
639
<Col md=12>
640
{@render_assignments_info()}
641
</Col>
642
</Row>
643
v.push(@render_note())
644
return v
645
646
render_basic_info: ->
647
<Row key='basic' style={backgroundColor:@props.background}>
648
<Col md=3>
649
<h6>
650
{@render_student()}
651
{@render_deleted()}
652
</h6>
653
</Col>
654
<Col md=2>
655
<h6 style={color:"#666"}>
656
{@render_student_email()}
657
</h6>
658
</Col>
659
<Col md=4 style={paddingTop:'10px'}>
660
{@render_last_active()}
661
</Col>
662
<Col md=3 style={paddingTop:'10px'}>
663
{@render_hosting()}
664
</Col>
665
</Row>
666
667
render_deleted: ->
668
if @props.student.get('deleted')
669
<b> (deleted)</b>
670
671
render_panel_header: ->
672
<div>
673
<Row>
674
<Col md=8>
675
{@render_project_access()}
676
</Col>
677
<Col md=4>
678
{@render_delete_button()}
679
</Col>
680
</Row>
681
{<Row>
682
<Col md=4>
683
{@render_edit_student_interface()}
684
</Col>
685
</Row> if @state.editing_student }
686
</div>
687
688
render_edit_student_interface: ->
689
<Well style={marginTop:'10px'}>
690
<Row>
691
<Col md=6>
692
First Name
693
<FormGroup>
694
<FormControl
695
type = 'text'
696
autoFocus = {true}
697
value = {@state.edited_first_name}
698
onClick = {(e) => e.stopPropagation(); e.preventDefault()}
699
onChange = {(e) => @setState(edited_first_name : e.target.value)}
700
onKeyDown = {@on_key_down}
701
/>
702
</FormGroup>
703
</Col>
704
<Col md=6>
705
Last Name
706
<FormGroup>
707
<FormControl
708
type = 'text'
709
value = {@state.edited_last_name}
710
onClick = {(e) => e.stopPropagation(); e.preventDefault()}
711
onChange = {(e) => @setState(edited_last_name : e.target.value)}
712
onKeyDown = {@on_key_down}
713
/>
714
</FormGroup>
715
</Col>
716
</Row>
717
<Row>
718
<Col md=12>
719
Email Address
720
<FormGroup>
721
<FormControl
722
type = 'text'
723
value = {@state.edited_email_address}
724
onClick = {(e) => e.stopPropagation(); e.preventDefault()}
725
onChange = {(e) => @setState(edited_email_address : e.target.value)}
726
onKeyDown = {@on_key_down}
727
/>
728
</FormGroup>
729
</Col>
730
</Row>
731
</Well>
732
733
render_more_panel: ->
734
<Row>
735
<Panel header={@render_panel_header()}>
736
{@render_more_info()}
737
</Panel>
738
</Row>
739
740
render: ->
741
<Row style={if @state.more then styles.selected_entry_style}>
742
<Col xs=12>
743
{@render_basic_info()}
744
{@render_more_panel() if @props.is_expanded}
745
</Col>
746
</Row>
747
748
immutable_to_list = (x, primary_key) ->
749
if not x?
750
return
751
v = []
752
x.map (val, key) ->
753
v.push(misc.merge(val.toJS(), {"#{primary_key}":key}))
754
return v
755
756
noncloud_emails = (v, s) ->
757
# Given a list v of user_search results, and a search string s,
758
# return entries for each email address not in v, in order.
759
{string_queries, email_queries} = misc.parse_user_search(s)
760
result_emails = misc.dict(([r.email_address, true] for r in v when r.email_address?))
761
return ({email_address:r} for r in email_queries when not result_emails[r]).sort (a,b)->
762
misc.cmp(a.email_address,b.email_address)
763