Newer
Older
import Notification from 'react-bulma-notification'
import Hero from '../components/Hero.jsx'
import './Exam.css'
import GeneratedExamPreview from '../components/GeneratedExamPreview.jsx'
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'
class Exams extends React.Component {

Thomas Roos
committed
state = {
examID: null,
page: 0,
numPages: null,
selectedWidgetId: null,

Thomas Roos
committed
widgets: [],
deletingExam: false,
deletingWidget: false

Thomas Roos
committed
}

Thomas Roos
committed
static getDerivedStateFromProps = (newProps, prevState) => {
if (newProps.exam.id !== prevState.examID) {
// initialize array to size of pdf
const widgets = []
newProps.exam.problems.forEach(problem => {
// keep page and name of problem as widget.problem object
widgets[problem.widget.id] = {
...problem.widget,
problem: {
id: problem.id,
page: problem.page,
name: problem.name,
graded: problem.graded

Thomas Roos
committed
})

Thomas Roos
committed
newProps.exam.widgets.forEach(examWidget => {
widgets[examWidget.id] = examWidget
})

Thomas Roos
committed
return {
examID: newProps.exam.id,
page: 0,
numPages: null,
selectedWidgetId: null,
widgets: widgets,
previewing: false

Thomas Roos
committed
return null
}
componentDidUpdate = (prevProps, prevState) => {

Thomas Roos
committed
if (prevProps.examID !== this.props.examID) {
this.props.updateExam(this.props.examID)
// This saves the problem name when switching to non-problem widgets
// The onBlur event is not fired when the input field is being disabled
if (prevState.selectedWidgetId !== this.state.selectedWidgetId) {
this.saveProblemName()
}

Thomas Roos
committed
}

Thomas Roos
committed
componentDidMount = () => {
if (this.props.examID !== this.props.exam.id) this.props.updateExam(this.props.examID)
}
componentWillUnmount = () => {
// This might try to save the name unnecessary, but better twice than never.
this.saveProblemName()
// Force an update of the upper exam state, since this component does not update and use that correctly
this.props.updateExam(this.props.examID)
}
saveProblemName = () => {
const changedWidgetId = this.state.changedWidgetId
if (!changedWidgetId) return
const changedWidget = this.state.widgets[changedWidgetId]
if (!changedWidget) return
const problem = changedWidget.problem
if (!problem) return
api.put('problems/' + problem.id + '/name', { name: problem.name })
.catch(e => Notification.error('Could not save new problem name: ' + e))
.then(this.setState({
changedWidgetId: null
}))
}
deleteWidget = (widgetId) => {

Thomas Roos
committed
const widget = this.state.widgets[widgetId]
if (widget) {
api.del('problems/' + widget.problem.id)
.then(() => {
this.setState((prevState) => {
return {
selectedWidgetId: null,
deletingWidget: false,
widgets: update(prevState.widgets, {
$unset: [widgetId]
})
}

Thomas Roos
committed
})
})
.catch(err => {
console.log(err)
err.json().then(res => {
this.setState({
deletingWidget: false
})
Notification.error('Could not delete problem' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
})

Thomas Roos
committed
}

Thomas Roos
committed
updateWidget = (widgetId, newData) => {
this.setState(prevState => ({
widgets: update(prevState.widgets, {
[widgetId]: newData

Thomas Roos
committed
}))
}

Thomas Roos
committed
renderContent = () => {
if (this.state.previewing) {

Thomas Roos
committed
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<GeneratedExamPreview
examID={this.state.examID}
page={this.state.page}
onPDFLoad={this.onPDFLoad}
/>
)
} else {
return (
<ExamEditor
finalized={this.props.exam.finalized}
widgets={this.state.widgets}
examID={this.state.examID}
page={this.state.page}
numPages={this.state.numPages}
onPDFLoad={this.onPDFLoad}
updateWidget={this.updateWidget}
selectedWidgetId={this.state.selectedWidgetId}
selectWidget={(widgetId) => {
this.setState({
selectedWidgetId: widgetId
})
}}
createNewWidget={(widgetData) => {
this.setState((prevState) => {
return {
selectedWidgetId: widgetData.id,

Thomas Roos
committed
[widgetData.id]: {
$set: widgetData

Thomas Roos
committed
}
})
}}
/>

Thomas Roos
committed
}

Thomas Roos
committed
Pager = (props) => {
const isDisabled = props.numPages == null
const pageNum = isDisabled ? '_' : props.page + 1
const numPages = isDisabled ? '_' : props.numPages
return (
<div className='field has-addons is-mobile'>
<div className='control'>
<button
disabled={isDisabled}
type='submit'
className='button is-link is-rounded'
onClick={() => props.setPage(props.page - 1)}
>
Previous
</button>
</div>
<div className='control'>
<div className='field-text is-rounded has-text-centered is-link'>
{'Page ' + pageNum + ' of ' + numPages}

Thomas Roos
committed
</div>
<div className='control'>
<button
disabled={isDisabled}
type='submit'
className='button is-link is-rounded'
onClick={() => props.setPage(props.page + 1)}
>
Next
</button>
</div>
</div>
)
}

Thomas Roos
committed
onPDFLoad = (pdf) => {
this.setState((newProps, prevState) => ({
numPages: pdf.numPages
}), () => {
this.props.updateExam(this.props.examID)
})
}

Thomas Roos
committed
setPage = (newPage) => {
this.setState((prevState) => {
return {
// clamp the page
selectedWidgetId: null,
page: Math.max(0, Math.min(newPage, prevState.numPages - 1))

Thomas Roos
committed
})
}

Thomas Roos
committed
SidePanel = (props) => {
const selectedWidgetId = this.state.selectedWidgetId
let selectedWidget = selectedWidgetId && this.state.widgets[selectedWidgetId]
let problem = selectedWidget && selectedWidget.problem
let widgetEditDisabled = this.state.previewing || !problem
let isGraded = problem && problem.graded
let widgetDeleteDisabled = widgetEditDisabled || isGraded
let totalNrAnswers = 20 // the upper limit for the nr of possible answer boxes
let disabledGenerateBoxes = false

Thomas Roos
committed
return (
<React.Fragment>
<this.PanelEdit
disabledEdit={widgetEditDisabled}
disabledDelete={widgetDeleteDisabled}

Thomas Roos
committed
onDeleteClick={() => {
this.setState({deletingWidget: true})

Thomas Roos
committed
}}
problem={problem}
changeProblemName={newName => {
this.setState(prevState => ({
changedWidgetId: selectedWidgetId,

Thomas Roos
committed
widgets: update(prevState.widgets, {
[selectedWidgetId]: {
problem: {
name: {
$set: newName
}
}
}

Thomas Roos
committed
}))
}}
saveProblemName={this.saveProblemName}

Thomas Roos
committed
/>
<this.PanelMCQ
totalNrAnswers={totalNrAnswers}
disabledGenerateBoxes={disabledGenerateBoxes}
problem={problem}
onGenerateBoxesClick={() => {
console.log('Generating boxes')
}}
/>

Thomas Roos
committed
<this.PanelExamActions />
</React.Fragment>
)
}
PanelEdit = (props) => {
const selectedWidgetId = this.state.selectedWidgetId

Thomas Roos
committed
return (
<nav className='panel'>
<p className='panel-heading'>
Problem details
</p>
<div className='panel-block'>
<div className='field'>
{selectedWidgetId === null ? (
<p style={{margin: '0.625em 0', minHeight: '3em'}}>
To create a problem, draw a rectangle on the exam.
</p>
) : (
<React.Fragment>
<label className='label'>Name</label>
<div className='control'>
<input
disabled={props.disabledEdit}
className='input'
placeholder='Problem name'
value={props.problem ? props.problem.name : ''}
onChange={(e) => {
props.changeProblemName(e.target.value)
}}
onBlur={(e) => {
props.saveProblemName(e.target.value)
}} />
</div>
</React.Fragment>
)}

Thomas Roos
committed
</div>
<div className='panel-block'>
<button
disabled={props.disabledDelete}

Thomas Roos
committed
className='button is-danger is-fullwidth'
onClick={() => props.onDeleteClick()}
>

Thomas Roos
committed
</button>
</div>

Thomas Roos
committed
</nav>
)
}
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
PanelMCQ = (props) => {
const selectedWidgetId = this.state.selectedWidgetId
return selectedWidgetId == null ? null : (
<nav className='panel'>
<p className='panel-heading'>
Multiple Choice Question
</p>
<div className='panel-block'>
<div className='field'>
<React.Fragment>
<label className='label'>Number possible answers</label>
<div className='control'>
{(function () {
var optionList = []
for (var i = 1; i <= props.totalNrAnswers; i++) {
const optionElement = <option value={String(i)}>{i}</option>
optionList.push(optionElement)
}
return (<div className='select is-info is-fullwidth'>
<select>{optionList}</select>
</div>)
}())}
</div>
</React.Fragment>
</div>
</div>
<div className='panel-block'>
<button
disabled={props.disabledGenerateBoxes}
className='button is-info is-fullwidth'
onClick={() => props.onGenerateBoxesClick()}
>
Generate boxes
</button>
</div>
</nav>
)
}

Thomas Roos
committed
PanelExamActions = () => {
if (this.props.exam.finalized) {
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'>

Thomas Roos
committed
return (
<nav className='panel'>
<p className='panel-heading'>
Actions
</p>

Thomas Roos
committed
</nav>
)
}
Finalize = (props) => {
return (
<button
className='button is-link is-fullwidth'
onClick={() => { this.setState({previewing: true}) }}

Thomas Roos
committed
>
Finalize
</button>
)
}
Delete = (props) => {
return (
<button
className='button is-link is-fullwidth is-danger'
onClick={() => { this.setState({deletingExam: true}) }}
</button>
)
}

Thomas Roos
committed
PanelConfirm = (props) => {
return (
<div>
<div className='panel-block'>

Thomas Roos
committed
<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>

Thomas Roos
committed
</div>

Thomas Roos
committed
)
}

Thomas Roos
committed
render () {
return <div>
<Hero />
<section className='section'>
<div className='container'>
<div className='columns is-centered' >
<div className='column is-one-quarter-widescreen is-one-third-desktop editor-side-panel' >
<p className='title is-1'>Exam details</p>
<p className='subtitle is-3'>{'Selected: ' + this.props.exam.name}</p>
<this.Pager
page={this.state.page}
numPages={this.state.numPages}
setPage={this.setPage}
/>
<this.SidePanel examID={this.state.examID} />
</div>
<div className='column is-narrow editor-content' >
{this.renderContent()}

Thomas Roos
committed
</div>
</section>
active={this.state.deletingExam}
color='is-danger'
headerText={`Are you sure you want to delete exam "${this.props.exam.name}"?`}
confirmText='Delete exam'
onCancel={() => this.setState({deletingExam: false})}
onConfirm={() => {
this.props.deleteExam(this.props.examID).then(this.props.leave)
}}
/>
<ConfirmationModal
active={this.state.deletingWidget && this.state.selectedWidgetId != null}
color='is-danger'
headerText={`Are you sure you want to delete problem "${
this.state.selectedWidgetId &&
this.state.widgets[this.state.selectedWidgetId] &&
this.state.widgets[this.state.selectedWidgetId].problem &&
this.state.widgets[this.state.selectedWidgetId].problem.name}"`}
confirmText='Delete problem'
onCancel={() => this.setState({deletingWidget: false})}
onConfirm={() => this.deleteWidget(this.state.selectedWidgetId)}
/>

Thomas Roos
committed
</div>
}