36 files + 946 − 522 Side-by-side Compare changes Side-by-side Inline Show whitespace changes Files 36 .gitlab-ci.yml +4 −2 Original line number Original line Diff line number Diff line # This base image can be found in 'Dockerfile' # This base image can be found in 'Dockerfile' image: zesje/base image: gitlab.kwant-project.org:5005/zesje/zesje/test:latest stages: stages: - build - build Loading @@ -13,11 +13,13 @@ stages: paths: paths: - .yarn-cache - .yarn-cache before_script: before_script: - source activate zesje-dev - yarn install --cache-folder .yarn-cache - yarn install --cache-folder .yarn-cache .python_packages: &python_packages .python_packages: &python_packages before_script: before_script: - pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt - source activate zesje-dev - conda env update build: build: <<: *node_modules <<: *node_modules Loading Dockerfile +21 −12 Original line number Original line Diff line number Diff line 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 apt-get update -y && apt-get install -y libdmtx0a libmagickwand-dev 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 && \ apt-get install -y \ WORKDIR ~ curl \ ADD requirements*.txt ./ poppler-utils build-essential libgl1-mesa-glx \ #ADD package.json . imagemagick libsm-dev libdmtx-dev libdmtx0a libmagickwand-dev \ RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt; && \ #RUN yarn install; \ apt-get -y --quiet install git supervisor nginx # yarn cache clean; \ # rm package.json 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 CMD bash README.md +7 −10 Original line number Original line Diff line number Diff line Loading @@ -15,10 +15,11 @@ Install Miniconda by following the instructions on this page: https://conda.io/miniconda.html https://conda.io/miniconda.html Create a Conda environment that you will use for installing all Make sure you cloned this repository and enter its directory. Then of zesje's dependencies: 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: Then, *activate* the conda environment: Loading @@ -31,10 +32,6 @@ Install all of the Javascript dependencies: yarn install 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 Unfortunately there is also another dependency that must be installed manually for now (we are working to bring this dependency into the manually for now (we are working to bring this dependency into the Conda ecosystem). You can install this dependency in the following way Conda ecosystem). You can install this dependency in the following way Loading Loading @@ -145,10 +142,10 @@ If you use Atom, install the [linter-js-standard-engine](https://atom.io/package ### Adding dependencies ### Adding dependencies #### Server-side #### 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`. 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 `pip` using 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 #### Client-side Loading barcode_example_generator.py +19 −16 Original line number Original line Diff line number Diff line import sys import os from io import BytesIO from io import BytesIO from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas import PIL from wand.image import Image from wand.image import Image from wand.color import Color from wand.color import Color from pystrich.datamatrix import DataMatrixEncoder sys.path.append(os.getcwd()) def generate_datamatrix(exam_id, page_num, copy_num): from zesje.pdf_generation import generate_datamatrix # noqa: E402 data = f'{exam_id}/{copy_num:04d}/{page_num:02d}' 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) fontsize = 12 datamatrix_x = datamatrix_y = 0 datamatrix_x = 0 fontsize = 8 datamatrix_y = fontsize margin = 3 datamatrix = generate_datamatrix(0, 0, 0000) datamatrix = generate_datamatrix(exam_token, page_num, copy_num) imagesize = (datamatrix.width, 3 + fontsize + datamatrix.height) imagesize = (datamatrix.width, fontsize + datamatrix.height) result_pdf = BytesIO() result_pdf = BytesIO() canv = canvas.Canvas(result_pdf, pagesize=imagesize) 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.setFont('Helvetica', fontsize) canv.drawString(0, 3, f" # 1519") canv.drawString(datamatrix_x, datamatrix_y - (fontsize * 0.66), f" # {copy_num}") canv.showPage() canv.showPage() canv.save() canv.save() Loading @@ -36,7 +39,7 @@ canv.save() result_pdf.seek(0) result_pdf.seek(0) # From https://stackoverflow.com/questions/27826854/python-wand-convert-pdf-to-png-disable-transparent-alpha-channel # 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(file=result_pdf, resolution=72) as img: with Image(width=img.width, height=img.height, background=Color("white")) as bg: with Image(width=imagesize[0], height=imagesize[1], background=Color("white")) as bg: bg.composite(img, 0, 0) bg.composite(img, 0, 0) bg.save(filename="client/components/barcode_example.png") bg.save(filename="client/components/barcode_example.png") client/components/PaneMCQ.jsx +100 −64 Original line number Original line Diff line number Diff line import React from 'react' 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 { class PanelMCQ extends React.Component { constructor (props) { constructor (props) { Loading @@ -9,23 +11,72 @@ class PanelMCQ extends React.Component { this.onChangeNPA = this.onChangeNPA.bind(this) this.onChangeNPA = this.onChangeNPA.bind(this) this.onChangeLabelType = this.onChangeLabelType.bind(this) this.onChangeLabelType = this.onChangeLabelType.bind(this) this.generateLabels = this.generateLabels.bind(this) this.generateLabels = this.generateLabels.bind(this) this.updateNumberOptions = this.updateNumberOptions.bind(this) this.state = { this.state = { chosenLabelType: 0, chosenLabelType: 2, nrPossibleAnswers: 2, nrPossibleAnswers: 2, labelTypes: ['None', 'True/False', 'A, B, C ...', '1, 2, 3 ...'] 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 // this function is called when the input is changed for the number of possible answers onChangeNPA (e) { onChangeNPA (e) { let value = parseInt(e.target.value) let value = parseInt(e.target.value) if (!isNaN(value)) { if (!isNaN(value) && value <= this.props.totalNrAnswers) { if (this.state.chosenLabelType === 1) { if (this.state.chosenLabelType === 1) { value = 2 value = 2 } } this.setState({ this.setState({ nrPossibleAnswers: value nrPossibleAnswers: value }) }, this.updateNumberOptions) } } } } Loading @@ -33,12 +84,22 @@ class PanelMCQ extends React.Component { onChangeLabelType (e) { onChangeLabelType (e) { let value = parseInt(e.target.value) let value = parseInt(e.target.value) if (!isNaN(value)) { if (!isNaN(value)) { // if the label type is True/False then reduce the number of mc options to 2 if (parseInt(value) === 1) { this.setState({ this.setState({ nrPossibleAnswers: 2, chosenLabelType: value chosenLabelType: value }, () => { this.updateNumberOptions() let labels = this.generateLabels(this.state.nrPossibleAnswers, 0) this.props.updateLabels(labels) }) }) if (parseInt(value) === 1) { } else { this.setState({ this.setState({ nrPossibleAnswers: 2 chosenLabelType: value }, () => { let labels = this.generateLabels(this.state.nrPossibleAnswers, 0) this.props.updateLabels(labels) }) }) } } } } Loading @@ -47,18 +108,20 @@ class PanelMCQ extends React.Component { /** /** * This function generates an array with the labels for each option * This function generates an array with the labels for each option * @param nrLabels the number of options that need to be generated * @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[]} * @returns {any[]|string[]|number[]} */ */ generateLabels (nrLabels) { generateLabels (nrLabels, startingAt) { let type = this.state.chosenLabelType let type = this.state.chosenLabelType switch (type) { switch (type) { case 1: case 1: return ['T', 'F'] return ['T', 'F'] case 2: 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: 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: default: return Array(nrLabels).fill(' ') return Array(nrLabels).fill(' ') } } Loading @@ -70,34 +133,29 @@ class PanelMCQ extends React.Component { */ */ render () { render () { return ( return ( <nav className='panel'> <p className='panel-heading'> Multiple Choice Question </p> <div className='panel-block'> <div className='field'> <React.Fragment> <React.Fragment> <label className='label'>Number possible answers</label> <div className='panel-block mcq-block'> <div className='control'> <label className='label'> Multiple choice </label> {(function () { <Switch color='info' outlined value={this.props.problem.mc_options.length > 0} onChange={(e) => { var optionList = [] if (e.target.checked) { for (var i = 1; i <= this.props.totalNrAnswers; i++) { let npa = this.state.nrPossibleAnswers const optionElement = <option key={i} value={String(i)}>{i}</option> let labels = this.generateLabels(npa, 0) optionList.push(optionElement) this.props.generateMCOs(labels) } } else { return (<div className='select is-hovered is-fullwidth'> this.props.deleteMCOs(this.props.problem.mc_options.length) <select value={this.state.nrPossibleAnswers} onChange={this.onChangeNPA}>{optionList}</select> } </div>) }} /> }.bind(this)())} </div> </React.Fragment> </div> </div> </div> <div className='panel-block'> { this.props.problem.mc_options.length > 0 ? ( <div className='field'> <React.Fragment> <React.Fragment> <label className='label'>Answer boxes labels</label> <div className='panel-block mcq-block'> <div className='control'> <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'> <div className='select is-hovered is-fullwidth'> {(function () { {(function () { var optionList = this.state.labelTypes.map( var optionList = this.state.labelTypes.map( Loading @@ -113,32 +171,10 @@ class PanelMCQ extends React.Component { }.bind(this)())} }.bind(this)())} </div> </div> </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> </div> </nav> </React.Fragment>) : null } </React.Fragment> ) ) } } } } Loading client/components/PanelMCQ.css 0 → 100644 +22 −0 Original line number Original line Diff line number Diff line .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 +70 B (453 B) 383 B 453 B 2-up Swipe Onion skin client/components/feedback/EditPanel.jsx +9 −0 Original line number Original line Diff line number Diff line Loading @@ -2,6 +2,7 @@ import React from 'react' import ConfirmationModal from '../../components/ConfirmationModal.jsx' import ConfirmationModal from '../../components/ConfirmationModal.jsx' import * as api from '../../api.jsx' import * as api from '../../api.jsx' import Notification from 'react-bulma-notification' const BackButton = (props) => ( const BackButton = (props) => ( <button className='button is-light is-fullwidth' onClick={props.onClick}> <button className='button is-light is-fullwidth' onClick={props.onClick}> Loading Loading @@ -116,6 +117,14 @@ class EditPanel extends React.Component { }) }) this.props.goBack() 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() }) }) } } } } Loading client/components/feedback/FeedbackPanel.jsx +15 −13 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ class FeedbackPanel extends React.Component { } } componentDidMount = () => { componentDidMount = () => { if (this.props.grading) { this.props.bindShortcut(['up', 'k'], (event) => { this.props.bindShortcut(['up', 'k'], (event) => { event.preventDefault() event.preventDefault() this.prevOption() this.prevOption() Loading @@ -31,6 +32,7 @@ class FeedbackPanel extends React.Component { this.toggleSelectedOption() this.toggleSelectedOption() }) }) } } } 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) { Loading Loading @@ -111,7 +113,7 @@ class FeedbackPanel extends React.Component { feedback={feedback} checked={this.props.grading && 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} grading={this.props.grading} 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 && {this.props.grading && <div className='panel-block'> <div className='panel-block'> Loading client/views/Exam.css +4 −0 Original line number Original line Diff line number Diff line Loading @@ -57,6 +57,10 @@ div.mcq-option img.mcq-box { background-color: hsla(171, 100%, 41%, 0.2) background-color: hsla(171, 100%, 41%, 0.2) } } .widget.selected .mcq-option:hover { background-color: rgb(25, 149, 216); } .editor-side-panel { .editor-side-panel { background: #fff; background: #fff; margin: 0.75em; margin: 0.75em; Loading
.gitlab-ci.yml +4 −2 Original line number Original line Diff line number Diff line # This base image can be found in 'Dockerfile' # This base image can be found in 'Dockerfile' image: zesje/base image: gitlab.kwant-project.org:5005/zesje/zesje/test:latest stages: stages: - build - build Loading @@ -13,11 +13,13 @@ stages: paths: paths: - .yarn-cache - .yarn-cache before_script: before_script: - source activate zesje-dev - yarn install --cache-folder .yarn-cache - yarn install --cache-folder .yarn-cache .python_packages: &python_packages .python_packages: &python_packages before_script: before_script: - pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt - source activate zesje-dev - conda env update build: build: <<: *node_modules <<: *node_modules Loading
Dockerfile +21 −12 Original line number Original line Diff line number Diff line 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 apt-get update -y && apt-get install -y libdmtx0a libmagickwand-dev 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 && \ apt-get install -y \ WORKDIR ~ curl \ ADD requirements*.txt ./ poppler-utils build-essential libgl1-mesa-glx \ #ADD package.json . imagemagick libsm-dev libdmtx-dev libdmtx0a libmagickwand-dev \ RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt; && \ #RUN yarn install; \ apt-get -y --quiet install git supervisor nginx # yarn cache clean; \ # rm package.json 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 CMD bash
README.md +7 −10 Original line number Original line Diff line number Diff line Loading @@ -15,10 +15,11 @@ Install Miniconda by following the instructions on this page: https://conda.io/miniconda.html https://conda.io/miniconda.html Create a Conda environment that you will use for installing all Make sure you cloned this repository and enter its directory. Then of zesje's dependencies: 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: Then, *activate* the conda environment: Loading @@ -31,10 +32,6 @@ Install all of the Javascript dependencies: yarn install 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 Unfortunately there is also another dependency that must be installed manually for now (we are working to bring this dependency into the manually for now (we are working to bring this dependency into the Conda ecosystem). You can install this dependency in the following way Conda ecosystem). You can install this dependency in the following way Loading Loading @@ -145,10 +142,10 @@ If you use Atom, install the [linter-js-standard-engine](https://atom.io/package ### Adding dependencies ### Adding dependencies #### Server-side #### 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`. 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 `pip` using 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 #### Client-side Loading
barcode_example_generator.py +19 −16 Original line number Original line Diff line number Diff line import sys import os from io import BytesIO from io import BytesIO from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas import PIL from wand.image import Image from wand.image import Image from wand.color import Color from wand.color import Color from pystrich.datamatrix import DataMatrixEncoder sys.path.append(os.getcwd()) def generate_datamatrix(exam_id, page_num, copy_num): from zesje.pdf_generation import generate_datamatrix # noqa: E402 data = f'{exam_id}/{copy_num:04d}/{page_num:02d}' 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) fontsize = 12 datamatrix_x = datamatrix_y = 0 datamatrix_x = 0 fontsize = 8 datamatrix_y = fontsize margin = 3 datamatrix = generate_datamatrix(0, 0, 0000) datamatrix = generate_datamatrix(exam_token, page_num, copy_num) imagesize = (datamatrix.width, 3 + fontsize + datamatrix.height) imagesize = (datamatrix.width, fontsize + datamatrix.height) result_pdf = BytesIO() result_pdf = BytesIO() canv = canvas.Canvas(result_pdf, pagesize=imagesize) 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.setFont('Helvetica', fontsize) canv.drawString(0, 3, f" # 1519") canv.drawString(datamatrix_x, datamatrix_y - (fontsize * 0.66), f" # {copy_num}") canv.showPage() canv.showPage() canv.save() canv.save() Loading @@ -36,7 +39,7 @@ canv.save() result_pdf.seek(0) result_pdf.seek(0) # From https://stackoverflow.com/questions/27826854/python-wand-convert-pdf-to-png-disable-transparent-alpha-channel # 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(file=result_pdf, resolution=72) as img: with Image(width=img.width, height=img.height, background=Color("white")) as bg: with Image(width=imagesize[0], height=imagesize[1], background=Color("white")) as bg: bg.composite(img, 0, 0) bg.composite(img, 0, 0) bg.save(filename="client/components/barcode_example.png") bg.save(filename="client/components/barcode_example.png")
client/components/PaneMCQ.jsx +100 −64 Original line number Original line Diff line number Diff line import React from 'react' 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 { class PanelMCQ extends React.Component { constructor (props) { constructor (props) { Loading @@ -9,23 +11,72 @@ class PanelMCQ extends React.Component { this.onChangeNPA = this.onChangeNPA.bind(this) this.onChangeNPA = this.onChangeNPA.bind(this) this.onChangeLabelType = this.onChangeLabelType.bind(this) this.onChangeLabelType = this.onChangeLabelType.bind(this) this.generateLabels = this.generateLabels.bind(this) this.generateLabels = this.generateLabels.bind(this) this.updateNumberOptions = this.updateNumberOptions.bind(this) this.state = { this.state = { chosenLabelType: 0, chosenLabelType: 2, nrPossibleAnswers: 2, nrPossibleAnswers: 2, labelTypes: ['None', 'True/False', 'A, B, C ...', '1, 2, 3 ...'] 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 // this function is called when the input is changed for the number of possible answers onChangeNPA (e) { onChangeNPA (e) { let value = parseInt(e.target.value) let value = parseInt(e.target.value) if (!isNaN(value)) { if (!isNaN(value) && value <= this.props.totalNrAnswers) { if (this.state.chosenLabelType === 1) { if (this.state.chosenLabelType === 1) { value = 2 value = 2 } } this.setState({ this.setState({ nrPossibleAnswers: value nrPossibleAnswers: value }) }, this.updateNumberOptions) } } } } Loading @@ -33,12 +84,22 @@ class PanelMCQ extends React.Component { onChangeLabelType (e) { onChangeLabelType (e) { let value = parseInt(e.target.value) let value = parseInt(e.target.value) if (!isNaN(value)) { if (!isNaN(value)) { // if the label type is True/False then reduce the number of mc options to 2 if (parseInt(value) === 1) { this.setState({ this.setState({ nrPossibleAnswers: 2, chosenLabelType: value chosenLabelType: value }, () => { this.updateNumberOptions() let labels = this.generateLabels(this.state.nrPossibleAnswers, 0) this.props.updateLabels(labels) }) }) if (parseInt(value) === 1) { } else { this.setState({ this.setState({ nrPossibleAnswers: 2 chosenLabelType: value }, () => { let labels = this.generateLabels(this.state.nrPossibleAnswers, 0) this.props.updateLabels(labels) }) }) } } } } Loading @@ -47,18 +108,20 @@ class PanelMCQ extends React.Component { /** /** * This function generates an array with the labels for each option * This function generates an array with the labels for each option * @param nrLabels the number of options that need to be generated * @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[]} * @returns {any[]|string[]|number[]} */ */ generateLabels (nrLabels) { generateLabels (nrLabels, startingAt) { let type = this.state.chosenLabelType let type = this.state.chosenLabelType switch (type) { switch (type) { case 1: case 1: return ['T', 'F'] return ['T', 'F'] case 2: 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: 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: default: return Array(nrLabels).fill(' ') return Array(nrLabels).fill(' ') } } Loading @@ -70,34 +133,29 @@ class PanelMCQ extends React.Component { */ */ render () { render () { return ( return ( <nav className='panel'> <p className='panel-heading'> Multiple Choice Question </p> <div className='panel-block'> <div className='field'> <React.Fragment> <React.Fragment> <label className='label'>Number possible answers</label> <div className='panel-block mcq-block'> <div className='control'> <label className='label'> Multiple choice </label> {(function () { <Switch color='info' outlined value={this.props.problem.mc_options.length > 0} onChange={(e) => { var optionList = [] if (e.target.checked) { for (var i = 1; i <= this.props.totalNrAnswers; i++) { let npa = this.state.nrPossibleAnswers const optionElement = <option key={i} value={String(i)}>{i}</option> let labels = this.generateLabels(npa, 0) optionList.push(optionElement) this.props.generateMCOs(labels) } } else { return (<div className='select is-hovered is-fullwidth'> this.props.deleteMCOs(this.props.problem.mc_options.length) <select value={this.state.nrPossibleAnswers} onChange={this.onChangeNPA}>{optionList}</select> } </div>) }} /> }.bind(this)())} </div> </React.Fragment> </div> </div> </div> <div className='panel-block'> { this.props.problem.mc_options.length > 0 ? ( <div className='field'> <React.Fragment> <React.Fragment> <label className='label'>Answer boxes labels</label> <div className='panel-block mcq-block'> <div className='control'> <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'> <div className='select is-hovered is-fullwidth'> {(function () { {(function () { var optionList = this.state.labelTypes.map( var optionList = this.state.labelTypes.map( Loading @@ -113,32 +171,10 @@ class PanelMCQ extends React.Component { }.bind(this)())} }.bind(this)())} </div> </div> </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> </div> </nav> </React.Fragment>) : null } </React.Fragment> ) ) } } } } Loading
client/components/PanelMCQ.css 0 → 100644 +22 −0 Original line number Original line Diff line number Diff line .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/feedback/EditPanel.jsx +9 −0 Original line number Original line Diff line number Diff line Loading @@ -2,6 +2,7 @@ import React from 'react' import ConfirmationModal from '../../components/ConfirmationModal.jsx' import ConfirmationModal from '../../components/ConfirmationModal.jsx' import * as api from '../../api.jsx' import * as api from '../../api.jsx' import Notification from 'react-bulma-notification' const BackButton = (props) => ( const BackButton = (props) => ( <button className='button is-light is-fullwidth' onClick={props.onClick}> <button className='button is-light is-fullwidth' onClick={props.onClick}> Loading Loading @@ -116,6 +117,14 @@ class EditPanel extends React.Component { }) }) this.props.goBack() 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() }) }) } } } } Loading
client/components/feedback/FeedbackPanel.jsx +15 −13 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ class FeedbackPanel extends React.Component { } } componentDidMount = () => { componentDidMount = () => { if (this.props.grading) { this.props.bindShortcut(['up', 'k'], (event) => { this.props.bindShortcut(['up', 'k'], (event) => { event.preventDefault() event.preventDefault() this.prevOption() this.prevOption() Loading @@ -31,6 +32,7 @@ class FeedbackPanel extends React.Component { this.toggleSelectedOption() this.toggleSelectedOption() }) }) } } } 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) { Loading Loading @@ -111,7 +113,7 @@ class FeedbackPanel extends React.Component { feedback={feedback} checked={this.props.grading && 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} grading={this.props.grading} 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 && {this.props.grading && <div className='panel-block'> <div className='panel-block'> Loading
client/views/Exam.css +4 −0 Original line number Original line Diff line number Diff line Loading @@ -57,6 +57,10 @@ div.mcq-option img.mcq-box { background-color: hsla(171, 100%, 41%, 0.2) background-color: hsla(171, 100%, 41%, 0.2) } } .widget.selected .mcq-option:hover { background-color: rgb(25, 149, 216); } .editor-side-panel { .editor-side-panel { background: #fff; background: #fff; margin: 0.75em; margin: 0.75em; Loading