From 00bc2b7e86ab1c5b94024aa53511345f19289358 Mon Sep 17 00:00:00 2001 From: bebra_dev Date: Fri, 24 Jan 2025 20:19:12 +0300 Subject: [PATCH 1/5] chore: banned students, no training display for not allowed students --- adminpage/api/crud/crud_training.py | 5 +++- adminpage/sport/admin/groupAdmin.py | 2 ++ ...d_students_alter_group_allowed_students.py | 23 +++++++++++++++++++ adminpage/sport/models/group.py | 13 ++++++++++- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 adminpage/sport/migrations/0126_group_banned_students_alter_group_allowed_students.py diff --git a/adminpage/api/crud/crud_training.py b/adminpage/api/crud/crud_training.py index cffa0af5..f8a139dd 100644 --- a/adminpage/api/crud/crud_training.py +++ b/adminpage/api/crud/crud_training.py @@ -75,6 +75,7 @@ def can_check_in( free_places = training.group.capacity - training.checkins.count() allowed_medical_groups = training.group.allowed_medical_groups.all() allowed_students = training.group.allowed_students.all() + banned_students = training.group.banned_students.all() # All conditions must be True for the student to be able to check in. result = ( @@ -83,6 +84,7 @@ def can_check_in( (total_hours + training.academic_duration) <= 4 and (same_type_hours + training.academic_duration) <= 2 and (student.medical_group in allowed_medical_groups or student in allowed_students) and + student not in banned_students and training.group.allowed_gender in (student.gender, -1) ) @@ -101,8 +103,9 @@ def get_trainings_for_student(student: Student, start: datetime, end: datetime): trainings = Training.objects.filter( Q(start__range=(start, end)) | Q(end__range=(start, end)) | (Q(start__lte=start) & Q(end__gte=end)), ~Q(group__sport=None), + Q(group__allowed_medical_groups=student.medical_group) | Q(group__allowed_students=student.pk), group__semester=semester_id, - ).prefetch_related( + ).exclude(group__banned_students=student.pk).prefetch_related( group_prefetch, "training_class", "checkins", diff --git a/adminpage/sport/admin/groupAdmin.py b/adminpage/sport/admin/groupAdmin.py index 287e3b7a..88fc8e5d 100644 --- a/adminpage/sport/admin/groupAdmin.py +++ b/adminpage/sport/admin/groupAdmin.py @@ -92,6 +92,7 @@ class GroupAdmin(DefaultFilterMixIn): # "trainer", 'trainers', 'allowed_students', + 'banned_students', ) list_filter = ( @@ -130,6 +131,7 @@ class GroupAdmin(DefaultFilterMixIn): "allowed_medical_groups", "allowed_gender", "allowed_students", + "banned_students", ) def get_changeform_initial_data(self, request): diff --git a/adminpage/sport/migrations/0126_group_banned_students_alter_group_allowed_students.py b/adminpage/sport/migrations/0126_group_banned_students_alter_group_allowed_students.py new file mode 100644 index 00000000..70152d9e --- /dev/null +++ b/adminpage/sport/migrations/0126_group_banned_students_alter_group_allowed_students.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.3 on 2025-01-24 16:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sport', '0125_alter_group_allowed_students'), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='banned_students', + field=models.ManyToManyField(blank=True, help_text='List of students that can not attent classes', related_name='banned_groups', to='sport.student'), + ), + migrations.AlterField( + model_name='group', + name='allowed_students', + field=models.ManyToManyField(blank=True, help_text='List of students that are allowed to attend classes', related_name='allowed_groups', to='sport.student'), + ), + ] diff --git a/adminpage/sport/models/group.py b/adminpage/sport/models/group.py index a410bc64..de8e68b2 100644 --- a/adminpage/sport/models/group.py +++ b/adminpage/sport/models/group.py @@ -26,7 +26,18 @@ class Group(models.Model): default=-1, verbose_name="Is a QR required?" ) - allowed_students = models.ManyToManyField('Student', related_name='allowed_groups', blank=True) + allowed_students = models.ManyToManyField( + 'Student', + related_name='allowed_groups', + blank=True, + help_text='List of students that are allowed to attend classes' + ) + banned_students = models.ManyToManyField( + 'Student', + related_name='banned_groups', + blank=True, + help_text='List of students that can not attent classes' + ) class Meta: db_table = "group" From ef2fc1bd3d332a3ff9e6291db6b426851c356961 Mon Sep 17 00:00:00 2001 From: bebra_dev Date: Sat, 25 Jan 2025 12:38:54 +0300 Subject: [PATCH 2/5] chore: mark attendance only for allowed and not banned students --- adminpage/api/crud/crud_users.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/adminpage/api/crud/crud_users.py b/adminpage/api/crud/crud_users.py index 984a18e7..349a0fe5 100644 --- a/adminpage/api/crud/crud_users.py +++ b/adminpage/api/crud/crud_users.py @@ -43,9 +43,6 @@ def get_email_name_like_students(pattern: str, limit: int = 5, requirement=~Q(pk def get_email_name_like_students_filtered_by_group(pattern: str, limit: int = 5, group=None): group = Group.objects.get(id=group) - - medical_group_condition = Q(pk=None) - for medical_group in group.allowed_medical_groups.all(): - medical_group_condition = medical_group_condition | Q(medical_group__id=medical_group.id) - - return get_email_name_like_students(pattern, limit, medical_group_condition) + not_banned_condition = ~Q(pk__in=group.banned_students.values_list('id', flat=True)) + allowed_condition = Q(pk__in=group.allowed_students.values_list('id', flat=True)) | Q(medical_group__in=group.allowed_medical_groups.all()) + return get_email_name_like_students(pattern, limit, not_banned_condition & allowed_condition) From 342334b3db271919c8791ea6eb772163988eee93 Mon Sep 17 00:00:00 2001 From: Artem Bulgakov Date: Mon, 27 Jan 2025 23:51:57 +0300 Subject: [PATCH 3/5] chore: do not check students gender when they are in 'Allowed students' list; add more comments about logic --- adminpage/api/crud/crud_training.py | 45 ++++++++++++++++++++++------- adminpage/api/crud/crud_users.py | 2 ++ adminpage/sport/models/group.py | 11 +++++-- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/adminpage/api/crud/crud_training.py b/adminpage/api/crud/crud_training.py index f8a139dd..0c273a82 100644 --- a/adminpage/api/crud/crud_training.py +++ b/adminpage/api/crud/crud_training.py @@ -79,13 +79,23 @@ def can_check_in( # All conditions must be True for the student to be able to check in. result = ( + # The training must have free places left free_places > 0 and + # The training must not be finished yet, and you can check in only during 1 week before the training start training.start < (time_now + _week_delta) and time_now < training.end and + # The student can only get 4 hours at one day (total_hours + training.academic_duration) <= 4 and + # The student can only get 2 hours at one day for the same sport type (same_type_hours + training.academic_duration) <= 2 and - (student.medical_group in allowed_medical_groups or student in allowed_students) and + # Students in "Banned students" list are always prohibited student not in banned_students and - training.group.allowed_gender in (student.gender, -1) + ( + # Students in "Allowed students" list can check in, no matter their medical group or gender + student in allowed_students or + # Other students must be of allowed medical groups and allowed gender + (student.medical_group in allowed_medical_groups and + training.group.allowed_gender in (student.gender, -1)) + ) ) return result @@ -100,15 +110,28 @@ def get_trainings_for_student(student: Student, start: datetime, end: datetime): # Assuming TrainingCheckIn model has a 'student' and 'training' foreign key. # And Training has a 'group' foreign key with an 'allowed_medical_groups' many-to-many field. semester_id = get_ongoing_semester().id - trainings = Training.objects.filter( - Q(start__range=(start, end)) | Q(end__range=(start, end)) | (Q(start__lte=start) & Q(end__gte=end)), - ~Q(group__sport=None), - Q(group__allowed_medical_groups=student.medical_group) | Q(group__allowed_students=student.pk), - group__semester=semester_id, - ).exclude(group__banned_students=student.pk).prefetch_related( - group_prefetch, - "training_class", - "checkins", + trainings = ( + Training.objects.filter( + # Filter by requested time range + Q(start__range=(start, end)) + | Q(end__range=(start, end)) + | (Q(start__lte=start) & Q(end__gte=end)), + # Do not show 'Self training', 'Extra sport events', 'Medical leave', etc. trainings + ~Q(group__sport=None), + # The student must either have acceptable medical group + Q(group__allowed_medical_groups=student.medical_group) + # ... or be in 'Allowed students' list + | Q(group__allowed_students=student.pk), + # Show only for current semester + group__semester=semester_id, + ) + # Do not show the training if a student is in 'Banned students' list + .exclude(group__banned_students=student.pk) + .prefetch_related( + group_prefetch, + "training_class", + "checkins", + ) ) # get all student check-ins for the given time range diff --git a/adminpage/api/crud/crud_users.py b/adminpage/api/crud/crud_users.py index 349a0fe5..3b5e8655 100644 --- a/adminpage/api/crud/crud_users.py +++ b/adminpage/api/crud/crud_users.py @@ -43,6 +43,8 @@ def get_email_name_like_students(pattern: str, limit: int = 5, requirement=~Q(pk def get_email_name_like_students_filtered_by_group(pattern: str, limit: int = 5, group=None): group = Group.objects.get(id=group) + # Don't suggest the student that is in 'Banned students' list not_banned_condition = ~Q(pk__in=group.banned_students.values_list('id', flat=True)) + # The student must either be in 'Allowed students' list, or have acceptable medical group allowed_condition = Q(pk__in=group.allowed_students.values_list('id', flat=True)) | Q(medical_group__in=group.allowed_medical_groups.all()) return get_email_name_like_students(pattern, limit, not_banned_condition & allowed_condition) diff --git a/adminpage/sport/models/group.py b/adminpage/sport/models/group.py index de8e68b2..49dc52a5 100644 --- a/adminpage/sport/models/group.py +++ b/adminpage/sport/models/group.py @@ -16,10 +16,15 @@ class Group(models.Model): accredited = models.BooleanField(default=True, null=False) # minimum_medical_group = models.ForeignKey('MedicalGroup', on_delete=models.DO_NOTHING, null=True, blank=True) - allowed_medical_groups = models.ManyToManyField('MedicalGroup', blank=True) + allowed_medical_groups = models.ManyToManyField( + 'MedicalGroup', + blank=True, + help_text='Select medical groups required to attend the trainings. If this is empty, nobody will see the training (except students in "Allowed students" list, they always see the training).' + ) allowed_gender = models.IntegerField( choices=GenderInFTGrading.choices, default=GenderInFTGrading.BOTH, + help_text='Select genders that are allowed to attend the trainings (works with "Allowed medical groups" filter).' ) allowed_qr = models.IntegerField( choices=GroupQR.choices, @@ -30,13 +35,13 @@ class Group(models.Model): 'Student', related_name='allowed_groups', blank=True, - help_text='List of students that are allowed to attend classes' + help_text='List of students that are allowed to attend classes (in addition to "Allowed medical groups" filter).' ) banned_students = models.ManyToManyField( 'Student', related_name='banned_groups', blank=True, - help_text='List of students that can not attent classes' + help_text='List of students that can not attend classes. The students will not see the training, and the teacher will not be able to set hours for these students.' ) class Meta: From 1da3ac65060d5478c49f0912133db3334bc63bd8 Mon Sep 17 00:00:00 2001 From: Artem Bulgakov Date: Mon, 27 Jan 2025 23:52:20 +0300 Subject: [PATCH 4/5] ci: fix migrations in deploy workflow --- .github/workflows/deploy_production.yaml | 2 +- .github/workflows/deploy_staging.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy_production.yaml b/.github/workflows/deploy_production.yaml index 032200ab..f8c7e232 100644 --- a/.github/workflows/deploy_production.yaml +++ b/.github/workflows/deploy_production.yaml @@ -30,7 +30,7 @@ jobs: docker compose -f docker-compose.prod.yaml down docker compose -f docker-compose.prod.yaml up -d docker compose -f docker-compose.prod.yaml exec -T adminpanel bash -c "while ! Date: Tue, 28 Jan 2025 00:43:17 +0300 Subject: [PATCH 5/5] fix: autocomplete when trainer marks attendance --- adminpage/api/crud/crud_users.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/adminpage/api/crud/crud_users.py b/adminpage/api/crud/crud_users.py index 3b5e8655..0d1aca70 100644 --- a/adminpage/api/crud/crud_users.py +++ b/adminpage/api/crud/crud_users.py @@ -37,14 +37,13 @@ def get_email_name_like_students(pattern: str, limit: int = 5, requirement=~Q(pk 'medical_group__name', 'gender' )[:limit] - return list(query) def get_email_name_like_students_filtered_by_group(pattern: str, limit: int = 5, group=None): group = Group.objects.get(id=group) # Don't suggest the student that is in 'Banned students' list - not_banned_condition = ~Q(pk__in=group.banned_students.values_list('id', flat=True)) + not_banned_condition = ~Q(user_id__in=group.banned_students.values_list('user_id', flat=True)) # The student must either be in 'Allowed students' list, or have acceptable medical group - allowed_condition = Q(pk__in=group.allowed_students.values_list('id', flat=True)) | Q(medical_group__in=group.allowed_medical_groups.all()) + allowed_condition = Q(user_id__in=group.allowed_students.values_list('user_id', flat=True)) | Q(medical_group__in=group.allowed_medical_groups.all()) return get_email_name_like_students(pattern, limit, not_banned_condition & allowed_condition)