...
 
Commits (8)
......@@ -48,17 +48,19 @@ const Fail = Loadable({
loading: Loading
})
const nullExam = () => ({
id: null,
name: '',
submissions: [],
problems: [],
widgets: []
})
class App extends React.Component {
menu = React.createRef();
state = {
exam: {
id: null,
name: '',
submissions: [],
problems: [],
widgets: []
},
exam: nullExam(),
grader: null
}
......@@ -68,6 +70,17 @@ class App extends React.Component {
exam: ex
}))
}
deleteExam = (examID) => {
api
.del('exams/' + examID)
.then(() => {
if (this.menu.current) {
return this.menu.current.updateExamList()
}
})
}
updateSubmission = (index, sub) => {
if (index === undefined) {
api.get('submissions/' + this.state.exam.id)
......@@ -129,6 +142,7 @@ class App extends React.Component {
exam={exam}
examID={match.params.examID}
updateExam={this.updateExam}
deleteExam={this.deleteExam}
updateSubmission={this.updateSubmission} />} />
<Route path='/exams' render={({ history }) =>
<AddExam updateExamList={this.menu.current ? this.menu.current.updateExamList : null} changeURL={history.push} />} />
......
import React from 'react'
const ConfirmationModal = (props) => {
return (
<div className={'modal ' + (props.active ? 'is-active' : '')}>
<div className='modal-background' onClick={props.onCancel} />
<div className='modal-card'>
<header className='modal-card-head'>
<p className='modal-card-title '>{props.contentText || 'Are you sure?'}</p>
</header>
<footer className='modal-card-footer'>
<div className='field is-grouped'>
<button className={'button is-fullwidth ' + (props.color || 'is-success')} onClick={props.onConfirm}>
{props.confirmText || 'Save changes'}
</button>
<button className='button is-fullwidth' onClick={props.onCancel}>
{props.cancelText || 'Cancel'}
</button>
</div>
</footer>
</div>
<button className='modal-close is-large' aria-label='close' />
</div>
)
}
export default ConfirmationModal
......@@ -101,7 +101,9 @@ class NavBar extends React.Component {
this.setState({
examList: exams
})
if (this.props.exam.id === null && exams.length) this.props.updateExam(exams[exams.length - 1].id)
const examIDs = exams.map(exam => exam.id)
const examIsValid = this.props.exam.id !== null && examIDs.includes(this.props.exam.id)
if (!examIsValid && exams.length) this.props.updateExam(exams[exams.length - 1].id)
})
}
......
......@@ -7,6 +7,7 @@ import PanelGenerate from '../components/PanelGenerate.jsx'
import ExamEditor from './ExamEditor.jsx'
import update from 'immutability-helper'
import ExamFinalizeMarkdown from './ExamFinalize.md'
import ConfirmationModal from '../components/ConfirmationModal.jsx'
import * as api from '../api.jsx'
......@@ -17,7 +18,8 @@ class Exams extends React.Component {
numPages: null,
selectedWidgetId: null,
widgets: [],
previewing: false
previewing: false,
deleting: false
}
static getDerivedStateFromProps = (newProps, prevState) => {
......@@ -291,32 +293,41 @@ class Exams 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
onFinalizeClicked={() => this.setState({
previewing: true
})}
/>,
<this.Delete
onDeleteClicked={() => this.props.deleteExam(this.props.examID)}
/>
</div>
}
return (
<nav className='panel'>
<p className='panel-heading'>
Actions
</p>
<div className='panel-block'>
{this.state.previewing
? <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
})}
/>
: <this.Finalize
onFinalizeClicked={() => this.setState({
previewing: true
})}
/>
}
</div>
{actionsBody}
</nav>
)
}
......@@ -332,36 +343,41 @@ class Exams extends React.Component {
)
}
Delete = (props) => {
return (
<button
className='button is-link is-fullwidth is-danger'
onClick={() => { this.setState({deleting: true}) }}
>
Delete
</button>
)
}
PanelConfirm = (props) => {
return (
<nav className='panel'>
<div className='content' dangerouslySetInnerHTML={{__html: ExamFinalizeMarkdown}} />
<div className='panel-heading'>
<div>
<div className='panel-block'>
<label className='label'>Are you sure?</label>
</div>
<div className='panel-block'>
<div className='field has-addons'>
<div className='control'>
<button
disabled={props.disabled}
className='button is-danger'
onClick={() => props.onYesClick()}
>
Yes
</button>
</div>
<div className='control'>
<button
disabled={props.disabled}
className='button is-link'
onClick={() => props.onNoClick()}
>
No
</button>
</div>
</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>
</nav>
</div>
)
}
......@@ -387,6 +403,9 @@ class Exams extends React.Component {
</div>
</div>
</section>
<ConfirmationModal active={this.state.deleting} color='is-danger'
confirmText='Delete exam' onCancel={() => this.setState({deleting: false})}
onConfirm={this.onDeleteClicked} />
</div>
}
}
......
import React from 'react'
import ConfirmationModal from '../../components/ConfirmationModal.jsx'
import * as api from '../../api.jsx'
const BackButton = (props) => (
......@@ -20,12 +21,22 @@ const SaveButton = (props) => (
</button>
)
const DeleteButton = (props) => (
<button className='button is-link is-fullwidth is-danger' disabled={!props.exists} onClick={props.onClick}>
<span className='icon is-small'>
<i className='fa fa-trash' />
</span>
<span>{'delete'}</span>
</button>
)
class EditPanel extends React.Component {
state = {
id: null,
name: '',
description: '',
score: ''
score: '',
deleting: false
}
static getDerivedStateFromProps (nextProps, prevState) {
......@@ -73,7 +84,7 @@ class EditPanel extends React.Component {
if (this.state.id) {
fb.id = this.state.id
api.put(uri, fb)
.then(() => this.props.goBack)
.then(() => this.props.goBack())
} else {
api.post(uri, fb)
.then(() => {
......@@ -87,6 +98,13 @@ class EditPanel extends React.Component {
}
}
deleteFeedback = () => {
if (this.state.id) {
api.del('feedback/' + this.props.problemID + '/' + this.state.id)
.then(() => this.props.goBack())
}
}
render () {
return (
<nav className='panel'>
......@@ -137,6 +155,10 @@ class EditPanel extends React.Component {
<BackButton onClick={this.props.goBack} />
<SaveButton onClick={this.saveFeedback} exists={this.props.feedback}
disabled={!this.state.name || !this.state.score || isNaN(parseInt(this.state.score))} />
<DeleteButton onClick={() => { this.setState({deleting: true}) }} exists={this.props.feedback} />
<ConfirmationModal contentText='Do you want to irreversibly delete this feedback?'
color='is-danger' confirmText='Delete feedback' active={this.state.deleting}
onConfirm={this.deleteFeedback} onCancel={() => { this.setState({deleting: false}) }} />
</div>
</nav>
)
......
......@@ -40,7 +40,9 @@ api.add_resource(Problems,
'/problems',
'/problems/<int:problem_id>',
'/problems/<int:problem_id>/<string:attr>')
api.add_resource(Feedback, '/feedback/<int:problem_id>')
api.add_resource(Feedback,
'/feedback/<int:problem_id>',
'/feedback/<int:problem_id>/<int:feedback_id>')
api.add_resource(Solutions, '/solution/<int:exam_id>/<int:submission_id>/<int:problem_id>')
api.add_resource(Widgets,
'/widgets',
......
......@@ -4,7 +4,7 @@ import zipfile
from io import BytesIO
from tempfile import TemporaryFile
from flask import current_app as app, send_file, request
from flask import current_app as app, send_file, request, abort
from flask_restful import Resource, reqparse
from werkzeug.datastructures import FileStorage
......@@ -31,6 +31,16 @@ class Exams(Resource):
else:
return self._get_all()
@orm.db_session
def delete(self, exam_id):
exam = Exam.get(id=exam_id)
if exam is None:
return dict(status=404, message='Exam does not exist.'), 404
elif exam.finalized:
return dict(status=409, message='Cannot delete a finalized exam.'), 409
else:
exam.delete()
@orm.db_session
def _get_all(self):
"""get list of uploaded exams.
......@@ -78,6 +88,9 @@ class Exams(Resource):
"""
exam = Exam[exam_id]
if exam is None:
return dict(status=404, message='Exam does not exist.'), 404
submissions = [
{
'id': sub.copy_number,
......
......@@ -97,3 +97,27 @@ class Feedback(Resource):
'description': fb.description,
'score': fb.score
}
@orm.db_session
def delete(self, problem_id, feedback_id):
"""Modify an existing feedback option
Parameters
----------
problem_id : int
The id of the problem to which the feedback belongs.
feedback_id : int
The database id of the feedback option.
Notes
-----
We use the problem id for extra safety check that we don't corrupt
things accidentally.
"""
fb = FeedbackOption.get(id=feedback_id)
if fb is None:
return dict(status=404, message="Feedback with this id does not exist"), 404
elif fb.problem.id != problem_id:
return dict(status=409, message="Feedback does not match the problem."), 409
else:
fb.delete()