Skip to content

Commit 7762f60

Browse files
authored
feat: Join public courses (#328)
* feat: Add public courses registeration page - Added the html template page - Added a test - Added translations - Added in the backend the logics of the pages and the public courses
1 parent f3ed156 commit 7762f60

File tree

10 files changed

+150
-27
lines changed

10 files changed

+150
-27
lines changed

lms/lmsdb/bootstrap.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,21 +278,34 @@ def _add_exercise_course_id_and_number_columns_constraint() -> bool:
278278
Exercise = models.Exercise
279279
migrator = db_config.get_migrator_instance()
280280
with db_config.database.transaction():
281-
course_not_exists = _add_not_null_column(Exercise, Exercise.course)
282-
number_not_exists = _add_not_null_column(Exercise, Exercise.number)
283-
if course_not_exists and number_not_exists:
281+
_add_not_null_column(Exercise, Exercise.course)
282+
_add_not_null_column(Exercise, Exercise.number)
283+
try:
284284
migrate(
285285
migrator.add_index('exercise', ('course_id', 'number'), True),
286286
)
287+
except OperationalError as e:
288+
if 'already exists' in str(e):
289+
log.info(f'index exercise already exists: {e}')
290+
else:
291+
raise
287292
db_config.database.commit()
288293

289294

290295
def _add_user_course_constaint() -> bool:
291296
migrator = db_config.get_migrator_instance()
292297
with db_config.database.transaction():
293-
migrate(
294-
migrator.add_index('usercourse', ('user_id', 'course_id'), True),
295-
)
298+
try:
299+
migrate(
300+
migrator.add_index(
301+
'usercourse', ('user_id', 'course_id'), True,
302+
),
303+
)
304+
except OperationalError as e:
305+
if 'already exists' in str(e):
306+
log.info(f'index usercourse already exists: {e}')
307+
else:
308+
raise
296309
db_config.database.commit()
297310

298311

lms/lmsdb/models.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ def fetch(cls, user: 'User') -> Iterable['Course']:
159159
.order_by(Course.name.desc())
160160
)
161161

162+
@classmethod
163+
def public_courses(cls):
164+
return cls.select().where(cls.is_public)
165+
166+
@classmethod
167+
def public_course_exists(cls):
168+
return cls.public_courses().exists()
169+
162170
def __str__(self):
163171
return f'{self.name}: {self.date} - {self.end_date}'
164172

@@ -279,7 +287,7 @@ def is_user_registered(cls, user_id: int, course_id: int) -> bool:
279287
@post_save(sender=UserCourse)
280288
def on_save_user_course(model_class, instance, created):
281289
"""Changes user's last course viewed."""
282-
if instance.user.last_course_viewed is None:
290+
if instance.user.last_course_viewed is None or instance.course.is_public:
283291
instance.user.last_course_viewed = instance.course
284292
instance.user.save()
285293

lms/lmsweb/translations/he/LC_MESSAGES/messages.po

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ msgid ""
77
msgstr ""
88
"Project-Id-Version: 1.0\n"
99
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10-
"POT-Creation-Date: 2021-10-03 22:01+0300\n"
10+
"POT-Creation-Date: 2021-10-06 15:07+0300\n"
1111
"PO-Revision-Date: 2021-09-29 11:30+0300\n"
1212
"Last-Translator: Or Ronai\n"
1313
"Language: he\n"
@@ -18,7 +18,7 @@ msgstr ""
1818
"Content-Transfer-Encoding: 8bit\n"
1919
"Generated-By: Babel 2.9.1\n"
2020

21-
#: lmsdb/models.py:879
21+
#: lmsdb/models.py:864
2222
msgid "Fatal error"
2323
msgstr "כישלון חמור"
2424

@@ -116,14 +116,19 @@ msgstr "%(checker)s הגיב לך על תרגיל \"%(subject)s\"."
116116
msgid "Your solution for the \"%(subject)s\" exercise has been checked."
117117
msgstr "הפתרון שלך לתרגיל \"%(subject)s\" נבדק."
118118

119-
#: models/users.py:28
119+
#: models/users.py:29
120120
msgid "Invalid username or password"
121121
msgstr "שם המשתמש או הסיסמה שהוזנו לא תקינים"
122122

123-
#: models/users.py:31
123+
#: models/users.py:32
124124
msgid "You have to confirm your registration with the link sent to your email"
125125
msgstr "עליך לאשר את מייל האימות"
126126

