├── doc ├── source │ ├── _static │ │ └── .keep │ ├── _templates │ │ └── .keep │ ├── changelog.md │ ├── _latex │ │ └── preamble.tex │ ├── module-plom-scan.rst │ ├── module-plom-create.rst │ ├── module-plom-finish.rst │ ├── development.rst │ ├── plom-demo.rst │ ├── plom-scan.rst │ ├── module-plom-solutions.rst │ ├── plom-create.rst │ ├── plom-finish.rst │ ├── install.rst │ ├── plom-solutions.rst │ ├── plom-new-demo.rst │ ├── plom-new-server.rst │ ├── plom-cli.rst │ ├── plom-client.rst │ ├── api.rst │ ├── install-from-source.rst │ ├── running_a_server.rst │ ├── cmdline.rst │ ├── getting_started.rst │ ├── index.rst │ ├── code.rst │ └── macos_installation.md ├── requirements.txt ├── README.md └── Makefile ├── plom ├── idBox2.pdf ├── idBox4.pdf ├── scan │ ├── test_rgb.png │ ├── test_zbar_fails.png │ └── clearScannerLogin.py ├── test_target_latex.png ├── test_target_latex_old.png ├── create │ ├── fonts │ │ ├── adr_handwriting.ttf │ │ ├── bt_handwriting.ttf │ │ ├── ejx_handwriting.ttf │ │ ├── ld_handwriting.ttf │ │ ├── nh_handwriting.ttf │ │ ├── pdl_handwriting.ttf │ │ └── __init__.py │ ├── test_build_source_exams.py │ ├── test_upload_classlist.py │ └── test_page_counts.py ├── templateUserList.csv ├── scripts │ ├── __init__.py │ └── test_script_help_ver.py ├── test_version.py ├── messenger │ ├── test_messengers.py │ └── __init__.py ├── canvas │ ├── README.md │ └── __init__.py ├── cli │ ├── task_tools.py │ ├── list_bundles.py │ ├── identify_tools.py │ └── bundle_tools.py ├── __init__.py ├── common.py ├── idreader │ └── __init__.py ├── solutions │ ├── checkSolutionStatus.py │ ├── getSolutionImage.py │ └── deleteSolutionImage.py ├── finish │ ├── clear_manager_login.py │ ├── audit.py │ └── rubric_downloads.py ├── test_print_score.py ├── templateSolutionSpec.toml ├── test_feedback_rules.py ├── comment_utils.py ├── test_tags.py ├── tagging.py ├── test_exceptions.py └── test_latex.py ├── testTemplates ├── idBox.pdf ├── idBox2.pdf ├── idBox4.pdf ├── dummy_qr_code.png ├── dummy_left_staple.png ├── dummy_qr_code_red.png ├── dummy_right_staple.png ├── dummy_left_staple_red.png ├── dummy_right_staple_red.png ├── idBox2-source.tex ├── idBox-source.tex └── idBox4-source.tex ├── org.plomgrading.PlomClient.png ├── plom_server ├── static │ ├── favicon.ico │ ├── plomLogo.png │ ├── defaultUserIcon.png │ ├── __init__.py │ ├── css │ │ ├── loginPage.css │ │ ├── profile.css │ │ └── diff_table.css │ ├── css3rdparty │ │ └── README.txt │ └── js3rdparty │ │ └── README.txt ├── Contrib │ └── __init__.py ├── Reports │ ├── __init__.py │ └── urls.py ├── Tags │ ├── __init__.py │ ├── services │ │ └── __init__.py │ ├── apps.py │ ├── urls.py │ └── forms.py ├── QuestionTags │ ├── __init__.py │ ├── services │ │ └── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ ├── urls.py │ └── admin.py ├── API │ ├── tests │ │ ├── __init__.py │ │ └── test_client_reject_list.py │ ├── services │ │ ├── __init__.py │ │ └── TokenService.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── views │ │ ├── experimental │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── rubrics.py │ │ │ └── annotations.py │ │ └── user_info.py │ ├── permissions │ │ └── __init__.py │ ├── apps.py │ └── routes │ │ ├── __init__.py │ │ └── tags_patterns.py ├── Identify │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── services │ │ └── __init__.py │ ├── admin.py │ └── urls.py ├── Launcher │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── services │ │ └── __init__.py │ ├── apps.py │ └── management │ │ └── commands │ │ ├── plom_build_scrap_extra_pdfs.py │ │ └── plom_get_static_javascript.py ├── Preparation │ ├── __init__.py │ ├── useful_files_for_testing │ │ ├── test_version1.pdf │ │ ├── test_version2.pdf │ │ ├── cl_warn.csv │ │ ├── cl_errs.csv │ │ ├── cl_good.csv │ │ ├── testing_test_spec.toml │ │ └── spec_with_shared_pages.toml │ ├── migrations │ │ └── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── tiny_spec.toml │ ├── apps.py │ ├── services │ │ └── __init__.py │ ├── views │ │ ├── needs_manager_view.py │ │ ├── mocker.py │ │ └── __init__.py │ └── admin.py ├── Progress │ ├── __init__.py │ ├── tests │ │ └── __init__.py │ ├── services │ │ └── __init__.py │ ├── apps.py │ ├── views │ │ ├── overview_landing.py │ │ └── __init__.py │ └── forms.py ├── Rubrics │ ├── tests │ │ └── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── __init__.py │ ├── apps.py │ ├── admin.py │ ├── services │ │ └── __init__.py │ └── serializers.py ├── Scan │ ├── tests │ │ ├── id_page_img.png │ │ └── __init__.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ ├── models │ │ ├── __init__.py │ │ └── scan_background_chores.py │ ├── services │ │ └── __init__.py │ └── admin.py ├── Finish │ ├── templatetags │ │ ├── __init__.py │ │ └── custom_tags.py │ ├── tests │ │ └── __init__.py │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0002_initial.py │ ├── apps.py │ ├── forms.py │ ├── useful_files_for_testing │ │ └── soln_spec_for_testing_shared_pages.toml │ ├── admin.py │ ├── management │ │ └── commands │ │ │ ├── plom_download_ta_info_csv.py │ │ │ ├── plom_download_marks_csv.py │ │ │ └── plom_build_all_soln.py │ ├── views │ │ ├── student_report.py │ │ └── __init__.py │ └── services │ │ ├── soln_images.py │ │ └── __init__.py ├── Rectangles │ ├── tests │ │ └── __init__.py │ ├── __init__.py │ ├── apps.py │ ├── services │ │ └── __init__.py │ └── urls.py ├── TestingSupport │ ├── config_files │ │ ├── __init__.py │ │ ├── just_demo_spec.toml │ │ ├── quick_demo_config.toml │ │ ├── long_demo_config.toml │ │ └── hw_bundle_config.toml │ ├── tests │ │ └── __init__.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── apps.py │ └── services │ │ ├── exceptions.py │ │ └── __init__.py ├── Profile │ ├── __init__.py │ ├── apps.py │ ├── edit_profile_form.py │ └── urls.py ├── scripts │ └── __init__.py ├── templates │ ├── QuestionClustering │ │ ├── modal_forms.html │ │ ├── fragments │ │ │ ├── clustering_tag_cell.html │ │ │ └── error_detail_modal.html │ │ └── clustering_jobs.html │ ├── Rubrics │ │ └── diff_partial.html │ ├── __init__.py │ ├── base │ │ ├── alert_messages.html │ │ ├── alert_message.html │ │ └── base-2col.html │ ├── Progress │ │ ├── Identify │ │ │ └── id_image_wrap_fragment.html │ │ └── Mark │ │ │ ├── annotation_image_wrap_fragment.html │ │ │ └── original_image_wrap_fragment.html │ ├── Scan │ │ └── fragments │ │ │ └── bundle_page_img_tag.html │ ├── Authentication │ │ ├── maintenance.html │ │ ├── no_group.html │ │ ├── activation_invalid.html │ │ ├── unauthorized.html │ │ ├── signup_multiple_users.html │ │ ├── home.html │ │ └── signup_import_users.html │ ├── SpecCreator │ │ ├── summary-question.html │ │ └── validation.html │ ├── Visualization │ │ ├── histogram.html │ │ └── heat_map.html │ ├── Finish │ │ ├── finish_no_spec.html │ │ ├── finish_not_printed.html │ │ └── soln_source_attempt.html │ ├── Rectangles │ │ └── home.html │ ├── 403.html │ ├── Preparation │ │ └── pqv_mapping_attempt.html │ └── BuildPaperPDF │ │ └── cannot_find_pdf.html ├── Base │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── services │ │ └── __init__.py │ ├── apps.py │ ├── admin.py │ ├── urls.py │ └── tests_settings.py ├── UserManagement │ ├── tests │ │ └── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── admin.py │ ├── __init__.py │ ├── apps.py │ ├── services │ │ └── __init__.py │ └── models.py ├── Authentication │ ├── tests │ │ └── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── services │ │ └── __init__.py │ ├── __init__.py │ ├── forms │ │ ├── __init__.py │ │ └── choices.py │ ├── admin.py │ ├── apps.py │ └── models.py ├── Papers │ ├── migrations │ │ └── __init__.py │ ├── __init__.py │ ├── tests │ │ └── __init__.py │ ├── apps.py │ ├── services │ │ └── __init__.py │ └── models │ │ └── __init__.py ├── QuestionClustering │ ├── migrations │ │ └── __init__.py │ ├── __init__.py │ ├── apps.py │ ├── exceptions │ │ ├── job_exception.py │ │ └── clustering_exception.py │ ├── admin.py │ ├── services │ │ ├── __init__.py │ │ └── model_loader.py │ └── forms.py ├── SpecCreator │ ├── migrations │ │ └── __init__.py │ ├── __init__.py │ ├── services │ │ └── __init__.py │ ├── apps.py │ ├── views │ │ ├── __init__.py │ │ ├── base.py │ │ ├── spec_upload.py │ │ └── spec_download.py │ └── urls.py ├── Mark │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── tests │ │ ├── tiny_qvmap.toml │ │ ├── __init__.py │ │ └── tiny_spec.toml │ ├── serializers │ │ ├── __init__.py │ │ ├── annotations.py │ │ └── tasks.py │ ├── apps.py │ ├── models │ │ └── __init__.py │ ├── services │ │ └── __init__.py │ └── admin.py ├── TaskOrder │ ├── __init__.py │ ├── services │ │ └── __init__.py │ ├── apps.py │ └── urls.py ├── Visualization │ ├── __init__.py │ ├── apps.py │ ├── urls.py │ └── views.py ├── BuildPaperPDF │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0002_initial.py │ ├── admin.py │ ├── apps.py │ └── services │ │ └── __init__.py ├── client_path_hack.sh ├── demo_files │ ├── bundle_for_quick_demo.toml │ ├── demo_solution_spec.toml │ ├── cl_for_quick_demo.csv │ ├── demo_assessment_qtags.csv │ ├── bundle_for_long_demo.toml │ ├── demo_assessment_spec.toml │ └── bundle_for_plaid_demo.toml ├── huey │ └── .gitignore ├── asgi.py ├── wsgi.py ├── __init__.py └── .gitignore ├── plom_ml ├── __init__.py ├── clustering │ ├── __init__.py │ ├── model │ │ ├── __init__.py │ │ ├── model_type.py │ │ └── model_config.yaml │ ├── pipeline │ │ └── __init__.py │ ├── preprocessing │ │ └── __init__.py │ ├── embedding │ │ └── __init__.py │ └── exceptions.py └── exceptions.py ├── contrib ├── papers_to_rooms.csv ├── README.txt ├── onlinedist-README.txt ├── onlinedist_01_clean_canvas.py └── onlinedist_05b_rename_papers_spares.py ├── .gitlab ├── issue_templates │ └── default.md └── merge_request_templates │ └── default.md ├── .dockerignore ├── .readthedocs.yaml ├── .codespell-ignorelines ├── maint └── maint-wipe-rebuild-migrations.sh ├── .codespell-ignorewords ├── .flake8 ├── manage.py └── nginx └── default.conf /doc/source/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/source/_templates/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plom/idBox2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/idBox2.pdf -------------------------------------------------------------------------------- /plom/idBox4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/idBox4.pdf -------------------------------------------------------------------------------- /plom/scan/test_rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/scan/test_rgb.png -------------------------------------------------------------------------------- /testTemplates/idBox.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/idBox.pdf -------------------------------------------------------------------------------- /testTemplates/idBox2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/idBox2.pdf -------------------------------------------------------------------------------- /testTemplates/idBox4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/idBox4.pdf -------------------------------------------------------------------------------- /plom/test_target_latex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/test_target_latex.png -------------------------------------------------------------------------------- /org.plomgrading.PlomClient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/org.plomgrading.PlomClient.png -------------------------------------------------------------------------------- /plom/scan/test_zbar_fails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/scan/test_zbar_fails.png -------------------------------------------------------------------------------- /plom/test_target_latex_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/test_target_latex_old.png -------------------------------------------------------------------------------- /plom_server/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom_server/static/favicon.ico -------------------------------------------------------------------------------- /plom_server/Contrib/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Divy Patel 3 | -------------------------------------------------------------------------------- /plom_server/Reports/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Divy Patel 3 | -------------------------------------------------------------------------------- /plom_server/Tags/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | -------------------------------------------------------------------------------- /plom_server/static/plomLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom_server/static/plomLogo.png -------------------------------------------------------------------------------- /testTemplates/dummy_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/dummy_qr_code.png -------------------------------------------------------------------------------- /plom_server/QuestionTags/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | -------------------------------------------------------------------------------- /testTemplates/dummy_left_staple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/dummy_left_staple.png -------------------------------------------------------------------------------- /testTemplates/dummy_qr_code_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/dummy_qr_code_red.png -------------------------------------------------------------------------------- /plom/create/fonts/adr_handwriting.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/create/fonts/adr_handwriting.ttf -------------------------------------------------------------------------------- /plom/create/fonts/bt_handwriting.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/create/fonts/bt_handwriting.ttf -------------------------------------------------------------------------------- /plom/create/fonts/ejx_handwriting.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/create/fonts/ejx_handwriting.ttf -------------------------------------------------------------------------------- /plom/create/fonts/ld_handwriting.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/create/fonts/ld_handwriting.ttf -------------------------------------------------------------------------------- /plom/create/fonts/nh_handwriting.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/create/fonts/nh_handwriting.ttf -------------------------------------------------------------------------------- /plom/create/fonts/pdl_handwriting.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom/create/fonts/pdl_handwriting.ttf -------------------------------------------------------------------------------- /plom_server/API/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | -------------------------------------------------------------------------------- /plom_server/Identify/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Colin B. Macdonald 3 | -------------------------------------------------------------------------------- /plom_server/Launcher/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | -------------------------------------------------------------------------------- /plom_server/Preparation/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Colin B. Macdonald 3 | -------------------------------------------------------------------------------- /plom_server/Progress/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Colin B. Macdonald 3 | -------------------------------------------------------------------------------- /plom_server/Rubrics/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Brennen Chiu 3 | -------------------------------------------------------------------------------- /plom_server/Scan/tests/id_page_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom_server/Scan/tests/id_page_img.png -------------------------------------------------------------------------------- /plom_server/static/defaultUserIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom_server/static/defaultUserIcon.png -------------------------------------------------------------------------------- /testTemplates/dummy_right_staple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/dummy_right_staple.png -------------------------------------------------------------------------------- /plom_server/Finish/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | -------------------------------------------------------------------------------- /plom_server/Rectangles/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | -------------------------------------------------------------------------------- /testTemplates/dummy_left_staple_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/dummy_left_staple_red.png -------------------------------------------------------------------------------- /testTemplates/dummy_right_staple_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/testTemplates/dummy_right_staple_red.png -------------------------------------------------------------------------------- /plom_server/TestingSupport/config_files/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Colin B. Macdonald 3 | -------------------------------------------------------------------------------- /plom_ml/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """plom_ml is an AI/ML package for plom.""" 4 | -------------------------------------------------------------------------------- /plom_ml/clustering/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """Clustering related modules.""" 4 | -------------------------------------------------------------------------------- /plom_server/Profile/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | -------------------------------------------------------------------------------- /plom/templateUserList.csv: -------------------------------------------------------------------------------- 1 | "user","password" 2 | "manager","1234" 3 | "scanner","4567" 4 | "reviewer","7890" 5 | "user0","0123" 6 | "user1","0123" 7 | "user2","0123" 8 | -------------------------------------------------------------------------------- /plom_server/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Scripts for the Plom server.""" 5 | -------------------------------------------------------------------------------- /plom_server/static/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Static files for Plom server.""" 5 | -------------------------------------------------------------------------------- /plom_server/templates/QuestionClustering/modal_forms.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /plom_server/Base/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022, 2025 Colin B. Macdonald 3 | 4 | """Base app of the Plom Server.""" 5 | -------------------------------------------------------------------------------- /plom_server/Finish/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Aidan Murphy 3 | 4 | from .test_student_mark import * # noqa 5 | -------------------------------------------------------------------------------- /plom_server/Tags/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from .tag_service import TagService 5 | -------------------------------------------------------------------------------- /plom/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020 Andrew Rechnitzer 3 | # Copyright (C) 2020-2021, 2023-2025 Colin B. Macdonald 4 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/config_files/just_demo_spec.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | test_spec = "demo" 5 | -------------------------------------------------------------------------------- /plom_server/Base/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | from .test_config import ServerConfigTests 5 | -------------------------------------------------------------------------------- /plom_server/UserManagement/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Aidan Murphy 3 | 4 | from .test_UsersService import * # noqa 5 | -------------------------------------------------------------------------------- /plom_server/templates/Rubrics/diff_partial.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | {{ diff|safe }} 7 | -------------------------------------------------------------------------------- /doc/source/changelog.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ```{include} ../../CHANGELOG.md 7 | ``` 8 | -------------------------------------------------------------------------------- /plom_server/Authentication/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Colin B. Macdonald 3 | 4 | from .test_create_users import * # noqa 5 | -------------------------------------------------------------------------------- /plom_server/Identify/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/Papers/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/test_version1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom_server/Preparation/useful_files_for_testing/test_version1.pdf -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/test_version2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plomgrading/plom/HEAD/plom_server/Preparation/useful_files_for_testing/test_version2.pdf -------------------------------------------------------------------------------- /plom_server/Rubrics/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/templates/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Template HTML content for the Plom server.""" 5 | -------------------------------------------------------------------------------- /plom_ml/clustering/model/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """Definition of all clustering models defined in plom_ml.""" 4 | -------------------------------------------------------------------------------- /plom_server/API/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Services supporting the API of the Plom Server.""" 5 | -------------------------------------------------------------------------------- /plom_server/Authentication/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/Preparation/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/QuestionTags/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | 4 | from .questiontag_service import QuestionTagService 5 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/UserManagement/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Migration files are autogenerated.""" 5 | -------------------------------------------------------------------------------- /plom_server/Base/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Colin B. Macdonald 3 | 4 | """Services of the Base app of the Plom Server.""" 5 | -------------------------------------------------------------------------------- /plom_ml/clustering/pipeline/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """A pipeline that orchestrates clustering components for inference.""" 4 | -------------------------------------------------------------------------------- /plom_server/Mark/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """The Plom Server Mark app.""" 6 | -------------------------------------------------------------------------------- /plom_server/Scan/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """The Plom Server Scan app.""" 6 | -------------------------------------------------------------------------------- /doc/source/_latex/preamble.tex: -------------------------------------------------------------------------------- 1 | % SPDX-License-Identifier: AGPL-3.0-or-later 2 | % Copyright (C) 2024 Aidan Murphy 3 | 4 | \usepackage{enumitem} 5 | \setlistdepth{99} 6 | 7 | \usepackage{common-unicode} 8 | -------------------------------------------------------------------------------- /plom_server/Finish/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Finish app of the Plom Server.""" 6 | -------------------------------------------------------------------------------- /plom_server/Rubrics/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | """The Plom Server Rubric app.""" 6 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | # Copyright (C) 2023 Andrew Rechnitzer 5 | -------------------------------------------------------------------------------- /plom_server/TaskOrder/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """The Plom Server TaskOrder app.""" 6 | -------------------------------------------------------------------------------- /plom_ml/clustering/preprocessing/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """Modules for raw inputs preprocessing that tend to help feature extraction.""" 4 | -------------------------------------------------------------------------------- /plom_server/Papers/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Andrew Rechnitzer 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | """The Plom Server Paper app.""" 6 | -------------------------------------------------------------------------------- /plom_server/Progress/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023-2024 Andrew Rechnitzer 4 | # Copyright (C) 2024 Colin B. Macdonald 5 | -------------------------------------------------------------------------------- /plom_server/Rectangles/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """The Plom Server Rectangle app.""" 6 | -------------------------------------------------------------------------------- /plom_server/Visualization/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Divy Patel 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """The Plom Server Visualization app.""" 6 | -------------------------------------------------------------------------------- /contrib/papers_to_rooms.csv: -------------------------------------------------------------------------------- 1 | room dir name,start,how many,"spare max(5, 5%)",end 2 | BigRoomA,1,893,45,938 3 | BigRoomB,1000,432,22,1453 4 | SmallRm1,1500,45,5,1549 5 | SmallRm2,1600,91,5,1695 6 | SmallRm2,1700,88,5,1792 7 | -------------------------------------------------------------------------------- /plom_server/Mark/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Migration files are autogenerated.""" 6 | -------------------------------------------------------------------------------- /plom_server/Scan/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Migration files are autogenerated.""" 6 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """The Plom Server TestingSupport app.""" 6 | -------------------------------------------------------------------------------- /plom_server/Finish/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Migration files are autogenerated.""" 6 | -------------------------------------------------------------------------------- /plom_server/QuestionTags/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Migration files are autogenerated.""" 6 | -------------------------------------------------------------------------------- /plom_server/UserManagement/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | 4 | from django.contrib import admin 5 | from .models import Quota 6 | 7 | admin.site.register(Quota) 8 | -------------------------------------------------------------------------------- /plom_server/static/css/loginPage.css: -------------------------------------------------------------------------------- 1 | .login_box{ 2 | background-color: #AD9CFF; 3 | } 4 | 5 | .login_card{ 6 | background-color: #F5F6FA; 7 | } 8 | 9 | .btn{ 10 | background-color: #7A7CFF; 11 | } 12 | -------------------------------------------------------------------------------- /plom_server/Launcher/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Migration files are autogenerated.""" 6 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Migration files are autogenerated.""" 6 | -------------------------------------------------------------------------------- /plom_server/API/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | """The API of the Plom Server.""" 7 | -------------------------------------------------------------------------------- /plom_server/Preparation/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | 4 | from .test_source_service import SourceServiceTests 5 | from .test_students import StagingStudentsTests 6 | -------------------------------------------------------------------------------- /plom_ml/clustering/embedding/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """Embedder related modules. 4 | 5 | Embedder is the component that generates features from certain raw inputs. 6 | """ 7 | -------------------------------------------------------------------------------- /plom_server/Authentication/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023-2024 Colin B. Macdonald 4 | 5 | from .authentication_services import AuthenticationServices 6 | -------------------------------------------------------------------------------- /plom_server/Mark/tests/tiny_qvmap.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | # paper_number = [question_1, question_2] 6 | 1 = [1, 1] 7 | 2 = [2, 2] 8 | -------------------------------------------------------------------------------- /plom_ml/exceptions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """Base exceptions for plom_ml package.""" 4 | 5 | 6 | class PlomMLException(Exception): 7 | """A base exception for plom_ml package.""" 8 | -------------------------------------------------------------------------------- /plom_server/API/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | """Migration files are autogenerated.""" 7 | -------------------------------------------------------------------------------- /plom_server/BuildPaperPDF/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | """The Plom Server BuildPaperPDF app.""" 7 | -------------------------------------------------------------------------------- /plom_server/TaskOrder/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Services of the Plom Server TaskOrder app.""" 5 | 6 | from .task_ordering_service import TaskOrderService 7 | -------------------------------------------------------------------------------- /plom_server/UserManagement/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Chris Jin 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | # Copyright (C) 2024 Elisa Pan 5 | 6 | """The Plom Server UserManagement app.""" 7 | -------------------------------------------------------------------------------- /plom_server/Authentication/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2022 Edith Coates 4 | # Copyright (C) 2023, 2025 Colin B. Macdonald 5 | 6 | """Migration files are autogenerated.""" 7 | -------------------------------------------------------------------------------- /plom_server/Papers/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from .test_paper_creator import * # noqa 6 | from .test_image_bundle import * # noqa 7 | -------------------------------------------------------------------------------- /plom_server/BuildPaperPDF/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | """Migration files are autogenerated.""" 7 | -------------------------------------------------------------------------------- /doc/source/module-plom-scan.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom.scan`` module 6 | -------------------- 7 | 8 | .. automodule:: plom.scan 9 | :members: 10 | -------------------------------------------------------------------------------- /plom_server/Authentication/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Brennen Chiu 3 | 4 | from .choices import ( 5 | USERNAME_CHOICES, 6 | USER_TYPE_WITH_MANAGER_CHOICES, 7 | USER_TYPE_WITHOUT_MANAGER_CHOICES, 8 | ) 9 | -------------------------------------------------------------------------------- /doc/source/module-plom-create.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom.create`` module 6 | ---------------------- 7 | 8 | .. automodule:: plom.create 9 | :members: 10 | -------------------------------------------------------------------------------- /doc/source/module-plom-finish.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom.finish`` module 6 | ---------------------- 7 | 8 | .. automodule:: plom.finish 9 | :members: 10 | -------------------------------------------------------------------------------- /plom_server/Mark/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from .test_marking_task_service import * # noqa 6 | from .test_marking_task_service_config import * # noqa 7 | -------------------------------------------------------------------------------- /doc/source/development.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2024 Colin B. Macdonald 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | Development 5 | =========== 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | install-get-source 11 | howto_run_the_demo.md 12 | dev_guide 13 | -------------------------------------------------------------------------------- /doc/source/plom-demo.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-demo`` 6 | ------------- 7 | 8 | .. argparse:: 9 | :ref: plom.demo.__main__.get_parser 10 | :prog: plom-demo 11 | -------------------------------------------------------------------------------- /doc/source/plom-scan.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-scan`` 6 | ------------- 7 | 8 | .. argparse:: 9 | :ref: plom.scan.__main__.get_parser 10 | :prog: plom-scan 11 | -------------------------------------------------------------------------------- /plom_server/Launcher/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | 4 | from .launch_demo_bundle_creator import DemoBundleCreationService 5 | from .launch_demo_homework_bundle_creator import DemoHWBundleCreationService 6 | -------------------------------------------------------------------------------- /plom_server/Tags/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class TagsConfig(AppConfig): 8 | default_auto_field = "django.db.models.BigAutoField" 9 | name = "Tags" 10 | -------------------------------------------------------------------------------- /doc/source/module-plom-solutions.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom.solutions`` module 6 | ------------------------- 7 | 8 | .. automodule:: plom.solutions 9 | :members: 10 | -------------------------------------------------------------------------------- /contrib/README.txt: -------------------------------------------------------------------------------- 1 | Plom contributed scripts 2 | ======================== 3 | 4 | These are various scripts that people have written or found useful. 5 | 6 | Caution: they have various degrees of reliability. 7 | 8 | Some of these features may eventually find their way into the main Plom module. 9 | -------------------------------------------------------------------------------- /doc/source/plom-create.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-create`` 6 | --------------- 7 | 8 | .. argparse:: 9 | :ref: plom.create.__main__.get_parser 10 | :prog: plom-create 11 | -------------------------------------------------------------------------------- /doc/source/plom-finish.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-finish`` 6 | --------------- 7 | 8 | .. argparse:: 9 | :ref: plom.finish.__main__.get_parser 10 | :prog: plom-finish 11 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | # Copyright (C) 2023 Andrew Rechnitzer 5 | 6 | from .template_spec_builder import TemplateSpecBuilderService 7 | -------------------------------------------------------------------------------- /doc/source/install.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020, 2023-2024 Colin B. Macdonald 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | Installing Plom 5 | =============== 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | install-client 11 | install-server 12 | install-from-source 13 | -------------------------------------------------------------------------------- /plom/test_version.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020, 2023-2025 Colin B. Macdonald 3 | 4 | from packaging.version import Version 5 | 6 | from plom.common import __version__ 7 | 8 | 9 | def test_valid_version() -> None: 10 | Version(__version__) 11 | -------------------------------------------------------------------------------- /plom_server/Finish/templatetags/custom_tags.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from django.template.defaulttags import register 5 | 6 | 7 | @register.filter 8 | def get_item(dictionary, key): 9 | return dictionary.get(key) 10 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/default.md: -------------------------------------------------------------------------------- 1 | # Issue template 2 | 3 | ## Description 4 | 5 | Be as clear as possible. 6 | 7 | 8 | ## Steps to reproduce 9 | 10 | 1. ... 11 | 2. ... 12 | 3. ... 13 | 4. ... 14 | 15 | 16 | ## Version information 17 | 18 | - Plom version: [xxx] 19 | - OS and version: [xxx] 20 | -------------------------------------------------------------------------------- /doc/source/plom-solutions.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-solutions`` 6 | ------------------ 7 | 8 | .. argparse:: 9 | :ref: plom.solutions.__main__.get_parser 10 | :prog: plom-solutions 11 | -------------------------------------------------------------------------------- /plom_server/API/views/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | from .base import ManagerReadOnlyViewSet 5 | from .rubrics import RubricViewSet 6 | from .annotations import AnnotationViewSet 7 | from .marking_tasks import MarkingTaskViewSet 8 | -------------------------------------------------------------------------------- /doc/source/plom-new-demo.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-new-demo`` 6 | ----------------- 7 | 8 | .. argparse:: 9 | :ref: plom_server.scripts.launch_plom_demo_server.get_parser 10 | :prog: plom-new-demo 11 | -------------------------------------------------------------------------------- /plom_server/Mark/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Serializers of the Plom Server Mark app.""" 6 | 7 | from .annotations import AnnotationSerializer 8 | from .tasks import MarkingTaskSerializer 9 | -------------------------------------------------------------------------------- /doc/source/plom-new-server.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-new-server`` 6 | ------------------- 7 | 8 | .. argparse:: 9 | :ref: plom_server.scripts.launch_plom_server.get_parser 10 | :prog: plom-new-server 11 | -------------------------------------------------------------------------------- /plom_server/Progress/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023-2024 Andrew Rechnitzer 4 | # Copyright (C) 2023 Brennen Chiu 5 | 6 | from .progress_overview import ProgressOverviewService 7 | from .userinfo_service import UserInfoServices 8 | -------------------------------------------------------------------------------- /plom_server/API/permissions/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | 5 | from .permissions import ( 6 | AllowAnyReadOnly, 7 | IsManager, 8 | IsManagerReadOnly, 9 | IsManagerOrAuthenticatedReadOnly, 10 | ) 11 | -------------------------------------------------------------------------------- /contrib/onlinedist-README.txt: -------------------------------------------------------------------------------- 1 | Online Distribution of exam papers 2 | ================================== 3 | 4 | This workflow distributes one PDF file to each student. Typically 5 | that PDF file is a Plom exam, with random versions, the same file we 6 | would print and give out in an exam room in a tradition in-person 7 | examination. 8 | -------------------------------------------------------------------------------- /plom_server/static/css/profile.css: -------------------------------------------------------------------------------- 1 | #name-label, 2 | #email-label { 3 | margin-bottom: 0; 4 | } 5 | 6 | .card-body button, 7 | .form button, 8 | #cancel{ 9 | padding: 5px 10px; 10 | border-radius: 5px; 11 | } 12 | 13 | #id_first_name, 14 | #id_email { 15 | border-radius: 5px; 16 | border-width: 1px; 17 | } 18 | -------------------------------------------------------------------------------- /plom_server/Authentication/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | 5 | from django.contrib import admin 6 | 7 | from .models import Profile 8 | 9 | # This makes models appear in the admin interface 10 | admin.site.register(Profile) 11 | -------------------------------------------------------------------------------- /plom_server/Scan/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class ScanConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Scan" 11 | -------------------------------------------------------------------------------- /plom_server/Mark/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class MarkConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Mark" 11 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | """QuestionClustering package. 5 | 6 | Provides tools and services for grouping student papers 7 | by question and version, so that identical or similar responses 8 | can be clustered together. 9 | """ 10 | -------------------------------------------------------------------------------- /plom_server/Finish/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class FinishConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Finish" 11 | -------------------------------------------------------------------------------- /plom_server/Papers/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Andrew Rechnitzer 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class CoreConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Papers" 11 | -------------------------------------------------------------------------------- /plom_server/Profile/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class AccountConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Profile" 11 | -------------------------------------------------------------------------------- /plom_server/Rubrics/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class RubricsConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Rubrics" 11 | -------------------------------------------------------------------------------- /plom_server/Identify/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class IdentifyConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Identify" 11 | -------------------------------------------------------------------------------- /plom_server/Progress/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class ProgressConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Progress" 11 | -------------------------------------------------------------------------------- /plom_server/TaskOrder/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class TaskOrderConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.TaskOrder" 11 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class DemoConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.TestingSupport" 11 | -------------------------------------------------------------------------------- /plom_server/Rectangles/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class RectanglesConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Rectangles" 11 | -------------------------------------------------------------------------------- /plom_server/UserManagement/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Chris Jin 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class MainConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.UserManagement" 11 | -------------------------------------------------------------------------------- /plom_server/Visualization/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Divy Patel 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class VisualizationConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Visualization" 11 | -------------------------------------------------------------------------------- /doc/source/plom-cli.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-cli`` 6 | ------------ 7 | 8 | An under-development tool for scripting and command-line access to Plom servers. 9 | 10 | .. argparse:: 11 | :ref: plom.cli.__main__.get_parser 12 | :prog: plom-cli 13 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class SpecCreatorConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.SpecCreator" 11 | -------------------------------------------------------------------------------- /plom_server/client_path_hack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # Copyright (C) 2022-2023 Colin B. Macdonald 5 | # Copyright (C) 2022 Edith Coates 6 | # Copyright (C) 2022 Brennen Chiu 7 | # Copyright (C) 2023 Andrew Rechnitzer 8 | 9 | set -e 10 | 11 | export PYTHONPATH=".." 12 | 13 | python3 -m plom.client $@ 14 | -------------------------------------------------------------------------------- /plom_server/API/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | from django.apps import AppConfig 7 | 8 | 9 | class ApiConfig(AppConfig): 10 | default_auto_field = "django.db.models.BigAutoField" 11 | name = "plom_server.API" 12 | -------------------------------------------------------------------------------- /plom_server/Authentication/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class AuthenticationConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Authentication" 11 | -------------------------------------------------------------------------------- /plom_server/Mark/models/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023 Andrew Rechnitzer 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | """Models of the Plom Server Mark app.""" 7 | 8 | from .tasks import MarkingTask, MarkingTaskTag 9 | from .annotations import AnnotationImage, Annotation 10 | -------------------------------------------------------------------------------- /plom_server/Preparation/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Andrew Rechnitzer 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class PreparationConfig(AppConfig): 9 | default_auto_field = "django.db.models.BigAutoField" 10 | name = "plom_server.Preparation" 11 | -------------------------------------------------------------------------------- /plom_server/templates/base/alert_messages.html: -------------------------------------------------------------------------------- 1 | 6 | {% if messages %} 7 | {% for message in messages %} 8 | {% include "./alert_message.html" with message=message %} 9 | {% endfor %} 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /plom/messenger/test_messengers.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2021, 2023-2025 Colin B. Macdonald 3 | 4 | from pytest import raises 5 | from .messenger import Messenger 6 | 7 | 8 | def test_invalid_url_too_many_colons() -> None: 9 | with raises(Exception): 10 | m = Messenger("example.com:1234:1234") 11 | m.start() 12 | -------------------------------------------------------------------------------- /plom_server/QuestionTags/serializers.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Aden Chan 3 | 4 | from rest_framework import serializers 5 | 6 | from .models import PedagogyTag 7 | 8 | 9 | class PedagogyTagSerializer(serializers.ModelSerializer): 10 | 11 | class Meta: 12 | model = PedagogyTag 13 | fields = "__all__" 14 | -------------------------------------------------------------------------------- /plom_server/Rubrics/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Brennen Chiu 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | 5 | from django.contrib import admin 6 | 7 | from .models import Rubric, RubricPane 8 | 9 | # This makes models appear in the admin interface 10 | admin.site.register(Rubric) 11 | admin.site.register(RubricPane) 12 | -------------------------------------------------------------------------------- /plom_server/UserManagement/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Andrew Rechnitzer 3 | # Copyright (C) 2024 Elisa Pan 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | # Copyright (C) 2025 Bryan Tanady 6 | 7 | """Services of the Plom Server UserManagement app.""" 8 | 9 | from .UsersService import get_users_groups_info 10 | -------------------------------------------------------------------------------- /plom_server/static/css3rdparty/README.txt: -------------------------------------------------------------------------------- 1 | Third-party CSS libraries 2 | ========================= 3 | 4 | Any libraries that we need to keep a copy of in-tree 5 | (that is, in the git repo) could be stored here. 6 | 7 | Note: most such files are downloaded; they don't live here, 8 | b/c this directory is probably readonly: we cannot download 9 | to it (Issue #2932). 10 | -------------------------------------------------------------------------------- /plom_server/Base/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2022 Edith Coates 4 | # Copyright (C) 2024, 2025 Colin B. Macdonald 5 | 6 | from django.apps import AppConfig 7 | 8 | 9 | class BaseConfig(AppConfig): 10 | default_auto_field = "django.db.models.BigAutoField" 11 | name = "plom_server.Base" 12 | -------------------------------------------------------------------------------- /plom_server/static/js3rdparty/README.txt: -------------------------------------------------------------------------------- 1 | Third-party Javascript libraries 2 | ================================ 3 | 4 | Any libraries that we need to keep a copy of in-tree 5 | (that is, in the git repo) could be stored here. 6 | 7 | Note: most such files are downloaded; they don't live here, 8 | b/c this directory is probably readonly: we cannot download 9 | to it (Issue #2932). 10 | -------------------------------------------------------------------------------- /plom_server/templates/Progress/Identify/id_image_wrap_fragment.html: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /plom/canvas/README.md: -------------------------------------------------------------------------------- 1 | # Plom–Canvas integration 2 | This is a collection of scripts and functions to interface Plom with 3 | Canvas using the [Canvas API](https://canvas.instructure.com/doc/api) via 4 | via the [`canvasapi` Python library](https://github.com/ucfopen/canvasapi). 5 | 6 | For now, see the `plom-push-to-canvas.py` and `plom-server-from-canvas.py` 7 | scripts in the `contrib/` folder. 8 | -------------------------------------------------------------------------------- /plom_server/BuildPaperPDF/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2023 Colin B. Macdonald 5 | 6 | from django.contrib import admin 7 | 8 | from .models import BuildPaperPDFChore 9 | 10 | # This makes models appear in the admin interface 11 | admin.site.register(BuildPaperPDFChore) 12 | -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/cl_warn.csv: -------------------------------------------------------------------------------- 1 | "id","name","paper_number" 2 | 10050380,"Fink, Iris",1 3 | 10060000,"Babbage, Johnson",2 4 | 10130103,"Vandeventer, Irene", 5 | 10152155,"Little, Abigail",3 6 | 10203891,"Coleman, Ashley", 7 | 10399145,"Obrien, John 漢字",5 8 | 10419996,"Robinson, Glen", 9 | 10433917,"Wood, James",4 10 | 10493869,"Titus, Ruben", 11 | 11015491,"C", 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2020 Colin B. Macdonald 3 | # 4 | # Copying and distribution of this file, with or without modification, 5 | # are permitted in any medium without royalty provided the copyright 6 | # notice and this notice are preserved. This file is offered as-is, 7 | # without any warranty. 8 | 9 | .git 10 | .cache 11 | 12 | **/__pycache__ 13 | -------------------------------------------------------------------------------- /plom_server/BuildPaperPDF/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | from django.apps import AppConfig 7 | 8 | 9 | class BuildtestpdfConfig(AppConfig): 10 | default_auto_field = "django.db.models.BigAutoField" 11 | name = "plom_server.BuildPaperPDF" 12 | -------------------------------------------------------------------------------- /plom_server/BuildPaperPDF/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2023 Andrew Rechnitzer 5 | # Copyright (C) 2025 Colin B. Macdonald 6 | 7 | """Services of the Plom Server BuildPaperPDF app.""" 8 | 9 | from .build_papers import huey_build_single_paper, BuildPapersService 10 | -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/cl_errs.csv: -------------------------------------------------------------------------------- 1 | "id","name","paper_number" 2 | 10050380,"Fink, Iris",1 3 | 10060000,"Babbage, Johnson",2 4 | 10130103,"Vandeventer, Irene",3 5 | 10152155,"Little, Abigail", 6 | 10203891,"Coleman, Ashley", 7 | 10399145,"Obrien, John 漢字", 8 | 10419996,"Robinson, Glen",4 9 | 10433917,"Wood, James",2 10 | 2,"Titus, Ruben", 11 | 11015491,"Crosby, Mary", 12 | -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/cl_good.csv: -------------------------------------------------------------------------------- 1 | "id","name","paper_number" 2 | 10050380,"Fink, Iris",1 3 | 10060000,"Babbage, Johnson",2 4 | 10130103,"Vandeventer, Irene",3 5 | 10152155,"Little, Abigail",4 6 | 10203891,"Coleman, Ashley",5 7 | 10399145,"Obrien, John", 8 | 10419996,"Robinson, Glen", 9 | 10433917,"Wood, James", 10 | 10493869,"Titus, Ruben", 11 | 11015491,"Crosby, Mary", 12 | -------------------------------------------------------------------------------- /plom_server/demo_files/bundle_for_quick_demo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2025 Andrew Rechnitzer 3 | 4 | # bundle 1 5 | [[bundles]] 6 | first_paper = 1 7 | last_paper = 10 8 | 9 | 10 | # HW bundle 1 11 | [[hw_bundles]] 12 | paper_number = 21 13 | student_id = 98989898 14 | student_name = "Kenson, Ken" 15 | pages = [[1], [2], [], [2, 3], [3, 4]] 16 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class QuestionClusteringConfig(AppConfig): 8 | """Configuration for the QuestionClustering Django app.""" 9 | 10 | default_auto_field = "django.db.models.BigAutoField" 11 | name = "plom_server.QuestionClustering" 12 | -------------------------------------------------------------------------------- /doc/source/plom-client.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023, 2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ``plom-client`` 6 | --------------- 7 | 8 | This command is provided by the `plom-client` package which you can 9 | install with ``pip install plom-client`` and is developed at 10 | the `Plom-Client repo `_. 11 | -------------------------------------------------------------------------------- /plom/cli/task_tools.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | from typing import Any 5 | 6 | from plom.cli import with_messenger 7 | 8 | 9 | @with_messenger 10 | def reset_task(papernum: int, question_idx: int, *, msgr) -> dict[str, Any]: 11 | """Upload a bundle from a local pdf file.""" 12 | return msgr.reset_task(papernum, question_idx) 13 | -------------------------------------------------------------------------------- /plom_server/Rubrics/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | """Services of the Plom Server Rubric app.""" 6 | 7 | from .rubric_service import RubricService 8 | from .rubric_permissions import RubricPermissionsService 9 | from .utils import _list_of_rubrics_to_dict_of_dict, _Rubric_to_dict 10 | -------------------------------------------------------------------------------- /plom_server/Preparation/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2024 Andrew Rechnitzer 3 | # Copyright (C) 2022 Edith Coates 4 | # Copyright (C) 2023-2024 Colin B. Macdonald 5 | 6 | from .prenaming_service import PrenameSettingService 7 | from .classlist import StagingStudentService 8 | from .pqv_mapping import PQVMappingService 9 | from .mocker import ExamMockerService 10 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/exceptions/job_exception.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | 5 | class ClusteringJobError(Exception): 6 | """Base exception for clustering job.""" 7 | 8 | pass 9 | 10 | 11 | class DuplicateClusteringJobError(ClusteringJobError): 12 | """Raised when trying to create a duplicate clsutering job.""" 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/services/exceptions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | 5 | class PlomConfigError(Exception): 6 | """An error for an invalid server config file.""" 7 | 8 | pass 9 | 10 | 11 | class PlomConfigCreationError(Exception): 12 | """An error for an invalid state while creating a server from a config file.""" 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | from django.contrib import admin 5 | 6 | from .models import QVCluster, QVClusterLink, QuestionClusteringChore 7 | 8 | # This makes models appear in the admin interface 9 | admin.site.register(QVCluster) 10 | admin.site.register(QVClusterLink) 11 | admin.site.register(QuestionClusteringChore) 12 | -------------------------------------------------------------------------------- /plom_server/Visualization/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-Licence-Identifier: AGPL-3.0-or-later 2 | # Copyright (c) 2023 Divy Patel 3 | # Copyright (c) 2024 Colin B. Macdonald 4 | 5 | from django.urls import path 6 | 7 | from .views import HistogramView, HeatMapView 8 | 9 | 10 | urlpatterns = [ 11 | path("histogram/", HistogramView.as_view(), name="histogram"), 12 | path("heat_map/", HeatMapView.as_view(), name="heat_map"), 13 | ] 14 | -------------------------------------------------------------------------------- /plom_server/templates/Scan/fragments/bundle_page_img_tag.html: -------------------------------------------------------------------------------- 1 | 7 | 12 | -------------------------------------------------------------------------------- /plom_server/huey/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2022-2023 Colin B. Macdonald 3 | # Copyright (C) 2022 Brennen Chiu 4 | # 5 | # Copying and distribution of this file, with or without modification, 6 | # are permitted in any medium without royalty provided the copyright 7 | # notice and this notice are preserved. This file is offered as-is, 8 | # without any warranty. 9 | 10 | *.sqlite3 11 | *.sqlite3* 12 | -------------------------------------------------------------------------------- /plom_server/API/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | from .annotation_patterns import ( 5 | AnnotationPatterns, 6 | AnnotationImagePatterns, 7 | PagedataPatterns, 8 | ) 9 | from .id_patterns import IdURLPatterns 10 | from .mark_patterns import MarkURLPatterns 11 | from .misc_patterns import MiscURLPatterns 12 | from .tags_patterns import TagsURLPatterns 13 | -------------------------------------------------------------------------------- /plom_server/asgi.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2022 Edith Coates 4 | # Copyright (C) 2023, 2025 Colin B. Macdonald 5 | 6 | """ASGI config for the Plom Server project.""" 7 | 8 | import os 9 | 10 | from django.core.asgi import get_asgi_application 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 13 | 14 | application = get_asgi_application() 15 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/maintenance.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "base/base.html" %} 7 | {% block title %} 8 | Under Maintenance 9 | {% endblock title %} 10 | {% block main_content %} 11 |

