Skip to content

Commit 15bf52b

Browse files
authored
feat: Revamp the UX of exercise page (#382)
* feat: Revamp the UX of exercise page
1 parent 314ddfc commit 15bf52b

File tree

4 files changed

+131
-79
lines changed

4 files changed

+131
-79
lines changed

lms/lmsdb/models.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ class SolutionAssessment(BaseModel):
590590
name = CharField()
591591
icon = CharField(null=True)
592592
color = CharField()
593-
active_color = CharField()
593+
active_color = CharField(default="#fff")
594594
order = IntegerField(default=0, index=True)
595595
course = ForeignKeyField(Course, backref='assessments')
596596

@@ -718,6 +718,19 @@ def ordered_versions(self) -> Iterable['Solution']:
718718
def test_results(self) -> Iterable[dict]:
719719
return SolutionExerciseTestExecution.by_solution(self)
720720

721+
@staticmethod
722+
def _get_summary(solution: 'Solution') -> dict:
723+
exercise = {}
724+
exercise['solution_id'] = solution.id
725+
exercise['is_checked'] = solution.is_checked
726+
exercise['comments_num'] = len(solution.staff_comments)
727+
if solution.is_checked and solution.checker:
728+
exercise['checker'] = solution.checker.fullname
729+
if solution.assessment:
730+
exercise['assessment'] = solution.assessment.name
731+
exercise['grade_color'] = solution.assessment.color
732+
return exercise
733+
721734
@classmethod
722735
def of_user(
723736
cls, user_id: int, with_archived: bool = False,
@@ -737,15 +750,10 @@ def of_user(
737750
.order_by(cls.submission_timestamp.desc())
738751
)
739752
for solution in solutions:
740-
exercise = exercises[solution.exercise_id]
741-
if exercise.get('solution_id') is None:
742-
exercise['solution_id'] = solution.id
743-
exercise['is_checked'] = solution.is_checked
744-
exercise['comments_num'] = len(solution.staff_comments)
745-
if solution.is_checked and solution.checker:
746-
exercise['checker'] = solution.checker.fullname
747-
if solution.assessment:
748-
exercise['assessment'] = solution.assessment.name
753+
id_ = solution.exercise_id
754+
if exercises[id_].get('solution_id') is None:
755+
exercises[id_].update(cls._get_summary(solution))
756+
749757
return tuple(exercises.values())
750758

751759
@property
@@ -1244,11 +1252,20 @@ def create_basic_roles() -> None:
12441252

12451253
def create_basic_assessments() -> None:
12461254
assessments_dict = {
1247-
'Excellent': {'color': 'green', 'icon': 'star', 'order': 1},
1248-
'Nice': {'color': 'blue', 'icon': 'check', 'order': 2},
1249-
'Try again': {'color': 'red', 'icon': 'exclamation', 'order': 3},
1255+
'Excellent': {
1256+
'color': 'green', 'icon': 'star', 'order': 1,
1257+
},
1258+
'Nice': {
1259+
'color': 'blue', 'icon': 'check', 'order': 2,
1260+
},
1261+
'Try again': {
1262+
'color': 'red', 'icon': 'exclamation', 'order': 3,
1263+
},
1264+
'Invalid': {
1265+
'color': 'red', 'icon': 'ban', 'order': 4,
1266+
},
12501267
'Plagiarism': {
1251-
'color': 'black', 'icon': 'exclamation-triangle', 'order': 4,
1268+
'color': 'black', 'icon': 'exclamation-triangle', 'order': 5,
12521269
},
12531270
}
12541271
courses = Course.select()

lms/static/my.css

Lines changed: 87 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,18 @@ a {
124124

125125
.exercise {
126126
align-items: center;
127-
background: #f8f8f8;
128-
border-radius: 5px;
129-
border: 1px solid;
127+
background: #f8f9fa;
128+
border-radius: 10px;
129+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
130130
display: flex;
131131
flex-direction: row;
132132
justify-content: space-between;
133-
margin-bottom: 0.5rem;
134-
padding: 1rem;
135-
text-align: center;
133+
margin-bottom: 1rem;
136134
overflow: auto;
135+
padding: 0.75rem;
136+
text-align: center;
137+
border-right: #00000000 8px solid;
138+
border-left: #00000000 8px solid;
137139
}
138140

139141
.centered {
@@ -146,26 +148,34 @@ a {
146148
align-items: stretch;
147149
}
148150

151+
.right-side {
152+
justify-content: start;
153+
}
154+
149155
.left-side {
150156
justify-content: end;
151157
}
152158

153159
.exercise-number {
154160
align-items: center;
155-
border-radius: 100%;
156-
border: 1px solid;
157161
display: flex;
158-
height: 3rem;
159162
justify-content: center;
160163
overflow: hidden;
161164
text-align: center;
162-
width: 3rem;
165+
font-size: 1.2rem;
166+
margin-right: 1rem;
167+
color: #495057;
168+
padding: 0.25rem 0.75rem;
169+
border-radius: 15px;
170+
min-width: 4rem;
171+
justify-content: start;
163172
}
164173

165174
.exercise-name {
166175
align-items: center;
167176
display: flex;
168-
font-size: 1.5em;
177+
font-size: 1.4rem;
178+
line-height: 1.3;
169179
height: 3rem;
170180
justify-content: center;
171181
text-align: center;
@@ -224,65 +234,62 @@ button#share-action:hover {
224234
border-bottom-right-radius: 0;
225235
}
226236

227-
.go-send {
228-
background: #247ba0;
229-
color: #ebf3f6;
230-
}
231-
232-
.go-send:hover {
233-
color: #ebf3f6;
234-
}
235-
236-
.go-view {
237-
background: #ffe066;
238-
color: #18150a;
239-
}
240-
241-
.go-view:hover {
242-
color: #18150a;
237+
#back-to-exercises {
238+
width: 100%;
239+
margin-top: 5vh;
243240
}
244241

245-
.go-checked {
246-
background: #70c1b3;
247-
color: #0b1211;
242+
.our-button {
243+
align-items: center;
244+
background-color: #ffffff;
245+
border-radius: 20px;
246+
border: 1px solid #dee2e6;
247+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
248+
color: #495057;
249+
cursor: pointer;
250+
display: flex;
251+
font-size: 0.9rem;
252+
font-weight: 500;
253+
justify-content: space-between;
254+
margin: 0.5em;
255+
width: 8em;
256+
padding: 0.55rem 1rem;
257+
text-align: center;
258+
transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s;
248259
}
249260

250-
.go-checked:hover {
251-
color: #0b1211;
261+
.our-button:hover {
262+
background-color: #0d6efd;
263+
color: white;
264+
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
265+
transform: translateY(-2px);
252266
}
253267

254-
.go-grader {
255-
background: #b2dbbf;
256-
color: #1f2d3d;
268+
.our-button:active {
269+
transform: translateY(1px);
270+
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
257271
}
258272

259-
.go-grader:hover {
260-
color: #1f2d3d;
273+
.our-button.active, .our-button:checked {
274+
background-color: #28a745;
275+
color: white;
261276
}
262277

263-
#back-to-exercises {
264-
width: 100%;
265-
margin-top: 5vh;
278+
.our-button.active:hover, .our-button:checked:hover {
279+
background-color: #218838; /* Slightly darker green on hover */
266280
}
267281

268-
.our-button {
269-
border-radius: 8px;
270-
display: flex;
271-
margin-right: 0.5em;
272-
outline: none;
273-
padding: 0.55rem 1rem;
274-
white-space: nowrap;
275-
justify-content: space-between;
276-
align-items: center;
277-
height: 3em;
278-
width: 8em;
282+
@keyframes pop {
283+
0% { transform: scale(0.9); }
284+
50% { transform: scale(1.1); }
285+
100% { transform: scale(1); }
279286
}
280287

281-
.our-button:hover {
282-
text-decoration: none;
288+
.our-button:active {
289+
animation: pop 0.5s;
283290
}
284291

285-
.our-button-narrow {
292+
button.our-button-narrow, a.our-button-narrow {
286293
width: 2em;
287294
display: flex;
288295
align-items: center;
@@ -887,19 +894,44 @@ code .grader-add .fa {
887894
}
888895

889896
.which-notebook {
897+
align-items: center;
890898
align-self: center;
899+
display: flex;
891900
margin-inline-end: 1rem;
892901
}
893902

903+
.fa-book {
904+
color: #495057;
905+
font-size: 1rem;
906+
margin-inline-end: 0.25rem;
907+
vertical-align: middle;
908+
}
909+
894910
.comments-count {
911+
display: flex;
912+
align-items: center;
913+
font-size: 0.9rem;
914+
color: #495057;
895915
display: flex;
896916
align-self: center;
897917
align-items: center;
898-
font-size: 1.25rem;
899918
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
900919
margin-inline-end: 1rem;
901920
}
902921

922+
.comments-count .our-badge {
923+
background-color: #e9ecef; /* Neutral background */
924+
color: #495057; /* Dark text color for readability */
925+
padding: 0.25rem 0.5rem;
926+
font-size: 0.75rem;
927+
border-radius: 15px;
928+
display: inline-flex;
929+
align-items: center;
930+
justify-content: center;
931+
margin-inline-end: 0.5rem;
932+
box-shadow: none; /* Remove the shadow if you prefer a flat design */
933+
}
934+
903935
#courses-list {
904936
overflow-y: auto;
905937
max-height: 10em;

lms/templates/exercises.html

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ <h1 id="exercises-head">{{ _('Exercises') }}</h1>
1010
</div>
1111
<div id="exercises">
1212
{%- for exercise in exercises %}
13-
<div class="exercise">
13+
<div class="exercise" {%- if exercise.grade_color %} style="border-right: 8px solid {{ exercise.grade_color }};" {%- endif -%}>
1414
<div class="right-side {{ direction }}-language">
1515
<div class="exercise-number me-3">{{ exercise['exercise_number'] }}</div>
1616
<div class="exercise-name"><div class="ex-title">{{ exercise['exercise_name'] | e }}</div></div>
1717
</div>
1818
<div class="left-side">
19-
<div class="comments-count">
20-
<span class="badge bg-secondary">{{ exercise['comments_num'] }}</span>
21-
<span class="visually-hidden">{{ _("Comments for the solution") }}</span>
22-
</div>
2319
{%- if exercise['notebook'] %}
2420
<div class="which-notebook">
2521
<i class="fa fa-book" aria-hidden="true"></i>
@@ -39,11 +35,6 @@ <h1 id="exercises-head">{{ _('Exercises') }}</h1>
3935
<i class="fa fa-{{ details['icon'] }}" aria-hidden="true"></i>
4036
</a>
4137
{% endif -%}
42-
{% if is_manager %}
43-
<a class="our-button our-button-narrow go-grader" href="check/{{ exercise['exercise_id'] }}">
44-
<i class="fa fa-graduation-cap" aria-hidden="true"></i>
45-
</a>
46-
{% endif %}
4738
</div>
4839
</div>
4940
{% endfor -%}

tests/test_solutions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,15 @@ def test_solutions_of_user(
686686
exercises = solution.of_user(student_user.id, from_all_courses=True)
687687
assert exercises[0].get('assessment') is None
688688
assert exercises[1].get('assessment') == 'Try again'
689+
690+
@staticmethod
691+
def test_get_solution_summary(solution: Solution, staff_user: User):
692+
summary = Solution._get_summary(solution)
693+
assert summary.get('assessment') is None
694+
assert summary['solution_id'] == solution.id
695+
assert not summary['is_checked']
696+
697+
solution.mark_as_checked(staff_user)
698+
solution = Solution.get_by_id(solution.id)
699+
summary = Solution._get_summary(solution)
700+
assert summary['is_checked']

0 commit comments

Comments
 (0)