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

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
Select Git revision
  • Feature/Anonimization-script
  • QRCodes-odd-pages
  • add-boxes-frontend
  • add/find-corner-markers-tests
  • approve_shortcut
  • barcode-sample-generation
  • bep-demo
  • blank-detection
  • box-fill-detection
  • box-loc-db
  • client-refactor
  • client-side-statistics
  • combinatory-fixes
  • demo
  • demo-warp-perspective
  • develop
  • draw-pdf-box
  • edit-mc-finalized
  • feature-anonimization
  • feature/add-question-title
  • feature/additional-tests
  • feature/conda-dev
  • feature/highlite_pregrade
  • feature/identify-blank-solutions
  • feature/logging
  • feature/mobx
  • feature/no-hero
  • feature/pre-grading
  • feature/precise-positioning
  • feature/randomisedGrading
  • feature/toggle-pregrading
  • feature/total-time-grading
  • feature/update-feedback-on-giving-feedback
  • feature/upgrade-dependencies
  • fix-cb-snap
  • fix-exam-widget
  • fix-truefalse-mc
  • fix/combine-precise-positioning-with-pregrading
  • fix/delete-feedback-mco
  • fix/delete-mc-option
  • fix/fix-feedback-mult-choice
  • fix/mco-feedback-rel
  • fix/parametrize_corner_test
  • fix/simplify-pregrader
  • fix/simplify-realign-image
  • fuzzy-search-student-number
  • highlight-feedback
  • issue-176-fix
  • issue/198-problem-name-is-not-submitted-on-editing-or-navigating-away-from-the-problem
  • issue/209-exam-always-switches-to-latest-on-page-re-load
  • layout-side-panel
  • legacy
  • master
  • mc-checkbox-exam-api
  • mc-option-inheritance
  • mcq-fixes
  • no-latex-pdf-gen-helper-db
  • premade-feedback-merge
  • pytest-cov
  • react-noti
  • react-temp
  • refactor/global-box-size
  • refactoring_frontend
  • remove-data-folder
  • scan-orientation
  • scan-orientation-v2
  • size-mc
  • stefan-beta
  • test/add-api-tests
  • test/pregrading-with-precise-positioning
  • tweak-mc-api
  • dualstack
  • v0.1
  • v0.2
