Skip to content

feat: Solution status view #314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lms/lmsdb/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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(
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions lms/lmsweb/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
5 changes: 5 additions & 0 deletions lms/static/my.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ a {
color: #860606;
}

#forgot-my-password-link {
display: block;
margin: 0.5rem;
}

.page {
margin: 3rem 0;
}
Expand Down
2 changes: 1 addition & 1 deletion lms/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ <h1 id="main-title" class="h3 font-weight-normal">{{ _('התחברות') }}</h1>
<input class="form-control form-control-lg" type="hidden" name="csrf_token" id="csrf_token" value="{{ csrf_token() }}" required>
<input class="form-control form-control-lg" type="hidden" name="next" id="next" value="{{ request.args.get('next', '') }}">
<button class="btn btn-primary btn-lg btn-block">{{ _('התחבר') }}</button>
<a href="{{ url_for('reset_password') }}" id="forgot-my-password-link" role="button">{{ _('שכחת את הסיסמה?') }}</a>
</form>
<a href="{{ url_for('reset_password') }}" id="forgot-my-password-link" role="button">{{ _('שכחת את הסיסמה?') }}</a>
{% if config.REGISTRATION_OPEN %}
<hr class="mt-3 mb-3">
<a href="{{ url_for('signup') }}" class="btn btn-success btn-sm" role="button">{{ _('הירשם') }}</a>
Expand Down
36 changes: 35 additions & 1 deletion tests/test_solutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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