5
5
from collections import Counter
6
6
from datetime import datetime
7
7
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 ,
10
10
)
11
11
from uuid import uuid4
12
12
@@ -134,21 +134,50 @@ def is_viewer(self) -> bool:
134
134
return self .name == RoleOptions .VIEWER .value or self .is_manager
135
135
136
136
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
+
137
162
class User (UserMixin , BaseModel ):
138
163
username = CharField (unique = True )
139
164
fullname = CharField ()
140
165
mail_address = CharField (unique = True )
141
166
password = CharField ()
142
167
role = ForeignKeyField (Role , backref = 'users' )
143
168
api_key = CharField ()
169
+ last_course_viewed = ForeignKeyField (Course , null = True )
144
170
uuid = UUIDField (default = uuid4 , unique = True )
145
171
146
172
def get_id (self ):
147
173
return str (self .uuid )
148
174
149
- def is_password_valid (self , password ):
175
+ def is_password_valid (self , password ) -> bool :
150
176
return check_password_hash (self .password , password )
151
177
178
+ def has_course (self , course_id : int ) -> bool :
179
+ return UserCourse .is_user_registered (self , course_id )
180
+
152
181
@classmethod
153
182
def get_system_user (cls ) -> 'User' :
154
183
instance , _ = cls .get_or_create (** {
@@ -170,6 +199,9 @@ def random_password(cls, stronger: bool = False) -> str:
170
199
def get_notifications (self ) -> Iterable ['Notification' ]:
171
200
return Notification .fetch (self )
172
201
202
+ def get_courses (self ) -> Iterable ['Course' ]:
203
+ return Course .fetch (self )
204
+
173
205
def notes (self ) -> Iterable ['Note' ]:
174
206
fields = (
175
207
Note .id , Note .creator .fullname , CommentText .text ,
@@ -217,6 +249,24 @@ def on_save_handler(model_class, instance, created):
217
249
instance .api_key = generate_password_hash (instance .api_key )
218
250
219
251
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
+
220
270
class Notification (BaseModel ):
221
271
ID_FIELD_NAME = 'id'
222
272
MAX_PER_USER = 10
@@ -306,15 +356,46 @@ class Exercise(BaseModel):
306
356
due_date = DateTimeField (null = True )
307
357
notebook_num = IntegerField (default = 0 )
308
358
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
+ )
309
366
310
367
def open_for_new_solutions (self ) -> bool :
311
368
if self .due_date is None :
312
369
return not self .is_archived
313
370
return datetime .now () < self .due_date and not self .is_archived
314
371
315
372
@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
+ )
318
399
if not fetch_archived :
319
400
exercises = exercises .where (cls .is_archived == False ) # NOQA: E712
320
401
return exercises
@@ -326,6 +407,9 @@ def as_dict(self) -> Dict[str, Any]:
326
407
'is_archived' : self .is_archived ,
327
408
'notebook' : self .notebook_num ,
328
409
'due_date' : self .due_date ,
410
+ 'exercise_number' : self .number ,
411
+ 'course_id' : self .course .id ,
412
+ 'course_name' : self .course .name ,
329
413
}
330
414
331
415
@staticmethod
@@ -336,6 +420,14 @@ def __str__(self):
336
420
return self .subject
337
421
338
422
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
+
339
431
class SolutionState (enum .Enum ):
340
432
CREATED = 'Created'
341
433
IN_CHECKING = 'In checking'
@@ -509,10 +601,13 @@ def test_results(self) -> Iterable[dict]:
509
601
@classmethod
510
602
def of_user (
511
603
cls , user_id : int , with_archived : bool = False ,
604
+ from_all_courses : bool = False ,
512
605
) -> 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
+ )
514
610
exercises = Exercise .as_dicts (db_exercises )
515
-
516
611
solutions = (
517
612
cls
518
613
.select (
@@ -994,7 +1089,7 @@ def generate_string(
994
1089
return '' .join (password )
995
1090
996
1091
997
- def create_demo_users ():
1092
+ def create_demo_users () -> None :
998
1093
print ('First run! Here are some users to get start with:' ) # noqa: T001
999
1094
fields = ['username' , 'fullname' , 'mail_address' , 'role' ]
1000
1095
student_role = Role .by_name ('Student' )
@@ -1012,12 +1107,12 @@ def create_demo_users():
1012
1107
print (f"User: { user ['username' ]} , Password: { password } " ) # noqa: T001
1013
1108
1014
1109
1015
- def create_basic_roles ():
1110
+ def create_basic_roles () -> None :
1016
1111
for role in RoleOptions :
1017
1112
Role .create (name = role .value )
1018
1113
1019
1114
1020
- def create_basic_assessments ():
1115
+ def create_basic_assessments () -> None :
1021
1116
assessments_dict = {
1022
1117
'Excellent' : {'color' : 'green' , 'icon' : 'star' , 'order' : 1 },
1023
1118
'Nice' : {'color' : 'blue' , 'icon' : 'check' , 'order' : 2 },
@@ -1033,4 +1128,8 @@ def create_basic_assessments():
1033
1128
)
1034
1129
1035
1130
1131
+ def create_basic_course () -> Course :
1132
+ return Course .create (name = 'Python Course' , date = datetime .now ())
1133
+
1134
+
1036
1135
ALL_MODELS = BaseModel .__subclasses__ ()
0 commit comments