Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
| Download
Views: 39598
1
##############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016 -- 2017, 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#
19
##############################################################################
20
# Upgrading quotas for all student projects
21
##############################################################################
22
23
underscore = require('underscore')
24
25
misc = require('smc-util/misc')
26
27
schema = require('smc-util/schema')
28
29
{React, rclass, rtypes, ReactDOM} = require('../smc-react')
30
31
{Icon, Loading, NoUpgrades, Tip, UPGRADE_ERROR_STYLE} = require('../r_misc')
32
33
{UpgradeRestartWarning} = require('../upgrade_restart_warning')
34
35
{Alert, Button, ButtonToolbar, Checkbox,
36
FormGroup, FormControl, Panel, Row, Col} = require('react-bootstrap')
37
38
39
exports.StudentProjectUpgrades = rclass
40
propTypes: ->
41
name : rtypes.string.isRequired
42
redux : rtypes.object.isRequired
43
upgrade_goal : rtypes.immutable.Map
44
45
getInitialState: ->
46
upgrade_quotas : false # true if display the quota upgrade panel
47
upgrades : undefined
48
upgrade_plan : undefined
49
50
upgrade_goal: ->
51
goal = {}
52
for quota, val of @state.upgrades
53
val = misc.parse_number_input(val, round_number=false)
54
display_factor = schema.PROJECT_UPGRADES.params[quota].display_factor
55
goal[quota] = val / display_factor
56
return goal
57
58
save_upgrade_quotas: ->
59
@setState(upgrade_quotas: false)
60
a = @actions(@props.name)
61
upgrade_goal = @upgrade_goal()
62
a.set_upgrade_goal(upgrade_goal)
63
a.upgrade_all_student_projects(upgrade_goal)
64
65
render_upgrade_heading: (num_projects) ->
66
<Row key="heading">
67
<Col md=5>
68
<b style={fontSize:'11pt'}>Quota</b>
69
</Col>
70
{# <Col md=2><b style={fontSize:'11pt'}>Current upgrades</b></Col> }
71
<Col md=7>
72
<b style={fontSize:'11pt'}>Distribute upgrades to your {num_projects} student {misc.plural(num_projects, 'project')} to get quota to the amount in this column (amounts may be decimals)</b>
73
</Col>
74
</Row>
75
76
is_upgrade_input_valid: (val, limit) ->
77
parsed_val = misc.parse_number_input(val, round_number=false)
78
if not parsed_val? or parsed_val > Math.max(0, limit) # val=0 is always valid
79
return false
80
else
81
return true
82
83
render_upgrade_row_input: (quota, input_type, current, yours, num_projects, limit) ->
84
ref = "upgrade_#{quota}"
85
if input_type == 'number'
86
val = @state.upgrades[quota] ? (yours / num_projects)
87
if not @state.upgrades[quota]?
88
if val is 0 and yours isnt 0
89
val = yours / num_projects
90
91
if not @is_upgrade_input_valid(val, limit)
92
bs_style = 'error'
93
@_upgrade_is_invalid = true
94
if misc.parse_number_input(val)?
95
label = <div style=UPGRADE_ERROR_STYLE>Reduce the above: you do not have enough upgrades</div>
96
else
97
label = <div style=UPGRADE_ERROR_STYLE>Please enter a number</div>
98
else
99
label = <span></span>
100
<FormGroup>
101
<FormControl
102
type = 'text'
103
ref = {ref}
104
value = {val}
105
bsStyle = {bs_style}
106
onChange = {=>u=@state.upgrades; u[quota] = ReactDOM.findDOMNode(@refs[ref]).value; @setState(upgrades:u); @update_plan()}
107
/>
108
{label}
109
</FormGroup>
110
else if input_type == 'checkbox'
111
val = @state.upgrades[quota] ? (if yours > 0 then 1 else 0)
112
is_valid = @is_upgrade_input_valid(val, limit)
113
if not is_valid
114
@_upgrade_is_invalid = true
115
label = <div style=UPGRADE_ERROR_STYLE>Uncheck this: you do not have enough upgrades</div>
116
else
117
label = if val == 0 then 'Enable' else 'Enabled'
118
<form>
119
<Checkbox
120
ref = {ref}
121
checked = {val > 0}
122
onChange = {(e)=>u=@state.upgrades; u[quota] = (if e.target.checked then 1 else 0); @setState(upgrades:u); @update_plan()}
123
/>
124
{label}
125
</form>
126
else
127
console.warn('Invalid input type in render_upgrade_row_input: ', input_type)
128
return
129
130
render_upgrade_row: (quota, available, current, yours, num_projects) ->
131
# quota -- name of the quota
132
# available -- How much of this quota the user has available to use on the student projects.
133
# This is the total amount the user purchased minus the amount allocated to other
134
# projects that aren't projects in this course.
135
# current -- Sum of total upgrades currently allocated by anybody to the course projects
136
# yours -- How much of this quota this user has allocated to this quota total.
137
# num_projects -- How many student projects there are.
138
{display, desc, display_factor, display_unit, input_type} = schema.PROJECT_UPGRADES.params[quota]
139
140
yours *= display_factor
141
current *= display_factor
142
143
x = @state.upgrades[quota]
144
input = if x == '' then 0 else misc.parse_number_input(x) ? (yours/num_projects) # currently typed in
145
if input_type == 'checkbox'
146
input = if input > 0 then 1 else 0
147
148
##console.log(quota, "remaining = (#{available} - #{input}/#{display_factor}*#{num_projects}) * #{display_factor}")
149
150
remaining = misc.round2( (available - input/display_factor*num_projects) * display_factor )
151
limit = (available / num_projects) * display_factor
152
153
cur = misc.round2(current / num_projects)
154
if input_type == 'checkbox'
155
if cur > 0 and cur < 1
156
cur = "#{misc.round2(cur*100)}%"
157
else if cur == 0
158
cur = 'none'
159
else
160
cur = 'all'
161
162
<Row key={quota}>
163
<Col md=5>
164
<Tip title={display} tip={desc}>
165
<strong>{display}</strong>
166
</Tip>
167
<span style={marginLeft:'1ex'}>({remaining} {misc.plural(remaining, display_unit)} remaining)</span>
168
</Col>
169
{# <Col md=2 style={marginTop: '8px'}>{cur}</Col> }
170
<Col md=5>
171
{@render_upgrade_row_input(quota, input_type, current, yours, num_projects, limit)}
172
</Col>
173
<Col md=2 style={marginTop: '8px'}>
174
&times; {num_projects}
175
</Col>
176
</Row>
177
178
render_upgrade_rows: (purchased_upgrades, applied_upgrades, num_projects, total_upgrades, your_upgrades) ->
179
# purchased_upgrades - how much of each quota this user has purchased
180
# applied_upgrades - how much of each quota user has already applied to projects total
181
# num_projects - number of student projects
182
# total_upgrades - the total amount of each quota that has been applied (by anybody) to these student projects
183
# your_upgrades - total amount of each quota that this user has applied to these student projects
184
@_upgrade_is_invalid = false # will get set to true by render_upgrade_row if invalid.
185
for quota in schema.PROJECT_UPGRADES.field_order
186
total = purchased_upgrades[quota]
187
yours = your_upgrades[quota] ? 0
188
available = total - (applied_upgrades[quota] ? 0) + yours
189
current = total_upgrades[quota] ? 0
190
@render_upgrade_row(quota, available, current, yours, num_projects)
191
192
render_upgrade_quotas: ->
193
redux = @props.redux
194
195
# Get available upgrades that instructor has to apply
196
account_store = redux.getStore('account')
197
if not account_store?
198
return <Loading/>
199
200
purchased_upgrades = account_store.get_total_upgrades()
201
if misc.is_zero_map(purchased_upgrades)
202
# user has no upgrades on their account
203
return <NoUpgrades cancel={=>@setState(upgrade_quotas:false)} />
204
205
course_store = redux.getStore(@props.name)
206
if not course_store?
207
return <Loading/>
208
209
# Get non-deleted student projects
210
project_ids = course_store.get_student_project_ids()
211
if not project_ids
212
return <Loading/>
213
num_projects = project_ids.length
214
if not num_projects
215
return <span>There are no student projects yet.<br/><br/>{@render_upgrade_submit_buttons()}</span>
216
217
# Get remaining upgrades
218
projects_store = redux.getStore('projects')
219
if not projects_store?
220
return <Loading/>
221
applied_upgrades = projects_store.get_total_upgrades_you_have_applied()
222
223
# Sum total amount of each quota that we have applied to all student projects
224
total_upgrades = {} # all upgrades by anybody
225
your_upgrades = {} # just by you
226
account_id = account_store.get_account_id()
227
for project_id in project_ids
228
your_upgrades = misc.map_sum(your_upgrades, projects_store.get_upgrades_you_applied_to_project(project_id))
229
total_upgrades = misc.map_sum(total_upgrades, projects_store.get_total_project_upgrades(project_id))
230
231
<Alert bsStyle='warning'>
232
<h3><Icon name='arrow-circle-up' /> Adjust your contributions to the student project quotas</h3>
233
<hr/>
234
{@render_upgrade_heading(num_projects)}
235
<hr/>
236
{@render_upgrade_rows(purchased_upgrades, applied_upgrades, num_projects, total_upgrades, your_upgrades)}
237
<UpgradeRestartWarning />
238
{@render_upgrade_submit_buttons()}
239
<div style={marginTop:'15px', color: '#333'}>
240
{@render_upgrade_plan()}
241
</div>
242
{@render_admin_upgrade() if redux.getStore('account').get('groups')?.contains('admin')}
243
</Alert>
244
245
save_admin_upgrade: (e) ->
246
e.preventDefault()
247
s = ReactDOM.findDOMNode(@refs.admin_input).value
248
quotas = JSON.parse(s)
249
console.log("admin upgrade '#{s}' -->", quotas)
250
@actions(@props.name).admin_upgrade_all_student_projects(quotas)
251
return false
252
253
render_admin_upgrade: ->
254
<div>
255
<br/>
256
<hr/>
257
<h3>Admin Upgrade</h3>
258
Enter a Javascript-parseable object and hit enter (see the Javascript console for feedback):
259
<form onSubmit={@save_admin_upgrade}>
260
<FormGroup>
261
<FormControl
262
ref = 'admin_input'
263
type = 'text'
264
placeholder = {JSON.stringify(schema.DEFAULT_QUOTAS)}
265
/>
266
</FormGroup>
267
</form>
268
</div>
269
270
render_upgrade_submit_buttons: ->
271
<ButtonToolbar>
272
<Button
273
bsStyle = 'primary'
274
onClick = {@save_upgrade_quotas}
275
disabled = {not @state.upgrade_plan? or misc.len(@state.upgrade_plan) == 0}
276
>
277
<Icon name='arrow-circle-up' /> Apply changes
278
</Button>
279
<Button onClick={=>@setState(upgrade_quotas:false)}>
280
Cancel
281
</Button>
282
</ButtonToolbar>
283
284
# call this function to switch state from not viewing the upgrader to viewing the upgrader.
285
adjust_quotas: ->
286
upgrades = @props.upgrade_goal?.toJS() ? {}
287
upgrade_plan = @props.redux.getStore(@props.name).get_upgrade_plan(upgrades)
288
for quota, val of upgrades
289
upgrades[quota] = val * schema.PROJECT_UPGRADES.params[quota].display_factor
290
@setState
291
upgrade_quotas : true
292
upgrades : upgrades
293
upgrade_plan : upgrade_plan
294
295
update_plan: ->
296
plan = @props.redux.getStore(@props.name).get_upgrade_plan(@upgrade_goal())
297
@setState(upgrade_plan: plan)
298
299
render_upgrade_plan: ->
300
if not @state.upgrade_plan?
301
return
302
n = misc.len(@state.upgrade_plan)
303
if n == 0
304
<span>
305
The upgrades requested above are already applied to all student projects.
306
</span>
307
else
308
<span>
309
{n} of the student projects will have their upgrades changed when you click the Apply button.
310
</span>
311
312
render_upgrade_quotas_button: ->
313
<Button bsStyle='primary' onClick={@adjust_quotas}>
314
<Icon name='arrow-circle-up' /> Adjust quotas...
315
</Button>
316
317
render: ->
318
<Panel header={<h4><Icon name='dashboard' /> Upgrade all student projects (you pay)</h4>}>
319
{if @state.upgrade_quotas then @render_upgrade_quotas() else @render_upgrade_quotas_button()}
320
<hr/>
321
<div style={color:"#666"}>
322
<p>Add or remove upgrades to student projects associated to this course, augmenting what is provided for free and what students may have purchased.</p>
323
</div>
324
</Panel>
325
326