55from collections import Counter
66from datetime import datetime
77from 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)
1111from uuid import uuid4
1212
@@ -132,21 +132,50 @@ def is_viewer(self) -> bool:
132132 return self .name == RoleOptions .VIEWER .value or self .is_manager
133133
134134
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+
135160class User (UserMixin , BaseModel ):
136161 username = CharField (unique = True )
137162 fullname = CharField ()
138163 mail_address = CharField (unique = True )
139164 password = CharField ()
140165 role = ForeignKeyField (Role , backref = 'users' )
141166 api_key = CharField ()
167+ last_course_viewed = ForeignKeyField (Course , null = True )
142168 uuid = UUIDField (default = uuid4 , unique = True )
143169
144170 def get_id (self ):
145171 return str (self .uuid )
146172
147- def is_password_valid (self , password ):
173+ def is_password_valid (self , password ) -> bool :
148174 return check_password_hash (self .password , password )
149175
176+ def has_course (self , course_id : int ) -> bool :
177+ return UserCourse .is_user_registered (self , course_id )
178+
150179 @classmethod
151180 def get_system_user (cls ) -> 'User' :
152181 instance , _ = cls .get_or_create (** {
@@ -168,6 +197,9 @@ def random_password(cls, stronger: bool = False) -> str:
168197 def get_notifications (self ) -> Iterable ['Notification' ]:
169198 return Notification .fetch (self )
170199
200+ def get_courses (self ) -> Iterable ['Course' ]:
201+ return Course .fetch (self )
202+
171203 def notes (self ) -> Iterable ['Note' ]:
172204 fields = (
173205 Note .id , Note .creator .fullname , CommentText .text ,
@@ -215,6 +247,24 @@ def on_save_handler(model_class, instance, created):
215247 instance .api_key = generate_password_hash (instance .api_key )
216248
217249
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+
218268class Notification (BaseModel ):
219269 ID_FIELD_NAME = 'id'
220270 MAX_PER_USER = 10
@@ -304,15 +354,46 @@ class Exercise(BaseModel):
304354 due_date = DateTimeField (null = True )
305355 notebook_num = IntegerField (default = 0 )
306356 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+ )
307364
308365 def open_for_new_solutions (self ) -> bool :
309366 if self .due_date is None :
310367 return not self .is_archived
311368 return datetime .now () < self .due_date and not self .is_archived
312369
313370 @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+ )
316397 if not fetch_archived :
317398 exercises = exercises .where (cls .is_archived == False ) # NOQA: E712
318399 return exercises
@@ -324,6 +405,9 @@ def as_dict(self) -> Dict[str, Any]:
324405 'is_archived' : self .is_archived ,
325406 'notebook' : self .notebook_num ,
326407 'due_date' : self .due_date ,
408+ 'exercise_number' : self .number ,
409+ 'course_id' : self .course .id ,
410+ 'course_name' : self .course .name ,
327411 }
328412
329413 @staticmethod
@@ -334,6 +418,14 @@ def __str__(self):
334418 return self .subject
335419
336420
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+
337429class SolutionState (enum .Enum ):
338430 CREATED = 'Created'
339431 IN_CHECKING = 'In checking'
@@ -469,10 +561,13 @@ def test_results(self) -> Iterable[dict]:
469561 @classmethod
470562 def of_user (
471563 cls , user_id : int , with_archived : bool = False ,
564+ from_all_courses : bool = False ,
472565 ) -> 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+ )
474570 exercises = Exercise .as_dicts (db_exercises )
475-
476571 solutions = (
477572 cls
478573 .select (cls .exercise , cls .id , cls .state , cls .checker )
@@ -946,7 +1041,7 @@ def generate_string(
9461041 return '' .join (password )
9471042
9481043
949- def create_demo_users ():
1044+ def create_demo_users () -> None :
9501045 print ('First run! Here are some users to get start with:' ) # noqa: T001
9511046 fields = ['username' , 'fullname' , 'mail_address' , 'role' ]
9521047 student_role = Role .by_name ('Student' )
@@ -964,9 +1059,13 @@ def create_demo_users():
9641059 print (f"User: { user ['username' ]} , Password: { password } " ) # noqa: T001
9651060
9661061
967- def create_basic_roles ():
1062+ def create_basic_roles () -> None :
9681063 for role in RoleOptions :
9691064 Role .create (name = role .value )
9701065
9711066
1067+ def create_basic_course () -> Course :
1068+ return Course .create (name = 'Python Course' , date = datetime .now ())
1069+
1070+
9721071ALL_MODELS = BaseModel .__subclasses__ ()
0 commit comments