Commit a8d71aae authored by Adrià Labay's avatar Adrià Labay
Browse files

finalize and delete unstructured exams and fixed and improved ui

parent 3c5bad24
......@@ -51,7 +51,7 @@ class ExamRouter extends React.PureComponent {
return api
.del('exams/' + examID)
.then(() => {
this.updateExamList()
this.props.updateExamList()
history.push('/')
})
}
......
......@@ -25,17 +25,17 @@ class Exams extends React.Component {
this.loadExam(this.props.examID)
}
loadExam = (id) => {
if (!id) return
loadExam = () => {
if (!this.props.examID) return
return api.get(`exams/${id}`)
return api.get(`exams/${this.props.examID}`)
.then(resp => {
this.setState({exam: resp})
this.setState({ exam: resp })
})
.catch(err => {
console.log(err)
err.json().then(data => {
this.setState({exam: null, status: data.message})
this.setState({ exam: null, status: data.message })
})
})
}
......@@ -43,17 +43,17 @@ class Exams extends React.Component {
renderExamContent = () => {
const layout = this.state.exam.layout
const commonProps = {
exam: this.state.exam,
examID: this.state.exam.id,
examName: this.state.exam.name,
updateExamList: this.props.updateExamList,
updateExam: this.loadExam
updateExam: this.loadExam,
deleteExam: () => { this.setState({deletingExam: true}) }
}
if (layout === 'templated') {
// templated exam
return <ExamTemplated
exam={this.state.exam}
deleteExam={() => { this.setState({deletingExam: true}) }}
setHelpPage={this.props.setHelpPage}
{...commonProps} />
} else if (layout === 'unstructured') {
......
......@@ -2,4 +2,4 @@ You _**will no longer**_ be able to:
+ modify the position of the student ID widget
+ modify the position of the page markers
+ create, delete or modify multiple choice options
\ No newline at end of file
+ create, delete or modify multiple choice options
......@@ -14,6 +14,7 @@ import ExamEditor from './ExamEditor.jsx'
import PanelGradeAnonymous from './PanelGradeAnonymous.jsx'
import ExamFinalizeMarkdown from './ExamFinalize.md'
import PanelExamName from './PanelExamName.jsx'
import PanelFinalize from './PanelFinalize.jsx'
import './Exam.css'
......@@ -220,7 +221,7 @@ class ExamTemplated extends React.Component {
}
backToFeedback = () => {
this.props.updateExam(this.props.exam.id)
this.props.updateExam()
this.setState({
editActive: false
})
......@@ -317,7 +318,7 @@ class ExamTemplated extends React.Component {
Notification.error('Could not delete problem' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
this.props.updateExam()
})
})
}
......@@ -376,9 +377,7 @@ class ExamTemplated extends React.Component {
})
}}
createNewWidget={this.createNewWidget}
updateExam={() => {
this.props.updateExam(this.props.examID)
}}
updateExam={this.props.updateExam}
/>
)
}
......@@ -388,7 +387,7 @@ class ExamTemplated extends React.Component {
this.setState((newProps, prevState) => ({
numPages: pdf.numPages
}), () => {
this.props.updateExam(this.props.examID)
this.props.updateExam()
})
}
......@@ -455,7 +454,7 @@ class ExamTemplated extends React.Component {
Notification.error('Could not create multiple choice option' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
this.props.updateExam()
this.setState({
selectedWidgetId: null
})
......@@ -529,7 +528,7 @@ class ExamTemplated extends React.Component {
Notification.error('Could not delete multiple choice option' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
this.props.updateExam()
this.setState({
selectedWidgetId: null
})
......@@ -614,7 +613,7 @@ class ExamTemplated extends React.Component {
{this.props.exam.finalized && <PanelGradeAnonymous
examID={this.props.exam.id}
gradeAnonymous={this.props.exam.gradeAnonymous}
onChange={(anonymous) => this.props.updateExam(null)} />}
onChange={(anonymous) => this.props.updateExam()} />}
</React.Fragment>
)
}
......@@ -721,7 +720,7 @@ class ExamTemplated extends React.Component {
Notification.error('Could not update feedback' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
this.props.updateExam()
})
})
})
......@@ -777,90 +776,13 @@ class ExamTemplated extends React.Component {
return <PanelGenerate examID={this.state.examID} />
}
let actionsBody
if (this.state.previewing) {
actionsBody =
<this.PanelConfirm
onYesClick={() =>
api.put(`exams/${this.props.examID}`, {finalized: true})
.then(() => {
this.props.updateExam(this.props.examID)
this.setState({ previewing: false })
})
}
onNoClick={() => this.setState({
previewing: false
})}
/>
} else {
actionsBody =
<div className='panel-block field is-grouped'>
<this.Finalize />
<this.Delete />
</div>
}
return (
<nav className='panel'>
<p className='panel-heading'>
Actions
</p>
{actionsBody}
</nav>
)
}
Finalize = (props) => {
return (
<button
className='button is-link is-fullwidth'
onClick={() => {
this.setState({
selectedWidgetId: null,
previewing: true
})
}}
>
Finalize
</button>
)
}
Delete = (props) => {
return (
<button
className='button is-link is-fullwidth is-danger'
onClick={() => this.props.deleteExam()}
>
Delete exam
</button>
)
}
PanelConfirm = (props) => {
return (
<div>
<div className='panel-block'>
<label className='label'>Are you sure?</label>
</div>
<div className='content panel-block' dangerouslySetInnerHTML={{__html: ExamFinalizeMarkdown}} />
<div className='panel-block field is-grouped'>
<button
disabled={props.disabled}
className='button is-danger is-link is-fullwidth'
onClick={() => props.onYesClick()}
>
Yes
</button>
<button
disabled={props.disabled}
className='button is-link is-fullwidth'
onClick={() => props.onNoClick()}
>
No
</button>
</div>
</div>
<PanelFinalize
examID={this.props.examID}
onFinalise={() => this.props.updateExam()}
deleteExam={this.props.deleteExam}>
<p className='content' dangerouslySetInnerHTML={{__html: ExamFinalizeMarkdown}} />
</PanelFinalize>
)
}
......
......@@ -6,6 +6,7 @@ import ConfirmationModal from '../../components/ConfirmationModal.jsx'
import ExamUnstructuredMarkdown from './ExamUnstructuredRules.md'
import PanelGradeAnonymous from './PanelGradeAnonymous.jsx'
import PanelExamName from './PanelExamName.jsx'
import PanelFinalize from './PanelFinalize.jsx'
import * as api from '../../api.jsx'
......@@ -23,6 +24,7 @@ const ExamContent = (props) => {
const pageCount = Object.keys(pages).length
const pageTitle = pageCount === 1 && 'Problem list'
const addPageButtonText = pageCount === 1 ? 'Specify pages' : 'Add page'
return (
<div>
{Object.keys(pages).map(page => (
......@@ -56,7 +58,7 @@ const ExamContent = (props) => {
</div>
</div>
))}
{problemCount ? <button
{problemCount && !props.finalized ? <button
className='button problem is-link is-fullwidth'
onClick={props.addPage}>
<span>{addPageButtonText}</span>
......@@ -67,8 +69,7 @@ const ExamContent = (props) => {
class PanelEditUnstructured extends React.Component {
state = {
examID: null,
gradeAnonymous: false,
exam: null,
problems: [],
problemName: '',
problemPage: -1,
......@@ -76,40 +77,33 @@ class PanelEditUnstructured extends React.Component {
deletingProblem: false
}
componentWillMount = () => {
if (this.props.examID !== null) {
this.setState(
{ examID: this.props.examID }, () => this.loadProblems(null)
)
}
}
static getDerivedStateFromProps = (newProps, prevState) => {
if (prevState.exam !== newProps.exam) {
const problems = newProps.exam.problems.sort((p1, p2) => p1.page - p2.page)
let problem = problems.find(p => p.id === prevState.selectedProblemId)
if (!problem && problems.length > 0) {
problem = problems[0]
}
componentDidUpdate = (prevProps, prevState) => {
if (this.props.examID !== prevProps.examID) {
this.setState({examID: this.props.examID}, () => this.loadProblems(null))
return {
exam: newProps.exam,
problems: problems,
selectedProblemId: problem ? problem.id : null,
problemName: problem ? problem.name : null,
problemPage: problem ? problem.page + 1 : -1,
deletingProblem: false
}
}
return null
}
loadProblems = (selectId) => {
api.get('exams/' + this.state.examID)
.then(exam => {
this.setState({
gradeAnonymous: exam.gradeAnonymous,
problems: exam.problems.sort((p1, p2) => p1.page - p2.page)
})
this.selectProblem(selectId)
})
.catch(err => {
console.log(err)
err.json().then(res => {
this.setState({
problems: [],
selectedProblemId: null,
problemName: '',
deletingProblem: false
})
})
})
componentWillUnmount = () => {
// This might try to save the name unnecessary, but better twice than never.
if (this.selectedProblemId !== null) {
this.saveProblemPage()
this.saveProblemName()
}
}
selectProblem = (id) => {
......@@ -133,7 +127,7 @@ class PanelEditUnstructured extends React.Component {
createProblem = (page) => {
const formData = new window.FormData()
formData.append('exam_id', this.state.examID)
formData.append('exam_id', this.props.examID)
formData.append('name', `Problem (${this.state.problems.length + 1})`)
formData.append('page', page)
formData.append('x', 0)
......@@ -141,7 +135,7 @@ class PanelEditUnstructured extends React.Component {
formData.append('width', 0)
formData.append('height', 0)
api.post('problems', formData).then(result => {
this.loadProblems(result.id)
this.setState({ selectedProblemId: result.id }, () => this.props.updateExam())
}).catch(err => {
console.log(err)
})
......@@ -154,7 +148,7 @@ class PanelEditUnstructured extends React.Component {
}
api.put('problems/' + id, { name: name })
.then(resp => this.loadProblems(id))
.then(resp => this.props.updateExam())
.catch(e => {
this.selectProblem(id) // takes care of updating the problem name to previous state
console.log(e)
......@@ -169,10 +163,10 @@ class PanelEditUnstructured extends React.Component {
}
api.patch(`widgets/${widgetId}`, {page: parseInt(page) - 1})
.then(resp => this.loadProblems(problemId))
.then(resp => this.props.updateExam())
.catch(e => {
console.log(e)
this.loadProblems(problemId)
this.props.updateExam()
e.json().then(res => {
Notification.warn('Could not save new problem page: ' + res.message)
})
......@@ -182,7 +176,7 @@ class PanelEditUnstructured extends React.Component {
deleteProblem = (id) => {
api.del('problems/' + id)
.then(() => {
this.loadProblems(null)
this.props.updateExam()
})
.catch(err => {
console.log(err)
......@@ -198,11 +192,7 @@ class PanelEditUnstructured extends React.Component {
inputColor = (name, originalName) => {
if (name) {
if (name !== originalName) {
return 'is-success'
} else {
return ''
}
return name !== originalName ? 'is-success' : ''
} else {
return 'is-danger'
}
......@@ -267,10 +257,10 @@ class PanelEditUnstructured extends React.Component {
{!this.state.editActive && <label className='label'>Feedback options</label>}
</div>
<FeedbackPanel
examID={this.state.examID}
examID={this.props.examID}
problem={props.problem}
grading={false}
updateFeedback={() => this.loadProblems(props.problem.id)} />
updateFeedback={() => this.props.updateExam()} />
<div className='panel-block'>
<button
......@@ -297,28 +287,36 @@ class PanelEditUnstructured extends React.Component {
return (
<React.Fragment>
<div className='columns is-centered' >
<div className='column is-one-third-fullhd is-half-tablet' >
<div className='columns is-centered is-multiline' >
<div className='column is-one-third-fullhd is-two-thirds-tablet' >
<PanelExamName
name={this.props.examName}
name={this.state.exam.name}
examID={this.props.examID}
updateExam={this.props.updateExam}
updateExamList={this.props.updateExamList} />
<this.PanelProblem
problem={problem} />
<PanelGradeAnonymous
examID={this.state.examID}
gradeAnonymous={this.state.gradeAnonymous}
examID={this.props.examID}
gradeAnonymous={this.state.exam.gradeAnonymous}
text='Please note that the student name or number can still be visible on the pages themselves.' />
{!this.state.exam.finalized &&
<PanelFinalize
examID={this.props.examID}
onFinalise={() => this.props.updateExam()}
deleteExam={this.props.deleteExam}>
Be careful, changing the amount of pages after finalization can lead to incorrect display of images during grading.
</PanelFinalize>
}
<nav className='panel'>
<p className='panel-heading'>
Tips
</p>
<div className='content panel-block' dangerouslySetInnerHTML={{__html: ExamUnstructuredMarkdown}} />
<div className='panel-block'>
<p className='content' dangerouslySetInnerHTML={{__html: ExamUnstructuredMarkdown}} />
</div>
</nav>
</div>
<div className='column is-one-third-fullhd is-half-tablet'>
......@@ -328,9 +326,14 @@ class PanelEditUnstructured extends React.Component {
selectedProblemId={this.state.selectedProblemId}
selectProblem={this.selectProblem}
createProblem={this.createProblem}
addPage={this.addPage} />
addPage={this.addPage}
finalized={this.state.finalized} />
</div>
</div>
<div className='column is-one-third-fullhd is-half-tablet'>
<this.PanelProblem
problem={problem} />
</div>
</div>
{problem && <ConfirmationModal
active={this.state.deletingProblem && this.state.selectedProblemId != null}
......
* Add problems to the exam, the order they appear within a page is not important but the order of pages it is. Zesje will show the full page during grading and you can move through the problems associated to that page.
* You may create a fully unstructured exam, for this add all the problems in the first page (0), Zesje will show you all the uploaded images and you will be able to scroll through all of them.
* You may create a fully unstructured exam, for this add all the problems in the first page, Zesje will show you all the uploaded images and you will be able to scroll through all of them.
* Remember to tell your students to upload the files in the correct format. Zesje currently supports ZIP, PDF and IMAGE files whose name has to identify the student and page. The allowed formats are:
* `studentId-page-copy.ext`
* `studentId.ext`
......
......@@ -12,7 +12,6 @@ class PanelExamName extends React.Component {
}
static getDerivedStateFromProps (nextProps, prevState) {
// In case nothing is set, use an empty function that no-ops
if (prevState.examID !== nextProps.examID) {
return {
examID: nextProps.examID,
......@@ -20,6 +19,7 @@ class PanelExamName extends React.Component {
editing: false
}
}
return null
}
......@@ -60,7 +60,7 @@ class PanelExamName extends React.Component {
onChange = () => {
// In order to change the name everywhere in the UI we need to update the exam
// in the parent view and reload the exam list in the navbar
this.props.updateExam(this.state.examID)
this.props.updateExam()
this.props.updateExamList()
}
......
import React from 'react'
import * as api from '../../api.jsx'
class PanelFinalize extends React.Component {
state = {
examID: null,
confirmationText: null,
previewing: false
}
static getDerivedStateFromProps (nextProps, prevState) {
// In case nothing is set, use an empty function that no-ops
if (prevState.examID !== nextProps.examID) {
return {
examID: nextProps.examID,
confirmationText: nextProps.confirmationText,
previewing: false
}
}
return null
}
finalize = () => {
api.put(`exams/${this.state.examID}`, { finalized: true })
.then(() => {
this.props.onFinalise()
this.setState({ previewing: false })
})
}
Finalize = (props) => {
return (
<button
className='button is-link is-fullwidth'
onClick={() => {
this.setState({
previewing: true
})
}}
>
Finalize
</button>
)
}
Delete = (props) => {
return (
<button
className='button is-danger is-fullwidth'
onClick={this.props.deleteExam}
>
Delete exam
</button>
)
}
render = () => {
return (
<nav className='panel'>
<p className='panel-heading'>
Actions
</p>
{this.state.previewing ? (
<div>
<div className='panel-block'>
<label className='label'>Are you sure?</label>
</div>
<div className='panel-block'>
{this.props.children}
</div>
<div className='panel-block field is-grouped'>
<button
className='button is-danger is-link is-fullwidth'
onClick={this.finalize}
>
Yes
</button>
<button
className='button is-link is-fullwidth'
onClick={() => this.setState({ previewing: false })}
>
No
</button>
</div>
</div>
) : (
<div className='panel-block field is-grouped'>
<this.Finalize />
<this.Delete />
</div>
)}
</nav>
)
}
}
export default PanelFinalize
......@@ -56,7 +56,6 @@ def test_add_unstructured_exam(test_client):
assert len(data) == 1
assert data[0]['layout'] == ExamLayout.unstructured.name
assert data[0]['finalized']