127+
#: models/users.py:50
128+
#, python-format
129+
msgid "You are already registered to %(course_name)s course."
130+
msgstr "אתה כבר רשום לקורס %(course_name)s."
131+
127132
#: templates/banned.html:8 templates/login.html:7
128133
#: templates/recover-password.html:8 templates/reset-password.html:8
129134
#: templates/signup.html:8
@@ -247,6 +252,11 @@ msgstr "בדוק תרגילים"
247252
msgid "Logout"
248253
msgstr "התנתקות"
249254

255+
#: templates/public-courses.html:6
256+
#, fuzzy
257+
msgid "Public Courses List"
258+
msgstr "רשימת קורסים פתוחים"
259+
250260
#: templates/recover-password.html:9 templates/recover-password.html:17
251261
#: templates/reset-password.html:9
252262
msgid "Reset Password"
@@ -293,7 +303,7 @@ msgstr "חמ\"ל תרגילים"
293303
msgid "Name"
294304
msgstr "שם"
295305

296-
#: templates/status.html:13 templates/user.html:44
306+
#: templates/status.html:13 templates/user.html:46
297307
msgid "Checked"
298308
msgstr "נבדק/ו"
299309

@@ -341,27 +351,31 @@ msgstr "פרטי משתמש"
341351
msgid "Actions"
342352
msgstr "פעולות"
343353

344-
#: templates/user.html:24
354+
#: templates/user.html:21
355+
msgid "Join Courses"
356+
msgstr "הירשם לקורסים"
357+
358+
#: templates/user.html:27
345359
msgid "Exercises Submitted"
346360
msgstr "תרגילים שהוגשו"
347361

348362
#: templates/user.html:29
349363
msgid "Course name"
350364
msgstr "שם קורס"
351365

352-
#: templates/user.html:30
366+
#: templates/user.html:33
353367
msgid "Exercise name"
354368
msgstr "שם תרגיל"
355369

356-
#: templates/user.html:31
370+
#: templates/user.html:34
357371
msgid "Submission status"
358372
msgstr "מצב הגשה"
359373

360-
#: templates/user.html:32
374+
#: templates/user.html:35
361375
msgid "Submission"
362376
msgstr "הגשה"
363377

364-
#: templates/user.html:33
378+
#: templates/user.html:36
365379
msgid "Checker"
366380
msgstr "בודק"
367381

lms/lmsweb/views.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@
3535
PERMISSIVE_CORS, get_next_url, login_manager,
3636
)
3737
from lms.models import (
38-
comments, notes, notifications, share_link, solutions, upload,
38+
comments, notes, notifications, share_link, solutions, upload, users,
3939
)
4040
from lms.models.errors import (
41-
FileSizeError, ForbiddenPermission, LmsError,
41+
AlreadyExists, FileSizeError, ForbiddenPermission, LmsError,
4242
UnauthorizedError, UploadError, fail,
4343
)
4444
from lms.models.users import SERIALIZER, auth, retrieve_salt
@@ -505,9 +505,37 @@ def user(user_id):
505505
user=target_user,
506506
is_manager=is_manager,
507507
notes_options=Note.get_note_options(),
508+
public_course_exists=Course.public_course_exists(),
508509
)
509510

510511

512+
@webapp.route('/course')
513+
@login_required
514+
def public_courses():
515+
return render_template(
516+
'public-courses.html',
517+
courses=Course.public_courses(),
518+
)
519+
520+
521+
@webapp.route('/course/join/<int:course_id>')
522+
@login_required
523+
def join_public_course(course_id: int):
524+
course = Course.get_or_none(course_id)
525+
if course is None:
526+
return fail(404, 'There is no such course.')
527+
if not course.is_public:
528+
return fail(403, "You aren't allowed to do this method.")
529+
530+
try:
531+
users.join_public_course(course, current_user)
532+
except AlreadyExists as e:
533+
error_message, status_code = e.args
534+
return fail(status_code, error_message)
535+
536+
return redirect(url_for('exercises_page'))
537+
538+
511539
@webapp.route('/send/<int:course_id>', methods=['GET'])
512540
@login_required
513541
def send_(course_id: int):

lms/models/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class LmsError(Exception):
55
pass
66

77

8-
class AlreadyExists(LmsError):
8+
class AlreadyExists(LmsError): # Usually a 409 HTTP Error
99
pass
1010

1111

lms/models/users.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from flask_babel import gettext as _ # type: ignore
44
from itsdangerous import URLSafeTimedSerializer
55

