diff --git a/lms/lmsdb/bootstrap.py b/lms/lmsdb/bootstrap.py index f559759e..c4472f33 100644 --- a/lms/lmsdb/bootstrap.py +++ b/lms/lmsdb/bootstrap.py @@ -240,6 +240,12 @@ def _api_keys_migration() -> bool: return True +def _last_status_view_migration() -> bool: + Solution = models.Solution + _migrate_column_in_table_if_needed(Solution, Solution.last_status_view) + _migrate_column_in_table_if_needed(Solution, Solution.last_time_view) + + def _uuid_migration() -> bool: User = models.User _add_not_null_column(User, User.uuid, _add_uuid_to_users_table) @@ -248,6 +254,9 @@ def _uuid_migration() -> bool: def main(): with models.database.connection_context(): + if models.database.table_exists(models.Solution.__name__.lower()): + _last_status_view_migration() + if models.database.table_exists(models.User.__name__.lower()): _api_keys_migration() _uuid_migration() diff --git a/lms/lmsdb/models.py b/lms/lmsdb/models.py index eee18c6a..35c111d1 100644 --- a/lms/lmsdb/models.py +++ b/lms/lmsdb/models.py @@ -354,8 +354,20 @@ def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]: return tuple((choice.name, choice.value) for choice in choices) +class SolutionStatusView(enum.Enum): + UPLOADED = 'Uploaded' + NOT_CHECKED = 'Not checked' + CHECKED = 'Checked' + + @classmethod + def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]: + choices = cast(Iterable[enum.Enum], tuple(cls)) + return tuple((choice.name, choice.value) for choice in choices) + + class Solution(BaseModel): STATES = SolutionState + STATUS_VIEW = SolutionStatusView MAX_CHECK_TIME_SECONDS = 60 * 10 exercise = ForeignKeyField(Exercise, backref='solutions') @@ -371,6 +383,12 @@ class Solution(BaseModel): ) submission_timestamp = DateTimeField(index=True) hashed = TextField() + last_status_view = CharField( + choices=STATUS_VIEW.to_choices(), + default=STATUS_VIEW.UPLOADED.name, + index=True, + ) + last_time_view = DateTimeField(default=datetime.now, null=True, index=True) @property def solution_files( @@ -412,6 +430,20 @@ def is_duplicate( return last_submission_hash == hash_ + def view_solution(self) -> None: + self.last_time_view = datetime.now() + if ( + self.last_status_view != self.STATUS_VIEW.NOT_CHECKED.name + and self.state == self.STATES.CREATED.name + ): + self.last_status_view = self.STATUS_VIEW.NOT_CHECKED.name + elif ( + self.last_status_view != self.STATUS_VIEW.CHECKED.name + and self.state == self.STATES.DONE.name + ): + self.last_status_view = self.STATUS_VIEW.CHECKED.name + self.save() + def start_checking(self) -> bool: return self.set_state(Solution.STATES.IN_CHECKING) diff --git a/lms/lmsweb/views.py b/lms/lmsweb/views.py index 49ffdc3f..0c0022c3 100644 --- a/lms/lmsweb/views.py +++ b/lms/lmsweb/views.py @@ -576,6 +576,9 @@ def view( error_message, status_code = e.args return fail(status_code, error_message) + if viewer_is_solver: + solution.view_solution() + return render_template('view.html', **view_params) diff --git a/lms/static/my.css b/lms/static/my.css index 886ed538..f3acc0aa 100644 --- a/lms/static/my.css +++ b/lms/static/my.css @@ -81,6 +81,11 @@ a { color: #860606; } +#forgot-my-password-link { + display: block; + margin: 0.5rem; +} + .page { margin: 3rem 0; } diff --git a/lms/templates/login.html b/lms/templates/login.html index f76945de..20dd92a6 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -33,8 +33,8 @@

{{ _('התחברות') }}

- {{ _('שכחת את הסיסמה?') }} + {{ _('שכחת את הסיסמה?') }} {% if config.REGISTRATION_OPEN %}
{{ _('הירשם') }} diff --git a/tests/test_solutions.py b/tests/test_solutions.py index e16cabcd..e4711a86 100644 --- a/tests/test_solutions.py +++ b/tests/test_solutions.py @@ -4,7 +4,9 @@ import pytest from lms.lmsdb import models -from lms.lmsdb.models import Comment, Exercise, SharedSolution, Solution, User +from lms.lmsdb.models import ( + Comment, Exercise, SharedSolution, Solution, SolutionStatusView, User, +) from lms.lmstests.public.general import tasks as general_tasks from lms.lmsweb import routes from lms.models import notifications, solutions @@ -518,3 +520,35 @@ def test_manager_reset_state_expect_exceptions( with pytest.raises(models.Solution.DoesNotExist): assert reset(solution_id_that_does_not_exists) is None assert 'does not exist' in caplog.text + + @staticmethod + def test_last_view_status( + solution: Solution, + student_user: User, + staff_user: User, + ): + client = conftest.get_logged_user(student_user.username) + assert solution.last_status_view == SolutionStatusView.UPLOADED.name + + client.get(f'/view/{solution.id}') + solution = Solution.get_by_id(solution.id) + assert solution.last_status_view == SolutionStatusView.NOT_CHECKED.name + + solutions.mark_as_checked(solution.id, staff_user.id) + solution = Solution.get_by_id(solution.id) + assert solution.last_status_view == SolutionStatusView.NOT_CHECKED.name + client.get(f'/view/{solution.id}') + solution = Solution.get_by_id(solution.id) + assert solution.last_status_view == SolutionStatusView.CHECKED.name + + @staticmethod + def test_invalid_file_solution( + solution: Solution, + student_user: User, + ): + client = conftest.get_logged_user(student_user.username) + successful_response = client.get(f'/view/{solution.id}') + assert successful_response.status_code == 200 + + fail_response = client.get(f'/view/{solution.id}/12345') + assert fail_response.status_code == 404