From 26e5e436606664112891a8e6af569abb0a1228e2 Mon Sep 17 00:00:00 2001 From: Ewout Verlinde Date: Tue, 9 Apr 2024 15:06:42 +0200 Subject: [PATCH] Role management (#260) * chore: add make teacher script * fix: added psychology faculty icon * fix: faculty image filter * chore: linting * feat: role user mixin * Calendar view improvements (#257) * chore: add make teacher script * fix: added psychology faculty icon --------- Co-authored-by: tyboro2002 * chore: user permissions account for is_active * chore: added role management tests, extracted common logic in helpers * fix: student service parameter * chore: linting * chore: removed comment in seeder --------- Co-authored-by: tyboro2002 --- backend/.gitignore | 1 + backend/api/fixtures/groups.yaml | 2 - .../commands/{makeAdmin.py => make_admin.py} | 0 .../api/management/commands/make_teacher.py | 23 ++ .../commands/{seedDb.py => seed_db.py} | 162 +++--------- ...xtrachecksresult_error_message_and_more.py | 29 +++ .../api/migrations/0013_student_is_active.py | 18 ++ ...4_assistant_is_active_teacher_is_active.py | 30 +++ ...4_assistant_is_active_teacher_is_active.py | 23 ++ backend/api/models/assistant.py | 3 +- backend/api/models/mixins/__init__.py | 0 backend/api/models/mixins/role.py | 33 +++ backend/api/models/student.py | 3 +- backend/api/models/teacher.py | 15 +- backend/api/permissions/course_permissions.py | 4 +- backend/api/permissions/group_permissions.py | 12 +- backend/api/permissions/role_permissions.py | 17 +- backend/api/serializers/course_serializer.py | 8 +- backend/api/serializers/group_serializer.py | 4 +- backend/api/serializers/student_serializer.py | 10 +- backend/api/serializers/teacher_serializer.py | 2 +- backend/api/signals.py | 4 +- backend/api/tests/helpers.py | 185 ++++++++++++++ backend/api/tests/test_admin.py | 47 +--- backend/api/tests/test_assistant.py | 91 ++++--- backend/api/tests/test_checks.py | 152 ++++------- backend/api/tests/test_course.py | 126 +++------ backend/api/tests/test_file_structure.py | 220 +++++++--------- backend/api/tests/test_group.py | 80 ++---- backend/api/tests/test_locale.py | 4 +- backend/api/tests/test_project.py | 239 ++++++------------ backend/api/tests/test_student.py | 116 +++++---- backend/api/tests/test_submission.py | 68 ++--- backend/api/tests/test_teacher.py | 50 +--- backend/api/views/assistant_view.py | 33 ++- backend/api/views/course_view.py | 8 +- backend/api/views/group_view.py | 4 +- backend/api/views/student_view.py | 31 ++- backend/api/views/teacher_view.py | 31 ++- backend/authentication/models.py | 2 +- .../components/courses/CourseGeneralCard.vue | 7 +- .../composables/services/students.service.ts | 8 +- 42 files changed, 959 insertions(+), 946 deletions(-) rename backend/api/management/commands/{makeAdmin.py => make_admin.py} (100%) create mode 100644 backend/api/management/commands/make_teacher.py rename backend/api/management/commands/{seedDb.py => seed_db.py} (77%) create mode 100644 backend/api/migrations/0012_errortemplate_alter_extrachecksresult_error_message_and_more.py create mode 100644 backend/api/migrations/0013_student_is_active.py create mode 100644 backend/api/migrations/0013_student_is_active_squashed_0014_assistant_is_active_teacher_is_active.py create mode 100644 backend/api/migrations/0014_assistant_is_active_teacher_is_active.py create mode 100644 backend/api/models/mixins/__init__.py create mode 100644 backend/api/models/mixins/role.py create mode 100644 backend/api/tests/helpers.py diff --git a/backend/.gitignore b/backend/.gitignore index 4c4ced1e..edbe73cb 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,5 +1,6 @@ .venv .idea +staticfiles db.sqlite3 __pycache__ *.mo \ No newline at end of file diff --git a/backend/api/fixtures/groups.yaml b/backend/api/fixtures/groups.yaml index 7b989dda..34af1aa7 100644 --- a/backend/api/fixtures/groups.yaml +++ b/backend/api/fixtures/groups.yaml @@ -6,7 +6,6 @@ students: - '1' - '2' - - '000200694919' - model: api.group pk: 3 fields: @@ -45,4 +44,3 @@ - '1' - '2' - '3' - - '000200694919' diff --git a/backend/api/management/commands/makeAdmin.py b/backend/api/management/commands/make_admin.py similarity index 100% rename from backend/api/management/commands/makeAdmin.py rename to backend/api/management/commands/make_admin.py diff --git a/backend/api/management/commands/make_teacher.py b/backend/api/management/commands/make_teacher.py new file mode 100644 index 00000000..6a019431 --- /dev/null +++ b/backend/api/management/commands/make_teacher.py @@ -0,0 +1,23 @@ +from django.core.management.base import BaseCommand +from api.models.student import Student +from api.models.teacher import Teacher + + +class Command(BaseCommand): + + help = 'make yourself teacher' + + def add_arguments(self, parser): + parser.add_argument('username', type=str, help='The username of the student user to make teacher') + + def handle(self, *args, **options): + username = options['username'] + student = Student.objects.filter(username=username) + + if student.count() == 0: + self.stdout.write(self.style.ERROR('User not found, first log in !')) + return + + Teacher.create(student.get(), create_time=student.create_time) + + self.stdout.write(self.style.SUCCESS('Successfully made the user teacher!')) diff --git a/backend/api/management/commands/seedDb.py b/backend/api/management/commands/seed_db.py similarity index 77% rename from backend/api/management/commands/seedDb.py rename to backend/api/management/commands/seed_db.py index 58425bd3..c1261534 100644 --- a/backend/api/management/commands/seedDb.py +++ b/backend/api/management/commands/seed_db.py @@ -4,7 +4,6 @@ from django.db.models import Max from faker.providers import BaseProvider, DynamicProvider import random -import time from authentication.models import Faculty from api.models.student import Student @@ -106,7 +105,8 @@ def provide_teacher(self, errHandler, min_faculty=1, max_faculty=2, staf_prob=0. ) if faculty is not None: - teacher.faculties.add(*faculty) # Add faculties in bulk + for fac in faculty: + teacher.faculties.add(fac) return teacher except Exception: @@ -140,7 +140,8 @@ def provide_assistant(self, errHandler, min_faculty=1, max_faculty=3, staf_prob= ) if faculty is not None: - assistant.faculties.add(*faculty) # Add faculties in bulk + for fac in faculty: + assistant.faculties.add(fac) return assistant except Exception: @@ -175,7 +176,8 @@ def provide_student(self, errHandler, min_faculty=1, max_faculty=3, staf_prob=0. ) if faculty is not None: - student.faculties.add(*faculty) # Add faculties in bulk + for fac in faculty: + student.faculties.add(fac) return student except Exception: @@ -212,34 +214,28 @@ def provide_course( # add students student_count = fake.random_int(min=min_students, max=max_students) - students_list = [] - while len(students_list) < student_count: + while course.students.count() < student_count: student = fake.student_provider() - if student not in students_list: - students_list.append(student) - course.students.add(*students_list) # Add students in bulk + if student not in course.students.all(): + course.students.add(student) # add teachers teacher_count = fake.random_int(min=min_teachers, max=max_teachers) - teachers_list = [] - while len(teachers_list) < teacher_count: + while course.teachers.count() < teacher_count: teacher = fake.teacher_provider() - if teacher not in teachers_list: - teachers_list.append(teacher) - course.teachers.add(*teachers_list) # Add teachers in bulk + if teacher not in course.teachers.all(): + course.teachers.add(teacher) # add assistants assistant_count = fake.random_int(min=min_assistants, max=max_assistants) - assistants_list = [] - while len(assistants_list) < assistant_count: + while course.assistants.count() < assistant_count: assistant = fake.assistant_provider() - if assistant not in assistants_list: - assistants_list.append(assistant) - course.assistants.add(*assistants_list) # Add assistants in bulk + if assistant not in course.assistants.all(): + course.assistants.add(assistant) + # print(course_name) return course - except Exception as e: - print(e) + except Exception: tries += 1 errHandler.stdout.write(errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique Course.")) @@ -311,12 +307,10 @@ def provide_group(self, errHandler, min_score=0): elif len(students_not_in_group) < max_group_size: group.students.extend(students_not_in_group) else: - choosen_students = [] for _ in range(0, max_group_size): random_student = students_not_in_group[fake.random_int(min=0, max=len(students_not_in_group))] - choosen_students.append(random_student) + group.students.add(random_student) students_not_in_group.remove(random_student) - group.students.add(*choosen_students) # bulk add the students return group except Exception: @@ -392,9 +386,10 @@ def provide_structure_check(self, errHandler, min_extensions=1, max_extensions=5 if extension not in blocked_extensions and extension not in obligated_extensions: blocked_extensions.append(extension) - check.obligated_extensions.add(*obligated_extensions) - check.blocked_extensions.add(*blocked_extensions) - + for ext in obligated_extensions: + check.obligated_extensions.add(ext) + for ext in blocked_extensions: + check.blocked_extensions.add(ext) return check except Exception: tries += 1 @@ -415,46 +410,6 @@ def update_providers(): structureCheck_provider.elements = StructureCheck.objects.all() -def update_Faculty_providers(): - faculty_provider.elements = Faculty.objects.all() - - -def update_Student_providers(): - student_provider.elements = Student.objects.all() - - -def update_Assistant_providers(): - assistant_provider.elements = Assistant.objects.all() - - -def update_Teacher_providers(): - teacher_provider.elements = Teacher.objects.all() - - -def update_Course_providers(): - course_provider.elements = Course.objects.all() - - -def update_Project_providers(): - project_provider.elements = Project.objects.all() - - -def update_Group_providers(): - group_provider.elements = Group.objects.all() - - -def update_Submission_providers(): - Submission_provider.elements = Submission.objects.all() - - -def update_FileExtension_providers(): - fileExtension_provider.elements = FileExtension.objects.all() - - -def update_StructureCheck_providers(): - structureCheck_provider.elements = StructureCheck.objects.all() - - # add new providers to faker instance fake.add_provider(Providers) fake.add_provider(faculty_provider) @@ -469,67 +424,34 @@ def update_StructureCheck_providers(): fake.add_provider(structureCheck_provider) -def format_time(execution_time): - if execution_time < 1: - return f"{execution_time * 1000:.2f} milliseconds" - elif execution_time < 60: - return f"{execution_time:.2f} seconds" - elif execution_time < 3600: - return f"{execution_time / 60:.2f} minutes" - else: - return f"{execution_time / 3600:.2f} hours" - - class Command(BaseCommand): help = 'seed the db with data' - def seed_data(self, amount, provider_function, update_function): + def seed_data(self, amount, provider_function): for _ in range(amount): provider_function(self) - update_function() + update_providers() def handle(self, *args, **options): - start_time = time.time() # TODO maybey take as option - amount_of_students = 50_000 - amount_of_assistants = 300 - amount_of_teachers = 500 + amount_of_students = 10_000 + amount_of_assistants = 1_000 + amount_of_teachers = 1_000 amount_of_courses = 1_000 - amount_of_projects = 3_000 - amount_of_groups = 9_000 + amount_of_projects = 5_000 + amount_of_groups = 20_000 amount_of_submissions = 50_000 amount_of_file_extensions = 20 - amount_of_structure_checks = 12_000 - - # amount_of_students = 0 - # amount_of_assistants = 0 - # amount_of_teachers = 0 - # amount_of_courses = 1 - # amount_of_projects = 0 - # amount_of_groups = 0 - # amount_of_submissions = 0 - # amount_of_file_extensions = 0 - # amount_of_structure_checks = 0 - - self.seed_data(amount_of_students, fake.provide_student, update_Student_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded students!')) - self.seed_data(amount_of_assistants, fake.provide_assistant, update_Assistant_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded assistants!')) - self.seed_data(amount_of_teachers, fake.provide_teacher, update_Teacher_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded teachers!')) - self.seed_data(amount_of_courses, fake.provide_course, update_Course_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded courses!')) - self.seed_data(amount_of_projects, fake.provide_project, update_Project_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded projects!')) - self.seed_data(amount_of_groups, fake.provide_group, update_Group_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded groups!')) - self.seed_data(amount_of_submissions, fake.provide_submission, update_Submission_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded submissions!')) - self.seed_data(amount_of_file_extensions, fake.provide_fileExtension, update_FileExtension_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded fileExtensions!')) - self.seed_data(amount_of_structure_checks, fake.provide_structure_check, update_StructureCheck_providers) - self.stdout.write(self.style.SUCCESS('Successfully seeded structure_checks!')) - - end_time = time.time() - execution_time = end_time - start_time - self.stdout.write(self.style.SUCCESS(f"Successfully seeded db in {format_time(execution_time)}!")) + amount_of_structure_checks = 10_000 + + self.seed_data(amount_of_students, fake.provide_student) + self.seed_data(amount_of_assistants, fake.provide_assistant) + self.seed_data(amount_of_teachers, fake.provide_teacher) + self.seed_data(amount_of_courses, fake.provide_course) + self.seed_data(amount_of_projects, fake.provide_project) + self.seed_data(amount_of_groups, fake.provide_group) + self.seed_data(amount_of_submissions, fake.provide_submission) + self.seed_data(amount_of_file_extensions, fake.provide_fileExtension) + self.seed_data(amount_of_structure_checks, fake.provide_structure_check) + + self.stdout.write(self.style.SUCCESS('Successfully seeded db!')) diff --git a/backend/api/migrations/0012_errortemplate_alter_extrachecksresult_error_message_and_more.py b/backend/api/migrations/0012_errortemplate_alter_extrachecksresult_error_message_and_more.py new file mode 100644 index 00000000..1b096d2f --- /dev/null +++ b/backend/api/migrations/0012_errortemplate_alter_extrachecksresult_error_message_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.4 on 2024-04-09 10:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0011_revise_extra_checks'), + ] + + operations = [ + migrations.CreateModel( + name='ErrorTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message_key', models.CharField(max_length=256)), + ], + ), + migrations.AlterField( + model_name='extrachecksresult', + name='error_message', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='extra_checks_results', to='api.errortemplate'), + ), + migrations.DeleteModel( + name='ErrorTemplates', + ), + ] diff --git a/backend/api/migrations/0013_student_is_active.py b/backend/api/migrations/0013_student_is_active.py new file mode 100644 index 00000000..9cc601a7 --- /dev/null +++ b/backend/api/migrations/0013_student_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-04-09 10:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0012_errortemplate_alter_extrachecksresult_error_message_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/backend/api/migrations/0013_student_is_active_squashed_0014_assistant_is_active_teacher_is_active.py b/backend/api/migrations/0013_student_is_active_squashed_0014_assistant_is_active_teacher_is_active.py new file mode 100644 index 00000000..f5194c2d --- /dev/null +++ b/backend/api/migrations/0013_student_is_active_squashed_0014_assistant_is_active_teacher_is_active.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.4 on 2024-04-09 10:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('api', '0013_student_is_active'), ('api', '0014_assistant_is_active_teacher_is_active')] + + dependencies = [ + ('api', '0012_errortemplate_alter_extrachecksresult_error_message_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='assistant', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='teacher', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/backend/api/migrations/0014_assistant_is_active_teacher_is_active.py b/backend/api/migrations/0014_assistant_is_active_teacher_is_active.py new file mode 100644 index 00000000..fa3825ae --- /dev/null +++ b/backend/api/migrations/0014_assistant_is_active_teacher_is_active.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.4 on 2024-04-09 10:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0013_student_is_active'), + ] + + operations = [ + migrations.AddField( + model_name='assistant', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='teacher', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/backend/api/models/assistant.py b/backend/api/models/assistant.py index 4c6d9f19..b5baff82 100644 --- a/backend/api/models/assistant.py +++ b/backend/api/models/assistant.py @@ -1,9 +1,10 @@ from django.db import models from authentication.models import User from api.models.course import Course +from api.models.mixins.role import RoleMixin -class Assistant(User): +class Assistant(RoleMixin, User): """This model represents a single assistant. It extends the User model from the authentication app with assistant-specific attributes. diff --git a/backend/api/models/mixins/__init__.py b/backend/api/models/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/models/mixins/role.py b/backend/api/models/mixins/role.py new file mode 100644 index 00000000..d51b8815 --- /dev/null +++ b/backend/api/models/mixins/role.py @@ -0,0 +1,33 @@ +from django.db import models +from authentication.models import User + + +class RoleMixin(models.Model): + # Whether the role model is active. + is_active = models.BooleanField( + default=True + ) + + @classmethod + def create(cls, user: User, **attributes) -> None: + """Create a model role for the given user""" + model = cls.objects.filter(id=user.id).first() + + if model is not None: + model.activate() + return model + + return cls(user_ptr=user, **attributes).save_base(raw=True) + + def activate(self): + """Activate the role""" + self.is_active = True + self.save() + + def deactivate(self) -> None: + """Deactivate the role""" + self.is_active = False + self.save() + + class Meta: + abstract = True diff --git a/backend/api/models/student.py b/backend/api/models/student.py index fea5cf73..9efaa27d 100644 --- a/backend/api/models/student.py +++ b/backend/api/models/student.py @@ -1,9 +1,10 @@ from django.db import models from authentication.models import User from api.models.course import Course +from api.models.mixins.role import RoleMixin -class Student(User): +class Student(RoleMixin, User): """This model represents a single student. It extends the User model from the authentication app with student-specific attributes. diff --git a/backend/api/models/teacher.py b/backend/api/models/teacher.py index 6866dd7b..a21161a5 100644 --- a/backend/api/models/teacher.py +++ b/backend/api/models/teacher.py @@ -1,9 +1,10 @@ from django.db import models from api.models.course import Course +from api.models.mixins.role import RoleMixin from authentication.models import User -class Teacher(User): +class Teacher(RoleMixin, User): """This model represents a single teacher. It extends the User model from the authentication app with teacher-specific attributes. @@ -20,3 +21,15 @@ class Teacher(User): def has_course(self, course: Course) -> bool: """Checks if the teacher has the given course.""" return self.courses.contains(course) + + +def make_teacher(user: User) -> Teacher: + """Activates the Teacher role for the given user.""" + teacher: Teacher = Teacher.objects.filter(id=user.id).first() + + if teacher is not None: + teacher.is_active = True + teacher.save() + return teacher + + return Teacher(user_ptr=user).save_base(raw=True) diff --git a/backend/api/permissions/course_permissions.py b/backend/api/permissions/course_permissions.py index ccb1314e..96fd11e2 100644 --- a/backend/api/permissions/course_permissions.py +++ b/backend/api/permissions/course_permissions.py @@ -60,7 +60,7 @@ def has_object_permission(self, request: Request, view: ViewSet, course: Course) return user.is_authenticated # Only students can add or remove themselves from a course. - if is_student(user) and request.data.get("student_id") == user.id: + if is_student(user) and request.data.get("student") == user.id: return True # Teachers and assistants can add and remove any student. @@ -77,4 +77,4 @@ def has_object_permission(self, request: Request, view: ViewSet, course: Course) return user.is_authenticated # Only teachers can add or remove themselves from a course. - return is_teacher(user) and request.data.get("teacher_id") == user.id + return is_teacher(user) and request.data.get("teacher") == user.id diff --git a/backend/api/permissions/group_permissions.py b/backend/api/permissions/group_permissions.py index c8739a4f..977ef77b 100644 --- a/backend/api/permissions/group_permissions.py +++ b/backend/api/permissions/group_permissions.py @@ -39,20 +39,20 @@ class GroupStudentPermission(BasePermission): def has_object_permission(self, request: Request, view: ViewSet, group) -> bool: user: User = request.user course = group.project.course - teacher_or_assitant = is_teacher(user) and user.teacher.courses.filter( + teacher_or_assistant = is_teacher(user) and user.teacher.courses.filter( id=course.id).exists() or is_assistant(user) and user.assistant.courses.filter(id=course.id).exists() if request.method in SAFE_METHODS: # Users related to the course can view the students of the group. - return teacher_or_assitant or (is_student(user) and user.student.courses.filter(id=course.id).exists()) + return teacher_or_assistant or (is_student(user) and user.student.courses.filter(id=course.id).exists()) # Students can only add and remove themselves from a group. - if is_student(user) and request.data.get("student_id") == user.id: + if is_student(user) and request.data.get("student") == user.id: # Make sure the student is actually part of the course. return user.student.courses.filter(id=course.id).exists() # Teachers and assistants can add and remove any student from a group - return teacher_or_assitant + return teacher_or_assistant class GroupSubmissionPermission(BasePermission): @@ -67,8 +67,8 @@ def had_object_permission(self, request: Request, view: ViewSet, group) -> bool: # Users related to the group can view the submissions of the group return teacher_or_assitant or (is_student(user) and user.student.groups.filter(id=group.id).exists()) - # Student can only add submissions to there own group - if is_student(user) and request.data.get("student_id") == user.id and view.action == "create": + # Student can only add submissions to their own group + if is_student(user) and request.data.get("student") == user.id and view.action == "create": return user.student.course.filter(id=course.id).exists() # Removing a Submissions is not possible for teachers and assistants diff --git a/backend/api/permissions/role_permissions.py b/backend/api/permissions/role_permissions.py index 2c50aa5b..68cc428e 100644 --- a/backend/api/permissions/role_permissions.py +++ b/backend/api/permissions/role_permissions.py @@ -7,16 +7,19 @@ from api.models.teacher import Teacher -def is_student(user: User): - return Student.objects.filter(id=user.id).exists() +def is_student(user: User) -> bool: + """Check whether the user is a student""" + return Student.objects.filter(id=user.id, is_active=True).exists() -def is_assistant(user: User): - return Assistant.objects.filter(id=user.id).exists() +def is_assistant(user: User) -> bool: + """Check whether the user is an assistant""" + return Assistant.objects.filter(id=user.id, is_active=True).exists() -def is_teacher(user: User): - return Teacher.objects.filter(id=user.id).exists() +def is_teacher(user: User) -> bool: + """Check whether the user is a teacher""" + return Teacher.objects.filter(id=user.id, is_active=True).exists() class IsStudent(IsAuthenticated): @@ -42,7 +45,7 @@ def has_permission(self, request, view): class IsSameUser(IsAuthenticated): def has_permission(self, request, view): - return False + return super().has_permission(request, view) def has_object_permission(self, request: Request, view: ViewSet, user: User): """Returns true if the request's user is the same as the given user""" diff --git a/backend/api/serializers/course_serializer.py b/backend/api/serializers/course_serializer.py index 38b4c281..940369d6 100644 --- a/backend/api/serializers/course_serializer.py +++ b/backend/api/serializers/course_serializer.py @@ -61,7 +61,7 @@ def validate(self, data): course: Course = self.context["course"] # Check if the student isn't already enrolled. - if course.students.contains(data["student_id"]): + if course.students.contains(data["student"]): raise ValidationError(gettext("courses.error.students.already_present")) # Check if the course is not from a past academic year. @@ -80,7 +80,7 @@ def validate(self, data): course: Course = self.context["course"] # Make sure the student is enrolled. - if not course.students.contains(data["student_id"]): + if not course.students.contains(data["student"]): raise ValidationError(gettext("courses.error.students.not_present")) # Check if the course is not from a past academic year. @@ -99,7 +99,7 @@ def validate(self, data): course: Course = self.context["course"] # Check if the teacher isn't already enrolled. - if course.teachers.contains(data["teacher_id"]): + if course.teachers.contains(data["teacher"]): raise ValidationError(gettext("courses.error.teachers.already_present")) # Check if the course is not from a past academic year. @@ -118,7 +118,7 @@ def validate(self, data): course: Course = self.context["course"] # Make sure the teacher is enrolled. - if not course.teachers.contains(data["teacher_id"]): + if not course.teachers.contains(data["teacher"]): raise ValidationError(gettext("courses.error.teachers.not_present")) # Check if the course is not from a past academic year. diff --git a/backend/api/serializers/group_serializer.py b/backend/api/serializers/group_serializer.py index eb1f6214..94e0a5de 100644 --- a/backend/api/serializers/group_serializer.py +++ b/backend/api/serializers/group_serializer.py @@ -58,7 +58,7 @@ def validate(self, data): # Get the group and student group: Group = self.context["group"] - student: Student = data["student_id"] + student: Student = data["student"] # Make sure a student can't join if groups are locked if group.project.is_groups_locked(): @@ -88,7 +88,7 @@ def validate(self, data): # Get the group and student group: Group = self.context["group"] - student: Student = data["student_id"] + student: Student = data["student"] # Make sure the student was in the group if not group.students.filter(id=student.id).exists(): diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 2c46593f..a1bef0fc 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -1,5 +1,6 @@ from rest_framework import serializers from api.models.student import Student +from authentication.models import User class StudentSerializer(serializers.ModelSerializer): @@ -22,7 +23,14 @@ class Meta: fields = '__all__' +class CreateStudentSerializer(serializers.Serializer): + user = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all() + ) + student_id = serializers.CharField() + + class StudentIDSerializer(serializers.Serializer): - student_id = serializers.PrimaryKeyRelatedField( + student = serializers.PrimaryKeyRelatedField( queryset=Student.objects.all() ) diff --git a/backend/api/serializers/teacher_serializer.py b/backend/api/serializers/teacher_serializer.py index 75550d65..032b5216 100644 --- a/backend/api/serializers/teacher_serializer.py +++ b/backend/api/serializers/teacher_serializer.py @@ -18,6 +18,6 @@ class Meta: class TeacherIDSerializer(serializers.Serializer): - teacher_id = serializers.PrimaryKeyRelatedField( + teacher = serializers.PrimaryKeyRelatedField( queryset=Teacher.objects.all() ) diff --git a/backend/api/signals.py b/backend/api/signals.py index 5b9e645b..5e580dd9 100644 --- a/backend/api/signals.py +++ b/backend/api/signals.py @@ -13,12 +13,12 @@ @receiver(user_created) -def user_creation(user: User, attributes: dict, **_): +def _user_creation(user: User, attributes: dict, **_): """Upon user creation, auto-populate additional properties""" student_id: str = attributes.get("ugentStudentID") if student_id is not None: - Student(user_ptr=user, student_id=student_id).save_base(raw=True) + Student.create(user, student_id=student_id) @receiver(run_extra_checks) diff --git a/backend/api/tests/helpers.py b/backend/api/tests/helpers.py new file mode 100644 index 00000000..82e372e3 --- /dev/null +++ b/backend/api/tests/helpers.py @@ -0,0 +1,185 @@ +from django.utils import timezone +from api.models.course import Course +from api.models.student import Student +from api.models.assistant import Assistant +from api.models.teacher import Teacher +from api.models.extension import FileExtension +from api.models.checks import StructureCheck, ExtraCheck +from api.models.project import Project +from api.models.group import Group +from api.models.submission import Submission +from authentication.models import Faculty, User + + +def create_faculty(name: str | int) -> Faculty: + """Create a Faculty with the given arguments.""" + return Faculty.objects.create(id=name, name=name) + + +def create_user(id: str | int, first_name: str, last_name: str, email: str, faculty: list[Faculty] = None) -> User: + username = f"{first_name.lower()}{last_name.lower()}" + + user = User.objects.create( + id=id, + username=username, + first_name=first_name, + last_name=last_name, + email=email + ) + + if faculty is not None: + for faculty in faculty: + user.faculties.add(faculty) + + return user + + +def create_admin(id: str | int, first_name: str, last_name: str, email: str, faculty: list[Faculty] = None): + """Create an Admin with the given arguments.""" + admin = create_user(id, first_name, last_name, email, faculty) + admin.make_admin() + return admin + + +def create_student( + id: str | int, + first_name: str, + last_name: str, + email: str, + student_id: str = "", + is_active: bool = True, + faculty: list[Faculty] = None, + courses: list[Course] = None +) -> Student: + """Create a student with the given arguments.""" + username = f"{first_name.lower()}{last_name.lower()}" + + student = Student.objects.create( + id=id, + student_id=student_id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + is_active=is_active, + create_time=timezone.now(), + ) + + if faculty is not None: + for fac in faculty: + student.faculties.add(fac) + + if courses is not None: + for course in courses: + student.courses.add(course) + + return student + + +def create_assistant(id, first_name, last_name, email, is_active: bool = True, faculty=None, courses=None): + """Create an assistant with the given arguments.""" + username = f"{first_name.lower()}{last_name.lower()}" + + assistant = Assistant.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + is_active=is_active, + create_time=timezone.now(), + ) + + if faculty is not None: + for fac in faculty: + assistant.faculties.add(fac) + + if courses is not None: + for course in courses: + assistant.courses.add(course) + + return assistant + + +def create_teacher(id, first_name, last_name, email, is_active: bool = True, faculty=None, courses=None): + """Create an assistant with the given arguments.""" + username = f"{first_name.lower()}{last_name.lower()}" + + assistant = Teacher.objects.create( + id=id, + first_name=first_name, + last_name=last_name, + username=username, + email=email, + is_active=is_active, + create_time=timezone.now(), + ) + + if faculty is not None: + for fac in faculty: + assistant.faculties.add(fac) + + if courses is not None: + for course in courses: + assistant.courses.add(course) + + return assistant + + +def create_file_extension(extension): + """Create a FileExtension with the given arguments.""" + return FileExtension.objects.create(extension=extension) + + +def create_structure_check(name, project, obligated_extensions, blocked_extensions): + """Create a StructureCheck with the given arguments.""" + check = StructureCheck.objects.create(name=name, project=project) + + for ext in obligated_extensions: + check.obligated_extensions.add(ext) + + for ext in blocked_extensions: + check.blocked_extensions.add(ext) + + return check + + +def create_project(name, description, days, course, max_score=5, group_size=5, visible=True, archived=False): + """Create a Project with the given arguments.""" + deadline = timezone.now() + timezone.timedelta(days=days) + + return Project.objects.create( + name=name, + description=description, + visible=visible, + archived=archived, + deadline=deadline, + course=course, + max_score=max_score, + group_size=group_size, + ) + + +def create_course(name: str | int, academic_startyear: int, description: str = None, parent_course: Course = None) -> Course: + """Create a Course with the given arguments.""" + return Course.objects.create( + name=name, + academic_startyear=academic_startyear, + description=description, + parent_course=parent_course, + ) + + +def create_group(project: Project, score: int = 0) -> Group: + """Create a Group with the given arguments.""" + return Group.objects.create(project=project, score=score) + + +def create_submission(submission_number: int, group: Group, structure_checks_passed: bool) -> Submission: + """Create a Submission with the given arguments.""" + + return Submission.objects.create( + submission_number=submission_number, + group=group, + structure_checks_passed=structure_checks_passed, + ) diff --git a/backend/api/tests/test_admin.py b/backend/api/tests/test_admin.py index be96e132..d9eb73a6 100644 --- a/backend/api/tests/test_admin.py +++ b/backend/api/tests/test_admin.py @@ -1,46 +1,8 @@ import json -from django.utils import timezone from django.urls import reverse from rest_framework.test import APITestCase -from authentication.models import Faculty, User - - -def create_faculty(name): - """ - Create a Faculty with the given arguments.""" - return Faculty.objects.create(id=name, name=name) - - -def create_admin(id, first_name, last_name, email, faculty=None): - """ - Create a Admin with the given arguments. - """ - username = f"{first_name}_{last_name}" - if faculty is None: - return User.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - is_staff=True, - create_time=timezone.now(), - ) - else: - admin = User.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - is_staff=True, - create_time=timezone.now(), - ) - - for fac in faculty: - admin.faculties.add(fac) - - return admin +from api.tests.helpers import create_admin, create_faculty +from authentication.models import User class AdminModelTests(APITestCase): @@ -99,10 +61,11 @@ def test_multiple_admins(self): """ # Create multiple admins admin1 = create_admin( - id=1, first_name="Johny", last_name="Doeg", email="john.doe@example.com" + id=1, first_name="Saul", last_name="Goodman", email="john.doe@example.com" ) + admin2 = create_admin( - id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + id=2, first_name="Liv", last_name="Doe", email="jane.doe@example.com" ) # Make a GET request to retrieve the admins diff --git a/backend/api/tests/test_assistant.py b/backend/api/tests/test_assistant.py index dd52a797..04957bff 100644 --- a/backend/api/tests/test_assistant.py +++ b/backend/api/tests/test_assistant.py @@ -1,61 +1,72 @@ import json -from django.utils import timezone from django.urls import reverse from rest_framework.test import APITestCase from api.models.assistant import Assistant from api.models.teacher import Teacher -from api.models.course import Course -from authentication.models import Faculty, User +from api.tests.helpers import create_faculty, create_course, create_assistant, create_user +from authentication.models import User -def create_course(name, academic_startyear, description=None, parent_course=None): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - name=name, - academic_startyear=academic_startyear, - description=description, - parent_course=parent_course, - ) +class AssistantModelTests(APITestCase): + def setUp(self) -> None: + self.client.force_authenticate( + User.get_dummy_admin() + ) + def test_activate_new(self): + """Able to add a new student role to a user""" + # Create the initial user + user = create_user("1", "Saul", "Goodman", "saul@goodman.com") -def create_faculty(name): - """Create a Faculty with the given arguments.""" - return Faculty.objects.create(id=name, name=name) + # Attempt to add the student role to the user + response_root = self.client.post( + reverse("assistant-list"), + data={"user": user.id}, + follow=True + ) + # Assert a 200 status code + self.assertEqual(response_root.status_code, 200) -def create_assistant(id, first_name, last_name, email, faculty=None, courses=None): - """ - Create a assistant with the given arguments. - """ - username = f"{first_name}_{last_name}" - assistant = Assistant.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) + # Assert that an active student exists with the user ID + self.assertTrue(Assistant.objects.filter(id=user.id, is_active=True).exists()) - if faculty is not None: - for fac in faculty: - assistant.faculties.add(fac) + def test_activate_old(self): + """Able to re-activate an existing student role""" + # Create the initial student, but don't activate + assistant = create_assistant("1", "Saul", "Goodman", "saul@goodman.com", False) - if courses is not None: - for cours in courses: - assistant.courses.add(cours) + # Attempt to add the student role to the user + response_root = self.client.post( + reverse("assistant-list"), + data={"user": assistant.id}, + follow=True + ) - return assistant + # Assert a 200 status code + self.assertEqual(response_root.status_code, 200) + # Assert that an active student exists with the user ID + self.assertTrue(Assistant.objects.filter(id=assistant.id, is_active=True).exists()) -class AssistantModelTests(APITestCase): - def setUp(self) -> None: - self.client.force_authenticate( - User.get_dummy_admin() + def test_deactivate(self): + """Able to deactivate an existing student role""" + # Create the initial student + assistant = create_assistant("1", "Saul", "Goodman", "saul@goodman.com", True) + + # Attempt to remove the student role from the user + response_root = self.client.delete( + reverse("assistant-detail", args=[assistant.id]), + data={"user": assistant.id}, + follow=True ) + # Assert a 200 status code + self.assertEqual(response_root.status_code, 200) + + # Assert that an active student with the user ID no longer exists + self.assertFalse(Assistant.objects.filter(id=assistant.id, is_active=True).exists()) + def test_no_assistant(self): """ able to retrieve no assistant before publishing it. diff --git a/backend/api/tests/test_checks.py b/backend/api/tests/test_checks.py index d34c36c4..6e774b3e 100644 --- a/backend/api/tests/test_checks.py +++ b/backend/api/tests/test_checks.py @@ -1,73 +1,14 @@ import json -from django.utils import timezone from django.urls import reverse from rest_framework.test import APITestCase from authentication.models import User -from api.models.checks import StructureCheck, ExtraCheck -from api.models.extension import FileExtension -from api.models.project import Project -from api.models.course import Course -from django.conf import settings - - -def create_fileExtension(id, extension): - """ - Create a FileExtension with the given arguments. - """ - return FileExtension.objects.create(id=id, extension=extension) - - -def create_structure_check(id, name, project, obligated_extensions, blocked_extensions): - """ - Create a StructureCheck with the given arguments. - """ - check = StructureCheck.objects.create(id=id, name=name, project=project) - - for ext in obligated_extensions: - check.obligated_extensions.add(ext) - for ext in blocked_extensions: - check.blocked_extensions.add(ext) - - return check - - -# def create_extra_check(id, project, run_script): -# """ -# Create an ExtraCheck with the given arguments. -# """ -# return ExtraCheck.objects.create(id=id, project=project, run_script=run_script) - - -def create_project(id, name, description, visible, archived, days, course, max_score, group_size): - """Create a Project with the given arguments.""" - deadline = timezone.now() + timezone.timedelta(days=days) - - return Project.objects.create( - id=id, - name=name, - description=description, - visible=visible, - archived=archived, - deadline=deadline, - course=course, - max_score=max_score, - group_size=group_size, - ) - - -def create_course(id, name, academic_startyear): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - id=id, name=name, academic_startyear=academic_startyear - ) +from api.tests.helpers import create_structure_check, create_file_extension, create_project, create_course def get_project(): - course = create_course(id=1, name="Course", academic_startyear=2021) + course = create_course(name="Course", academic_startyear=2021) + project = create_project( - id=1, name="Project", description="Description", visible=True, @@ -86,9 +27,9 @@ def setUp(self) -> None: User.get_dummy_admin() ) - def test_no_fileExtension(self): + def test_no_file_extension(self): """ - Able to retrieve no FileExtension before publishing it. + Able to retrieve no file_extension before publishing it. """ response_root = self.client.get(reverse("file-extension-list"), follow=True) self.assertEqual(response_root.status_code, 200) @@ -99,13 +40,13 @@ def test_no_fileExtension(self): # Assert that the parsed JSON is an empty list self.assertEqual(content_json, []) - def test_fileExtension_exists(self): + def test_file_extension_exists(self): """ - Able to retrieve a single fileExtension after creating it. + Able to retrieve a single file_extension after creating it. """ - fileExtension = create_fileExtension(id=5, extension="pdf") + file_extension = create_file_extension(extension="pdf") - # Make a GET request to retrieve the fileExtension + # Make a GET request to retrieve the file_extension response = self.client.get(reverse("file-extension-list"), follow=True) # Check if the response was successful @@ -117,23 +58,23 @@ def test_fileExtension_exists(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with one fileExtension + # Assert that the parsed JSON is a list with one file_extension self.assertEqual(len(content_json), 1) - # Assert the details of the retrieved fileExtension - # match the created fileExtension - retrieved_fileExtension = content_json[0] - self.assertEqual(retrieved_fileExtension["extension"], fileExtension.extension) + # Assert the details of the retrieved file_extension + # match the created file_extension + retrieved_file_extension = content_json[0] + self.assertEqual(retrieved_file_extension["extension"], file_extension.extension) - def test_multiple_fileExtension(self): + def test_multiple_file_extension(self): """ - Able to retrieve multiple fileExtension after creating them. + Able to retrieve multiple file_extension after creating them. """ - # Create multiple fileExtension - fileExtension1 = create_fileExtension(id=1, extension="jpg") - fileExtension2 = create_fileExtension(id=2, extension="png") + # Create multiple file_extension + file_extension1 = create_file_extension(extension="jpg") + file_extension2 = create_file_extension(extension="png") - # Make a GET request to retrieve the fileExtension + # Make a GET request to retrieve the file_extension response = self.client.get(reverse("file-extension-list"), follow=True) # Check if the response was successful @@ -145,30 +86,30 @@ def test_multiple_fileExtension(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert that the parsed JSON is a list with multiple fileExtension + # Assert that the parsed JSON is a list with multiple file_extension self.assertEqual(len(content_json), 2) - # Assert the details of the retrieved fileExtension - # match the created fileExtension - retrieved_fileExtension1, retrieved_fileExtension2 = content_json + # Assert the details of the retrieved file_extension + # match the created file_extension + retrieved_file_extension1, retrieved_file_extension2 = content_json self.assertEqual( - retrieved_fileExtension1["extension"], fileExtension1.extension + retrieved_file_extension1["extension"], file_extension1.extension ) self.assertEqual( - retrieved_fileExtension2["extension"], fileExtension2.extension + retrieved_file_extension2["extension"], file_extension2.extension ) - def test_fileExtension_detail_view(self): + def test_file_extension_detail_view(self): """ - Able to retrieve details of a single fileExtension. + Able to retrieve details of a single file_extension. """ - # Create an fileExtension for testing. - fileExtension = create_fileExtension(id=3, extension="zip") + # Create an file_extension for testing. + file_extension = create_file_extension(extension="zip") - # Make a GET request to retrieve the fileExtension details + # Make a GET request to retrieve the file_extension details response = self.client.get( - reverse("file-extension-detail", args=[str(fileExtension.id)]), follow=True + reverse("file-extension-detail", args=[str(file_extension.id)]), follow=True ) # Check if the response was successful @@ -180,9 +121,9 @@ def test_fileExtension_detail_view(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - # Assert the details of the retrieved fileExtension - # match the created fileExtension - self.assertEqual(content_json["extension"], fileExtension.extension) + # Assert the details of the retrieved file_extension + # match the created file_extension + self.assertEqual(content_json["extension"], file_extension.extension) class StructureCheckModelTests(APITestCase): @@ -206,16 +147,15 @@ def test_structure_checks_exists(self): Able to retrieve a single Checks after creating it. """ # Create a Checks instance with some file extensions - fileExtension1 = create_fileExtension(id=1, extension="jpg") - fileExtension2 = create_fileExtension(id=2, extension="png") - fileExtension3 = create_fileExtension(id=3, extension="tar") - fileExtension4 = create_fileExtension(id=4, extension="wfp") + file_extension1 = create_file_extension(extension="jpg") + file_extension2 = create_file_extension(extension="png") + file_extension3 = create_file_extension(extension="tar") + file_extension4 = create_file_extension(extension="wfp") checks = create_structure_check( - id=1, name=".", project=get_project(), - obligated_extensions=[fileExtension1, fileExtension4], - blocked_extensions=[fileExtension2, fileExtension3], + obligated_extensions=[file_extension1, file_extension4], + blocked_extensions=[file_extension2, file_extension3], ) # Make a GET request to retrieve the Checks @@ -241,10 +181,10 @@ def test_structure_checks_exists(self): self.assertEqual(len(retrieved_obligated_file_extensions), 2) self.assertEqual( - retrieved_obligated_file_extensions[0]["extension"], fileExtension1.extension + retrieved_obligated_file_extensions[0]["extension"], file_extension1.extension ) self.assertEqual( - retrieved_obligated_file_extensions[1]["extension"], fileExtension4.extension + retrieved_obligated_file_extensions[1]["extension"], file_extension4.extension ) retrieved_blocked_file_extensions = retrieved_checks[ @@ -253,11 +193,11 @@ def test_structure_checks_exists(self): self.assertEqual(len(retrieved_blocked_file_extensions), 2) self.assertEqual( retrieved_blocked_file_extensions[0]["extension"], - fileExtension2.extension, + file_extension2.extension, ) self.assertEqual( retrieved_blocked_file_extensions[1]["extension"], - fileExtension3.extension, + file_extension3.extension, ) @@ -282,7 +222,7 @@ def test_structure_checks_exists(self): # Able to retrieve a single Checks after creating it. # """ # checks = create_extra_check( -# id=1, project=get_project(), run_script="test.sh" +# project=get_project(), run_script="test.sh" # ) # # Make a GET request to retrieve the Checks diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py index c19abb32..ecc66d7c 100644 --- a/backend/api/tests/test_course.py +++ b/backend/api/tests/test_course.py @@ -5,83 +5,8 @@ from authentication.models import User from api.models.course import Course from api.models.teacher import Teacher -from api.models.assistant import Assistant from api.models.student import Student -from api.models.project import Project - - -def create_project(name, description, visible, archived, days, course): - """Create a Project with the given arguments.""" - deadline = timezone.now() + timezone.timedelta(days=days) - - return Project.objects.create( - name=name, - description=description, - visible=visible, - archived=archived, - deadline=deadline, - course=course, - ) - - -def create_student(id, first_name, last_name, email): - """ - Create a student with the given arguments. - """ - username = f"{first_name}_{last_name}" - student = Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) - return student - - -def create_assistant(id, first_name, last_name, email): - """ - Create a assistant with the given arguments. - """ - username = f"{first_name}_{last_name}" - assistant = Assistant.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) - return assistant - - -def create_teacher(id, first_name, last_name, email): - """ - Create a teacher with the given arguments. - """ - username = f"{first_name}_{last_name}" - teacher = Teacher.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) - return teacher - - -def create_course(name, academic_startyear, description=None, parent_course=None): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - name=name, - academic_startyear=academic_startyear, - description=description, - parent_course=parent_course, - ) +from api.tests.helpers import create_course, create_assistant, create_student, create_teacher, create_project def get_course(): @@ -109,7 +34,9 @@ def get_student(): """ Return a random student to use in tests. """ - return create_student(id=5, first_name="Simon", last_name="Mignolet", email="Simon.Mignolet@gmai.com") + return create_student( + id=5, first_name="Simon", last_name="Mignolet", email="Simon.Mignolet@gmai.com", student_id="02000341" + ) class CourseModelTests(APITestCase): @@ -347,10 +274,11 @@ def test_course_student(self): first_name="Simon", last_name="Mignolet", email="simon.mignolet@ugent.be", + student_id="0100" ) student2 = create_student( - id=6, first_name="Ronny", last_name="Deila", email="ronny.deila@brugge.be" + id=6, first_name="Ronny", last_name="Deila", email="ronny.deila@brugge.be", student_id="0200" ) course = create_course( @@ -411,6 +339,8 @@ def test_course_project(self): ) project1 = create_project( + max_score=10, + group_size=5, name="become champions", description="win the jpl", visible=True, @@ -420,6 +350,8 @@ def test_course_project(self): ) project2 = create_project( + max_score=10, + group_size=5, name="become european champion", description="win the cfl", visible=True, @@ -532,7 +464,7 @@ def test_add_self_to_course(self): response = self.client.post( reverse("course-students", args=[str(course.id)]), - data={"student_id": self.user.id}, + data={"student": self.user.id}, follow=True, ) @@ -547,7 +479,7 @@ def test_try_add_self_as_teacher_to_course(self): response = self.client.post( reverse("course-teachers", args=[str(course.id)]), - data={"teacher_id": self.user.id}, + data={"teacher": self.user.id}, follow=True, ) @@ -564,7 +496,7 @@ def test_remove_self_from_course(self): response = self.client.delete( reverse("course-students", args=[str(course.id)]), - data={"student_id": self.user.id}, + data={"student": self.user.id}, follow=True, ) @@ -582,7 +514,7 @@ def test_try_add_other_student_to_course(self): response = self.client.post( reverse("course-students", args=[str(course.id)]), - data={"student_id": other_student.id}, + data={"student": other_student.id}, follow=True, ) @@ -603,7 +535,7 @@ def test_try_remove_other_student_from_course(self): response = self.client.delete( reverse("course-students", args=[str(course.id)]), - data={"student_id": other_student.id}, + data={"student": other_student.id}, follow=True, ) @@ -666,7 +598,7 @@ def test_try_join_old_year_course(self): response = self.client.post( reverse("course-students", args=[str(course.id)]), - data={"student_id": self.user.id}, + data={"student": self.user.id}, follow=True, ) @@ -686,7 +618,7 @@ def test_try_leave_old_year_course(self): response = self.client.delete( reverse("course-students", args=[str(course.id)]), - data={"student_id": self.user.id}, + data={"student": self.user.id}, follow=True, ) @@ -702,7 +634,7 @@ def test_try_leave_course_not_part_of(self): response = self.client.delete( reverse("course-students", args=[str(course.id)]), - data={"student_id": self.user.id}, + data={"student": self.user.id}, follow=True, ) @@ -733,7 +665,7 @@ def test_add_self(self): response = self.client.post( reverse("course-teachers", args=[str(course.id)]), - data={"teacher_id": self.user.id}, + data={"teacher": self.user.id}, follow=True, ) @@ -750,7 +682,7 @@ def test_remove_self(self): response = self.client.delete( reverse("course-teachers", args=[str(course.id)]), - data={"teacher_id": self.user.id}, + data={"teacher": self.user.id}, follow=True, ) @@ -766,7 +698,7 @@ def test_try_remove_self_when_not_part_of(self): response = self.client.delete( reverse("course-teachers", args=[str(course.id)]), - data={"teacher_id": self.user.id}, + data={"teacher": self.user.id}, follow=True, ) @@ -822,7 +754,7 @@ def test_add_student(self): response = self.client.post( reverse("course-students", args=[str(course.id)]), - data={"student_id": student.id}, + data={"student": student.id}, follow=True, ) @@ -842,7 +774,7 @@ def test_remove_student(self): response = self.client.delete( reverse("course-students", args=[str(course.id)]), - data={"student_id": student.id}, + data={"student": student.id}, follow=True, ) @@ -936,9 +868,15 @@ def test_create_individual_project(self): course.teachers.add(self.user) # Create some students - student1 = create_student(id=5, first_name="Simon", last_name="Mignolet", email="Simon.Mignolet@gmail.com") - student2 = create_student(id=6, first_name="Ronny", last_name="Deila", email="Ronny.Deila@gmail.com") - student3 = create_student(id=7, first_name="Karel", last_name="Geraerts", email="Karel.Geraerts@gmail.com") + student1 = create_student( + id=5, first_name="Simon", last_name="Mignolet", email="Simon.Mignolet@gmail.com", student_id="0100" + ) + student2 = create_student( + id=6, first_name="Ronny", last_name="Deila", email="Ronny.Deila@gmail.com", student_id="0200" + ) + student3 = create_student( + id=7, first_name="Karel", last_name="Geraerts", email="Karel.Geraerts@gmail.com", student_id="0300" + ) # Add the students to the course course.students.add(student1) diff --git a/backend/api/tests/test_file_structure.py b/backend/api/tests/test_file_structure.py index f3186b28..74d5bdf2 100644 --- a/backend/api/tests/test_file_structure.py +++ b/backend/api/tests/test_file_structure.py @@ -1,62 +1,12 @@ import json import os - -from api.logic.check_folder_structure import check_zip_file, parse_zip_file -from api.models.checks import StructureCheck -from api.models.course import Course -from api.models.extension import FileExtension -from api.models.project import Project -from authentication.models import User from django.conf import settings from django.urls import reverse from django.utils import timezone from rest_framework.test import APITestCase - - -def create_course(id, name, academic_startyear): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - id=id, name=name, academic_startyear=academic_startyear - ) - - -def create_file_extension(extension): - """ - Create a FileExtension with the given arguments. - """ - return FileExtension.objects.create(extension=extension) - - -def create_project(name, description, visible, archived, days, course): - """Create a Project with the given arguments.""" - deadline = timezone.now() + timezone.timedelta(days=days) - - return Project.objects.create( - name=name, - description=description, - visible=visible, - archived=archived, - deadline=deadline, - course=course, - ) - - -def create_structure_check(name, project, obligated, blocked): - """ - Create a StructureCheck with the given arguments. - """ - structure_check = StructureCheck.objects.create( - name=name, - project=project, - ) - for ch in obligated: - structure_check.obligated_extensions.add(ch) - for ch in blocked: - structure_check.blocked_extensions.add(ch) - - return structure_check +from api.logic.check_folder_structure import check_zip_file, parse_zip_file +from api.tests.helpers import create_course, create_file_extension, create_project, create_structure_check +from authentication.models import User class FileTestsTests(APITestCase): @@ -73,8 +23,10 @@ def tearDown(self): settings.MEDIA_ROOT = self.old_media_root def test_parsing(self): - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( + group_size=5, + max_score=10, name="test", description="descr", visible=True, @@ -150,8 +102,10 @@ def test_parsing(self): self.assertEqual(len(content["blocked_extensions"]), 0) def test_checking(self): - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( + max_score=10, + group_size=5, name="test", description="descr", visible=True, @@ -174,50 +128,52 @@ def test_checking(self): create_structure_check( name=".", project=project, - obligated=[], - blocked=[]) + obligated_extensions=[], + blocked_extensions=[]) create_structure_check( name="folder_struct1", project=project, - obligated=[fileExtensionHS], - blocked=[]) + obligated_extensions=[fileExtensionHS], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1", project=project, - obligated=[fileExtensionPDF, fileExtensionDOCX], - blocked=[]) + obligated_extensions=[fileExtensionPDF, fileExtensionDOCX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1/templates", project=project, - obligated=[fileExtensionLATEX], - blocked=[]) + obligated_extensions=[fileExtensionLATEX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2", project=project, - obligated=[fileExtensionMD], - blocked=[]) + obligated_extensions=[fileExtensionMD], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2/src", project=project, - obligated=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], - blocked=[]) + obligated_extensions=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap3", project=project, - obligated=[fileExtensionTS, fileExtensionTSX], - blocked=[]) + obligated_extensions=[fileExtensionTS, fileExtensionTSX], + blocked_extensions=[]) self.assertTrue(check_zip_file(project=project, dir_path="structures/zip_struct1.zip")[0]) def test_checking_obligated_not_found(self): - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( + group_size=5, + max_score=10, name="test", description="descr", visible=True, @@ -240,49 +196,51 @@ def test_checking_obligated_not_found(self): create_structure_check( name=".", project=project, - obligated=[], - blocked=[fileExtensionDOCX]) + obligated_extensions=[], + blocked_extensions=[fileExtensionDOCX]) create_structure_check( name="folder_struct1", project=project, - obligated=[fileExtensionHS], - blocked=[]) + obligated_extensions=[fileExtensionHS], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1", project=project, - obligated=[fileExtensionPDF, fileExtensionDOCX], - blocked=[]) + obligated_extensions=[fileExtensionPDF, fileExtensionDOCX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1/templates", project=project, - obligated=[fileExtensionLATEX], - blocked=[]) + obligated_extensions=[fileExtensionLATEX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2", project=project, - obligated=[fileExtensionMD], - blocked=[]) + obligated_extensions=[fileExtensionMD], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2/src", project=project, - obligated=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], - blocked=[]) + obligated_extensions=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap3", project=project, - obligated=[fileExtensionTS, fileExtensionTSX], - blocked=[]) + obligated_extensions=[fileExtensionTS, fileExtensionTSX], + blocked_extensions=[]) self.assertFalse(check_zip_file(project=project, dir_path="tests/test_zip2struct1.zip")[0]) def test_checking_obligated_directory_not_found(self): - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( + group_size=5, + max_score=10, name="test", description="descr", visible=True, @@ -305,49 +263,51 @@ def test_checking_obligated_directory_not_found(self): create_structure_check( name=".", project=project, - obligated=[], - blocked=[fileExtensionDOCX]) + obligated_extensions=[], + blocked_extensions=[fileExtensionDOCX]) create_structure_check( name="folder_struct1", project=project, - obligated=[fileExtensionHS], - blocked=[]) + obligated_extensions=[fileExtensionHS], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1", project=project, - obligated=[fileExtensionPDF, fileExtensionDOCX], - blocked=[]) + obligated_extensions=[fileExtensionPDF, fileExtensionDOCX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1/templates", project=project, - obligated=[fileExtensionLATEX], - blocked=[]) + obligated_extensions=[fileExtensionLATEX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2", project=project, - obligated=[fileExtensionMD], - blocked=[]) + obligated_extensions=[fileExtensionMD], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2/src", project=project, - obligated=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], - blocked=[]) + obligated_extensions=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap3", project=project, - obligated=[fileExtensionTS, fileExtensionTSX], - blocked=[]) + obligated_extensions=[fileExtensionTS, fileExtensionTSX], + blocked_extensions=[]) self.assertFalse(check_zip_file(project=project, dir_path="tests/test_zip4struct1.zip")[0]) def test_checking_blocked_extension_found(self): - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( + group_size=5, + max_score=10, name="test", description="descr", visible=True, @@ -370,50 +330,52 @@ def test_checking_blocked_extension_found(self): create_structure_check( name=".", project=project, - obligated=[], - blocked=[fileExtensionDOCX]) + obligated_extensions=[], + blocked_extensions=[fileExtensionDOCX]) create_structure_check( name="folder_struct1", project=project, - obligated=[fileExtensionHS], - blocked=[]) + obligated_extensions=[fileExtensionHS], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1", project=project, - obligated=[fileExtensionDOCX], - blocked=[fileExtensionPDF]) + obligated_extensions=[fileExtensionDOCX], + blocked_extensions=[fileExtensionPDF]) create_structure_check( name="folder_struct1/submap1/templates", project=project, - obligated=[fileExtensionLATEX], - blocked=[]) + obligated_extensions=[fileExtensionLATEX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2", project=project, - obligated=[fileExtensionMD], - blocked=[]) + obligated_extensions=[fileExtensionMD], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2/src", project=project, - obligated=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], - blocked=[]) + obligated_extensions=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap3", project=project, - obligated=[fileExtensionTS, fileExtensionTSX], - blocked=[]) + obligated_extensions=[fileExtensionTS, fileExtensionTSX], + blocked_extensions=[]) self.assertFalse(check_zip_file(project=project, dir_path="tests/test_zip1struct1.zip")[0]) def test_checking_extra_directory_found(self): - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( + group_size=5, + max_score=10, name="test", description="descr", visible=True, @@ -436,44 +398,44 @@ def test_checking_extra_directory_found(self): create_structure_check( name=".", project=project, - obligated=[], - blocked=[fileExtensionDOCX]) + obligated_extensions=[], + blocked_extensions=[fileExtensionDOCX]) create_structure_check( name="folder_struct1", project=project, - obligated=[fileExtensionHS], - blocked=[]) + obligated_extensions=[fileExtensionHS], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1", project=project, - obligated=[fileExtensionPDF, fileExtensionDOCX], - blocked=[]) + obligated_extensions=[fileExtensionPDF, fileExtensionDOCX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap1/templates", project=project, - obligated=[fileExtensionLATEX], - blocked=[]) + obligated_extensions=[fileExtensionLATEX], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2", project=project, - obligated=[fileExtensionMD], - blocked=[]) + obligated_extensions=[fileExtensionMD], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap2/src", project=project, - obligated=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], - blocked=[]) + obligated_extensions=[fileExtensionPY, fileExtensionHPP, fileExtensionCPP], + blocked_extensions=[]) create_structure_check( name="folder_struct1/submap3", project=project, - obligated=[fileExtensionTS, fileExtensionTSX], - blocked=[]) + obligated_extensions=[fileExtensionTS, fileExtensionTSX], + blocked_extensions=[]) self.assertFalse( check_zip_file(project=project, dir_path="tests/test_zip3struct1.zip", restrict_extra_folders=True)[0]) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index 12ab152b..ead8611f 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -1,53 +1,11 @@ import json -from datetime import timedelta from django.urls import reverse -from django.utils import timezone +from django.conf import settings from rest_framework.test import APITestCase -from authentication.models import User -from api.models.project import Project from api.models.student import Student -from api.models.group import Group -from api.models.course import Course -from django.conf import settings from api.models.teacher import Teacher - - -def create_course(name, academic_startyear, description=None, parent_course=None): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - name=name, - academic_startyear=academic_startyear, - description=description, - parent_course=parent_course, - ) - - -def create_project(name, description, days, course, group_size=2, max_score=20, score_visible=True): - """Create a Project with the given arguments.""" - deadline = timezone.now() + timedelta(days=days) - return Project.objects.create( - name=name, description=description, deadline=deadline, course=course, - group_size=group_size, max_score=max_score, score_visible=score_visible - ) - - -def create_student(id, first_name, last_name, email): - """Create a Student with the given arguments.""" - username = f"{first_name}_{last_name}" - return Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - ) - - -def create_group(project, score): - """Create a Group with the given arguments.""" - return Group.objects.create(project=project, score=score) +from api.tests.helpers import create_course, create_project, create_student, create_group +from authentication.models import User class GroupModelTests(APITestCase): @@ -140,11 +98,11 @@ def test_group_students(self): name="Project 1", description="Description 1", days=7, course=course ) student1 = create_student( - id=5, first_name="John", last_name="Doe", email="john.doe@example.com" + id=5, first_name="John", last_name="Doe", email="john.doe@example.com", student_id="0200" ) student2 = create_student( - id=6, first_name="kom", last_name="mor_up", email="kom.mor_up@example.com" + id=6, first_name="kom", last_name="mor_up", email="kom.mor_up@example.com", student_id="0300" ) group = create_group(project=project, score=10) @@ -220,7 +178,7 @@ def test_assign_student_to_group(self): response = self.client.post( reverse("group-students", args=[str(group.id)]), - {"student_id": student.id}, + {"student": student.id}, follow=True, ) @@ -249,7 +207,7 @@ def test_remove_student_from_group(self): response = self.client.delete( reverse("group-students", args=[str(group.id)]), - {"student_id": student.id}, + {"student": student.id}, follow=True, ) @@ -324,7 +282,7 @@ def test_join_group(self): # Try to join a group that is part of a course the student is not enrolled in response = self.client.post( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -337,7 +295,7 @@ def test_join_group(self): # Join the group now that the student is enrolled in the course response = self.client.post( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -351,7 +309,7 @@ def test_join_group(self): response = self.client.post( reverse("group-students", args=[str(group2.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -376,7 +334,7 @@ def test_join_full_group(self): # Join the group response = self.client.post( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -396,7 +354,7 @@ def test_leave_group(self): # Join the group response = self.client.post( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -408,7 +366,7 @@ def test_leave_group(self): # Leave the group response = self.client.delete( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -434,7 +392,7 @@ def test_try_leave_locked_group(self): # Try to leave the group response = self.client.delete( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -455,7 +413,7 @@ def test_try_leave_group_not_part_of(self): # Try to leave the group response = self.client.delete( reverse("group-students", args=[str(group.id)]), - {"student_id": self.user.id}, + {"student": self.user.id}, follow=True, ) @@ -481,7 +439,7 @@ def test_try_to_assign_other_student_to_group(self): response = self.client.post( reverse("group-students", args=[str(group.id)]), - {"student_id": student.id}, + {"student": student.id}, follow=True, ) @@ -509,7 +467,7 @@ def test_try_to_delete_other_student_from_group(self): response = self.client.delete( reverse("group-students", args=[str(group.id)]), - {"student_id": student.id}, + {"student": student.id}, follow=True, ) @@ -547,7 +505,7 @@ def test_group_score_visibility(self): course = create_course(name="sel2", academic_startyear=2023) project = create_project( - name="Project 1", description="Description 1", days=7, course=course, score_visible=True + name="Project 1", description="Description 1", days=7, course=course, visible=True ) group = create_group(project=project, score=10) course.students.add(self.user) @@ -566,7 +524,7 @@ def test_group_score_visibility(self): # Add the student to the group group.students.add(self.user) - # Set the visibility of the score to False, to make sure the score is not included if it is not visible + # Set the visibility of the score to False, in order to make sure the score is not included if it is not visible project.score_visible = False project.save() diff --git a/backend/api/tests/test_locale.py b/backend/api/tests/test_locale.py index 43403caa..13a53844 100644 --- a/backend/api/tests/test_locale.py +++ b/backend/api/tests/test_locale.py @@ -20,7 +20,7 @@ def setUp(self) -> None: def test_default_locale(self): response = self.client.post(reverse("course-students", args=["1"]), - {"student_id": 1}) + {"student": 1}) self.assertEqual(response.status_code, 400) body = json.loads(response.content.decode('utf-8')) @@ -29,7 +29,7 @@ def test_default_locale(self): def test_nl_locale(self): response = self.client.post(reverse("course-students", args=["1"]), - {"student_id": 1}, + {"student": 1}, headers={"accept-language": "nl"}) self.assertEqual(response.status_code, 400) diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index 827d2561..0d9c8363 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -1,93 +1,17 @@ import json -from api.models.checks import ExtraCheck, StructureCheck -from api.models.course import Course -from api.models.extension import FileExtension -from api.models.group import Group -from api.models.project import Project -from api.models.student import Student -from api.models.submission import Submission -from api.models.teacher import Teacher -from authentication.models import User from django.conf import settings from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext from rest_framework.test import APITestCase - - -def create_course(id, name, academic_startyear): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - id=id, name=name, academic_startyear=academic_startyear - ) - - -def create_fileExtension(id, extension): - """ - Create a FileExtension with the given arguments. - """ - return FileExtension.objects.create(id=id, extension=extension) - - -def create_project(name, description, visible, archived, days, course): - """Create a Project with the given arguments.""" - deadline = timezone.now() + timezone.timedelta(days=days) - - return Project.objects.create( - name=name, - description=description, - visible=visible, - archived=archived, - deadline=deadline, - course=course, - ) - - -def create_group(project): - """Create a Group with the given arguments.""" - - return Group.objects.create( - project=project, - ) - - -def create_submission(submission_number, group, structure_checks_passed): - """Create a Submission with the given arguments.""" - - return Submission.objects.create( - submission_number=submission_number, - group=group, - structure_checks_passed=structure_checks_passed, - ) - - -def create_student(id, first_name, last_name, email): - """Create a Student with the given arguments.""" - username = f"{first_name}_{last_name}" - return Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - ) - - -def create_structure_check(id, name, project, obligated_extensions, blocked_extensions): - """ - Create a StructureCheck with the given arguments. - """ - check = StructureCheck.objects.create(id=id, name=name, project=project) - - for ext in obligated_extensions: - check.obligated_extensions.add(ext) - for ext in blocked_extensions: - check.blocked_extensions.add(ext) - - return check +from api.models.checks import ExtraCheck, StructureCheck +from api.models.project import Project +from api.models.student import Student +from api.models.teacher import Teacher +from api.tests.helpers import create_course, create_file_extension, create_project, create_group, create_submission, \ + create_student, create_structure_check +from authentication.models import User class ProjectModelTests(APITestCase): @@ -98,7 +22,7 @@ def test_toggle_visible(self): """ toggle the visible state of a project. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) past_project = create_project( name="test", description="descr", @@ -117,7 +41,7 @@ def test_toggle_archived(self): """ toggle the archived state of a project. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) past_project = create_project( name="test", description="descr", @@ -137,7 +61,7 @@ def test_toggle_locked_groups(self): """ toggle the locked state of the project groups. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) past_project = create_project( name="test", description="descr", @@ -156,13 +80,13 @@ def test_automatically_create_groups_when_creating_project(self): """ creating a project as a teacher should open the same amount of groups as students enrolled in the project. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) student1 = create_student( - id=1, first_name="John", last_name="Doe", email="john.doe@example.com" + id=1, first_name="John", last_name="Doe", email="john.doe@example.com", student_id="0200" ) student2 = create_student( - id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com", student_id="0300" ) student1.courses.add(course) student2.courses.add(course) @@ -204,7 +128,7 @@ def test_start_date_Project_not_in_past(self): """ unable to create a project as a teacher/admin if the start date lies within the past. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) start_date = timezone.now() - timezone.timedelta(days=1) project_data = { @@ -227,7 +151,7 @@ def test_deadline_Project_before_start_date(self): """ unable to create a project as a teacher/admin if the deadline lies before the start date. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) deadline = timezone.now() + timezone.timedelta(days=1) start_date = timezone.now() + timezone.timedelta(days=2) @@ -252,7 +176,7 @@ def test_deadline_approaching_in_with_past_Project(self): deadline_approaching_in() returns False for Projects whose Deadline is in the past. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) past_project = create_project( name="test", description="descr", @@ -268,7 +192,7 @@ def test_deadline_approaching_in_with_future_Project_within_time(self): deadline_approaching_in() returns True for Projects whose Deadline is in the timerange given. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) future_project = create_project( name="test", description="descr", @@ -284,7 +208,7 @@ def test_deadline_approaching_in_with_future_Project_not_within_time(self): deadline_approaching_in() returns False for Projects whose Deadline is out of the timerange given. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) future_project = create_project( name="test", description="descr", @@ -300,7 +224,7 @@ def test_deadline_passed_with_future_Project(self): deadline_passed() returns False for Projects whose Deadline is not passed. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) future_project = create_project( name="test", description="descr", @@ -316,7 +240,7 @@ def test_deadline_passed_with_past_Project(self): deadline_passed() returns True for Projects whose Deadline is passed. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) past_project = create_project( name="test", description="descr", @@ -332,7 +256,7 @@ def test_project_exists(self): Able to retrieve a single project after creating it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test project", description="test description", @@ -368,7 +292,7 @@ def test_project_course(self): Able to retrieve a course of a project after creating it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test project", description="test description", @@ -414,11 +338,11 @@ def test_project_structure_checks(self): Able to retrieve a structure check of a project after creating it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) - fileExtension1 = create_fileExtension(id=1, extension="jpg") - fileExtension2 = create_fileExtension(id=2, extension="png") - fileExtension3 = create_fileExtension(id=3, extension="tar") - fileExtension4 = create_fileExtension(id=4, extension="wfp") + course = create_course(name="test course", academic_startyear=2024) + file_extension1 = create_file_extension(extension="jpg") + file_extension2 = create_file_extension(extension="png") + file_extension3 = create_file_extension(extension="tar") + file_extension4 = create_file_extension(extension="wfp") project = create_project( name="test project", description="test description", @@ -428,11 +352,10 @@ def test_project_structure_checks(self): course=course, ) checks = create_structure_check( - id=5, name=".", project=project, - obligated_extensions=[fileExtension1, fileExtension4], - blocked_extensions=[fileExtension2, fileExtension3], + obligated_extensions=[file_extension1, file_extension4], + blocked_extensions=[file_extension2, file_extension3], ) response = self.client.get( @@ -475,21 +398,21 @@ def test_project_structure_checks(self): self.assertEqual(len(retrieved_obligated_extensions), 2) self.assertEqual( - retrieved_obligated_extensions[0]["extension"], fileExtension1.extension + retrieved_obligated_extensions[0]["extension"], file_extension1.extension ) self.assertEqual( - retrieved_obligated_extensions[1]["extension"], fileExtension4.extension + retrieved_obligated_extensions[1]["extension"], file_extension4.extension ) retrieved_blocked_file_extensions = content_json["blocked_extensions"] self.assertEqual(len(retrieved_blocked_file_extensions), 2) self.assertEqual( retrieved_blocked_file_extensions[0]["extension"], - fileExtension2.extension, + file_extension2.extension, ) self.assertEqual( retrieved_blocked_file_extensions[1]["extension"], - fileExtension3.extension, + file_extension3.extension, ) def test_project_structure_checks_post(self): @@ -497,11 +420,11 @@ def test_project_structure_checks_post(self): Able to retrieve a structure check of a project after posting it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) - fileExtension1 = create_fileExtension(id=1, extension="jpg") - fileExtension2 = create_fileExtension(id=2, extension="png") - fileExtension3 = create_fileExtension(id=3, extension="tar") - fileExtension4 = create_fileExtension(id=4, extension="wfp") + course = create_course(name="test course", academic_startyear=2024) + file_extension1 = create_file_extension(extension="jpg") + file_extension2 = create_file_extension(extension="png") + file_extension3 = create_file_extension(extension="tar") + file_extension4 = create_file_extension(extension="wfp") project = create_project( name="test project", description="test description", @@ -515,8 +438,8 @@ def test_project_structure_checks_post(self): reverse("project-structure-checks", args=[str(project.id)]), { "name": ".", - "obligated_extensions": [fileExtension1.extension, fileExtension4.extension], - "blocked_extensions": [fileExtension2.extension, fileExtension3.extension]}, + "obligated_extensions": [file_extension1.extension, file_extension4.extension], + "blocked_extensions": [file_extension2.extension, file_extension3.extension]}, follow=True, ) @@ -532,20 +455,20 @@ def test_project_structure_checks_post(self): self.assertEqual(len(retrieved_obligated_extensions), 2) self.assertEqual( - retrieved_obligated_extensions[0].extension, fileExtension1.extension + retrieved_obligated_extensions[0].extension, file_extension1.extension ) self.assertEqual( - retrieved_obligated_extensions[1].extension, fileExtension4.extension + retrieved_obligated_extensions[1].extension, file_extension4.extension ) self.assertEqual(len(retrieved_blocked_file_extensions), 2) self.assertEqual( retrieved_blocked_file_extensions[0].extension, - fileExtension2.extension, + file_extension2.extension, ) self.assertEqual( retrieved_blocked_file_extensions[1].extension, - fileExtension3.extension, + file_extension3.extension, ) def test_project_structure_checks_post_already_existing(self): @@ -553,11 +476,11 @@ def test_project_structure_checks_post_already_existing(self): Able to retrieve a structure check of a project after posting it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) - fileExtension1 = create_fileExtension(id=1, extension="jpg") - fileExtension2 = create_fileExtension(id=2, extension="png") - fileExtension3 = create_fileExtension(id=3, extension="tar") - fileExtension4 = create_fileExtension(id=4, extension="wfp") + course = create_course(name="test course", academic_startyear=2024) + file_extension1 = create_file_extension(extension="jpg") + file_extension2 = create_file_extension(extension="png") + file_extension3 = create_file_extension(extension="tar") + file_extension4 = create_file_extension(extension="wfp") project = create_project( name="test project", description="test description", @@ -568,19 +491,18 @@ def test_project_structure_checks_post_already_existing(self): ) create_structure_check( - id=5, name=".", project=project, - obligated_extensions=[fileExtension1, fileExtension4], - blocked_extensions=[fileExtension2, fileExtension3], + obligated_extensions=[file_extension1, file_extension4], + blocked_extensions=[file_extension2, file_extension3], ) response = self.client.post( reverse("project-structure-checks", args=[str(project.id)]), { "name": ".", - "obligated_extensions": [fileExtension1.extension, fileExtension4.extension], - "blocked_extensions": [fileExtension2.extension, fileExtension3.extension]}, + "obligated_extensions": [file_extension1.extension, file_extension4.extension], + "blocked_extensions": [file_extension2.extension, file_extension3.extension]}, follow=True, ) @@ -594,11 +516,11 @@ def test_project_structure_checks_post_blocked_and_obligated(self): Able to retrieve a structure check of a project after posting it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) - fileExtension1 = create_fileExtension(id=1, extension="jpg") - fileExtension2 = create_fileExtension(id=2, extension="png") - fileExtension3 = create_fileExtension(id=3, extension="tar") - fileExtension4 = create_fileExtension(id=4, extension="wfp") + course = create_course(name="test course", academic_startyear=2024) + file_extension1 = create_file_extension(extension="jpg") + file_extension2 = create_file_extension(extension="png") + file_extension3 = create_file_extension(extension="tar") + file_extension4 = create_file_extension(extension="wfp") project = create_project( name="test project", description="test description", @@ -612,8 +534,9 @@ def test_project_structure_checks_post_blocked_and_obligated(self): reverse("project-structure-checks", args=[str(project.id)]), { "name": ".", - "obligated_extensions": [fileExtension1.extension, fileExtension4.extension], - "blocked_extensions": [fileExtension1.extension, fileExtension2.extension, fileExtension3.extension]}, + "obligated_extensions": [file_extension1.extension, file_extension4.extension], + "blocked_extensions": [file_extension1.extension, file_extension2.extension, + file_extension3.extension]}, follow=True, ) @@ -677,7 +600,7 @@ def test_project_groups(self): """ Able to retrieve a list of groups of a project after creating it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test project", description="test description", @@ -687,8 +610,8 @@ def test_project_groups(self): course=course, ) - group1 = create_group(project=project) - group2 = create_group(project=project) + group1 = create_group(project=project, score=0) + group2 = create_group(project=project, score=0) response = self.client.get( reverse("project-groups", args=[str(project.id)]), follow=True @@ -708,7 +631,7 @@ def test_project_submissions(self): """ Able to retrieve a list of submissions of a project after creating it. """ - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test project", description="test description", @@ -718,8 +641,8 @@ def test_project_submissions(self): course=course, ) - group1 = create_group(project=project) - group2 = create_group(project=project) + group1 = create_group(project=project, score=0) + group2 = create_group(project=project, score=0) submission1 = create_submission(submission_number=1, group=group1, structure_checks_passed=True) submission2 = create_submission(submission_number=2, group=group2, structure_checks_passed=False) @@ -740,7 +663,7 @@ def test_project_submissions(self): def test_cant_join_locked_groups(self): """Should not be able to add a student to a group if the groups are locked.""" - course = create_course(id=3, name="sel2", academic_startyear=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="test project", @@ -753,17 +676,17 @@ def test_cant_join_locked_groups(self): # Create example students student1 = create_student( - id=5, first_name="John", last_name="Doe", email="John.Doe@gmail.com" + id=5, first_name="John", last_name="Doe", email="John.Doe@gmail.com", student_id="1" ) student2 = create_student( - id=7, first_name="Jane", last_name="Doe", email="Jane.Doe@gmail.com" + id=7, first_name="Jane", last_name="Doe", email="Jane.Doe@gmail.com", student_id="2" ) # Add these student to the course course.students.add(student1) course.students.add(student2) - # Create an exmample group + # Create an example group group = create_group(project=project) # Already add one student to the group @@ -787,7 +710,7 @@ def test_cant_join_locked_groups(self): self.assertFalse(group.students.filter(id=student2.id).exists()) # Try to remove a student from the group - response = self.client.post( + self.client.post( reverse("group-students", args=[str(group.id)]), {"student_id": student1.id}, follow=True, @@ -811,7 +734,7 @@ def setUp(self) -> None: def test_create_groups(self): """Able to create groups for a project.""" - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test", description="descr", @@ -846,7 +769,7 @@ def test_create_groups(self): def test_submission_status_non_empty_groups(self): """Submission status returns the correct amount of non empty groups participating in the project.""" - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test", description="descr", @@ -868,10 +791,10 @@ def test_submission_status_non_empty_groups(self): # Create example students student1 = create_student( - id=1, first_name="John", last_name="Doe", email="john.doe@example.com" + id=1, first_name="John", last_name="Doe", email="john.doe@example.com", student_id="0100" ) student2 = create_student( - id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com", student_id="0200" ) # Create example groups @@ -897,7 +820,7 @@ def test_submission_status_non_empty_groups(self): def test_submission_status_groups_submitted_and_passed_checks(self): """Retrieve the submission status for a project.""" - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test", description="descr", @@ -919,10 +842,10 @@ def test_submission_status_groups_submitted_and_passed_checks(self): # Create example students student1 = create_student( - id=1, first_name="John", last_name="Doe", email="john.doe@example.com" + id=1, first_name="John", last_name="Doe", email="john.doe@example.com", student_id="0100" ) student2 = create_student( - id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com", student_id="0200" ) student3 = create_student( id=3, first_name="Joe", last_name="Doe", email="Joe.doe@example.com" @@ -958,7 +881,7 @@ def test_submission_status_groups_submitted_and_passed_checks(self): def test_retrieve_list_submissions(self): """Able to retrieve a list of submissions for a project.""" - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test", description="descr", @@ -1001,7 +924,7 @@ def setUp(self) -> None: def test_try_to_create_groups(self): """Not able to create groups for a project.""" - course = create_course(id=3, name="test course", academic_startyear=2024) + course = create_course(name="test course", academic_startyear=2024) project = create_project( name="test", description="descr", diff --git a/backend/api/tests/test_student.py b/backend/api/tests/test_student.py index 68bcbee9..e4027563 100644 --- a/backend/api/tests/test_student.py +++ b/backend/api/tests/test_student.py @@ -1,52 +1,9 @@ import json -from django.utils import timezone from django.urls import reverse from rest_framework.test import APITestCase from api.models.student import Student -from api.models.course import Course -from authentication.models import Faculty, User - - -def create_course(name, academic_startyear, description=None, parent_course=None): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - name=name, - academic_startyear=academic_startyear, - description=description, - parent_course=parent_course, - ) - - -def create_faculty(name): - """Create a Faculty with the given arguments.""" - return Faculty.objects.create(id=name, name=name) - - -def create_student(id, first_name, last_name, email, faculty=None, courses=None): - """ - Create a student with the given arguments. - """ - username = f"{first_name}_{last_name}" - student = Student.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) - - if faculty is not None: - for fac in faculty: - student.faculties.add(fac) - - if courses is not None: - for cours in courses: - student.courses.add(cours) - - return student +from api.tests.helpers import create_student, create_course, create_faculty, create_user +from authentication.models import User class StudentModelTests(APITestCase): @@ -58,7 +15,7 @@ def setUp(self) -> None: def test_no_student(self): """ - able to retrieve no student before publishing it. + Able to retrieve no student before publishing it. """ response_root = self.client.get(reverse("student-list"), follow=True) @@ -70,6 +27,63 @@ def test_no_student(self): # Assert that the parsed JSON is an empty list self.assertEqual(content_json, []) + def test_activate_new(self): + """Able to add a new student role to a user""" + # Create the initial user + user = create_user("1", "Saul", "Goodman", "saul@goodman.com") + + # Attempt to add the student role to the user + response_root = self.client.post( + reverse("student-list"), + data={"user": user.id, "student_id": "02000341"}, + follow=True + ) + + # Assert a 200 status code + self.assertEqual(response_root.status_code, 200) + + # Assert that an active student exists with the user ID + self.assertTrue(Student.objects.filter(id=user.id, is_active=True).exists()) + + def test_activate_old(self): + """Able to re-activate an existing student role""" + # Create the initial student, but don't activate + student = create_student("1", "Saul", "Goodman", "saul@goodman.com", "02000341", False) + + # Attempt to add the student role to the user + response_root = self.client.post( + reverse("student-list"), + data={"user": student.id, "student_id": "14300020"}, + follow=True + ) + + # Assert a 200 status code + self.assertEqual(response_root.status_code, 200) + + # Assert that an active student exists with the user ID + self.assertTrue(Student.objects.filter(id=student.id, is_active=True).exists()) + + # Assert that the old student ID was kept + self.assertTrue(Student.objects.filter(student_id=student.student_id).exists()) + + def test_deactivate(self): + """Able to deactivate an existing student role""" + # Create the initial student + student = create_student("1", "Saul", "Goodman", "saul@goodman.com", "02000341", True) + + # Attempt to remove the student role from the user + response_root = self.client.delete( + reverse("student-detail", args=[student.id]), + data={"user": student.id, "student_id": "14300020"}, + follow=True + ) + + # Assert a 200 status code + self.assertEqual(response_root.status_code, 200) + + # Assert that an active student with the user ID no longer exists + self.assertFalse(Student.objects.filter(id=student.id, is_active=True).exists()) + def test_student_exists(self): """ Able to retrieve a single student after creating it. @@ -106,10 +120,10 @@ def test_multiple_students(self): """ # Create multiple assistant student1 = create_student( - id=1, first_name="Johny", last_name="Doeg", email="john.doe@example.com" + id=1, first_name="Johny", last_name="Doe", email="john.doe@example.com", student_id="0100" ) student2 = create_student( - id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com" + id=2, first_name="Jane", last_name="Doe", email="jane.doe@example.com", student_id="0200" ) # Make a GET request to retrieve the student @@ -144,7 +158,7 @@ def test_student_detail_view(self): """ Able to retrieve details of a single student. """ - # Create an student for testing with the name "Bob Peeters" + # Create a student for testing with the name "Bob Peeters" student = create_student( id=5, first_name="Bob", last_name="Peeters", email="bob.peeters@example.com" ) @@ -173,7 +187,7 @@ def test_student_faculty(self): """ Able to retrieve faculty details of a single student. """ - # Create an student for testing with the name "Bob Peeters" + # Create a student for testing with the name "Bob Peeters" faculty = create_faculty(name="testing faculty") student = create_student( id=5, @@ -220,7 +234,7 @@ def test_student_courses(self): """ Able to retrieve courses details of a single student. """ - # Create an student for testing with the name "Bob Peeters" + # Create a student for testing with the name "Bob Peeters" course1 = create_course( name="Introduction to Computer Science", academic_startyear=2022, diff --git a/backend/api/tests/test_submission.py b/backend/api/tests/test_submission.py index 248081fd..5d3383d1 100644 --- a/backend/api/tests/test_submission.py +++ b/backend/api/tests/test_submission.py @@ -1,63 +1,37 @@ import json from datetime import timedelta - -from api.models.checks import ExtraCheck -from api.models.course import Course -from api.models.group import Group -from api.models.project import Project -from api.models.submission import ExtraChecksResult, Submission, SubmissionFile -from authentication.models import User from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile -from django.db.models import Max from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext from rest_framework.test import APITestCase - - -def create_course(name, academic_start_year, description=None, parent_course=None): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - name=name, - academic_startyear=academic_start_year, - description=description, - parent_course=parent_course, - ) - - -def create_project(name, description, days, course): - """Create a Project with the given arguments.""" - deadline = timezone.now() + timedelta(days=days) - return Project.objects.create( - name=name, description=description, deadline=deadline, course=course, score_visible=True - ) +from api.models.course import Course +from api.models.group import Group +from api.models.project import Project +from api.models.submission import Submission, SubmissionFile +from api.tests.helpers import create_course, create_project, create_group +from authentication.models import User def create_past_project(name, description, days, course, days_start_date): """Create a Project with the given arguments.""" deadline = timezone.now() + timedelta(days=days) - startDate = timezone.now() + timedelta(days=days_start_date) + start_date = timezone.now() + timedelta(days=days_start_date) + return Project.objects.create( - name=name, description=description, deadline=deadline, course=course, score_visible=True, start_date=startDate + name=name, description=description, deadline=deadline, course=course, score_visible=True, start_date=start_date ) -def create_group(project, score): - """Create a Group with the given arguments.""" - return Group.objects.create(project=project, score=score) - - def create_submission(group, submission_number): - """Create an Submission with the given arguments.""" + """Create a Submission with the given arguments.""" return Submission.objects.create( group=group, submission_number=submission_number, submission_time=timezone.now(), structure_checks_passed=True ) -def create_submissionFile(submission, file): +def create_submission_file(submission, file): """Create an SubmissionFile with the given arguments.""" return SubmissionFile.objects.create(submission=submission, file=file) @@ -87,7 +61,7 @@ def test_submission_exists(self): """ Able to retrieve a single submission after creating it. """ - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="Project 1", description="Description 1", days=7, course=course ) @@ -126,7 +100,7 @@ def test_multiple_submission_exists(self): """ Able to retrieve multiple submissions after creating them. """ - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="Project 1", description="Description 1", days=7, course=course ) @@ -178,7 +152,7 @@ def test_submission_detail_view(self): """ Able to retrieve details of a single submission. """ - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="Project 1", description="Description 1", days=7, course=course ) @@ -215,7 +189,7 @@ def test_submission_group(self): """ Able to retrieve group of a single submission. """ - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="Project 1", description="Description 1", days=7, course=course ) @@ -267,7 +241,7 @@ def test_submission_group(self): # """ # Able to retrieve extra checks of a single submission. # """ - # course = create_course(name="sel2", academic_start_year=2023) + # course = create_course(name="sel2", academic_startyear=2023) # project = create_project( # name="Project 1", description="Description 1", days=7, course=course # ) @@ -314,7 +288,7 @@ def test_submission_group(self): # with open(zip_file_path, 'rb') as file: # files = {'files': SimpleUploadedFile('mixed.zip', file.read())} - # course = create_course(name="sel2", academic_start_year=2023) + # course = create_course(name="sel2", academic_startyear=2023) # project = create_project( # name="Project 1", description="Description 1", days=7, course=course # ) @@ -339,7 +313,7 @@ def test_submission_after_deadline(self): with open(zip_file_path, 'rb') as f: files = {'files': SimpleUploadedFile('mixed.zip', f.read())} - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_past_project( name="Project 1", description="Description 1", days=-7, course=course, days_start_date=-84 ) @@ -366,7 +340,7 @@ def test_submission_after_deadline(self): # with open(zip_file_path, 'rb') as f: # files = {'files': SimpleUploadedFile('mixed.zip', f.read())} - # course = create_course(name="sel2", academic_start_year=2023) + # course = create_course(name="sel2", academic_startyear=2023) # project = create_project( # name="Project 1", description="Description 1", days=7, course=course # ) @@ -407,7 +381,7 @@ def test_submission_invisible_project(self): with open(zip_file_path, 'rb') as f: files = {'files': SimpleUploadedFile('mixed.zip', f.read())} - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="Project 1", description="Description 1", days=7, course=course ) @@ -437,7 +411,7 @@ def test_submission_archived_project(self): with open(zip_file_path, 'rb') as f: files = {'files': SimpleUploadedFile('mixed.zip', f.read())} - course = create_course(name="sel2", academic_start_year=2023) + course = create_course(name="sel2", academic_startyear=2023) project = create_project( name="Project 1", description="Description 1", days=7, course=course ) diff --git a/backend/api/tests/test_teacher.py b/backend/api/tests/test_teacher.py index ee41e6a9..6a439a88 100644 --- a/backend/api/tests/test_teacher.py +++ b/backend/api/tests/test_teacher.py @@ -1,52 +1,8 @@ import json -from django.utils import timezone from django.urls import reverse from rest_framework.test import APITestCase -from api.models.teacher import Teacher -from api.models.course import Course -from authentication.models import Faculty, User - - -def create_course(name, academic_startyear, description=None, parent_course=None): - """ - Create a Course with the given arguments. - """ - return Course.objects.create( - name=name, - academic_startyear=academic_startyear, - description=description, - parent_course=parent_course, - ) - - -def create_faculty(name): - """Create a Faculty with the given arguments.""" - return Faculty.objects.create(id=name, name=name) - - -def create_teacher(id, first_name, last_name, email, faculty=None, courses=None): - """ - Create a teacher with the given arguments. - """ - username = f"{first_name}_{last_name}" - teacher = Teacher.objects.create( - id=id, - first_name=first_name, - last_name=last_name, - username=username, - email=email, - create_time=timezone.now(), - ) - - if faculty is not None: - for fac in faculty: - teacher.faculties.add(fac) - - if courses is not None: - for cours in courses: - teacher.courses.add(cours) - - return teacher +from api.tests.helpers import create_teacher, create_course, create_faculty +from authentication.models import User class TeacherModelTests(APITestCase): @@ -171,7 +127,7 @@ def test_teacher_faculty(self): """ Able to retrieve faculty details of a single teacher. """ - # Create an teacher for testing with the name "Bob Peeters" + # Create a teacher for testing with the name "Bob Peeters" faculty = create_faculty(name="testing faculty") teacher = create_teacher( id=5, diff --git a/backend/api/views/assistant_view.py b/backend/api/views/assistant_view.py index 87328432..c4cdc200 100644 --- a/backend/api/views/assistant_view.py +++ b/backend/api/views/assistant_view.py @@ -1,11 +1,15 @@ +from django.utils.translation import gettext from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.request import Request from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import IsAdminUser +from drf_yasg.utils import swagger_auto_schema from api.permissions.assistant_permissions import AssistantPermission -from ..models.assistant import Assistant -from ..serializers.assistant_serializer import AssistantSerializer -from ..serializers.course_serializer import CourseSerializer +from api.models.assistant import Assistant +from api.serializers.assistant_serializer import AssistantSerializer, AssistantIDSerializer +from api.serializers.course_serializer import CourseSerializer +from authentication.serializers import UserIDSerializer class AssistantViewSet(ModelViewSet): @@ -14,6 +18,29 @@ class AssistantViewSet(ModelViewSet): serializer_class = AssistantSerializer permission_classes = [IsAdminUser | AssistantPermission] + @swagger_auto_schema(request_body=UserIDSerializer) + def create(self, request: Request, *args, **kwargs) -> Response: + """Add the student role to the user""" + serializer = UserIDSerializer( + data=request.data + ) + + if serializer.is_valid(raise_exception=True): + Assistant.create(serializer.validated_data.get('user')) + + return Response({ + "message": gettext("teachers.success.add") + }) + + @swagger_auto_schema(request_body=AssistantIDSerializer) + def destroy(self, request: Request, *args, **kwargs) -> Response: + """Delete the student role from the user""" + self.get_object().deactivate() + + return Response({ + "message": gettext("teachers.success.destroy") + }) + @action(detail=True, methods=["get"]) def courses(self, request, **_): """Returns a list of courses for the given assistant""" diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index 297401b9..a9522790 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -160,7 +160,7 @@ def _add_student(self, request: Request, **_): if serializer.is_valid(raise_exception=True): course.students.add( - serializer.validated_data["student_id"] + serializer.validated_data["student"] ) return Response({ @@ -181,7 +181,7 @@ def _remove_student(self, request: Request, **_): if serializer.is_valid(raise_exception=True): course.students.remove( - serializer.validated_data["student_id"] + serializer.validated_data["student"] ) return Response({ @@ -216,7 +216,7 @@ def _add_teacher(self, request, **_): if serializer.is_valid(raise_exception=True): course.teachers.add( - serializer.validated_data["teacher_id"] + serializer.validated_data["teacher"] ) return Response({ @@ -237,7 +237,7 @@ def _remove_teacher(self, request, **_): if serializer.is_valid(raise_exception=True): course.teachers.remove( - serializer.validated_data["teacher_id"] + serializer.validated_data["teacher"] ) return Response({ diff --git a/backend/api/views/group_view.py b/backend/api/views/group_view.py index 156f7b55..60902442 100644 --- a/backend/api/views/group_view.py +++ b/backend/api/views/group_view.py @@ -64,7 +64,7 @@ def _add_student(self, request, **_): # Validate the serializer if serializer.is_valid(raise_exception=True): group.students.add( - serializer.validated_data["student_id"] + serializer.validated_data["student"] ) return Response({ @@ -84,7 +84,7 @@ def _remove_student(self, request, **_): # Validate the serializer if serializer.is_valid(raise_exception=True): group.students.remove( - serializer.validated_data["student_id"] + serializer.validated_data["student"] ) return Response({ diff --git a/backend/api/views/student_view.py b/backend/api/views/student_view.py index 1261d6ab..ff1fba92 100644 --- a/backend/api/views/student_view.py +++ b/backend/api/views/student_view.py @@ -1,11 +1,13 @@ +from django.utils.translation import gettext from rest_framework import viewsets from rest_framework.decorators import action +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.permissions import IsAdminUser +from drf_yasg.utils import swagger_auto_schema from api.permissions.student_permissions import StudentPermission -from api.permissions.role_permissions import IsSameUser, IsTeacher from api.models.student import Student -from api.serializers.student_serializer import StudentSerializer +from api.serializers.student_serializer import StudentSerializer, CreateStudentSerializer, StudentIDSerializer from api.serializers.course_serializer import CourseSerializer from api.serializers.group_serializer import GroupSerializer @@ -15,6 +17,31 @@ class StudentViewSet(viewsets.ModelViewSet): serializer_class = StudentSerializer permission_classes = [IsAdminUser | StudentPermission] + @swagger_auto_schema(request_body=CreateStudentSerializer) + def create(self, request: Request, *args, **kwargs) -> Response: + """Add the student role to the user""" + serializer = CreateStudentSerializer( + data=request.data + ) + + if serializer.is_valid(raise_exception=True): + Student.create(serializer.validated_data.get('user'), + student_id=serializer.validated_data.get('student_id') + ) + + return Response({ + "message": gettext("students.success.add") + }) + + @swagger_auto_schema(request_body=StudentIDSerializer) + def destroy(self, request: Request, *args, **kwargs) -> Response: + """Delete the student role from the user""" + self.get_object().deactivate() + + return Response({ + "message": gettext("students.success.destroy") + }) + @action(detail=True) def courses(self, request, **_): """Returns a list of courses for the given student""" diff --git a/backend/api/views/teacher_view.py b/backend/api/views/teacher_view.py index cdc6160d..fcb63414 100644 --- a/backend/api/views/teacher_view.py +++ b/backend/api/views/teacher_view.py @@ -1,12 +1,16 @@ +from django.utils.translation import gettext from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.request import Request from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import IsAdminUser +from rest_framework.permissions import IsAuthenticated +from drf_yasg.utils import swagger_auto_schema from api.models.teacher import Teacher -from api.serializers.teacher_serializer import TeacherSerializer +from api.serializers.teacher_serializer import TeacherSerializer, TeacherIDSerializer from api.serializers.course_serializer import CourseSerializer from api.permissions.teacher_permissions import TeacherPermission -from rest_framework.permissions import IsAuthenticated +from authentication.serializers import UserIDSerializer class TeacherViewSet(ModelViewSet): @@ -14,6 +18,29 @@ class TeacherViewSet(ModelViewSet): serializer_class = TeacherSerializer permission_classes = [IsAdminUser | TeacherPermission] + @swagger_auto_schema(request_body=UserIDSerializer) + def create(self, request: Request, *args, **kwargs) -> Response: + """Add the student role to the user""" + serializer = UserIDSerializer( + data=request.data + ) + + if serializer.is_valid(raise_exception=True): + Teacher.create(serializer.validated_data.get('user')) + + return Response({ + "message": gettext("teachers.success.add") + }) + + @swagger_auto_schema(request_body=TeacherIDSerializer) + def destroy(self, request: Request, *args, **kwargs) -> Response: + """Delete the student role from the user""" + self.get_object().deactivate() + + return Response({ + "message": gettext("teachers.success.destroy") + }) + @action(detail=True, methods=["get"], permission_classes=[IsAuthenticated]) def courses(self, request, pk=None): """Returns a list of courses for the given teacher""" diff --git a/backend/authentication/models.py b/backend/authentication/models.py index e7f3e7dd..fa0ce29a 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -53,7 +53,7 @@ def roles(self): # ...that inherit the User class. if model is not self.__class__ if issubclass(model, self.__class__) - if model.objects.filter(id=self.id).exists() + if model.objects.filter(id=self.id, is_active=True).exists() ] @staticmethod diff --git a/frontend/src/components/courses/CourseGeneralCard.vue b/frontend/src/components/courses/CourseGeneralCard.vue index a29ccfce..a2bb3ac2 100644 --- a/frontend/src/components/courses/CourseGeneralCard.vue +++ b/frontend/src/components/courses/CourseGeneralCard.vue @@ -25,7 +25,12 @@ const images = Object.keys( * @param faculty */ function getFacultyIcon(faculty: Faculty): string { - return images.find((image) => image.includes(faculty.id)) ?? ''; + return ( + images.find((image) => { + image = image.replace('/src/assets/img/faculties/', ''); + return image === faculty.id + '.png'; + }) ?? '' + ); } diff --git a/frontend/src/composables/services/students.service.ts b/frontend/src/composables/services/students.service.ts index d1931a07..dcf0ba53 100644 --- a/frontend/src/composables/services/students.service.ts +++ b/frontend/src/composables/services/students.service.ts @@ -56,22 +56,22 @@ export function useStudents(): StudentsState { async function studentJoinCourse(courseId: string, studentId: string): Promise { const endpoint = endpoints.students.byCourse.replace('{courseId}', courseId); - await create(endpoint, { student_id: studentId }, response, Response.fromJSON); + await create(endpoint, { student: studentId }, response, Response.fromJSON); } async function studentLeaveCourse(courseId: string, studentId: string): Promise { const endpoint = endpoints.students.byCourse.replace('{courseId}', courseId); - await deleteIdWithData(endpoint, { student_id: studentId }, response, Response.fromJSON); + await deleteIdWithData(endpoint, { student: studentId }, response, Response.fromJSON); } async function studentJoinGroup(groupId: string, studentId: string): Promise { const endpoint = endpoints.students.byGroup.replace('{groupId}', groupId); - await create(endpoint, { student_id: studentId }, response, Response.fromJSON); + await create(endpoint, { student: studentId }, response, Response.fromJSON); } async function studentLeaveGroup(groupId: string, studentId: string): Promise { const endpoint = endpoints.students.byGroup.replace('{groupId}', groupId); - await deleteIdWithData(endpoint, { student_id: studentId }, response, Response.fromJSON); + await deleteIdWithData(endpoint, { student: studentId }, response, Response.fromJSON); } async function createStudent(studentData: Student): Promise {