Commit 4fc136e6 authored by Hugo Kerstens's avatar Hugo Kerstens

Merge branch 'master' into conda-environment

parents 38124651 d29ed2d6
......@@ -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>
)
}
}
......
......@@ -10,6 +10,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'
......@@ -17,6 +19,9 @@ class Exams extends React.Component {
state = {
examID: null,
page: 0,
editActive: false,
feedbackToEdit: null,
problemIdToEditFeedbackOf: null,
numPages: null,
selectedWidgetId: null,
changedWidgetId: null,
......@@ -38,7 +43,8 @@ class Exams extends React.Component {
id: problem.id,
page: problem.page,
name: problem.name,
graded: problem.graded
graded: problem.graded,
feedback: problem.feedback || []
}
}
})
......@@ -57,7 +63,6 @@ class Exams extends React.Component {
previewing: false
}
}
return null
}
......@@ -69,6 +74,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
})
}
}
......@@ -85,6 +94,38 @@ class Exams extends React.Component {
}
}
editFeedback = (feedback) => {
this.setState({
editActive: true,
feedbackToEdit: feedback,
problemIdToEditFeedbackOf: this.state.selectedWidgetId
})
}
updateFeedback = (feedback) => {
var widgets = this.state.widgets
const idx = widgets[this.state.selectedWidgetId].problem.feedback.findIndex(e => { return e.id === feedback.id })
if (idx === -1) widgets[this.state.selectedWidgetId].problem.feedback.push(feedback)
else {
if (feedback.deleted) widgets[this.state.selectedWidgetId].problem.feedback.splice(idx, 1)
else widgets[this.state.selectedWidgetId].problem.feedback[idx] = feedback
}
this.setState({
widgets: 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
......@@ -112,6 +153,8 @@ class Exams extends React.Component {
selectedWidgetId: null,
changedWidgetId: null,
deletingWidget: false,
editActive: false,
problemIdToEditFeedbackOf: null,
widgets: update(prevState.widgets, {
$unset: [widgetId]
})
......@@ -308,6 +351,20 @@ class Exams extends React.Component {
)}
</div>
</div>
{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}
......
......@@ -86,7 +86,8 @@ class ExamEditor extends React.Component {
if (selectionBox.width >= this.props.problemMinWidth && selectionBox.height >= this.props.problemMinHeight) {
const problemData = {
name: 'New problem', // TODO: Name
page: this.props.page
page: this.props.page,
feedback: []
}
const widgetData = {
x: Math.round(selectionBox.left),
......
......@@ -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'
......@@ -210,17 +210,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'>
......
......@@ -13,11 +13,11 @@ def test_exam_generate_token_length_uppercase(duplicate_count, monkeypatch):
self.duplicates = duplicate_count + 1
def filter(self, *args):
return self
return self
def first(self):
self.duplicates -= 1
return None if self.duplicates else True
self.duplicates -= 1
return None if self.duplicates else True
app = Flask(__name__, static_folder=None)
app.config.update(
......
......@@ -57,27 +57,27 @@ def render_email(exam_id, student_id, template):
def build_email(exam_id, student_id, template, attach, from_address, copy_to=None):
student = Student.query.get(student_id)
if student is None:
abort(
404,
message=f"Student #{student_id} does not exist"
)
if not student.email:
abort(
409,
message=f'Student #{student_id} has no email address'
)
return emails.build(
student.email,
render_email(exam_id, student_id, template),
emails.build_solution_attachment(exam_id, student_id)
if attach
else None,
copy_to=copy_to,
email_from=from_address,
student = Student.query.get(student_id)
if student is None:
abort(
404,
message=f"Student #{student_id} does not exist"
)
if not student.email:
abort(
409,
message=f'Student #{student_id} has no email address'
)
return emails.build(
student.email,
render_email(exam_id, student_id, template),
emails.build_solution_attachment(exam_id, student_id)
if attach
else None,
copy_to=copy_to,
email_from=from_address,
)
class EmailTemplate(Resource):
......
......@@ -103,7 +103,7 @@ def send(
server_type = smtplib.SMTP_SSL if use_ssl else smtplib.SMTP
with server_type(server, port) as s:
if user and password:
s.login(user, password)
s.login(user, password)
for identifier, message in messages.items():
recipients = [
*message['To'].split(','),
......
......@@ -68,7 +68,7 @@ def full_exam_data(exam_id):
if not data:
# No students were assigned.
columns = []
for problem in exam.problems.order_by(Problem.id):
for problem in exam.problems: # Sorted by problem.id
if not len(problem.feedback_options):
# There is no possible feedback for this problem.
continue
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment