-
Notifications
You must be signed in to change notification settings - Fork 479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Progress tab: Initial work #19881
Progress tab: Initial work #19881
Changes from 13 commits
4f1a42c
8ca8100
e916e7f
52dee36
2e8bce6
11b477b
7b6d462
b27c0f0
2b6ef79
97eb31c
1b00355
960a742
03e48e4
85f0588
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React, { Component, PropTypes } from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed this is a js file not jsx, on purpose? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. either is fine. I think I usually use .js |
||
import _ from 'lodash'; | ||
|
||
// TODO: Can/should we share any logic with AssignmentSelector? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The answer might be that they're just different. This is a little confusing in that they use slightly difference sources of data for their list of scripts. AssignmentSelector uses a set of assignments that have been slightly processed, to be able to differentiate scripts and courses. In part this involves giving each one an "assignment id". That said, both have the same set of fields used for grouping I believe. |
||
|
||
/** | ||
* Group our assignments into categories for our dropdown | ||
*/ | ||
const groupedAssignments = assignments => ( | ||
_(assignments) | ||
.values() | ||
.orderBy(['category_priority', 'category', 'position', 'name']) | ||
.groupBy('category') | ||
.value() | ||
); | ||
|
||
export default class ScriptSelector extends Component { | ||
static propTypes = { | ||
validScripts: PropTypes.arrayOf(PropTypes.shape({ | ||
// This shape is similar to that used by AssignmentSelector, but in that | ||
// case they've been semi-processed and given assignIds to diferentiate | ||
// courses and scripts | ||
category: PropTypes.string.isRequired, | ||
category_priority: PropTypes.number.isRequired, | ||
id: PropTypes.number.isRequired, | ||
name: PropTypes.string.isRequired, | ||
position: PropTypes.number, | ||
})).isRequired, | ||
scriptId: PropTypes.string, | ||
onChange: PropTypes.func.isRequired, | ||
}; | ||
|
||
render() { | ||
const { validScripts, scriptId, onChange } = this.props; | ||
|
||
const grouped = groupedAssignments(validScripts); | ||
|
||
return ( | ||
<div> | ||
<select | ||
value={scriptId} | ||
onChange={event => onChange(event.target.value)} | ||
> | ||
<option key="default" value={''}/> | ||
{Object.keys(grouped).map((groupName, index) => ( | ||
<optgroup key={index} label={groupName}> | ||
{grouped[groupName].map((assignment) => ( | ||
(assignment !== undefined) && | ||
<option | ||
key={assignment.id} | ||
value={assignment.id} | ||
> | ||
{assignment.name} | ||
</option> | ||
))} | ||
</optgroup> | ||
))} | ||
</select> | ||
</div> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import React, { PropTypes, Component } from 'react'; | ||
import ScriptSelector from './ScriptSelector'; | ||
import { getLevelResult } from '@cdo/apps/code-studio/progressRedux'; | ||
import SectionScriptProgress from './SectionScriptProgress'; | ||
import _ from 'lodash'; | ||
|
||
/** | ||
* Given a particular section, this component owns figuring out which script to | ||
* show progress for (selected via a dropdown), and querying the server for | ||
* student progress. Child components then have the responsibility for displaying | ||
* that progress. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decided to separate the data gathering component from the UI component for the most part. That said, this component does still include the dropdown. |
||
export default class SectionProgress extends Component { | ||
static propTypes = { | ||
// The section we get directly from angular right now. This gives us a | ||
// different shape than some other places we use sections. For now, I'm just | ||
// going to document the parts of section that we use here | ||
section: PropTypes.shape({ | ||
id: PropTypes.number.isRequired, | ||
students: PropTypes.arrayOf(PropTypes.shape({ | ||
id: PropTypes.number.isRequired, | ||
name: PropTypes.string.isRequired, | ||
})).isRequired | ||
}).isRequired, | ||
validScripts: PropTypes.arrayOf(PropTypes.shape({ | ||
category: PropTypes.string.isRequired, | ||
category_priority: PropTypes.number.isRequired, | ||
id: PropTypes.number.isRequired, | ||
name: PropTypes.string.isRequired, | ||
position: PropTypes.number, | ||
})).isRequired, | ||
}; | ||
|
||
state = { | ||
// TODO: default to what is assigned to section, or at least come up with | ||
// some heuristic so that we have a default | ||
scriptId: "112", | ||
scriptData: null, | ||
studentLevelProgress: null, | ||
} | ||
|
||
componentDidMount() { | ||
this.loadScript(this.state.scriptId); | ||
} | ||
|
||
onChangeScript = scriptId => { | ||
this.setState({ | ||
scriptId, | ||
scriptData: null, | ||
studentLevelProgress: null, | ||
}); | ||
this.loadScript(scriptId); | ||
} | ||
|
||
/** | ||
* Query the server for script data (info about the levels in the script) and | ||
* also for user progress on that script | ||
*/ | ||
loadScript(scriptId) { | ||
$.getJSON(`/dashboardapi/script_structure/${scriptId}`, scriptData => { | ||
this.setState({ | ||
scriptData | ||
}); | ||
}); | ||
|
||
$.getJSON(`/dashboardapi/section_level_progress/${this.props.section.id}?script_id=${scriptId}`, dataByStudent => { | ||
// dataByStudent is an object where the keys are student.id and the values | ||
// are a map of levelId to status | ||
let studentLevelProgress = {}; | ||
Object.keys(dataByStudent).forEach(studentId => { | ||
studentLevelProgress[studentId] = _.mapValues(dataByStudent[studentId], getLevelResult); | ||
}); | ||
|
||
this.setState({ | ||
studentLevelProgress: studentLevelProgress | ||
}); | ||
}); | ||
} | ||
|
||
render() { | ||
const { section, validScripts } = this.props; | ||
const { scriptId, scriptData, studentLevelProgress } = this.state; | ||
|
||
let levelDataInitialized = scriptData && studentLevelProgress; | ||
|
||
return ( | ||
<div> | ||
<ScriptSelector | ||
validScripts={validScripts} | ||
scriptId={scriptId} | ||
onChange={this.onChangeScript} | ||
/> | ||
{!levelDataInitialized && <div>Loading...</div>} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. todo: this is the spinner we normally use |
||
{levelDataInitialized && | ||
<SectionScriptProgress | ||
section={section} | ||
scriptData={scriptData} | ||
studentLevelProgress={studentLevelProgress} | ||
/> | ||
} | ||
</div> | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be more specific about the experiment here. There are a few going at a time in teacher dashboard.