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 {
}
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) {
const fb = nextProps.feedback
return {
id: fb.id,
name: fb.name,
description: fb.description,
score: fb.score
score: fb.score,
updateCallback: updateCallback
}
}
return null
return {updateCallback: updateCallback}
}
changeText = (event) => {
......@@ -84,10 +87,15 @@ class EditPanel extends React.Component {
if (this.state.id) {
fb.id = this.state.id
api.put(uri, fb)
.then(() => this.props.goBack())
.then(() => {
this.state.updateCallback(fb)
this.props.goBack()
})
} else {
api.post(uri, fb)
.then(() => {
.then((response) => {
// Response is the feedback option
this.state.updateCallback(response)
this.setState({
id: null,
name: '',
......@@ -101,14 +109,20 @@ class EditPanel extends React.Component {
deleteFeedback = () => {
if (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 () {
return (
<nav className='panel'>
<p className='panel-heading'>
<React.Fragment>
<p className={this.props.grading ? 'panel-heading' : 'panel-heading is-radiusless'}>
Manage feedback
</p>
......@@ -168,7 +182,7 @@ class EditPanel extends React.Component {
onCancel={() => { this.setState({deleting: false}) }}
/>
</div>
</nav>
</React.Fragment>
)
}
}
......
......@@ -32,7 +32,9 @@ class FeedbackBlock extends React.Component {
render () {
const shortcut = (this.props.index < 11 ? '' : 'shift + ') + this.props.index % 10
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'} : {}}
>
<span
......@@ -40,7 +42,9 @@ class FeedbackBlock extends React.Component {
? ' tooltip is-tooltip-active is-tooltip-left' : '')}
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 style={{ width: '80%' }}>
{this.props.feedback.name}
......
......@@ -4,7 +4,7 @@ import Notification from 'react-bulma-notification'
import * as api from '../../api.jsx'
import withShortcuts from '../../components/ShortcutBinder.jsx'
import withShortcuts from '../ShortcutBinder.jsx'
import FeedbackBlock from './FeedbackBlock.jsx'
class FeedbackPanel extends React.Component {
......@@ -35,7 +35,7 @@ class FeedbackPanel extends React.Component {
static getDerivedStateFromProps (nextProps, prevState) {
if (prevState.problemID !== nextProps.problem.id || prevState.submissionID !== nextProps.submissionID) {
return {
remark: nextProps.solution.remark,
remark: nextProps.grading && nextProps.solution.remark,
problemID: nextProps.problem.id,
submissionID: nextProps.submissionID,
selectedFeedbackIndex: null
......@@ -90,29 +90,34 @@ class FeedbackPanel extends React.Component {
const blockURI = this.props.examID + '/' + this.props.submissionID + '/' + this.props.problem.id
let totalScore = 0
for (let i = 0; i < this.props.solution.feedback.length; i++) {
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
if (this.props.grading) {
for (let i = 0; i < this.props.solution.feedback.length; i++) {
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 &&
this.props.problem.feedback[this.state.selectedFeedbackIndex].id
return (
<nav className='panel'>
<p className='panel-heading'>
Total:&nbsp;<b>{totalScore}</b>
</p>
<React.Fragment>
{this.props.grading &&
<p className='panel-heading'>
Total:&nbsp;<b>{totalScore}</b>
</p>}
{this.props.problem.feedback.map((feedback, index) =>
<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}
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} />
)}
<div className='panel-block'>
<textarea className='textarea' rows='2' placeholder='remark' value={this.state.remark} onBlur={this.saveRemark} onChange={this.changeRemark} />
</div>
{this.props.grading &&
<div className='panel-block'>
<textarea className='textarea' rows='2' placeholder='remark' value={this.state.remark} onBlur={this.saveRemark} onChange={this.changeRemark} />
</div>
}
<div className='panel-block'>
<button className='button is-link is-outlined is-fullwidth' onClick={() => this.props.editFeedback()}>
<span className='icon is-small'>
......@@ -121,7 +126,7 @@ class FeedbackPanel extends React.Component {
<span>option</span>
</button>
</div>
</nav>
</React.Fragment>
)
}
}
......
......@@ -11,6 +11,8 @@ import ExamEditor from './ExamEditor.jsx'
import update from 'immutability-helper'
import ExamFinalizeMarkdown from './ExamFinalize.md'
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'
......@@ -18,6 +20,9 @@ class Exams extends React.Component {
state = {
examID: null,
page: 0,
editActive: false,
feedbackToEdit: null,
problemIdToEditFeedbackOf: null,
numPages: null,
selectedWidgetId: null,
changedWidgetId: null,
......@@ -42,6 +47,7 @@ class Exams extends React.Component {
page: problem.page,
name: problem.name,
graded: problem.graded,
feedback: problem.feedback || [],
mc_options: problem.mc_options.map((option) => {
option.cbOffsetX = 7 // checkbox offset relative to option position on x axis
option.cbOffsetY = 21 // checkbox offset relative to option position on y axis
......@@ -70,7 +76,6 @@ class Exams extends React.Component {
previewing: false
}
}
return null
}
......@@ -82,6 +87,10 @@ class Exams extends React.Component {
// The onBlur event is not fired when the input field is being disabled
if (prevState.selectedWidgetId !== this.state.selectedWidgetId) {
this.saveProblemName()
this.setState({
editActive: false,
problemIdToEditFeedbackOf: false
})
}
}
......@@ -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 = () => {
const changedWidgetId = this.state.changedWidgetId
if (!changedWidgetId) return
......@@ -138,6 +184,8 @@ class Exams extends React.Component {
selectedWidgetId: null,
changedWidgetId: null,
deletingWidget: false,
editActive: false,
problemIdToEditFeedbackOf: null,
widgets: update(prevState.widgets, {
$unset: [widgetId]
})
......@@ -275,11 +323,16 @@ class Exams extends React.Component {
// update to try and get a consistent state
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
// 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) => {
return {
widgets: update(prevState.widgets, {
......@@ -363,6 +416,12 @@ class Exams extends React.Component {
generateAnswerBoxes = (problemWidget, labels, index, xPos, yPos) => {
if (labels.length === index) return
let feedback = {
'name': labels[index],
'description': '',
'score': 0
}
let data = {
'label': labels[index],
'problem_id': problemWidget.problem.id,
......@@ -383,9 +442,14 @@ class Exams extends React.Component {
formData.append('y', data.widget.y + data.cbOffsetY)
formData.append('problem_id', data.problem_id)
formData.append('label', data.label)
formData.append('fb_description', feedback.description)
formData.append('fb_score', feedback.score)
api.put('mult-choice/', formData).then(result => {
data.id = result.mult_choice_id
data.feedback_id = result.feedback_id
feedback.id = result.feedback_id
this.createNewMCWidget(problemWidget, data)
this.updateFeedback(feedback)
this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + problemWidget.problem.widthMCO, yPos)
}).catch(err => {
console.log(err)
......@@ -510,17 +574,29 @@ class Exams extends React.Component {
</div>
<div className='panel-block'>
<div className='field'>
<label className='label'>
<input disabled={props.disableIsMCQ} type='checkbox' checked={props.isMCQProblem} onChange={
(e) => {
props.onMCQChange(e.target.checked)
}} />
Multiple choice question
</label>
<label className='label'> Multiple choice question </label>
<input disabled={props.disableIsMCQ} type='checkbox' checked={props.isMCQProblem} onChange={
(e) => {
props.onMCQChange(e.target.checked)
}} />
</div>
</div>
</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'>
<button
disabled={props.disabledDelete}
......
......@@ -88,6 +88,7 @@ class ExamEditor extends React.Component {
const problemData = {
name: 'New problem', // TODO: Name
page: this.props.page,
feedback: [],
mc_options: [],
widthMCO: 24,
heightMCO: 38,
......
......@@ -2,9 +2,9 @@ import React from 'react'
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 EditPanel from './grade/EditPanel.jsx'
import EditPanel from '../components/feedback/EditPanel.jsx'
import SearchBox from '../components/SearchBox.jsx'
import ProgressBar from '../components/ProgressBar.jsx'
import withShortcuts from '../components/ShortcutBinder.jsx'
......@@ -225,17 +225,18 @@ class Grade extends React.Component {
<div className='column is-one-quarter-desktop is-one-third-tablet'>
<ProblemSelector problems={exam.problems} changeProblem={this.changeProblem}
current={this.state.pIndex} showTooltips={this.state.showTooltips} />
{this.state.editActive
? <EditPanel problemID={problem.id} feedback={this.state.feedbackToEdit}
goBack={this.backToFeedback} />
: <FeedbackPanel examID={exam.id} submissionID={submission.id}
problem={problem} solution={solution} graderID={this.props.graderID}
editFeedback={this.editFeedback} showTooltips={this.state.showTooltips}
updateSubmission={() => {
this.props.updateSubmission(this.state.sIndex)
}
} />
}
<nav className='panel'>
{this.state.editActive
? <EditPanel problemID={problem.id} feedback={this.state.feedbackToEdit}
goBack={this.backToFeedback} />
: <FeedbackPanel examID={exam.id} submissionID={submission.id}
problem={problem} solution={solution} graderID={this.props.graderID}
editFeedback={this.editFeedback} showTooltips={this.state.showTooltips}
updateSubmission={() => {
this.props.updateSubmission(this.state.sIndex)
}} grading />
}
</nav>
</div>
<div className='column'>
......@@ -275,9 +276,13 @@ class Grade extends React.Component {
renderSuggestion={(submission) => {
const stud = submission.student
return (
<div>
<b>{`${stud.firstName} ${stud.lastName}`}</b>
<i style={{float: 'right'}}>({stud.id})</i>
<div className='flex-parent'>
<b className='flex-child truncated'>
{`${stud.firstName} ${stud.lastName}`}
</b>
<i className='flex-child fixed'>
({stud.id})
</i>
</div>
)
}}
......
.box.is-graded {
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):
put_parser.add_argument('x', 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('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
def put(self):
......@@ -50,12 +52,15 @@ class MultipleChoice(Resource):
x = args['x']
y = args['y']
label = args['label']
fb_description = args['fb_description']
fb_score = args['fb_score']
problem_id = args['problem_id']
mc_type = 'mcq_widget'
# 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.commit()
......@@ -162,5 +167,6 @@ class MultipleChoice(Resource):
db.session.delete(mult_choice.feedback)
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
......@@ -5,7 +5,7 @@ from sqlalchemy.orm.exc import NoResultFound
import numpy as np
import pandas
from .database import Exam, Problem, Student
from .database import Exam, Student
def solution_data(exam_id, student_id):
......