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
@@ -132,21 +132,50 @@ def is_viewer(self) -> bool:
132
132
return self .name == RoleOptions .VIEWER .value or self .is_manager
133
133
134
134
135
+ class Course (BaseModel ):
136
+ name = CharField (unique = True )
137
+ date = DateTimeField (default = datetime .now )
138
+ end_date = DateTimeField (null = True )
139
+ close_registration_date = DateTimeField (default = datetime .now )
140
+ invite_code = CharField (null = True )
141
+ is_public = BooleanField (default = False )
142
+
143
+ def has_user (self , user_id : int ) -> bool :
144
+ return UserCourse .is_user_registered (user_id , self )
145
+
146
+ @classmethod
147
+ def fetch (cls , user : 'User' ) -> Iterable ['Course' ]:
148
+ return (
149
+ cls
150
+ .select ()
151
+ .join (UserCourse )
152
+ .where (UserCourse .user == user .id )
153
+ .order_by (Course .name .desc ())
154
+ )
155
+
156
+ def __str__ (self ):
157
+ return f'{ self .name } : { self .date } - { self .end_date } '
158
+
159
+
135
160
class User (UserMixin , BaseModel ):
136
161
username = CharField (unique = True )
137
162
fullname = CharField ()
138
163
mail_address = CharField (unique = True )
139
164
password = CharField ()
140
165
role = ForeignKeyField (Role , backref = 'users' )
141
166
api_key = CharField ()
167
+ last_course_viewed = ForeignKeyField (Course , null = True )
142
168
uuid = UUIDField (default = uuid4 , unique = True )
143
169
144
170
def get_id (self ):
145
171
return str (self .uuid )
146
172
147
- def is_password_valid (self , password ):
173
+ def is_password_valid (self , password ) -> bool :
148
174
return check_password_hash (self .password , password )
149
175
176
+ def has_course (self , course_id : int ) -> bool :
177
+ return UserCourse .is_user_registered (self , course_id )
178
+
150
179
@classmethod
151
180
def get_system_user (cls ) -> 'User' :
152
181
instance , _ = cls .get_or_create (** {
@@ -168,6 +197,9 @@ def random_password(cls, stronger: bool = False) -> str:
168
197
def get_notifications (self ) -> Iterable ['Notification' ]:
169
198
return Notification .fetch (self )
170
199
200
+ def get_courses (self ) -> Iterable ['Course' ]:
201
+ return Course .fetch (self )
202
+
171
203
def notes (self ) -> Iterable ['Note' ]:
172
204
fields = (
173
205
Note .id , Note .creator .fullname , CommentText .text ,
@@ -215,6 +247,24 @@ def on_save_handler(model_class, instance, created):
215
247
instance .api_key = generate_password_hash (instance .api_key )
216
248
217
249
250
+ class UserCourse (BaseModel ):
251
+ user = ForeignKeyField (User , backref = 'usercourses' )
252
+ course = ForeignKeyField (Course , backref = 'usercourses' )
253
+ date = DateTimeField (default = datetime .now )
254
+
255
+ @classmethod
256
+ def is_user_registered (cls , user_id : int , course_id : int ) -> bool :
257
+ return (
258
+ cls .
259
+ select ()
260
+ .where (
261
+ cls .user == user_id ,
262
+ cls .course == course_id ,
263
+ )
264
+ .exists ()
265
+ )
266
+
267
+
218
268
class Notification (BaseModel ):
219
269
ID_FIELD_NAME = 'id'
220
270
MAX_PER_USER = 10
@@ -304,15 +354,46 @@ class Exercise(BaseModel):
304
354
due_date = DateTimeField (null = True )
305
355
notebook_num = IntegerField (default = 0 )
306
356
order = IntegerField (default = 0 , index = True )
357
+ course = ForeignKeyField (Course , backref = 'exercise' )
358
+ number = IntegerField (default = 1 )
359
+
360
+ class Meta :
361
+ indexes = (
362
+ (('course_id' , 'number' ), True ),
363
+ )
307
364
308
365
def open_for_new_solutions (self ) -> bool :
309
366
if self .due_date is None :
310
367
return not self .is_archived
311
368
return datetime .now () < self .due_date and not self .is_archived
312
369
313
370
@classmethod
314
- def get_objects (cls , fetch_archived : bool = False ):
315
- exercises = cls .select ().order_by (Exercise .order )
371
+ def get_highest_number (cls ):
372
+ return cls .select (fn .MAX (cls .number )).scalar ()
373
+
374
+ @classmethod
375
+ def is_number_exists (cls , number : int ) -> bool :
376
+ return cls .select ().where (cls .number == number ).exists ()
377
+
378
+ @classmethod
379
+ def get_objects (
380
+ cls , user_id : int , fetch_archived : bool = False ,
381
+ from_all_courses : bool = False ,
382
+ ):
383
+ user = User .get (User .id == user_id )
384
+ exercises = (
385
+ cls
386
+ .select ()
387
+ .join (Course )
388
+ .join (UserCourse )
389
+ .where (UserCourse .user == user_id )
390
+ .switch ()
391
+ .order_by (UserCourse .date , Exercise .number , Exercise .order )
392
+ )
393
+ if not from_all_courses :
394
+ exercises = exercises .where (
395
+ UserCourse .course == user .last_course_viewed ,
396
+ )
316
397
if not fetch_archived :
317
398
exercises = exercises .where (cls .is_archived == False ) # NOQA: E712
318
399
return exercises
@@ -324,6 +405,9 @@ def as_dict(self) -> Dict[str, Any]:
324
405
'is_archived' : self .is_archived ,
325
406
'notebook' : self .notebook_num ,
326
407
'due_date' : self .due_date ,
408
+ 'exercise_number' : self .number ,
409
+ 'course_id' : self .course .id ,
410
+ 'course_name' : self .course .name ,
327
411
}
328
412
329
413
@staticmethod
@@ -334,6 +418,14 @@ def __str__(self):
334
418
return self .subject
335
419
336
420
421
+ @pre_save (sender = Exercise )
422
+ def exercise_number_save_handler (model_class , instance , created ):
423
+ """Change the exercise number to the highest consecutive number."""
424
+
425
+ if model_class .is_number_exists (instance .number ):
426
+ instance .number = model_class .get_highest_number () + 1
427
+
428
+
337
429
class SolutionState (enum .Enum ):
338
430
CREATED = 'Created'
339
431
IN_CHECKING = 'In checking'
@@ -469,10 +561,13 @@ def test_results(self) -> Iterable[dict]:
469
561
@classmethod
470
562
def of_user (
471
563
cls , user_id : int , with_archived : bool = False ,
564
+ from_all_courses : bool = False ,
472
565
) -> Iterable [Dict [str , Any ]]:
473
- db_exercises = Exercise .get_objects (fetch_archived = with_archived )
566
+ db_exercises = Exercise .get_objects (
567
+ user_id = user_id , fetch_archived = with_archived ,
568
+ from_all_courses = from_all_courses ,
569
+ )
474
570
exercises = Exercise .as_dicts (db_exercises )
475
-
476
571
solutions = (
477
572
cls
478
573
.select (cls .exercise , cls .id , cls .state , cls .checker )
@@ -946,7 +1041,7 @@ def generate_string(
946
1041
return '' .join (password )
947
1042
948
1043
949
- def create_demo_users ():
1044
+ def create_demo_users () -> None :
950
1045
print ('First run! Here are some users to get start with:' ) # noqa: T001
951
1046
fields = ['username' , 'fullname' , 'mail_address' , 'role' ]
952
1047
student_role = Role .by_name ('Student' )
@@ -964,9 +1059,13 @@ def create_demo_users():
964
1059
print (f"User: { user ['username' ]} , Password: { password } " ) # noqa: T001
965
1060
966
1061
967
- def create_basic_roles ():
1062
+ def create_basic_roles () -> None :
968
1063
for role in RoleOptions :
969
1064
Role .create (name = role .value )
970
1065
971
1066
1067
+ def create_basic_course () -> Course :
1068
+ return Course .create (name = 'Python Course' , date = datetime .now ())
1069
+
1070
+
972
1071
ALL_MODELS = BaseModel .__subclasses__ ()
0 commit comments