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 |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("", 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 |
18 | Go to assessment preparation page
19 |
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 |
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 |
21 | Go to assessment preparation page
22 |
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 |
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("