Skip to content
Snippets Groups Projects
Commit 4e69f7d7 authored by Thomas Roos's avatar Thomas Roos
Browse files

Move exam state up from pages to central app level

parent 748f8890
No related branches found
No related tags found
No related merge requests found
import React from 'react';
import { Link } from 'react-router-dom';
import * as api from '../api.jsx'
const BurgerButton = (props) => (
<button className={"button navbar-burger" + (props.foldOut ? " is-active" : "")}
onClick={props.burgerClick}>
......@@ -12,8 +14,8 @@ const BurgerButton = (props) => (
const ExamDropdown = (props) => (
<div className="navbar-item has-dropdown is-hoverable">
<Link className="navbar-link" to={'/exams/' + (props.exam ? props.exam.id : "")}>
{props.exam ? <i>{props.exam.name}</i> : "Add exam"}
<Link className="navbar-link" to={'/exams/' + (props.exam.id ? props.exam.id : "")}>
{props.exam.id ? <i>{props.exam.name}</i> : "Add exam"}
</Link>
<div className="navbar-dropdown">
{props.list.map((exam) => (
......@@ -33,7 +35,22 @@ const ExamDropdown = (props) => (
class NavBar extends React.Component {
state = {
foldOut: false
foldOut: false,
examList: []
}
componentDidMount = () => {
this.updateExamList();
}
updateExamList = () => {
api.get('exams')
.then(exams => {
this.setState({
examList: exams
})
if (this.props.exam.id == null && exams.length) this.props.updateExam(exams[exams.length - 1].id)
})
}
burgerClick = () => {
......@@ -44,7 +61,7 @@ class NavBar extends React.Component {
render() {
const examStyle = this.props.exam && this.props.exam.submissions ? {} : { pointerEvents: 'none', opacity: .65 }
const examStyle = this.props.exam.submissions.length ? {} : { pointerEvents: 'none', opacity: .65 }
return (
<nav className="navbar" role="navigation" aria-label="dropdown navigation">
......@@ -65,8 +82,8 @@ class NavBar extends React.Component {
<div className={"navbar-menu" + (this.state.foldOut ? " is-active" : "")} onClick={this.burgerClick}>
<div className="navbar-start">
{this.props.exam ?
<ExamDropdown exam={this.props.exam} list={this.props.list} />
{this.state.examList.length ?
<ExamDropdown exam={this.props.exam} list={this.state.examList} />
:
<Link className="navbar-item" to='/exams'>Add exam</Link>
}
......
......@@ -51,65 +51,63 @@ const Fail = Loadable({
class App extends React.Component {
menu = React.createRef();
state = {
examIndex: null,
examList: []
exam: {
id: null,
name: "",
submissions: [],
problems: [],
yaml: ""
}
}
componentDidMount() {
this.updateExamList();
updateExam = (examID) => {
api.get('exams/' + examID)
.then(ex => this.setState({
exam: ex
}))
}
updateExamList = (callback, onlyList) => {
api.get('exams')
.then(exams => {
if (exams.length) {
if (onlyList) {
this.setState({
examList: exams
})
} else {
this.setState({
examIndex: exams.length - 1,
examList: exams
}, callback)
updateSubmission = (index, sub) => {
if (index != undefined) {
if (sub) {
let newList = this.state.exam.submissions;
newList[index] = sub;
this.setState({
exam: {
...this.state.exam,
submissions: newList
}
}
})
.catch(resp => {
alert('failed to get exams (see javascript console for details)')
console.error('failed to get exams:', resp)
})
}
changeExam = (examID) => {
const index = this.state.examList.findIndex(exam => exam.id === examID)
if (index === -1) {
alert('Wrong exam url entered');
return;
})
}
} else {
this.setState({
examIndex: index
})
api.get('submissions/' + this.state.exam.id)
.then(subs => this.setState({
exam: {
...this.state.exam,
submissions: subs
}
}))
}
}
render() {
const exam = this.state.examIndex === null ? null : this.state.examList[this.state.examIndex];
const exam = this.state.exam
return (
<Router>
<div>
<NavBar exam={exam} list={this.state.examList} changeExam={this.changeExam} />
<NavBar exam={exam} updateExam={this.updateExam} ref={this.menu} />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/exams/:examID" render={({match}) =>
<Exam exam={exam} urlID={match.params.examID} changeExam={this.changeExam} updateList={this.updateExamList}/> }/>
<Exam exam={exam} urlID={match.params.examID} changeExam={this.changeExam} updateSubmission={this.updateSubmission}/> }/>
<Route path="/exams" render={({history}) =>
<AddExam updateExamList={this.updateExamList} changeURL={history.push} /> }/>
<AddExam updateExamList={() => this.menu.current.updateExamList()} changeURL={history.push} /> }/>
<Route path="/students" render={() =>
<Students exam={exam} /> }/>
<Students exam={exam} updateSubmission={this.updateSubmission}/> }/>
<Route path="/grade" render={() => (
exam && exam.submissions ? <Grade exam={exam}/> : <Fail message="No exams uploaded. Please do not bookmark URLs" />
)} />
......
......@@ -17,7 +17,8 @@ class Exams extends React.Component {
data.append('yaml', accepted[0])
api.post('exams', data)
.then(exam => {
this.props.updateExamList(() => this.props.changeURL('/exams/' + exam.id));
this.props.updateExamList()
this.props.changeURL('/exams/' + exam.id);
})
.catch(resp => {
alert('failed to upload yaml (see javascript console for details)')
......
......@@ -34,22 +34,8 @@ class Exams extends React.Component {
pdfs: []
};
loadExam = (id) => {
if (this.props.exam.id !== parseInt(id)) {
console.log('Changing exam id to ' + id)
this.props.changeExam(parseInt(id));
}
api.get('exams/' + id)
.then(exam => {
this.setState({
yaml: exam.yaml
})
})
}
putYaml = () => {
api.patch('exams/' + this.props.urlID, { yaml: this.state.yaml })
api.patch('exams/' + this.props.exam.id, { yaml: this.state.yaml })
.then(() => alert('thank you for the update; it was delicious'))
.catch(resp => {
alert('failed to update the YAML (see javascript console)')
......@@ -64,13 +50,13 @@ class Exams extends React.Component {
}
updatePDFs = () => {
api.get('pdfs/' + this.props.urlID)
api.get('pdfs/' + this.props.exam.id)
.then(pdfs => {
if (JSON.stringify(pdfs) != JSON.stringify(this.state.pdfs)) {
this.props.updateList(null, true)
this.setState({
pdfs: pdfs
})
this.props.updateSubmission()
}
})
}
......@@ -83,7 +69,7 @@ class Exams extends React.Component {
accepted.map(file => {
const data = new FormData()
data.append('pdf', file)
api.post('pdfs/' + this.props.urlID, data)
api.post('pdfs/' + this.props.exam.id, data)
.then(() => {
this.updatePDFs();
})
......@@ -95,14 +81,22 @@ class Exams extends React.Component {
}
componentDidMount = () => {
this.loadExam(this.props.urlID);
this.pdfUpdater = setInterval(this.updatePDFs, 1000)
if (this.props.exam.id) this.updatePDFs()
}
componentWillReceiveProps = (newProps) => {
if (newProps.urlID !== this.props.urlID) {
this.loadExam(newProps.urlID)
static getDerivedStateFromProps = (newProps, prevState) => {
if (newProps.exam.id != prevState.examID) {
return {
yaml: newProps.exam.yaml,
pdfs: [],
examID: newProps.exam.id
}
}
return null
}
componentDidUpdate = (prevProps) => {
if (prevProps.exam.id != this.props.exam.id) this.updatePDFs()
}
componentWillUnmount = () => {
......
......@@ -156,7 +156,7 @@ class Grade extends React.Component {
<EditPanel problem={this.state.problem} feedbackID={this.state.editFeedback} toggleEdit={this.toggleEdit}/>
:
<FeedbackPanel problem={this.state.problem} toggleEdit={this.toggleEdit}
editFeedback={this.editFeedback}/>
editFeedback={this.editFeedback} />
}
</div>
......
......@@ -15,15 +15,9 @@ class CheckStudents extends React.Component {
state = {
editActive: false,
editStud: null,
submission: {
id: 0,
input: 0,
index: 0,
student: null,
validated: false,
imagePath: null,
list: []
}
input: "",
index: 0,
examID: null
};
componentWillUnmount = () => {
......@@ -45,88 +39,73 @@ class CheckStudents extends React.Component {
event.preventDefault();
this.prevUnchecked();
});
}
if (this.props.exam) this.loadSubmissions();
static getDerivedStateFromProps = (newProps, prevState) => {
if (newProps.exam.id != prevState.examID && newProps.exam.submissions.length) {
return {
input: newProps.exam.submissions[0].id,
index: 0,
examID: newProps.examID
}
}
return null
}
prev = () => {
const newIndex = this.state.submission.index - 1;
const newIndex = this.state.index - 1;
if (newIndex >= 0 && newIndex < this.state.submission.list.length) {
if (newIndex >= 0 && newIndex < this.props.exam.submissions.length) {
this.setState({
submission: {
...this.state.submission,
input: this.state.submission.list[newIndex].id
}
input: this.props.exam.submissions[newIndex].id
}, this.setSubmission)
}
}
next = () => {
const newIndex = this.state.submission.index + 1;
const newIndex = this.state.index + 1;
if (newIndex >= 0 && newIndex < this.state.submission.list.length) {
if (newIndex >= 0 && newIndex < this.props.exam.submissions.length) {
this.setState({
submission: {
...this.state.submission,
input: this.state.submission.list[newIndex].id
}
input: this.props.exam.submissions[newIndex].id
}, this.setSubmission)
}
}
prevUnchecked = () => {
const unchecked = this.state.submission.list.filter(sub => sub.validated === false).map(sub => sub.id);
const newInput = getClosest.lowerNumber(this.state.submission.id - 1, unchecked);
const unchecked = this.props.exam.submissions.filter(sub => sub.validated === false).map(sub => sub.id);
const newInput = getClosest.lowerNumber(this.props.exam.submissions[this.state.index].id - 1, unchecked);
if (typeof newInput !== 'undefined') {
this.setState({
submission: {
...this.state.submission,
input: unchecked[newInput]
}
input: unchecked[newInput]
}, this.setSubmission)
}
}
nextUnchecked = () => {
const unchecked = this.state.submission.list.filter(sub => sub.validated === false).map(sub => sub.id);
const newInput = getClosest.greaterNumber(this.state.submission.id + 1, unchecked);
const unchecked = this.props.exam.submissions.filter(sub => sub.validated === false).map(sub => sub.id);
const newInput = getClosest.greaterNumber(this.props.exam.submissions[this.state.index].id + 1, unchecked);
if (typeof newInput !== 'undefined') {
this.setState({
submission: {
...this.state.submission,
input: unchecked[newInput]
}
input: unchecked[newInput]
}, this.setSubmission)
}
}
setSubmission = () => {
const input = parseInt(this.state.submission.input);
const i = this.state.submission.list.findIndex(sub => sub.id === input);
const sub = this.state.submission.list[i];
const input = parseInt(this.state.input);
const i = this.props.exam.submissions.findIndex(sub => sub.id === input);
if (i >= 0) {
this.setState({
submission: {
...this.state.submission,
id: input,
student: sub.student,
validated: sub.validated,
index: i,
imagePath: 'api/images/signature/' + this.props.exam.id + '/' + input
}
}, this.getSubmission)
index: i,
}, /* UPDATE SUBMISSION IN TOP COMPONENT */)
} else {
this.setState({
submission: {
...this.state.submission,
input: this.state.submission.id
}
input: this.props.submissions[this.state.index].id
})
alert('Could not find that submission number :(\nSorry!');
}
......@@ -136,91 +115,24 @@ class CheckStudents extends React.Component {
const patt = new RegExp(/^([1-9]\d*|0)?$/);
if (patt.test(event.target.value)) {
this.setState({
submission: {
...this.state.submission,
input: event.target.value
}
})
this.setState({ input: event.target.value })
}
}
getSubmission = () => {
api.get('submissions/' + this.props.exam.id + '/' + this.state.submission.id)
.then(sub => {
let newList = this.state.submission.list;
const index = newList.findIndex(localSub => localSub.id === sub.id)
newList[index] = sub;
this.setState({
submission: {
...this.state.submission,
student: sub.student,
validated: sub.validated,
list: newList
}
})
})
.catch(err => {
alert('failed to get submission (see javascript console for details)')
console.error('failed to get submission:', err)
throw err
})
}
loadSubmissions = () => {
api.get('submissions/' + this.props.exam.id)
.then(subs => {
if (subs.length) {
this.setState({
submission: {
...this.state.submission,
id: subs[0].id,
input: subs[0].id,
student: subs[0].student,
validated: subs[0].validated,
imagePath: 'api/images/signature/' + this.props.exam.id + '/' + subs[0].id,
list: subs
}
})
}
})
.catch(err => {
alert('failed to get submissions (see javascript console for details)')
console.error('failed to get submissions:', err)
throw err
})
}
matchStudent = (stud) => {
if(!this.state.submission.list.length) return;
let newList = this.state.submission.list;
const index = this.state.submission.index;
this.setState({
submission: {
...this.state.submission,
student: stud,
validated: true
}
}, this.nextUnchecked)
if (!this.props.exam.submissions.length) return;
api.put('submissions/' + this.props.exam.id + '/' + this.state.submission.id, { studentID: stud.id })
api.put('submissions/' + this.props.exam.id + '/' + this.props.exam.submissions[this.state.index].id, { studentID: stud.id })
.then(sub => {
newList[index] = sub;
this.setState({
submission: {
...this.state.submission,
list: newList
}
})
this.props.updateSubmission(this.state.index, sub)
this.nextUnchecked()
})
.catch(err => {
alert('failed to put submission (see javascript console for details)')
console.error('failed to put submission:', err)
throw err
})
})
}
toggleEdit = (student) => {
......@@ -242,7 +154,9 @@ class CheckStudents extends React.Component {
width: '5em'
};
const maxSubmission = Math.max(...this.state.submission.list.map(o => o.id));
const maxSubmission = Math.max(...this.props.exam.submissions.map(o => o.id));
const subm = this.props.exam.submissions[this.state.index];
return (
<div>
......@@ -259,11 +173,11 @@ class CheckStudents extends React.Component {
<EditPanel toggleEdit={this.toggleEdit} editStud={this.state.editStud} />
:
<SearchPanel matchStudent={this.matchStudent} toggleEdit={this.toggleEdit}
student={this.state.submission.student} validated={this.state.submission.validated} />
student={subm && subm.student} validated={subm && subm.validated} />
}
</div>
{this.state.submission.list.length ?
{this.props.exam.submissions.length ?
<div className="column">
<div className="level">
<div className="level-item">
......@@ -271,17 +185,17 @@ class CheckStudents extends React.Component {
<div className="control">
<button type="submit" className="button is-info is-rounded is-hidden-mobile"
onClick={this.prevUnchecked}>unchecked</button>
<button type="submit" className={"button" + (this.state.submission.validated ? " is-success" : " is-link")}
<button type="submit" className={"button" + (subm.validated ? " is-success" : " is-link")}
onClick={this.prev}>Previous</button>
</div>
<div className="control">
<input className={"input is-rounded has-text-centered" + (this.state.submission.validated ? " is-success" : " is-link")}
value={this.state.submission.input} type="text"
<input className={"input is-rounded has-text-centered" + (subm.validated ? " is-success" : " is-link")}
value={this.state.input} type="text"
onChange={this.setSubInput} onSubmit={this.setSubmission}
onBlur={this.setSubmission} maxLength="4" size="6" style={inputStyle} />
</div>
<div className="control">
<button type="submit" className={"button" + (this.state.submission.validated ? " is-success" : " is-link")}
<button type="submit" className={"button" + (subm.validated ? " is-success" : " is-link")}
onClick={this.next}>Next</button>
<button type="submit" className="button is-info is-rounded is-hidden-mobile"
onClick={this.nextUnchecked}>unchecked</button>
......@@ -290,14 +204,14 @@ class CheckStudents extends React.Component {
</div>
</div>
<ProgressBar submissions={this.state.submission.list}/>
<ProgressBar submissions={this.props.exam.submissions} />
<p className="box">
<img src={this.state.submission.imagePath} alt="" />
<img src={'api/images/signature/' + this.props.exam.id + '/' + subm.id} alt="" />
</p>
</div>
: null }
: null}
</div>
</div>
</section>
......
......@@ -7,7 +7,15 @@ import FeedbackBlock from './FeedbackBlock.jsx';
class FeedbackPanel extends React.Component {
state = {
feedback: []
feedback: [],
remark: null,
remarkActive: false
}
addRemark = () => {
this.setState({
remarkActive: true
})
}
componentDidMount = () => {
......@@ -33,11 +41,11 @@ class FeedbackPanel extends React.Component {
}
shouldComponentUpdate = (nextProps, nextState) => {
if (this.props.problem !== nextProps.problem || this.state.feedback != nextState.feedback) {
if (this.props.problem !== nextProps.problem || this.state.feedback != nextState.feedback || this.state.rem) {
return true;
} else {
console.log('halting re-render')
return false;
return true;
}
}
......@@ -51,12 +59,24 @@ class FeedbackPanel extends React.Component {
{this.state.feedback.map((feedback, i) =>
<FeedbackBlock key={i} index={i} feedback={feedback} checked={false} onClick={this.props.editFeedback} />
)}
<div className="panel-block is-hidden-mobile">
{this.state.remarkActive ?
<div className="panel-block">
<textarea className="textarea" rows="2" placeholder="remark" />
</div>
: null
}
<div className="panel-block">
<button className="button is-link is-outlined is-fullwidth" onClick={this.props.toggleEdit}>
<span className="icon is-small">
<i className="fa fa-plus"></i>
</span>
<span>add option</span>
<span>option</span>
</button>
<button className="button is-link is-outlined is-fullwidth" onClick={this.addRemark}>
<span className="icon is-small">
<i className="fa fa-plus"></i>
</span>
<span>remark</span>
</button>
</div>
</nav>
......
......@@ -42,7 +42,47 @@ class ExamConfig(Resource):
return {
'id': exam_id,
'name': exam.name,
'submissions': exam.submissions.count(),
'submissions':
[
{
'id': sub.copy_number,
'student':
{
'id': sub.student.id,
'firstName': sub.student.first_name,
'lastName': sub.student.last_name,
'email': sub.student.email
} if sub.student else None,
'validated': sub.signature_validated,
'solutions':
[
{
'problem': sol.problem.id,
'graded_by': sol.graded_by,
'graded_at': sol.graded_at.isoformat() if sol.graded_at else None,
'feedback': [
fb.id for fb in sol.feedback
],
'remarks': sol.remarks
} for sol in sub.solutions.order_by(lambda s: s.problem.id)
]
} for sub in exam.submissions.order_by(lambda s: s.copy_number)
],
'problems': [
{
'id': prob.id,
'name': prob.name,
'feedback': [
{
'id': fb.id,
'name': fb.text,
'description': fb.description,
'score': fb.score,
'used': fb.solutions.count()
} for fb in prob.feedback_options.order_by(lambda f: f.id)
]
} for prob in exam.problems.order_by(lambda p: p.id)
],
'yaml': yml
}
......
......@@ -31,35 +31,57 @@ class Submissions(Resource):
exam = Exam[exam_id]
if submission_id is not None:
s = Submission.get(exam=exam, copy_number=submission_id)
if not s:
sub = Submission.get(exam=exam, copy_number=submission_id)
if not sub:
raise orm.core.ObjectNotFound(Submission)
return {
'id': s.copy_number,
'id': sub.copy_number,
'student':
{
'id': s.student.id,
'firstName': s.student.first_name,
'lastName': s.student.last_name,
'email': s.student.email
} if s.student else None,
'validated': s.signature_validated,
'id': sub.student.id,
'firstName': sub.student.first_name,
'lastName': sub.student.last_name,
'email': sub.student.email
} if sub.student else None,
'validated': sub.signature_validated,
'solutions':
[
{
'problem': sol.problem.id,
'graded_by': sol.graded_by,
'graded_at': sol.graded_at.isoformat() if sol.graded_at else None,
'feedback': [
fb.id for fb in sol.feedback
],
'remarks': sol.remarks
} for sol in sub.solutions.order_by(lambda s: s.problem.id)
]
}
return [
{
'id': s.copy_number,
'id': sub.copy_number,
'student':
{
'id': s.student.id,
'firstName': s.student.first_name,
'lastName': s.student.last_name,
'email': s.student.email
} if s.student else None,
'validated': s.signature_validated,
}
for s in Submission.select(lambda s: s.exam == exam)
.order_by(Submission.copy_number)
'id': sub.student.id,
'firstName': sub.student.first_name,
'lastName': sub.student.last_name,
'email': sub.student.email
} if sub.student else None,
'validated': sub.signature_validated,
'solutions':
[
{
'problem': sol.problem.id,
'graded_by': sol.graded_by,
'graded_at': sol.graded_at.isoformat() if sol.graded_at else None,
'feedback': [
fb.id for fb in sol.feedback
],
'remarks': sol.remarks
} for sol in sub.solutions.order_by(lambda s: s.problem.id)
]
} for sub in Submission.select(lambda s: s.exam == exam).order_by(lambda s: s.copy_number)
]
put_parser = reqparse.RequestParser()
......@@ -91,8 +113,8 @@ class Submissions(Resource):
args = self.put_parser.parse_args()
exam = Exam[exam_id]
submission = Submission.get(exam=exam, copy_number=submission_id)
if not submission:
sub = Submission.get(exam=exam, copy_number=submission_id)
if not sub:
raise orm.core.ObjectNotFound(Submission)
student = Student.get(id=args.studentID)
......@@ -100,10 +122,28 @@ class Submissions(Resource):
msg = f'Student {args.studentID} does not exist'
return dict(status=404, message=msg), 404
submission.student = student
submission.signature_validated = True
return {
'id': submission.copy_number,
'studentID': submission.student.id,
'validated': submission.signature_validated,
}
sub.student = student
sub.signature_validated = True
return {
'id': sub.copy_number,
'student':
{
'id': sub.student.id,
'firstName': sub.student.first_name,
'lastName': sub.student.last_name,
'email': sub.student.email
} if sub.student else None,
'validated': sub.signature_validated,
'solutions':
[
{
'problem': sol.problem.id,
'graded_by': sol.graded_by,
'graded_at': sol.graded_at.isoformat() if sol.graded_at else None,
'feedback': [
fb.id for fb in sol.feedback
],
'remarks': sol.remarks
} for sol in sub.solutions.order_by(lambda s: s.problem.id)
]
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment