Skip to content

Commit 4fb3dcd

Browse files
committed
fixed merge conflicts
2 parents 4d4755d + f18eeca commit 4fb3dcd

File tree

17 files changed

+487
-112
lines changed

17 files changed

+487
-112
lines changed

lms/lmsdb/bootstrap.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,27 @@ def _add_api_keys_to_users_table(table: Model, _column: Field) -> None:
226226
user.save()
227227

228228

229+
def _add_course_and_numbers_to_exercises_table(
230+
table: Model, course: models.Course,
231+
) -> None:
232+
log.info(
233+
'Adding Course, Numbers for exercises, might take some extra time...',
234+
)
235+
with db_config.database.transaction():
236+
for exercise in table:
237+
exercise.number = exercise.id
238+
exercise.course = course
239+
exercise.save()
240+
241+
242+
def _create_usercourses_objects(table: Model, course: models.Course) -> None:
243+
log.info('Adding UserCourse for all users, might take some extra time...')
244+
UserCourse = models.UserCourse
245+
with db_config.database.transaction():
246+
for user in table:
247+
UserCourse.create(user=user, course=course)
248+
249+
229250
def _add_uuid_to_users_table(table: Model, _column: Field) -> None:
230251
log.info('Adding UUIDs for all users, might take some extra time...')
231252
with db_config.database.transaction():
@@ -240,6 +261,32 @@ def _api_keys_migration() -> bool:
240261
return True
241262

242263

264+
def _last_course_viewed_migration() -> bool:
265+
User = models.User
266+
_add_not_null_column(User, User.last_course_viewed)
267+
return True
268+
269+
270+
def _exercise_course_migration(course: models.Course) -> bool:
271+
Exercise = models.Exercise
272+
_create_usercourses_objects(models.User, course)
273+
_add_course_and_numbers_to_exercises_table(Exercise, course)
274+
return True
275+
276+
277+
def _add_exercise_course_id_and_number_columns_constraint() -> bool:
278+
Exercise = models.Exercise
279+
migrator = db_config.get_migrator_instance()
280+
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:
284+
migrate(
285+
migrator.add_index('exercise', ('course_id', 'number'), True),
286+
)
287+
db_config.database.commit()
288+
289+
243290
def _last_status_view_migration() -> bool:
244291
Solution = models.Solution
245292
_migrate_column_in_table_if_needed(Solution, Solution.last_status_view)
@@ -261,12 +308,16 @@ def _assessment_migration() -> bool:
261308

262309
def main():
263310
with models.database.connection_context():
311+
if models.database.table_exists(models.Exercise.__name__.lower()):
312+
_add_exercise_course_id_and_number_columns_constraint()
313+
264314
if models.database.table_exists(models.Solution.__name__.lower()):
265315
_last_status_view_migration()
266316
_assessment_migration()
267317

268318
if models.database.table_exists(models.User.__name__.lower()):
269319
_api_keys_migration()
320+
_last_course_viewed_migration()
270321
_uuid_migration()
271322

272323
models.database.create_tables(models.ALL_MODELS, safe=True)
@@ -277,6 +328,9 @@ def main():
277328
models.create_demo_users()
278329
if models.SolutionAssessment.select().count() == 0:
279330
models.create_basic_assessments()
331+
if models.Course.select().count() == 0:
332+
course = models.create_basic_course()
333+
_exercise_course_migration(course)
280334

281335
text_fixer.fix_texts()
282336
import_tests.load_tests_from_path('/app_dir/notebooks-tests')

lms/lmsdb/models.py

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from collections import Counter
66
from datetime import datetime
77
from typing import (
8-
Any, Dict, Iterable, List, Optional, TYPE_CHECKING, Tuple,
9-
Type, Union, cast,
8+
Any, Dict, Iterable, List, Optional,
9+
TYPE_CHECKING, Tuple, Type, Union, cast,
1010
)
1111
from uuid import uuid4
1212

@@ -134,21 +134,50 @@ def is_viewer(self) -> bool:
134134
return self.name == RoleOptions.VIEWER.value or self.is_manager
135135

136136

137+
class Course(BaseModel):
138+
name = CharField(unique=True)
139+
date = DateTimeField(default=datetime.now)
140+
end_date = DateTimeField(null=True)
141+
close_registration_date = DateTimeField(default=datetime.now)
142+
invite_code = CharField(null=True)
143+
is_public = BooleanField(default=False)
144+
145+
def has_user(self, user_id: int) -> bool:
146+
return UserCourse.is_user_registered(user_id, self)
147+
148+
@classmethod
149+
def fetch(cls, user: 'User') -> Iterable['Course']:
150+
return (
151+
cls
152+
.select()
153+
.join(UserCourse)
154+
.where(UserCourse.user == user.id)
155+
.order_by(Course.name.desc())
156+
)
157+
158+
def __str__(self):
159+
return f'{self.name}: {self.date} - {self.end_date}'
160+
161+
137162
class User(UserMixin, BaseModel):
138163
username = CharField(unique=True)
139164
fullname = CharField()
140165
mail_address = CharField(unique=True)
141166
password = CharField()
142167
role = ForeignKeyField(Role, backref='users')
143168
api_key = CharField()
169+
last_course_viewed = ForeignKeyField(Course, null=True)
144170
uuid = UUIDField(default=uuid4, unique=True)
145171

146172
def get_id(self):
147173
return str(self.uuid)
148174

149-
def is_password_valid(self, password):
175+
def is_password_valid(self, password) -> bool:
150176
return check_password_hash(self.password, password)
151177