74 results
Show changes
Commits on Source (2575)
Showing with 583 additions and 312 deletions
{ {
"presets":[ "presets":[
"react", "@babel/react",
"flow" "@babel/flow",
"@babel/preset-env"
], ],
"plugins": [ "plugins": [
"syntax-dynamic-import", "@babel/syntax-dynamic-import",
"transform-class-properties", "react-hot-loader/babel",
"transform-object-rest-spread", "@babel/plugin-proposal-class-properties",
"react-hot-loader/babel" "@babel/plugin-proposal-object-rest-spread"
], ],
"env": { "env": {
"test": { "test": {
"presets":[ "presets":[
[ "env", [ "@babel/preset-env",
{ {
"modules": "commonjs" "modules": "commonjs"
} }
] ]
], ],
"plugins": [ "plugins": [
"transform-class-properties" "@babel/plugin-proposal-class-properties"
] ]
} }
} }
......
cov.html
data
data-dev
node_modules/
*.Dockerfile
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
exclude = .git, __pycache__, client/, .venv/, node_modules/ # no need to traverse these directories exclude = .git, __pycache__, client/, .venv/, node_modules/ # no need to traverse these directories
extend-ignore = E203,W503
\ No newline at end of file
...@@ -58,6 +58,9 @@ typings/ ...@@ -58,6 +58,9 @@ typings/
yarn.lock yarn.lock
package-lock.json package-lock.json
# Yarn cache
.yarn/cache
# dotenv environment variables file # dotenv environment variables file
.env .env
...@@ -105,3 +108,7 @@ stats.json ...@@ -105,3 +108,7 @@ stats.json
.coverage .coverage
cov.xml cov.xml
cov.html/ cov.html/
tests.xml
#test output
junit.xml
# This base image can be found in 'Dockerfile' # This base image can be found in 'test.Dockerfile'
image: zesje/base image: $TEST_IMAGE
stages: stages:
- build-env
- build - build
- test - test
# Special hidden job that is merged with JS jobs variables:
.node_modules: &node_modules TEST_IMAGE: ${CI_REGISTRY_IMAGE}/test
# Cache the JS modules that yarn fetches
cache: build-image:
untracked: true stage: build-env
paths: when: manual
- .yarn-cache image:
name: gcr.io/kaniko-project/executor:v1.17.0-debug
entrypoint: [""]
before_script: before_script:
- yarn install --cache-folder .yarn-cache - mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
script:
- mkdir docker
- cp test.Dockerfile docker/
- cp environment.yml docker/
- cp package.json docker/
- /kaniko/executor
--context $CI_PROJECT_DIR/docker
--dockerfile $CI_PROJECT_DIR/docker/test.Dockerfile
--destination $TEST_IMAGE
--cache=true
.python_packages: &python_packages .conda-env: &conda-env
before_script: before_script:
- pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt - ln -s /yarn/* ./
- if [[ $CI_JOB_NAME != *"py"* ]]; then yarn install; fi
build: build:
<<: *node_modules <<: *conda-env
stage: build stage: build
script: script:
- python3 -m compileall zesje - python3 -m compileall zesje
...@@ -30,31 +45,49 @@ build: ...@@ -30,31 +45,49 @@ build:
- zesje/static - zesje/static
expire_in: 1 week expire_in: 1 week
test_js: # test_js:
<<: *node_modules # <<: *conda-env
stage: test # stage: test
script: yarn test:js # script:
# - yarn test:js
# artifacts:
# reports:
# junit: junit.xml
lint_js: lint_js:
<<: *node_modules <<: *conda-env
stage: test stage: test
allow_failure: true allow_failure: true
script: script:
- yarn lint:js - yarn lint:js
lint_py: lint_py:
<<: *python_packages <<: *conda-env
stage: test stage: test
allow_failure: true allow_failure: true
script: script:
- yarn lint:py - yarn lint:py
test_py: test_py:
<<: *python_packages <<: *conda-env
stage: test stage: test
services:
- mysql:8.0
variables:
MYSQL_DATABASE: "course_test"
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
script: script:
- echo -e "\nMYSQL_HOST = 'mysql'\nMYSQL_USER = 'root'\nMYSQL_PASSWORD = None" >> zesje_test_cfg.py
- yarn test:py:cov - yarn test:py:cov
artifacts: artifacts:
paths: paths:
- cov.html/ - cov.html/
reports:
coverage_report:
coverage_format: cobertura
path: cov.xml
junit: tests.xml
expire_in: 1 week expire_in: 1 week
include:
template: Jobs/SAST.gitlab-ci.yml
Hidde Leistra <hleistra@gmail.com>
Stefan Hugtenburg <s.hugtenburg@gmail.com>
Robin Bijl <r.a.bijl@student.tudelft.nl>
Richard van de Kuilen <richardvdk@live.nl>
<richardvdk@live.nl> <rvdk>
Thomas Roos <thomasroos@live.nl>
Timotei Jugariu <timotei96@gmail.com>
...@@ -11,10 +11,20 @@ ...@@ -11,10 +11,20 @@
* Hidde Leistra * Hidde Leistra
* Pim Otte * Pim Otte
* Luc Enthoven * Luc Enthoven
* Otto Kaaij
* Lucas Holten
* Robin Bijl
* Ruben Young On
* Timotei Jugariu
* Richard van de Kuilen
<!-- <!--
Execute Execute
git shortlog -s | sed -e "s/^ *[0-9\t ]*//"| xargs -i sh -c 'grep -q "{}" AUTHORS.md || echo "{}"' git shortlog -s | grep -v "\[bot\]" | sed -e "s/^ *[0-9\t ]*//"| xargs -i sh -c 'grep -q "{}" AUTHORS.md || echo "{}"'
To check if any authors are missing from this list. To check if any authors are missing from this list.
If you add any new authors that do not specify a correct name
in their commits, please add them to the `.mailmap` file.
--> -->
FROM archlinux/base # A Dockerfile containing the production deployment for Zesje
## 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. FROM mambaorg/micromamba:1.5-jammy
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
WORKDIR ~ USER root
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
CMD bash RUN apt-get update && \
\ No newline at end of file apt-get install -y libdmtx-dev && \
apt-get install -y git supervisor nginx cron
COPY --chown=$MAMBA_USER:$MAMBA_USER environment.yml /tmp/env.yaml
USER $MAMBA_USER
RUN micromamba install -y -n base -f /tmp/env.yaml && \
micromamba clean --all --yes
ARG MAMBA_DOCKERFILE_ACTIVATE=1
USER root
WORKDIR /app
ADD . .
RUN yarn install
RUN yarn build
RUN rm -rf node_modules
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh", "/usr/bin/bash"]
...@@ -4,9 +4,35 @@ ...@@ -4,9 +4,35 @@
Zesje is an online grading system for written exams. Zesje is an online grading system for written exams.
## Running Zesje
### Running Zesje using Docker
Running Zesje using Docker is the easiest method to run Zesje
yourself with minimal technical knowledge required. For this approach
we assume that you already have [Docker](https://www.docker.com/)
installed, cloned the Zesje repository and entered its directory.
First create a volume to store the data:
docker volume create zesje
Then build the Docker image using the following below. Anytime you
update Zesje by pulling the repository you have to run this command again.
docker build -f auto.Dockerfile . -t zesje:auto
Finally, you can run the container to start Zesje using:
docker run -p 8881:80 --volume zesje:/app/data-dev -it zesje:auto
Zesje should be available at http://127.0.0.1:8881. If you get
the error `502 - Bad Gateway` it means that Zesje is still starting.
## Development ## Development
### Setting up a development environment ### Setting up a development environment
*Zesje currently doesn't support native Windows, but WSL works.*
We recommend using the Conda tool for managing your development We recommend using the Conda tool for managing your development
environment. If you already have Anaconda or Miniconda installed, environment. If you already have Anaconda or Miniconda installed,
you may skip this step. you may skip this step.
...@@ -15,10 +41,11 @@ Install Miniconda by following the instructions on this page: ...@@ -15,10 +41,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,24 +58,30 @@ Install all of the Javascript dependencies: ...@@ -31,24 +58,30 @@ 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
on different platforms: on different platforms:
| OS | Command | | OS | Command |
|---------------|------------------------------| | -------------- | ------------------------- |
| macOS | `brew install libdmtx` | | macOS | `brew install libdmtx` |
| Debian/Ubuntu | `sudo apt install libdmtx0a` | | Debian, Ubuntu | `apt install libdmtx-dev` |
| Arch | `pacman -S libdmtx` | | Arch | `pacman -S libdmtx` |
| Fedora | `dnf install libdmtx` | | Fedora | `dnf install libdmtx` |
| openSUSE | `zypper install libdmtx0` | | openSUSE | `zypper install libdmtx0` |
| Windows | *not necessary* |
#### Setting up MySQL server
If this is the first time that you will run Zesje with MySQL in
development, then run the following command from the Zesje
repository directory:
yarn dev:mysql-init
That's all it needs to create the MySQL files in the data directory,
migrate the database to the last schema and move all your previous data.
### Running a development server ### Running a development server
...@@ -60,12 +93,40 @@ to start the development server, which you can access on http://127.0.0.1:8881. ...@@ -60,12 +93,40 @@ to start the development server, which you can access on http://127.0.0.1:8881.
It will automatically reload whenever you change any source files in `client/` It will automatically reload whenever you change any source files in `client/`
or `zesje/`. or `zesje/`.
### Running Oauth2 Mock Server
By default, login is disabled during development but it can be enabled by setting `LOGIN_DISABLED = False` in
the [development configuration file](./zesje_dev_cfg.py). You can see the set of supported login providers in
[constants.py](./zesje/constants.py) but take into account that for GitLab and Surf Conext you will need to request
for a valid `CLIENT_ID` and `CLIENT_SECRET`.
In case you only want to test the flow, you can set the provider to `mock` and start the
[mock oauth server](./mock_oauth_server.py) in a different terminal from where you
started the development server by running `yarn dev:oauth`.
### Generate sample data
The script `example_data.py` can be used to create a sample exam that mimics the appearance and behavior of a typical grading process in Zesje. The prototype exam consist on 3 open answer questions per page and 1 multiple choice question per page (excluding the first one). The script partially solves those questions and assigns random feedback to the answered questions while the not answered questions are processed as blank.
The script is called from the command line with the following parameters:
- `-d, --delete` If specified, removes any existing data and creates a new empty database. Otherwise, new exams are added without deleting previous data.
- `--exams (int)` the number of exams to add, default is 1.
- `--pages (int)` the number of pages per exam, default is 3.
- `--students (int)` the number of students per exam, default is 30
- `--graders (int)` the number of graders to add, default is 4.
- `--solve (float)` between 0 and 100, indicates the percentage of questions to answer (including MCQ), default is 90%.
- `--grade (float)` between 0 and 100, indicate the percentage of solved questions to grade (that is, excluding blank answers), default is 60%.
- `--skip-processing` if specified, fakes the pdf processing to reduce time. As a drawback, blanks will not be detected.
- `--multiple-copies (float)` between 0 and 100, indicates how much of the students submit multiple copies, default is 5%
The actual processing of the exam takes a while, specially when the number of students is large.
### Running the tests ### Running the tests
You can run the tests by running You can run the tests by running
yarn test yarn test
#### Viewing test coverage #### Viewing test coverage
As a test coverage tool for Python tests, `pytest-cov` is used. As a test coverage tool for Python tests, `pytest-cov` is used.
...@@ -102,32 +163,36 @@ a first resort. ...@@ -102,32 +163,36 @@ a first resort.
### Database modifications ### Database modifications
Zesje uses Flask-Migrate and Alembic for database versioning and migration. Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic. Zesje uses Flask-Migrate and Alembic for database versioning and migration. Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic.
To change something in the database schema, simply add this change to `zesje/database.py`. After that run the following command to prepare a new migration: To change something in the database schema, simply add this change to `zesje/database.py`. After that run the following command to prepare a new migration:
yarn prepare-migration yarn dev:prepare-migration
This uses Flask-Migrate to make a new migration script in `migrations/versions` which needs to be reviewed and edited. Please suffix the name of this file with something distinctive and add a short description at the top of the file. To apply the database migration run: This uses Flask-Migrate to make a new migration script in `migrations/versions` which needs to be reviewed and edited. Please suffix the name of this file with something distinctive and add a short description at the top of the file. To apply the database migration run:
yarn migrate:dev # (for the development database) yarn dev:mysql-migrate # (for the development database)
yarn migrate # (for the production database) yarn migrate # (for the production database, MySQL must be running)
### Building and running the production version ### Building and running the production version
### Code style ### Code style
#### Python #### Python
Adhere to [PEP8](https://www.python.org/dev/peps/pep-0008/), but use a column width of 120 characters (instead of 79). Adhere to [PEP8](https://www.python.org/dev/peps/pep-0008/), but use a column width of 120 characters (instead of 79).
If you followed the instructions above, the linter `flake8` is installed in your virtual environment. If you use Visual Studio Code, install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) extension and add the following lines to your workspace settings: If you followed the instructions above, the linter `flake8` and the formatter `black` are installed in your virtual environment. If you use Visual Studio Code, install the [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) extension and add the following lines to your workspace settings:
"python.linting.pylintEnabled": false, "python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true, "python.linting.flake8Enabled": true,
"[python]": { "[python]": {
"editor.rulers": [120] "editor.rulers": [120],
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
}, },
"black-formatter.args": [
"--line-length=120"
]
If you use Atom, install the [linter-flake8](https://atom.io/packages/linter-flake8) plugin and add the following lines to your config: If you use Atom, install the [linter-flake8](https://atom.io/packages/linter-flake8) plugin and add the following lines to your config:
...@@ -138,18 +203,17 @@ If you use Atom, install the [linter-flake8](https://atom.io/packages/linter-fla ...@@ -138,18 +203,17 @@ If you use Atom, install the [linter-flake8](https://atom.io/packages/linter-fla
#### Javascript #### Javascript
Adhere to [StandardJS](https://standardjs.com/). Adhere to [StandardJS](https://standardjs.com/).
If you use Visual Studio Code, install the [vscode-standardjs](https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs) plugin. If you use Visual Studio Code, install the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) plugin.
If you use Atom, install the [linter-js-standard-engine](https://atom.io/packages/linter-js-standard-engine) plugin. If you use Atom, install the [linter-eslint](https://atom.io/packages/linter-eslint) plugin.
### 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
Yarn keeps track of all the client-side dependancies in `package.json` when you install new packages with `yarn add [packageName]`. Yarn will install and update your packages if your run Yarn keeps track of all the client-side dependancies in `package.json` when you install new packages with `yarn add [packageName]`. Yarn will install and update your packages if your run
......
# A Dockerfile containing everything to run Zesje automatically
FROM continuumio/miniconda3
RUN apt-get update -y && apt-get install -y libdmtx-dev nginx sudo
RUN echo "server { listen 80; location / { proxy_pass http://127.0.0.1:5000; } }" > /etc/nginx/sites-enabled/proxy.conf
RUN rm /etc/nginx/sites-enabled/default
RUN groupadd -r zesje && useradd --no-log-init -r -g zesje zesje
RUN echo 'zesje ALL= NOPASSWD: /usr/sbin/service nginx restart,/bin/chown -R zesje\:zesje /app/data-dev' | sudo EDITOR='tee -a' visudo
WORKDIR /app
COPY . /app
RUN conda env create -n zesje-dev
ENV PATH /opt/conda/envs/zesje-dev/bin:$PATH
RUN yarn install
RUN yarn build
USER zesje
VOLUME /app/data-dev
EXPOSE 80
CMD sudo service nginx restart && \
sudo chown -R zesje:zesje /app/data-dev && \
yarn dev:mysql-init && yarn dev:backend
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 +37,7 @@ canv.save() ...@@ -36,7 +37,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")
import React from 'react' import React from 'react'
import Loadable from 'react-loadable' import loadable from '@loadable/component'
import { hot } from 'react-hot-loader' import { hot } from 'react-hot-loader'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import { BrowserRouter as Router, Route, Routes, Navigate, Outlet, useLocation } from 'react-router-dom'
import 'bulma/css/bulma.css'
import 'react-bulma-notification/build/css/index.css' import './App.scss'
import 'font-awesome/css/font-awesome.css'
import * as api from './api.jsx' import * as api from './api.jsx'
import NavBar from './components/NavBar.jsx' import NavBar from './components/NavBar.jsx'
import ExamRouter from './components/ExamRouter.jsx'
import Footer from './components/Footer.jsx' import Footer from './components/Footer.jsx'
import Loading from './views/Loading.jsx' import Loading from './views/Loading.jsx'
import HelpModal, { HELP_PAGES } from './components/modals/HelpModal.jsx'
const Home = Loadable({ const Login = loadable(() => import('./views/Login.jsx'), { fallback: <Loading /> })
loader: () => import('./views/Home.jsx'), const Home = loadable(() => import('./views/Home.jsx'), { fallback: <Loading /> })
loading: Loading const AddExam = loadable(() => import('./views/AddExam.jsx'), { fallback: <Loading /> })
}) const Graders = loadable(() => import('./views/Graders.jsx'), { fallback: <Loading /> })
const AddExam = Loadable({ const Fail = loadable(() => import('./views/Fail.jsx'), { fallback: <Loading /> })
loader: () => import('./views/AddExam.jsx'),
loading: Loading
})
const Exam = Loadable({
loader: () => import('./views/Exam.jsx'),
loading: Loading
})
const Submissions = Loadable({
loader: () => import('./views/Submissions.jsx'),
loading: Loading
})
const Students = Loadable({
loader: () => import('./views/Students.jsx'),
loading: Loading
})
const Grade = Loadable({
loader: () => import('./views/Grade.jsx'),
loading: Loading
})
const Graders = Loadable({
loader: () => import('./views/Graders.jsx'),
loading: Loading
})
const Overview = Loadable({
loader: () => import('./views/Overview.jsx'),
loading: Loading
})
const Email = Loadable({
loader: () => import('./views/Email.jsx'),
loading: Loading
})
const Fail = Loadable({
loader: () => import('./views/Fail.jsx'),
loading: Loading
})
const nullExam = () => ({ const NavBarView = (props) => {
id: null, const location = useLocation()
name: '',
submissions: [],
problems: [],
widgets: []
})
class App extends React.Component { return props.grader == null
menu = React.createRef(); ? <Navigate to={{ pathname: '/login', search: `from=${location.pathname}` }} state={{ from: location }} replace />
: <div>
<NavBar
logout={props.logout}
ref={props.menu}
grader={props.grader}
examID={props.examID}
setHelpPage={props.setHelpPage} />
<section className='section'>
<div className='container is-fluid'>
<Outlet />
</div>
</section>
<HelpModal
page={HELP_PAGES[props.helpPage] || { content: null, title: null }}
closeHelp={() => props.setHelpPage({ helpPage: null })}
/>
<Footer />
</div>
}
class App extends React.Component {
state = { state = {
exam: nullExam(), examID: null,
grader: null /*
* The id of the current user. An undefined user means that the App has not check the backend for the registrered
* user yet, hence wait for the response before going to the desired url by showing a Home/Welcome screen.
* Instead, a null user means that is not logged in, then go to the Home page through the router.
*/
grader: undefined,
helpPage: null
} }
updateExam = (examID) => { constructor (props) {
if (examID === null) { super(props)
this.setState({ exam: nullExam() }) this.menu = React.createRef()
} else {
api.get('exams/' + examID)
.catch(resp => this.setState({ exam: nullExam() }))
.then(ex => this.setState({ exam: ex }))
}
} }
deleteExam = (examID) => { componentDidMount = () => {
return api api.get('oauth/status').then(status => {
.del('exams/' + examID) this.setState({
.then(() => { grader: status.grader,
if (this.menu.current) { loginProvider: status.provider
this.menu.current.updateExamList()
}
}) })
} }).catch(err => {
if (err.status === 401) {
updateSubmission = (index, sub) => { this.setState({
if (index === undefined) { grader: null,
api.get('submissions/' + this.state.exam.id) loginProvider: err.provider
.then(subs => this.setState({ })
exam: {
...this.state.exam,
submissions: subs
}
}))
} else {
if (sub) {
if (JSON.stringify(sub) !== JSON.stringify(this.state.exam.submissions[index])) {
let newList = this.state.exam.submissions
newList[index] = sub
this.setState({
exam: {
...this.state.exam,
submissions: newList
}
})
}
} else { } else {
api.get('submissions/' + this.state.exam.id + '/' + this.state.exam.submissions[index].id) console.log(err)
.then(sub => {
if (JSON.stringify(sub) !== JSON.stringify(this.state.exam.submissions[index])) {
let newList = this.state.exam.submissions
newList[index] = sub
this.setState({
exam: {
...this.state.exam,
submissions: newList
}
})
}
})
} }
} })
} }
changeGrader = (grader) => { selectExam = (id) => this.setState({ examID: parseInt(id) })
this.setState({ logout = () => api.get('oauth/logout').then(() => this.setState({ grader: null }))
grader: grader setHelpPage = (helpPage) => this.setState({ helpPage })
})
window.sessionStorage.setItem('graderID', grader.id) updateExamList = () => {
if (this.menu.current) {
this.menu.current.updateExamList()
}
} }
render () { render () {
const exam = this.state.exam if (this.state.grader === undefined) return <Loading />
const grader = this.state.grader
return ( return (
<Router> <Router>
<div> <Routes>
<NavBar exam={exam} updateExam={this.updateExam} grader={grader} changeGrader={this.changeGrader} ref={this.menu} /> <Route path='login' element={
<Switch> <Login provider={this.state.loginProvider} grader={this.state.grader} />
<Route exact path='/' component={Home} /> } />
<Route path='/exams/:examID' render={({ match, history }) => <Route path='unauthorized' element={
<Exam <Fail message='Your account is not authorized to access this instance of Zesje.' />
exam={exam} }/>
examID={match.params.examID} <Route path='*' element={<Fail message="404. Could not find that page :'(" />}/>
updateExam={this.updateExam} <Route path='/' element={
deleteExam={this.deleteExam} <NavBarView
updateSubmission={this.updateSubmission} logout={this.logout}
leave={() => history.push('/')} />} /> menu={this.menu}
<Route path='/exams' render={({ history }) => grader={this.state.grader}
<AddExam updateExamList={this.menu.current ? this.menu.current.updateExamList : null} changeURL={history.push} />} /> examID={this.state.examID}
<Route path='/submissions/:examID' render={({ match }) => setHelpPage={this.setHelpPage}
<Submissions helpPage={this.state.helpPage} />
exam={exam} }>
urlID={match.params.examID} <Route index element={<Home />} />
updateExam={this.updateExam} <Route path='exams' element={<Outlet />}>
updateSubmission={this.updateSubmission} />} /> <Route index element={<AddExam updateExamList={this.updateExamList}/>} />
<Route path='/students' render={() => <Route path=':examID/*' element={
<Students exam={exam} updateSubmission={this.updateSubmission} />} /> <ExamRouter
<Route path='/grade' render={() => ( selectExam={this.selectExam}
exam.submissions.length && grader updateExamList={this.updateExamList}
? <Grade exam={exam} graderID={this.state.grader.id} setHelpPage={this.setHelpPage}
updateSubmission={this.updateSubmission} updateExam={this.updateExam} /> />}
: <Fail message='No exams uploaded or no grader selected. Please do not bookmark URLs' /> />
)} /> </Route>
<Route path='/overview' render={() => ( <Route path='graders' element={<Graders />} />
exam.submissions.length ? <Overview exam={exam} /> : <Fail message='No exams uploaded. Please do not bookmark URLs' /> </Route>
)} /> </Routes>
<Route path='/email' render={() => (
exam.submissions.length ? <Email exam={exam} /> : <Fail message='No exams uploaded. Please do not bookmark URLs' />
)} />
<Route path='/graders' render={() =>
<Graders updateGraderList={this.menu.current ? this.menu.current.updateGraderList : null} />} />
<Route render={() =>
<Fail message="404. Could not find that page :'(" />} />
</Switch>
<Footer />
</div>
</Router> </Router>
) )
} }
......
@charset "utf-8";
/* Bulma Switch Control */
@import "~bulma-switch-control/bulma-switch-control";
@import 'bulma-popover/bulma-popover';
@import "bulma/bulma";
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/solid";
@import '@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css';
.dropzone {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
border-width: 2px;
border-radius: 2px;
border-color: $info;
border-style: dashed;
background-color: #fafafa;
color: $info;
outline: none;
transition: border .24s ease-in-out;
}
.dropzone:focus {
border-color: #2196f3;
}
.dropzone.disabled {
border-color: #eeeeee;
color: #bdbdbd;
opacity: 0.6;
}
.tag.is-circular {
border-radius: 50%;
border-color: $link;
}
.tag.is-circular.is-danger {
border-radius: 50%;
border-color: $danger;
}
.tag.is-squared {
border-radius: 2px;
border-color: $link;
}
// close navbar dropdown automatically
// extracted from: https://github.com/jgthms/bulma/issues/2514#issuecomment-510451361
@media screen and (min-width: 1025px) {
.navbar-item.is-hoverable:hover .navbar-dropdown {
display: block !important;
}
.navbar-item.is-hoverable:focus-within .navbar-dropdown {
display: none;
}
}
.has-tooltip-hidden {
&:after, &:before {
opacity: 0 !important;
display: none !important;
}
}
/* Class that manually sets the z-index of sticky objects with a modal as a child.
Required since the model will inherit the z-index of sticky parents. */
.is-sticky.has-modal {
z-index: 40;
}
...@@ -7,29 +7,31 @@ function _typeof (a) { ...@@ -7,29 +7,31 @@ function _typeof (a) {
function _fetch (method) { function _fetch (method) {
return (endpoint, data) => { return (endpoint, data) => {
var headers = new window.Headers() const headers = new window.Headers()
if (_typeof(data) === 'Object') { if (_typeof(data) === 'Object') {
headers.append('Content-Type', 'application/json') headers.append('Content-Type', 'application/json')
data = JSON.stringify(data) data = JSON.stringify(data)
} }
return window.fetch('/api/' + endpoint, { return window.fetch('/api/' + endpoint, {
method: method, method,
credentials: 'same-origin', credentials: 'same-origin',
body: data, body: data,
headers: headers headers
}) })
.catch(error => .catch(error => console.error('Error: ', error, ' in', method, endpoint, 'with data', data))
console.error('Error: ', error, ' in', method, endpoint, 'with data', data))
.then(resp => { .then(resp => {
if (!resp.ok) { if (!resp.json) throw resp
throw resp
} else { return resp.json().then(json => {
return resp if (resp.ok) {
} return json
} else {
console.error(json)
return Promise.reject(json)
}
})
}) })
// valid responses always return JSON
.then(r => r.json())
} }
} }
......
import React from 'react'
class ColorInput extends React.Component {
state = {
editing: true
}
onStartEditing = () => this.setState({ editing: true })
onFinishEditing = () => this.setState({ editing: false })
inputColor = (value) => {
if (this.state.editing) {
return ''
} else if (value) {
return 'is-success'
} else {
return 'is-danger'
}
}
render () {
return (
<input
className={'input ' + this.inputColor(this.props.value)}
onFocus={this.onStartEditing}
onBlur={this.onFinishEditing}
{...this.props}
/>
)
}
}
export default ColorInput
import React from 'react'
const DropzoneContent = () => (
<div className='file has-name is-boxed is-centered'>
<label className='file-label'>
<span className='file-cta'>
<span className='file-icon'>
<i className='fa fa-upload' />
</span>
<span className='file-label'>
Choose a file…
</span>
</span>
</label>
</div>
)
export default DropzoneContent
import React from 'react'
const EmptyPDF = (props) => {
let width
let height
switch (props.format) {
case 'a4':
case 'A4':
default:
width = 595
height = 841
}
return (
<div
style={{
paddingTop: '4rem',
minWidth: (width) + 'px',
minHeight: (height) + 'px',
backgroundColor: 'white'
}}
className={'has-text-centered'}
>
<i className={'fa fa-refresh fa-spin'} />
</div>
)
}
export default EmptyPDF
import React from 'react'
import { Route, Routes, Outlet } from 'react-router-dom'
import loadable from '@loadable/component'
import withRouter from './RouterBinder.jsx'
import * as api from '../api.jsx'
import Loading from '../views/Loading.jsx'
const Exam = loadable(() => import('../views/Exam.jsx'), { fallback: <Loading /> })
const Scans = loadable(() => import('../views/Scans.jsx'), { fallback: <Loading /> })
const Students = loadable(() => import('../views/Students.jsx'), { fallback: <Loading /> })
const Grade = loadable(() => import('../views/Grade.jsx'), { fallback: <Loading /> })
const Overview = loadable(() => import('../views/Overview.jsx'), { fallback: <Loading /> })
const Email = loadable(() => import('../views/Email.jsx'), { fallback: <Loading /> })
const Fail = loadable(() => import('../views/Fail.jsx'), { fallback: <Loading /> })
class ExamRouter extends React.PureComponent {
componentDidMount = () => {
this.props.selectExam(this.props.router.params.examID)
}
componentDidUpdate = (prevProps, prevState) => {
const { examID } = this.props.router.params
if (prevProps.router.params.examID !== examID) {
// sends the selected exam to the navbar
this.props.selectExam(examID)
}
}
deleteExam = (examID) => {
return api
.del('exams/' + examID)
.then(() => {
this.props.updateExamList()
this.props.router.navigate('/', { replace: true })
})
}
render = () => {
const { examID } = this.props.router.params
if (!examID || isNaN(examID)) {
return <Fail message='Invalid exam' />
}
return (
<Routes>
<Route
path='scans' element={<Scans examID={examID} />}
/>
<Route
path='students' element={<Students examID={examID} />}
>
<Route path=':copyNumber' element={<Outlet />} />
</Route>
<Route
path='grade' element={<Grade examID={examID} />}
>
<Route path=':submissionID' element={<Outlet />} />
<Route path=':submissionID/:problemID' element={<Outlet />} />
</Route>
<Route
path='overview' element={<Overview examID={examID} />}
/>
<Route
path='email' element={<Email examID={examID} />}
/>
<Route
path='/' element={
<Exam
examID={examID}
updateExamList={this.props.updateExamList}
deleteExam={this.deleteExam}
setHelpPage={this.props.setHelpPage}
/>}
/>
</Routes>
)
}
}
export default withRouter(ExamRouter)
import React from 'react' import React from 'react'
const Hero = (props) => { const Footer = (props) => {
return ( return (
<footer className='footer'> <footer className='footer'>
<div className='container'> <div className='container'>
<div className='content has-text-centered'> <div className='content has-text-centered'>
<p> <p>
<strong>Zesje</strong> by <a href='https://gitlab.kwant-project.org/zesje/zesje/blob/master/AUTHORS.md' target='_blank'>the team</a>. <strong>Zesje</strong> by
The code is licensed under <a href='https://choosealicense.com/licenses/agpl-3.0/' target='_blank'> AGPLv3 </a> <a
and available <a href='https://gitlab.kwant-project.org/zesje/zesje/' target='_blank'>here</a>. href='https://gitlab.kwant-project.org/zesje/zesje/blob/master/AUTHORS.md'
target='_blank'
rel="noreferrer"
> the team
</a>.
The code is licensed under
<a href='https://choosealicense.com/licenses/agpl-3.0/' target='_blank' rel="noreferrer"> AGPLv3 </a>
and available
<a href='https://gitlab.kwant-project.org/zesje/zesje/' target='_blank' rel="noreferrer"> here</a>.
<br />
Version {__ZESJE_VERSION__}
</p> </p>
</div> </div>
</div> </div>
...@@ -16,4 +26,4 @@ const Hero = (props) => { ...@@ -16,4 +26,4 @@ const Hero = (props) => {
) )
} }
export default Hero export default Footer
import React from 'react' import React from 'react'
import EmptyPDF from '../components/EmptyPDF.jsx' import { LoadingPDF, ErrorPDF } from '../components/PDFPlaceholders.jsx'
import { Document, Page } from 'react-pdf/dist/entry.webpack' import { Document, Page } from 'react-pdf/dist/esm/entry.webpack5'
class GeneratedExamPreview extends React.Component { class GeneratedExamPreview extends React.Component {
render = () => { render = () => {
...@@ -9,13 +9,15 @@ class GeneratedExamPreview extends React.Component { ...@@ -9,13 +9,15 @@ class GeneratedExamPreview extends React.Component {
<Document <Document
file={'/api/exams/' + this.props.examID + '/preview'} file={'/api/exams/' + this.props.examID + '/preview'}
onLoadSuccess={this.props.onPDFLoad} onLoadSuccess={this.props.onPDFLoad}
loading={<EmptyPDF />} loading={<LoadingPDF />}
noData={<EmptyPDF />} error={<ErrorPDF />}
noData={<ErrorPDF text='No PDF file specified.' />}
> >
<Page <Page
renderAnnotations={false} renderAnnotations={false}
renderTextLayer={false} renderTextLayer={false}
pageIndex={this.props.page} /> pageIndex={this.props.page}
/>
</Document> </Document>
) )
} }
......