Sorry!

12 |

Page not implemented yet.

13 | {% endblock main_content %} 14 | -------------------------------------------------------------------------------- /plom_server/Preparation/views/needs_manager_view.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Andrew Rechnitzer 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | 5 | from braces.views import GroupRequiredMixin, LoginRequiredMixin 6 | from django.views import View 7 | 8 | 9 | class ManagerRequiredBaseView(LoginRequiredMixin, GroupRequiredMixin, View): 10 | login_url = "login" 11 | group_required = ["manager"] 12 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | """QuestionClustering.services. 5 | 6 | Contains all services related to question clustering app. 7 | """ 8 | 9 | 10 | from .question_clustering_service import ( 11 | QuestionClusteringJobService, 12 | QuestionClusteringService, 13 | ) 14 | 15 | from .model_loader import get_ClusteringStrategy 16 | -------------------------------------------------------------------------------- /plom_server/Papers/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2025 Andrew Rechnitzer 3 | # Copyright (C) 2022-2023 Edith Coates 4 | # Copyright (C) 2023, 2025 Colin B. Macdonald 5 | 6 | """Services of the Plom Server Paper app.""" 7 | 8 | from .paper_creator import PaperCreatorService 9 | from .paper_info import PaperInfoService, fixedpage_version_count 10 | from .image_bundle import ImageBundleService 11 | -------------------------------------------------------------------------------- /plom_server/Finish/forms.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from django import forms 5 | 6 | 7 | class StudentMarksFilterForm(forms.Form): 8 | version_info = forms.BooleanField(required=False, label="Version Information") 9 | timing_info = forms.BooleanField(required=False, label="Timing Information") 10 | warning_info = forms.BooleanField(required=False, label="Warning Information") 11 | -------------------------------------------------------------------------------- /plom_server/Identify/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023 Natalie Balashov 4 | # Copyright (C) 2023 Brennen Chiu 5 | # Copyright (C) 2023-2024 Andrew Rechnitzer 6 | 7 | from .id_tasks import IdentifyTaskService 8 | from .id_reader import IDReaderService, IDBoxProcessorService 9 | from .id_progress import IDProgressService 10 | from .id_direct import IDDirectService 11 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/exceptions/clustering_exception.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | 5 | class ClusteringCleanupError(Exception): 6 | """Base exception for clustering-cleanup related operations.""" 7 | 8 | pass 9 | 10 | 11 | class EmptySelectedError(ClusteringCleanupError): 12 | """Exception when there is no selection in selecting-based operations.""" 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /plom_server/Scan/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023-2024 Andrew Rechnitzer 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | from .test_scanner import ScanServiceTests 7 | from .test_scan_cast import ScanCastServiceTests 8 | from .test_image_process import PageImageProcessorTests 9 | from .test_manage_scan import * # noqa 10 | from .test_manage_discard import * # noqa 11 | -------------------------------------------------------------------------------- /plom_server/demo_files/demo_solution_spec.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2024 Andrew Rechnitzer 3 | 4 | # Solution spec for 'Midterm Demo using Plom' 5 | 6 | numberOfPages = 7 7 | 8 | # for question 1 9 | [[solution]] 10 | pages = [3] 11 | 12 | # for question 2 13 | [[solution]] 14 | pages = [4,5] 15 | 16 | # for question 3 17 | [[solution]] 18 | pages = [6] 19 | 20 | # for question 4 21 | [[solution]] 22 | pages = [7] 23 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024-2025 Colin B. Macdonald 4 | # Copyright (C) 2024 Andrew Rechnitzer 5 | 6 | """Services of the Plom Server TestingSupport app.""" 7 | 8 | from .exceptions import PlomConfigError, PlomConfigCreationError 9 | from .ConfigFileService import ( 10 | PlomServerConfig, 11 | DemoBundleConfig, 12 | DemoHWBundleConfig, 13 | ) 14 | -------------------------------------------------------------------------------- /plom_server/Reports/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Divy Patel 3 | # Copyright (C) 2023 Julian Lapenna 4 | 5 | from django.urls import path 6 | 7 | from . import views 8 | 9 | 10 | urlpatterns = [ 11 | path("", views.ReportLandingPageView.as_view(), name="reports_landing"), 12 | path( 13 | "report_download/", 14 | views.ReportLandingPageView.report_download, 15 | name="report_download", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /plom_ml/clustering/model/model_type.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | # from enum import StrEnum 5 | 6 | from enum import Enum 7 | 8 | 9 | class ClusteringType(str, Enum): 10 | """Defines what type of clustering task to tackle on. 11 | 12 | Attributes: 13 | MCQ: Multiple choice question (A-F, a-f). 14 | HME: Simple handwritten math expression. 15 | """ 16 | 17 | MCQ = "mcq" 18 | HME = "hme" 19 | -------------------------------------------------------------------------------- /plom_server/wsgi.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | """WSGI config for the Plom Server project.""" 6 | 7 | import os 8 | 9 | from django.core.wsgi import get_wsgi_application 10 | from whitenoise import WhiteNoise 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plom_server.settings") 13 | 14 | application = get_wsgi_application() 15 | application = WhiteNoise(application) 16 | -------------------------------------------------------------------------------- /plom_server/Finish/useful_files_for_testing/soln_spec_for_testing_shared_pages.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Andrew Rechnitzer 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | # See spec_with_shared_pages.toml in Preparation 6 | 7 | numberOfPages = 5 8 | 9 | [[solution]] 10 | pages = [3] 11 | 12 | [[solution]] 13 | pages = [3] 14 | 15 | [[solution]] 16 | pages = [4] 17 | 18 | [[solution]] 19 | pages = [5] 20 | 21 | [[solution]] 22 | pages = [5] 23 | -------------------------------------------------------------------------------- /plom_server/Profile/edit_profile_form.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023-2024 Colin B. Macdonald 4 | 5 | from django.contrib.auth.forms import UserChangeForm 6 | from django.contrib.auth.models import User 7 | 8 | 9 | class EditProfileForm(UserChangeForm): 10 | class Meta: 11 | model = User 12 | # Note we only use first_name; last_name field from User unused 13 | fields = ["first_name", "email"] 14 | -------------------------------------------------------------------------------- /plom_server/Preparation/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Andrew Rechnitzer 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.contrib import admin 6 | 7 | from .models import ( 8 | PaperSourcePDF, 9 | StagingPQVMapping, 10 | StagingStudent, 11 | ) 12 | 13 | # This makes models appear in the admin interface 14 | admin.site.register(PaperSourcePDF) 15 | admin.site.register(StagingPQVMapping) 16 | admin.site.register(StagingStudent) 17 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2023-2025 Colin B. Macdonald 3 | 4 | # Copying and distribution of this file, with or without modification, 5 | # are permitted in any medium without royalty provided the copyright 6 | # notice and this notice are preserved. This file is offered as-is, 7 | # without any warranty. 8 | 9 | tomli~=2.2.1 10 | myst-parser~=4.0.1 11 | # Note sphinx >=8.2.0 requires Python 3.11 12 | sphinx~=8.1.3 13 | sphinx_rtd_theme~=3.0.2 14 | sphinx-argparse~=0.5.2 15 | -------------------------------------------------------------------------------- /plom_server/Mark/tests/tiny_spec.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024 Andrew Rechnitzer 4 | # Copyright (C) 2025 Aidan Murphy 5 | 6 | name = "tiny" 7 | longName = "tiny exam" 8 | 9 | numberOfVersions = 2 10 | numberOfPages = 4 11 | totalMarks = 2 12 | numberOfQuestions = 2 13 | idPage = 1 14 | doNotMarkPages = [] 15 | 16 | 17 | [[question]] 18 | pages = [2] 19 | mark = 1 20 | 21 | 22 | [[question]] 23 | pages = [3, 4] 24 | mark = 1 25 | select = [1, 2] 26 | -------------------------------------------------------------------------------- /plom_server/Profile/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023-2024 Colin B. Macdonald 4 | # Copyright (C) 2024 Aden Chan 5 | 6 | from django.urls import path 7 | 8 | from .views import ProfileView, password_change_redirect 9 | 10 | urlpatterns = [ 11 | path("profile/", ProfileView.as_view(), name="profile"), 12 | path( 13 | "profile/password", 14 | password_change_redirect, 15 | name="self-password-reset", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/config_files/quick_demo_config.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Andrew Rechnitzer 3 | 4 | test_spec = "demo" 5 | test_sources = "demo" 6 | prenaming_enabled = true 7 | classlist = "demo" 8 | num_to_produce = 25 9 | 10 | # bundle 1 11 | [[bundles]] 12 | first_paper = 1 13 | last_paper = 10 14 | 15 | 16 | # HW bundle 1 17 | [[hw_bundles]] 18 | paper_number = 21 19 | student_id = 98989898 20 | student_name = "Kenson, Ken" 21 | pages = [[1], [2], [], [2, 3], [3]] 22 | -------------------------------------------------------------------------------- /plom_server/Mark/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023 Andrew Rechnitzer 4 | # Copyright (C) 2025 Colin B. Macdonald 5 | 6 | """Services of the Plom Server Mark app.""" 7 | 8 | from .annotations import create_new_annotation_in_database 9 | from .marking_task_service import MarkingTaskService 10 | from .page_data import PageDataService 11 | from .question_marking import QuestionMarkingService 12 | from .marking_stats import MarkingStatsService 13 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/views/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023-2025 Colin B. Macdonald 4 | # Copyright (C) 2023 Andrew Rechnitzer 5 | 6 | from .base import SpecBaseView 7 | from .summary import SpecSummaryView, HTMXSummaryQuestion, HTMXDeleteSpec 8 | from .template_spec_builder import TemplateSpecBuilderView 9 | from .spec_download import SpecDownloadView 10 | from .spec_editor import SpecEditorView 11 | from .spec_upload import SpecUploadView 12 | -------------------------------------------------------------------------------- /plom_ml/clustering/model/model_config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | # Defines where are the model weights stored, references to HuggingFace repos 5 | 6 | models: 7 | hme_symbolic: 8 | filename: "hme_symbolic_model.onnx" 9 | repo_id: "bryantanady/plom_ml_HME" 10 | 11 | hme_trocr: 12 | filename: "hme_trocr_model.onnx" 13 | repo_id: "bryantanady/plom_ml_HME" 14 | 15 | mcq: 16 | filename: "mcq_model.onnx" 17 | repo_id: "bryantanady/plom_ml_MCQ" 18 | -------------------------------------------------------------------------------- /plom_server/Preparation/tests/tiny_spec.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024 Andrew Rechnitzer 4 | # Copyright (C) 2025 Aidan Murphy 5 | 6 | name = "tiny" 7 | longName = "tiny exam" 8 | 9 | numberOfVersions = 2 10 | numberOfPages = 4 11 | totalMarks = 2 12 | numberOfQuestions = 2 13 | idPage = 1 14 | doNotMarkPages = [] 15 | 16 | 17 | [[question]] 18 | pages = [2] 19 | mark = 1 20 | 21 | 22 | [[question]] 23 | pages = [3, 4] 24 | mark = 1 25 | select = [1, 2] 26 | -------------------------------------------------------------------------------- /plom_server/demo_files/cl_for_quick_demo.csv: -------------------------------------------------------------------------------- 1 | id,name,paper_number 2 | 10050380,"Fink, Iris",1 3 | 10130103,"Vandeventer, Irene",2 4 | 10152155,"Little, Abigail",3 5 | 10203891,"Coleman, Ashley",4 6 | 10399145,"Obrien, John",5 7 | 10419996,"Robinson, Glen", 8 | 10433917,"Wood, James", 9 | 10493869,"Titus, Ruben", 10 | 11015491,"Crosby, Mary", 11 | 11135153,"Garcia, Theodore", 12 | 11243985,"Mcginity, Jessie", 13 | 11292393,"Linthicum, Lawrence", 14 | 11321020,"Montoya, Richard", 15 | 11415065,"Ingram, Evelyn", 16 | 11717306,"Berry, Arnold", 17 | -------------------------------------------------------------------------------- /plom_server/API/services/TokenService.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | from django.contrib.auth.models import User 5 | from rest_framework.authtoken.models import Token 6 | 7 | 8 | def drop_api_token(user_obj: User) -> None: 9 | """Remove the API access token for this user. 10 | 11 | If the token does not exist, no action taken (not an exception). 12 | """ 13 | try: 14 | user_obj.auth_token.delete() 15 | except Token.DoesNotExist: 16 | pass 17 | -------------------------------------------------------------------------------- /plom_server/Mark/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin Macdonald 4 | 5 | from django.contrib import admin 6 | 7 | from .models import ( 8 | Annotation, 9 | AnnotationImage, 10 | MarkingTask, 11 | MarkingTaskTag, 12 | ) 13 | 14 | # This makes models appear in the admin interface 15 | admin.site.register(Annotation) 16 | admin.site.register(AnnotationImage) 17 | admin.site.register(MarkingTask) 18 | admin.site.register(MarkingTaskTag) 19 | -------------------------------------------------------------------------------- /plom_server/templates/SpecCreator/summary-question.html: -------------------------------------------------------------------------------- 1 | 6 |
Question index {{ question_index }}
7 |
8 | 14 |
15 | -------------------------------------------------------------------------------- /plom_server/templates/Visualization/histogram.html: -------------------------------------------------------------------------------- 1 | 6 | {% load static %} 7 | {% block main_content %} 8 | 9 | 10 | 14 | {% endblock main_content %} 15 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2023, 2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | Plom API 6 | ======== 7 | 8 | The Plom API is not yet properly documented and is in a state of flux. 9 | 10 | .. caution:: Subject to change! 11 | 12 | 13 | 14 | Plom "Legacy" API 15 | ----------------- 16 | 17 | The "legacy" server also has an API, currently with considerable overlap, 18 | although most of this should be considered deprecated and not all of it is 19 | planned to be ported to the new server. 20 | -------------------------------------------------------------------------------- /plom_server/Rectangles/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | """Services of the Plom Server Rectangle app.""" 6 | 7 | from .rectangle import ( 8 | RectangleExtractor, 9 | get_reference_qr_coords_for_page, 10 | get_reference_rectangle_for_page, 11 | extract_rect_region_from_image, 12 | ) 13 | 14 | from .idbox_utils import ( 15 | get_idbox_rectangle, 16 | set_idbox_rectangle, 17 | clear_idbox_rectangle, 18 | ) 19 | -------------------------------------------------------------------------------- /plom/scan/clearScannerLogin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020 Andrew Rechnitzer 3 | # Copyright (C) 2020-2021, 2023 Colin B. Macdonald 4 | # Copyright (C) 2021 Peter Lee 5 | 6 | from plom.messenger import ScanMessenger 7 | 8 | 9 | def clear_login(server=None, password=None): 10 | scanMessenger = ScanMessenger(server) 11 | scanMessenger.start() 12 | 13 | try: 14 | scanMessenger.clearAuthorisation("scanner", password) 15 | print("Scanner login cleared.") 16 | finally: 17 | scanMessenger.stop() 18 | -------------------------------------------------------------------------------- /plom/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2018-2024 Andrew Rechnitzer 3 | # Copyright (C) 2020-2025 Colin B. Macdonald 4 | # Copyright (C) 2023 Edith Coates 5 | # Copyright (C) 2024 Aden Chan 6 | 7 | """Plom is Paperless Open Marking. 8 | 9 | Plom creates multi-versioned tests, scans them, coordinates online 10 | marking/grading, and returns them online. 11 | """ 12 | 13 | __copyright__ = "Copyright (C) 2018-2025 Andrew Rechnitzer, Colin B. Macdonald, et al" 14 | __credits__ = "The Plom Project Developers" 15 | __license__ = "AGPL-3.0-or-later" 16 | -------------------------------------------------------------------------------- /doc/source/install-from-source.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020, 2022-2023 Colin B. Macdonald 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | Installing from Source 5 | ====================== 6 | 7 | Installing Plom from source code involves various steps. 8 | Depending on what you are trying to do, you may find it easier 9 | to follow the instructions for :doc:`install-client` 10 | or :doc:`install-server`. 11 | 12 | Instructions for various OSes: 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | linux_installation.md 18 | macos_installation.md 19 | wsl_installation.md 20 | -------------------------------------------------------------------------------- /doc/source/running_a_server.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2024 Colin B. Macdonald 3 | Copyright (C) 2023 Philip D. Loewen 4 | SPDX-License-Identifier: AGPL-3.0-or-later 5 | 6 | Running your own server 7 | ======================= 8 | 9 | Plom has several components. One of these is the Plom server, 10 | used both for preparing new assessments and for coordinating grading. 11 | 12 | If you already have a running server, you can skip this section and move onto :doc:`preparing_an_assessment` 13 | 14 | .. note:: 15 | 16 | Stub: move and/or write documentation. 17 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/no_group.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "base/base.html" %} 7 | {% block title %} 8 | Error - User is not in a group 9 | {% endblock title %} 10 | {% block main_content %} 11 |

