Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • zesje/zesje
  • jbweston/grader_app
  • dj2k/zesje
  • MrHug/zesje
  • okaaij/zesje
  • tsoud/zesje
  • pimotte/zesje
  • works-on-my-machine/zesje
  • labay11/zesje
  • reouvenassouly/zesje
  • t.v.aerts/zesje
  • giuseppe.deininger/zesje
12 results
Show changes
Commits on Source (32)
...@@ -40,16 +40,19 @@ class EditPanel extends React.Component { ...@@ -40,16 +40,19 @@ class EditPanel extends React.Component {
} }
static getDerivedStateFromProps (nextProps, prevState) { static getDerivedStateFromProps (nextProps, prevState) {
// In case nothing is set, use an empty function that no-ops
const updateCallback = nextProps.updateCallback || (_ => {})
if (nextProps.feedback && prevState.id !== nextProps.feedback.id) { if (nextProps.feedback && prevState.id !== nextProps.feedback.id) {
const fb = nextProps.feedback const fb = nextProps.feedback
return { return {
id: fb.id, id: fb.id,
name: fb.name, name: fb.name,
description: fb.description, description: fb.description,
score: fb.score score: fb.score,
updateCallback: updateCallback
} }
} }
return null return {updateCallback: updateCallback}
} }
changeText = (event) => { changeText = (event) => {
...@@ -84,10 +87,15 @@ class EditPanel extends React.Component { ...@@ -84,10 +87,15 @@ class EditPanel extends React.Component {
if (this.state.id) { if (this.state.id) {
fb.id = this.state.id fb.id = this.state.id
api.put(uri, fb) api.put(uri, fb)
.then(() => this.props.goBack()) .then(() => {
this.state.updateCallback(fb)
this.props.goBack()
})
} else { } else {
api.post(uri, fb) api.post(uri, fb)
.then(() => { .then((response) => {
// Response is the feedback option
this.state.updateCallback(response)
this.setState({ this.setState({
id: null, id: null,
name: '', name: '',
...@@ -101,14 +109,20 @@ class EditPanel extends React.Component { ...@@ -101,14 +109,20 @@ class EditPanel extends React.Component {
deleteFeedback = () => { deleteFeedback = () => {
if (this.state.id) { if (this.state.id) {
api.del('feedback/' + this.props.problemID + '/' + this.state.id) api.del('feedback/' + this.props.problemID + '/' + this.state.id)
.then(() => this.props.goBack()) .then(() => {
this.state.updateCallback({
id: this.state.id,
deleted: true
})
this.props.goBack()
})
} }
} }
render () { render () {
return ( return (
<nav className='panel'> <React.Fragment>
<p className='panel-heading'> <p className={this.props.grading ? 'panel-heading' : 'panel-heading is-radiusless'}>
Manage feedback Manage feedback
</p> </p>
...@@ -168,7 +182,7 @@ class EditPanel extends React.Component { ...@@ -168,7 +182,7 @@ class EditPanel extends React.Component {
onCancel={() => { this.setState({deleting: false}) }} onCancel={() => { this.setState({deleting: false}) }}
/> />
</div> </div>
</nav> </React.Fragment>
) )
} }
} }
......
...@@ -32,7 +32,9 @@ class FeedbackBlock extends React.Component { ...@@ -32,7 +32,9 @@ class FeedbackBlock extends React.Component {
render () { render () {
const shortcut = (this.props.index < 11 ? '' : 'shift + ') + this.props.index % 10 const shortcut = (this.props.index < 11 ? '' : 'shift + ') + this.props.index % 10
return ( return (
<a className='panel-block is-active' onClick={this.toggle} <a
className={this.props.grading ? 'panel-block is-active' : 'panel-block'}
onClick={this.props.grading ? this.toggle : this.props.editFeedback}
style={this.props.selected ? {backgroundColor: '#209cee'} : {}} style={this.props.selected ? {backgroundColor: '#209cee'} : {}}
> >
<span <span
...@@ -40,7 +42,9 @@ class FeedbackBlock extends React.Component { ...@@ -40,7 +42,9 @@ class FeedbackBlock extends React.Component {
? ' tooltip is-tooltip-active is-tooltip-left' : '')} ? ' tooltip is-tooltip-active is-tooltip-left' : '')}
data-tooltip={shortcut} data-tooltip={shortcut}
> >
<i className={'fa fa-' + (this.props.checked ? 'check-square-o' : 'square-o')} /> {this.props.grading &&
<i className={'fa fa-' + (this.props.checked ? 'check-square-o' : 'square-o')} />
}
</span> </span>
<span style={{ width: '80%' }}> <span style={{ width: '80%' }}>
{this.props.feedback.name} {this.props.feedback.name}
......
...@@ -4,7 +4,7 @@ import Notification from 'react-bulma-notification' ...@@ -4,7 +4,7 @@ import Notification from 'react-bulma-notification'
import * as api from '../../api.jsx' import * as api from '../../api.jsx'
import withShortcuts from '../../components/ShortcutBinder.jsx' import withShortcuts from '../ShortcutBinder.jsx'
import FeedbackBlock from './FeedbackBlock.jsx' import FeedbackBlock from './FeedbackBlock.jsx'
class FeedbackPanel extends React.Component { class FeedbackPanel extends React.Component {
...@@ -35,7 +35,7 @@ class FeedbackPanel extends React.Component { ...@@ -35,7 +35,7 @@ class FeedbackPanel extends React.Component {
static getDerivedStateFromProps (nextProps, prevState) { static getDerivedStateFromProps (nextProps, prevState) {
if (prevState.problemID !== nextProps.problem.id || prevState.submissionID !== nextProps.submissionID) { if (prevState.problemID !== nextProps.problem.id || prevState.submissionID !== nextProps.submissionID) {
return { return {
remark: nextProps.solution.remark, remark: nextProps.grading && nextProps.solution.remark,
problemID: nextProps.problem.id, problemID: nextProps.problem.id,
submissionID: nextProps.submissionID, submissionID: nextProps.submissionID,
selectedFeedbackIndex: null selectedFeedbackIndex: null
...@@ -90,29 +90,34 @@ class FeedbackPanel extends React.Component { ...@@ -90,29 +90,34 @@ class FeedbackPanel extends React.Component {
const blockURI = this.props.examID + '/' + this.props.submissionID + '/' + this.props.problem.id const blockURI = this.props.examID + '/' + this.props.submissionID + '/' + this.props.problem.id
let totalScore = 0 let totalScore = 0
for (let i = 0; i < this.props.solution.feedback.length; i++) { if (this.props.grading) {
const probIndex = this.props.problem.feedback.findIndex(fb => fb.id === this.props.solution.feedback[i]) for (let i = 0; i < this.props.solution.feedback.length; i++) {
if (probIndex >= 0) totalScore += this.props.problem.feedback[probIndex].score const probIndex = this.props.problem.feedback.findIndex(fb => fb.id === this.props.solution.feedback[i])
if (probIndex >= 0) totalScore += this.props.problem.feedback[probIndex].score
}
} }
let selectedFeedbackId = this.state.selectedFeedbackIndex !== null && let selectedFeedbackId = this.state.selectedFeedbackIndex !== null &&
this.props.problem.feedback[this.state.selectedFeedbackIndex].id this.props.problem.feedback[this.state.selectedFeedbackIndex].id
return ( return (
<nav className='panel'> <React.Fragment>
<p className='panel-heading'> {this.props.grading &&
Total:&nbsp;<b>{totalScore}</b> <p className='panel-heading'>
</p> Total:&nbsp;<b>{totalScore}</b>
</p>}
{this.props.problem.feedback.map((feedback, index) => {this.props.problem.feedback.map((feedback, index) =>
<FeedbackBlock key={feedback.id} uri={blockURI} graderID={this.props.graderID} <FeedbackBlock key={feedback.id} uri={blockURI} graderID={this.props.graderID}
feedback={feedback} checked={this.props.solution.feedback.includes(feedback.id)} feedback={feedback} checked={this.props.grading && this.props.solution.feedback.includes(feedback.id)}
editFeedback={() => this.props.editFeedback(feedback)} updateSubmission={this.props.updateSubmission} editFeedback={() => this.props.editFeedback(feedback)} updateSubmission={this.props.updateSubmission}
ref={(selectedFeedbackId === feedback.id) ? this.feedbackBlock : null} ref={(selectedFeedbackId === feedback.id) ? this.feedbackBlock : null} grading={this.props.grading}
selected={selectedFeedbackId === feedback.id} showIndex={this.props.showTooltips} index={index + 1} /> selected={selectedFeedbackId === feedback.id} showIndex={this.props.showTooltips} index={index + 1} />
)} )}
<div className='panel-block'> {this.props.grading &&
<textarea className='textarea' rows='2' placeholder='remark' value={this.state.remark} onBlur={this.saveRemark} onChange={this.changeRemark} /> <div className='panel-block'>
</div> <textarea className='textarea' rows='2' placeholder='remark' value={this.state.remark} onBlur={this.saveRemark} onChange={this.changeRemark} />
</div>
}
<div className='panel-block'> <div className='panel-block'>
<button className='button is-link is-outlined is-fullwidth' onClick={() => this.props.editFeedback()}> <button className='button is-link is-outlined is-fullwidth' onClick={() => this.props.editFeedback()}>
<span className='icon is-small'> <span className='icon is-small'>
...@@ -121,7 +126,7 @@ class FeedbackPanel extends React.Component { ...@@ -121,7 +126,7 @@ class FeedbackPanel extends React.Component {
<span>option</span> <span>option</span>
</button> </button>
</div> </div>
</nav> </React.Fragment>
) )
} }
} }
......
...@@ -11,6 +11,8 @@ import ExamEditor from './ExamEditor.jsx' ...@@ -11,6 +11,8 @@ import ExamEditor from './ExamEditor.jsx'
import update from 'immutability-helper' import update from 'immutability-helper'
import ExamFinalizeMarkdown from './ExamFinalize.md' import ExamFinalizeMarkdown from './ExamFinalize.md'
import ConfirmationModal from '../components/ConfirmationModal.jsx' import ConfirmationModal from '../components/ConfirmationModal.jsx'
import FeedbackPanel from '../components/feedback/FeedbackPanel.jsx'
import EditPanel from '../components/feedback/EditPanel.jsx'
import * as api from '../api.jsx' import * as api from '../api.jsx'
...@@ -18,6 +20,9 @@ class Exams extends React.Component { ...@@ -18,6 +20,9 @@ class Exams extends React.Component {
state = { state = {
examID: null, examID: null,
page: 0, page: 0,
editActive: false,
feedbackToEdit: null,
problemIdToEditFeedbackOf: null,
numPages: null, numPages: null,
selectedWidgetId: null, selectedWidgetId: null,
changedWidgetId: null, changedWidgetId: null,
...@@ -42,6 +47,7 @@ class Exams extends React.Component { ...@@ -42,6 +47,7 @@ class Exams extends React.Component {
page: problem.page, page: problem.page,
name: problem.name, name: problem.name,
graded: problem.graded, graded: problem.graded,
feedback: problem.feedback || [],
mc_options: problem.mc_options.map((option) => { mc_options: problem.mc_options.map((option) => {
option.cbOffsetX = 7 // checkbox offset relative to option position on x axis option.cbOffsetX = 7 // checkbox offset relative to option position on x axis
option.cbOffsetY = 21 // checkbox offset relative to option position on y axis option.cbOffsetY = 21 // checkbox offset relative to option position on y axis
...@@ -70,7 +76,6 @@ class Exams extends React.Component { ...@@ -70,7 +76,6 @@ class Exams extends React.Component {
previewing: false previewing: false
} }
} }
return null return null
} }
...@@ -82,6 +87,10 @@ class Exams extends React.Component { ...@@ -82,6 +87,10 @@ class Exams extends React.Component {
// The onBlur event is not fired when the input field is being disabled // The onBlur event is not fired when the input field is being disabled
if (prevState.selectedWidgetId !== this.state.selectedWidgetId) { if (prevState.selectedWidgetId !== this.state.selectedWidgetId) {
this.saveProblemName() this.saveProblemName()
this.setState({
editActive: false,
problemIdToEditFeedbackOf: false
})
} }
} }
...@@ -98,6 +107,43 @@ class Exams extends React.Component { ...@@ -98,6 +107,43 @@ class Exams extends React.Component {
} }
} }
editFeedback = (feedback) => {
this.setState({
editActive: true,
feedbackToEdit: feedback,
problemIdToEditFeedbackOf: this.state.selectedWidgetId
})
}
updateFeedback = (feedback) => {
let problemWidget = this.state.widgets[this.state.selectedWidgetId]
const index = problemWidget.problem.feedback.findIndex(e => { return e.id === feedback.id })
this.updateFeedbackAtIndex(feedback, problemWidget, index)
}
updateFeedbackAtIndex = (feedback, problemWidget, idx) => {
if (idx === -1) {
problemWidget.problem.feedback.push(feedback)
} else {
if (feedback.deleted) problemWidget.problem.feedback.splice(idx, 1)
else problemWidget.problem.feedback[idx] = feedback
}
this.setState({
widgets: this.state.widgets
})
}
backToFeedback = () => {
this.props.updateExam(this.props.exam.id)
this.setState({
editActive: false
})
}
isProblemWidget = (widget) => {
return widget && this.state.widgets[widget].problem
}
saveProblemName = () => { saveProblemName = () => {
const changedWidgetId = this.state.changedWidgetId const changedWidgetId = this.state.changedWidgetId
if (!changedWidgetId) return if (!changedWidgetId) return
...@@ -138,6 +184,8 @@ class Exams extends React.Component { ...@@ -138,6 +184,8 @@ class Exams extends React.Component {
selectedWidgetId: null, selectedWidgetId: null,
changedWidgetId: null, changedWidgetId: null,
deletingWidget: false, deletingWidget: false,
editActive: false,
problemIdToEditFeedbackOf: null,
widgets: update(prevState.widgets, { widgets: update(prevState.widgets, {
$unset: [widgetId] $unset: [widgetId]
}) })
...@@ -275,11 +323,16 @@ class Exams extends React.Component { ...@@ -275,11 +323,16 @@ class Exams extends React.Component {
// update to try and get a consistent state // update to try and get a consistent state
this.props.updateExam(this.props.examID) this.props.updateExam(this.props.examID)
}) })
}).then(res => {
let index = widget.problem.feedback.findIndex(e => { return e.id === res.feedback_id })
let feedback = widget.problem.feedback[index]
feedback.deleted = true
this.updateFeedbackAtIndex(feedback, widget, index)
}) })
}) })
// remove the mc options from the state // remove the mc options from the state
// note that his can happen before they are removed in the DB due to async calls // note that this can happen before they are removed in the DB due to async calls
this.setState((prevState) => { this.setState((prevState) => {
return { return {
widgets: update(prevState.widgets, { widgets: update(prevState.widgets, {
...@@ -363,6 +416,12 @@ class Exams extends React.Component { ...@@ -363,6 +416,12 @@ class Exams extends React.Component {
generateAnswerBoxes = (problemWidget, labels, index, xPos, yPos) => { generateAnswerBoxes = (problemWidget, labels, index, xPos, yPos) => {
if (labels.length === index) return if (labels.length === index) return
let feedback = {
'name': labels[index],
'description': '',
'score': 0
}
let data = { let data = {
'label': labels[index], 'label': labels[index],
'problem_id': problemWidget.problem.id, 'problem_id': problemWidget.problem.id,
...@@ -383,9 +442,14 @@ class Exams extends React.Component { ...@@ -383,9 +442,14 @@ class Exams extends React.Component {
formData.append('y', data.widget.y + data.cbOffsetY) formData.append('y', data.widget.y + data.cbOffsetY)
formData.append('problem_id', data.problem_id) formData.append('problem_id', data.problem_id)
formData.append('label', data.label) formData.append('label', data.label)
formData.append('fb_description', feedback.description)
formData.append('fb_score', feedback.score)
api.put('mult-choice/', formData).then(result => { api.put('mult-choice/', formData).then(result => {
data.id = result.mult_choice_id data.id = result.mult_choice_id
data.feedback_id = result.feedback_id
feedback.id = result.feedback_id
this.createNewMCWidget(problemWidget, data) this.createNewMCWidget(problemWidget, data)
this.updateFeedback(feedback)
this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + problemWidget.problem.widthMCO, yPos) this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + problemWidget.problem.widthMCO, yPos)
}).catch(err => { }).catch(err => {
console.log(err) console.log(err)
...@@ -510,17 +574,29 @@ class Exams extends React.Component { ...@@ -510,17 +574,29 @@ class Exams extends React.Component {
</div> </div>
<div className='panel-block'> <div className='panel-block'>
<div className='field'> <div className='field'>
<label className='label'> <label className='label'> Multiple choice question </label>
<input disabled={props.disableIsMCQ} type='checkbox' checked={props.isMCQProblem} onChange={ <input disabled={props.disableIsMCQ} type='checkbox' checked={props.isMCQProblem} onChange={
(e) => { (e) => {
props.onMCQChange(e.target.checked) props.onMCQChange(e.target.checked)
}} /> }} />
Multiple choice question
</label>
</div> </div>
</div> </div>
</React.Fragment> </React.Fragment>
)} )}
{this.isProblemWidget(selectedWidgetId) &&
<React.Fragment>
<div className='panel-block'>
{!this.state.editActive && <label className='label'>Feedback options</label>}
</div>
{this.state.editActive
? <EditPanel problemID={props.problem.id} feedback={this.state.feedbackToEdit}
goBack={this.backToFeedback} updateCallback={this.updateFeedback} />
: <FeedbackPanel examID={this.props.examID} problem={props.problem}
editFeedback={this.editFeedback} showTooltips={this.state.showTooltips}
grading={false}
/>}
</React.Fragment>
}
<div className='panel-block'> <div className='panel-block'>
<button <button
disabled={props.disabledDelete} disabled={props.disabledDelete}
......
...@@ -88,6 +88,7 @@ class ExamEditor extends React.Component { ...@@ -88,6 +88,7 @@ class ExamEditor extends React.Component {
const problemData = { const problemData = {
name: 'New problem', // TODO: Name name: 'New problem', // TODO: Name
page: this.props.page, page: this.props.page,
feedback: [],
mc_options: [], mc_options: [],
widthMCO: 24, widthMCO: 24,
heightMCO: 38, heightMCO: 38,
......
...@@ -2,9 +2,9 @@ import React from 'react' ...@@ -2,9 +2,9 @@ import React from 'react'
import Hero from '../components/Hero.jsx' import Hero from '../components/Hero.jsx'
import FeedbackPanel from './grade/FeedbackPanel.jsx' import FeedbackPanel from '../components/feedback/FeedbackPanel.jsx'
import ProblemSelector from './grade/ProblemSelector.jsx' import ProblemSelector from './grade/ProblemSelector.jsx'
import EditPanel from './grade/EditPanel.jsx' import EditPanel from '../components/feedback/EditPanel.jsx'
import SearchBox from '../components/SearchBox.jsx' import SearchBox from '../components/SearchBox.jsx'
import ProgressBar from '../components/ProgressBar.jsx' import ProgressBar from '../components/ProgressBar.jsx'
import withShortcuts from '../components/ShortcutBinder.jsx' import withShortcuts from '../components/ShortcutBinder.jsx'
...@@ -225,17 +225,18 @@ class Grade extends React.Component { ...@@ -225,17 +225,18 @@ class Grade extends React.Component {
<div className='column is-one-quarter-desktop is-one-third-tablet'> <div className='column is-one-quarter-desktop is-one-third-tablet'>
<ProblemSelector problems={exam.problems} changeProblem={this.changeProblem} <ProblemSelector problems={exam.problems} changeProblem={this.changeProblem}
current={this.state.pIndex} showTooltips={this.state.showTooltips} /> current={this.state.pIndex} showTooltips={this.state.showTooltips} />
{this.state.editActive <nav className='panel'>
? <EditPanel problemID={problem.id} feedback={this.state.feedbackToEdit} {this.state.editActive
goBack={this.backToFeedback} /> ? <EditPanel problemID={problem.id} feedback={this.state.feedbackToEdit}
: <FeedbackPanel examID={exam.id} submissionID={submission.id} goBack={this.backToFeedback} />
problem={problem} solution={solution} graderID={this.props.graderID} : <FeedbackPanel examID={exam.id} submissionID={submission.id}
editFeedback={this.editFeedback} showTooltips={this.state.showTooltips} problem={problem} solution={solution} graderID={this.props.graderID}
updateSubmission={() => { editFeedback={this.editFeedback} showTooltips={this.state.showTooltips}
this.props.updateSubmission(this.state.sIndex) updateSubmission={() => {
} this.props.updateSubmission(this.state.sIndex)
} /> }} grading />
} }
</nav>
</div> </div>
<div className='column'> <div className='column'>
...@@ -275,9 +276,13 @@ class Grade extends React.Component { ...@@ -275,9 +276,13 @@ class Grade extends React.Component {
renderSuggestion={(submission) => { renderSuggestion={(submission) => {
const stud = submission.student const stud = submission.student
return ( return (
<div> <div className='flex-parent'>
<b>{`${stud.firstName} ${stud.lastName}`}</b> <b className='flex-child truncated'>
<i style={{float: 'right'}}>({stud.id})</i> {`${stud.firstName} ${stud.lastName}`}
</b>
<i className='flex-child fixed'>
({stud.id})
</i>
</div> </div>
) )
}} }}
......
.box.is-graded { .box.is-graded {
box-shadow: 0px 0px 6px #23d160, 0 0 0 1px rgba(10, 10, 10, 0.1); box-shadow: 0px 0px 6px #23d160, 0 0 0 1px rgba(10, 10, 10, 0.1);
} }
.flex-parent {
display: flex;
align-items: center;
}
.flex-child.truncated {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flex-child.fixed {
white-space: nowrap;
}
...@@ -34,6 +34,8 @@ class MultipleChoice(Resource): ...@@ -34,6 +34,8 @@ class MultipleChoice(Resource):
put_parser.add_argument('x', type=int, required=True) put_parser.add_argument('x', type=int, required=True)
put_parser.add_argument('y', type=int, required=True) put_parser.add_argument('y', type=int, required=True)
put_parser.add_argument('label', type=str, required=False) put_parser.add_argument('label', type=str, required=False)
put_parser.add_argument('fb_description', type=str, required=False)
put_parser.add_argument('fb_score', type=str, required=False)
put_parser.add_argument('problem_id', type=int, required=True) # Used for FeedbackOption put_parser.add_argument('problem_id', type=int, required=True) # Used for FeedbackOption
def put(self): def put(self):
...@@ -50,12 +52,15 @@ class MultipleChoice(Resource): ...@@ -50,12 +52,15 @@ class MultipleChoice(Resource):
x = args['x'] x = args['x']
y = args['y'] y = args['y']
label = args['label'] label = args['label']
fb_description = args['fb_description']
fb_score = args['fb_score']
problem_id = args['problem_id'] problem_id = args['problem_id']
mc_type = 'mcq_widget' mc_type = 'mcq_widget'
# Insert new empty feedback option that links to the same problem # Insert new empty feedback option that links to the same problem
new_feedback_option = FeedbackOption(problem_id=problem_id, text='') new_feedback_option = FeedbackOption(problem_id=problem_id, text=label,
description=fb_description, score=fb_score)
db.session.add(new_feedback_option) db.session.add(new_feedback_option)
db.session.commit() db.session.commit()
...@@ -162,5 +167,6 @@ class MultipleChoice(Resource): ...@@ -162,5 +167,6 @@ class MultipleChoice(Resource):
db.session.delete(mult_choice.feedback) db.session.delete(mult_choice.feedback)
db.session.commit() db.session.commit()
return dict(status=200, message=f'Multiple choice question with id {id} deleted.' return dict(status=200, mult_choice_id=id, feedback_id=mult_choice.feedback_id,
message=f'Multiple choice question with id {id} deleted.'
+ f'Feedback option with id {mult_choice.feedback_id} deleted.'), 200 + f'Feedback option with id {mult_choice.feedback_id} deleted.'), 200
...@@ -5,7 +5,7 @@ from sqlalchemy.orm.exc import NoResultFound ...@@ -5,7 +5,7 @@ from sqlalchemy.orm.exc import NoResultFound
import numpy as np import numpy as np
import pandas import pandas
from .database import Exam, Problem, Student from .database import Exam, Student
def solution_data(exam_id, student_id): def solution_data(exam_id, student_id):
......