diff --git a/lms/lmsdb/models.py b/lms/lmsdb/models.py index 1b6ecca5..585e1781 100644 --- a/lms/lmsdb/models.py +++ b/lms/lmsdb/models.py @@ -590,7 +590,7 @@ class SolutionAssessment(BaseModel): name = CharField() icon = CharField(null=True) color = CharField() - active_color = CharField() + active_color = CharField(default="#fff") order = IntegerField(default=0, index=True) course = ForeignKeyField(Course, backref='assessments') @@ -718,6 +718,19 @@ def ordered_versions(self) -> Iterable['Solution']: def test_results(self) -> Iterable[dict]: return SolutionExerciseTestExecution.by_solution(self) + @staticmethod + def _get_summary(solution: 'Solution') -> dict: + exercise = {} + exercise['solution_id'] = solution.id + exercise['is_checked'] = solution.is_checked + exercise['comments_num'] = len(solution.staff_comments) + if solution.is_checked and solution.checker: + exercise['checker'] = solution.checker.fullname + if solution.assessment: + exercise['assessment'] = solution.assessment.name + exercise['grade_color'] = solution.assessment.color + return exercise + @classmethod def of_user( cls, user_id: int, with_archived: bool = False, @@ -737,15 +750,10 @@ def of_user( .order_by(cls.submission_timestamp.desc()) ) for solution in solutions: - exercise = exercises[solution.exercise_id] - if exercise.get('solution_id') is None: - exercise['solution_id'] = solution.id - exercise['is_checked'] = solution.is_checked - exercise['comments_num'] = len(solution.staff_comments) - if solution.is_checked and solution.checker: - exercise['checker'] = solution.checker.fullname - if solution.assessment: - exercise['assessment'] = solution.assessment.name + id_ = solution.exercise_id + if exercises[id_].get('solution_id') is None: + exercises[id_].update(cls._get_summary(solution)) + return tuple(exercises.values()) @property @@ -1244,11 +1252,20 @@ def create_basic_roles() -> None: def create_basic_assessments() -> None: assessments_dict = { - 'Excellent': {'color': 'green', 'icon': 'star', 'order': 1}, - 'Nice': {'color': 'blue', 'icon': 'check', 'order': 2}, - 'Try again': {'color': 'red', 'icon': 'exclamation', 'order': 3}, + 'Excellent': { + 'color': 'green', 'icon': 'star', 'order': 1, + }, + 'Nice': { + 'color': 'blue', 'icon': 'check', 'order': 2, + }, + 'Try again': { + 'color': 'red', 'icon': 'exclamation', 'order': 3, + }, + 'Invalid': { + 'color': 'red', 'icon': 'ban', 'order': 4, + }, 'Plagiarism': { - 'color': 'black', 'icon': 'exclamation-triangle', 'order': 4, + 'color': 'black', 'icon': 'exclamation-triangle', 'order': 5, }, } courses = Course.select() diff --git a/lms/static/my.css b/lms/static/my.css index c01495cf..5a724cf7 100644 --- a/lms/static/my.css +++ b/lms/static/my.css @@ -124,16 +124,18 @@ a { .exercise { align-items: center; - background: #f8f8f8; - border-radius: 5px; - border: 1px solid; + background: #f8f9fa; + border-radius: 10px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: flex; flex-direction: row; justify-content: space-between; - margin-bottom: 0.5rem; - padding: 1rem; - text-align: center; + margin-bottom: 1rem; overflow: auto; + padding: 0.75rem; + text-align: center; + border-right: #00000000 8px solid; + border-left: #00000000 8px solid; } .centered { @@ -146,26 +148,34 @@ a { align-items: stretch; } +.right-side { + justify-content: start; +} + .left-side { justify-content: end; } .exercise-number { align-items: center; - border-radius: 100%; - border: 1px solid; display: flex; - height: 3rem; justify-content: center; overflow: hidden; text-align: center; - width: 3rem; + font-size: 1.2rem; + margin-right: 1rem; + color: #495057; + padding: 0.25rem 0.75rem; + border-radius: 15px; + min-width: 4rem; + justify-content: start; } .exercise-name { align-items: center; display: flex; - font-size: 1.5em; + font-size: 1.4rem; + line-height: 1.3; height: 3rem; justify-content: center; text-align: center; @@ -224,65 +234,62 @@ button#share-action:hover { border-bottom-right-radius: 0; } -.go-send { - background: #247ba0; - color: #ebf3f6; -} - -.go-send:hover { - color: #ebf3f6; -} - -.go-view { - background: #ffe066; - color: #18150a; -} - -.go-view:hover { - color: #18150a; +#back-to-exercises { + width: 100%; + margin-top: 5vh; } -.go-checked { - background: #70c1b3; - color: #0b1211; +.our-button { + align-items: center; + background-color: #ffffff; + border-radius: 20px; + border: 1px solid #dee2e6; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + color: #495057; + cursor: pointer; + display: flex; + font-size: 0.9rem; + font-weight: 500; + justify-content: space-between; + margin: 0.5em; + width: 8em; + padding: 0.55rem 1rem; + text-align: center; + transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s; } -.go-checked:hover { - color: #0b1211; +.our-button:hover { + background-color: #0d6efd; + color: white; + box-shadow: 0 2px 4px rgba(0,0,0,0.15); + transform: translateY(-2px); } -.go-grader { - background: #b2dbbf; - color: #1f2d3d; +.our-button:active { + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0,0,0,0.15); } -.go-grader:hover { - color: #1f2d3d; +.our-button.active, .our-button:checked { + background-color: #28a745; + color: white; } -#back-to-exercises { - width: 100%; - margin-top: 5vh; +.our-button.active:hover, .our-button:checked:hover { + background-color: #218838; /* Slightly darker green on hover */ } -.our-button { - border-radius: 8px; - display: flex; - margin-right: 0.5em; - outline: none; - padding: 0.55rem 1rem; - white-space: nowrap; - justify-content: space-between; - align-items: center; - height: 3em; - width: 8em; +@keyframes pop { + 0% { transform: scale(0.9); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } } -.our-button:hover { - text-decoration: none; +.our-button:active { + animation: pop 0.5s; } -.our-button-narrow { +button.our-button-narrow, a.our-button-narrow { width: 2em; display: flex; align-items: center; @@ -887,19 +894,44 @@ code .grader-add .fa { } .which-notebook { + align-items: center; align-self: center; + display: flex; margin-inline-end: 1rem; } +.fa-book { + color: #495057; + font-size: 1rem; + margin-inline-end: 0.25rem; + vertical-align: middle; +} + .comments-count { + display: flex; + align-items: center; + font-size: 0.9rem; + color: #495057; display: flex; align-self: center; align-items: center; - font-size: 1.25rem; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; margin-inline-end: 1rem; } +.comments-count .our-badge { + background-color: #e9ecef; /* Neutral background */ + color: #495057; /* Dark text color for readability */ + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 15px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-inline-end: 0.5rem; + box-shadow: none; /* Remove the shadow if you prefer a flat design */ +} + #courses-list { overflow-y: auto; max-height: 10em; diff --git a/lms/templates/exercises.html b/lms/templates/exercises.html index 0b08335f..3abb75ed 100644 --- a/lms/templates/exercises.html +++ b/lms/templates/exercises.html @@ -10,16 +10,12 @@

{{ _('Exercises') }}

{%- for exercise in exercises %} -
+
{{ exercise['exercise_number'] }}
{{ exercise['exercise_name'] | e }}
-
- {{ exercise['comments_num'] }} - {{ _("Comments for the solution") }} -
{%- if exercise['notebook'] %}
@@ -39,11 +35,6 @@

{{ _('Exercises') }}

{% endif -%} - {% if is_manager %} - - - - {% endif %}
{% endfor -%} diff --git a/tests/test_solutions.py b/tests/test_solutions.py index f01c5a4e..22a02d7c 100644 --- a/tests/test_solutions.py +++ b/tests/test_solutions.py @@ -686,3 +686,15 @@ def test_solutions_of_user( exercises = solution.of_user(student_user.id, from_all_courses=True) assert exercises[0].get('assessment') is None assert exercises[1].get('assessment') == 'Try again' + + @staticmethod + def test_get_solution_summary(solution: Solution, staff_user: User): + summary = Solution._get_summary(solution) + assert summary.get('assessment') is None + assert summary['solution_id'] == solution.id + assert not summary['is_checked'] + + solution.mark_as_checked(staff_user) + solution = Solution.get_by_id(solution.id) + summary = Solution._get_summary(solution) + assert summary['is_checked']