Skip to content

Commit d642693

Browse files
authored
feat: Solution status view (#314)
* feat: Status of last time viewed solution - Added two columns of status and datetime - Added migration - Added tests - Fixed the view of the login page
1 parent 95a3f60 commit d642693

File tree

6 files changed

+85
-2
lines changed

6 files changed

+85
-2
lines changed

lms/lmsdb/bootstrap.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ def _api_keys_migration() -> bool:
240240
return True
241241

242242

243+
def _last_status_view_migration() -> bool:
244+
Solution = models.Solution
245+
_migrate_column_in_table_if_needed(Solution, Solution.last_status_view)
246+
_migrate_column_in_table_if_needed(Solution, Solution.last_time_view)
247+
248+
243249
def _uuid_migration() -> bool:
244250
User = models.User
245251
_add_not_null_column(User, User.uuid, _add_uuid_to_users_table)
@@ -248,6 +254,9 @@ def _uuid_migration() -> bool:
248254

249255
def main():
250256
with models.database.connection_context():
257+
if models.database.table_exists(models.Solution.__name__.lower()):
258+
_last_status_view_migration()
259+
251260
if models.database.table_exists(models.User.__name__.lower()):
252261
_api_keys_migration()
253262
_uuid_migration()

lms/lmsdb/models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,20 @@ def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]:
354354
return tuple((choice.name, choice.value) for choice in choices)
355355

356356

357+
class SolutionStatusView(enum.Enum):
358+
UPLOADED = 'Uploaded'
359+
NOT_CHECKED = 'Not checked'
360+
CHECKED = 'Checked'
361+
362+
@classmethod
363+
def to_choices(cls: enum.EnumMeta) -> Tuple[Tuple[str, str], ...]:
364+
choices = cast(Iterable[enum.Enum], tuple(cls))
365+
return tuple((choice.name, choice.value) for choice in choices)
366+
367+
357368
class Solution(BaseModel):
358369
STATES = SolutionState
370+
STATUS_VIEW = SolutionStatusView
359371
MAX_CHECK_TIME_SECONDS = 60 * 10
360372

361373
exercise = ForeignKeyField(Exercise, backref='solutions')
@@ -371,6 +383,12 @@ class Solution(BaseModel):
371383
)
372384
submission_timestamp = DateTimeField(index=True)
373385
hashed = TextField()
386+
last_status_view = CharField(
387+
choices=STATUS_VIEW.to_choices(),
388+
default=STATUS_VIEW.UPLOADED.name,
389+
index=True,
390+
)
391+
last_time_view = DateTimeField(default=datetime.now, null=True, index=True)
374392

375393
@property
376394
def solution_files(
@@ -412,6 +430,20 @@ def is_duplicate(
412430

413431
return last_submission_hash == hash_
414432

433+
def view_solution(self) -> None:
434+
self.last_time_view = datetime.now()
435+
if (
436+
self.last_status_view != self.STATUS_VIEW.NOT_CHECKED.name
437+
and self.state == self.STATES.CREATED.name
438+
):
439+
self.last_status_view = self.STATUS_VIEW.NOT_CHECKED.name
440+
elif (
441+
self.last_status_view != self.STATUS_VIEW.CHECKED.name
442+
and self.state == self.STATES.DONE.name
443+
):
444+
self.last_status_view = self.STATUS_VIEW.CHECKED.name
445+
self.save()
446+
415447
def start_checking(self) -> bool:
416448
return self.set_state(Solution.STATES.IN_CHECKING)
417449

lms/lmsweb/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,9 @@ def view(
576576
error_message, status_code = e.args
577577
return fail(status_code, error_message)
578578

579+
if viewer_is_solver:
580+
solution.view_solution()
581+
579582
return render_template('view.html', **view_params)
580583

581584

lms/static/my.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ a {
8181
color: #860606;
8282
}
8383

84+
#forgot-my-password-link {
85+
display: block;
86+
margin: 0.5rem;
87+
}
88+
8489
.page {
8590
margin: 3rem 0;
8691
}

lms/templates/login.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ <h1 id="main-title" class="h3 font-weight-normal">{{ _('התחברות') }}</h1>
3333
<input class="form-control form-control-lg" type="hidden" name="csrf_token" id="csrf_token" value="{{ csrf_token() }}" required>
3434
<input class="form-control form-control-lg" type="hidden" name="next" id="next" value="{{ request.args.get('next', '') }}">
3535
<button class="btn btn-primary btn-lg btn-block">{{ _('התחבר') }}</button>
36-
<a href="{{ url_for('reset_password') }}" id="forgot-my-password-link" role="button">{{ _('שכחת את הסיסמה?') }}</a>
3736
</form>
37+
<a href="{{ url_for('reset_password') }}" id="forgot-my-password-link" role="button">{{ _('שכחת את הסיסמה?') }}</a>
3838
{% if config.REGISTRATION_OPEN %}
3939
<hr class="mt-3 mb-3">
4040
<a href="{{ url_for('signup') }}" class="btn btn-success btn-sm" role="button">{{ _('הירשם') }}</a>

tests/test_solutions.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import pytest
55

66
from lms.lmsdb import models
7-
from lms.lmsdb.models import Comment, Exercise, SharedSolution, Solution, User
7+
from lms.lmsdb.models import (
8+
Comment, Exercise, SharedSolution, Solution, SolutionStatusView, User,
9+
)
810
from lms.lmstests.public.general import tasks as general_tasks
911
from lms.lmsweb import routes
1012
from lms.models import notifications, solutions
@@ -518,3 +520,35 @@ def test_manager_reset_state_expect_exceptions(
518520
with pytest.raises(models.Solution.DoesNotExist):
519521
assert reset(solution_id_that_does_not_exists) is None
520522
assert 'does not exist' in caplog.text
523+
524+
@staticmethod
525+
def test_last_view_status(
526+
solution: Solution,
527+
student_user: User,
528+
staff_user: User,
529+
):
530+
client = conftest.get_logged_user(student_user.username)
531+
assert solution.last_status_view == SolutionStatusView.UPLOADED.name
532+
533+
client.get(f'/view/{solution.id}')
534+
solution = Solution.get_by_id(solution.id)
535+
assert solution.last_status_view == SolutionStatusView.NOT_CHECKED.name
536+
537+
solutions.mark_as_checked(solution.id, staff_user.id)
538+
solution = Solution.get_by_id(solution.id)
539+
assert solution.last_status_view == SolutionStatusView.NOT_CHECKED.name
540+
client.get(f'/view/{solution.id}')
541+
solution = Solution.get_by_id(solution.id)
542+
assert solution.last_status_view == SolutionStatusView.CHECKED.name
543+
544+
@staticmethod
545+
def test_invalid_file_solution(
546+
solution: Solution,
547+
student_user: User,
548+
):
549+
client = conftest.get_logged_user(student_user.username)
550+
successful_response = client.get(f'/view/{solution.id}')
551+
assert successful_response.status_code == 200
552+
553+
fail_response = client.get(f'/view/{solution.id}/12345')
554+
assert fail_response.status_code == 404

0 commit comments

Comments
 (0)