Sorry!

12 |

13 | You are in this page because you are not in a group. 14 | Please contact the administrator and ask them to add you to the appropriate group. 15 |

16 | {% endblock main_content %} 17 | -------------------------------------------------------------------------------- /plom_server/Base/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023, 2025 Colin Macdonald 4 | # Copyright (C) 2025 Andrew Rechnitzer 5 | 6 | from django.contrib import admin 7 | 8 | from .models import ( 9 | BaseImage, 10 | HueyTaskTracker, 11 | SettingsModel, 12 | SettingsBooleanModel, 13 | ) 14 | 15 | # This makes models appear in the admin interface 16 | admin.site.register(BaseImage) 17 | admin.site.register(HueyTaskTracker) 18 | admin.site.register(SettingsModel) 19 | admin.site.register(SettingsBooleanModel) 20 | -------------------------------------------------------------------------------- /plom_server/Launcher/apps.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class LauncherConfig(AppConfig): 9 | name = "plom_server.Launcher" 10 | verbose_name = "A launcher app for where start-up things should go" 11 | 12 | # This function is called on django startup, including 13 | # when a django-command is called 14 | def ready(self): 15 | """A placeholder, for now, that is called on each django start-up.""" 16 | pass 17 | -------------------------------------------------------------------------------- /doc/source/cmdline.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2022-2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | Command-line tools 6 | ================== 7 | 8 | Plom includes various command-line tools for managing servers: 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | plom-cli 14 | plom-create 15 | plom-scan 16 | plom-solutions 17 | plom-finish 18 | 19 | There are also commands to launch the server and client themselves: 20 | 21 | .. toctree:: 22 | :maxdepth: 1 23 | 24 | plom-client 25 | plom-new-server 26 | plom-new-demo 27 | -------------------------------------------------------------------------------- /plom_server/Scan/models/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023-2025 Colin B. Macdonald 4 | 5 | """Models of the Plom Server Scan app.""" 6 | 7 | from .staging_bundle import StagingBundle 8 | 9 | from .staging_images import ( 10 | StagingImage, 11 | StagingThumbnail, 12 | KnownStagingImage, 13 | ExtraStagingImage, 14 | UnknownStagingImage, 15 | DiscardStagingImage, 16 | ErrorStagingImage, 17 | ) 18 | 19 | from .scan_background_chores import ( 20 | PagesToImagesChore, 21 | ManageParseQRChore, 22 | ) 23 | -------------------------------------------------------------------------------- /plom_server/Tags/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from django.urls import path 5 | 6 | from .views import TagLandingPageView, TagItemView 7 | 8 | 9 | urlpatterns = [ 10 | path("", TagLandingPageView.as_view(), name="tags_landing"), 11 | path("tag_filter/", TagLandingPageView.tag_filter, name="tag_filter"), 12 | path("/item/", TagItemView.as_view(), name="tag_item"), 13 | path("/edit/", TagItemView.post, name="tag_edit"), 14 | path("/tag_delete/", TagItemView.tag_delete, name="tag_delete"), 15 | ] 16 | -------------------------------------------------------------------------------- /plom_server/Base/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Colin B. Macdonald 3 | # Copyright (C) 2024 Aden Chan 4 | 5 | from django.urls import path 6 | from .views import TroublesAfootGenericErrorView 7 | from .views import ServerStatusView, ResetView 8 | 9 | 10 | urlpatterns = [ 11 | path( 12 | "troubles_afoot/", 13 | TroublesAfootGenericErrorView.as_view(), 14 | name="troubles_afoot", 15 | ), 16 | path("reset/", ResetView.as_view(), name="reset"), 17 | path("server_status", ServerStatusView.as_view(), name="server_status"), 18 | ] 19 | -------------------------------------------------------------------------------- /plom_server/TaskOrder/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from django.urls import path 5 | 6 | from .views import TaskOrderPageView 7 | 8 | 9 | urlpatterns = [ 10 | path("", TaskOrderPageView.as_view(), name="task_order_landing"), 11 | path( 12 | "upload_task_priorities/", 13 | TaskOrderPageView.upload_task_priorities, 14 | name="upload_task_priorities", 15 | ), 16 | path( 17 | "download_priorities/", 18 | TaskOrderPageView.download_priorities, 19 | name="download_priorities", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /plom_server/templates/Progress/Mark/annotation_image_wrap_fragment.html: -------------------------------------------------------------------------------- 1 | 6 |
7 |
8 | Paper {{ paper }} question index {{ question_idx }} 9 |
10 |
11 |
12 | 15 |
16 | -------------------------------------------------------------------------------- /plom_server/templates/Visualization/heat_map.html: -------------------------------------------------------------------------------- 1 | 6 | {% load static %} 7 | {% block main_content %} 8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | {% endblock main_content %} 16 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2023, 2025 Colin B. Macdonald 3 | 4 | # Copying and distribution of this file, with or without modification, 5 | # are permitted in any medium without royalty provided the copyright 6 | # notice and this notice are preserved. This file is offered as-is, 7 | # without any warranty. 8 | 9 | version: 2 10 | 11 | build: 12 | os: "ubuntu-24.04" 13 | tools: 14 | python: "3.12" 15 | 16 | sphinx: 17 | configuration: doc/source/conf.py 18 | 19 | python: 20 | install: 21 | - requirements: requirements.txt 22 | - requirements: doc/requirements.txt 23 | -------------------------------------------------------------------------------- /plom/common.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2018-2024 Andrew Rechnitzer 3 | # Copyright (C) 2020-2025 Colin B. Macdonald 4 | # Copyright (C) 2023 Edith Coates 5 | # Copyright (C) 2024 Aden Chan 6 | 7 | # Any utilities that don't have there own version can use this one 8 | # Also in plom_server/__init__.py 9 | __version__ = "0.19.8.dev0" 10 | 11 | import sys 12 | 13 | # probably we don't need this sort of thing any more, but doesn't hurt...? 14 | if sys.version_info[0] == 2: 15 | raise RuntimeError("Plom requires Python 3; it will not work with Python 2") 16 | 17 | Default_Port = 41984 18 | -------------------------------------------------------------------------------- /plom_server/Identify/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.contrib import admin 6 | 7 | from .models import ( 8 | IDPrediction, 9 | IDReadingHueyTaskTracker, 10 | IDRectangle, 11 | PaperIDAction, 12 | PaperIDTask, 13 | ) 14 | 15 | # This makes models appear in the admin interface 16 | admin.site.register(IDPrediction) 17 | admin.site.register(IDReadingHueyTaskTracker) 18 | admin.site.register(IDRectangle) 19 | admin.site.register(PaperIDAction) 20 | admin.site.register(PaperIDTask) 21 | -------------------------------------------------------------------------------- /plom_server/static/css/diff_table.css: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: AGPL-3.0-or-later */ 2 | /* Copyright (C) 2024 Aden Chan */ 3 | 4 | table.diff { 5 | font-family: var(--bs-font-monospace); 6 | border: medium; 7 | } 8 | 9 | .diff_header { 10 | background-color: #e0e0e0; 11 | } 12 | 13 | td.diff_header { 14 | text-align: right 15 | } 16 | 17 | .diff_next { 18 | background-color: #c0c0c0; 19 | display: none; 20 | } 21 | 22 | .diff_add { 23 | background-color: #aaffaa 24 | } 25 | 26 | .diff_chg { 27 | background-color: #ffff77 28 | } 29 | 30 | .diff_sub { 31 | background-color: #ffaaaa 32 | } 33 | -------------------------------------------------------------------------------- /plom_server/API/views/experimental/base.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | from rest_framework.viewsets import ModelViewSet 5 | from rest_framework.authentication import BasicAuthentication 6 | 7 | from ...permissions import IsManagerReadOnly 8 | 9 | 10 | class ManagerReadOnlyViewSet(ModelViewSet): 11 | """Base viewset for exposing DRF-generated endpoints to models. 12 | 13 | Only the Manager user has read access, and no user has write access. 14 | """ 15 | 16 | authentication_classes = [BasicAuthentication] 17 | permission_classes = [IsManagerReadOnly] 18 | -------------------------------------------------------------------------------- /plom_server/demo_files/demo_assessment_qtags.csv: -------------------------------------------------------------------------------- 1 | Name,Description,Confidential_Info, Help_Threshold, Help_Resources 2 | limits,Basic limits and arithmetic of limits,Hopefully most students will get these questions, 0.8, Review Lecture 1-3 and Problem Set 1 3 | derivatives,"Basic derivatives, chain, product, quotient rules","Most students okay with this, but some will have trouble with algebraic manipulations", 0.5, Review Lecture 4-6 and Problem Set 2 4 | applications,"Taylor, estimates, exp growth/decay","Taylor might be tricky, but the exp growth should be pretty standard, expect some issues with logs", 0.5, Review Lecture 7-9 and Problem Set 3 5 | -------------------------------------------------------------------------------- /.codespell-ignorelines: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2021, 2023 Colin B. Macdonald 3 | # 4 | # Copying and distribution of this file, with or without modification, 5 | # are permitted in any medium without royalty provided the copyright 6 | # notice and this notice are preserved. This file is offered as-is, 7 | # without any warranty. 8 | 9 | # lines that codespell should ignore: whitespace matters! 10 | 11 | # "datas" 12 | a.datas, 13 | datas=[ 14 | # "nwe" 15 | \coordinate (nwe) at ($(nw) + (\cornerlen, 0cm)$); 16 | \draw[line width=\cornerwidth] (nwe) -- (nw) -- (nws); 17 | 18 | # eof 19 | -------------------------------------------------------------------------------- /plom/cli/list_bundles.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | # Copyright (C) 2025 Philip D. Loewen 4 | 5 | from tabulate import tabulate 6 | 7 | from plom.cli import with_messenger 8 | 9 | 10 | @with_messenger 11 | def list_bundles(*, msgr): 12 | """Prints summary of test/hw uploads. 13 | 14 | Keyword Args: 15 | msgr (plom.Messenger/tuple): either a connected Messenger or a 16 | tuple appropriate for credentials. 17 | """ 18 | st = msgr.new_server_list_bundles() 19 | # , tablefmt="simple_outline") 20 | print(tabulate(st, headers="firstrow")) 21 | -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/testing_test_spec.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Colin B. Macdonald 3 | # Copyright (C) 2025 Aidan Murphy 4 | 5 | name = "example_3q" 6 | longName = "Three-question example using Plom" 7 | 8 | numberOfVersions = 2 9 | numberOfPages = 6 10 | totalMarks = 20 11 | numberOfQuestions = 3 12 | idPage = 1 13 | doNotMarkPages = [2] 14 | 15 | [[question]] 16 | pages = [3] 17 | mark = 5 18 | 19 | [[question]] 20 | label = "Q(2)" 21 | pages = [4] 22 | mark = 5 23 | select = 1 24 | 25 | [[question]] 26 | label = "Ex.3" 27 | pages = [5, 6] 28 | mark = 10 29 | -------------------------------------------------------------------------------- /plom_server/BuildPaperPDF/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | import django.db.models.deletion 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | initial = True 8 | 9 | dependencies = [ 10 | ("BuildPaperPDF", "0001_initial"), 11 | ("Papers", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="buildpaperpdfchore", 17 | name="paper", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, to="Papers.paper" 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /plom_server/templates/Progress/Mark/original_image_wrap_fragment.html: -------------------------------------------------------------------------------- 1 | 6 |
7 |
8 | Paper {{ paper }} question index {{ question }} 9 |
10 |
11 |
12 | {% for img in img_list %} 13 | 16 | {% endfor %} 17 |
18 | -------------------------------------------------------------------------------- /contrib/onlinedist_01_clean_canvas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # Copyright (C) 2021, 2023-2024 Colin B. Macdonald 5 | 6 | """Read Canvas exported csv and remove test students etc, ensure all have Student Numbers.""" 7 | 8 | from pathlib import Path 9 | from plom.finish.return_tools import import_canvas_csv 10 | 11 | 12 | where_csv = Path(".") 13 | canvas_in = where_csv / "Canvas_classlist_export.csv" 14 | canvas_out = where_csv / "Canvas_classlist_01_cleaned.csv" 15 | 16 | df = import_canvas_csv(canvas_in) 17 | 18 | # TODO: assert all non-null "Student Number" column? 19 | 20 | df.to_csv(canvas_out, index=False) 21 | -------------------------------------------------------------------------------- /plom/messenger/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | """Backend bits 'n bobs to talk to a Plom server.""" 5 | 6 | from .messenger import Messenger 7 | from .scanMessenger import ScanMessenger 8 | from .managerMessenger import ManagerMessenger 9 | from .base_messenger import Plom_API_Version 10 | from .plom_admin_messenger import PlomAdminMessenger 11 | 12 | # No one should be calling BaseMessenger directly but maybe 13 | # its useful for typing hints. 14 | from .base_messenger import BaseMessenger 15 | 16 | __all__ = [ 17 | "Messenger", 18 | "ManagerMessenger", 19 | "ScanMessenger", 20 | ] 21 | -------------------------------------------------------------------------------- /plom_server/Finish/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022-2023 Brennen Chiu 4 | # Copyright (C) 2023 Andrew Rechnitzer 5 | # Copyright (C) 2023, 2025 Colin B. Macdonald 6 | 7 | from django.contrib import admin 8 | 9 | from .models import ( 10 | BuildSolutionPDFChore, 11 | ReassemblePaperChore, 12 | SolutionImage, 13 | SolutionSourcePDF, 14 | ) 15 | 16 | # This makes models appear in the admin interface 17 | admin.site.register(BuildSolutionPDFChore) 18 | admin.site.register(ReassemblePaperChore) 19 | admin.site.register(SolutionImage) 20 | admin.site.register(SolutionSourcePDF) 21 | -------------------------------------------------------------------------------- /plom/cli/identify_tools.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | from typing import Any 5 | 6 | from plom.cli import with_messenger 7 | 8 | 9 | @with_messenger 10 | def id_paper( 11 | papernum: int, student_id: str, student_name: str, *, msgr 12 | ) -> dict[str, Any]: 13 | """Directly identify a particular paper as belonging to a particular student id.""" 14 | return msgr.beta_id_paper(papernum, student_id, student_name) 15 | 16 | 17 | @with_messenger 18 | def un_id_paper(papernum: int, *, msgr) -> dict[str, Any]: 19 | """Unidentify a particular paper.""" 20 | return msgr.beta_un_id_paper(papernum) 21 | -------------------------------------------------------------------------------- /plom_server/API/routes/tags_patterns.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023-2024 Colin B. Macdonald 4 | 5 | from django.urls import path 6 | 7 | from ..views import TagsFromCodeView, GetAllTags 8 | 9 | 10 | class TagsURLPatterns: 11 | """URLs for handling marking task-related tags.""" 12 | 13 | prefix = "tags/" 14 | 15 | @classmethod 16 | def patterns(cls): 17 | tag_patterns = [ 18 | path("", GetAllTags.as_view(), name="api_all_tags"), 19 | path("", TagsFromCodeView.as_view(), name="api_tags_code"), 20 | ] 21 | 22 | return tag_patterns 23 | -------------------------------------------------------------------------------- /plom_server/API/views/user_info.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | from rest_framework.response import Response 5 | from rest_framework.request import Request 6 | from rest_framework.views import APIView 7 | from rest_framework import status 8 | from plom_server.UserManagement.services import get_users_groups_info 9 | 10 | 11 | # GET /info/users/ 12 | class UsersInfo(APIView): 13 | def get(self, request: Request) -> Response: 14 | """Get a dictionary mapping all users' username to their groups.""" 15 | userInfo = get_users_groups_info() 16 | return Response(userInfo, status=status.HTTP_200_OK) 17 | -------------------------------------------------------------------------------- /plom_server/demo_files/bundle_for_long_demo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | 4 | # bundle 1 5 | [[bundles]] 6 | first_paper = 1 7 | last_paper = 25 8 | # bundle 2 9 | [[bundles]] 10 | first_paper = 26 11 | last_paper = 50 12 | # bundle 3 13 | [[bundles]] 14 | first_paper = 51 15 | last_paper = 100 16 | # bundle 4 17 | [[bundles]] 18 | first_paper = 101 19 | last_paper = 150 20 | # bundle 5 21 | [[bundles]] 22 | first_paper = 151 23 | last_paper = 200 24 | 25 | # HW bundle 1 26 | [[hw_bundles]] 27 | paper_number = 241 28 | student_id = 98989898 29 | student_name = "Kenson, Ken" 30 | pages = [[1], [2], [], [2, 3], [3]] 31 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/activation_invalid.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | Invalid Link 11 | 12 | 13 |

