Skip to content

Commit 431f350

Browse files
committed
Added a test and changed course' style
1 parent 2408d06 commit 431f350

File tree

7 files changed

+155
-110
lines changed

7 files changed

+155
-110
lines changed

lms/lmsdb/models.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from collections import Counter, defaultdict
1+
from collections import Counter
22
import enum
33
import html
44
import secrets
55
import string
66
from datetime import datetime
77
from typing import (
8-
Any, DefaultDict, Dict, Iterable, List, Optional,
8+
Any, Dict, Iterable, List, Optional,
99
TYPE_CHECKING, Tuple, Type, Union, cast,
1010
)
1111

@@ -28,7 +28,7 @@
2828

2929

3030
database = database_config.get_db_instance()
31-
ExercisesDictById = DefaultDict[str, Dict[int, Dict[str, Any]]]
31+
ExercisesDictById = Dict[int, Dict[str, Any]]
3232
if TYPE_CHECKING:
3333
from lms.extractors.base import File
3434

@@ -141,6 +141,16 @@ class Course(BaseModel):
141141
invite_code = CharField(default=generate_invite_code, unique=True)
142142
is_public = BooleanField(default=False)
143143

144+
@classmethod
145+
def fetch(cls, user: 'User'):
146+
return (
147+
cls
148+
.select()
149+
.join(UserCourse)
150+
.where(UserCourse.user == user.id)
151+
.order_by(Course.name.desc())
152+
)
153+
144154
def __str__(self):
145155
return f'{self.name}: {self.date} - {self.due_date}'
146156

@@ -152,6 +162,7 @@ class User(UserMixin, BaseModel):
152162
password = CharField()
153163
role = ForeignKeyField(Role, backref='users')
154164
api_key = CharField()
165+
last_course_viewed = ForeignKeyField(Course, null=True)
155166

156167
def is_password_valid(self, password):
157168
return check_password_hash(self.password, password)
@@ -177,6 +188,9 @@ def random_password(cls, stronger: bool = False) -> str:
177188
def get_notifications(self) -> Iterable['Notification']:
178189
return Notification.fetch(self)
179190

191+
def get_courses(self) -> Iterable['Course']:
192+
return Course.fetch(self)
193+
180194
def notes(self) -> Iterable['Note']:
181195
fields = (
182196
Note.id, Note.creator.fullname, CommentText.text,
@@ -344,16 +358,24 @@ def open_for_new_solutions(self) -> bool:
344358
return datetime.now() < self.due_date and not self.is_archived
345359

346360
@classmethod
347-
def get_objects(cls, user_id: int, fetch_archived: bool = False):
361+
def get_objects(
362+
cls, user_id: int, fetch_archived: bool = False,
363+
select_all: bool = False,
364+
):
365+
user = User.get(User.id == user_id)
348366
exercises = (
349367
cls
350368
.select()
351369
.join(Course)
352370
.join(UserCourse)
353371
.where(UserCourse.user == user_id)
354372
.switch()
355-
.order_by(UserCourse.date, Exercise.order)
373+
.order_by(UserCourse.date, Exercise.number, Exercise.order)
356374
)
375+
if not select_all:
376+
exercises = exercises.where(
377+
UserCourse.course == user.last_course_viewed,
378+
)
357379
if not fetch_archived:
358380
exercises = exercises.where(cls.is_archived == False) # NOQA: E712
359381
return exercises
@@ -372,10 +394,7 @@ def as_dict(self) -> Dict[str, Any]:
372394

373395
@staticmethod
374396
def as_dicts(exercises: Iterable['Exercise']) -> ExercisesDictById:
375-
nested_dict = defaultdict(dict)
376-
for exercise in exercises:
377-
nested_dict[exercise.course.name][exercise.id] = exercise.as_dict()
378-
return nested_dict
397+
return {exercise.id: exercise.as_dict() for exercise in exercises}
379398

380399
def __str__(self):
381400
return self.subject
@@ -484,9 +503,11 @@ def test_results(self) -> Iterable[dict]:
484503
@classmethod
485504
def of_user(
486505
cls, user_id: int, with_archived: bool = False,
487-
) -> Iterable[DefaultDict[str, Dict[str, Any]]]:
506+
select_all: bool = False,
507+
) -> Iterable[Dict[str, Any]]:
488508
db_exercises = Exercise.get_objects(
489509
user_id=user_id, fetch_archived=with_archived,
510+
select_all=select_all,
490511
)
491512
exercises = Exercise.as_dicts(db_exercises)
492513
solutions = (
@@ -496,15 +517,14 @@ def of_user(
496517
.order_by(cls.submission_timestamp.desc())
497518
)
498519
for solution in solutions:
499-
course_name = solution.exercise.course.name
500-
exercise = exercises[course_name][solution.exercise_id]
520+
exercise = exercises[solution.exercise_id]
501521
if exercise.get('solution_id') is None:
502522
exercise['solution_id'] = solution.id
503523
exercise['is_checked'] = solution.is_checked
504524
exercise['comments_num'] = len(solution.staff_comments)
505525
if solution.is_checked and solution.checker:
506526
exercise['checker'] = solution.checker.fullname
507-
return exercises
527+
return tuple(exercises.values())
508528

509529
@property
510530
def comments(self):

lms/lmsweb/views.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from werkzeug.utils import redirect
1616

1717
from lms.lmsdb.models import (
18-
ALL_MODELS, Comment, Note, Role, RoleOptions, SharedSolution,
18+
ALL_MODELS, Comment, Course, Note, Role, RoleOptions, SharedSolution,
1919
Solution, SolutionFile, User, UserCourse, database,
2020
)
2121
from lms.lmsweb import babel, limiter, routes, webapp
@@ -242,6 +242,20 @@ def status():
242242
)
243243

