diff --git a/backend/api/management/commands/__init__.py b/backend/api/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/management/commands/makeAdmin.py b/backend/api/management/commands/makeAdmin.py new file mode 100644 index 00000000..fc24c7fb --- /dev/null +++ b/backend/api/management/commands/makeAdmin.py @@ -0,0 +1,21 @@ +from django.core.management.base import BaseCommand +from api.models.student import Student + + +class Command(BaseCommand): + + help = 'seed the db with data' + + def add_arguments(self, parser): + parser.add_argument('username', type=str, help='The username of the student user to make admin') + + 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 + student = student.get() + student.is_staff = True + student.save() + self.stdout.write(self.style.SUCCESS('Successfully made yourself admin!')) diff --git a/backend/api/management/commands/seedDb.py b/backend/api/management/commands/seedDb.py new file mode 100644 index 00000000..ca35547c --- /dev/null +++ b/backend/api/management/commands/seedDb.py @@ -0,0 +1,467 @@ +from django.core.management.base import BaseCommand +from faker import Faker +from django.utils import timezone +from django.db.models import Max +from faker.providers import BaseProvider, DynamicProvider +import random + +from authentication.models import Faculty +from api.models.student import Student +from api.models.assistant import Assistant +from api.models.teacher import Teacher +from api.models.course import Course +from api.models.group import Group +from api.models.project import Project +from api.models.submission import Submission +from api.models.checks import FileExtension, StructureCheck +fake = Faker() + +# Faker.seed(4321) # set to make data same each time + +faculty_provider = DynamicProvider( + provider_name="faculty_provider", + elements=Faculty.objects.all(), +) + +student_provider = DynamicProvider( + provider_name="student_provider", + elements=Student.objects.all(), +) + +assistant_provider = DynamicProvider( + provider_name="assistant_provider", + elements=Assistant.objects.all(), +) + +teacher_provider = DynamicProvider( + provider_name="teacher_provider", + elements=Teacher.objects.all(), +) + +course_provider = DynamicProvider( + provider_name="course_provider", + elements=Course.objects.all(), +) + +project_provider = DynamicProvider( + provider_name="project_provider", + elements=Project.objects.all(), +) + +group_provider = DynamicProvider( + provider_name="group_provider", + elements=Group.objects.all(), +) + +Submission_provider = DynamicProvider( + provider_name="Submission_provider", + elements=Submission.objects.all(), +) + +fileExtension_provider = DynamicProvider( + provider_name="fileExtension_provider", + elements=FileExtension.objects.all(), +) + +structureCheck_provider = DynamicProvider( + provider_name="structureCheck_provider", + elements=StructureCheck.objects.all(), +) + + +# create new provider class +class Providers(BaseProvider): + + MAX_TRIES = 1000 + min_id = 1 + max_id = 9_999_999_999_999_999_999_999 + + min_salt = 1 + max_salt = 100_000 + + def provide_teacher(self, errHandler, min_faculty=1, max_faculty=2, staf_prob=0.1): + """ + Create a teacher with the given arguments. + """ + tries = 0 + while tries < self.MAX_TRIES: + try: + first = fake.first_name() + last = fake.last_name() + id = fake.unique.random_int(min=self.min_id, max=self.max_id) + faculty = [ + fake.faculty_provider().id for _ in range(0, fake.random_int(min=min_faculty, max=max_faculty)) + ] # generate 1 or 2 facultys + username = f"{first}_{last}_{fake.random_int(min=self.min_salt, max=self.max_salt)}" + teacher = Teacher.objects.create( + id=id, + first_name=first, + last_name=last, + username=username, + email=username + "@example.com", + create_time=timezone.now(), + last_enrolled=timezone.now().year, + is_staff=fake.boolean(chance_of_getting_true=staf_prob) + ) + + if faculty is not None: + for fac in faculty: + teacher.faculties.add(fac) + + return teacher + except Exception: + tries += 1 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique teacher.")) + + def provide_assistant(self, errHandler, min_faculty=1, max_faculty=3, staf_prob=0.01): + """ + Create a assistant with the given arguments. + """ + tries = 0 + while tries < self.MAX_TRIES: + try: + first = fake.first_name() + last = fake.last_name() + id = fake.unique.random_int(min=self.min_id, max=self.max_id) + faculty = [ + fake.faculty_provider().id for _ in range(0, fake.random_int(min=min_faculty, max=max_faculty)) + ] # generate 1 or 2 or 3 facultys + username = f"{first}_{last}_{fake.random_int(min=self.min_salt, max=self.max_salt)}" + assistant = Assistant.objects.create( + id=id, + first_name=first, + last_name=last, + username=username, + email=username + "@example.com", + create_time=timezone.now(), + last_enrolled=timezone.now().year, + is_staff=fake.boolean(chance_of_getting_true=staf_prob) + ) + + if faculty is not None: + for fac in faculty: + assistant.faculties.add(fac) + + return assistant + except Exception: + tries += 1 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique assistant.")) + + def provide_student(self, errHandler, min_faculty=1, max_faculty=3, staf_prob=0.01): + """ + Create a student with the given arguments. + """ + tries = 0 + while tries < self.MAX_TRIES: + try: + first = fake.first_name() + last = fake.last_name() + id = fake.unique.random_int(min=self.min_id, max=self.max_id) + faculty = [ + fake.faculty_provider().id for _ in range(0, fake.random_int(min=min_faculty, max=max_faculty)) + ] # generate 1 or 2 or 3 facultys + username = f"{first}_{last}_{fake.random_int(min=self.min_salt, max=self.max_salt)}" + student = Student.objects.create( + id=id, + first_name=first, + last_name=last, + username=username, + email=username + "@example.com", + create_time=timezone.now(), + last_enrolled=timezone.now().year, + student_id=id, + is_staff=fake.boolean(chance_of_getting_true=staf_prob) + ) + + if faculty is not None: + for fac in faculty: + student.faculties.add(fac) + + return student + except Exception: + tries += 1 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique student.")) + + def provide_course( + self, + errHandler, + min_year_passed=0, + max_year_passed=3, + min_students=1, + max_students=100, + min_teachers=1, + max_teachers=5, + min_assistants=0, + max_assistants=5): + """ + Create a Course with the given arguments. + """ + tries = 0 + while tries < self.MAX_TRIES: + try: + parent_course = None # TODO make this sometimes a course + course_name = fake.catch_phrase() + course: Course = Course.objects.create( + name=course_name, + academic_startyear=timezone.now().year - fake.random_int(min=min_year_passed, max=max_year_passed), + faculty=fake.faculty_provider(), + description=fake.paragraph(), + parent_course=parent_course + ) + + # add students + student_count = fake.random_int(min=min_students, max=max_students) + while course.students.count() < student_count: + student = fake.student_provider() + if student not in course.students.all(): + course.students.add(student) + + # add teachers + teacher_count = fake.random_int(min=min_teachers, max=max_teachers) + while course.teachers.count() < teacher_count: + teacher = fake.teacher_provider() + if teacher not in course.teachers.all(): + course.teachers.add(teacher) + + # add assistants + assistant_count = fake.random_int(min=min_assistants, max=max_assistants) + while course.assistants.count() < assistant_count: + assistant = fake.assistant_provider() + if assistant not in course.assistants.all(): + course.assistants.add(assistant) + + # print(course_name) + return course + except Exception: + tries += 1 + errHandler.stdout.write(errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique Course.")) + + def provide_project( + self, + errHandler, + min_start_date_dev=-100, + max_start_date_dev=100, + min_deadline_dev=1, + max_deadline_dev=100, + visible_prob=80, + archived_prob=10, + score_visible_prob=30, + locked_groups_prob=30, + min_max_score=1, + max_max_score=100, + min_group_size=1, + max_group_size=15): + """Create a Project with the given arguments.""" + tries = 0 + while tries < self.MAX_TRIES: + try: + start_date = timezone.now() + timezone.timedelta( + days=fake.random_int(min=min_start_date_dev, max=max_start_date_dev)) + deadline = start_date + timezone.timedelta(days=fake.random_int(min=min_deadline_dev, max=max_deadline_dev)) + course = fake.course_provider() + return Project.objects.create( + name=fake.catch_phrase(), + description=fake.paragraph(), + visible=fake.boolean(chance_of_getting_true=visible_prob), + archived=fake.boolean(chance_of_getting_true=archived_prob), + score_visible=fake.boolean(chance_of_getting_true=score_visible_prob), + locked_groups=fake.boolean(chance_of_getting_true=locked_groups_prob), + deadline=deadline, + course=course, + start_date=start_date, + max_score=fake.random_int(min=min_max_score, max=max_max_score), + group_size=fake.random_int(min=min_group_size, max=max_group_size) + ) + except Exception: + tries += 1 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique project.")) + + def provide_group(self, errHandler, min_score=0): + """Create a Group with the given arguments.""" + tries = 0 + while tries < self.MAX_TRIES: + try: + project: Project = fake.project_provider() + group: Group = Group.objects.create( + project=project, + score=fake.random_int(min=min_score, max=project.max_score) + ) + + max_group_size = group.project.group_size + + students = group.project.course.students.all() + groups = group.project.groups.all() + joined_students = [] + + for groupStudents in groups: + joined_students.extend(groupStudents.students.all()) + + students_not_in_group = [student for student in students if student not in joined_students] + + if len(students_not_in_group) == 0: + pass + elif len(students_not_in_group) < max_group_size: + group.students.extend(students_not_in_group) + else: + for _ in range(0, max_group_size): + random_student = students_not_in_group[fake.random_int(min=0, max=len(students_not_in_group))] + group.students.add(random_student) + students_not_in_group.remove(random_student) + + return group + except Exception: + tries += 1 + errHandler.stdout.write(errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique group.")) + + def provide_submission(self, errHandler, struct_check_passed_prob=70): + """Create an Submission with the given arguments.""" + tries = 0 + while tries < self.MAX_TRIES: + try: + group: Group = fake.group_provider() + # Generate a random timestamp between start and end timestamps + random_timestamp = random.uniform(group.project.start_date.timestamp(), group.project.deadline.timestamp()) + + # Convert the random timestamp back to a datetime object + random_datetime = timezone.make_aware(timezone.datetime.fromtimestamp(random_timestamp)) + + # get all submisions of this group + max_submission_number = Submission.objects.filter( + group=group + ).aggregate(Max('submission_number'))['submission_number__max'] or 0 + + # print(fake.zip(uncompressed_size=10, num_files=5, min_file_size=1)) + return Submission.objects.create( # TODO add fake files + group=group, + submission_time=random_datetime, + structure_checks_passed=fake.boolean(chance_of_getting_true=struct_check_passed_prob), + submission_number=max_submission_number + 1 + ) + except Exception: + tries += 1 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique submission.")) + + def provide_fileExtension(self, errHandler): + """ + Create a FileExtension with the given arguments. + """ + tries = 0 + while tries < self.MAX_TRIES: + try: + ext = fake.file_extension() + return FileExtension.objects.create(extension=ext) + except Exception: + tries += 1 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique file extension.")) + + def provide_structure_check(self, errHandler, min_extensions=1, max_extensions=5, min_path_depth=1, max_path_depth=10): + """ + Create a StructureCheck with the given arguments. + """ + tries = 0 + while tries < self.MAX_TRIES: + try: + check = StructureCheck.objects.create( + name=fake.file_path(extension="", depth=fake.random_int(min=min_path_depth, max=max_path_depth)), + project=fake.project_provider() + ) + + obligated_extensions = [] + obl_amount = fake.random_int(min=min_extensions, max=max_extensions) + while len(obligated_extensions) < obl_amount: + extension = fake.fileExtension_provider() + if extension not in obligated_extensions: + obligated_extensions.append(extension) + + blocked_extensions = [] + blo_amount = fake.random_int(min=min_extensions, max=max_extensions) + while len(blocked_extensions) < blo_amount: + extension = fake.fileExtension_provider() + if extension not in blocked_extensions and extension not in obligated_extensions: + blocked_extensions.append(extension) + + 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 + errHandler.stdout.write( + errHandler.style.WARNING("Exceeded maximum number of attempts to generate a unique structure check.")) + + +def update_providers(): + faculty_provider.elements = Faculty.objects.all() + student_provider.elements = Student.objects.all() + assistant_provider.elements = Assistant.objects.all() + teacher_provider.elements = Teacher.objects.all() + course_provider.elements = Course.objects.all() + project_provider.elements = Project.objects.all() + group_provider.elements = Group.objects.all() + Submission_provider.elements = Submission.objects.all() + fileExtension_provider.elements = FileExtension.objects.all() + structureCheck_provider.elements = StructureCheck.objects.all() + + +# add new providers to faker instance +fake.add_provider(Providers) +fake.add_provider(faculty_provider) +fake.add_provider(student_provider) +fake.add_provider(assistant_provider) +fake.add_provider(teacher_provider) +fake.add_provider(course_provider) +fake.add_provider(project_provider) +fake.add_provider(group_provider) +fake.add_provider(Submission_provider) +fake.add_provider(fileExtension_provider) +fake.add_provider(structureCheck_provider) + + +class Command(BaseCommand): + help = 'seed the db with data' + + def seed_data(self, amount, provider_function): + for _ in range(amount): + provider_function(self) + update_providers() + + def handle(self, *args, **options): + # TODO maybey take as option + # amount_of_students = 10_000 + # amount_of_assistants = 1_000 + # amount_of_teachers = 1_000 + # amount_of_courses = 1_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 = 10_000 + + amount_of_students = 1 + amount_of_assistants = 0 + amount_of_teachers = 0 + amount_of_courses = 0 + 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) + 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/seeders/__init__.py b/backend/api/seeders/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/poetry.lock b/backend/poetry.lock index d2d6ad5d..df20168f 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -376,13 +376,13 @@ files = [ [[package]] name = "django" -version = "5.0.3" +version = "5.0.4" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.0.3-py3-none-any.whl", hash = "sha256:5c7d748ad113a81b2d44750ccc41edc14e933f56581683db548c9257e078cc83"}, - {file = "Django-5.0.3.tar.gz", hash = "sha256:5fb37580dcf4a262f9258c1f4373819aacca906431f505e4688e37f3a99195df"}, + {file = "Django-5.0.4-py3-none-any.whl", hash = "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775"}, + {file = "Django-5.0.4.tar.gz", hash = "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd"}, ] [package.dependencies] @@ -429,6 +429,21 @@ djangorestframework = ">=3.5.4" openapi-codec = ">=1.3.1" simplejson = "*" +[[package]] +name = "django-seed" +version = "0.3.1" +description = "Seed your Django project with fake data" +optional = false +python-versions = "*" +files = [ + {file = "django-seed-0.3.1.tar.gz", hash = "sha256:93e65d2c10449c464b83e9031be02763ec10e786aa064d649c4dd5ad06fa0eea"}, +] + +[package.dependencies] +django = ">=1.11" +Faker = ">=0.7.7" +toposort = ">=1.5" + [[package]] name = "django-sslserver" version = "0.22" @@ -504,6 +519,20 @@ uritemplate = ">=3.0.0" coreapi = ["coreapi (>=2.3.3)", "coreschema (>=0.0.4)"] validation = ["swagger-spec-validator (>=2.1.0)"] +[[package]] +name = "faker" +version = "24.7.1" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-24.7.1-py3-none-any.whl", hash = "sha256:73f2bd886e8ce751e660c7d37a6c0a128aab5e1551359335bb79cfea0f4fabfc"}, + {file = "Faker-24.7.1.tar.gz", hash = "sha256:39d34c63f0d62ed574161e23fe32008917b923d18098ce94c2650fe16463b7d5"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "filelock" version = "3.13.3" @@ -1220,6 +1249,17 @@ dev = ["build", "flake8"] doc = ["sphinx"] test = ["pytest", "pytest-cov"] +[[package]] +name = "toposort" +version = "1.10" +description = "Implements a topological sort algorithm." +optional = false +python-versions = "*" +files = [ + {file = "toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87"}, + {file = "toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd"}, +] + [[package]] name = "tox" version = "4.14.2" @@ -1355,4 +1395,4 @@ brotli = ["Brotli"] [metadata] lock-version = "2.0" python-versions = "^3.11.4" -content-hash = "2341567194c05d05a9617a1b812d26c63366ba3cc07ae842859102d9eadac972" +content-hash = "fc72c38f240e3a93e01f87850a6c16277b594999fd49891fc71107bd9e5ceeaf" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4dcfceac..7cd91ced 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -23,8 +23,10 @@ gunicorn = "^21.2.0" whitenoise = "^6.6.0" flake8 = "^7.0.0" celery-types = "^0.22.0" +faker = "^24.7.1" +django-seed = "^0.3.1" [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api"