Sorry, the link has expired or was already used

14 |

15 | Password reset links expire after a certain amount of time, or after they are used. 16 | Please contact the person who shared this link with you. 17 |

18 | 19 | 20 | -------------------------------------------------------------------------------- /plom_server/templates/base/alert_message.html: -------------------------------------------------------------------------------- 1 | 6 | {% if 'success' in message.tags %} 7 |
{{ message }}
8 | {% elif 'warning' in message.tags %} 9 |
{{ message }}
10 | {% elif 'error' in message.tags %} 11 |
{{ message }}
12 | {% elif 'info' in message.tags %} 13 |
{{ message }}
14 | {% else %} 15 |
{{ message }}
16 | {% endif %} 17 | -------------------------------------------------------------------------------- /plom_server/Authentication/forms/choices.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Brennen Chiu 3 | 4 | USER_TYPE_WITH_MANAGER_CHOICES = [ 5 | ("marker", "Marker"), 6 | ("scanner", "Scanner"), 7 | ("manager", "Manager"), 8 | ] 9 | 10 | USER_TYPE_WITHOUT_MANAGER_CHOICES = [ 11 | ("marker", "Marker"), 12 | ("scanner", "Scanner"), 13 | ] 14 | 15 | USERNAME_CHOICES = [ 16 | ("basic", "Basic numbered usernames"), 17 | ( 18 | "funky", 19 | "\N{LEFT DOUBLE QUOTATION MARK}Funky\N{RIGHT DOUBLE QUOTATION MARK} usernames (such as \N{LEFT DOUBLE QUOTATION MARK}hungryHeron8\N{RIGHT DOUBLE QUOTATION MARK})", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /plom/idreader/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2018-2020 Andrew Rechnitzer 3 | # Copyright (C) 2020 Vala Vakilian 4 | # Copyright (C) 2022-2023, 2025 Colin B. Macdonald 5 | 6 | """Read student IDs using OpenCV and Scikit-learn.""" 7 | 8 | # 2022-03: For some reason CI fails if we make this a real module 9 | 10 | # from .model_utils import download_or_train_model 11 | # from .predictStudentID import compute_probabilities 12 | # from .idReader import assemble_cost_matrix, lap_solver 13 | 14 | # __all__ = [ 15 | # "download_or_train_model", 16 | # "compute_probabilities", 17 | # "assemble_cost_matrix", 18 | # "lap_solver", 19 | # ] 20 | -------------------------------------------------------------------------------- /plom_server/Finish/management/commands/plom_download_ta_info_csv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | 4 | from django.core.management.base import BaseCommand 5 | 6 | from ...services import TaMarkingService 7 | 8 | 9 | class Command(BaseCommand): 10 | """Get csv of TA marking information.""" 11 | 12 | help = "Get csv of TA marking information." 13 | 14 | def add_arguments(self, parser): 15 | pass 16 | 17 | def handle(self, *args, **options): 18 | csv_as_string = TaMarkingService().build_ta_info_csv_as_string() 19 | with open("ta_info.csv", "w+") as fh: 20 | fh.write(csv_as_string) 21 | -------------------------------------------------------------------------------- /plom/solutions/checkSolutionStatus.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020-2021 Andrew Rechnitzer 3 | # Copyright (C) 2020-2022 Colin B. Macdonald 4 | 5 | from plom.solutions import with_manager_messenger 6 | 7 | 8 | @with_manager_messenger 9 | def checkStatus(*, msgr): 10 | """Checks the status of solutions on a server. 11 | 12 | Keyword Args: 13 | msgr (plom.Messenger/tuple): either a connected Messenger or a 14 | tuple appropriate for credientials. 15 | 16 | Returns: 17 | list: each entry is a list of triples ``[q, v, md5sum]`` or 18 | ``[q, v, ""]``. TODO: explain more. 19 | 20 | """ 21 | return msgr.getSolutionStatus() 22 | -------------------------------------------------------------------------------- /plom_server/Finish/views/student_report.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Bryan Tanady 3 | # Copyright (C) 2024-2025 Colin B. Macdonald 4 | # Copyright (C) 2024-2025 Andrew Rechnitzer 5 | 6 | from django.http import FileResponse 7 | 8 | from plom_server.Base.base_group_views import ManagerRequiredView 9 | 10 | from ..services import ReassembleService 11 | 12 | 13 | class StudentReportView(ManagerRequiredView): 14 | def get(self, request, paper_number): 15 | """Return the student report pdf of the given paper.""" 16 | pdf_file, filename = ReassembleService.get_single_student_report(paper_number) 17 | return FileResponse(pdf_file, filename=filename) 18 | -------------------------------------------------------------------------------- /plom/finish/clear_manager_login.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2019-2020 Andrew Rechnitzer 3 | # Copyright (C) 2019-2021, 2023 Colin B. Macdonald 4 | 5 | from plom.messenger import Messenger 6 | 7 | 8 | def clear_manager_login(server=None, password=None): 9 | """Force clear the "manager" authorisation, e.g., after a crash. 10 | 11 | Args: 12 | server (str): in the form "example.com" or "example.com:41984". 13 | password (str): if not specified, prompt on the command line. 14 | """ 15 | msgr = Messenger(server) 16 | msgr.start() 17 | 18 | msgr.clearAuthorisation("manager", password) 19 | print("Manager login cleared.") 20 | msgr.stop() 21 | -------------------------------------------------------------------------------- /plom_server/templates/SpecCreator/validation.html: -------------------------------------------------------------------------------- 1 | 6 | {% if success %} 7 |

{{ msg }}

8 | {% else %} 9 |
10 |

{{ msg }}

11 |
    12 | {% for err in error_list %} 13 |
  • 14 | {{ err }} 15 |
  • 16 | {% endfor %} 17 |
18 |
19 | {% endif %} 20 | -------------------------------------------------------------------------------- /maint/maint-wipe-rebuild-migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # Copyright (C) 2023-2025 Colin B. Macdonald 5 | 6 | # Run this script from the project root (where pyproject.toml is) 7 | # ./maint/maint-wipe-rebuild-migrations.sh 8 | # 9 | # As of 2025-03 we do not use "layered" migrations: we change the 10 | # database design only between major versions, and we rebuild from 11 | # scratch. You can run this script after database edits, or perhaps 12 | # after Django-upgrades to regenerate the migration files that are 13 | # used to initialize the database. 14 | 15 | python3 plom_server/scripts/wipe_migrations.py 16 | 17 | PYTHONPATH=. python3 manage.py makemigrations --no-header 18 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/unauthorized.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | Unauthorized 11 | 12 | 13 |
14 |

401 - Unauthorized Access

15 |

Your account do not have permission to view this directory or page.

16 |

Click below to go back.

17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /plom/canvas/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020-2021 Forest Kobayashi 3 | # Copyright (C) 2021-2022 Colin B. Macdonald 4 | 5 | """Plom features supporting integration with Canvas""" 6 | 7 | __DEFAULT_CANVAS_API_URL__ = "https://canvas.ubc.ca" 8 | 9 | from .canvas_utils import get_student_list, download_classlist 10 | from .canvas_utils import ( 11 | canvas_login, 12 | get_assignment_by_id_number, 13 | get_conversion_table, 14 | get_courses_teaching, 15 | get_course_by_id_number, 16 | get_section_by_id_number, 17 | get_sis_id_to_canvas_id_table, 18 | interactively_get_assignment, 19 | interactively_get_course, 20 | interactively_get_section, 21 | ) 22 | -------------------------------------------------------------------------------- /plom_server/Visualization/views.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Divy Patel 3 | 4 | from django.shortcuts import render 5 | 6 | from plom_server.Base.base_group_views import ManagerRequiredView 7 | 8 | 9 | class HistogramView(ManagerRequiredView): 10 | """Histogram view for D3.js.""" 11 | 12 | template_name = "Visualization/histogram.html" 13 | 14 | def get(self, request): 15 | return render(request, self.template_name) 16 | 17 | 18 | class HeatMapView(ManagerRequiredView): 19 | """Heat map view for D3.js.""" 20 | 21 | template_name = "Visualization/heat_map.html" 22 | 23 | def get(self, request): 24 | return render(request, self.template_name) 25 | -------------------------------------------------------------------------------- /plom_server/Finish/services/soln_images.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.core.files import File 6 | 7 | from ..models import SolutionImage 8 | 9 | 10 | class SolnImageService: 11 | @staticmethod 12 | def get_soln_image(qidx: int, version: int) -> File: 13 | """Return the solution image file for the given q/v. 14 | 15 | If the image is not present then an ObjectDoesNotExist 16 | exception thrown and it is up to the caller to deal with that. 17 | """ 18 | return SolutionImage.objects.get( 19 | question_index=qidx, version=version 20 | ).image_file 21 | -------------------------------------------------------------------------------- /plom_server/Tags/forms.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | 4 | from django import forms 5 | 6 | from plom_server.Mark.models import MarkingTaskTag 7 | 8 | 9 | class TagFormFilter(forms.Form): 10 | tag_filter_text = forms.CharField( 11 | required=False, widget=forms.TextInput, label="Tag Text" 12 | ) 13 | strict_match = forms.BooleanField(required=False, label="Strict Match") 14 | 15 | 16 | class TagEditForm(forms.ModelForm): 17 | class Meta: 18 | model = MarkingTaskTag 19 | fields = [ 20 | "text", 21 | ] 22 | widgets = { 23 | "text": forms.TextInput(attrs={"style": "width: 60%;"}), 24 | } 25 | -------------------------------------------------------------------------------- /plom_server/demo_files/demo_assessment_spec.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023, 2025 Colin B. Macdonald 3 | # Copyright (C) 2024-2025 Andrew Rechnitzer 4 | 5 | name = "plomdemo" 6 | longName = "Midterm Demo using Plom" 7 | 8 | numberOfVersions = 3 9 | numberOfPages = 8 10 | totalMarks = 25 11 | numberOfQuestions = 4 12 | idPage = 1 13 | doNotMarkPages = [2] 14 | 15 | [[question]] 16 | pages = [3] 17 | mark = 6 18 | 19 | [[question]] 20 | label = "Q(2)" 21 | pages = [4,5] 22 | mark = 8 23 | select = 1 24 | 25 | [[question]] 26 | label = "Ex.3" 27 | pages = [6] 28 | mark = 4 29 | select = [1,2] 30 | 31 | [[question]] 32 | label = "LastQ" 33 | pages = [7,8] 34 | mark = 7 35 | select = [3] 36 | -------------------------------------------------------------------------------- /plom_server/Mark/serializers/annotations.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from rest_framework.serializers import ( 6 | ModelSerializer, 7 | StringRelatedField, 8 | HyperlinkedRelatedField, 9 | ) 10 | 11 | from ..models.annotations import Annotation 12 | 13 | 14 | class AnnotationSerializer(ModelSerializer): 15 | user = StringRelatedField() 16 | image = StringRelatedField() 17 | # TODO: Issue #3521: potentially broken URLs, anyone using this? 18 | task = HyperlinkedRelatedField("marking-tasks-detail", read_only=True) 19 | 20 | class Meta: 21 | model = Annotation 22 | exclude = ["annotation_data"] 23 | -------------------------------------------------------------------------------- /plom_server/templates/Finish/finish_no_spec.html: -------------------------------------------------------------------------------- 1 | 5 | {% extends "base/base.html" %} 6 | {% load static %} 7 | {% block title %} 8 | No specification 9 | {% endblock title %} 10 | {% block page_heading %} 11 | No specification 12 | {% endblock page_heading %} 13 | {% block main_content %} 14 |
15 |

There is no assessment specification; this page will be accessible after the system has a specification.

16 |
17 | 20 | {% endblock main_content %} 21 | -------------------------------------------------------------------------------- /plom_server/Progress/views/overview_landing.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Andrew Rechnitzer 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from django.shortcuts import render 6 | 7 | from plom_server.Base.base_group_views import LeadMarkerOrManagerView 8 | 9 | 10 | class OverviewLandingView(LeadMarkerOrManagerView): 11 | """Page displaying a menu of different progress views.""" 12 | 13 | def get(self, request): 14 | return render(request, "Progress/overview_landing.html") 15 | 16 | 17 | class ToolsLandingView(LeadMarkerOrManagerView): 18 | """Page giving an overview of various misc tools.""" 19 | 20 | def get(self, request): 21 | return render(request, "Progress/tools_landing.html") 22 | -------------------------------------------------------------------------------- /plom/test_print_score.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Colin B. Macdonald 3 | 4 | from plom.misc_utils import pprint_score as pp 5 | 6 | 7 | def test_print_score_string() -> None: 8 | assert isinstance(pp(5), str) 9 | assert isinstance(pp(5.25), str) 10 | assert isinstance(pp(None), str) 11 | 12 | 13 | def test_print_score_int_as_int() -> None: 14 | assert pp(5) == "5" 15 | 16 | 17 | def test_print_score_no_trailing_zeros() -> None: 18 | assert not pp(5.25).endswith("0") 19 | assert not pp(5.5).startswith("5.50") 20 | 21 | 22 | def test_print_score_none_as_blank() -> None: 23 | assert pp(None) == "" 24 | 25 | 26 | def test_print_score_large_int() -> None: 27 | assert pp(1234567) == "1234567" 28 | -------------------------------------------------------------------------------- /plom_server/Finish/management/commands/plom_download_marks_csv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Aden Chan 4 | # Copyright (C) 2024-2025 Colin B. Macdonald 5 | 6 | from django.core.management.base import BaseCommand 7 | 8 | from ...services import StudentMarkService 9 | 10 | 11 | class Command(BaseCommand): 12 | """Get csv of student marks.""" 13 | 14 | help = "Get csv of student marks." 15 | 16 | def add_arguments(self, parser): 17 | pass 18 | 19 | def handle(self, *args, **options): 20 | csv_as_string = StudentMarkService.build_marks_csv_as_string(True, True, True) 21 | with open("marks.csv", "w+") as fh: 22 | fh.write(csv_as_string) 23 | -------------------------------------------------------------------------------- /plom/templateSolutionSpec.toml: -------------------------------------------------------------------------------- 1 | # Example solution specification for Plom 2 | 3 | # >>> Edit the data below <<< 4 | 5 | # Information about the test 6 | # May have one or more versions of solutions - must be the same as test-specification 7 | numberOfVersions = 2 8 | # how many pages in the solution pdf - may be different from test-specification 9 | numberOfPages = 6 10 | # how many questions - must be the same as test-specification 11 | numberOfQuestions = 3 12 | 13 | # for each question give a non-empty list of pages 14 | # must be contiguous list of positive integers between 1 and numberOfPages 15 | # need not be distinct (ie one page can belong to two solutions) 16 | [solution.1] 17 | pages = [3] 18 | 19 | [solution.2] 20 | pages = [4] 21 | 22 | [solution.3] 23 | pages = [5] 24 | -------------------------------------------------------------------------------- /plom_server/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2025 Colin B. Macdonald 3 | 4 | """Plom is Paperless Open Marking. 5 | 6 | Plom creates multi-versioned tests, scans them, coordinates online 7 | marking/grading, and returns them online. 8 | 9 | This is the Plom Server. 10 | """ 11 | 12 | __copyright__ = "Copyright (C) 2018-2025 Andrew Rechnitzer, Colin B. Macdonald, et al" 13 | __credits__ = "The Plom Project Developers" 14 | __license__ = "AGPL-3.0-or-later" 15 | 16 | # Also in plom/common.py 17 | __version__ = "0.19.8.dev0" 18 | 19 | Plom_API_Version = "115" 20 | 21 | # __all__ = [ 22 | # "Preparation", 23 | # "BuildPaperPDF", 24 | # "Papers", 25 | # "Preparation", 26 | # "Scan", 27 | # "SpecCreator", 28 | # ] 29 | -------------------------------------------------------------------------------- /plom/test_feedback_rules.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Colin B. Macdonald 3 | 4 | from plom.feedback_rules import feedback_rules 5 | 6 | 7 | def test_codes_are_strings() -> None: 8 | for code in feedback_rules.keys(): 9 | assert isinstance(code, str) 10 | 11 | 12 | def test_homogeneous_keys() -> None: 13 | # this test will need to change if the format of the rules changes 14 | keys = set(("explanation", "allowed", "warn", "dama_allowed", "override_allowed")) 15 | for code, data in feedback_rules.items(): 16 | assert set(data.keys()) == keys 17 | 18 | 19 | def test_explanations_are_strings() -> None: 20 | for code, data in feedback_rules.items(): 21 | assert isinstance(data["explanation"], str) 22 | -------------------------------------------------------------------------------- /plom/finish/audit.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Andrew Rechnitzer 3 | # Copyright (C) 2022-2023 Colin B. Macdonald 4 | 5 | import json 6 | 7 | from plom.finish import with_finish_messenger 8 | 9 | 10 | @with_finish_messenger 11 | def audit(*, msgr): 12 | audit = {} 13 | audit["tests"] = msgr.getFilesInAllTests() 14 | audit["unknowns"] = msgr.getUnknownPages() 15 | audit["discards"] = msgr.getDiscardedPages() 16 | print("Warning: calling potentially-slow getDanglingPages API...") 17 | audit["dangling"] = msgr.getDanglingPages() 18 | audit["collisions"] = msgr.getCollidingPageNames() 19 | 20 | with open("audit.json", "w+") as fh: 21 | json.dump(audit, fh, indent=" ") 22 | print("Wrote file audit to 'audit.json'") 23 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/signup_multiple_users.html: -------------------------------------------------------------------------------- 1 | 5 | {% extends "Authentication/signup_base.html" %} 6 | {% load static %} 7 | {% block card_content %} 8 |
9 | {% csrf_token %} 10 |
11 | {{ form.user_types.label }} 12 | {{ form.user_types }} 13 |
14 | {{ form.basic_or_funky_username.label }} 15 | {{ form.basic_or_funky_username }} 16 |

How many users would you like to create?

17 | {{ form.num_users }} 18 | 19 |
20 | {% endblock card_content %} 21 | -------------------------------------------------------------------------------- /plom_server/Finish/views/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2025 Andrew Rechnitzer 3 | # Copyright (C) 2024 Bryan Tanady 4 | 5 | from .marking_info import ( 6 | MarkingInformationPaperView, 7 | MarkingInformationView, 8 | ) 9 | from .reassembly import ( 10 | ReassemblePapersView, 11 | StartOneReassembly, 12 | StartAllReassembly, 13 | CancelQueuedReassembly, 14 | DownloadRangeOfReassembled, 15 | ) 16 | from .build_soln_pdf import ( 17 | BuildSolutionsView, 18 | CancelQueuedBuildSoln, 19 | StartAllBuildSoln, 20 | StartOneBuildSoln, 21 | ) 22 | from .soln_home import SolnHomeView 23 | from .soln_spec import SolnSpecView, TemplateSolnSpecView 24 | from .soln_sources import SolnSourcesView 25 | 26 | from .student_report import StudentReportView 27 | -------------------------------------------------------------------------------- /plom_server/Papers/models/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2024 Andrew Rechnitzer 3 | # Copyright (C) 2022-2023 Edith Coates 4 | # Copyright (C) 2023, 2025 Colin B. Macdonald 5 | 6 | """Models of the Plom Server Paper app.""" 7 | 8 | from .image_bundle import ( 9 | Bundle, 10 | Image, 11 | DiscardPage, 12 | ) 13 | 14 | from .paper_structure import ( 15 | Paper, 16 | FixedPage, 17 | DNMPage, 18 | IDPage, 19 | QuestionPage, 20 | MobilePage, 21 | ) 22 | from .specifications import ( 23 | SpecQuestion, 24 | Specification, 25 | SolnSpecification, 26 | SolnSpecQuestion, 27 | ) 28 | 29 | # TODO: Issue #3140 30 | from .background_tasks import CreateImageHueyTask, PopulateEvacuateDBChore 31 | 32 | from .reference_image import ReferenceImage 33 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/config_files/long_demo_config.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2024 Andrew Rechnitzer 3 | 4 | test_spec = "demo" 5 | test_sources = "demo" 6 | prenaming_enabled = true 7 | classlist = "long_demo" 8 | num_to_produce = 550 9 | 10 | # bundle 1 11 | [[bundles]] 12 | first_paper = 1 13 | last_paper = 25 14 | # bundle 2 15 | [[bundles]] 16 | first_paper = 26 17 | last_paper = 50 18 | # bundle 3 19 | [[bundles]] 20 | first_paper = 51 21 | last_paper = 100 22 | # bundle 4 23 | [[bundles]] 24 | first_paper = 101 25 | last_paper = 150 26 | # bundle 5 27 | [[bundles]] 28 | first_paper = 151 29 | last_paper = 200 30 | 31 | # HW bundle 1 32 | [[hw_bundles]] 33 | paper_number = 241 34 | student_id = 98989898 35 | student_name = "Kenson, Ken" 36 | pages = [[1], [2], [], [2, 3], [3]] 37 | -------------------------------------------------------------------------------- /plom/comment_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020 Vala Vakilian 3 | # Copyright (C) 2021-2022 Colin B. Macdonald 4 | 5 | import random 6 | 7 | 8 | def generate_new_comment_ID(num_of_digits=12): 9 | """Generate a random number string as a new comment ID. 10 | 11 | Args: 12 | num_of_digits (int, optional): Number of digits for comment ID. 13 | Defaults to 12. 14 | 15 | Returns: 16 | str: A 12 digit number as a string representing the new comment 17 | ID. 18 | """ 19 | # TODO: Why string you ask ? Well because of this: 20 | # comIDi = QStandardItem(com["id"]) 21 | # OverflowError: argument 1 overflowed: value must be in the range -2147483648 to 2147483647 22 | return str(random.randint(10**num_of_digits, 10 ** (num_of_digits + 1) - 1)) 23 | -------------------------------------------------------------------------------- /plom_server/Progress/forms.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023 Colin B. Macdonald 4 | # Copyright (C) 2023 Brennen Chiu 5 | 6 | from django import forms 7 | 8 | 9 | class AnnotationFilterForm(forms.Form): 10 | TIME_CHOICES = [ 11 | ("", ""), 12 | ("300", "the last 5 minutes"), 13 | ("1800", "the last 30 minutes"), 14 | ("3600", "the last hour"), 15 | ("7200", "the last 2 hours"), 16 | ("10800", "the last 3 hours"), 17 | ("21600", "the last 6 hours"), 18 | ("86400", "the last day"), 19 | ] 20 | 21 | time_filter_seconds = forms.TypedChoiceField( 22 | choices=TIME_CHOICES, 23 | coerce=int, 24 | required=False, 25 | label="Filter for annotations created in", 26 | ) 27 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/views/base.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023 Andrew Rechnitzer 4 | # Copyright (C) 2023, 2025 Colin B. Macdonald 5 | 6 | from django.http import HttpResponseRedirect 7 | from django.urls import reverse 8 | 9 | from plom_server.Base.base_group_views import ManagerRequiredView 10 | from plom_server.Papers.services import PaperInfoService 11 | 12 | 13 | class SpecBaseView(ManagerRequiredView): 14 | def dispatch(self, request, *args, **kwargs): 15 | """Redirect to the assessment preparation page if the Papers database is already populated.""" 16 | if PaperInfoService.is_paper_database_populated(): 17 | return HttpResponseRedirect(reverse("prep_landing")) 18 | return super().dispatch(request, *args, **kwargs) 19 | -------------------------------------------------------------------------------- /plom/cli/bundle_tools.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | from plom.cli import with_messenger 8 | from plom.scan.question_list_utils import _parse_questions 9 | 10 | 11 | @with_messenger 12 | def upload_bundle(pdf: Path, *, msgr) -> dict[str, Any]: 13 | """Upload a bundle from a local pdf file.""" 14 | return msgr.new_server_upload_bundle(pdf) 15 | 16 | 17 | @with_messenger 18 | def bundle_map_page( 19 | bundle_id: int, page: int, *, papernum: int, questions: str | list, msgr 20 | ) -> None: 21 | """Map a page of a bundle to zero or more questions.""" 22 | questions = _parse_questions(questions) 23 | msgr.new_server_bundle_map_page( 24 | bundle_id, page, papernum=papernum, questions=questions 25 | ) 26 | -------------------------------------------------------------------------------- /plom_server/templates/Finish/finish_not_printed.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "base/base.html" %} 7 | {% load static %} 8 | {% block title %} 9 | Papers not yet printed 10 | {% endblock title %} 11 | {% block page_heading %} 12 | Papers not yet printed 13 | {% endblock page_heading %} 14 | {% block main_content %} 15 |
16 |

17 | Papers have not yet been printed; this page will be accessible after you tell the system that papers have been printed. 18 |

19 |
20 | 23 | {% endblock main_content %} 24 | -------------------------------------------------------------------------------- /plom_server/Preparation/useful_files_for_testing/spec_with_shared_pages.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2025 Colin B. Macdonald 3 | 4 | # modified an older version of the demo to experiment with shared pages 5 | 6 | name = "example_shared" 7 | longName = "Midterm example using shared pages" 8 | 9 | numberOfVersions = 2 10 | numberOfPages = 6 11 | totalMarks = 20 12 | numberOfQuestions = 5 13 | idPage = 1 14 | doNotMarkPages = [2] 15 | allowSharedPages = true 16 | 17 | [[question]] 18 | label = "Q1a" 19 | pages = [3] 20 | mark = 2 21 | 22 | [[question]] 23 | label = "Q1bc" 24 | pages = [3] 25 | mark = 3 26 | 27 | [[question]] 28 | label = "Q2" 29 | pages = [4] 30 | mark = 5 31 | 32 | [[question]] 33 | label = "Q3" 34 | pages = [5, 6] 35 | mark = 9 36 | 37 | [[question]] 38 | label = "Q3ex" 39 | pages = [6] 40 | mark = 1 41 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/views/spec_upload.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Colin B. Macdonald 3 | 4 | from django.http import HttpRequest, HttpResponse 5 | from django.shortcuts import render 6 | 7 | from plom_server.Base.base_group_views import ManagerRequiredView 8 | from plom_server.Papers.services import SpecificationService 9 | 10 | 11 | class SpecUploadView(ManagerRequiredView): 12 | """Serves an "upload file" page but somewhat strangely doesn't process the form. 13 | 14 | Processing is handled by :class:`SpecEditorView`. 15 | """ 16 | 17 | def get(self, request: HttpRequest) -> HttpResponse: 18 | context = self.build_context() 19 | context.update({"is_there_a_spec": SpecificationService.is_there_a_spec()}) 20 | return render(request, "SpecCreator/spec_upload.html", context) 21 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/home.html: -------------------------------------------------------------------------------- 1 | 7 | {% extends "base/base.html" %} 8 | {% load static %} 9 | {% block title %} 10 | Welcome to Plom 11 | {% endblock title %} 12 | {% block page_heading %} 13 | Welcome to Plom 14 | {% endblock page_heading %} 15 | {% block main_content %} 16 | {% include "../base/alert_messages.html" with messages=messages %} 17 | {% if assessment_longname_if_defined %} 18 |

19 | This server is configured for the 20 | assessment {{ assessment_longname_if_defined }}. 21 |

22 | {% endif %} 23 |

Choose a task from the menu.

24 | {% endblock main_content %} 25 | -------------------------------------------------------------------------------- /plom_server/templates/Authentication/signup_import_users.html: -------------------------------------------------------------------------------- 1 | 5 | {% extends "Authentication/signup_base.html" %} 6 | {% load static %} 7 | {% block card_content %} 8 |
12 | {% csrf_token %} 13 |
14 | 20 |
21 | 22 |
23 | {% endblock card_content %} 24 | -------------------------------------------------------------------------------- /plom_server/templates/Rectangles/home.html: -------------------------------------------------------------------------------- 1 | 5 | {% extends "base/base.html" %} 6 | {% block title %} 7 | Extract rectangles from pages 8 | {% endblock title %} 9 | {% block page_heading %} 10 | Extract rectangles from pages 11 | {% endblock page_heading %} 12 | {% block main_content %} 13 | {% for v in version_list %} 14 |
15 |
16 |
Pages from version {{ v }}
17 | {% for pg in page_list %} 18 | page {{ pg }} 19 | {% endfor %} 20 |
21 |
22 | {% endfor %} 23 | {% endblock main_content %} 24 | -------------------------------------------------------------------------------- /plom/test_tags.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2024 Colin B. Macdonald 3 | 4 | from plom.tagging import is_valid_tag_text as valid 5 | 6 | 7 | def test_tag_basic_valid() -> None: 8 | assert valid("hello") 9 | assert valid("numb3rs") 10 | assert valid("under_score") 11 | assert valid("hy-phen") 12 | assert valid("me+you") 13 | assert valid(":colon:") 14 | assert valid("semicolon;") 15 | assert valid("@user1") 16 | 17 | 18 | def test_tag_invalid_chars() -> None: 19 | assert not valid("I <3 Plom") 20 | assert not valid("") 21 | 22 | 23 | # less confident about these! If the rules change, these could be adjusted 24 | def test_tag_quiestionable() -> None: 25 | assert not valid("two words") 26 | assert not valid(" stray_whitespace") 27 | assert not valid("stray_whitespace ") 28 | -------------------------------------------------------------------------------- /plom_server/templates/QuestionClustering/fragments/clustering_tag_cell.html: -------------------------------------------------------------------------------- 1 | 5 | {% if tags %} 6 | {% for tag_pk, tag_text in tags %} 7 | 8 | {{ tag_text }} 9 | 16 | 17 | {% endfor %} 18 | {% else %} 19 | No tags 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /.codespell-ignorewords: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2021, 2023, 2025 Colin B. Macdonald 3 | # Copyright (C) 2022-2023 Natalie Balashov 4 | # Copyright (C) 2022 Edith Coates 5 | # 6 | # Copying and distribution of this file, with or without modification, 7 | # are permitted in any medium without royalty provided the copyright 8 | # notice and this notice are preserved. This file is offered as-is, 9 | # without any warranty. 10 | 11 | # words that codespell should not complain about 12 | 13 | # used for histogram in variable names 14 | hist 15 | # TE commonly used for QTextEdit 16 | te 17 | # unvalidate() function used in the plom_server/SpecCreator directory 18 | unvalidate 19 | # Frobenius norm calculated in plom_server/Scan/tests/test_image_process.py 20 | fro 21 | # unit test framework 22 | assertIn 23 | # regarding regrading... 24 | regrading 25 | regraded 26 | -------------------------------------------------------------------------------- /plom_server/Scan/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2023-2024 Andrew Rechnitzer 5 | # Copyright (C) 2025 Colin B. Macdonald 6 | 7 | """Services of the Plom Server Scan app.""" 8 | 9 | from .scan_service import ScanService 10 | from .cast_service import ScanCastService 11 | from .image_process import PageImageProcessor 12 | from .qr_service import QRService 13 | from .image_rotate import ImageRotateService 14 | 15 | from .hard_rotate import hard_rotate_image_from_file_by_exif_and_angle 16 | from .util import ( 17 | check_bundle_object_is_neither_locked_nor_pushed, 18 | check_any_bundle_push_locked, 19 | update_thumbnail_after_rotation, 20 | ) 21 | 22 | from .manage_scan import ManageScanService 23 | from .manage_discard import ManageDiscardService 24 | -------------------------------------------------------------------------------- /plom/create/fonts/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-or-later 2 | # Copyright (C) 2021 Elizabeth Xiao 3 | # Copyright (C) 2025 Bryan Tanaday 4 | # Copyright (C) 2025 Andrew Rechnitzer 5 | # Copyright (C) 2025 Philip D. Loewen 6 | # Copyright (C) 2025 Lindsey Daniels 7 | # Copyright (C) 2025 Negar Harandi 8 | 9 | """Fonts based on handwriting samples of the people who wrote this module. 10 | 11 | To add a new font based on your own handwriting, we have used the 12 | tool Handwrite (https://pypi.org/project/handwrite). Its not necessary 13 | to commit the PDF/jpeg source, just the resulting font. 14 | 15 | As indicated at the top of this init file, the fonts are licensed under 16 | the LGPL-3.0-or-later by default (the license chosen by Elizabeth Xiao 17 | who did the first one). If you prefer a different license, speak up 18 | and we'll document them each separately. 19 | """ 20 | -------------------------------------------------------------------------------- /plom_server/Rectangles/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | from django.urls import path 4 | 5 | from .views import ( 6 | RectangleHomeView, 7 | SelectRectangleView, 8 | ExtractedRectangleView, 9 | ZipExtractedRectangleView, 10 | ) 11 | 12 | urlpatterns = [ 13 | path("", RectangleHomeView.as_view(), name="rectangle_home"), 14 | path( 15 | "select//", 16 | SelectRectangleView.as_view(), 17 | name="select_rectangle", 18 | ), 19 | path( 20 | "extract///", 21 | ExtractedRectangleView.as_view(), 22 | name="extracted_rectangle", 23 | ), 24 | path( 25 | "zip//", 26 | ZipExtractedRectangleView.as_view(), 27 | name="zip_rectangles", 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /plom_server/templates/Finish/soln_source_attempt.html: -------------------------------------------------------------------------------- 1 | 5 | {% extends "base/base.html" %} 6 | {% block page_heading %} 7 | Manage solution source pdfs 8 | {% endblock page_heading %} 9 | {% block main_content %} 10 |
11 |
12 | {% if success %} 13 |

Source PDF version {{ version }} uploaded successfully

14 | {% else %} 15 |

Source PDF version {{ version }} upload failed

16 |

{{ message }}

17 | {% endif %} 18 | return 19 |
20 |
21 | {% endblock main_content %} 22 | -------------------------------------------------------------------------------- /plom/create/test_build_source_exams.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020-2021, 2024-2025 Colin B. Macdonald 3 | # Copyright (C) 2020 Dryden Wiebe 4 | 5 | import os 6 | 7 | from plom.misc_utils import working_directory 8 | from .demotools import buildDemoSourceFiles 9 | 10 | 11 | def test_latex_demofiles(tmp_path) -> None: 12 | """Builds the demo LaTeX source files and confirms the files exist.""" 13 | with working_directory(tmp_path): 14 | assert buildDemoSourceFiles() 15 | assert set(os.listdir("sourceVersions")) == set( 16 | ("version1.pdf", "version2.pdf") 17 | ) 18 | 19 | 20 | def test_latex_demofiles_dir(tmp_path) -> None: 21 | assert buildDemoSourceFiles(tmp_path) 22 | pdfs = [x.name for x in (tmp_path / "sourceVersions").glob("*.pdf")] 23 | assert set(pdfs) == set(("version1.pdf", "version2.pdf")) 24 | -------------------------------------------------------------------------------- /plom_server/Finish/management/commands/plom_build_all_soln.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023-2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from tqdm import tqdm 6 | 7 | from django.core.management.base import BaseCommand 8 | from django.core.management import call_command 9 | 10 | from plom_server.Scan.services import ManageScanService 11 | 12 | 13 | class Command(BaseCommand): 14 | """Build solutions for all scanned papers.""" 15 | 16 | def handle(self, *args, **options): 17 | # get a list of all complete papers 18 | complete_paper_keys = ManageScanService.get_all_complete_papers().keys() 19 | for n, pn in tqdm( 20 | enumerate(complete_paper_keys), 21 | desc="Building solution pdfs for each paper.", 22 | ): 23 | call_command("plom_build_soln", pn, "-w") 24 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2021, 2023, 2025 Colin B. Macdonald 3 | # Copyright (C) 2022 Andrew Rechnitzer 4 | # 5 | # Copying and distribution of this file, with or without modification, 6 | # are permitted in any medium without royalty provided the copyright 7 | # notice and this notice are preserved. This file is offered as-is, 8 | # without any warranty. 9 | 10 | [flake8] 11 | max-line-length = 88 12 | # E203 = conflicts with black 13 | # E402 = seems to happen our init files - module import must be at top of file 14 | # E501 = we have too many long lines 15 | # W503 = black breaks lines before a binary op which flake does not like, but black does not mind 16 | # E741 = do not use variables called I or O (confusion with one zero) 17 | extend-ignore = E203, E501, W503, E741 18 | per-file-ignores = 19 | */__init__.py: F401, E402 20 | doc/source/conf.py: E402 21 | -------------------------------------------------------------------------------- /plom_server/Finish/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | import django.db.models.deletion 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | initial = True 8 | 9 | dependencies = [ 10 | ("Finish", "0001_initial"), 11 | ("Papers", "0001_initial"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="buildsolutionpdfchore", 17 | name="paper", 18 | field=models.ForeignKey( 19 | on_delete=django.db.models.deletion.CASCADE, to="Papers.paper" 20 | ), 21 | ), 22 | migrations.AddField( 23 | model_name="reassemblepaperchore", 24 | name="paper", 25 | field=models.ForeignKey( 26 | on_delete=django.db.models.deletion.CASCADE, to="Papers.paper" 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2022-2023 Edith Coates 5 | # Copyright (C) 2023, 2025 Colin B. Macdonald 6 | 7 | """Django's command-line utility for administrative tasks.""" 8 | 9 | import os 10 | import sys 11 | 12 | 13 | def main(): 14 | """Run administrative tasks.""" 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plom_server.settings") 16 | 17 | try: 18 | from django.core.management import execute_from_command_line 19 | except ImportError as exc: 20 | raise ImportError( 21 | "Couldn't import Django. Are you sure it's installed and " 22 | "available on your PYTHONPATH environment variable? Did you " 23 | "forget to activate a virtual environment?" 24 | ) from exc 25 | execute_from_command_line(sys.argv) 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /plom_server/Authentication/models.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2023, 2025 Colin B. Macdonald 4 | 5 | from django.db import models 6 | from django.contrib.auth.models import User 7 | from django.dispatch import receiver 8 | from django.db.models.signals import post_save 9 | 10 | 11 | class Profile(models.Model): 12 | user = models.OneToOneField(User, on_delete=models.CASCADE) 13 | email = models.EmailField(max_length=100) 14 | signup_confirmation = models.BooleanField(default=False) 15 | 16 | def __str__(self): 17 | """Conversion to a string.""" 18 | return self.user.username 19 | 20 | 21 | @receiver(post_save, sender=User) 22 | def update_profile_signal(sender, instance: User, created: bool, **kwargs): 23 | if created: 24 | Profile.objects.create(user=instance) 25 | instance.profile.save() 26 | -------------------------------------------------------------------------------- /plom_server/Preparation/views/mocker.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022 Brennen Chiu 4 | # Copyright (C) 2023-2025 Colin B. Macdonald 5 | # Copyright (C) 2024 Andrew Rechnitzer 6 | 7 | from io import BytesIO 8 | 9 | from django.http import HttpRequest, HttpResponse, FileResponse 10 | from django.core.files import File 11 | 12 | from plom_server.Base.base_group_views import ManagerRequiredView 13 | 14 | from ..services import ExamMockerService 15 | 16 | 17 | class MockExamView(ManagerRequiredView): 18 | """Create a mock test PDF.""" 19 | 20 | def post(self, request: HttpRequest, *, version: int) -> HttpResponse: 21 | mock_exam_pdf_bytes = ExamMockerService.mock_exam(version) 22 | mock_exam_file = File(BytesIO(mock_exam_pdf_bytes), name=f"mock_v{version}.pdf") 23 | return FileResponse(mock_exam_file, content_type="application/pdf") 24 | -------------------------------------------------------------------------------- /plom_server/QuestionTags/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | 4 | from django.urls import path 5 | from .views import ( 6 | QTagsLandingView, 7 | AddQuestionTagLinkView, 8 | CreateTagView, 9 | DeleteTagView, 10 | EditTagView, 11 | DownloadQuestionTagsView, 12 | ImportTagsView, 13 | ) 14 | 15 | urlpatterns = [ 16 | path("qtags/", QTagsLandingView.as_view(), name="qtags_landing"), 17 | path("add/", AddQuestionTagLinkView.as_view(), name="add_question_tag"), 18 | path("create/", CreateTagView.as_view(), name="create_tag"), 19 | path("delete/", DeleteTagView.as_view(), name="delete_tag"), 20 | path("edit/", EditTagView.as_view(), name="edit_tag"), 21 | path( 22 | "download/", DownloadQuestionTagsView.as_view(), name="download_question_tags" 23 | ), 24 | path("import-tags/", ImportTagsView.as_view(), name="import_tags"), 25 | ] 26 | -------------------------------------------------------------------------------- /plom_server/templates/403.html: -------------------------------------------------------------------------------- 1 | 7 | {% extends "base/base.html" %} 8 | {% load static %} 9 | {% block title %} 10 | 403 - Forbidden 11 | {% endblock title %} 12 | {% block page_heading %} 13 |

Error!

14 | {% endblock page_heading %} 15 | {% block main_content %} 16 |
17 |
403 - Forbidden
18 |
19 |

Your account does not have permission to view this directory or page.

20 |

Click below to go back.

21 | 22 |
23 |
24 | {% endblock main_content %} 25 | -------------------------------------------------------------------------------- /doc/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2021-2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | .. _plom-client: 6 | 7 | Getting started with the Plom Client 8 | ==================================== 9 | 10 | So you want to use Plom to grade some papers? 11 | You probably want to start with the Plom Client, which can be 12 | obtained in several ways: 13 | 14 | * GNU/Linux users can install `from Flathub`_. 15 | * Compiled binaries are available from our `releases page`_. 16 | * Install from source or using `pip`. 17 | 18 | .. _from Flathub: https://flathub.org/apps/org.plomgrading.PlomClient 19 | .. _releases page: https://gitlab.com/plom/plom-client/-/releases/ 20 | 21 | See :doc:`install-client` for more details, including a few caveats 22 | and "gotchas". 23 | 24 | 25 | Using the client 26 | ---------------- 27 | 28 | .. note:: 29 | 30 | Stub: move and/or write documentation. 31 | -------------------------------------------------------------------------------- /plom/tagging.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2019-2023 Andrew Rechnitzer 3 | # Copyright (C) 2020-2023 Colin B. Macdonald 4 | # Copyright (C) 2020 Vala Vakilian 5 | # Copyright (C) 2022 Chris Jin 6 | 7 | """Misc utilities related to tagging.""" 8 | 9 | import re 10 | 11 | plom_valid_tag_text_description = "letters, numbers, _ - + : ; or @, but no spaces." 12 | plom_valid_tag_text_pattern = r"^[\w\-\+\:\;\@]+$" 13 | plom_valid_tag_text_re = re.compile(plom_valid_tag_text_pattern) 14 | 15 | 16 | def is_valid_tag_text(tag_text: str) -> bool: 17 | """Compare tag text against an allow list of acceptable characters. 18 | 19 | The allow list is currently: 20 | * alphanumeric characters 21 | * "_", "-", "+", ":", ";", "@" 22 | """ 23 | # allow_list = ("_", "-", "+", ":", ";", "@") 24 | if plom_valid_tag_text_re.match(tag_text): 25 | return True 26 | else: 27 | return False 28 | -------------------------------------------------------------------------------- /plom_server/Launcher/management/commands/plom_build_scrap_extra_pdfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | # Copyright (C) 2025 Colin B. Macdonald 4 | 5 | from django.core.management.base import BaseCommand 6 | from django.conf import settings 7 | 8 | from plom.create import ( 9 | build_extra_page_pdf, 10 | build_scrap_paper_pdf, 11 | build_bundle_separator_paper_pdf, 12 | ) 13 | 14 | 15 | class Command(BaseCommand): 16 | """Build the extra-page and scrap-paper PDFs and put them into static storage.""" 17 | 18 | def handle(self, *args, **options): 19 | """Build and store the extra-page and scrap paper pdfs.""" 20 | dest_dir = settings.MEDIA_ROOT / "non_db_files/" 21 | dest_dir.mkdir(exist_ok=True, parents=True) 22 | build_extra_page_pdf(dest_dir) 23 | build_scrap_paper_pdf(dest_dir) 24 | build_bundle_separator_paper_pdf(dest_dir) 25 | -------------------------------------------------------------------------------- /plom_server/TestingSupport/config_files/hw_bundle_config.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023 Andrew Rechnitzer 4 | 5 | test_spec = "demo" 6 | test_sources = "demo" 7 | num_to_produce = 5 8 | 9 | [[hw_bundles]] 10 | paper_number = 1 11 | student_id = 98989898 12 | student_name = "Kenson, Ken" 13 | pages = [[1], [2], [3]] 14 | 15 | [[hw_bundles]] 16 | paper_number = 2 17 | student_id = 97979797 18 | student_name = "Lawrence, Larry" 19 | pages = [[1, 2], [2, 3], [3]] 20 | 21 | [[hw_bundles]] 22 | paper_number = 3 23 | student_id = 95959595 24 | student_name = "Margery, Mary" 25 | pages = [[1], [2], [], [2, 3], []] 26 | 27 | [[hw_bundles]] 28 | paper_number = 4 29 | student_id = 94949494 30 | student_name = "Nowra, Nora" 31 | pages = [[1, 2, 3], [3]] 32 | 33 | [[hw_bundles]] 34 | paper_number = 5 35 | student_id = 93939393 36 | student_name = "Ostler, Owen" 37 | pages = [[], [1], [2, 3]] 38 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | Building Plom's Documentation 2 | ============================= 3 | 4 | In the doc/ directory: 5 | ``` 6 | make autodocs 7 | # all of the following produce different outputs, `make help` for more info 8 | make html 9 | make singlehtml 10 | make latexpdf 11 | make linkcheck 12 | ``` 13 | then display the webpages in your browser 14 | ``` 15 | firefox build/html/index.html 16 | ``` 17 | 18 | ## Notes: 19 | 20 | * TODO: what should be the relationship between this autogen stuff 21 | and the official website? PrairieLearn strikes a good balance (I think), 22 | although it's target users are somewhat adept programmers. 23 | 24 | * Many projects don't use sphinx-apidoc at all; they manually populate .rst files with 25 | automodule / autodoc / auto... for each thing they want presented: 26 | [python docs](https://github.com/python/cpython/tree/main/Doc) 27 | using `sphinx-build` to generate html files. 28 | TODO: We should follow suit? 29 | -------------------------------------------------------------------------------- /plom_server/templates/base/base-2col.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "base/base.html" %} 7 | {% block main_content %} 8 |
9 | {% block alert %} 10 | {% endblock alert %} 11 |
12 |
13 | {% block left_column %} 14 | {% endblock left_column %} 15 |
16 |
17 |
18 |
19 | {% block right_column %} 20 | {% endblock right_column %} 21 |
22 |
23 |
24 |
25 | {% endblock main_content %} 26 | -------------------------------------------------------------------------------- /plom/create/test_upload_classlist.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020 Dryden S. Wiebe 3 | # Copyright (C) 2020-2022, 2024 Colin B. Macdonald 4 | 5 | from unittest.mock import MagicMock 6 | 7 | from plom.messenger import ManagerMessenger 8 | 9 | from plom.create.upload_classlist import _raw_upload_classlist 10 | 11 | 12 | def test_produce_upload_classlist() -> None: 13 | classlist = [{"id": 10050380, "name": "Fink, Iris"}] 14 | expected_call_cl = classlist 15 | 16 | msgr = ManagerMessenger() 17 | msgr.upload_classlist = MagicMock(return_value=None) # type: ignore 18 | msgr.closeUser = MagicMock(return_value=None) # type: ignore 19 | msgr.stop = MagicMock(return_value=None) # type: ignore 20 | 21 | _raw_upload_classlist(classlist=classlist, msgr=msgr) 22 | 23 | msgr.upload_classlist.assert_called_with(expected_call_cl, False) 24 | msgr.closeUser.assert_called() 25 | msgr.stop.assert_called() 26 | -------------------------------------------------------------------------------- /plom_server/templates/Preparation/pqv_mapping_attempt.html: -------------------------------------------------------------------------------- 1 | 6 | {% extends "base/base.html" %} 7 | {% block page_heading %} 8 | Question-version mapping upload 9 | {% endblock page_heading %} 10 | {% block main_content %} 11 |
Upload failed due to errors:
12 | 13 | return to upload page 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for err in errors %} 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 |
TypeMessage
{{ err.kind }}{{ err.err_text }}
27 | {% endblock main_content %} 28 | -------------------------------------------------------------------------------- /plom_server/UserManagement/models.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from django.db import models 6 | from django.contrib.auth.models import User 7 | 8 | 9 | class Quota(models.Model): 10 | """Represents a limitation on a user to mark only a certain number of questions.""" 11 | 12 | default_limit = 12 13 | user = models.OneToOneField(User, on_delete=models.CASCADE) 14 | limit = models.IntegerField(default=default_limit) 15 | 16 | def __str__(self) -> str: 17 | """Return a string representation of the quota.""" 18 | return f"Quota for {self.user.username} with limit {self.limit}" 19 | 20 | @classmethod 21 | def set_default_limit(cls, new_limit: int) -> None: 22 | """Change the default quota limit. 23 | 24 | Args: 25 | new_limit: the new quota limit. 26 | """ 27 | cls.default_limit = new_limit 28 | -------------------------------------------------------------------------------- /plom/create/test_page_counts.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | import pymupdf 6 | 7 | from plom.create.demotools import buildDemoSourceFiles 8 | from plom.create.buildDatabaseAndPapers import check_equal_page_count 9 | 10 | 11 | def test_equal_page_count_true(tmp_path) -> None: 12 | """Checks that the page counts of each source version pdf are equal.""" 13 | # build the source version pdfs in sourceVersions/ 14 | buildDemoSourceFiles(tmp_path) 15 | assert check_equal_page_count(tmp_path / "sourceVersions") 16 | 17 | 18 | def test_equal_page_count_false(tmp_path) -> None: 19 | buildDemoSourceFiles(tmp_path) 20 | # create a new file with a single page 21 | with pymupdf.open() as clone: 22 | clone.new_page() 23 | clone.save(tmp_path / "sourceVersions/version3.pdf") 24 | assert not check_equal_page_count(tmp_path / "sourceVersions") 25 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/services/model_loader.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | from functools import lru_cache 5 | 6 | from plom_ml.clustering.model.model_type import ClusteringType 7 | from plom_ml.clustering.model.clustering_strategy import ( 8 | ClusteringStrategy, 9 | MCQClusteringStrategy, 10 | HMEClusteringStrategy, 11 | ) 12 | 13 | 14 | @lru_cache() 15 | def get_ClusteringStrategy(model_type: ClusteringType) -> ClusteringStrategy: 16 | """Load and cache one ClusteringStrategy instance per type, per process. 17 | 18 | Note: we use @lru_cache to reduce memory blow-up due to multiple model instantiations 19 | for same task. 20 | """ 21 | if model_type == ClusteringType.MCQ: 22 | return MCQClusteringStrategy() 23 | elif model_type == ClusteringType.HME: 24 | return HMEClusteringStrategy() 25 | else: 26 | raise ValueError(f"Unsupported model type: {model_type}") 27 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2023-2024 Colin B. Macdonald 4 | # Copyright (C) 2023-2024 Andrew Rechnitzer 5 | 6 | from django.urls import path 7 | 8 | from . import views 9 | 10 | 11 | urlpatterns = [ 12 | path("", views.SpecEditorView.as_view(), name="creator_launch"), 13 | path("delete", views.HTMXDeleteSpec.as_view(), name="spec_delete"), 14 | path("download", views.SpecDownloadView.as_view(), name="spec_download"), 15 | path("upload", views.SpecUploadView.as_view(), name="spec_upload"), 16 | path("summary", views.SpecSummaryView.as_view(), name="spec_summary"), 17 | path( 18 | "summary/", 19 | views.HTMXSummaryQuestion.as_view(), 20 | name="spec_summary_q", 21 | ), 22 | path( 23 | "template", 24 | views.TemplateSpecBuilderView.as_view(), 25 | name="template_spec_builder", 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation main file 2 | Copyright (C) 2020-2025 Colin B. Macdonald 3 | SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | ################################ 6 | Welcome to Plom's documentation! 7 | ################################ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | getting_started.rst 14 | 15 | running_a_server.rst 16 | 17 | preparing_an_assessment.rst 18 | 19 | multiversion.rst 20 | 21 | scanning 22 | 23 | identifying_papers.rst 24 | 25 | marking.rst 26 | 27 | hw_processing.rst 28 | 29 | rubrics.rst 30 | 31 | solutions.rst 32 | 33 | returning.rst 34 | 35 | cmdline.rst 36 | 37 | api.rst 38 | 39 | code.rst 40 | 41 | install.rst 42 | 43 | development 44 | 45 | faq 46 | 47 | changelog.md 48 | 49 | glossary 50 | 51 | 52 | ################## 53 | Indices and tables 54 | ################## 55 | 56 | * :ref:`genindex` 57 | * :ref:`modindex` 58 | * :ref:`search` 59 | -------------------------------------------------------------------------------- /.gitlab/merge_request_templates/default.md: -------------------------------------------------------------------------------- 1 | # Merge request template 2 | 3 | (Please give your MR a descriptive title: not just "Fixes #xyz") 4 | 5 | (Describe your change. Make sure you link to any related issues. For example, include 6 | the phrase `Fixes #xyz`.) 7 | 8 | 9 | ## Changelog entry 10 | 11 | (delete all but one choice from this list) 12 | 13 | - I have included a draft below. 14 | - I have included a Changelog entry within the commit. 15 | - This change does not need a changelog entry. 16 | - I'm not sure about this (don't worry, we'll help!) 17 | 18 | (Not all merge requests need a Changelog entry: ask yourself if it would be useful for a Plom 19 | Server administrator or a Plom Client user to read a single-sentence summary of this change? 20 | If so, please draft a sentence here; we will copy this into 21 | [the Changelog](https://gitlab.com/plom/plom/-/blob/master/CHANGELOG.md) after mergeing.) 22 | 23 | 24 | ## Anything else 25 | 26 | E.g., you could suggest a particular reviewer. 27 | -------------------------------------------------------------------------------- /plom_server/Scan/models/scan_background_chores.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2022-2023 Brennen Chiu 4 | # Copyright (C) 2023 Andrew Rechnitzer 5 | # Copyright (C) 2023-2024 Colin B. Macdonald 6 | 7 | from django.db import models 8 | 9 | from plom_server.Base.models import HueyTaskTracker 10 | from ..models import StagingBundle 11 | 12 | 13 | class PagesToImagesChore(HueyTaskTracker): 14 | """Manage the background chore (Huey Task) for converting PDF pages into images.""" 15 | 16 | bundle = models.ForeignKey(StagingBundle, null=True, on_delete=models.CASCADE) 17 | completed_pages = models.PositiveIntegerField(default=0) 18 | 19 | 20 | class ManageParseQRChore(HueyTaskTracker): 21 | """Manage the background chore (Huey Task) to parse QR codes from images.""" 22 | 23 | bundle = models.ForeignKey(StagingBundle, null=True, on_delete=models.CASCADE) 24 | completed_pages = models.PositiveIntegerField(default=0) 25 | -------------------------------------------------------------------------------- /plom_server/Launcher/management/commands/plom_get_static_javascript.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | from pathlib import Path 5 | 6 | from django.core.management.base import BaseCommand 7 | from django.conf import settings 8 | 9 | from plom_server.get_js import download_javascript_and_css_to_static 10 | 11 | 12 | class Command(BaseCommand): 13 | """Download Javascript dependencies and cache them somewhere in static.""" 14 | 15 | def handle(self, *args, **options): 16 | """Download Javascript dependencies and cache them somewhere in static.""" 17 | # TODO: hardcoding here not great: the first entry is the source code itself 18 | # please don't write there (Issue #2932). 2nd entry is relative to CWD, at least 19 | # we should have write permission there! 20 | destdir = settings.STATICFILES_DIRS[1] 21 | Path(destdir).mkdir(exist_ok=True) 22 | download_javascript_and_css_to_static(destdir) 23 | -------------------------------------------------------------------------------- /plom_server/Preparation/views/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2024 Andrew Rechnitzer 3 | # Copyright (C) 2022-2023 Edith Coates 4 | # Copyright (C) 2023-2025 Colin B. Macdonald 5 | # Copyright (C) 2024 Aidan Murphy 6 | 7 | from .home import ( 8 | PreparationLandingView, 9 | PreparationDependencyConflictView, 10 | PreparationFinishedView, 11 | ) 12 | from .source_manage import SourceManageView, ReferenceImageView 13 | from .prenaming import PrenamingView, PrenamingConfigView 14 | from .classlist_manage import ( 15 | ClasslistView, 16 | ClasslistDownloadView, 17 | ) 18 | from .pqv_mapping import ( 19 | PQVMappingView, 20 | PQVMappingDownloadView, 21 | PQVMappingDeleteView, 22 | PQVMappingUploadView, 23 | ) 24 | from .mocker import MockExamView 25 | from .miscellanea import ( 26 | MiscellaneaView, 27 | MiscellaneaDownloadExtraPageView, 28 | MiscellaneaDownloadScrapPaperView, 29 | MiscellaneaDownloadBundleSeparatorView, 30 | ) 31 | -------------------------------------------------------------------------------- /plom_server/API/views/experimental/rubrics.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from .base import ManagerReadOnlyViewSet 6 | 7 | from plom_server.Rubrics.models import Rubric 8 | from plom_server.Rubrics.serializers import RubricSerializer 9 | 10 | 11 | class RubricViewSet(ManagerReadOnlyViewSet): 12 | """Endpoints for the Rubric model. Only safe methods are enabled. 13 | 14 | 'rubrics/': 15 | GET: list all rubrics (can be filtered) 16 | POST: create a new rubric (disabled) 17 | 18 | 'rubrics/rid/': 19 | GET: retrieve rubric with rid 20 | PUT: update rubric by rid (disabled) 21 | PATCH: modify rubric by rid(disabled) 22 | DELETE: delete rubric by rid (disabled) 23 | """ 24 | 25 | queryset = Rubric.objects.all() 26 | serializer_class = RubricSerializer 27 | filterset_fields = ("kind", "display_delta", "value", "out_of", "text") 28 | lookup_field = "rid" 29 | -------------------------------------------------------------------------------- /plom/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020-2021, 2023-2024 Colin B. Macdonald 3 | 4 | import plom.plom_exceptions 5 | from plom.plom_exceptions import PlomException, PlomAuthenticationException 6 | from plom.plom_exceptions import * # noqa 7 | 8 | 9 | def test_plom_exc_string() -> None: 10 | e = PlomException("foo") 11 | assert str(e) == "foo" 12 | 13 | 14 | def test_exc_inheritance() -> None: 15 | e = PlomAuthenticationException() 16 | assert isinstance(e, PlomException) 17 | 18 | 19 | def test_exc_auth_has_default_msg() -> None: 20 | e = PlomAuthenticationException() 21 | assert "authenticate" in str(e).lower() 22 | e = PlomAuthenticationException("foo") 23 | assert str(e) == "foo" 24 | 25 | 26 | def test_exc_all_print_properly() -> None: 27 | excs = [eval(e) for e in dir(plom.plom_exceptions) if e.startswith("Plom")] 28 | assert len(excs) > 10 # we have some exceptions 29 | for exc in excs: 30 | assert str(exc("foo")) == "foo" 31 | -------------------------------------------------------------------------------- /plom_server/SpecCreator/views/spec_download.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Colin B. Macdonald 3 | 4 | import io 5 | 6 | from django.core.exceptions import ObjectDoesNotExist 7 | from django.http import HttpRequest, HttpResponse, FileResponse, Http404 8 | 9 | from plom_server.Base.base_group_views import ManagerRequiredView 10 | from plom_server.Papers.services import SpecificationService 11 | 12 | 13 | class SpecDownloadView(ManagerRequiredView): 14 | """Grab the toml of the current server specification.""" 15 | 16 | def get(self, request: HttpRequest) -> HttpResponse | FileResponse: 17 | try: 18 | toml = SpecificationService.get_the_spec_as_toml(include_public_code=True) 19 | except ObjectDoesNotExist as e: 20 | raise Http404(e) from e 21 | return FileResponse( 22 | io.BytesIO(toml.encode("utf-8")), 23 | as_attachment=True, 24 | filename=SpecificationService.get_short_name_slug() + "_spec.toml", 25 | ) 26 | -------------------------------------------------------------------------------- /plom_server/templates/QuestionClustering/fragments/error_detail_modal.html: -------------------------------------------------------------------------------- 1 | 3 | 23 | -------------------------------------------------------------------------------- /plom_server/Rubrics/serializers.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022-2023 Edith Coates 3 | # Copyright (C) 2023 Brennen Chiu 4 | # Copyright (C) 2024 Aden Chan 5 | # Copyright (C) 2024 Colin B. Macdonald 6 | 7 | from django.contrib.auth.models import User 8 | from rest_framework import serializers 9 | 10 | from .models import Rubric 11 | from plom_server.QuestionTags.serializers import PedagogyTagSerializer 12 | 13 | 14 | class RubricSerializer(serializers.ModelSerializer): 15 | user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) 16 | pedagogy_tags = PedagogyTagSerializer(many=True, read_only=True, required=False) 17 | 18 | class Meta: 19 | model = Rubric 20 | fields = "__all__" 21 | extra_kwargs = { 22 | "tags": { 23 | "required": False, 24 | "allow_blank": True, 25 | }, 26 | "meta": { 27 | "required": False, 28 | "allow_blank": True, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /plom_server/demo_files/bundle_for_plaid_demo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Andrew Rechnitzer 3 | 4 | # bundle 1 5 | [[bundles]] 6 | first_paper = 1 7 | last_paper = 25 8 | # bundle 2 9 | [[bundles]] 10 | first_paper = 26 11 | last_paper = 50 12 | # bundle 3 13 | [[bundles]] 14 | first_paper = 51 15 | last_paper = 100 16 | # bundle 4 17 | [[bundles]] 18 | first_paper = 101 19 | last_paper = 150 20 | # bundle 5 21 | [[bundles]] 22 | first_paper = 151 23 | last_paper = 200 24 | # bundle 6 25 | [[bundles]] 26 | first_paper = 201 27 | last_paper = 300 28 | # bundle 7 29 | [[bundles]] 30 | first_paper = 301 31 | last_paper = 400 32 | # bundle 8 33 | [[bundles]] 34 | first_paper = 401 35 | last_paper = 500 36 | # bundle 9 37 | [[bundles]] 38 | first_paper = 501 39 | last_paper = 600 40 | # bundle 10 41 | [[bundles]] 42 | first_paper = 601 43 | last_paper = 700 44 | # bundle 11 45 | [[bundles]] 46 | first_paper = 701 47 | last_paper = 800 48 | 49 | # bundle 12 50 | [[bundles]] 51 | first_paper = 801 52 | last_paper = 900 53 | -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2023-2024 Colin B. Macdonald 4 | 5 | upstream localhost { 6 | server plom:8000; 7 | } 8 | 9 | server { 10 | # For plain ol' unsecured non-https 11 | # listen 80; 12 | listen 443 ssl; 13 | 14 | # Seems to work without naming server here... 15 | #server_name example.com; 16 | 17 | # "Nobody will ever need more than 640K of RAM" 18 | client_max_body_size 512M; 19 | 20 | # If using self-signed certs browsers will get a warning 21 | ssl_certificate /etc/ssl/private/plom.crt; 22 | ssl_certificate_key /etc/ssl/private/plom.key; 23 | 24 | location / { 25 | proxy_pass http://localhost; 26 | proxy_set_header Host $http_host; 27 | proxy_set_header X-Real-IP $remote_addr; 28 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 | proxy_set_header X-Forwarded-Proto $scheme; 30 | proxy_set_header X-Forwarded-Host $host; 31 | proxy_redirect off; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plom_server/Identify/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Andrew Rechnitzer 3 | from django.urls import path 4 | 5 | from .views import ( 6 | IDPredictionView, 7 | IDPredictionHXDeleteView, 8 | IDPredictionLaunchHXPutView, 9 | IDBoxParentView, 10 | GetIDBoxesRectangleView, 11 | ) 12 | 13 | urlpatterns = [ 14 | path( 15 | "id_prediction_del/", 16 | IDPredictionHXDeleteView.as_view(), 17 | name="id_prediction_delete", 18 | ), 19 | path("id_predictions", IDPredictionView.as_view(), name="id_prediction_home"), 20 | path( 21 | "id_predictions_launch", 22 | IDPredictionLaunchHXPutView.as_view(), 23 | name="id_prediction_launch", 24 | ), 25 | path( 26 | "id_rectangle_parent", 27 | IDBoxParentView.as_view(), 28 | name="get_id_box_parent", 29 | ), 30 | path( 31 | "id_rectangle/", 32 | GetIDBoxesRectangleView.as_view(), 33 | name="get_id_box_rectangle", 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /plom_server/QuestionClustering/forms.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | 4 | from django import forms 5 | from .models import ClusteringModelType 6 | 7 | 8 | class ClusteringJobForm(forms.Form): 9 | """Form used to create a background clustering job.""" 10 | 11 | choice = forms.ChoiceField( 12 | choices=ClusteringModelType.choices, 13 | widget=forms.RadioSelect( 14 | attrs={ 15 | "class": "form-radio text-blue-600 focus:ring-blue-500", 16 | } 17 | ), 18 | label="Pick answer type for clustering", 19 | ) 20 | question = forms.IntegerField(widget=forms.HiddenInput()) 21 | version = forms.IntegerField(widget=forms.HiddenInput()) 22 | page_num = forms.IntegerField(widget=forms.HiddenInput()) 23 | top = forms.FloatField(widget=forms.HiddenInput()) 24 | left = forms.FloatField(widget=forms.HiddenInput()) 25 | bottom = forms.FloatField(widget=forms.HiddenInput()) 26 | right = forms.FloatField(widget=forms.HiddenInput()) 27 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Aidan Murphy 3 | 4 | # makefile for Sphinx documentation 5 | 6 | # You can set these variables from the command line, and also 7 | # from the environment for the first two. 8 | SPHINXOPTS ?=-W -v 9 | SPHINXBUILD ?= sphinx-build 10 | SOURCEDIR = source 11 | BUILDDIR = build 12 | 13 | MODULES = plom_server plom 14 | SPHINXAPI = sphinx-apidoc 15 | SPHINXAPIOPTS =-d 1 -e -f 16 | 17 | # Put it first so that "make" without argument is like "make help". 18 | help: 19 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | 21 | .PHONY: help Makefile 22 | 23 | # sphinx-apidoc 24 | .PHONY: autodocs 25 | autodocs: $(MODULES) 26 | $(MODULES): 27 | @$(SPHINXAPI) -o "$(SOURCEDIR)/$@" "../$@" $(SPHINXAPIOPTS) $(O) 28 | 29 | 30 | # Catch-all target: route all unknown targets to Sphinx using the new 31 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 32 | %: Makefile 33 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 34 | -------------------------------------------------------------------------------- /plom_server/Scan/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022-2023 Brennen Chiu 4 | # Copyright (C) 2023, 2025 Andrew Rechnitzer 5 | # Copyright (C) 2023-2024 Colin B. Macdonald 6 | 7 | from django.contrib import admin 8 | 9 | from .models import ( 10 | StagingBundle, 11 | StagingImage, 12 | StagingThumbnail, 13 | PagesToImagesChore, 14 | ManageParseQRChore, 15 | KnownStagingImage, 16 | UnknownStagingImage, 17 | ExtraStagingImage, 18 | ErrorStagingImage, 19 | DiscardStagingImage, 20 | ) 21 | 22 | # This makes models appear in the admin interface 23 | admin.site.register(StagingBundle) 24 | admin.site.register(StagingImage) 25 | admin.site.register(StagingThumbnail) 26 | admin.site.register(KnownStagingImage) 27 | admin.site.register(ExtraStagingImage) 28 | admin.site.register(ErrorStagingImage) 29 | admin.site.register(DiscardStagingImage) 30 | admin.site.register(UnknownStagingImage) 31 | admin.site.register(PagesToImagesChore) 32 | admin.site.register(ManageParseQRChore) 33 | -------------------------------------------------------------------------------- /plom_server/templates/BuildPaperPDF/cannot_find_pdf.html: -------------------------------------------------------------------------------- 1 | 8 | {% extends "base/base.html" %} 9 | {% load static %} 10 | {% block title %} 11 | Something has gone wrong 12 | {% endblock title %} 13 | {% block page_heading %} 14 | Something has gone wrong 15 | {% endblock page_heading %} 16 | {% block main_content %} 17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | {% endblock main_content %} 27 | -------------------------------------------------------------------------------- /plom_ml/clustering/exceptions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Bryan Tanady 3 | """Exceptions for clustering module.""" 4 | from plom_ml.exceptions import PlomMLException 5 | 6 | 7 | class ClusteringException(PlomMLException): 8 | """A base exception for clustering related module.""" 9 | 10 | pass 11 | 12 | 13 | class NoThresholdFound(ClusteringException): 14 | """No distance threshold is found for clusterings.""" 15 | 16 | pass 17 | 18 | 19 | class PreprocessingException(ClusteringException): 20 | """Preprocessing steps related exception.""" 21 | 22 | pass 23 | 24 | 25 | class MissingRequiredInputKeys(PreprocessingException): 26 | """Input images are missing required keys required by the preprocessor.""" 27 | 28 | pass 29 | 30 | 31 | class EmbeddingExceptions(ClusteringException): 32 | """Embedding related exception.""" 33 | 34 | pass 35 | 36 | 37 | class MissingEmbedderException(EmbeddingExceptions): 38 | """Missing embedder that is required to generate feature vector.""" 39 | 40 | pass 41 | -------------------------------------------------------------------------------- /doc/source/code.rst: -------------------------------------------------------------------------------- 1 | .. Plom documentation 2 | Copyright (C) 2021-2023, 2025 Colin B. Macdonald 3 | Copyright (C) 2024 Aidan Murphy 4 | SPDX-License-Identifier: AGPL-3.0-or-later 5 | 6 | Python modules 7 | ============== 8 | 9 | Most Plom code can be found in one of two directories in the 10 | `project repository `_: 11 | 12 | * ``plom/`` 13 | * ``plom_server/`` 14 | 15 | The former is currently (2025 March) "in-flux" as the marking client has 16 | moved to a separate `Plom-Client repo `_ 17 | and the legacy server has been deprecated and subsequently removed. 18 | 19 | The ``plom_server`` module contains the "current" (non-legacy) Plom server. 20 | 21 | 22 | 23 | plom 24 | ---- 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | 29 | module-plom-create 30 | 31 | module-plom-scan 32 | 33 | module-plom-solutions 34 | 35 | module-plom-finish 36 | 37 | module-plom-other 38 | 39 | 40 | plom_server 41 | ----------- 42 | 43 | .. toctree:: 44 | :maxdepth: 2 45 | 46 | plom_server/modules.rst 47 | -------------------------------------------------------------------------------- /plom/finish/rubric_downloads.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2021 Andrew Rechnitzer 3 | # Copyright (C) 2022 Colin B. Macdonald 4 | 5 | import json 6 | 7 | from plom.finish import with_finish_messenger 8 | from plom.finish import RubricListFilename, TestRubricMatrixFilename 9 | 10 | 11 | @with_finish_messenger 12 | def download_rubric_files(*, msgr): 13 | """Download two files with information about rubrics. 14 | 15 | Keyword Args: 16 | msgr (plom.Messenger/tuple): either a connected Messenger or a 17 | tuple appropriate for credientials. 18 | """ 19 | counts = msgr.RgetRubricCounts() 20 | tr_matrix = msgr.RgetTestRubricMatrix() 21 | 22 | # counts is a dict indexed by key - turn it into a list 23 | # this makes it compatible with plom-create rubric upload 24 | rubric_list = [Y for X, Y in counts.items()] 25 | 26 | with open(RubricListFilename, "w") as fh: 27 | json.dump(rubric_list, fh, indent=" ") 28 | 29 | with open(TestRubricMatrixFilename, "w") as fh: 30 | json.dump(tr_matrix, fh) 31 | -------------------------------------------------------------------------------- /plom_server/templates/QuestionClustering/clustering_jobs.html: -------------------------------------------------------------------------------- 1 | 3 | {% extends "base/base.html" %} 4 | {% load static %} 5 | {% block title %} 6 | Question clustering jobs 7 | {% endblock title %} 8 | {% block page_heading %} 9 | Question clustering jobs 10 | {% endblock page_heading %} 11 | {% block main_content %} 12 | {% include "../base/alert_messages.html" with messages=messages %} 13 | 20 |
21 |
{% include "QuestionClustering/fragments/clustering_jobs_table.html" with tasks=tasks %}
22 |
23 | {% include "QuestionClustering/fragments/error_detail_modal.html" %} 24 | {% endblock main_content %} 25 | -------------------------------------------------------------------------------- /testTemplates/idBox2-source.tex: -------------------------------------------------------------------------------- 1 | % SPDX-License-Identifier: AGPL-3.0-or-later 2 | % Copyright (C) 2019 Andrew Rechnitzer 3 | % Copyright (C) 2023 Colin B. Macdonald 4 | 5 | \documentclass[12pt]{article} 6 | 7 | \usepackage{tikz} 8 | \usetikzlibrary{backgrounds} 9 | 10 | \begin{document} 11 | \thispagestyle{empty} 12 | \begin{center} 13 | \begin{tikzpicture}[scale=1.25, background rectangle/.style={line width=4pt, draw=black}, show background rectangle] 14 | 15 | \draw (0,1) rectangle (3,2) node[pos=0.5] {\large Signature}; 16 | \draw (3,1) rectangle (11,2); 17 | 18 | \draw (0,2) rectangle (3,4) node[pos=0.5] {\large Name}; 19 | \draw[loosely dotted] (3.2,3) -- (10.8,3); 20 | \draw (3,2) rectangle (11,4); 21 | 22 | \draw (0,4) rectangle (3,5) node[pos=0.5] {\large Section}; 23 | \foreach \x in {0,...,2} { 24 | \draw (\x+3,4) rectangle (\x+4,5); 25 | } 26 | 27 | \draw (0,5) rectangle (3,6) node[pos=0.5] {\large Student number}; 28 | \foreach \x in {0,...,7} { 29 | \draw (\x+3,5) rectangle (\x+4,6); 30 | } 31 | \end{tikzpicture} 32 | \end{center} 33 | 34 | \end{document} 35 | -------------------------------------------------------------------------------- /plom_server/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: FSFAP 2 | # Copyright (C) 2022 Brennen Chiu 3 | # Copyright (C) 2022-2023 Edith Coates 4 | # Copyright (C) 2022 Andrew Rechnitzer 5 | # Copyright (C) 2022-2023 Colin B. Macdonald 6 | # 7 | # Copying and distribution of this file, with or without modification, 8 | # are permitted in any medium without royalty provided the copyright 9 | # notice and this notice are preserved. This file is offered as-is, 10 | # without any warranty. 11 | 12 | # Byte-compiled / optimized / DLL files: 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # Environments: 18 | venv/ 19 | 20 | # Django stuff: 21 | *.log 22 | local_settings.py 23 | db.sqlite3 24 | db.sqlite3-journal 25 | 26 | # Code coverage: 27 | .coverage 28 | 29 | # emacs backup files 30 | *.bak 31 | 32 | # PyCharm 33 | .idea/* 34 | 35 | # Ignore PDF folders/zipfiles 36 | produced_papers.csv 37 | papersToPrint/* 38 | sourceVersions/* 39 | media/* 40 | plomdemo.zip 41 | 42 | # whitenoise static files (need to run python3 manage.py collectstatic) 43 | staticfiles/ 44 | 45 | # test fixtures 46 | fixtures/ 47 | -------------------------------------------------------------------------------- /plom_server/API/views/experimental/annotations.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | 4 | from .base import ManagerReadOnlyViewSet 5 | 6 | from plom_server.Mark.models import Annotation 7 | from plom_server.Mark.serializers import AnnotationSerializer 8 | 9 | 10 | class AnnotationViewSet(ManagerReadOnlyViewSet): 11 | """Endpoints for the Annotation model. Only safe methods are enabled. 12 | 13 | 'annotations/': 14 | GET: list all annotations (can be filtered) 15 | POST: create a new annotation (disabled) 16 | 17 | 'annotations/pk/': 18 | GET: retrieve annotation by primary key 19 | PUT: update annotation by primary key (disabled) 20 | PATCH: modify annotation by primary key (disabled) 21 | DELETE: delete annotation by primary key (disabled) 22 | """ 23 | 24 | queryset = Annotation.objects.all() 25 | serializer_class = AnnotationSerializer 26 | filterset_fields = ( 27 | "edition", 28 | "score", 29 | "marking_time", 30 | "task", 31 | "user", 32 | ) 33 | -------------------------------------------------------------------------------- /plom_server/Finish/services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Julian Lapenna 3 | # Copyright (C) 2023 Edith Coates 4 | # Copyright (C) 2023-2025 Andrew Rechnitzer 5 | # Copyright (C) 2024 Bryan Tanady 6 | # Copyright (C) 2024 Elisa Pan 7 | # Copyright (C) 2025 Aden Chan 8 | 9 | """Services of the Finish app of the Plom Server.""" 10 | 11 | from .student_marks_service import StudentMarkService 12 | from .ta_marking_service import TaMarkingService 13 | from .annotation_data_service import AnnotationDataService 14 | from .reassemble_service import ReassembleService 15 | from .data_extraction_service import DataExtractionService 16 | from .matplotlib_service import MatplotlibService, MinimalPlotService 17 | from .d3_service import D3Service 18 | 19 | from .build_soln_service import BuildSolutionService 20 | from .soln_images import SolnImageService 21 | from .soln_source import SolnSourceService 22 | from .template_soln_spec import TemplateSolnSpecService 23 | from .build_student_report_service import BuildStudentReportService 24 | from .ReportPDFService import GRAPH_DETAILS 25 | -------------------------------------------------------------------------------- /doc/source/macos_installation.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | Installing from source on MacOS 10 | =============================== 11 | 12 | Tested on Catalina 10.15.4, in mid 2020. 13 | First some stuff from a package manager, here using [Homebrew](https://brew.sh): 14 | 15 | ``` 16 | brew install cmake pango 17 | ``` 18 | You will also need Python, perhaps: 19 | ``` 20 | brew install python3 21 | ``` 22 | You will also need latex. Here is one approach: 23 | ``` 24 | brew install basictex 25 | eval "$(/usr/libexec/path_helper)" 26 | sudo tlmgr update --self 27 | sudo tlmgr install latexmk dvipng preview exam preprint 28 | ``` 29 | or maybe `brew install mactex-no-gui` or perhaps via some other UI means. 30 | 31 | At this point `pip install plom` (or `pip install --user .` from inside 32 | the Plom source tree) should pull in the remaining dependencies. 33 | -------------------------------------------------------------------------------- /plom_server/Base/tests_settings.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024-2025 Colin B. Macdonald 3 | 4 | from django.test import TestCase 5 | 6 | from .services import Settings 7 | 8 | 9 | class TestPapersPrintedSetting(TestCase): 10 | def test_settings_create_rubrics_tristates(self) -> None: 11 | Settings.set_who_can_create_rubrics("locked") 12 | Settings.set_who_can_create_rubrics("permissive") 13 | Settings.set_who_can_create_rubrics("per-user") 14 | 15 | def test_settings_modify_rubrics_tristates(self) -> None: 16 | Settings.set_who_can_modify_rubrics("locked") 17 | Settings.set_who_can_modify_rubrics("permissive") 18 | Settings.set_who_can_modify_rubrics("per-user") 19 | 20 | def test_settings_create_rubrics_some_other_state(self) -> None: 21 | with self.assertRaises(ValueError): 22 | Settings.set_who_can_create_rubrics("meh") 23 | 24 | def test_settings_modify_rubrics_some_other_state(self) -> None: 25 | with self.assertRaises(ValueError): 26 | Settings.set_who_can_modify_rubrics("foobar") 27 | -------------------------------------------------------------------------------- /testTemplates/idBox-source.tex: -------------------------------------------------------------------------------- 1 | % SPDX-License-Identifier: AGPL-3.0-or-later 2 | % Copyright (C) 2019 Andrew Rechnitzer 3 | % Copyright (C) 2023 Colin B. Macdonald 4 | 5 | \documentclass[12pt]{article} 6 | 7 | \usepackage{tikz} 8 | \usetikzlibrary{backgrounds} 9 | 10 | \begin{document} 11 | \thispagestyle{empty} 12 | \begin{center} 13 | \begin{tikzpicture}[scale=1.25, background rectangle/.style={line width=4pt, draw=black}, show background rectangle] 14 | \draw (0,1) rectangle (3,2) node[pos=0.5] {\large Family Name}; \draw (3,1) rectangle (11,2); 15 | \draw (0,2) rectangle (3,3) node[pos=0.5] {\large Given Name}; \draw (3,2) rectangle (11,3); 16 | \draw (0,3) rectangle (3,4) node[pos=0.5] {\large Preferred Name}; \draw (3,3) rectangle (11,4); 17 | 18 | \draw (0,4) rectangle (3,5) node[pos=0.5] {\large Section}; 19 | \foreach \x in {0,...,2} { 20 | \draw (\x+3,4) rectangle (\x+4,5); 21 | } 22 | 23 | \draw (0,5) rectangle (3,6) node[pos=0.5] {\large Student number}; 24 | \foreach \x in {0,...,7} { 25 | \draw (\x+3,5) rectangle (\x+4,6); 26 | } 27 | \end{tikzpicture} 28 | \end{center} 29 | 30 | \end{document} 31 | -------------------------------------------------------------------------------- /plom_server/QuestionTags/admin.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2024 Elisa Pan 3 | 4 | from django.contrib import admin 5 | from .models import TmpAbstractQuestion, PedagogyTag, QuestionTagLink 6 | 7 | 8 | class QuestionTagInline(admin.TabularInline): 9 | """Inline admin class to manage the relationship between questions and tags. 10 | 11 | This class allows the admin to manage QuestionTagLink objects directly 12 | from the TmpAbstractQuestion admin page. 13 | """ 14 | 15 | model = QuestionTagLink 16 | extra = 1 17 | 18 | 19 | @admin.register(TmpAbstractQuestion) 20 | class TmpAbstractQuestionAdmin(admin.ModelAdmin): 21 | """Admin class for TmpAbstractQuestion model. 22 | 23 | This class customizes the admin interface for TmpAbstractQuestion objects, 24 | including inline editing of related QuestionTagLink objects and displaying 25 | the question_index field in the list view. 26 | """ 27 | 28 | inlines = [QuestionTagInline] 29 | list_display = ["question_index"] 30 | 31 | 32 | admin.site.register(PedagogyTag) 33 | admin.site.register(QuestionTagLink) 34 | -------------------------------------------------------------------------------- /plom_server/Progress/views/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2022 Edith Coates 3 | # Copyright (C) 2022-2023 Brennen Chiu 4 | # Copyright (C) 2023-2024 Andrew Rechnitzer 5 | # Copyright (C) 2024 Colin B. Macdonald 6 | 7 | from .overview_landing import OverviewLandingView, ToolsLandingView 8 | 9 | from .progress_identify import ( 10 | ProgressIdentifyHome, 11 | IDImageView, 12 | ClearID, 13 | IDImageWrapView, 14 | ) 15 | 16 | from .progress_mark import ( 17 | ProgressMarkHome, 18 | ProgressMarkStatsView, 19 | ProgressMarkDetailsView, 20 | ProgressMarkVersionCompareView, 21 | ProgressMarkStartMarking, 22 | ) 23 | 24 | from .progress_task_annot import ( 25 | ProgressMarkingTaskFilterView, 26 | ProgressMarkingTaskDetailsView, 27 | ProgressNewestMarkingTaskDetailsView, 28 | AnnotationImageWrapView, 29 | AnnotationImageView, 30 | OriginalImageWrapView, 31 | AllTaskOverviewView, 32 | MarkingTaskTagView, 33 | MarkingTaskResetView, 34 | MarkingTaskReassignView, 35 | ) 36 | 37 | from .progress_userinfo import ( 38 | ProgressUserInfoHome, 39 | ) 40 | -------------------------------------------------------------------------------- /plom/test_latex.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2020-2025 Colin B. Macdonald 3 | 4 | import tempfile 5 | from importlib import resources 6 | 7 | import plom 8 | 9 | from plom.textools import buildLaTeX 10 | 11 | 12 | def test_latex_exam_template() -> None: 13 | content = (resources.files(plom) / "latexTemplate.tex").read_text() 14 | with tempfile.NamedTemporaryFile() as f: 15 | r, out = buildLaTeX(content, f) 16 | assert r == 0 17 | 18 | 19 | def test_latex_exam_templatev2() -> None: 20 | content = (resources.files(plom) / "latexTemplatev2.tex").read_text() 21 | with tempfile.NamedTemporaryFile() as f: 22 | r, out = buildLaTeX(content, f) 23 | assert r == 0 24 | 25 | 26 | def test_latex_fails_and_makes_useful_output() -> None: 27 | content = r"""\documentclass{article} 28 | \begin{document} 29 | \InvalidCommand 30 | \end{document} 31 | """ 32 | with tempfile.NamedTemporaryFile() as f: 33 | r, out = buildLaTeX(content, f) 34 | assert r != 0 35 | assert r"\InvalidCommand" in out 36 | assert "Undefined" in out 37 | -------------------------------------------------------------------------------- /testTemplates/idBox4-source.tex: -------------------------------------------------------------------------------- 1 | % SPDX-License-Identifier: AGPL-3.0-or-later 2 | % Copyright (C) 2019 Andrew Rechnitzer 3 | % Copyright (C) 2023 Colin B. Macdonald 4 | 5 | \documentclass[12pt]{article} 6 | 7 | \usepackage{tikz} 8 | \usetikzlibrary{backgrounds} 9 | 10 | \begin{document} 11 | \thispagestyle{empty} 12 | \begin{center} 13 | \begin{tikzpicture}[scale=1.25, background rectangle/.style={line width=4pt, draw=black}, show background rectangle] 14 | 15 | \draw (0,1) rectangle (3,2.5) node[pos=0.5] {\large Name}; 16 | \draw[loosely dotted] (3.2,1.3) -- (10.8,1.3); 17 | \draw (3,1) rectangle (11,2.5); 18 | 19 | \draw (0,2.5) rectangle (3,4) node[pos=0.5] {\large Signature}; 20 | \draw[loosely dotted] (3.2,2.8) -- (10.8,2.8); 21 | \draw (3,2.5) rectangle (11,4); 22 | 23 | \draw (0,4) rectangle (3,5) node[pos=0.5] {\large Section}; 24 | \foreach \x in {0,...,2} { 25 | \draw (\x+3,4) rectangle (\x+4,5); 26 | } 27 | 28 | \draw (0,5) rectangle (3,6) node[pos=0.5] {\large Student number}; 29 | \foreach \x in {0,...,7} { 30 | \draw (\x+3,5) rectangle (\x+4,6); 31 | } 32 | \end{tikzpicture} 33 | \end{center} 34 | 35 | \end{document} 36 | -------------------------------------------------------------------------------- /plom/scripts/test_script_help_ver.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020, 2025 Colin B. Macdonald 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | import importlib.metadata 5 | import subprocess 6 | 7 | 8 | from plom.common import __version__ 9 | 10 | 11 | def find_my_console_scripts(package_name): 12 | # Note I think this gets what is installed rather than the dev-tree 13 | entrypoints = ( 14 | ep.name 15 | for ep in importlib.metadata.entry_points(group="console_scripts") 16 | if ep.name.startswith(package_name) and not ep.name.startswith("plom-client") 17 | ) 18 | return entrypoints 19 | 20 | 21 | scripts = list(find_my_console_scripts("plom")) 22 | 23 | 24 | def test_scripts_have_hyphen_version(): 25 | for s in scripts: 26 | assert __version__ in subprocess.check_output([s, "--version"]).decode() 27 | 28 | 29 | def test_scripts_have_hyphen_help(): 30 | for s in scripts: 31 | subprocess.check_call([s, "--help"]) 32 | subprocess.check_call([s, "-h"]) 33 | 34 | 35 | def test_scripts_nonsense_cmdline(): 36 | for s in scripts: 37 | assert subprocess.call([s, "--TheCatHasAlreadyBeenFed"]) != 0 38 | -------------------------------------------------------------------------------- /plom/solutions/getSolutionImage.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2021 Andrew Rechnitzer 3 | # Copyright (C) 2021-2024 Colin B. Macdonald 4 | 5 | from plom.solutions import with_manager_messenger 6 | 7 | 8 | @with_manager_messenger 9 | def getSolutionImage(question, version, *, msgr): 10 | """Get a solution image from the server. 11 | 12 | Args: 13 | question (int): which question. 14 | version (int): which version. 15 | 16 | Keyword Args: 17 | msgr (plom.Messenger/tuple): either a connected Messenger or a 18 | tuple appropriate for credientials. 19 | 20 | Returns: 21 | bytes: the bitmap of the solution or None if there was no 22 | solution. If you wish to know what sort of image it is, 23 | see recent changes to `get_annotations_image` which could 24 | expose this. 25 | 26 | Raises: 27 | PlomNoSolutionException: the question/version asked for does 28 | not have a solution image on the server. This is also 29 | returned if the values are out of range. 30 | """ 31 | return msgr.getSolutionImage(question, version) 32 | -------------------------------------------------------------------------------- /plom_server/Mark/serializers/tasks.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2023 Edith Coates 3 | # Copyright (C) 2024 Colin B. Macdonald 4 | 5 | from rest_framework.serializers import ( 6 | ModelSerializer, 7 | StringRelatedField, 8 | SerializerMethodField, 9 | HyperlinkedRelatedField, 10 | ) 11 | from ..models import MarkingTask 12 | 13 | 14 | class MarkingTaskSerializer(ModelSerializer): 15 | assigned_user = StringRelatedField() 16 | status = SerializerMethodField() 17 | # some nonsense to avoid pretty printing using Paper.str 18 | # paper = serializers.SlugRelatedField(slug_field="paper_number", queryset=TODO.sth.sth) 19 | tags = SerializerMethodField() 20 | # TODO: Issue #3521: potentially broken URLs, anyone using this? 21 | latest_annotation = HyperlinkedRelatedField("annotations-detail", read_only=True) 22 | 23 | class Meta: 24 | model = MarkingTask 25 | fields = "__all__" 26 | 27 | def get_tags(self, obj): 28 | return [str(tag) for tag in obj.markingtasktag_set.all()] 29 | 30 | def get_status(self, obj): 31 | return obj.StatusChoices.choices[obj.status - 1][1] 32 | -------------------------------------------------------------------------------- /contrib/onlinedist_05b_rename_papers_spares.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | # Copyright (C) 2020-2021, 2024 Colin B. Macdonald 5 | 6 | """Rename files for online distribution: just the spares. 7 | 8 | This was original written to do all the files but if there are multiple 9 | room than we needed more work. 10 | 11 | 1. Make the files in Plom first 12 | 2. Run the 01/02 script to make randomized filenames 13 | 3. Run this. 14 | """ 15 | 16 | from pathlib import Path 17 | import shutil 18 | import pandas as pd 19 | 20 | where_csv = Path(".") 21 | in_csv = where_csv / "random_codes.csv" 22 | where_pdf = Path("papersToPrint") 23 | dist_pdf = Path("distribute") 24 | 25 | 26 | def rename_and_move_spares(r): 27 | """Rename files based on info from each row of the spreadsheet.""" 28 | if not pd.isnull(r["sID"]): 29 | return 30 | f = where_pdf / "exam_{:04d}.pdf".format(int(r["test_number"])) 31 | out = dist_pdf / "spare" / r["test_filename"] 32 | print("{} -> {}".format(f, out)) 33 | shutil.copy2(f, out) 34 | 35 | 36 | df = pd.read_csv(in_csv, dtype="object") 37 | 38 | df.apply(rename_and_move_spares, axis=1) 39 | -------------------------------------------------------------------------------- /plom_server/API/tests/test_client_reject_list.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2025 Colin B. Macdonald 3 | 4 | from packaging.version import Version 5 | 6 | from django.test import TestCase 7 | from ..views.server_info import _client_reject_list 8 | 9 | 10 | class TestsClientRejectList(TestCase): 11 | 12 | def test_reject_list_key_validity(self) -> None: 13 | lst = _client_reject_list() 14 | for entry in lst: 15 | for x in entry.keys(): 16 | assert isinstance(x, str) 17 | assert x in ("client-id", "version", "operator", "reason", "action") 18 | 19 | def test_reject_list_action_operator_validity(self) -> None: 20 | lst = _client_reject_list() 21 | for entry in lst: 22 | act = entry.get("action") 23 | assert act in (None, "warn", "block") 24 | op = entry.get("operator") 25 | assert op in (None, "==", "<=", ">=", "<", ">") 26 | 27 | def test_reject_list_version_parses(self) -> None: 28 | lst = _client_reject_list() 29 | for entry in lst: 30 | ver = entry["version"] 31 | Version(ver) 32 | -------------------------------------------------------------------------------- /plom/solutions/deleteSolutionImage.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-or-later 2 | # Copyright (C) 2021 Andrew Rechnitzer 3 | # Copyright (C) 2022, 2024 Colin B. Macdonald 4 | 5 | from plom.solutions import with_manager_messenger 6 | from plom.plom_exceptions import PlomNoSolutionException 7 | 8 | 9 | @with_manager_messenger 10 | def deleteSolutionImage(question, version, *, msgr): 11 | """Delete one of the solution images on the server. 12 | 13 | Args: 14 | question (int): which question. 15 | version (int): which version. 16 | 17 | Keyword Args: 18 | msgr (plom.Messenger/tuple): either a connected Messenger or a 19 | tuple appropriate for credientials. 20 | 21 | Returns: 22 | None 23 | 24 | Raises: 25 | PlomNoSolutionException: the question/version asked for does 26 | not have a solution image on the server. This is also 27 | raised if the values are out of range. 28 | """ 29 | if msgr.deleteSolutionImage(question, version): 30 | return 31 | raise PlomNoSolutionException( 32 | f"Server has no solution to question {question} version {version} to remove" 33 | ) 34 | --------------------------------------------------------------------------------