244244

245+
@webapp.route('/change-course/<int:course_id>')
246+
@login_required
247+
def change_last_course_viewed(course_id: int):
248+
course = Course.get_or_none(course_id)
249+
if course is None:
250+
return fail(404, f'No such course {course_id}.')
251+
user = User.get(User.id == current_user.id)
252+
if not UserCourse.is_user_registered(user.id, course.id):
253+
return fail(403, "You're not allowed to access this page.")
254+
user.last_course_viewed = course
255+
user.save()
256+
return redirect(url_for('exercises_page'))
257+
258+
245259
@webapp.route('/exercises')
246260
@login_required
247261
def exercises_page():
@@ -398,7 +412,9 @@ def user(user_id):
398412

399413
return render_template(
400414
'user.html',
401-
solutions=Solution.of_user(target_user.id, with_archived=True),
415+
solutions=Solution.of_user(
416+
target_user.id, with_archived=True, select_all=True,
417+
),
402418
user=target_user,
403419
is_manager=is_manager,
404420
notes_options=Note.get_note_options(),

lms/static/my.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,11 +764,18 @@ code .grader-add .fa {
764764
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
765765
}
766766

767-
#notification-icon {
767+
#courses-list {
768+
overflow-y: scroll;
769+
max-height: 10em;
770+
}
771+
772+
#notification-icon,
773+
#courses-icon {
768774
cursor: pointer;
769775
}
770776

