From bc46df0eb4bc685cbc2bc17393267976c1411b2b Mon Sep 17 00:00:00 2001 From: Arseniks <48888367+Arseniks@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:54:14 +0300 Subject: [PATCH] =?UTF-8?q?feature:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=D0=B0=D1=85=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding pagination on page with students submissions and test for it. Updating README for MAC OS * Refactoring issues * Refactoring naming * Improving pagination on teacher submissions page. Adding tests. --------- Co-authored-by: Arseniy Korotkov --- .gitignore | 3 + poetry.lock | 68 +++++----------------- pyproject.toml | 1 + tests/database/test_check.py | 46 +++++++++++++++ webapp/managers.py | 17 +++++- webapp/repositories.py | 19 ++++++ webapp/templates/student/submissions.jinja | 16 +---- webapp/templates/teacher/submissions.jinja | 16 +---- webapp/views/student.py | 19 +++++- webapp/views/teacher.py | 28 ++++++++- 10 files changed, 148 insertions(+), 85 deletions(-) create mode 100644 tests/database/test_check.py diff --git a/.gitignore b/.gitignore index bb82ca6..fbcfe2f 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Mac/OSX +.DS_Store diff --git a/poetry.lock b/poetry.lock index 81bc289..a0acde5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" version = "1.7.5" description = "A database migration tool for SQLAlchemy." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -23,7 +22,6 @@ tz = ["python-dateutil"] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -35,7 +33,6 @@ files = [ name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -53,7 +50,6 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy" name = "authlib" version = "1.2.0" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -category = "main" optional = false python-versions = "*" files = [ @@ -68,7 +64,6 @@ cryptography = ">=3.2" name = "bcrypt" version = "3.2.2" description = "Modern password hashing for your software and your servers" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -96,7 +91,6 @@ typecheck = ["mypy"] name = "beautifulsoup4" version = "4.10.0" description = "Screen-scraping library" -category = "dev" optional = false python-versions = ">3.0.0" files = [ @@ -115,7 +109,6 @@ lxml = ["lxml"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -127,7 +120,6 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -204,7 +196,6 @@ pycparser = "*" name = "charset-normalizer" version = "3.0.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = "*" files = [ @@ -302,7 +293,6 @@ files = [ name = "click" version = "8.0.4" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -317,7 +307,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -329,7 +318,6 @@ files = [ name = "coverage" version = "6.4.3" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -386,7 +374,6 @@ toml = ["tomli"] name = "cryptography" version = "39.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -432,7 +419,6 @@ tox = ["tox"] name = "flask" version = "2.0.2" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -454,7 +440,6 @@ dotenv = ["python-dotenv"] name = "flask-jwt-extended" version = "4.3.1" description = "Extended JWT integration with Flask" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -470,11 +455,24 @@ Werkzeug = ">=0.14" [package.extras] asymmetric-crypto = ["cryptography (>=35.0.0)"] +[[package]] +name = "flask-paginate" +version = "2023.10.24" +description = "Simple paginate support for flask" +optional = false +python-versions = "*" +files = [ + {file = "flask-paginate-2023.10.24.tar.gz", hash = "sha256:57790fd4c543c802511ade71b6a9d9654e7295fcde39eda00d3cd4aa3bd68859"}, + {file = "flask_paginate-2023.10.24-py2.py3-none-any.whl", hash = "sha256:48cc477fd64c95b6c7be980bc96198a8eadeca863484f78c1e1fe22608fc3b28"}, +] + +[package.dependencies] +Flask = "*" + [[package]] name = "flask-wtf" version = "1.0.0" description = "Form rendering, validation, and CSRF protection for Flask with WTForms." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -494,7 +492,6 @@ email = ["email-validator"] name = "greenlet" version = "1.1.2" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -562,7 +559,6 @@ docs = ["Sphinx"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -574,7 +570,6 @@ files = [ name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" files = [ @@ -586,7 +581,6 @@ files = [ name = "isort" version = "5.9.3" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.6.1,<4.0" files = [ @@ -604,7 +598,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "itsdangerous" version = "2.1.1" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -616,7 +609,6 @@ files = [ name = "jinja2" version = "3.0.3" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -634,7 +626,6 @@ i18n = ["Babel (>=2.7)"] name = "joblib" version = "1.1.0" description = "Lightweight pipelining with Python functions" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -646,7 +637,6 @@ files = [ name = "mako" version = "1.2.0" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -666,7 +656,6 @@ testing = ["pytest"] name = "mando" version = "0.6.4" description = "Create Python CLI apps with little to no effort at all!" -category = "main" optional = false python-versions = "*" files = [ @@ -684,7 +673,6 @@ restructuredtext = ["rst2ansi"] name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -734,7 +722,6 @@ files = [ name = "numpy" version = "1.23.1" description = "NumPy is the fundamental package for array computing with Python." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -766,7 +753,6 @@ files = [ name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -781,7 +767,6 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -797,7 +782,6 @@ testing = ["pytest", "pytest-benchmark"] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -809,7 +793,6 @@ files = [ name = "pycodestyle" version = "2.8.0" description = "Python style guide checker" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -821,7 +804,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -833,7 +815,6 @@ files = [ name = "pyjwt" version = "2.4.0" description = "JSON Web Token implementation in Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -851,7 +832,6 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pyparsing" version = "3.0.7" description = "Python parsing module" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -866,7 +846,6 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.0.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -891,7 +870,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-alembic" version = "0.7.0" description = "A pytest plugin for verifying alembic migrations." -category = "dev" optional = false python-versions = ">=3.6,<4" files = [ @@ -908,7 +886,6 @@ sqlalchemy = "*" name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -927,7 +904,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "radon" version = "6.0.1" description = "Code Metrics in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -946,7 +922,6 @@ toml = ["tomli (>=2.0.1)"] name = "requests" version = "2.28.2" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7, <4" files = [ @@ -968,7 +943,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "scikit-learn" version = "1.1.1" description = "A set of python modules for machine learning and data mining" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1008,7 +982,6 @@ tests = ["black (>=22.3.0)", "flake8 (>=3.8.2)", "matplotlib (>=3.1.2)", "mypy ( name = "scipy" version = "1.8.1" description = "SciPy: Scientific Library for Python" -category = "main" optional = false python-versions = ">=3.8,<3.11" files = [ @@ -1044,7 +1017,6 @@ numpy = ">=1.17.3,<1.25.0" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1056,7 +1028,6 @@ files = [ name = "soupsieve" version = "2.3.1" description = "A modern CSS selector implementation for Beautiful Soup." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1068,7 +1039,6 @@ files = [ name = "sqlalchemy" version = "1.4.29" description = "Database Abstraction Library" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1138,7 +1108,6 @@ sqlcipher = ["sqlcipher3-binary"] name = "threadpoolctl" version = "3.1.0" description = "threadpoolctl" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1150,7 +1119,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1162,7 +1130,6 @@ files = [ name = "urllib3" version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1179,7 +1146,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "waitress" version = "2.0.0" description = "Waitress WSGI server" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -1195,7 +1161,6 @@ testing = ["coverage (>=5.0)", "pytest", "pytest-cover"] name = "werkzeug" version = "2.0.3" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1210,7 +1175,6 @@ watchdog = ["watchdog"] name = "wtforms" version = "3.0.1" description = "Form validation and rendering for Python web development." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1227,4 +1191,4 @@ email = ["email-validator"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "84f80045b69a44fa94dee5786d17730446766493a27de38d6d3aa195defdb891" +content-hash = "08b50ed047f0995eb93fd00b69cc6c03612f2682553a3a4fa6cc7f42c49c21ee" diff --git a/pyproject.toml b/pyproject.toml index 78944ca..497bf56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ Flask-JWT-Extended = "4.3.1" bcrypt = "3.2.2" authlib = "1.2.0" requests = "2.28.2" +flask-paginate = "2023.10.24" [tool.poetry.dev-dependencies] beautifulsoup4 = "4.10.0" diff --git a/tests/database/test_check.py b/tests/database/test_check.py new file mode 100644 index 0000000..bf9c148 --- /dev/null +++ b/tests/database/test_check.py @@ -0,0 +1,46 @@ +from tests.utils import arrange_task, unique_str + +from webapp.models import Student, TaskStatus +from webapp.repositories import AppDatabase + + +def test_count_student_submissions(db: AppDatabase): + group_id, variant_id, task_id = arrange_task(db) + + student_email = f"{unique_str()}@test.ru" + + student = db.students.create(student_email, unique_str()) + db.students.confirm(student_email) + db.students.update_group(db.students.find_by_email(student_email), group_id) + + code = "main = lambda: 42" + ip = "0.0.0.0" + + message = db.messages.submit_task(task_id, variant_id, group_id, code, ip, student.id) + stat = db.statuses.submit_task(task_id, variant_id, group_id, code, ip) + db.checks.record_check(message.id, stat.status, "test") + + result = db.checks.count_student_submissions(student) + + assert result == 1 + + +def test_count_submissions_by_info(db: AppDatabase): + group_id, variant_id, task_id = arrange_task(db) + + student_email = f"{unique_str()}@test.ru" + + student = db.students.create(student_email, unique_str()) + db.students.confirm(student_email) + db.students.update_group(db.students.find_by_email(student_email), group_id) + + code = "main = lambda: 42" + ip = "0.0.0.0" + + message = db.messages.submit_task(task_id, variant_id, group_id, code, ip, student.id) + stat = db.statuses.submit_task(task_id, variant_id, group_id, code, ip) + db.checks.record_check(message.id, stat.status, "test") + + result = db.checks.count_submissions_by_info(group_id, variant_id, task_id, False) + + assert result == 1 diff --git a/webapp/managers.py b/webapp/managers.py index 28245b8..0848cac 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -229,7 +229,14 @@ def get_task_status(self, gid: int, vid: int, tid: int) -> TaskStatusDto: achievements = self.__get_task_achievements(tid) return self.__get_task_status_dto(gid, vid, tid, status, achievements) - def get_submissions_statuses_by_info(self, group: int, variant: int, task: int, skip: int, take: int): + def get_submissions_statuses_by_info( + self, + group: int, + variant: int, + task: int, + skip: int, + take: int, + ) -> list[SubmissionDto]: registration = self.config.config.enable_registration submissions = [] checks = self.checks.get_by_task(group, variant, task, skip, take, registration) @@ -237,6 +244,11 @@ def get_submissions_statuses_by_info(self, group: int, variant: int, task: int, submissions.append(self.__get_submissions(check, message, student)) return submissions + def count_submissions_by_info(self, group: int, variant: int, task: int) -> int: + registration = self.config.config.enable_registration + + return self.checks.count_submissions_by_info(group, variant, task, registration) + def get_submissions_statuses(self, student: Student, skip: int, take: int) -> list[SubmissionDto]: checks_and_messages: list[tuple[MessageCheck, Message]] = self.checks.get_by_student(student, skip, take) submissions = [] @@ -244,6 +256,9 @@ def get_submissions_statuses(self, student: Student, skip: int, take: int) -> li submissions.append(self.__get_submissions(check, message, None)) return submissions + def count_student_submissions(self, student: Student) -> int: + return self.checks.count_student_submissions(student) + def __get_submissions( self, check: MessageCheck, diff --git a/webapp/repositories.py b/webapp/repositories.py index 19c42ff..f2817f3 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -348,6 +348,13 @@ def get_by_student(self, student: Student, skip: int, take: int) -> list[tuple[M .limit(take) \ .all() + def count_student_submissions(self, student: Student) -> int: + with self.db.create_session() as session: + return session.query(MessageCheck, Message) \ + .join(Message, Message.id == MessageCheck.message) \ + .filter(Message.student == student.id) \ + .count() + def get_by_task(self, group: int, variant: int, task: int, skip: int, take: int, registration: bool): with self.db.create_session() as session: query = session \ @@ -362,6 +369,18 @@ def get_by_task(self, group: int, variant: int, task: int, skip: int, take: int, .limit(take) \ .all() + def count_submissions_by_info(self, group: int, variant: int, task: int, registration: bool): + with self.db.create_session() as session: + query = session \ + .query(MessageCheck, Message, Student if registration else null()) \ + .join(Message, Message.id == MessageCheck.message) + if registration: + query = query.join(Student, Student.id == Message.student) + return query \ + .filter(Message.group == group, Message.variant == variant, Message.task == task) \ + .order_by(desc(Message.time)) \ + .count() + def record_achievement(self, check: int, achievement: int): with self.db.create_session() as session: session.query(MessageCheck) \ diff --git a/webapp/templates/student/submissions.jinja b/webapp/templates/student/submissions.jinja index 32e440c..4f5dd88 100644 --- a/webapp/templates/student/submissions.jinja +++ b/webapp/templates/student/submissions.jinja @@ -57,20 +57,8 @@ style="font-family: monospace;">{{ submission.code }} {% endfor %} - + + {{ pagination.links }} {% else %} Список отправленных решений пуст {% endif %} diff --git a/webapp/templates/teacher/submissions.jinja b/webapp/templates/teacher/submissions.jinja index f996968..c4d2555 100644 --- a/webapp/templates/teacher/submissions.jinja +++ b/webapp/templates/teacher/submissions.jinja @@ -70,20 +70,8 @@ style="font-family: monospace;">{{ submission.code }} {% endfor %} - + + {{ pagination.links }} {% else %} Список отправленных решений пуст {% endif %} diff --git a/webapp/views/student.py b/webapp/views/student.py index d9d11e9..9fc25d4 100644 --- a/webapp/views/student.py +++ b/webapp/views/student.py @@ -1,6 +1,7 @@ from authlib.integrations.requests_client import OAuth2Auth from flask_jwt_extended import create_access_token, set_access_cookies, unset_jwt_cookies from flask_jwt_extended.exceptions import JWTExtendedException +from flask_paginate import Pagination from jwt.exceptions import PyJWTError from flask import Blueprint, abort @@ -46,9 +47,24 @@ def submissions(student: Student | None, page: int): if student is None: abort(401) size = 5 - submissions_statuses = statuses.get_submissions_statuses(student, page * size, size) + submissions_statuses = statuses.get_submissions_statuses(student, (page - 1) * size, size) if not submissions_statuses and page > 0: return redirect(f"/submissions/{page - 1}") + + submissions_count = statuses.count_student_submissions(student) + + pagination = Pagination( + page=page, + per_page=size, + total=submissions_count, + search=False, + prev_label="<", + next_label=">", + inner_window=2, + outer_window=0, + css_framework="bootstrap5", + ) + return render_template( "student/submissions.jinja", submissions=submissions_statuses, @@ -57,6 +73,7 @@ def submissions(student: Student | None, page: int): student=student, highlight=config.config.highlight_syntax, page=page, + pagination=pagination, ) diff --git a/webapp/views/teacher.py b/webapp/views/teacher.py index 687d867..621c6f0 100644 --- a/webapp/views/teacher.py +++ b/webapp/views/teacher.py @@ -1,5 +1,6 @@ from flask_jwt_extended import create_access_token, set_access_cookies, unset_jwt_cookies, verify_jwt_in_request from flask_jwt_extended.exceptions import JWTExtendedException +from flask_paginate import Pagination from jwt.exceptions import PyJWTError from flask import Blueprint @@ -28,11 +29,32 @@ @teacher_jwt_required(db.teachers) def teacher_submissions(teacher: Teacher, gid: int, vid: int, tid: int, page: int): size = 5 - submissions_statuses = statuses.get_submissions_statuses_by_info(gid, vid, tid, page * size, size) + submissions_statuses = statuses.get_submissions_statuses_by_info(gid, vid, tid, (page - 1) * size, size) if not submissions_statuses and page > 0: return redirect(f"/teacher/submissions/group/{gid}/variant/{vid}/task/{tid}/{page - 1}") - return render_template("teacher/submissions.jinja", submissions=submissions_statuses, - highlight=config.config.highlight_syntax, page=page, info=(gid, vid, tid)) + + submissions_count = statuses.count_submissions_by_info(gid, vid, tid) + + pagination = Pagination( + page=page, + per_page=size, + total=submissions_count, + search=False, + prev_label="<", + next_label=">", + inner_window=2, + outer_window=0, + css_framework="bootstrap5", + ) + + return render_template( + "teacher/submissions.jinja", + submissions=submissions_statuses, + highlight=config.config.highlight_syntax, + page=page, + info=(gid, vid, tid), + pagination=pagination, + ) @blueprint.route("/teacher/submissions", methods=["GET"])