178+
def has_course(self, course_id: int) -> bool:
179+
return UserCourse.is_user_registered(self, course_id)
180+
152181
@classmethod
153182
def get_system_user(cls) -> 'User':
154183
instance, _ = cls.get_or_create(**{
@@ -170,6 +199,9 @@ def random_password(cls, stronger: bool = False) -> str:
170199
def get_notifications(self) -> Iterable['Notification']:
171200
return Notification.fetch(self)
172201

202+
def get_courses(self) -> Iterable['Course']:
203+
return Course.fetch(self)
204+
173205
def notes(self) -> Iterable['Note']:
174206
fields = (
175207
Note.id, Note.creator.fullname, CommentText.text,
@@ -217,6 +249,24 @@ def on_save_handler(model_class, instance, created):
217249
instance.api_key = generate_password_hash(instance.api_key)
218250

219251

252+
class UserCourse(BaseModel):
253+
user = ForeignKeyField(User, backref='usercourses')
254+
course = ForeignKeyField(Course, backref='usercourses')
255+
date = DateTimeField(default=datetime.now)
256+
257+
@classmethod
258+
def is_user_registered(cls, user_id: int, course_id: int) -> bool:
259+
return (
260+
cls.
261+
select()
262+
.where(
263+
cls.user == user_id,
264+
cls.course == course_id,
265+
)
266+
.exists()
267+
)
268+
269+
220270
class Notification(BaseModel):
221271
ID_FIELD_NAME = 'id'
222272
MAX_PER_USER = 10
@@ -306,15 +356,46 @@ class Exercise(BaseModel):
306356
due_date = DateTimeField(null=True)
307357
notebook_num = IntegerField(default=0)
308358
order = IntegerField(default=0, index=True)
359+
course = ForeignKeyField(Course, backref='exercise')
360+
number = IntegerField(default=1)
361+
362+
class Meta:
363+
indexes = (
364+
(('course_id', 'number'), True),
365+
)
309366

310367
def open_for_new_solutions(self) -> bool:
311368
if self.due_date is None:
312369
return not self.is_archived
313370
return datetime.now() < self.due_date and not self.is_archived
314371

315372
@classmethod
316-
def get_objects(cls, fetch_archived: bool = False):
317-
exercises = cls.select().order_by(Exercise.order)
373+
def get_highest_number(cls):
374+
return cls.select(fn.MAX(cls.number)).scalar()
375+
376+
@classmethod
377+
def is_number_exists(cls, number: int) -> bool:
378+
return cls.select().where(cls.number == number).exists()
379+
380+
@classmethod
381+
def get_objects(
382+
cls, user_id: int, fetch_archived: bool = False,
383+
from_all_courses: bool = False,
384+
):
385+
user = User.get(User.id == user_id)
386+
exercises = (
387+
cls
388+
.select()
389+
.join(Course)
390+
.join(UserCourse)
391+
.where(UserCourse.user == user_id)
392+
.switch()
393+
.order_by(UserCourse.date, Exercise.number, Exercise.order)
394+
)
395+
if not from_all_courses:
396+
exercises = exercises.where(
397+
UserCourse.course == user.last_course_viewed,
398+
)
318399
if not fetch_archived:
319400
exercises = exercises.where(cls.is_archived == False) # NOQA: E712
320401
return exercises
@@ -326,6 +407,9 @@ def as_dict(self) -> Dict[str, Any]:
326407
'is_archived': self.is_archived,
327408
'notebook': self.notebook_num,
328409
'due_date': self.due_date,
410+
'exercise_number': self.number,
411+
'course_id': self.course.id,
412+
'course_name': self.course.name,
329413
}
330414

331415
@staticmethod
@@ -336,6 +420,14 @@ def __str__(self):
336420
return self.subject
337421

338422

423+
@pre_save(sender=Exercise)
424+
def exercise_number_save_handler(model_class, instance, created):
425+
"""Change the exercise number to the highest consecutive number."""
426+
427+
if model_class.is_number_exists(instance.number):
428+
instance.number = model_class.get_highest_number() + 1
429+
430+
339431
class SolutionState(enum.Enum):
340432
CREATED = 'Created'
341433
IN_CHECKING = 'In checking'
@@ -509,10 +601,13 @@ def test_results(self) -> Iterable[dict]:
509601
@classmethod
510602
def of_user(
511603
cls, user_id: int, with_archived: bool = False,
604+
from_all_courses: bool = False,
512605
) -> Iterable[Dict[str, Any]]:
513-
db_exercises = Exercise.get_objects(fetch_archived=with_archived)
606+
db_exercises = Exercise.get_objects(
607+
user_id=user_id, fetch_archived=with_archived,
608+
from_all_courses=from_all_courses,
609+
)
514610
exercises = Exercise.as_dicts(db_exercises)
515-
516611
solutions = (
517612
cls
518613
.select(
@@ -994,7 +1089,7 @@ def generate_string(
9941089
return ''.join(password)
9951090

9961091

997-
def create_demo_users():
1092+
def create_demo_users() -> None:
9981093
print('First run! Here are some users to get start with:') # noqa: T001
9991094
fields = ['username', 'fullname', 'mail_address', 'role']
10001095
student_role = Role.by_name('Student')
@@ -1012,12 +1107,12 @@ def create_demo_users():
10121107
print(f"User: {user['username']}, Password: {password}") # noqa: T001
10131108

10141109

1015-
def create_basic_roles():
1110+
def create_basic_roles() -> None:
10161111
for role in RoleOptions:
10171112
Role.create(name=role.value)
10181113

10191114

1020-
def create_basic_assessments():
1115+
def create_basic_assessments() -> None:
10211116
assessments_dict = {
10221117
'Excellent': {'color': 'green', 'icon': 'star', 'order': 1},
10231118
'Nice': {'color': 'blue', 'icon': 'check', 'order': 2},
@@ -1033,4 +1128,8 @@ def create_basic_assessments():
10331128
)
10341129

10351130

1131+
def create_basic_course() -> Course:
1132+
return Course.create(name='Python Course', date=datetime.now())
1133+
1134+
10361135
ALL_MODELS = BaseModel.__subclasses__()

0 commit comments

Comments
 (0)