771-
.notification {
777+
.notification,
778+
.course {
772779
flex-direction: column;
773780
text-align: start;
774781
}

lms/templates/exercises.html

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,44 @@ <h1 id="exercises-head">{{ _('תרגילים') }}</h1>
99
</div>
1010
</div>
1111
<div id="exercises">
12-
{% for course, values in exercises.items() %}
13-
<h3>{{ course }}</h3>
14-
{%- for exercise in values.values() %}
15-
<div class="exercise">
16-
<div class="right-side {{ direction }}-language">
17-
<div class="exercise-number">{{ exercise['exercise_number'] }}</div>
18-
<div class="exercise-name"><div class="ex-title">{{ exercise['exercise_name'] | e }}</div></div>
12+
{%- for exercise in exercises %}
13+
<div class="exercise">
14+
<div class="right-side {{ direction }}-language">
15+
<div class="exercise-number">{{ exercise['exercise_number'] }}</div>
16+
<div class="exercise-name"><div class="ex-title">{{ exercise['exercise_name'] | e }}</div></div>
17+
</div>
18+
<div class="left-side">
19+
<div class="comments-count">
20+
<span class="badge bg-secondary">{{ exercise['comments_num'] }}</span>
21+
<span class="visually-hidden">{{ _("הערות על התרגיל") }}</span>
1922
</div>
20-
<div class="left-side">
21-
<div class="comments-count">
22-
<span class="badge bg-secondary">{{ exercise['comments_num'] }}</span>
23-
<span class="visually-hidden">{{ _("הערות על התרגיל") }}</span>
24-
</div>
25-
{%- if exercise['notebook'] %}
26-
<div class="which-notebook">
27-
<i class="fa fa-book" aria-hidden="true"></i>
28-
{{ exercise['notebook'] }}
29-
</div>
30-
{%- endif %}
31-
{%- if exercise.get('is_checked') is none %}
32-
{% set details = {'page': 'send', 'icon': 'upload', 'text': _('שלח'), 'css': 'send', 'page_id': exercise['course_id']} %}
33-
{% elif not exercise.get('is_checked') %}
34-
{% set details = {'page': 'view', 'icon': 'eye', 'text': _('הצצה'), 'css': 'view', 'page_id': exercise['solution_id']} %}
35-
{% else %}
36-
{% set details = {'page': 'view', 'icon': 'check-circle-o', 'text': _('לבדיקה'), 'css': 'checked', 'page_id': exercise['solution_id']} %}
37-
{% endif -%}
38-
{%- if not exercise.get('is_archived') or exercise.get('is_checked') is not none %}
39-
<a class="our-button go-{{ details['css'] }}" href="{{ details['page'] }}/{{ details['page_id'] }}">
40-
{{ details['text'] | e }}
41-
<i class="fa fa-{{ details['icon'] }}" aria-hidden="true"></i>
42-
</a>
43-
{% endif -%}
44-
{% if is_manager %}
45-
<a class="our-button our-button-narrow go-grader" href="check/{{ exercise['exercise_id'] }}">
46-
<i class="fa fa-graduation-cap" aria-hidden="true"></i>
47-
</a>
48-
{% endif %}
23+
{%- if exercise['notebook'] %}
24+
<div class="which-notebook">
25+
<i class="fa fa-book" aria-hidden="true"></i>
26+
{{ exercise['notebook'] }}
4927
</div>
28+
{%- endif %}
29+
{%- if exercise.get('is_checked') is none %}
30+
{% set details = {'page': 'send', 'icon': 'upload', 'text': _('שלח'), 'css': 'send', 'page_id': exercise['course_id']} %}
31+
{% elif not exercise.get('is_checked') %}
32+
{% set details = {'page': 'view', 'icon': 'eye', 'text': _('הצצה'), 'css': 'view', 'page_id': exercise['solution_id']} %}
33+
{% else %}
34+
{% set details = {'page': 'view', 'icon': 'check-circle-o', 'text': _('לבדיקה'), 'css': 'checked', 'page_id': exercise['solution_id']} %}
35+
{% endif -%}
36+
{%- if not exercise.get('is_archived') or exercise.get('is_checked') is not none %}
37+
<a class="our-button go-{{ details['css'] }}" href="{{ details['page'] }}/{{ details['page_id'] }}">
38+
{{ details['text'] | e }}
39+
<i class="fa fa-{{ details['icon'] }}" aria-hidden="true"></i>
40+
</a>
41+
{% 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 %}
5047
</div>
51-
{% endfor -%}
52-
{% endfor %}
48+
</div>
49+
{% endfor -%}
5350
{%- if not fetch_archived %}
5451
<div class="exercise centered">
5552
<div id="show-all-exercises-action" class="right-side">

lms/templates/navbar.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@
3737
</div>
3838
</div>
3939
</li>
40+
<li class="nav-item dropdown">
41+
<a id="courses-icon" href="#" class="nav-link" aria-expanded="false" aria-haspopup="true" aria-label="Courses" role="button" data-toggle="dropdown">
42+
<i class="fa fa-graduation-cap" aria-hidden="true"></i>
43+
{% if not current_user.last_course_viewed %}
44+
{{ _('רשימת הקורסים') }}
45+
{% else %}
46+
{{ current_user.last_course_viewed.name | e }}
47+
{% endif %}
48+
</a>
49+
<div id="courses-list" aria-labelledby="navbarDropdown" class="dropdown-menu
50+
{%- if direction == 'rtl' %}
51+
dropdown-menu-right
52+
{%- else %}
53+
dropdown-menu-left
54+
{%- endif %}
55+
">
56+
{% for course in current_user.get_courses() %}
57+
<div class="course dropdown-item {{ direction }}">
58+
<i class="fa fa-book" aria-hidden="true"></i>
59+
<a href="{{ url_for('change_last_course_viewed', course_id=course.id) }}" class="course-text {{ direction }}">
60+
{{ course.name | e }}
61+
</a>
62+
</div>
63+
{% endfor -%}
64+
</div>
65+
</li>
4066
{%- if not exercises or fetch_archived %}
4167
<li class="nav-item">
4268
<a href="/exercises" class="nav-link">

lms/templates/user.html

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,18 @@ <h2>{{ _('תרגילים שהוגשו:') }}</h2>
2626
</tr>
2727
</thead>
2828
<tbody>
29-
{%- for exercise in solutions.values() %}
30-
{% for solution in exercise.values() %}
31-
<tr>
32-
<th scope="row">{{ solution.exercise_number | e }}</th>
33-
<td>{{ solution.course_name | e }}</td>
34-
<td>{{ solution.exercise_name | e }}</td>
35-
<td>
36-
{{ _('נבדק') if solution.is_checked else _('הוגש') if solution.solution_id else _('לא הוגש') }}
37-
</td>
38-
<td><a href="/view/{{ solution.solution_id }}">{{ solution.solution_id }}</a></td>
39-
<td>{{ solution.get('checker', '') | e }}</a></td>
40-
</tr>
41-
{% endfor %}
42-
{% endfor -%}
29+
{% for solution in solutions %}
30+
<tr>
31+
<th scope="row">{{ solution.exercise_number | e }}</th>
32+
<td>{{ solution.course_name | e }}</td>
33+
<td>{{ solution.exercise_name | e }}</td>
34+
<td>
35+
{{ _('נבדק') if solution.is_checked else _('הוגש') if solution.solution_id else _('לא הוגש') }}
36+
</td>
37+
<td><a href="/view/{{ solution.solution_id }}">{{ solution.solution_id }}</a></td>
38+
<td>{{ solution.get('checker', '') | e }}</a></td>
39+
</tr>
40+
{% endfor %}
4341
</tbody>
4442
</table>
4543
</div>

0 commit comments

Comments
 (0)