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
Select Git revision
  • demo-warp-perspective
  • feature/toggle-pregrading
  • develop
  • demo
  • add/find-corner-markers-tests
  • tweak-mc-api
  • fix/simplify-realign-image
  • client-side-statistics
  • size-mc
  • fix-truefalse-mc
  • edit-mc-finalized
  • fix/fix-feedback-mult-choice
  • fix/simplify-pregrader
  • box-loc-db
  • feature/add-question-title
  • feature/identify-blank-solutions
  • master
  • fix/combine-precise-positioning-with-pregrading
  • refactor/global-box-size
  • feature/highlite_pregrade
  • layout-side-panel
  • remove-data-folder
  • approve_shortcut
  • feature/precise-positioning
  • feature/total-time-grading
  • fix/delete-feedback-mco
  • test/pregrading-with-precise-positioning
  • premade-feedback-merge
  • test/add-api-tests
  • highlight-feedback
  • fix/parametrize_corner_test
  • fix-exam-widget
  • feature/pre-grading
  • combinatory-fixes
  • refactoring_frontend
  • fix-cb-snap
  • QRCodes-odd-pages
  • pytest-cov
  • stefan-beta
  • add-boxes-frontend
  • fix/mco-feedback-rel
  • fix/delete-mc-option
  • draw-pdf-box
  • box-fill-detection
  • mc-option-inheritance
  • mcq-fixes
  • mc-checkbox-exam-api
  • feature/upgrade-dependencies
  • feature/additional-tests
  • feature/update-feedback-on-giving-feedback
  • feature/logging
  • Feature/Anonimization-script
  • feature/randomisedGrading
  • feature-anonimization
  • feature/conda-dev
  • feature/no-hero
  • issue/209-exam-always-switches-to-latest-on-page-re-load
  • issue/198-problem-name-is-not-submitted-on-editing-or-navigating-away-from-the-problem
  • client-refactor
  • feature/mobx
  • issue-176-fix
  • bep-demo
  • scan-orientation-v2
  • fuzzy-search-student-number
  • scan-orientation
  • no-latex-pdf-gen-helper-db
  • blank-detection
  • barcode-sample-generation
  • react-temp
  • react-noti
  • legacy
  • dualstack
  • v0.1
  • v0.2
74 results

Target

Select target project
No results found
Select Git revision
  • 694-separate-migrations-app
  • master
  • 672-mc-api-collective
  • 320-copy-feedback
  • 673-delete-copies
  • 670-modals-not-reacting-to-x
  • 481-detach-image-from-page
  • 658-fix-auto-approve-modal-not-showing
  • 590-auto-dockerfile-is-broken
  • v0.3.0a7
  • 568-design-for-multiple-exam-types
  • 561-progress-bar-can-exceed-100
  • 530-improve-ui-for-multi-page-solutions
  • 550-frontend-api-feedback-filter
  • 515-3-cols-layout
  • 481-split-pipelines
  • 501-bulma-0-8
  • 496-create-a-frotend-view-for-courses
  • 487-courses
  • 497-add-course-id-prop
  • 464-create-and-modify-endpoints-to-allow-checking-and-modifying-user-permission-2
  • 467-create-element-for-adding-graders
  • 477-exam-owner-access
  • 430-email-take-home-exams
  • 465_create_roles_table_rest_api
  • test-coverage-report
  • test-coverage-report-2
  • 452-login-page-frontend
  • anonymize-script
  • sqlite_backport
  • 398-explain-meaning-of-boxes-when-exam-is-created
  • 397-autograding-approval-is-counter-intuitive
  • 386-previous-graded
  • 370-rename-id-to-copy
  • 257-randomize-next-ungraded
  • overviewStats
  • feature/logging
  • patch-1
  • filter-submissions
  • feature/upgrade-dependencies
  • feature/additional-tests
  • feature/update-feedback-on-giving-feedback
  • Feature/Anonimization-script
  • feature/randomisedGrading
  • feature-anonimization
  • feature/conda-dev
  • feature/no-hero
  • issue/209-exam-always-switches-to-latest-on-page-re-load
  • issue/198-problem-name-is-not-submitted-on-editing-or-navigating-away-from-the-problem
  • client-refactor
  • issue-176-fix
  • bep-demo
  • scan-orientation-v2
  • fuzzy-search-student-number
  • scan-orientation
  • no-latex-pdf-gen-helper-db
  • blank-detection
  • barcode-sample-generation
  • legacy
  • dualstack
  • sqlite
  • v0.1
  • v0.2
  • v0.3.0a1
  • v0.3.0a2
  • v0.3.0a3
  • v0.3.0a4
  • v0.3.0a5
  • v0.3.0a6
  • v0.4.0
  • v0.4.1
  • v0.4.2
  • v0.4.3
  • v0.4.4
  • v0.4.5
  • v0.4.6
  • v0.4.7
  • v0.4.8
  • v0.4.9
79 results
Show changes

Commits on Source 132

32 additional commits have been omitted to prevent performance issues.
36 files
+ 946
522
Compare changes
  • Side-by-side
  • Inline

Files

+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
@@ -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
+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
+7 −10
Original line number Original line Diff line number Diff line
@@ -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:


@@ -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
@@ -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
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()
@@ -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")
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) {
@@ -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)
    }
    }
  }
  }


@@ -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)
        })
        })
      }
      }
    }
    }
@@ -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(' ')
    }
    }
@@ -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(
@@ -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>
    )
    )
  }
  }
}
}
+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
Original line number Original line Diff line number Diff line
@@ -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}>
@@ -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()
          })
        })
    }
    }
  }
  }


Original line number Original line Diff line number Diff line
@@ -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()
@@ -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) {
@@ -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'>
Original line number Original line Diff line number Diff line
@@ -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;