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 (132)
Showing
with 486 additions and 325 deletions
# This base image can be found in 'Dockerfile'
image: zesje/base
image: gitlab.kwant-project.org:5005/zesje/zesje/test:latest
stages:
- build
......@@ -13,11 +13,13 @@ stages:
paths:
- .yarn-cache
before_script:
- source activate zesje-dev
- yarn install --cache-folder .yarn-cache
.python_packages: &python_packages
before_script:
- pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
- source activate zesje-dev
- conda env update
build:
<<: *node_modules
......
FROM archlinux/base
FROM continuumio/miniconda3
## Install packages and clear the cache after installation. Yarn is fixed at 1.6.0 untill 1.8.0 is released due to a critical bug.
RUN pacman -Sy --noconfirm nodejs python-pip git libdmtx libsm libxrender libxext gcc libmagick6 imagemagick ghostscript; \
pacman -U --noconfirm https://archive.archlinux.org/packages/y/yarn/yarn-1.6.0-1-any.pkg.tar.xz
RUN apt-get update -y && apt-get install -y libdmtx0a libmagickwand-dev
WORKDIR ~
ADD requirements*.txt ./
#ADD package.json .
RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt;
#RUN yarn install; \
# yarn cache clean; \
# rm package.json
RUN apt-get update && \
apt-get install -y \
curl \
poppler-utils build-essential libgl1-mesa-glx \
imagemagick libsm-dev libdmtx-dev libdmtx0a libmagickwand-dev \
&& \
apt-get -y --quiet install git supervisor nginx
CMD bash
\ No newline at end of file
WORKDIR /app
ADD environment.yml /app/environment.yml
RUN conda env create
# From https://medium.com/@chadlagore/conda-environments-with-docker-82cdc9d25754
RUN echo "source activate $(head -1 /app/environment.yml | cut -d' ' -f2)" > ~/.bashrc
ENV PATH /opt/conda/envs/$(head -1 /app/environment.yml | cut -d' ' -f2)/bin:$PATH
RUN rm /app/environment.yml
CMD bash
......@@ -15,10 +15,11 @@ Install Miniconda by following the instructions on this page:
https://conda.io/miniconda.html
Create a Conda environment that you will use for installing all
of zesje's dependencies:
Make sure you cloned this repository and enter its directory. Then
create a Conda environment that will automatically install all
of zesje's Python dependencies:
conda create -c conda-forge -n zesje-dev python=3.6 yarn
conda env create # Creates an environment from environment.yml
Then, *activate* the conda environment:
......@@ -31,10 +32,6 @@ Install all of the Javascript dependencies:
yarn install
Install all of the Python dependencies:
pip install -r requirements.txt -r requirements-dev.txt
Unfortunately there is also another dependency that must be installed
manually for now (we are working to bring this dependency into the
Conda ecosystem). You can install this dependency in the following way
......@@ -145,10 +142,10 @@ If you use Atom, install the [linter-js-standard-engine](https://atom.io/package
### Adding dependencies
#### Server-side
If you start using a new Python library, be sure to add it to `requirements.txt`. Python libraries for the testing are in `requirements-dev.txt`.
The packages can be installed and updated in your environment by `pip` using
If you start using a new Python library, be sure to add it to `environment.yml`.
The packages can be installed and updated in your environment by `conda` using
pip install -r requirements.txt -r requirements-dev.txt
conda env update
#### Client-side
......
import sys
import os
from io import BytesIO
from reportlab.pdfgen import canvas
import PIL
from wand.image import Image
from wand.color import Color
from pystrich.datamatrix import DataMatrixEncoder
sys.path.append(os.getcwd())
def generate_datamatrix(exam_id, page_num, copy_num):
data = f'{exam_id}/{copy_num:04d}/{page_num:02d}'
from zesje.pdf_generation import generate_datamatrix # noqa: E402
from zesje.database import token_length # noqa: E402
image_bytes = DataMatrixEncoder(data).get_imagedata(cellsize=2)
return PIL.Image.open(BytesIO(image_bytes))
exam_token = "A" * token_length
copy_num = 1559
page_num = 0
datamatrix = generate_datamatrix(0, 0, 0)
datamatrix_x = datamatrix_y = 0
fontsize = 8
margin = 3
fontsize = 12
datamatrix_x = 0
datamatrix_y = fontsize
datamatrix = generate_datamatrix(0, 0, 0000)
imagesize = (datamatrix.width, 3 + fontsize + datamatrix.height)
datamatrix = generate_datamatrix(exam_token, page_num, copy_num)
imagesize = (datamatrix.width, fontsize + datamatrix.height)
result_pdf = BytesIO()
canv = canvas.Canvas(result_pdf, pagesize=imagesize)
canv.drawInlineImage(datamatrix, 0, 3 + fontsize)
canv.drawInlineImage(datamatrix, datamatrix_x, datamatrix_y)
canv.setFont('Helvetica', fontsize)
canv.drawString(0, 3, f" # 1519")
canv.drawString(datamatrix_x, datamatrix_y - (fontsize * 0.66),
f" # {copy_num}")
canv.showPage()
canv.save()
......@@ -36,7 +39,7 @@ canv.save()
result_pdf.seek(0)
# From https://stackoverflow.com/questions/27826854/python-wand-convert-pdf-to-png-disable-transparent-alpha-channel
with Image(file=result_pdf, resolution=80) as img:
with Image(width=img.width, height=img.height, background=Color("white")) as bg:
with Image(file=result_pdf, resolution=72) as img:
with Image(width=imagesize[0], height=imagesize[1], background=Color("white")) as bg:
bg.composite(img, 0, 0)
bg.save(filename="client/components/barcode_example.png")
import React from 'react'
import Switch from 'react-bulma-switch/full'
import './PanelMCQ.css'
/**
* PanelMCQ is a component that allows the user to generate mcq options
* PanelMCQ is a component that allows the user to generate mc questions and options
*/
class PanelMCQ extends React.Component {
constructor (props) {
......@@ -9,23 +11,72 @@ class PanelMCQ extends React.Component {
this.onChangeNPA = this.onChangeNPA.bind(this)
this.onChangeLabelType = this.onChangeLabelType.bind(this)
this.generateLabels = this.generateLabels.bind(this)
this.updateNumberOptions = this.updateNumberOptions.bind(this)
this.state = {
chosenLabelType: 0,
chosenLabelType: 2,
nrPossibleAnswers: 2,
labelTypes: ['None', 'True/False', 'A, B, C ...', '1, 2, 3 ...']
}
}
// modify the state if the properties are changed
static getDerivedStateFromProps (newProps, prevState) {
// if another problem is selected, update the state and implicitly the contents of the inputs
if (prevState.problemId !== newProps.problem.id) {
let prob = newProps.problem
return {
problemId: prob.id,
nrPossibleAnswers: prob.mc_options.length || 2,
chosenLabelType: PanelMCQ.deriveLabelType(prob.mc_options)
}
}
return null
}
/**
* Derive the label type given an array of options.
* @param options the options that correspond to a problem
* @returns {number} the index in the labelTypes array representing the label type
*/
static deriveLabelType (options) {
if (options.length === 0) {
return 2
} else if (options.length === 2 && ((options[0].label === 'T' && options[1].label === 'F') ||
(options[0].label === 'F' && options[1].label === 'T'))) {
return 1
} else if (options[0].label.match(/[A-Z]/)) {
return 2
} else if (parseInt(options[0].label)) {
return 3
} else {
return 0
}
}
// this functions calculates
updateNumberOptions () {
let difference = this.state.nrPossibleAnswers - this.props.problem.mc_options.length
if (difference > 0) {
let startingAt = this.props.problem.mc_options.length
let labels = this.generateLabels(difference, startingAt)
return this.props.generateMCOs(labels)
} else if (difference < 0) {
return this.props.deleteMCOs(-difference)
}
}
// this function is called when the input is changed for the number of possible answers
onChangeNPA (e) {
let value = parseInt(e.target.value)
if (!isNaN(value)) {
if (!isNaN(value) && value <= this.props.totalNrAnswers) {
if (this.state.chosenLabelType === 1) {
value = 2
}
this.setState({
nrPossibleAnswers: value
})
}, this.updateNumberOptions)
}
}
......@@ -33,12 +84,22 @@ class PanelMCQ extends React.Component {
onChangeLabelType (e) {
let value = parseInt(e.target.value)
if (!isNaN(value)) {
this.setState({
chosenLabelType: value
})
// if the label type is True/False then reduce the number of mc options to 2
if (parseInt(value) === 1) {
this.setState({
nrPossibleAnswers: 2
nrPossibleAnswers: 2,
chosenLabelType: value
}, () => {
this.updateNumberOptions()
let labels = this.generateLabels(this.state.nrPossibleAnswers, 0)
this.props.updateLabels(labels)
})
} else {
this.setState({
chosenLabelType: value
}, () => {
let labels = this.generateLabels(this.state.nrPossibleAnswers, 0)
this.props.updateLabels(labels)
})
}
}
......@@ -47,18 +108,20 @@ class PanelMCQ extends React.Component {
/**
* This function generates an array with the labels for each option
* @param nrLabels the number of options that need to be generated
* @param startingAt at which number/character to start generating labels
* @returns {any[]|string[]|number[]}
*/
generateLabels (nrLabels) {
generateLabels (nrLabels, startingAt) {
let type = this.state.chosenLabelType
switch (type) {
case 1:
return ['T', 'F']
case 2:
return Array.from(Array(nrLabels).keys()).map((e) => String.fromCharCode(e + 65))
return Array.from(Array(nrLabels).keys()).map(
(e) => String.fromCharCode(e + 65 + startingAt))
case 3:
return Array.from(Array(nrLabels).keys()).map(e => e + 1)
return Array.from(Array(nrLabels).keys()).map(e => String(e + 1 + startingAt))
default:
return Array(nrLabels).fill(' ')
}
......@@ -70,34 +133,29 @@ class PanelMCQ extends React.Component {
*/
render () {
return (
<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 <= this.props.totalNrAnswers; i++) {
const optionElement = <option key={i} value={String(i)}>{i}</option>
optionList.push(optionElement)
}
return (<div className='select is-hovered is-fullwidth'>
<select value={this.state.nrPossibleAnswers} onChange={this.onChangeNPA}>{optionList}</select>
</div>)
}.bind(this)())}
</div>
</React.Fragment>
</div>
<React.Fragment>
<div className='panel-block mcq-block'>
<label className='label'> Multiple choice </label>
<Switch color='info' outlined value={this.props.problem.mc_options.length > 0} onChange={(e) => {
if (e.target.checked) {
let npa = this.state.nrPossibleAnswers
let labels = this.generateLabels(npa, 0)
this.props.generateMCOs(labels)
} else {
this.props.deleteMCOs(this.props.problem.mc_options.length)
}
}} />
</div>
<div className='panel-block'>
<div className='field'>
<React.Fragment>
<label className='label'>Answer boxes labels</label>
<div className='control'>
{ this.props.problem.mc_options.length > 0 ? (
<React.Fragment>
<div className='panel-block mcq-block'>
<div className='inline-mcq-edit'>
<label>#</label>
<input type='number' value={this.state.nrPossibleAnswers} min='1'
max={this.props.totalNrAnswers} className='input' onChange={this.onChangeNPA} />
</div>
<div className='inline-mcq-edit'>
<label>Labels</label>
<div className='select is-hovered is-fullwidth'>
{(function () {
var optionList = this.state.labelTypes.map(
......@@ -113,32 +171,10 @@ class PanelMCQ extends React.Component {
}.bind(this)())}
</div>
</div>
</React.Fragment>
</div>
</div>
<div className='panel-block field is-grouped'>
<button
disabled={this.props.disabledGenerateBoxes}
className='button is-link is-fullwidth'
onClick={() => {
let npa = this.state.nrPossibleAnswers
let labels = this.generateLabels(npa)
this.props.onGenerateBoxesClick(labels)
}}
>
Generate
</button>
<button
disabled={this.props.disabledDeleteBoxes}
className='button is-danger is-fullwidth'
onClick={() => {
this.props.onDeleteBoxesClick()
}}
>
Delete
</button>
</div>
</nav>
</div>
</React.Fragment>) : null
}
</React.Fragment>
)
}
}
......
.panel-block.mcq-block {
justify-content: space-between;
}
.mcq-block .inline-mcq-edit {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
}
.mcq-block .inline-mcq-edit label {
margin-right: 4px;
}
.mcq-block .inline-mcq-edit:first-of-type {
margin-right: 20px;
}
.mcq-block input, .mcq-block .select {
max-width: 130px;
}
\ No newline at end of file
client/components/barcode_example.png

383 B | W: 0px | H: 0px

client/components/barcode_example.png

453 B | W: 0px | H: 0px

client/components/barcode_example.png
client/components/barcode_example.png
client/components/barcode_example.png
client/components/barcode_example.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -2,6 +2,7 @@ import React from 'react'
import ConfirmationModal from '../../components/ConfirmationModal.jsx'
import * as api from '../../api.jsx'
import Notification from 'react-bulma-notification'
const BackButton = (props) => (
<button className='button is-light is-fullwidth' onClick={props.onClick}>
......@@ -116,6 +117,14 @@ class EditPanel extends React.Component {
})
this.props.goBack()
})
.catch(err => {
err.json().then(res => {
Notification.error('Could not delete feedback' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.goBack()
})
})
}
}
......
......@@ -18,18 +18,20 @@ class FeedbackPanel extends React.Component {
}
componentDidMount = () => {
this.props.bindShortcut(['up', 'k'], (event) => {
event.preventDefault()
this.prevOption()
})
this.props.bindShortcut(['down', 'j'], (event) => {
event.preventDefault()
this.nextOption()
})
this.props.bindShortcut(['space'], (event) => {
event.preventDefault()
this.toggleSelectedOption()
})
if (this.props.grading) {
this.props.bindShortcut(['up', 'k'], (event) => {
event.preventDefault()
this.prevOption()
})
this.props.bindShortcut(['down', 'j'], (event) => {
event.preventDefault()
this.nextOption()
})
this.props.bindShortcut(['space'], (event) => {
event.preventDefault()
this.toggleSelectedOption()
})
}
}
static getDerivedStateFromProps (nextProps, prevState) {
......@@ -111,7 +113,7 @@ class FeedbackPanel extends React.Component {
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} grading={this.props.grading}
selected={selectedFeedbackId === feedback.id} showIndex={this.props.showTooltips} index={index + 1} />
selected={selectedFeedbackId === feedback.id || feedback.highlight} showIndex={this.props.showTooltips} index={index + 1} />
)}
{this.props.grading &&
<div className='panel-block'>
......
......@@ -57,6 +57,10 @@ div.mcq-option img.mcq-box {
background-color: hsla(171, 100%, 41%, 0.2)
}
.widget.selected .mcq-option:hover {
background-color: rgb(25, 149, 216);
}
.editor-side-panel {
background: #fff;
margin: 0.75em;
......
......@@ -56,8 +56,7 @@ class Exams extends React.Component {
return option
}),
widthMCO: 24,
heightMCO: 38,
isMCQ: problem.mc_options && problem.mc_options.length !== 0 // is the problem a mc question - used to display PanelMCQ
heightMCO: 38
}
}
})
......@@ -122,14 +121,26 @@ class Exams extends React.Component {
}
updateFeedbackAtIndex = (feedback, problemWidget, idx) => {
let fb = [...problemWidget.problem.feedback]
if (idx === -1) {
problemWidget.problem.feedback.push(feedback)
fb.push(feedback)
} else {
if (feedback.deleted) problemWidget.problem.feedback.splice(idx, 1)
else problemWidget.problem.feedback[idx] = feedback
if (feedback.deleted) fb.splice(idx, 1)
else fb[idx] = feedback
}
this.setState({
widgets: this.state.widgets
this.setState(prevState => {
return {
widgets: update(prevState.widgets, {
[problemWidget.id]: {
'problem': {
'feedback': {
$set: fb
}
}
}
})
}
})
}
......@@ -140,10 +151,6 @@ class Exams extends React.Component {
})
}
isProblemWidget = (widget) => {
return widget && this.state.widgets[widget].problem
}
saveProblemName = () => {
const changedWidgetId = this.state.changedWidgetId
if (!changedWidgetId) return
......@@ -234,8 +241,20 @@ class Exams extends React.Component {
numPages={this.state.numPages}
onPDFLoad={this.onPDFLoad}
updateWidget={this.updateWidget}
updateMCWidget={this.updateMCWidget}
updateMCOsInState={this.updateMCOsInState}
selectedWidgetId={this.state.selectedWidgetId}
highlightFeedback={(widget, feedbackId) => {
let index = widget.problem.feedback.findIndex(e => { return e.id === feedbackId })
let feedback = widget.problem.feedback[index]
feedback.highlight = true
this.updateFeedbackAtIndex(feedback, widget, index)
}}
removeHighlight={(widget, feedbackId) => {
let index = widget.problem.feedback.findIndex(e => { return e.id === feedbackId })
let feedback = widget.problem.feedback[index]
feedback.highlight = false
this.updateFeedbackAtIndex(feedback, widget, index)
}}
selectWidget={(widgetId) => {
this.setState({
selectedWidgetId: widgetId
......@@ -304,58 +323,63 @@ class Exams extends React.Component {
}
/**
* This function deletes the mc options coupled to a problem.
* This method generates MC options by making the right calls to the api and creating
* the widget object in the mc_options array of the corresponding problem.
* @param problemWidget the problem widget the mc options belong to
* @param labels the labels for the options
* @param index the index in the labels array (the function is recusive, this index is increased)
* @param xPos x position of the current option
* @param yPos y position of the current option
*/
deleteMCWidget = () => {
const widget = this.state.widgets[this.state.selectedWidgetId]
const options = widget.problem.mc_options
if (options.length > 0) {
options.forEach((option) => {
api.del('mult-choice/' + option.id)
.catch(err => {
console.log(err)
err.json().then(res => {
this.setState({
deletingMCWidget: false
})
Notification.error('Could not delete multiple choice option' +
(res.message ? ': ' + res.message : ''))
// 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)
})
})
generateMCOs = (problemWidget, labels, index, xPos, yPos) => {
if (labels.length === index) return
// remove the mc options from the state
// note that this can happen before they are removed in the DB due to async calls
this.setState((prevState) => {
return {
widgets: update(prevState.widgets, {
[widget.id]: {
problem: {
mc_options: {
$set: []
}
}
}
}),
deletingMCWidget: false
}
})
let feedback = {
'name': labels[index],
'description': '',
'score': 0
}
let data = {
'label': labels[index],
'problem_id': problemWidget.problem.id,
'feedback_id': null,
'cbOffsetX': 7, // checkbox offset relative to option position on x axis
'cbOffsetY': 21, // checkbox offset relative to option position on y axis
'widget': {
'name': 'mc_option_' + labels[index],
'x': xPos,
'y': yPos,
'type': 'mcq_widget'
}
}
const formData = new window.FormData()
formData.append('name', data.widget.name)
formData.append('x', data.widget.x + data.cbOffsetX)
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.addMCOtoState(problemWidget, data)
this.updateFeedback(feedback)
this.generateMCOs(problemWidget, labels, index + 1, xPos + problemWidget.problem.widthMCO, yPos)
}).catch(err => {
console.log(err)
})
}
/**
* This method creates a mc option widget object and adds it to the corresponding problem
* This method creates a mc option widget object and adds it to the corresponding problem in the state
* @param problemWidget The widget the mc option belongs to
* @param data the mc option
*/
createNewMCWidget = (problemWidget, data) => {
addMCOtoState = (problemWidget, data) => {
this.setState((prevState) => {
return {
widgets: update(prevState.widgets, {
......@@ -371,13 +395,57 @@ class Exams extends React.Component {
})
}
/**
* This method deletes mc options coupled to a problem in both the state and the database.
* @param widgetId the id of the widget for which the mc options need to be deleted
* @param index the index of the first mc option to be removed
* @param nrMCOs the number of mc options to remove
* @returns {Promise<T | never>}
*/
deleteMCOs = (widgetId, index, nrMCOs) => {
let widget = this.state.widgets[widgetId]
if (nrMCOs <= 0 || !widget.problem.mc_options.length) return
let option = widget.problem.mc_options[index]
return api.del('mult-choice/' + option.id)
.catch(err => {
console.log(err)
err.json().then(res => {
Notification.error('Could not delete multiple choice option' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
})
})
.then(res => {
let feedback = widget.problem.feedback[index]
feedback.deleted = true
this.updateFeedback(feedback)
this.setState((prevState) => {
return {
widgets: update(prevState.widgets, {
[widget.id]: {
problem: {
mc_options: {
$splice: [[index, 1]]
}
}
}
})
}
}, () => {
this.deleteMCOs(widgetId, index, nrMCOs - 1)
})
})
}
/**
* This method is called when the mcq widget is moved. The positions of the options are stored separately and they
* all need to be updated
* all need to be updated in the state. This method does not update the positions of the mc options in the DB.
* @param widget the problem widget that includes the mcq widget
* @param data the new location of the mcq widget (the location of the top-left corner)
*/
updateMCWidget = (widget, data) => {
updateMCOsInState = (widget, data) => {
let newMCO = widget.problem.mc_options.map((option, i) => {
return {
'widget': {
......@@ -404,76 +472,19 @@ class Exams extends React.Component {
}))
}
/**
* This method generates MC options by making the right calls to the api and creating
* the widget object in the mc_options array of the corresponding problem.
* @param problemWidget the problem widget the mc options belong to
* @param labels the labels for the options
* @param index the index in the labels array (the function is recusive, this index is increased)
* @param xPos x position of the current option
* @param yPos y position of the current option
*/
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,
'feedback_id': null,
'cbOffsetX': 7, // checkbox offset relative to option position on x axis
'cbOffsetY': 21, // checkbox offset relative to option position on y axis
'widget': {
'name': 'mc_option_' + labels[index],
'x': xPos,
'y': yPos,
'type': 'mcq_widget'
}
}
const formData = new window.FormData()
formData.append('name', data.widget.name)
formData.append('x', data.widget.x + data.cbOffsetX)
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)
})
}
SidePanel = (props) => {
const selectedWidgetId = this.state.selectedWidgetId
let selectedWidget = selectedWidgetId && this.state.widgets[selectedWidgetId]
let problem = selectedWidget && selectedWidget.problem
let containsMCOptions = (problem && problem.mc_options.length > 0) || false
let widgetEditDisabled = (this.state.previewing || !problem) || (this.props.exam.finalized && containsMCOptions)
let widgetEditDisabled = (this.state.previewing || !problem) ||
(this.props.exam.finalized && problem.mc_options.length > 0)
let isGraded = problem && problem.graded
let widgetDeleteDisabled = widgetEditDisabled || isGraded
let totalNrAnswers = 12 // the upper limit for the nr of possible answer boxes
let disabledDeleteBoxes = !containsMCOptions
let isMCQ = (problem && problem.isMCQ) || false
let showPanelMCQ = isMCQ && !this.state.previewing && !this.props.exam.finalized
return (
<React.Fragment>
<this.PanelEdit
disabledEdit={widgetEditDisabled}
disableIsMCQ={widgetEditDisabled || containsMCOptions}
disabledDelete={widgetDeleteDisabled}
onDeleteClick={() => {
this.setState({deletingWidget: true})
......@@ -494,42 +505,7 @@ class Exams extends React.Component {
}))
}}
saveProblemName={this.saveProblemName}
isMCQProblem={isMCQ}
onMCQChange={
(checked) => {
this.setState(prevState => ({
changedWidgetId: selectedWidgetId,
widgets: update(prevState.widgets, {
[selectedWidgetId]: {
problem: {
isMCQ: {
$set: checked
}
}
}
})
}))
}
}
/>
{ showPanelMCQ ? (
<PanelMCQ
totalNrAnswers={totalNrAnswers}
disabledGenerateBoxes={containsMCOptions}
disabledDeleteBoxes={disabledDeleteBoxes}
problem={problem}
onGenerateBoxesClick={(labels) => {
let problemWidget = this.state.widgets[this.state.selectedWidgetId]
// position the new mc option widget inside the problem widget
let xPos = problemWidget.x + 2
let yPos = problemWidget.y + 2
this.generateAnswerBoxes(problemWidget, labels, 0, xPos, yPos)
}}
onDeleteBoxesClick={() => {
this.setState({deletingMCWidget: true})
}}
/>
) : null }
<this.PanelExamActions />
</React.Fragment>
)
......@@ -537,6 +513,7 @@ class Exams extends React.Component {
PanelEdit = (props) => {
const selectedWidgetId = this.state.selectedWidgetId
let totalNrAnswers = 9 // the upper limit for the nr of possible answer boxes
return (
<nav className='panel'>
......@@ -572,18 +549,77 @@ class Exams extends React.Component {
</div>
</div>
</div>
<div className='panel-block'>
<div className='field'>
<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>
{props.problem ? (
<PanelMCQ
totalNrAnswers={totalNrAnswers}
problem={props.problem}
generateMCOs={(labels) => {
let problemWidget = this.state.widgets[this.state.selectedWidgetId]
let xPos, yPos
if (props.problem.mc_options.length > 0) {
// position the new mc options widget next to the last mc options
let last = props.problem.mc_options[props.problem.mc_options.length - 1].widget
xPos = last.x + problemWidget.problem.widthMCO
yPos = last.y
} else {
// position the new mc option widget inside the problem widget
xPos = problemWidget.x + 2
yPos = problemWidget.y + 2
}
this.generateMCOs(problemWidget, labels, 0, xPos, yPos)
}}
deleteMCOs={(nrMCOs) => {
let len = props.problem.mc_options.length
if (nrMCOs >= len) {
this.setState({deletingMCWidget: true})
} else if (nrMCOs > 0) {
this.deleteMCOs(selectedWidgetId, len - nrMCOs, nrMCOs)
}
}}
updateLabels={(labels) => {
labels.map((label, index) => {
let option = props.problem.mc_options[index]
const formData = new window.FormData()
formData.append('name', option.widget.name)
formData.append('x', option.widget.x + option.cbOffsetX)
formData.append('y', option.widget.y + option.cbOffsetY)
formData.append('problem_id', props.problem.id)
formData.append('label', labels[index])
api.patch('mult-choice/' + option.id, formData)
.then(() => {
this.setState((prevState) => {
return {
widgets: update(prevState.widgets, {
[selectedWidgetId]: {
'problem': {
'mc_options': {
[index]: {
label: {
$set: labels[index]
}
}
}
}
}
})
}
})
})
.catch(err => {
console.log(err)
err.json().then(res => {
Notification.error('Could not update feedback' +
(res.message ? ': ' + res.message : ''))
// update to try and get a consistent state
this.props.updateExam(this.props.examID)
})
})
})
}}
/>) : null}
</React.Fragment>
)}
{this.isProblemWidget(selectedWidgetId) &&
{props.problem &&
<React.Fragment>
<div className='panel-block'>
{!this.state.editActive && <label className='label'>Feedback options</label>}
......@@ -751,7 +787,12 @@ class Exams extends React.Component {
this.state.widgets[this.state.selectedWidgetId].problem.name}"`}
confirmText='Delete multiple choice options'
onCancel={() => this.setState({deletingMCWidget: false})}
onConfirm={() => this.deleteMCWidget(this.state.selectedWidgetId)}
onConfirm={() => {
this.setState({
deletingMCWidget: false
})
this.deleteMCOs(this.state.selectedWidgetId, 0)
}}
/>
</div>
}
......
......@@ -112,6 +112,7 @@ class ExamEditor extends React.Component {
api.post('problems', formData).then(result => {
widgetData.id = result.widget_id
problemData.id = result.id
problemData.name = result.problem_name
widgetData.problem = problemData
this.props.createNewWidget(widgetData)
......@@ -198,7 +199,7 @@ class ExamEditor extends React.Component {
*/
updateMCO = (widget, data) => {
// update state
this.props.updateMCWidget(widget, {
this.props.updateMCOsInState(widget, {
x: Math.round(data.x),
y: Math.round(data.y)
})
......@@ -246,7 +247,7 @@ class ExamEditor extends React.Component {
let changed = (oldX !== newX) || (oldY !== newY) // update the state only if the mc options were moved
if (changed) {
this.props.updateMCWidget(widget, {
this.props.updateMCOsInState(widget, {
x: Math.round(newX),
y: Math.round(newY)
})
......@@ -301,7 +302,14 @@ class ExamEditor extends React.Component {
<div className={isSelected ? 'mcq-widget widget selected' : 'mcq-widget widget '}>
{widget.problem.mc_options.map((option) => {
return (
<div key={'widget_mco_' + option.id} className='mcq-option'>
<div key={'widget_mco_' + option.id} className='mcq-option'
onMouseEnter={() => {
this.props.highlightFeedback(widget, option.feedback_id)
}}
onMouseLeave={() => {
this.props.removeHighlight(widget, option.feedback_id)
}}
>
<div className='mcq-option-label'>
{option.label}
</div>
......
File deleted
name: zesje-dev
channels:
- conda-forge
- anaconda
dependencies:
- python=3.6
- yarn
- gunicorn
- redis
- pip
- pip:
# Core components
- flask
- flask_restful
- flask_sqlalchemy
- sqlalchemy
- Flask-Migrate
- alembic
- pyyaml
- celery
- redis
# General utilities
- numpy==1.15.4
- scipy==1.3.0
# summary plot generation
- matplotlib
- seaborn
# PDF generation
- pdfrw
- reportlab
- Wand
- Pillow # also scan processing
# Scan processing
- opencv-python
- pikepdf
- pylibdmtx
- pdfminer3
# Exporting
- pandas
- openpyxl # required for writing dataframes as Excel spreadsheets
#
# Development dependencies
#
# Tests
- pytest
- pyssim
- pytest-cov
# Linting
- flake8
......@@ -3,7 +3,7 @@
"main": "index.js",
"license": "AGPL-3.0",
"scripts": {
"dev": "concurrently --kill-others --names \"WEBPACK,PYTHON,CELERY\" --prefix-colors \"bgBlue.bold,bgGreen.bold,bgRed.bold\" \"webpack-dev-server --hot --inline --progress --config webpack.dev.js\" \"ZESJE_SETTINGS=$(pwd)/zesje.dev.cfg python3 zesje\" \"ZESJE_SETTINGS=$(pwd)/zesje.dev.cfg celery -A zesje.celery worker -l info --autoscale=4,1 --max-tasks-per-child=16\"",
"dev": "concurrently --kill-others --names \"WEBPACK,PYTHON,CELERY,REDIS\" --prefix-colors \"bgBlue.bold,bgGreen.bold,bgRed.bold,bgYellow.bold\" \"webpack-dev-server --hot --inline --progress --config webpack.dev.js\" \"ZESJE_SETTINGS=$(pwd)/zesje.dev.cfg python3 zesje\" \"ZESJE_SETTINGS=$(pwd)/zesje.dev.cfg celery -A zesje.celery worker -l info --autoscale=4,1 --max-tasks-per-child=16\" \"redis-server redis.conf\"",
"build": "webpack --config webpack.prod.js",
"ci": "yarn lint; yarn test",
"lint": "yarn lint:js; yarn lint:py",
......@@ -33,6 +33,7 @@
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"bulma": "^0.7.1",
"bulma-switch": "^2.0.0",
"bulma-tooltip": "^2.0.1",
"concurrently": "^3.6.0",
"css-loader": "^1.0.0",
......@@ -52,6 +53,7 @@
"react": "^16.4.0",
"react-autosuggest": "^9.3.4",
"react-bulma-notification": "^1.1.0",
"react-bulma-switch": "^0.0.3",
"react-dom": "^16.4.0",
"react-dropzone": "^4.2.13",
"react-hot-loader": "^4.3.3",
......
port 6479
loglevel notice
\ No newline at end of file
# Tests
pytest
pyssim
pytest-cov
# Linting
flake8
# Core components
flask
flask_restful
flask_sqlalchemy
sqlalchemy
Flask-Migrate
alembic
pyyaml
celery
redis
# General utilities
numpy
scipy
# summary plot generation
matplotlib
seaborn
# PDF generation
pdfrw
reportlab
Wand
Pillow # also scan processing
pyStrich # TODO: can we replace this with stuff from pylibdmtx?
# Scan processing
opencv-python
git+https://github.com/mstamy2/PyPDF2
pylibdmtx
# Exporting
pandas
openpyxl # required for writing dataframes as Excel spreadsheets
import os
import pytest
from flask import Flask
from zesje.api import api_bp
from zesje.database import db
......@@ -40,3 +39,12 @@ def test_client(app):
with app.app_context():
db.drop_all()
db.create_all()
@pytest.fixture
def empty_app(app):
with app.app_context():
db.drop_all()
db.create_all()
return app
File added