6-
from lms.lmsdb.models import User
6+
from lms.lmsdb.models import Course, User, UserCourse
77
from lms.lmsweb import config
88
from lms.models.errors import (
9-
ForbiddenPermission, UnauthorizedError, UnhashedPasswordError,
9+
AlreadyExists, ForbiddenPermission, UnauthorizedError,
10+
UnhashedPasswordError,
1011
)
1112

1213

@@ -38,3 +39,16 @@ def auth(username: str, password: str) -> User:
3839

3940
def generate_user_token(user: User) -> str:
4041
return SERIALIZER.dumps(user.mail_address, salt=retrieve_salt(user))
42+
43+
44+
def join_public_course(course: Course, user: User) -> None:
45+
__, created = UserCourse.get_or_create(**{
46+
UserCourse.user.name: user, UserCourse.course.name: course,
47+
})
48+
if not created:
49+
raise AlreadyExists(
50+
_(
51+
'You are already registered to %(course_name)s course.',
52+
course_name=course.name,
53+
), 409,
54+
)

lms/static/my.css

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -835,16 +835,18 @@ code .grader-add .fa {
835835
}
836836

837837
/* User's page */
838-
#user {
838+
#user, #public-courses {
839839
text-align: right;
840840
}
841841

842-
#user h1 {
842+
#user h1,
843+
#public-courses h1 {
843844
margin: 5vh 0;
844845
text-align: center;
845846
}
846847

847-
#user .body {
848+
#user .body,
849+
#public-courses .body {
848850
width: 80vw;
849851
margin: auto;
850852
}
@@ -853,7 +855,8 @@ code .grader-add .fa {
853855
width: 80vw;
854856
}
855857

856-
#user .user-actions {
858+
#user .user-actions,
859+
#public-courses .public-courses-links {
857860
margin-bottom: 5em;
858861
}
859862

lms/templates/public-courses.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% extends 'base.html' %}
2+
3+
{% block page_content %}
4+
<div id="page-public-courses">
5+
<div id="public-courses" class="{{ direction }}">
6+
<h1>{{ _('Public Courses List') }}</h1>
7+
<div class="body">
8+
<div class="public-courses-links {{ direction }}">
9+
<ul>
10+
{% for course in courses %}
11+
<li><a href="{{ url_for('join_public_course', course_id=course.id) }}" role="button">{{ course.name | e }}</a></li>
12+
{% endfor %}
13+
</ul>
14+
</div>
15+
</div>
16+
</div>
17+
</div>
18+
19+
{% endblock %}

lms/templates/user.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ <h2>{{ _('Actions') }}:</h2>
1717
<div id="change-password-user">
1818
<ul>
1919
<li><a href="{{ url_for('change_password') }}" role="button">{{ _('Change Password') }}</a></li>
20+
{% if public_course_exists %}
21+
<li><a href="{{ url_for('public_courses') }}" role="button">{{ _('Join Courses') }}</a></li>
22+
{% endif %}
2023
</ul>
2124
</div>
2225
</div>

tests/test_registration.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from flask.testing import FlaskClient
55

66
from lms.lmsweb.config import CONFIRMATION_TIME
7-
from lms.lmsdb.models import User
7+
from lms.lmsdb.models import Course, User
88
from lms.models.users import generate_user_token
99
from tests import conftest
1010

@@ -146,3 +146,24 @@ def test_registartion_closed(client: FlaskClient, captured_templates):
146146
template, _ = captured_templates[-1]
147147
assert template.name == 'login.html'
148148
assert '/signup' not in response.get_data(as_text=True)
149+
150+
@staticmethod
151+
def test_register_public_course(
152+
student_user: User, course: Course, captured_templates,
153+
):
154+
client = conftest.get_logged_user(username=student_user.username)
155+
not_public_course_response = client.get(f'/course/join/{course.id}')
156+
assert not_public_course_response.status_code == 403
157+
158+
unknown_course_response = client.get('/course/join/123456')
159+
assert unknown_course_response.status_code == 404
160+
161+
course.is_public = True
162+
course.save()
163+
course = Course.get_by_id(course.id)
164+
client.get(f'/course/join/{course.id}')
165+
template, _ = captured_templates[-1]
166+
assert template.name == 'exercises.html'
167+
168+
already_registered_response = client.get(f'/course/join/{course.id}')
169+
assert already_registered_response.status_code == 409

0 commit comments

Comments
 (0)