diff --git a/tests/database/test_check.py b/tests/database/test_check.py index bf9c148..176e679 100644 --- a/tests/database/test_check.py +++ b/tests/database/test_check.py @@ -1,9 +1,34 @@ +from secrets import token_hex + from tests.utils import arrange_task, unique_str -from webapp.models import Student, TaskStatus from webapp.repositories import AppDatabase +def test_get_by_student(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.get_by_student(student, 0, 10) + + assert len(result) == 1 + assert result[0][1].code == code + assert result[0][1].ip == ip + assert result[0][0].output == "test" + + def test_count_student_submissions(db: AppDatabase): group_id, variant_id, task_id = arrange_task(db) @@ -44,3 +69,38 @@ def test_count_submissions_by_info(db: AppDatabase): result = db.checks.count_submissions_by_info(group_id, variant_id, task_id, False) assert result == 1 + + +def test_count_session_id_submissions(db: AppDatabase): + group_id, variant_id, task_id = arrange_task(db) + + session_id = token_hex(16) + + code = "main = lambda: 42" + ip = "0.0.0.0" + + message = db.messages.submit_task(task_id, variant_id, group_id, code, ip, None, session_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_session_id_submissions(session_id=session_id) + + assert result == 1 + + +def test_get_by_session_id(db: AppDatabase): + group_id, variant_id, task_id = arrange_task(db) + + session_id = token_hex(16) + + code = "main = lambda: 42" + ip = "0.0.0.0" + + message = db.messages.submit_task(task_id, variant_id, group_id, code, ip, None, session_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.get_by_session_id(session_id=session_id, skip=0, take=10) + + assert len(result) == 1 + assert result[0][1].session_id == session_id diff --git a/tests/ui/test_submissions.py b/tests/ui/test_submissions.py new file mode 100644 index 0000000..f09d17f --- /dev/null +++ b/tests/ui/test_submissions.py @@ -0,0 +1,68 @@ +from secrets import token_hex + +from tests.database.test_check import arrange_task +from tests.utils import mode, unique_str + +from flask.testing import FlaskClient + +from webapp.repositories import AppDatabase + + +@mode("registration") +def test_unauthorized_submissions(db: AppDatabase, client: FlaskClient): + response = client.get('/submissions') + html = response.get_data(as_text=True) + + assert "401 Unauthorized" in html + + +@mode("exam") +def test_empty_submissions(db: AppDatabase, client: FlaskClient): + session_id = token_hex(16) + client.set_cookie("localhost", "anonymous_identifier", session_id) + response = client.get('/submissions') + html = response.get_data(as_text=True) + + assert response.status_code == 200 + assert "Список отправленных решений пуст" in html + + +@mode("exam") +def test_exam_anonymous_submissions(db: AppDatabase, client: FlaskClient): + group_id, variant_id, task_id = arrange_task(db) + + session_id = token_hex(16) + + code = "main = lambda: 42" + unique_str() + ip = "0.0.0.0" + + message = db.messages.submit_task(task_id, variant_id, group_id, code, ip, None, session_id) + stat = db.statuses.submit_task(task_id, variant_id, group_id, code, ip) + db.checks.record_check(message.id, stat.status, "test_exam_anonymous_submissions") + + client.set_cookie("localhost", "anonymous_identifier", session_id) + response = client.get('/submissions') + html = response.get_data(as_text=True) + + assert response.status_code == 200 + assert code in html + + +def test_no_reg_anonymous_submissions(db: AppDatabase, client: FlaskClient): + group_id, variant_id, task_id = arrange_task(db) + + session_id = token_hex(16) + + code = "main = lambda: 42" + unique_str() + ip = "0.0.0.0" + + message = db.messages.submit_task(task_id, variant_id, group_id, code, ip, None, session_id) + stat = db.statuses.submit_task(task_id, variant_id, group_id, code, ip) + db.checks.record_check(message.id, stat.status, "test_no_reg_anonymous_submissions") + + client.set_cookie("localhost", "anonymous_identifier", session_id) + response = client.get('/submissions') + html = response.get_data(as_text=True) + + assert response.status_code == 200 + assert code in html diff --git a/webapp/alembic/versions/20240229.01-04.add_session_id.py b/webapp/alembic/versions/20240229.01-04.add_session_id.py new file mode 100644 index 0000000..8715969 --- /dev/null +++ b/webapp/alembic/versions/20240229.01-04.add_session_id.py @@ -0,0 +1,24 @@ +""" + +Revision ID: 30b579748cec +Revises: eeff291eb0fb +Create Date: 2024-02-29 01:04:59.268423 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '30b579748cec' +down_revision = 'eeff291eb0fb' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("messages", sa.Column("session_id", sa.String, nullable=True)) + + +def downgrade(): + op.drop_column("messages", "session_id") diff --git a/webapp/managers.py b/webapp/managers.py index 0848cac..eb6cf5d 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -256,9 +256,19 @@ def get_submissions_statuses(self, student: Student, skip: int, take: int) -> li submissions.append(self.__get_submissions(check, message, None)) return submissions + def get_anonymous_submissions_statuses(self, session_id: str, skip: int, take: int) -> list[SubmissionDto]: + checks_and_messages: list[tuple[MessageCheck, Message]] = self.checks.get_by_session_id(session_id, skip, take) + submissions = [] + for check, message in checks_and_messages: + 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 count_session_id_submissions(self, session_id: str) -> int: + return self.checks.count_session_id_submissions(session_id) + def __get_submissions( self, check: MessageCheck, diff --git a/webapp/models.py b/webapp/models.py index 86241a3..01dbe58 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -100,6 +100,7 @@ class Message(Base): time = sa.Column("time", sa.DateTime, nullable=False) code = sa.Column("code", sa.String, nullable=False) ip = sa.Column("ip", sa.String, nullable=False) + session_id = sa.Column("session_id", sa.String, nullable=True) processed = sa.Column("processed", sa.Boolean, nullable=False) student = sa.Column("student", sa.Integer, sa.ForeignKey("students.id"), nullable=True) diff --git a/webapp/repositories.py b/webapp/repositories.py index f2817f3..d6c94b6 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -255,6 +255,7 @@ def submit_task( code: str, ip: str, student: int | None, + session_id: str | None = None ) -> Message: with self.db.create_session() as session: message = Message( @@ -266,6 +267,7 @@ def submit_task( code=code, ip=ip, student=student, + session_id=session_id ) session.add(message) return message @@ -348,6 +350,16 @@ def get_by_student(self, student: Student, skip: int, take: int) -> list[tuple[M .limit(take) \ .all() + def get_by_session_id(self, session_id: str, skip: int, take: int) -> list[tuple[MessageCheck, Message]]: + with self.db.create_session() as session: + return session.query(MessageCheck, Message) \ + .join(Message, Message.id == MessageCheck.message) \ + .filter(Message.session_id == session_id) \ + .order_by(desc(Message.time)) \ + .offset(skip) \ + .limit(take) \ + .all() + def count_student_submissions(self, student: Student) -> int: with self.db.create_session() as session: return session.query(MessageCheck, Message) \ @@ -355,6 +367,13 @@ def count_student_submissions(self, student: Student) -> int: .filter(Message.student == student.id) \ .count() + def count_session_id_submissions(self, session_id: str) -> int: + with self.db.create_session() as session: + return session.query(MessageCheck, Message) \ + .join(Message, Message.id == MessageCheck.message) \ + .filter(Message.session_id == session_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 \ diff --git a/webapp/templates/student/student_layout.jinja b/webapp/templates/student/student_layout.jinja index 96ca7f9..14b7def 100644 --- a/webapp/templates/student/student_layout.jinja +++ b/webapp/templates/student/student_layout.jinja @@ -48,6 +48,9 @@
  • Войти
  • {% endif %} {% endif %} + {% if exam or not registration %} +
  • Отправленные решения
  • + {% endif %} diff --git a/webapp/views/student.py b/webapp/views/student.py index 9fc25d4..ea18302 100644 --- a/webapp/views/student.py +++ b/webapp/views/student.py @@ -1,10 +1,12 @@ +from secrets import token_hex + 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 +from flask import Blueprint, Response, abort from flask import current_app as app from flask import redirect, render_template, request @@ -26,6 +28,15 @@ students = StudentManager(config, db.students, db.mailers) +def set_anonymous_identifier(response: Response) -> Response: + if not request.cookies.get("anonymous_identifier"): + response.set_cookie("anonymous_identifier", value=token_hex(16)) + return response + + +blueprint.after_request(set_anonymous_identifier) + + @blueprint.route("/", methods=["GET"]) @student_jwt_optional(db.students) def dashboard(student: Student | None): @@ -44,15 +55,22 @@ def dashboard(student: Student | None): @blueprint.route("/submissions/", methods=["GET"]) @student_jwt_optional(db.students) def submissions(student: Student | None, page: int): - if student is None: - abort(401) size = 5 - submissions_statuses = statuses.get_submissions_statuses(student, (page - 1) * size, size) + session_id = request.cookies.get("anonymous_identifier") + if config.config.exam or not config.config.enable_registration: + if not session_id: + return redirect('/') + submissions_statuses = statuses.get_anonymous_submissions_statuses(session_id, (page - 1) * size, size) + submissions_count = statuses.count_session_id_submissions(session_id) + elif student is not None: + submissions_statuses = statuses.get_submissions_statuses(student, (page - 1) * size, size) + submissions_count = statuses.count_student_submissions(student) + else: + abort(401) + 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, @@ -70,6 +88,7 @@ def submissions(student: Student | None, page: int): submissions=submissions_statuses, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, student=student, highlight=config.config.highlight_syntax, page=page, @@ -89,6 +108,7 @@ def group(student: Student | None, group_id: int): blocked=blocked, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, student=student, ) @@ -102,6 +122,7 @@ def rating_groups(student: Student | None): groupings=groupings, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, student=student, ) @@ -115,6 +136,7 @@ def rating(student: Student | None): groupings=groupings, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, student=student, ) @@ -129,6 +151,7 @@ def task(student: Student | None, gid: int, vid: int, tid: int): highlight=config.config.highlight_syntax, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, status=status, form=form, student=student, @@ -146,13 +169,15 @@ def submit_task(student: Student | None, gid: int, vid: int, tid: int): allow_ip = config.config.allow_ip or "" if valid and allowed and not status.disabled and allow_ip in ip: sid = student.id if student else None - db.messages.submit_task(tid, vid, gid, form.code.data, ip, sid) + session_id = request.cookies.get("anonymous_identifier") + db.messages.submit_task(tid, vid, gid, form.code.data, ip, sid, session_id) db.statuses.submit_task(tid, vid, gid, form.code.data, ip) return render_template( "student/success.jinja", status=status, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, student=student, ) return render_template( @@ -160,6 +185,7 @@ def submit_task(student: Student | None, gid: int, vid: int, tid: int): highlight=config.config.highlight_syntax, registration=config.config.registration, group_rating=config.config.groups, + exam=config.config.exam, status=status, form=form, student=student, @@ -252,7 +278,7 @@ def hide_email_filter(value: str): @blueprint.errorhandler(Exception) def handle_view_errors(e): print(get_exception_info()) - return render_template("error.jinja", redirect="/") + return render_template("error.jinja", redirect="/", error_message=str(e)) @blueprint.errorhandler(JWTExtendedException)