From 8f5ab6a66b5fe3b55de8d2b3186b445ad001b3d2 Mon Sep 17 00:00:00 2001 From: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:41:24 +0200 Subject: [PATCH 01/16] Late September Update Fix (#880) * Feat(kontres)/add image to bookable item (#785) * added optional image to bookable item model * added update method in serializer to handle new images * linting * remove update method for images * Feat(kontres)/add approved by (#786) * added approved by field * endpoint will now set approved by * serializer will return full user object in approved_by_detail * created test for approved by * migration * remove unnecessary code * removed write-only field in approved-by context * Create minutes for Codex (#787) * init * format * Feat(minute)/viewset (#788) * added richer reponse on post and put * added to admin panel * added filter for minute * Feat(kontres)/add notification (#790) * created methods for sending notification to admin and user * endpoint will now send notification if needed * add migrations for new notification types * Memberships with fines activated (#791) init * Feat(user)/user bio (#758) * Created model, serializer and view for user-bio * Created user bio model and made migrations * Created user bio serializer + viewsets + added new endpoint * Tested create method + added bio serializer to user serializer * Format * Created update method and started testing * Debugging test failures in user retrieve * fixed model error * Created user_bio_factory + started testing put method * Created fixture for UserBio * Created custom excpetion for duplicate user bio * Added permissions and inherited from BaseModel * Modularized serializer for bio * Use correct serializers in viewset + added destroy method * Finished testing bio viewset integration + format * Changed environent file to .env to avoid pushing up keys * Fix: Flipped assertion statement in test, since user bio should not be deleted * skiped buggy test from kontres * added mark to pytest.skip * Moved keys to .env file and reverted docker variables * Skip buggy kontres test * format * Added str method to user_bio * Removed unused imports * format * Changed user relation to a OneToOne-field (same affect as ForeignKey(unique=True) + removed check for duplicate bio in serializer * Migrations + changed assertion status code in duplicate bio test (could try catch in serializer to produce 400 status code) * format * format * Changed limit for description 50 -> 500 + migrations * Migrate * added id to serializer * merged leaf nodes in migrations * format --------- Co-authored-by: Ester2109 <126612066+Ester2109@users.noreply.github.com> Co-authored-by: Mads Nylund Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Co-authored-by: Tam Le * Update CHANGELOG.md * added filter for allowed photos for user (#794) added filter for allowed photos * Upped payment time when coming from waiting list (#796) * fixed paymenttime saved to db (#798) * fixed bug (#800) * Disallow users to unregister when payment is done (#802) added 400 status code for deleting paid registration * update changelog * Added serializer for category in event (#804) added serializer for category in event * Permission middelware (#806) * added a check for existing user and id on request * format * Permission refactor of QR Codes (#807) * added permissions to qr code and refactored viewset * format * removed unused imports * Permissions for payment orders (#808) * added read permissions * added permissions for payment order and tests * format * chore(iac): updated docs and force https (#810) chore: updated docs and force https * feat(iac): add terraform guardrails so index don't nuke our infra (#811) feat: add guardrails so index don't fup * Automatic registration for new users with Feide (#809) * started on feide registration endpoint * made endpoint for creating user with Feide * added test for parse group * finished * format * removes three years if in digtrans * changelog update * Feide env variables Terraform (#814) added feid env variables * added delete endpoint for file (#815) * added delete endpoint for file * Trigger Build * changed workflow to checkout v4 * changed from docker-compose to docker compose * Update CHANGELOG.md * format * format * fixed permission for committee leaders for group forms * updated csv for forms (#818) * Permission for group forms and news (#820) added permission for committees to create news, and all leaders of groups to create group forms * Update reservation_seralizer.py (#822) * Update reservation_seralizer.py * Fixed linting * Put a band aid on it *smack* * Removed blank line.. * ???? * Group ownership of Minutes (#847) * Refactor MinuteFactory to include group field, and added validation for checkin group access * added validation for POST request * Changed endpoint response (#846) * Changed endpoint response * Fixed test * Update test_reservation_integration.py --------- Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * updated changelog.md * finished events now appear in the correct order (newest first) (#849) * finished events now appear in the correct order (newest first) * added description of change in changelog * fixed formatting * updated method to use Django ORM instead of using python methods * Implement Swagger (#858) * started on removing choiceenums * refactored cheatsheet and membership * refacotered strike enum * refactored Groups enum * removed AppModel choiceenum * added swagger * Swagger GitHub Action (#860) * added github action for checking if Swagger is up * new action * try another * tried implementing check for container * added curl to docker image * added check if swagger is up * test if swagger does not get status code 200 * added ?format=openai to trigger error * checking that the request is working * updated CHANGELOG.md * Add new app (#862) * added script for adding new app to Lepton * added command to Makefile * Upgrade all dependencies to latest (#857) * Add endpoint to create new group as admin Signed-off-by: Tmpecho * Upgrade all dependencies to latest Signed-off-by: Tmpecho * remove bad exception handling in serializers/group.py * fix inheritance ordering in views/group.py * refactored group integration test, added non_public_groups to enums * fix linting * reformat files * remove unused import in groups/views/group.py * Upgrade dependency "black" Signed-off-by: Tmpecho * Upgrade dependency "sentry-sdk" Signed-off-by: Tmpecho * Upgrade dependency "azure-storage-blob" and remove outdated comment in requirements.txt Signed-off-by: Tmpecho * Upgrade all non-django dependencies Signed-off-by: Tmpecho * Upgrade dependency "Django" Signed-off-by: Tmpecho * Upgrade dependencies and remove ignored version from docker-compose.yml Signed-off-by: Tmpecho --------- Signed-off-by: Tmpecho Co-authored-by: 1Cezzo * Allow HS members to create a new group (#864) * Add endpoint to create new group as admin Signed-off-by: Tmpecho * remove bad exception handling in serializers/group.py * fix inheritance ordering in views/group.py * refactored group integration test, added non_public_groups to enums * fix linting * reformat files * remove unused import in groups/views/group.py --------- Signed-off-by: Tmpecho Co-authored-by: 1Cezzo * App Script Fix (#875) added serializers dir to script * Event registration payment orders (#876) * added list of payment orders for registrations * update CHANGELOG.md * chore(deps): update python-dotenv requirement from ~=0.21.1 to ~=1.0.1 (#871) Updates the requirements on [python-dotenv](https://github.com/theskumar/python-dotenv) to permit the latest version. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v0.21.1...v1.0.1) --- updated-dependencies: - dependency-name: python-dotenv dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Chore(deps): Bump sentry-sdk from 1.14.0 to 2.8.0 (#866) Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.14.0 to 2.8.0. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/1.14.0...2.8.0) --- updated-dependencies: - dependency-name: sentry-sdk dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Codex Course (#852) * added models for course and registration, and viewset for course and tests * added validation for date checking for courses * added viewset for registration for codex courses * removed unused fields from course model * removed unused imports * added API error mixins as mother clas * fixed error mixin * refactored to event model * fixed wrong import * fixed tests * format * skipped broken tests, must be refactored * updated CHANGELOG.md * format * fixed filtering of groups and made tests --------- Signed-off-by: Tmpecho Signed-off-by: dependabot[bot] Co-authored-by: Erik Skjellevik <98759397+eriskjel@users.noreply.github.com> Co-authored-by: haruixu <114171733+haruixu@users.noreply.github.com> Co-authored-by: Ester2109 <126612066+Ester2109@users.noreply.github.com> Co-authored-by: Tam Le Co-authored-by: martcl Co-authored-by: Frikk Balder <33499052+MindChirp@users.noreply.github.com> Co-authored-by: Emil Johnsen <111747340+1Cezzo@users.noreply.github.com> Co-authored-by: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> Co-authored-by: 1Cezzo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/group/filters/group.py | 6 ++-- app/tests/groups/test_group_integration.py | 35 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/group/filters/group.py b/app/group/filters/group.py index fd3a7904..2164e5c7 100644 --- a/app/group/filters/group.py +++ b/app/group/filters/group.py @@ -15,11 +15,9 @@ class Meta: fields = ["type", "overview"] def filter_type(self, queryset, _, value): - """Django Rest does not know hot to convert incoming string values into EnumChoiceField values and we must do this manually.""" - mapped = list(GroupType[v] for v in value) - return queryset.filter(type__in=mapped) + return queryset.filter(type__in=value) - def filter_overview(self, queryset, _): + def filter_overview(self, queryset, *_): if is_admin_user(self.request): return queryset return queryset.filter(type__in=GroupType.public_groups()) diff --git a/app/tests/groups/test_group_integration.py b/app/tests/groups/test_group_integration.py index 96675cf9..b7be6840 100644 --- a/app/tests/groups/test_group_integration.py +++ b/app/tests/groups/test_group_integration.py @@ -4,6 +4,7 @@ from app.common.enums import AdminGroup from app.common.enums import NativeGroupType as GroupType +from app.group.factories import GroupFactory from app.util.test_utils import get_api_client GROUP_URL = "/groups/" @@ -13,6 +14,14 @@ def _get_group_url(group=None): return f"{GROUP_URL}{group.slug}/" if (group) else f"{GROUP_URL}" +def _get_overview_group_url(): + return f"{GROUP_URL}?overview=true" + + +def _get_group_type_filtered_list(slug: str): + return f"{GROUP_URL}?type={slug}" + + def _get_group_post_data(group): return { "name": group.name, @@ -38,6 +47,32 @@ def test_list_as_anonymous_user(default_client): assert response.status_code == status.HTTP_200_OK +@pytest.mark.django_db +def test_list_overview_of_groups_as_member(member): + """Tests if a member can list an overview of groups""" + + client = get_api_client(user=member) + url = _get_overview_group_url() + response = client.get(url) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_list_type_of_groups_as_member(member): + """Tests if a member can list a type of groups""" + + GroupFactory(type=GroupType.BOARD) + GroupFactory(type=GroupType.COMMITTEE) + + client = get_api_client(user=member) + url = _get_group_type_filtered_list(GroupType.BOARD.value) + response = client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 + + @pytest.mark.django_db def test_retrieve_as_anonymous_user(default_client, group): """Tests if an anonymous user can retrieve a group""" From 3af9f5e0b9154111b9468b2ed70e23477e64dc38 Mon Sep 17 00:00:00 2001 From: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:33:05 +0200 Subject: [PATCH 02/16] Cheatsheet List Fix (#881) * Feat(kontres)/add image to bookable item (#785) * added optional image to bookable item model * added update method in serializer to handle new images * linting * remove update method for images * Feat(kontres)/add approved by (#786) * added approved by field * endpoint will now set approved by * serializer will return full user object in approved_by_detail * created test for approved by * migration * remove unnecessary code * removed write-only field in approved-by context * Create minutes for Codex (#787) * init * format * Feat(minute)/viewset (#788) * added richer reponse on post and put * added to admin panel * added filter for minute * Feat(kontres)/add notification (#790) * created methods for sending notification to admin and user * endpoint will now send notification if needed * add migrations for new notification types * Memberships with fines activated (#791) init * Feat(user)/user bio (#758) * Created model, serializer and view for user-bio * Created user bio model and made migrations * Created user bio serializer + viewsets + added new endpoint * Tested create method + added bio serializer to user serializer * Format * Created update method and started testing * Debugging test failures in user retrieve * fixed model error * Created user_bio_factory + started testing put method * Created fixture for UserBio * Created custom excpetion for duplicate user bio * Added permissions and inherited from BaseModel * Modularized serializer for bio * Use correct serializers in viewset + added destroy method * Finished testing bio viewset integration + format * Changed environent file to .env to avoid pushing up keys * Fix: Flipped assertion statement in test, since user bio should not be deleted * skiped buggy test from kontres * added mark to pytest.skip * Moved keys to .env file and reverted docker variables * Skip buggy kontres test * format * Added str method to user_bio * Removed unused imports * format * Changed user relation to a OneToOne-field (same affect as ForeignKey(unique=True) + removed check for duplicate bio in serializer * Migrations + changed assertion status code in duplicate bio test (could try catch in serializer to produce 400 status code) * format * format * Changed limit for description 50 -> 500 + migrations * Migrate * added id to serializer * merged leaf nodes in migrations * format --------- Co-authored-by: Ester2109 <126612066+Ester2109@users.noreply.github.com> Co-authored-by: Mads Nylund Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Co-authored-by: Tam Le * Update CHANGELOG.md * added filter for allowed photos for user (#794) added filter for allowed photos * Upped payment time when coming from waiting list (#796) * fixed paymenttime saved to db (#798) * fixed bug (#800) * Disallow users to unregister when payment is done (#802) added 400 status code for deleting paid registration * update changelog * Added serializer for category in event (#804) added serializer for category in event * Permission middelware (#806) * added a check for existing user and id on request * format * Permission refactor of QR Codes (#807) * added permissions to qr code and refactored viewset * format * removed unused imports * Permissions for payment orders (#808) * added read permissions * added permissions for payment order and tests * format * chore(iac): updated docs and force https (#810) chore: updated docs and force https * feat(iac): add terraform guardrails so index don't nuke our infra (#811) feat: add guardrails so index don't fup * Automatic registration for new users with Feide (#809) * started on feide registration endpoint * made endpoint for creating user with Feide * added test for parse group * finished * format * removes three years if in digtrans * changelog update * Feide env variables Terraform (#814) added feid env variables * added delete endpoint for file (#815) * added delete endpoint for file * Trigger Build * changed workflow to checkout v4 * changed from docker-compose to docker compose * Update CHANGELOG.md * format * format * fixed permission for committee leaders for group forms * updated csv for forms (#818) * Permission for group forms and news (#820) added permission for committees to create news, and all leaders of groups to create group forms * Update reservation_seralizer.py (#822) * Update reservation_seralizer.py * Fixed linting * Put a band aid on it *smack* * Removed blank line.. * ???? * Group ownership of Minutes (#847) * Refactor MinuteFactory to include group field, and added validation for checkin group access * added validation for POST request * Changed endpoint response (#846) * Changed endpoint response * Fixed test * Update test_reservation_integration.py --------- Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * updated changelog.md * finished events now appear in the correct order (newest first) (#849) * finished events now appear in the correct order (newest first) * added description of change in changelog * fixed formatting * updated method to use Django ORM instead of using python methods * Implement Swagger (#858) * started on removing choiceenums * refactored cheatsheet and membership * refacotered strike enum * refactored Groups enum * removed AppModel choiceenum * added swagger * Swagger GitHub Action (#860) * added github action for checking if Swagger is up * new action * try another * tried implementing check for container * added curl to docker image * added check if swagger is up * test if swagger does not get status code 200 * added ?format=openai to trigger error * checking that the request is working * updated CHANGELOG.md * Add new app (#862) * added script for adding new app to Lepton * added command to Makefile * Upgrade all dependencies to latest (#857) * Add endpoint to create new group as admin Signed-off-by: Tmpecho * Upgrade all dependencies to latest Signed-off-by: Tmpecho * remove bad exception handling in serializers/group.py * fix inheritance ordering in views/group.py * refactored group integration test, added non_public_groups to enums * fix linting * reformat files * remove unused import in groups/views/group.py * Upgrade dependency "black" Signed-off-by: Tmpecho * Upgrade dependency "sentry-sdk" Signed-off-by: Tmpecho * Upgrade dependency "azure-storage-blob" and remove outdated comment in requirements.txt Signed-off-by: Tmpecho * Upgrade all non-django dependencies Signed-off-by: Tmpecho * Upgrade dependency "Django" Signed-off-by: Tmpecho * Upgrade dependencies and remove ignored version from docker-compose.yml Signed-off-by: Tmpecho --------- Signed-off-by: Tmpecho Co-authored-by: 1Cezzo * Allow HS members to create a new group (#864) * Add endpoint to create new group as admin Signed-off-by: Tmpecho * remove bad exception handling in serializers/group.py * fix inheritance ordering in views/group.py * refactored group integration test, added non_public_groups to enums * fix linting * reformat files * remove unused import in groups/views/group.py --------- Signed-off-by: Tmpecho Co-authored-by: 1Cezzo * App Script Fix (#875) added serializers dir to script * Event registration payment orders (#876) * added list of payment orders for registrations * update CHANGELOG.md * chore(deps): update python-dotenv requirement from ~=0.21.1 to ~=1.0.1 (#871) Updates the requirements on [python-dotenv](https://github.com/theskumar/python-dotenv) to permit the latest version. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v0.21.1...v1.0.1) --- updated-dependencies: - dependency-name: python-dotenv dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Chore(deps): Bump sentry-sdk from 1.14.0 to 2.8.0 (#866) Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.14.0 to 2.8.0. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/1.14.0...2.8.0) --- updated-dependencies: - dependency-name: sentry-sdk dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Codex Course (#852) * added models for course and registration, and viewset for course and tests * added validation for date checking for courses * added viewset for registration for codex courses * removed unused fields from course model * removed unused imports * added API error mixins as mother clas * fixed error mixin * refactored to event model * fixed wrong import * fixed tests * format * skipped broken tests, must be refactored * updated CHANGELOG.md * format * fixed filtering of groups and made tests * fixed list endpoint for cheatsheets * trigger * format --------- Signed-off-by: Tmpecho Signed-off-by: dependabot[bot] Co-authored-by: Erik Skjellevik <98759397+eriskjel@users.noreply.github.com> Co-authored-by: haruixu <114171733+haruixu@users.noreply.github.com> Co-authored-by: Ester2109 <126612066+Ester2109@users.noreply.github.com> Co-authored-by: Tam Le Co-authored-by: martcl Co-authored-by: Frikk Balder <33499052+MindChirp@users.noreply.github.com> Co-authored-by: Emil Johnsen <111747340+1Cezzo@users.noreply.github.com> Co-authored-by: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> Co-authored-by: 1Cezzo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/common/enums.py | 15 +++++++++++++++ app/content/views/cheatsheet.py | 6 ++++-- app/tests/content/test_cheatsheet_integration.py | 5 +++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/common/enums.py b/app/common/enums.py index 10d63dcf..68afaa78 100644 --- a/app/common/enums.py +++ b/app/common/enums.py @@ -31,6 +31,21 @@ def get_user_class_number(user_class: NativeUserClass) -> int: return int(_class.split(".")[0]) +def get_user_class_name(user_class: int): + if user_class == 1: + return NativeUserClass.FIRST + elif user_class == 2: + return NativeUserClass.SECOND + elif user_class == 3: + return NativeUserClass.THIRD + elif user_class == 4: + return NativeUserClass.FOURTH + elif user_class == 5: + return NativeUserClass.FIFTH + else: + return NativeUserClass.ALUMNI + + # This can't be removed because it is used in the migrations. It is not used in the code class UserStudy(ChoiceEnum): DATAING = "Dataingeniør" diff --git a/app/content/views/cheatsheet.py b/app/content/views/cheatsheet.py index 8037f48b..56304dd5 100644 --- a/app/content/views/cheatsheet.py +++ b/app/content/views/cheatsheet.py @@ -4,7 +4,8 @@ from sentry_sdk import capture_exception -from app.common.enums import UserClass, UserStudy +from app.common.enums import NativeUserStudy as UserStudy +from app.common.enums import get_user_class_name from app.common.pagination import BasePagination from app.common.permissions import BasicViewPermission, is_admin_user from app.common.viewsets import BaseViewSet @@ -24,8 +25,9 @@ class CheatsheetViewSet(BaseViewSet): def get_object(self): if "pk" not in self.kwargs: + grade = get_user_class_name(int(self.kwargs["grade"])) return self.filter_queryset(self.queryset).filter( - grade=UserClass(int(self.kwargs["grade"])), + grade=grade, study=UserStudy[self.kwargs["study"]], ) diff --git a/app/tests/content/test_cheatsheet_integration.py b/app/tests/content/test_cheatsheet_integration.py index a148e10d..7e08d6f6 100644 --- a/app/tests/content/test_cheatsheet_integration.py +++ b/app/tests/content/test_cheatsheet_integration.py @@ -47,16 +47,17 @@ def test_list_as_anonymous_user(default_client, cheatsheet): @pytest.mark.django_db -def test_list_as_member(cheatsheet, member): +def test_list_cheatsheets_as_member(cheatsheet, member): """ A member of TIHLDE should be able to list all cheatsheets. """ client = get_api_client(user=member) url = _get_cheatsheet_url(cheatsheet) response = client.get(url) + data = response.data assert response.status_code == status.HTTP_200_OK - assert len(response.json()) + assert len(data.get("results")) @pytest.mark.django_db From 8c8199a33a693cc1a2e917727770be5b67423873 Mon Sep 17 00:00:00 2001 From: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:33:15 +0200 Subject: [PATCH 03/16] Mid October Update (#903) * Feat(kontres)/add image to bookable item (#785) * added optional image to bookable item model * added update method in serializer to handle new images * linting * remove update method for images * Feat(kontres)/add approved by (#786) * added approved by field * endpoint will now set approved by * serializer will return full user object in approved_by_detail * created test for approved by * migration * remove unnecessary code * removed write-only field in approved-by context * Create minutes for Codex (#787) * init * format * Feat(minute)/viewset (#788) * added richer reponse on post and put * added to admin panel * added filter for minute * Feat(kontres)/add notification (#790) * created methods for sending notification to admin and user * endpoint will now send notification if needed * add migrations for new notification types * Memberships with fines activated (#791) init * Feat(user)/user bio (#758) * Created model, serializer and view for user-bio * Created user bio model and made migrations * Created user bio serializer + viewsets + added new endpoint * Tested create method + added bio serializer to user serializer * Format * Created update method and started testing * Debugging test failures in user retrieve * fixed model error * Created user_bio_factory + started testing put method * Created fixture for UserBio * Created custom excpetion for duplicate user bio * Added permissions and inherited from BaseModel * Modularized serializer for bio * Use correct serializers in viewset + added destroy method * Finished testing bio viewset integration + format * Changed environent file to .env to avoid pushing up keys * Fix: Flipped assertion statement in test, since user bio should not be deleted * skiped buggy test from kontres * added mark to pytest.skip * Moved keys to .env file and reverted docker variables * Skip buggy kontres test * format * Added str method to user_bio * Removed unused imports * format * Changed user relation to a OneToOne-field (same affect as ForeignKey(unique=True) + removed check for duplicate bio in serializer * Migrations + changed assertion status code in duplicate bio test (could try catch in serializer to produce 400 status code) * format * format * Changed limit for description 50 -> 500 + migrations * Migrate * added id to serializer * merged leaf nodes in migrations * format --------- Co-authored-by: Ester2109 <126612066+Ester2109@users.noreply.github.com> Co-authored-by: Mads Nylund Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Co-authored-by: Tam Le * Update CHANGELOG.md * added filter for allowed photos for user (#794) added filter for allowed photos * Upped payment time when coming from waiting list (#796) * fixed paymenttime saved to db (#798) * fixed bug (#800) * Disallow users to unregister when payment is done (#802) added 400 status code for deleting paid registration * update changelog * Added serializer for category in event (#804) added serializer for category in event * Permission middelware (#806) * added a check for existing user and id on request * format * Permission refactor of QR Codes (#807) * added permissions to qr code and refactored viewset * format * removed unused imports * Permissions for payment orders (#808) * added read permissions * added permissions for payment order and tests * format * chore(iac): updated docs and force https (#810) chore: updated docs and force https * feat(iac): add terraform guardrails so index don't nuke our infra (#811) feat: add guardrails so index don't fup * Automatic registration for new users with Feide (#809) * started on feide registration endpoint * made endpoint for creating user with Feide * added test for parse group * finished * format * removes three years if in digtrans * changelog update * Feide env variables Terraform (#814) added feid env variables * added delete endpoint for file (#815) * added delete endpoint for file * Trigger Build * changed workflow to checkout v4 * changed from docker-compose to docker compose * Update CHANGELOG.md * format * format * fixed permission for committee leaders for group forms * updated csv for forms (#818) * Permission for group forms and news (#820) added permission for committees to create news, and all leaders of groups to create group forms * Update reservation_seralizer.py (#822) * Update reservation_seralizer.py * Fixed linting * Put a band aid on it *smack* * Removed blank line.. * ???? * Group ownership of Minutes (#847) * Refactor MinuteFactory to include group field, and added validation for checkin group access * added validation for POST request * Changed endpoint response (#846) * Changed endpoint response * Fixed test * Update test_reservation_integration.py --------- Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * updated changelog.md * finished events now appear in the correct order (newest first) (#849) * finished events now appear in the correct order (newest first) * added description of change in changelog * fixed formatting * updated method to use Django ORM instead of using python methods * Implement Swagger (#858) * started on removing choiceenums * refactored cheatsheet and membership * refacotered strike enum * refactored Groups enum * removed AppModel choiceenum * added swagger * Swagger GitHub Action (#860) * added github action for checking if Swagger is up * new action * try another * tried implementing check for container * added curl to docker image * added check if swagger is up * test if swagger does not get status code 200 * added ?format=openai to trigger error * checking that the request is working * updated CHANGELOG.md * Add new app (#862) * added script for adding new app to Lepton * added command to Makefile * Upgrade all dependencies to latest (#857) * Add endpoint to create new group as admin Signed-off-by: Tmpecho * Upgrade all dependencies to latest Signed-off-by: Tmpecho * remove bad exception handling in serializers/group.py * fix inheritance ordering in views/group.py * refactored group integration test, added non_public_groups to enums * fix linting * reformat files * remove unused import in groups/views/group.py * Upgrade dependency "black" Signed-off-by: Tmpecho * Upgrade dependency "sentry-sdk" Signed-off-by: Tmpecho * Upgrade dependency "azure-storage-blob" and remove outdated comment in requirements.txt Signed-off-by: Tmpecho * Upgrade all non-django dependencies Signed-off-by: Tmpecho * Upgrade dependency "Django" Signed-off-by: Tmpecho * Upgrade dependencies and remove ignored version from docker-compose.yml Signed-off-by: Tmpecho --------- Signed-off-by: Tmpecho Co-authored-by: 1Cezzo * Allow HS members to create a new group (#864) * Add endpoint to create new group as admin Signed-off-by: Tmpecho * remove bad exception handling in serializers/group.py * fix inheritance ordering in views/group.py * refactored group integration test, added non_public_groups to enums * fix linting * reformat files * remove unused import in groups/views/group.py --------- Signed-off-by: Tmpecho Co-authored-by: 1Cezzo * App Script Fix (#875) added serializers dir to script * Event registration payment orders (#876) * added list of payment orders for registrations * update CHANGELOG.md * chore(deps): update python-dotenv requirement from ~=0.21.1 to ~=1.0.1 (#871) Updates the requirements on [python-dotenv](https://github.com/theskumar/python-dotenv) to permit the latest version. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v0.21.1...v1.0.1) --- updated-dependencies: - dependency-name: python-dotenv dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Chore(deps): Bump sentry-sdk from 1.14.0 to 2.8.0 (#866) Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.14.0 to 2.8.0. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/1.14.0...2.8.0) --- updated-dependencies: - dependency-name: sentry-sdk dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Codex Course (#852) * added models for course and registration, and viewset for course and tests * added validation for date checking for courses * added viewset for registration for codex courses * removed unused fields from course model * removed unused imports * added API error mixins as mother clas * fixed error mixin * refactored to event model * fixed wrong import * fixed tests * format * skipped broken tests, must be refactored * updated CHANGELOG.md * format * fixed filtering of groups and made tests * fixed list endpoint for cheatsheets * trigger * format * Endpoint for sending email (#883) * fix formatting * created tests for send_email endpoint * Fix code scanning alert no. 45: Information exposure through an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixed errors in send_email and in tests * lint * added tests for empty lists and for sending mail to multiple users --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * chore(deps): bump black from 24.3.0 to 24.8.0 (#869) Bumps [black](https://github.com/psf/black) from 24.3.0 to 24.8.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/24.3.0...24.8.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * chore(deps): Bump azure-storage-blob from 12.13.1 to 12.23.1 (#885) Bumps [azure-storage-blob](https://github.com/Azure/azure-sdk-for-python) from 12.13.1 to 12.23.1. - [Release notes](https://github.com/Azure/azure-sdk-for-python/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/esrp_release.md) - [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-storage-blob_12.13.1...azure-storage-blob_12.23.1) --- updated-dependencies: - dependency-name: azure-storage-blob dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Added admin.py to root in new app, and added app dir to tests (#892) added admin.py to root in new app, and added app dir to tests * Description to forms (#894) added description to form * Bug report system (#865) * Created New App named Index * created model * Refactor: Change admin file * admin * added serializer and viewsets for list * fixed typing error for permission_classes * Started on the create serializer for feedback * Made tests for create feedback * Implemented update serializer for feedback * made destroy method and testing create method as member * Made destroy method and tests * Fixed linting * Fixed linting --------- Co-authored-by: Tam Le Co-authored-by: Josefine Arntsen Co-authored-by: Mads Nylund Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Renaming of index app to feedback (#901) renamed index app to feedback * chore(deps): bump django from 4.2.16 to 5.1.1 (#889) Bumps [django](https://github.com/django/django) from 4.2.16 to 5.1.1. - [Commits](https://github.com/django/django/compare/4.2.16...5.1.1) --- updated-dependencies: - dependency-name: django dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> * Event registration race condition (#902) * started on fixing race condition * fixed race condition * upadted CHANGELOG.md --------- Signed-off-by: Tmpecho Signed-off-by: dependabot[bot] Co-authored-by: Erik Skjellevik <98759397+eriskjel@users.noreply.github.com> Co-authored-by: haruixu <114171733+haruixu@users.noreply.github.com> Co-authored-by: Ester2109 <126612066+Ester2109@users.noreply.github.com> Co-authored-by: Tam Le Co-authored-by: martcl Co-authored-by: Frikk Balder <33499052+MindChirp@users.noreply.github.com> Co-authored-by: Emil Johnsen <111747340+1Cezzo@users.noreply.github.com> Co-authored-by: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> Co-authored-by: 1Cezzo Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Josefine Arntsen <128137082+josefinearntsen@users.noreply.github.com> Co-authored-by: Josefine Arntsen --- CHANGELOG.md | 5 +- app/communication/enums.py | 6 + ...ernotificationsetting_notification_type.py | 37 +++ app/communication/tests/test_send_email.py | 216 ++++++++++++++++++ app/content/serializers/user.py | 1 + app/content/urls.py | 2 + app/content/views/__init__.py | 1 + app/content/views/registration.py | 4 +- app/content/views/send_email.py | 84 +++++++ app/feedback/__init__.py | 0 app/feedback/admin.py | 5 + app/feedback/app.py | 5 + app/feedback/enums.py | 8 + app/feedback/exceptions.py | 0 app/feedback/factories/__init__.py | 2 + app/feedback/factories/bug_factory.py | 13 ++ app/feedback/factories/idea_factory.py | 13 ++ app/feedback/filters/__init__.py | 0 app/feedback/migrations/0001_initial.py | 112 +++++++++ app/feedback/migrations/__init__.py | 0 app/feedback/mixins.py | 0 app/feedback/models/__init__.py | 3 + app/feedback/models/bug.py | 5 + app/feedback/models/feedback.py | 76 ++++++ app/feedback/models/idea.py | 5 + app/feedback/serializers/__init__.py | 3 + app/feedback/serializers/bug.py | 17 ++ app/feedback/serializers/feedback.py | 80 +++++++ app/feedback/serializers/idea.py | 17 ++ app/feedback/tasks/__init__.py | 0 app/feedback/tests/__init__.py | 0 app/feedback/urls.py | 8 + app/feedback/util/__init__.py | 0 app/feedback/views/__init__.py | 0 app/feedback/views/feedback.py | 81 +++++++ app/forms/migrations/0014_form_description.py | 18 ++ app/forms/models/forms.py | 1 + app/forms/serializers/forms.py | 1 + app/settings.py | 1 + app/tests/conftest.py | 11 + app/tests/index/__init__.py | 0 app/tests/index/test_feedback_integration.py | 174 ++++++++++++++ app/urls.py | 1 + requirements.txt | 6 +- scripts/app.py | 12 +- 45 files changed, 1028 insertions(+), 6 deletions(-) create mode 100644 app/communication/migrations/0011_alter_usernotificationsetting_notification_type.py create mode 100644 app/communication/tests/test_send_email.py create mode 100644 app/content/views/send_email.py create mode 100644 app/feedback/__init__.py create mode 100644 app/feedback/admin.py create mode 100644 app/feedback/app.py create mode 100644 app/feedback/enums.py create mode 100644 app/feedback/exceptions.py create mode 100644 app/feedback/factories/__init__.py create mode 100644 app/feedback/factories/bug_factory.py create mode 100644 app/feedback/factories/idea_factory.py create mode 100644 app/feedback/filters/__init__.py create mode 100644 app/feedback/migrations/0001_initial.py create mode 100644 app/feedback/migrations/__init__.py create mode 100644 app/feedback/mixins.py create mode 100644 app/feedback/models/__init__.py create mode 100644 app/feedback/models/bug.py create mode 100644 app/feedback/models/feedback.py create mode 100644 app/feedback/models/idea.py create mode 100644 app/feedback/serializers/__init__.py create mode 100644 app/feedback/serializers/bug.py create mode 100644 app/feedback/serializers/feedback.py create mode 100644 app/feedback/serializers/idea.py create mode 100644 app/feedback/tasks/__init__.py create mode 100644 app/feedback/tests/__init__.py create mode 100644 app/feedback/urls.py create mode 100644 app/feedback/util/__init__.py create mode 100644 app/feedback/views/__init__.py create mode 100644 app/feedback/views/feedback.py create mode 100644 app/forms/migrations/0014_form_description.py create mode 100644 app/tests/index/__init__.py create mode 100644 app/tests/index/test_feedback_integration.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e14785..ae1eaec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,11 @@ ## Neste versjon -## Versjon 2024.09.25 +## Versjon 2024.10.11 +- ✨ **Tilbakemelding-funksjon**. Man kan nå opprette tilbakemeldinger for bugs og idé. +- 🦟 **Påmelding**. Det vil nå ikke være mulig med flere påmeldinger på et arrangement enn maksgrensen. +## Versjon 2024.09.25 - ✨**Codex arrangementer**. Det kan nå opprettes arrangementer på Codex, som medlemmer av Codex kan melde seg på. - ⚡**Betalingsordre**. Man kan nå se historikk over betalingsordre for en påmelding til et arrangement. - ✨**Gruppe**. HS kan nå opprette en ny gruppe. diff --git a/app/communication/enums.py b/app/communication/enums.py index d9c6fc60..f664be7f 100644 --- a/app/communication/enums.py +++ b/app/communication/enums.py @@ -18,3 +18,9 @@ class UserNotificationSettingType(models.TextChoices): RESERVATION_NEW = "RESERVATION NEW", "Ny reservasjon" RESERVATION_APPROVED = "RESERVATION APPROVED", "Godkjent reservasjon" RESERVATION_CANCELLED = "RESERVATION CANCELLED", "Avslått reservasjon" + KONTRES = "KONTRES", "Kontres" + BLITZED = "BLITZED", "Blitzed" + + @classmethod + def get_kontres_and_blitzed(cls): + return [cls.KONTRES, cls.BLITZED] diff --git a/app/communication/migrations/0011_alter_usernotificationsetting_notification_type.py b/app/communication/migrations/0011_alter_usernotificationsetting_notification_type.py new file mode 100644 index 00000000..b368b134 --- /dev/null +++ b/app/communication/migrations/0011_alter_usernotificationsetting_notification_type.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.5 on 2024-09-23 18:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("communication", "0010_alter_usernotificationsetting_notification_type"), + ] + + operations = [ + migrations.AlterField( + model_name="usernotificationsetting", + name="notification_type", + field=models.CharField( + choices=[ + ("REGISTRATION", "Påmeldingsoppdateringer"), + ("UNREGISTRATION", "Avmeldingsoppdateringer"), + ("STRIKE", "Prikkoppdateringer"), + ("EVENT_SIGN_UP_START", "Arrangementer - påmeldingsstart"), + ("EVENT_SIGN_OFF_DEADLINE", "Arrangementer - avmeldingsfrist"), + ("EVENT_EVALUATION", "Arrangementer - evaluering"), + ("EVENT_INFO", "Arrangementer - info fra arrangør"), + ("FINE", "Grupper - bot"), + ("GROUP_MEMBERSHIP", "Grupper - medlemsskap"), + ("OTHER", "Andre"), + ("RESERVATION NEW", "Ny reservasjon"), + ("RESERVATION APPROVED", "Godkjent reservasjon"), + ("RESERVATION CANCELLED", "Avslått reservasjon"), + ("KONTRES", "Kontres"), + ("BLITZED", "Blitzed"), + ], + max_length=30, + ), + ), + ] diff --git a/app/communication/tests/test_send_email.py b/app/communication/tests/test_send_email.py new file mode 100644 index 00000000..eef51ca4 --- /dev/null +++ b/app/communication/tests/test_send_email.py @@ -0,0 +1,216 @@ +import os +from unittest.mock import patch + +from rest_framework import status + +import pytest + +from app.communication.enums import UserNotificationSettingType +from app.communication.notifier import Notify +from app.content.factories import UserFactory +from app.util.test_utils import get_api_client + +EMAIL_URL = "/send-email/" +EMAIL_API_KEY = os.environ.get("EMAIL_API_KEY") + + +def _get_email_url(): + return f"{EMAIL_URL}" + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_success(mock_send): + """ + Test that the send_email endpoint sends an email successfully. + """ + test_user = UserFactory() + + data = { + "user_id_list": [test_user.user_id], + "notification_type": "KONTRES", + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_201_CREATED + mock_send.assert_called_once() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_fails_when_field_missing(mock_send): + """ + Test that the send_email endpoint returns 400 when one of the fields is missing. + """ + test_user = UserFactory() + + data = { + "user_id_list": [test_user.user_id], + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + mock_send.assert_not_called() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_fails_when_wrong_api_key(mock_send): + """ + Test that the send_email endpoint returns 403 when the API key is invalid. + """ + test_user = UserFactory() + + data = { + "user_id_list": [test_user.user_id], + "notification_type": "KONTRES", + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": "wrong_key"} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_403_FORBIDDEN + mock_send.assert_not_called() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_fails_when_user_id_invalid(mock_send): + """ + Test that the send_email endpoint returns 404 when the user id is invalid. + """ + test_user = UserFactory() + + data = { + "user_id_list": [999], + "notification_type": "KONTRES", + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_404_NOT_FOUND + mock_send.assert_not_called() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +@pytest.mark.parametrize( + "notification_type", UserNotificationSettingType.get_kontres_and_blitzed() +) +def test_email_success_with_kontres_and_blitzed(mock_send, notification_type): + """ + Tests that the send_email endpoint works with both KONTRES and BLITZED notification types. + """ + test_user = UserFactory() + + data = { + "user_id_list": [test_user.user_id], + "notification_type": notification_type, + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_201_CREATED + mock_send.assert_called_once() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_success_with_user_id_list(mock_send): + """ + Test that the send_email endpoint sends an email successfully to a list of users. + """ + test_user1 = UserFactory() + test_user2 = UserFactory() + test_user3 = UserFactory() + + data = { + "user_id_list": [ + test_user.user_id for test_user in [test_user1, test_user2, test_user3] + ], + "notification_type": "KONTRES", + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user1) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_201_CREATED + mock_send.assert_called_once() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_fails_when_user_id_list_is_empty(mock_send): + """ + Test that the send_email endpoint returns 400 when the user id list is empty. + """ + test_user = UserFactory() + + data = { + "user_id_list": [], + "notification_type": "KONTRES", + "title": "Test Notification", + "paragraphs": ["This is a test paragraph.", "This is another paragraph."], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + mock_send.assert_not_called() + + +@pytest.mark.django_db +@patch.object(Notify, "send", return_value=None) +def test_send_email_fails_when_user_paragraph_list_is_empty(mock_send): + """ + Test that the send_email endpoint returns 400 when the paragraph list is empty. + """ + test_user = UserFactory() + + data = { + "user_id_list": [test_user.user_id], + "notification_type": "KONTRES", + "title": "Test Notification", + "paragraphs": [], + } + + client = get_api_client(user=test_user) + url = _get_email_url() + headers = {"api_key": EMAIL_API_KEY} + response = client.post(url, data, format="json", **headers) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + mock_send.assert_not_called() diff --git a/app/content/serializers/user.py b/app/content/serializers/user.py index 2378c562..9150dc28 100644 --- a/app/content/serializers/user.py +++ b/app/content/serializers/user.py @@ -127,6 +127,7 @@ class Meta: "user_id", "first_name", "last_name", + "image", ) diff --git a/app/content/urls.py b/app/content/urls.py index c5a48239..26911fd6 100644 --- a/app/content/urls.py +++ b/app/content/urls.py @@ -20,6 +20,7 @@ accept_form, delete, register_with_feide, + send_email, upload, ) @@ -53,6 +54,7 @@ re_path(r"", include(router.urls)), path("accept-form/", accept_form), path("upload/", upload), + path("send-email/", send_email), path("delete-file///", delete), path("feide/", register_with_feide), re_path(r"users/(?P[^/.]+)/events.ics", UserCalendarEvents()), diff --git a/app/content/views/__init__.py b/app/content/views/__init__.py index 3392d438..5a9a5c3f 100644 --- a/app/content/views/__init__.py +++ b/app/content/views/__init__.py @@ -16,3 +16,4 @@ from app.content.views.logentry import LogEntryViewSet from app.content.views.minute import MinuteViewSet from app.content.views.feide import register_with_feide +from app.content.views.send_email import send_email diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 2b5d680a..0aa13b3a 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -1,5 +1,6 @@ import uuid +from django.db.transaction import atomic from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters, status @@ -48,6 +49,7 @@ def _is_own_registration(self): def _is_not_own_registration(self): return not self._is_own_registration() + @atomic def create(self, request, *args, **kwargs): """Register the current user for the given event.""" @@ -68,7 +70,7 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) event_id = self.kwargs.get("event_id", None) - event = Event.objects.get(pk=event_id) + event = Event.objects.select_for_update().get(pk=event_id) registration = super().perform_create( serializer, event=event, user=request.user diff --git a/app/content/views/send_email.py b/app/content/views/send_email.py new file mode 100644 index 00000000..2619632d --- /dev/null +++ b/app/content/views/send_email.py @@ -0,0 +1,84 @@ +import os + +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response + +from app.communication.enums import UserNotificationSettingType +from app.communication.notifier import Notify +from app.content.models import User + + +@api_view(["POST"]) +def send_email(request): + """ + Endpoint for sending a notification and email to a user. + + Body should contain: + - 'user_id_list': A list of user ids to send the email to. + - 'notification_type': KONTRES or BLITZED. + - 'title': The title of the notification. + - 'paragraphs': A list of paragraphs to include in the notification. + + The header should contain: + - 'api_key': A key for validating access. + + """ + try: + EMAIL_API_KEY = os.environ.get("EMAIL_API_KEY") + api_key = request.META.get("api_key") + if api_key != EMAIL_API_KEY: + return Response( + {"detail": "Feil API nøkkel"}, + status=status.HTTP_403_FORBIDDEN, + ) + + user_id_list = request.data.get("user_id_list") + paragraphs = request.data.get("paragraphs") + title = request.data.get("title") + notification_type = request.data.get("notification_type") + + if not isinstance(user_id_list, list) or not user_id_list: + return Response( + {"detail": "En ikke-tom liste med bruker id-er må inkluderes"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if not isinstance(paragraphs, list) or not paragraphs: + return Response( + {"detail": "En ikke-tom liste med paragrafer må inkluderes"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if not notification_type or not title: + return Response( + { + "detail": "Notifikasjonstype (KONTRES/BLITZED) og tittel må være satt" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + users = list(User.objects.filter(user_id__in=user_id_list)) + if not users or len(users) != len(user_id_list): + return Response( + {"detail": "En eller flere brukere ble ikke funnet"}, + status=status.HTTP_404_NOT_FOUND, + ) + + email = Notify( + users, + f"{title}", + UserNotificationSettingType(notification_type), + ) + + for paragraph in paragraphs: + email.add_paragraph(paragraph) + + email.send() + return Response({"detail": "Emailen ble sendt"}, status=status.HTTP_201_CREATED) + except Exception as e: + print(e) + return Response( + {"detail": "Det oppsto en feil under sending av email"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) diff --git a/app/feedback/__init__.py b/app/feedback/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/admin.py b/app/feedback/admin.py new file mode 100644 index 00000000..3f450ab7 --- /dev/null +++ b/app/feedback/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from app.feedback import models + +admin.site.register(models.Bug) diff --git a/app/feedback/app.py b/app/feedback/app.py new file mode 100644 index 00000000..c310f0d3 --- /dev/null +++ b/app/feedback/app.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FeedbackConfig(AppConfig): + name = "app.feedback" diff --git a/app/feedback/enums.py b/app/feedback/enums.py new file mode 100644 index 00000000..5adae939 --- /dev/null +++ b/app/feedback/enums.py @@ -0,0 +1,8 @@ +from django.db import models + + +class Status(models.TextChoices): + OPEN = "OPEN", "Åpen" + CLOSED = "CLOSED", "Lukket" + IN_PROGRESS = "IN_PROGRESS", "Under arbeid" + REJECTED = "REJECTED", "Avvist" diff --git a/app/feedback/exceptions.py b/app/feedback/exceptions.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/factories/__init__.py b/app/feedback/factories/__init__.py new file mode 100644 index 00000000..6dba0161 --- /dev/null +++ b/app/feedback/factories/__init__.py @@ -0,0 +1,2 @@ +from app.feedback.factories.bug_factory import BugFactory +from app.feedback.factories.idea_factory import IdeaFactory diff --git a/app/feedback/factories/bug_factory.py b/app/feedback/factories/bug_factory.py new file mode 100644 index 00000000..4ff95a21 --- /dev/null +++ b/app/feedback/factories/bug_factory.py @@ -0,0 +1,13 @@ +import factory +from factory.django import DjangoModelFactory + +from app.content.factories import UserFactory +from app.feedback.models.bug import Bug + + +class BugFactory(DjangoModelFactory): + class Meta: + model = Bug + + title = factory.Sequence(lambda n: f"Bug{n}") + author = factory.SubFactory(UserFactory) diff --git a/app/feedback/factories/idea_factory.py b/app/feedback/factories/idea_factory.py new file mode 100644 index 00000000..c923c471 --- /dev/null +++ b/app/feedback/factories/idea_factory.py @@ -0,0 +1,13 @@ +import factory +from factory.django import DjangoModelFactory + +from app.content.factories import UserFactory +from app.feedback.models.idea import Idea + + +class IdeaFactory(DjangoModelFactory): + class Meta: + model = Idea + + title = factory.Sequence(lambda n: f"Idea{n}") + author = factory.SubFactory(UserFactory) diff --git a/app/feedback/filters/__init__.py b/app/feedback/filters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/migrations/0001_initial.py b/app/feedback/migrations/0001_initial.py new file mode 100644 index 00000000..ff17edca --- /dev/null +++ b/app/feedback/migrations/0001_initial.py @@ -0,0 +1,112 @@ +# Generated by Django 4.2.16 on 2024-10-11 15:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("contenttypes", "0002_remove_content_type_name"), + ] + + operations = [ + migrations.CreateModel( + name="Feedback", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=100)), + ("description", models.TextField(blank=True, default="")), + ( + "status", + models.CharField( + default="OPEN", + max_length=20, + verbose_name=[ + ("OPEN", "Åpen"), + ("CLOSED", "Lukket"), + ("IN_PROGRESS", "Under arbeid"), + ("REJECTED", "Avvist"), + ], + ), + ), + ( + "author", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "polymorphic_ctype", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="polymorphic_%(app_label)s.%(class)s_set+", + to="contenttypes.contenttype", + ), + ), + ], + options={ + "ordering": ("created_at",), + }, + ), + migrations.CreateModel( + name="Bug", + fields=[ + ( + "feedback_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="feedback.feedback", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("feedback.feedback",), + ), + migrations.CreateModel( + name="Idea", + fields=[ + ( + "feedback_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="feedback.feedback", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("feedback.feedback",), + ), + ] diff --git a/app/feedback/migrations/__init__.py b/app/feedback/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/mixins.py b/app/feedback/mixins.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/models/__init__.py b/app/feedback/models/__init__.py new file mode 100644 index 00000000..604302af --- /dev/null +++ b/app/feedback/models/__init__.py @@ -0,0 +1,3 @@ +from app.feedback.models.bug import Bug +from app.feedback.models.feedback import Feedback +from app.feedback.models.idea import Idea diff --git a/app/feedback/models/bug.py b/app/feedback/models/bug.py new file mode 100644 index 00000000..39962659 --- /dev/null +++ b/app/feedback/models/bug.py @@ -0,0 +1,5 @@ +from app.feedback.models.feedback import Feedback + + +class Bug(Feedback): + pass diff --git a/app/feedback/models/feedback.py b/app/feedback/models/feedback.py new file mode 100644 index 00000000..13d698db --- /dev/null +++ b/app/feedback/models/feedback.py @@ -0,0 +1,76 @@ +from django.db import models + +from polymorphic.models import PolymorphicModel + +from app.common.enums import AdminGroup, Groups +from app.common.permissions import BasePermissionModel, check_has_access +from app.content.models.user import User +from app.feedback.enums import Status +from app.util.models import BaseModel + + +class Feedback(BaseModel, BasePermissionModel, PolymorphicModel): + + read_access = (Groups.TIHLDE,) + write_access = (Groups.TIHLDE,) + + title = models.CharField(max_length=100) + description = models.TextField(default="", blank=True) + + author = models.ForeignKey( + User, blank=True, null=True, default=None, on_delete=models.SET_NULL + ) + status = models.CharField(Status.choices, default=Status.OPEN, max_length=20) + + def __str__(self): + return f"{self.title} - {self.status}" + + class Meta: + ordering = ("created_at",) + + @classmethod + def has_read_permission(cls, request): + return super().has_read_permission(request) + + @classmethod + def has_write_permission(cls, request): + return super().has_write_permission(request) + + @classmethod + def has_retrieve_permission(cls, request): + return cls.has_read_permission(request) + + @classmethod + def has_create_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_update_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_destroy_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_list_permission(cls, request): + return cls.has_read_permission(request) + + def has_object_read_permission(self, request): + return self.has_read_permission(request) + + def has_object_write_permission(self, request): + return self.has_write_permission(request) + + def has_object_retrieve_permission(self, request): + return self.has_object_read_permission(request) + + def has_object_update_permission(self, request): + return ( + check_has_access([AdminGroup.INDEX], request) or self.author == request.user + ) + + def has_object_destroy_permission(self, request): + return ( + check_has_access([AdminGroup.INDEX], request) or self.author == request.user + ) diff --git a/app/feedback/models/idea.py b/app/feedback/models/idea.py new file mode 100644 index 00000000..42ecb651 --- /dev/null +++ b/app/feedback/models/idea.py @@ -0,0 +1,5 @@ +from app.feedback.models.feedback import Feedback + + +class Idea(Feedback): + pass diff --git a/app/feedback/serializers/__init__.py b/app/feedback/serializers/__init__.py new file mode 100644 index 00000000..25f74a27 --- /dev/null +++ b/app/feedback/serializers/__init__.py @@ -0,0 +1,3 @@ +from app.feedback.serializers.bug import BugListSerializer +from app.feedback.serializers.idea import IdeaListSerializer +from app.feedback.serializers.feedback import FeedbackListPolymorphicSerializer diff --git a/app/feedback/serializers/bug.py b/app/feedback/serializers/bug.py new file mode 100644 index 00000000..85046256 --- /dev/null +++ b/app/feedback/serializers/bug.py @@ -0,0 +1,17 @@ +from app.common.serializers import BaseModelSerializer +from app.content.serializers.user import SimpleUserSerializer +from app.feedback.models.bug import Bug + + +class BugListSerializer(BaseModelSerializer): + author = SimpleUserSerializer(read_only=True) + + class Meta: + model = Bug + fields = ( + "id", + "title", + "status", + "created_at", + "author", + ) diff --git a/app/feedback/serializers/feedback.py b/app/feedback/serializers/feedback.py new file mode 100644 index 00000000..588fc680 --- /dev/null +++ b/app/feedback/serializers/feedback.py @@ -0,0 +1,80 @@ +from rest_polymorphic.serializers import PolymorphicSerializer + +from app.common.serializers import BaseModelSerializer +from app.feedback.models import Bug, Feedback, Idea +from app.feedback.serializers import BugListSerializer, IdeaListSerializer + + +class FeedbackListPolymorphicSerializer(PolymorphicSerializer, BaseModelSerializer): + resource_type_field_name = "feedback_type" + + model_serializer_mapping = { + Bug: BugListSerializer, + Idea: IdeaListSerializer, + } + + class Meta: + model = Feedback + fields = ( + "id", + "title", + "status", + "created_at", + "author", + ) + + +class IdeaCreateSerializer(BaseModelSerializer): + class Meta: + model = Idea + fields = ( + "title", + "description", + ) + + def create(self, validated_data): + user = self.context["request"].user + validated_data["author"] = user + + return super().create(validated_data) + + +class BugCreateSerializer(BaseModelSerializer): + class Meta: + model = Bug + fields = ( + "title", + "description", + ) + + def create(self, validated_data): + user = self.context["request"].user + validated_data["author"] = user + + return super().create(validated_data) + + +class IdeaUpdateSerializer(BaseModelSerializer): + class Meta: + model = Feedback + fields = ( + "title", + "description", + "status", + ) + + def update(self, instance, validated_data): + return super().update(instance, validated_data) + + +class BugUpdateSerializer(BaseModelSerializer): + class Meta: + model = Feedback + fields = ( + "title", + "description", + "status", + ) + + def update(self, instance, validated_data): + return super().update(instance, validated_data) diff --git a/app/feedback/serializers/idea.py b/app/feedback/serializers/idea.py new file mode 100644 index 00000000..6b415eba --- /dev/null +++ b/app/feedback/serializers/idea.py @@ -0,0 +1,17 @@ +from app.common.serializers import BaseModelSerializer +from app.content.serializers.user import SimpleUserSerializer +from app.feedback.models.idea import Idea + + +class IdeaListSerializer(BaseModelSerializer): + author = SimpleUserSerializer(read_only=True) + + class Meta: + model = Idea + fields = ( + "id", + "title", + "status", + "created_at", + "author", + ) diff --git a/app/feedback/tasks/__init__.py b/app/feedback/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/tests/__init__.py b/app/feedback/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/urls.py b/app/feedback/urls.py new file mode 100644 index 00000000..c8922e8b --- /dev/null +++ b/app/feedback/urls.py @@ -0,0 +1,8 @@ +from django.urls import include, re_path +from rest_framework import routers + +from app.feedback.views.feedback import FeedbackViewSet + +router = routers.DefaultRouter() +router.register("feedbacks", FeedbackViewSet) +urlpatterns = [re_path(r"", include(router.urls))] diff --git a/app/feedback/util/__init__.py b/app/feedback/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/views/__init__.py b/app/feedback/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/feedback/views/feedback.py b/app/feedback/views/feedback.py new file mode 100644 index 00000000..0ccbc653 --- /dev/null +++ b/app/feedback/views/feedback.py @@ -0,0 +1,81 @@ +from rest_framework import status +from rest_framework.response import Response + +from app.common.pagination import BasePagination +from app.common.permissions import BasicViewPermission +from app.common.viewsets import BaseViewSet +from app.feedback.models.feedback import Feedback +from app.feedback.serializers.feedback import ( + BugCreateSerializer, + BugUpdateSerializer, + FeedbackListPolymorphicSerializer, + IdeaCreateSerializer, + IdeaUpdateSerializer, +) + + +class FeedbackViewSet(BaseViewSet): + serializer_class = FeedbackListPolymorphicSerializer + queryset = Feedback.objects.select_related("author") + pagination_class = BasePagination + permission_classes = [BasicViewPermission] + + def create(self, request, *_args, **_kwargs): + data = request.data + + feedback_type = data.get("feedback_type") + + if feedback_type == "Idea": + serializer = IdeaCreateSerializer(data=data, context={"request": request}) + + elif feedback_type == "Bug": + serializer = BugCreateSerializer(data=data, context={"request": request}) + + else: + return Response( + {"detail": "Ugyldig type tilbakemelding"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if serializer.is_valid(): + feedback = self.perform_create(serializer) + data = FeedbackListPolymorphicSerializer(feedback).data + return Response( + data, + status=status.HTTP_201_CREATED, + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + + def update(self, request, *_args, **_kwargs): + instance = self.get_object() + data = request.data + + feedback_type = instance.feedback_type + + if feedback_type == "Idea": + serializer = IdeaUpdateSerializer(instance, data=data) + + elif feedback_type == "Bug": + serializer = BugUpdateSerializer(instance, data=data) + + if serializer.is_valid(): + super().perform_update(serializer) + return Response( + serializer.data, + status=status.HTTP_200_OK, + ) + + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, *_args, **_kwargs): + super().destroy(request, *_args, **_kwargs) + return Response( + {"detail": "Tilbakemeldingen ble slettet"}, + status=status.HTTP_200_OK, + ) diff --git a/app/forms/migrations/0014_form_description.py b/app/forms/migrations/0014_form_description.py new file mode 100644 index 00000000..3cecfc4a --- /dev/null +++ b/app/forms/migrations/0014_form_description.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-01 06:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("forms", "0013_alter_field_type"), + ] + + operations = [ + migrations.AddField( + model_name="form", + name="description", + field=models.TextField(blank=True, default=""), + ), + ] diff --git a/app/forms/models/forms.py b/app/forms/models/forms.py index 08e2ff97..642654ec 100644 --- a/app/forms/models/forms.py +++ b/app/forms/models/forms.py @@ -24,6 +24,7 @@ class Form(PolymorphicModel, BasePermissionModel): write_access = (*AdminGroup.admin(), AdminGroup.NOK) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=400) + description = models.TextField(blank=True, default="") template = models.BooleanField(default=False) viewer_has_answered = None diff --git a/app/forms/serializers/forms.py b/app/forms/serializers/forms.py index 8d88a2fb..8944a112 100644 --- a/app/forms/serializers/forms.py +++ b/app/forms/serializers/forms.py @@ -51,6 +51,7 @@ class Meta: fields = ( "id", "title", + "description", "fields", "template", "resource_type", diff --git a/app/settings.py b/app/settings.py index d7a79e4d..26e8c1fe 100644 --- a/app/settings.py +++ b/app/settings.py @@ -106,6 +106,7 @@ "app.payment", "app.kontres", "app.emoji", + "app.feedback", "app.codex", ] diff --git a/app/tests/conftest.py b/app/tests/conftest.py index e2efd030..37961992 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -32,6 +32,7 @@ EventReactionFactory, NewsReactionFactory, ) +from app.feedback.factories import BugFactory, IdeaFactory from app.forms.tests.form_factories import FormFactory, SubmissionFactory from app.group.factories import GroupFactory, MembershipFactory from app.group.factories.fine_factory import FineFactory @@ -311,3 +312,13 @@ def codex_event(): @pytest.fixture() def codex_event_registration(): return CodexEventRegistrationFactory() + + +@pytest.fixture() +def feedback_bug(): + return BugFactory() + + +@pytest.fixture() +def feedback_idea(): + return IdeaFactory() diff --git a/app/tests/index/__init__.py b/app/tests/index/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/tests/index/test_feedback_integration.py b/app/tests/index/test_feedback_integration.py new file mode 100644 index 00000000..890c8ad4 --- /dev/null +++ b/app/tests/index/test_feedback_integration.py @@ -0,0 +1,174 @@ +from rest_framework import status + +import pytest + +from app.util.test_utils import get_api_client + +FEEDBACK_BASE_URL = "/feedbacks/" + + +def get_data(type): + return { + "feedback_type": type, + "title": "This is a type title", + "description": f"This is a {type} report", + } + + +@pytest.mark.django_db +def test_list_feedback_with_both_bug_and_idea_as_member( + member, feedback_bug, feedback_idea +): + """All members should be able to list all types of feedbacks.""" + + url = FEEDBACK_BASE_URL + client = get_api_client(member) + response = client.get(url) + + data = response.data + results = data["results"] + bug_type = list(filter(lambda x: "Bug" == x["feedback_type"], results)) + idea_type = list(filter(lambda x: "Idea" == x["feedback_type"], results)) + + assert response.status_code == status.HTTP_200_OK + assert data["count"] == 2 + assert bug_type + assert idea_type + + +@pytest.mark.django_db +def test_list_feedback_as_anonymous_user(default_client): + """Non TIHLDE users should not be able to list feedbacks""" + + url = FEEDBACK_BASE_URL + response = default_client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "type", + ["Bug", "Idea"], +) +def test_create_feedback_with_both_bug_and_idea_as_member(member, type): + """All members should be able to create a bug and a idea feedback""" + + url = FEEDBACK_BASE_URL + client = get_api_client(member) + data = get_data(type) + response = client.post(url, data=data) + data = response.data + + assert response.status_code == status.HTTP_201_CREATED + assert data["feedback_type"] == type + + +@pytest.mark.django_db +def test_create_feedback_with_wrong_type_as_member(member): + """No members should be able to create feedback of another type than bug and idea""" + + url = FEEDBACK_BASE_URL + client = get_api_client(member) + data = get_data("wrong_type") + response = client.post(url, data=data) + data = response.data + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "type", + ["Bug", "Idea"], +) +def test_create_feedback_as_anonymous_user(default_client, type): + """Non TIHLDE users should not be able to create feedbacks""" + + url = FEEDBACK_BASE_URL + data = get_data(type) + response = default_client.post(url, data=data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "type", + ["Bug", "Idea"], +) +def test_update_feedback_with_both_bug_and_idea_as_member(member, type): + """All members should be able to update their own bug and idea feedback""" + + url = FEEDBACK_BASE_URL + data = get_data(type) + client = get_api_client(member) + response = client.post(url, data=data) + data = response.data + + assert response.status_code == status.HTTP_201_CREATED + assert data["feedback_type"] == type + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "type", + ["Bug", "Idea"], +) +def test_destroy_feedback_as_index_user(index_member, type): + """An Index user should be able to delete other members feedback""" + + url = FEEDBACK_BASE_URL + data = get_data(type) + client = get_api_client(user=index_member) + + initial_response = client.post(url, data=data) + + feedback_id = initial_response.data["id"] + + url = f"{FEEDBACK_BASE_URL}{feedback_id}/" + + response = client.delete(url) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "type", + ["Bug", "Idea"], +) +def test_destroy_your_own_feedback_as_member(member, type): + """A user should be able to delete their own feedback as a member""" + + url = FEEDBACK_BASE_URL + data = get_data(type) + client = get_api_client(user=member) + + initial_response = client.post(url, data=data) + + print(initial_response.data) + print(f"Author: {initial_response.data['author']}") + + feedback_id = initial_response.data["id"] + + url = f"{FEEDBACK_BASE_URL}{feedback_id}/" + + response = client.delete(url) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "type", + ["Bug", "Idea"], +) +def test_destroy_feedback_as_anonymous_user(default_client, type): + """Non TIHLDE users should not be able to delete feedbacks""" + + url = FEEDBACK_BASE_URL + data = get_data(type) + response = default_client.delete(url, data=data) + + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/app/urls.py b/app/urls.py index d4fb286b..301f2342 100644 --- a/app/urls.py +++ b/app/urls.py @@ -55,4 +55,5 @@ path("kontres/", include("app.kontres.urls")), path("emojis/", include("app.emoji.urls")), path("codex/", include("app.codex.urls")), + path("", include("app.feedback.urls")), ] diff --git a/requirements.txt b/requirements.txt index f62db6e7..4f6210f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ wheel mysqlclient == 2.1.1 sentry-sdk == 2.8.0 celery == 5.4.0 -azure-storage-blob == 12.13.1 +azure-storage-blob == 12.23.1 python-dotenv ~= 1.0.1 gunicorn == 23.0.0 uvicorn == 0.30.6 @@ -16,7 +16,7 @@ pyjwt ~= 2.9.0 # Django # ------------------------------------------------------------------------------ -Django == 4.2.16 +Django == 5.1.1 django-enumchoicefield == 3.0.1 django-filter == 24.3 django-ordered-model ~= 3.7.4 @@ -39,7 +39,7 @@ django-mptt == 0.16.0 # Code quality # ------------------------------------------------------------------------------ pylint -black == 24.3.0 +black == 24.8.0 isort flake8 flake8-django diff --git a/scripts/app.py b/scripts/app.py index 0987e993..48532246 100644 --- a/scripts/app.py +++ b/scripts/app.py @@ -20,7 +20,6 @@ def create_app(): os.makedirs(app_path, exist_ok=True) # Create the app's directories - init_dir(app_path, "admin") init_dir(app_path, "factories") init_dir(app_path, "filters") init_dir(app_path, "migrations") @@ -31,8 +30,19 @@ def create_app(): init_dir(app_path, "views") init_dir(app_path, "serializers") + # create tests directory + TESTS_PATH = os.path.join(BASE_PATH, "tests") + + if app_name in os.listdir(TESTS_PATH): + print(f"App '{app_name}' already exists in 'tests'.") + else: + path = os.path.join(TESTS_PATH, app_name) + os.makedirs(os.path.join(TESTS_PATH, app_name), exist_ok=True) + init_app_file(path, "__init__.py") + # Create the app's files init_app_file(app_path, "__init__.py") + init_app_file(app_path, "admin.py") config_content = f"""from django.apps import AppConfig From 0e053d4b81a999585327e9e481407c8943d13e0b Mon Sep 17 00:00:00 2001 From: Yazan Zarka Date: Mon, 28 Oct 2024 17:28:03 +0100 Subject: [PATCH 04/16] Feat(registration)/filter participants (#895) * filtering by year and study * linting fix * added allergy filter * added filter by allergies and participants with allergy count to event. * Lint fix * Add new fixture for admin user * Start testing filtering + finished allergy filter test * Added integration test for participants filtering * lint fix * removed unused import * Update changelog * merge with dev and more filters * Fixed has_paid filter and added filter combination test * ran linting script --------- Co-authored-by: Harry Linrui XU Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- CHANGELOG.md | 1 + app/content/filters/registration.py | 64 ++++++++- app/content/serializers/event.py | 23 +++ app/tests/conftest.py | 7 + .../content/test_registration_integration.py | 134 ++++++++++++++++++ 5 files changed, 228 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1eaec3..c4c74f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ --- ## Neste versjon +- ✨ **Filtrering**. Admin kan nå filtere deltakere av et arrangement på studie, studieår, om deltakere har allergier, (om deltakere godtar å bli tatt bilde av, om deltakere har ankommet), i tillegg til søk på fornavn og etternavn. ## Versjon 2024.10.11 - ✨ **Tilbakemelding-funksjon**. Man kan nå opprette tilbakemeldinger for bugs og idé. diff --git a/app/content/filters/registration.py b/app/content/filters/registration.py index 94f35b03..92b7eee6 100644 --- a/app/content/filters/registration.py +++ b/app/content/filters/registration.py @@ -1,9 +1,71 @@ +from django.db.models import Exists, OuterRef +from django_filters import rest_framework as filters from django_filters.rest_framework import FilterSet +from app.common.enums import NativeGroupType as GroupType from app.content.models.registration import Registration +from app.payment.enums import OrderStatus +from app.payment.models import Order class RegistrationFilter(FilterSet): + + study = filters.CharFilter( + field_name="user__groups__name", lookup_expr="icontains", method="filter_study" + ) + year = filters.CharFilter( + field_name="user__groups__name", lookup_expr="icontains", method="filter_year" + ) + + has_allergy = filters.BooleanFilter( + field_name="user__allergy", method="filter_has_allergy" + ) + + has_paid = filters.BooleanFilter( + field_name="event__orders__status", method="filter_has_paid" + ) + class Meta: model = Registration - fields = ["has_attended", "is_on_wait"] + fields = [ + "has_attended", + "is_on_wait", + "study", + "year", + "has_allergy", + "allow_photo", + "has_paid", + ] + + def filter_study(self, queryset, name, value): + return queryset.filter( + user__memberships__group__name__icontains=value, + user__memberships__group__type=GroupType.STUDY, + ) + + def filter_has_paid(self, queryset, name, value): + sale_order_exists = Order.objects.filter( + event=OuterRef("event_id"), + user=OuterRef("user_id"), + status=OrderStatus.SALE, + ) + + if value: + return queryset.filter(Exists(sale_order_exists)) + else: + return queryset.exclude(Exists(sale_order_exists)) + + def filter_year(self, queryset, name, value): + return queryset.filter( + user__memberships__group__name__icontains=value, + user__memberships__group__type=GroupType.STUDYYEAR, + ) + + def filter_has_allergy(self, queryset, name, value): + if value: + return queryset.exclude(user__allergy__isnull=True).exclude( + user__allergy__exact="" + ) + return queryset.filter(user__allergy__isnull=True) | queryset.filter( + user__allergy__exact="" + ) diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index f1042051..b9e9fd4c 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -15,6 +15,7 @@ from app.emoji.serializers.reaction import ReactionSerializer from app.group.models.group import Group from app.group.serializers.group import SimpleGroupSerializer +from app.payment.enums import OrderStatus from app.payment.models.paid_event import PaidEvent from app.payment.serializers.paid_event import PaidEventCreateSerializer @@ -263,6 +264,9 @@ class EventStatisticsSerializer(BaseModelSerializer): has_attended_count = serializers.SerializerMethodField() studyyears = serializers.SerializerMethodField() studies = serializers.SerializerMethodField() + has_allergy_count = serializers.SerializerMethodField() + has_paid_count = serializers.SerializerMethodField() + allow_photo_count = serializers.SerializerMethodField() class Meta: model = Event @@ -272,11 +276,21 @@ class Meta: "waiting_list_count", "studyyears", "studies", + "has_allergy_count", + "has_paid_count", + "allow_photo_count", ) def get_has_attended_count(self, obj, *args, **kwargs): return obj.registrations.filter(is_on_wait=False, has_attended=True).count() + def get_has_allergy_count(self, obj, *args, **kwargs): + return ( + obj.registrations.exclude(user__allergy__isnull=True) + .exclude(user__allergy__exact="") + .count() + ) + def get_studyyears(self, obj, *args, **kwargs): return filter( lambda studyyear: studyyear["amount"] > 0, @@ -304,3 +318,12 @@ def get_studies(self, obj, *args, **kwargs): Group.objects.filter(type=GroupType.STUDY), ), ) + + def get_allow_photo_count(self, obj, *args, **kwargs): + return obj.registrations.filter(allow_photo=False).count() + + def get_has_paid_count(self, obj, *args, **kwargs): + if obj.is_paid_event: + orders = obj.orders.filter(status=OrderStatus.SALE, event=obj).count() + return orders + return 0 diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 37961992..fbdccddf 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -314,6 +314,13 @@ def codex_event_registration(): return CodexEventRegistrationFactory() +@pytest.fixture() +def new_admin_user(): + admin = UserFactory() + add_user_to_group_with_name(admin, AdminGroup.HS) + return admin + + @pytest.fixture() def feedback_bug(): return BugFactory() diff --git a/app/tests/content/test_registration_integration.py b/app/tests/content/test_registration_integration.py index 5b9f20f8..86d11115 100644 --- a/app/tests/content/test_registration_integration.py +++ b/app/tests/content/test_registration_integration.py @@ -7,12 +7,14 @@ from app.common.enums import AdminGroup from app.common.enums import NativeGroupType as GroupType from app.common.enums import NativeMembershipType as MembershipType +from app.common.enums import NativeUserStudy as StudyType from app.content.factories import EventFactory, RegistrationFactory, UserFactory from app.content.factories.priority_pool_factory import PriorityPoolFactory from app.forms.enums import NativeEventFormType as EventFormType from app.forms.tests.form_factories import EventFormFactory, SubmissionFactory from app.group.factories import GroupFactory from app.payment.enums import OrderStatus +from app.payment.factories import OrderFactory from app.util.test_utils import add_user_to_group_with_name, get_api_client from app.util.utils import now @@ -1071,3 +1073,135 @@ def test_delete_registration_with_paid_order_as_self( response = client.delete(url) assert response.status_code == status_code + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("filter_params", "participant_count", "status_code"), + [ + ({"has_allergy": True}, 2, status.HTTP_200_OK), + ({"year": "2050"}, 1, status.HTTP_200_OK), + ({"year": "2051"}, 1, status.HTTP_200_OK), + ({"study": StudyType.DATAING}, 2, status.HTTP_200_OK), + ({"year": "2050", "study": StudyType.DATAING}, 1, status.HTTP_200_OK), + ( + {"has_allergy": True, "year": "2051", "study": StudyType.DATAING}, + 1, + status.HTTP_200_OK, + ), + ( + {"has_allergy": True, "year": "2050", "study": StudyType.DATAING}, + 1, + status.HTTP_200_OK, + ), + ], +) +def test_filter_participants( + new_admin_user, member, event, filter_params, participant_count, status_code +): + """ + An admin should be able to filter the participants of an event using multiple parameters + """ + + member.allergy = "Pizza" + member.save() + + new_admin_user.allergy = "Fisk" + new_admin_user.save() + + add_user_to_group_with_name(member, StudyType.DATAING, GroupType.STUDY) + add_user_to_group_with_name(member, "2050", GroupType.STUDYYEAR) + + add_user_to_group_with_name(new_admin_user, "2051", GroupType.STUDYYEAR) + add_user_to_group_with_name(new_admin_user, StudyType.DATAING, GroupType.STUDY) + + RegistrationFactory(user=member, event=event) + RegistrationFactory(user=new_admin_user, event=event) + client = get_api_client(user=new_admin_user) + + # Build the query string with multiple filter parameters + url = ( + _get_registration_url(event) + + "?" + + "&".join([f"{key}={value}" for key, value in filter_params.items()]) + ) + response = client.get(url) + + assert participant_count == response.data["count"] + assert response.status_code == status_code + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("filter_params", "participant_count", "status_code"), + [ + ({"study": StudyType.DATAING, "has_paid": True}, 1, status.HTTP_200_OK), + ({"study": StudyType.DIGFOR, "has_paid": True}, 2, status.HTTP_200_OK), + ({"study": StudyType.DIGFOR, "has_paid": False}, 1, status.HTTP_200_OK), + ({"has_paid": True, "year": "2050"}, 1, status.HTTP_200_OK), + ({"has_paid": True, "year": "2051"}, 1, status.HTTP_200_OK), + ({"has_paid": True}, 4, status.HTTP_200_OK), + ({"has_paid": False}, 2, status.HTTP_200_OK), + ], +) +def test_filter_participants_paid_event( + new_admin_user, + member, + event, + paid_event, + filter_params, + participant_count, + status_code, +): + """ + An admin should be able to filter the participants of an event using multiple parameters + """ + + paid_event.event = event + + paid_event.save() + + member.allergy = "Pizza" + member.save() + + new_admin_user.allergy = "Fisk" + new_admin_user.save() + + new_user = UserFactory() + new_user2 = UserFactory() + new_user3 = UserFactory() + new_user4 = UserFactory() + + add_user_to_group_with_name(member, StudyType.DATAING, GroupType.STUDY) + add_user_to_group_with_name(member, "2050", GroupType.STUDYYEAR) + + add_user_to_group_with_name(new_admin_user, "2051", GroupType.STUDYYEAR) + add_user_to_group_with_name(new_admin_user, StudyType.DIGFOR, GroupType.STUDY) + add_user_to_group_with_name(new_user2, StudyType.DIGFOR, GroupType.STUDY) + add_user_to_group_with_name(new_user3, StudyType.DIGFOR, GroupType.STUDY) + + RegistrationFactory(user=member, event=event) + RegistrationFactory(user=new_admin_user, event=event) + RegistrationFactory(user=new_user, event=event) + RegistrationFactory(user=new_user2, event=event) + RegistrationFactory(user=new_user3, event=event) + RegistrationFactory(user=new_user4, event=event) + + OrderFactory(event=event, user=member, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_admin_user, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_user4, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_user2, status=OrderStatus.SALE) + OrderFactory(event=event, user=new_user, status=OrderStatus.CANCEL) + OrderFactory(event=event, user=new_user3, status=OrderStatus.CANCEL) + + client = get_api_client(user=new_admin_user) + + # Build the query string with multiple filter parameters + url = ( + _get_registration_url(paid_event) + + "?" + + "&".join([f"{key}={value}" for key, value in filter_params.items()]) + ) + response = client.get(url) + assert participant_count == response.data["count"] + assert response.status_code == status_code From 07a2ab5b29e44d8e870493fbff797b49b8283005 Mon Sep 17 00:00:00 2001 From: Yazan Zarka Date: Mon, 28 Oct 2024 18:58:11 +0100 Subject: [PATCH 05/16] Feat(registration)/filter participants (#915) * filtering by year and study * linting fix * added allergy filter * added filter by allergies and participants with allergy count to event. * Lint fix * Add new fixture for admin user * Start testing filtering + finished allergy filter test * Added integration test for participants filtering * lint fix * removed unused import * Update changelog * merge with dev and more filters * Fixed has_paid filter and added filter combination test * ran linting script * fixed has allergy count bug, and has_paid bug * post linting --------- Co-authored-by: Harry Linrui XU Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- app/content/serializers/event.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index b9e9fd4c..0a731a4c 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -1,3 +1,4 @@ +from django.db.models import Q from rest_framework import serializers from dry_rest_permissions.generics import DRYPermissionsField @@ -265,7 +266,7 @@ class EventStatisticsSerializer(BaseModelSerializer): studyyears = serializers.SerializerMethodField() studies = serializers.SerializerMethodField() has_allergy_count = serializers.SerializerMethodField() - has_paid_count = serializers.SerializerMethodField() + has_not_paid_count = serializers.SerializerMethodField() allow_photo_count = serializers.SerializerMethodField() class Meta: @@ -277,7 +278,7 @@ class Meta: "studyyears", "studies", "has_allergy_count", - "has_paid_count", + "has_not_paid_count", "allow_photo_count", ) @@ -287,6 +288,7 @@ def get_has_attended_count(self, obj, *args, **kwargs): def get_has_allergy_count(self, obj, *args, **kwargs): return ( obj.registrations.exclude(user__allergy__isnull=True) + .filter(is_on_wait=False) .exclude(user__allergy__exact="") .count() ) @@ -320,10 +322,10 @@ def get_studies(self, obj, *args, **kwargs): ) def get_allow_photo_count(self, obj, *args, **kwargs): - return obj.registrations.filter(allow_photo=False).count() + return obj.registrations.filter(allow_photo=False, is_on_wait=False).count() - def get_has_paid_count(self, obj, *args, **kwargs): + def get_has_not_paid_count(self, obj, *args, **kwargs): if obj.is_paid_event: - orders = obj.orders.filter(status=OrderStatus.SALE, event=obj).count() + orders = obj.orders.filter(~Q(status=OrderStatus.SALE), event=obj).count() return orders return 0 From ef656d3b708ce266773a6da0e49cfac68360eb53 Mon Sep 17 00:00:00 2001 From: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:21:07 +0100 Subject: [PATCH 06/16] Fix issue where full error is exposed to external users (#914) Signed-off-by: Tmpecho --- app/career/views/weekly_business.py | 11 ++++++----- app/content/views/upload.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/career/views/weekly_business.py b/app/career/views/weekly_business.py index 3e336630..d19bf38f 100644 --- a/app/career/views/weekly_business.py +++ b/app/career/views/weekly_business.py @@ -12,7 +12,6 @@ class WeeklyBusinessViewSet(BaseViewSet): - queryset = WeeklyBusiness.objects.none() serializer_class = WeeklyBusinessSerializer permission_classes = [BasicViewPermission] @@ -57,9 +56,10 @@ def create(self, request, *args, **kwargs): return Response( {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST ) - except ValueError as value_error: + except ValueError: return Response( - {"detail": str(value_error)}, status=status.HTTP_400_BAD_REQUEST + {"detail": "En feil oppstod under behandlingen av forespørselen."}, + status=status.HTTP_400_BAD_REQUEST ) def update(self, request, pk): @@ -78,9 +78,10 @@ def update(self, request, pk): return Response( {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST ) - except ValueError as value_error: + except ValueError: return Response( - {"detail": str(value_error)}, status=status.HTTP_400_BAD_REQUEST + {"detail": "En feil oppstod under behandlingen av forespørselen."}, + status=status.HTTP_400_BAD_REQUEST ) def destroy(self, request, *args, **kwargs): diff --git a/app/content/views/upload.py b/app/content/views/upload.py index e203c0f7..669c81d1 100644 --- a/app/content/views/upload.py +++ b/app/content/views/upload.py @@ -32,11 +32,10 @@ def upload(request): {"url": url}, status=status.HTTP_200_OK, ) - - except ValueError as value_error: + except ValueError: return Response( - {"detail": str(value_error)}, - status=status.HTTP_400_BAD_REQUEST, + {"detail": "En feil oppstod under behandlingen av forespørselen."}, + status=status.HTTP_400_BAD_REQUEST ) @@ -54,9 +53,8 @@ def delete(request, container_name, blob_name): {"detail": "Filen ble slettet"}, status=status.HTTP_200_OK, ) - - except ValueError as value_error: + except ValueError: return Response( - {"detail": str(value_error)}, - status=status.HTTP_400_BAD_REQUEST, + {"detail": "En feil oppstod under behandlingen av forespørselen."}, + status=status.HTTP_400_BAD_REQUEST ) From 02bb20904677c55846f70babb90b62f9bc195d8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:01:49 +0100 Subject: [PATCH 07/16] chore(deps): Bump sentry-sdk from 2.8.0 to 2.14.0 (#893) Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.8.0 to 2.14.0. - [Release notes](https://github.com/getsentry/sentry-python/releases) - [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-python/compare/2.8.0...2.14.0) --- updated-dependencies: - dependency-name: sentry-sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4f6210f3..a2c8be27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ logzero aiohttp-cors wheel mysqlclient == 2.1.1 -sentry-sdk == 2.8.0 +sentry-sdk == 2.14.0 celery == 5.4.0 azure-storage-blob == 12.23.1 python-dotenv ~= 1.0.1 From e6901819890aa3c89beed4415c71b9472ea0ab51 Mon Sep 17 00:00:00 2001 From: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:08:00 +0100 Subject: [PATCH 08/16] Refactor/minor code cleanup (#884) * Prefix unused variable names with underscore, remove redundant parenthesis, split long lines into several, rename functions to snake_case, Signed-off-by: Tmpecho * Update CHANGELOG.md Signed-off-by: Tmpecho * Make constant uppercase Signed-off-by: Tmpecho * Empty-Commit --------- Signed-off-by: Tmpecho Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- CHANGELOG.md | 1 + .../serializers/change_password.py | 1 + .../serializers/reset_password.py | 5 ++++ app/badge/filters/badge.py | 10 +++---- app/career/filters/job_post.py | 4 +-- app/career/views/weekly_business.py | 2 +- app/common/azure_file_handler.py | 22 +++++++------- app/common/enums.py | 4 +-- app/common/file_handler.py | 10 +++---- app/common/tasks.py | 2 +- app/common/tests/test_azure_filehandler.py | 2 +- app/communication/factories/mail_factory.py | 2 +- app/communication/models/banner.py | 2 +- app/communication/models/mail.py | 5 ++-- app/communication/tasks.py | 2 +- app/communication/tests/test_banner_model.py | 6 ++-- app/communication/views/banner.py | 4 +-- app/communication/views/notification.py | 2 +- .../views/user_notification_setting.py | 2 +- app/content/admin/admin.py | 2 +- .../factories/priority_pool_factory.py | 2 +- app/content/filters/event.py | 4 +-- app/content/filters/strike.py | 4 +-- app/content/filters/user.py | 12 ++++---- app/content/models/registration.py | 3 +- app/content/models/short_link.py | 2 +- app/content/models/strike.py | 8 ++--- app/content/models/user.py | 5 ++-- app/content/serializers/event.py | 4 +-- app/content/tasks/event.py | 12 ++++---- app/content/tests/test_strike_model.py | 14 ++++----- app/content/util/event_utils.py | 21 ++----------- app/content/util/feide_utils.py | 4 +-- app/content/views/accept_form.py | 4 ++- app/content/views/event.py | 14 ++++----- app/content/views/page.py | 2 +- app/content/views/registration.py | 2 +- app/content/views/upload.py | 6 ++-- app/content/views/user.py | 30 +++++++++---------- app/content/views/user_bio.py | 2 +- app/content/views/user_calendar_events.py | 3 +- app/emoji/views/reaction.py | 2 +- app/forms/models/forms.py | 2 +- app/forms/views/form.py | 2 +- app/forms/views/submission.py | 2 +- app/gallery/views/picture.py | 2 +- app/group/filters/membership.py | 2 +- app/group/serializers/group.py | 4 +-- app/group/views/fine.py | 13 ++++---- app/group/views/group.py | 2 +- app/group/views/law.py | 2 +- app/group/views/membership.py | 13 ++++++-- app/payment/models/order.py | 4 +-- app/payment/tasks.py | 2 +- app/payment/util/payment_utils.py | 2 +- app/payment/views/vipps_callback.py | 2 +- app/util/export_user_data.py | 2 -- app/util/migrations.py | 2 +- app/util/utils.py | 4 +-- 59 files changed, 158 insertions(+), 154 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c74f5f..4263f47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ --- ## Neste versjon +- 🎨**Overordnet**. Endret variabel og funksjonsnavn til å følge konvensjoner og andre små endringer. - ✨ **Filtrering**. Admin kan nå filtere deltakere av et arrangement på studie, studieår, om deltakere har allergier, (om deltakere godtar å bli tatt bilde av, om deltakere har ankommet), i tillegg til søk på fornavn og etternavn. ## Versjon 2024.10.11 diff --git a/app/authentication/serializers/change_password.py b/app/authentication/serializers/change_password.py index bc1dc685..5d214bdc 100644 --- a/app/authentication/serializers/change_password.py +++ b/app/authentication/serializers/change_password.py @@ -11,6 +11,7 @@ class ChangePasswordSerializer(serializers.Serializer): set_password_form_class = SetPasswordForm def __init__(self, *args, **kwargs): + self.set_password_form = None self.old_password_field_enabled = getattr( settings, "OLD_PASSWORD_FIELD_ENABLED", True ) diff --git a/app/authentication/serializers/reset_password.py b/app/authentication/serializers/reset_password.py index 73baeb6f..67d612d2 100644 --- a/app/authentication/serializers/reset_password.py +++ b/app/authentication/serializers/reset_password.py @@ -2,6 +2,7 @@ from django.contrib.auth.forms import PasswordResetForm from rest_framework import serializers +from rest_framework.fields import empty from sentry_sdk import capture_exception @@ -16,6 +17,10 @@ class PasswordResetSerializer(serializers.Serializer): email = serializers.EmailField() password_reset_form_class = PasswordResetForm + def __init__(self, instance=None, data=empty, **kwargs): + super().__init__(instance, data, kwargs) + self.reset_form = None + def validate_email(self, value): # Create PasswordResetForm with the serializer try: diff --git a/app/badge/filters/badge.py b/app/badge/filters/badge.py index 91e91a03..a7c887de 100644 --- a/app/badge/filters/badge.py +++ b/app/badge/filters/badge.py @@ -23,15 +23,15 @@ class UserWithBadgesFilter(FilterSet): queryset=BadgeCategory.objects.all(), ) - def filter_category(self, queryset, name, value): + def filter_category(self, queryset, _name, value): return queryset.filter(user_badges__badge__badge_category=value) - def filter_is_in_study(self, queryset, name, value): + def filter_is_in_study(self, queryset, _name, value): return queryset.filter( memberships__group__slug=value, memberships__group__type=GroupType.STUDY ) - def filter_is_in_studyyear(self, queryset, name, value): + def filter_is_in_studyyear(self, queryset, _name, value): return queryset.filter( memberships__group__slug=value, memberships__group__type=GroupType.STUDYYEAR ) @@ -49,7 +49,7 @@ class UserWithSpecificBadgeFilter(FilterSet): study = filters.NumberFilter(method="filter_study") studyyear = filters.NumberFilter(method="filter_studyyear") - def filter_study(self, queryset, name, value): + def filter_study(self, queryset, _name, value): return queryset.filter( Exists( Membership.objects.filter( @@ -60,7 +60,7 @@ def filter_study(self, queryset, name, value): ) ) - def filter_studyyear(self, queryset, name, value): + def filter_studyyear(self, queryset, _name, value): return queryset.filter( Exists( Membership.objects.filter( diff --git a/app/career/filters/job_post.py b/app/career/filters/job_post.py index 179b26bf..bcafc9d7 100644 --- a/app/career/filters/job_post.py +++ b/app/career/filters/job_post.py @@ -20,12 +20,12 @@ class Meta: model: JobPost fields = ["expired", "job_type"] - def filter_expired(self, queryset, name, value): + def filter_expired(self, queryset, _name, value): if value: return queryset.filter(deadline__lt=yesterday()).order_by("-deadline") return queryset.filter(deadline__gte=yesterday()).order_by("deadline") - def filter_classes(self, queryset, name, value): + def filter_classes(self, queryset, _name, value): query = Q() for year in value: query |= Q(class_start__lte=year, class_end__gte=year) diff --git a/app/career/views/weekly_business.py b/app/career/views/weekly_business.py index d19bf38f..657c3af0 100644 --- a/app/career/views/weekly_business.py +++ b/app/career/views/weekly_business.py @@ -29,7 +29,7 @@ def get_queryset(self): in_future_this_year_filter = Q(year=now().year) & Q(week__gte=week_nr(now())) next_year_filter = Q(year__gt=now().year) return WeeklyBusiness.objects.filter( - (in_future_this_year_filter) | next_year_filter + in_future_this_year_filter | next_year_filter ).order_by("year", "week") def list(self, request, *args, **kwargs): diff --git a/app/common/azure_file_handler.py b/app/common/azure_file_handler.py index 3476f422..b963ba4b 100644 --- a/app/common/azure_file_handler.py +++ b/app/common/azure_file_handler.py @@ -13,7 +13,7 @@ def __init__(self, blob=None, url=None): self.blob = blob self.url = url if url: - data = self.getContainerAndNameFromUrl() + data = self.get_container_and_name_from_url() self.containerName = data[0] self.blobName = data[1] @@ -29,24 +29,24 @@ def get_or_create_container(self, name="default"): container = blob_service_client.create_container(name, public_access="blob") return container - def getContainerAndNameFromUrl(self): + def get_container_and_name_from_url(self): import urllib.parse url = urllib.parse.unquote(self.url) # fmt: off - return re.sub("\w+:\/{2}[\d\w-]+(\.[\d\w-]+)*/", "", url).split("/") # noqa: W605 + return re.sub("\w+:/{2}[\d\w-]+(\.[\d\w-]+)*/", "", url).split("/") # noqa: W605 # fmt: on - def uploadBlob(self): - "Uploads the given blob to Azure and returns a url to the blob" + def upload_blob(self): + """Uploads the given blob to Azure and returns a url to the blob""" if not self.blob: raise ValueError("Du må sende med en blob for som skal lastes opp") - self.checkBlobSize() - containerName = self.getContainerNameFromBlob() - container = self.get_or_create_container(containerName) + self.check_blob_size() + container_name = self.get_container_name_from_blob() + container = self.get_or_create_container(container_name) - blob_name = f"{uuid.uuid4()}{self.getBlobName()}" + blob_name = f"{uuid.uuid4()}{self.get_blob_name()}" content_settings = ContentSettings( content_type=self.blob.content_type if self.blob.content_type else None, @@ -62,8 +62,8 @@ def uploadBlob(self): return blob_client.url raise ValueError("Noe gikk galt under filopplastningen") - def deleteBlob(self): - "Delete a blob by it's url" + def delete_blob(self): + """Delete a blob by it's url""" if not self.blobName and not self.containerName: raise ValueError("Du kan ikke slette en blob uten en url") diff --git a/app/common/enums.py b/app/common/enums.py index 68afaa78..20d1f7b3 100644 --- a/app/common/enums.py +++ b/app/common/enums.py @@ -75,11 +75,11 @@ class AdminGroup(models.TextChoices): @classmethod def all(cls): - return (cls.HS, cls.INDEX, cls.NOK, cls.PROMO, cls.SOSIALEN, cls.KOK) + return cls.HS, cls.INDEX, cls.NOK, cls.PROMO, cls.SOSIALEN, cls.KOK @classmethod def admin(cls): - return (cls.HS, cls.INDEX) + return cls.HS, cls.INDEX class Groups(models.TextChoices): diff --git a/app/common/file_handler.py b/app/common/file_handler.py index 3b745075..68a566ed 100644 --- a/app/common/file_handler.py +++ b/app/common/file_handler.py @@ -14,22 +14,22 @@ def __init__(self, blob=None): def get_or_create_container(self, name="default"): pass - def getBlobName(self): + def get_blob_name(self): return self.blob.name if self.blob.name else "" - def getContainerNameFromBlob(self): + def get_container_name_from_blob(self): return ( "".join(e for e in self.blob.content_type if e.isalnum()) if self.blob.content_type else "default" ) - def checkBlobSize(self): + def check_blob_size(self): if self.blob.size > self.SIZE_50_MB: raise ValueError("Filen kan ikke være større enn 50 MB") @abstractmethod - def uploadBlob(self): + def upload_blob(self): pass @@ -43,6 +43,6 @@ def replace_file(instance_image, validated_data_image): if instance_image and instance_image != validated_data_image: if settings.AZURE_BLOB_STORAGE_NAME in instance_image: try: - AzureFileHandler(url=instance_image).deleteBlob() + AzureFileHandler(url=instance_image).delete_blob() except Exception as e: capture_exception(e) diff --git a/app/common/tasks.py b/app/common/tasks.py index a2fe0cc0..97b4a2b4 100644 --- a/app/common/tasks.py +++ b/app/common/tasks.py @@ -3,7 +3,7 @@ @app.task(bind=True, base=BaseTask) -def delete_old_log_entries(self, *args, **kwargs): +def delete_old_log_entries(self, *_args, **_kwargs): from datetime import timedelta from django.contrib.admin.models import LogEntry diff --git a/app/common/tests/test_azure_filehandler.py b/app/common/tests/test_azure_filehandler.py index 5b9fcf6c..b8a649ee 100644 --- a/app/common/tests/test_azure_filehandler.py +++ b/app/common/tests/test_azure_filehandler.py @@ -9,6 +9,6 @@ def test_get_getContainerAndNameFromUrl(): handler = AzureFileHandler( url=f"https://{settings.AZURE_BLOB_STORAGE_NAME}/{container_name}/{file_name}" ) - data = handler.getContainerAndNameFromUrl() + data = handler.get_container_and_name_from_url() assert data[0] == container_name assert data[1] == file_name diff --git a/app/communication/factories/mail_factory.py b/app/communication/factories/mail_factory.py index 67da2a50..271ee0b8 100644 --- a/app/communication/factories/mail_factory.py +++ b/app/communication/factories/mail_factory.py @@ -12,7 +12,7 @@ class Meta: body = factory.Faker("paragraph", nb_sentences=10) @factory.post_generation - def users(self, create, extracted, **kwargs): + def users(self, create, extracted, **_kwargs): """Add users to the mail: `MailFactory.create(users=(user1, user2, user3))`""" if not create or not extracted: # Simple build, or nothing to add, do nothing. diff --git a/app/communication/models/banner.py b/app/communication/models/banner.py index f40fb1dd..61f7cfe2 100644 --- a/app/communication/models/banner.py +++ b/app/communication/models/banner.py @@ -66,5 +66,5 @@ def is_visible(self): return self.visible_from <= now() <= self.visible_until @classmethod - def has_visible_permission(cls, request): + def has_visible_permission(cls, _request): return True diff --git a/app/communication/models/mail.py b/app/communication/models/mail.py index e4ba7ec2..692fdd00 100644 --- a/app/communication/models/mail.py +++ b/app/communication/models/mail.py @@ -27,7 +27,7 @@ class Meta: def send(self, connection): from app.communication.notifier import send_html_email - emails = (user.email for user in self.users.all()) + emails = [user.email for user in self.users.all()] is_success = send_html_email( to_mails=emails, html=self.body, @@ -40,4 +40,5 @@ def send(self, connection): return is_success def __str__(self): - return f"\"{self.subject}\", to {self.users.all()[0] if self.users.count() == 1 else f'{self.users.count()} users'}, {'sent' if self.sent else 'eta'} {self.eta}" + return (f"\"{self.subject}\", to {self.users.all()[0] if self.users.count() == 1 else f'{self.users.count()} users'}, " + f"{'sent' if self.sent else 'eta'} {self.eta}") diff --git a/app/communication/tasks.py b/app/communication/tasks.py index fd49f1d4..f2867875 100644 --- a/app/communication/tasks.py +++ b/app/communication/tasks.py @@ -5,7 +5,7 @@ @app.task(bind=True, base=BaseTask) -def send_due_mails(self, *args, **kwargs): +def send_due_mails(self, *_args, **_kwargs): from django.core.mail import get_connection from app.communication.models.mail import Mail diff --git a/app/communication/tests/test_banner_model.py b/app/communication/tests/test_banner_model.py index e46051d0..cbb2936f 100644 --- a/app/communication/tests/test_banner_model.py +++ b/app/communication/tests/test_banner_model.py @@ -7,7 +7,7 @@ DatesMixedError, ) from app.communication.factories.banner_factory import BannerFactory -from app.util.utils import getTimezone, now +from app.util.utils import get_timezone, now @pytest.mark.parametrize( @@ -27,8 +27,8 @@ def test_two_banners_can_not_be_visible_simultaneously_in_any_period( This test uses timedelta and parameterize to switch visible_from and visible_until between 5 days earlier or later.""" existing_banner = BannerFactory( - visible_from=datetime(2020, 1, 1, tzinfo=getTimezone()), - visible_until=datetime(2021, 1, 1, tzinfo=getTimezone()), + visible_from=datetime(2020, 1, 1, tzinfo=get_timezone()), + visible_until=datetime(2021, 1, 1, tzinfo=get_timezone()), ) with pytest.raises(AnotherVisibleBannerError): diff --git a/app/communication/views/banner.py b/app/communication/views/banner.py index 4dd0ddd3..888f8319 100644 --- a/app/communication/views/banner.py +++ b/app/communication/views/banner.py @@ -21,12 +21,12 @@ class Meta: model = Banner fields = ["is_visible", "is_expired"] - def filter_is_visible(self, queryset, name, value): + def filter_is_visible(self, queryset, _name, value): if value: return queryset.filter(visible_from__lte=now(), visible_until__gte=now()) return queryset - def filter_is_expired(self, queryset, name, value): + def filter_is_expired(self, queryset, _name, value): if value: return queryset.filter(visible_until__lt=now()) return queryset diff --git a/app/communication/views/notification.py b/app/communication/views/notification.py index 40c614ee..1b96fa26 100644 --- a/app/communication/views/notification.py +++ b/app/communication/views/notification.py @@ -36,6 +36,6 @@ def update(self, request, pk): status=status.HTTP_200_OK, ) return Response( - {"detail": ("Kunne ikke oppdatere varslet")}, + {"detail": "Kunne ikke oppdatere varslet"}, status=status.HTTP_403_FORBIDDEN, ) diff --git a/app/communication/views/user_notification_setting.py b/app/communication/views/user_notification_setting.py index e24360c9..0786ac99 100644 --- a/app/communication/views/user_notification_setting.py +++ b/app/communication/views/user_notification_setting.py @@ -50,7 +50,7 @@ def create(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="choices") - def choices(self, request, *args, **kwargs): + def choices(self, _request, *_args, **_kwargs): return Response( list( map( diff --git a/app/content/admin/admin.py b/app/content/admin/admin.py index 1a219f7e..16191c23 100644 --- a/app/content/admin/admin.py +++ b/app/content/admin/admin.py @@ -40,7 +40,7 @@ class StrikeAdmin(admin.ModelAdmin): ) -def admin_delete_registration(modeladmin, request, queryset): +def admin_delete_registration(_modeladmin, _request, queryset): for registration in queryset: registration.admin_unregister() diff --git a/app/content/factories/priority_pool_factory.py b/app/content/factories/priority_pool_factory.py index ec2af803..3e783d85 100644 --- a/app/content/factories/priority_pool_factory.py +++ b/app/content/factories/priority_pool_factory.py @@ -12,7 +12,7 @@ class Meta: event = factory.SubFactory(EventFactory) @factory.post_generation - def groups(self, create, extracted, **kwargs): + def groups(self, create, extracted, **_kwargs): if not create or not extracted: # Simple build, do nothing. return diff --git a/app/content/filters/event.py b/app/content/filters/event.py index 48ad63d3..abf18987 100644 --- a/app/content/filters/event.py +++ b/app/content/filters/event.py @@ -26,7 +26,7 @@ class Meta: model = Event fields = ["category", "organizer", "end_range", "start_range"] - def filter_open_for_sign_up(self, queryset, name, value): + def filter_open_for_sign_up(self, queryset, _name, value): if value: return queryset.filter( sign_up=True, @@ -35,7 +35,7 @@ def filter_open_for_sign_up(self, queryset, name, value): ) return queryset - def filter_user_favorites(self, queryset, name, value): + def filter_user_favorites(self, queryset, _name, value): if value and self.request.user: return queryset.filter(favorite_users__user_id=self.request.user.user_id) return queryset diff --git a/app/content/filters/strike.py b/app/content/filters/strike.py index f8b82000..75750908 100644 --- a/app/content/filters/strike.py +++ b/app/content/filters/strike.py @@ -11,7 +11,7 @@ class StrikeFilter(FilterSet): study = filters.NumberFilter(method="filter_study") studyyear = filters.NumberFilter(method="filter_studyyear") - def filter_study(self, queryset, name, value): + def filter_study(self, queryset, _name, value): return queryset.filter( Exists( Membership.objects.filter( @@ -22,7 +22,7 @@ def filter_study(self, queryset, name, value): ) ) - def filter_studyyear(self, queryset, name, value): + def filter_studyyear(self, queryset, _name, value): return queryset.filter( Exists( Membership.objects.filter( diff --git a/app/content/filters/user.py b/app/content/filters/user.py index a189f239..9b83b1ff 100644 --- a/app/content/filters/user.py +++ b/app/content/filters/user.py @@ -35,28 +35,28 @@ class Meta: "in_group", ] - def filter_is_in_study(self, queryset, name, value): + def filter_is_in_study(self, queryset, _name, value): return queryset.filter( memberships__group__slug=value, memberships__group__type=GroupType.STUDY ) - def filter_is_in_studyyear(self, queryset, name, value): + def filter_is_in_studyyear(self, queryset, _name, value): return queryset.filter( memberships__group__slug=value, memberships__group__type=GroupType.STUDYYEAR ) - def filter_is_in_group(self, queryset, name, value): + def filter_is_in_group(self, queryset, _name, value): return queryset.filter(memberships__group__slug=value) - def filter_is_TIHLDE_member(self, queryset, name, value): + def filter_is_TIHLDE_member(self, queryset, _name, value): if value is False: return queryset.exclude(memberships__group__slug=Groups.TIHLDE) return queryset.filter(memberships__group__slug=Groups.TIHLDE) - def filter_has_active_strikes(self, queryset, name, value): + def filter_has_active_strikes(self, queryset, _name, value): if value is False: return queryset.exclude(strikes__in=Strike.objects.active()).distinct() return queryset.filter(strikes__in=Strike.objects.active()).distinct() - def filter_has_allowed_photo(self, queryset, name, value): + def filter_has_allowed_photo(self, queryset, _name, value): return queryset.filter(allows_photo_by_default=value) diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 19c6c379..22006637 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -50,7 +50,7 @@ class Meta: verbose_name_plural = "Registrations" @classmethod - def has_retrieve_permission(cls, request): + def has_retrieve_permission(cls, _request): return True @classmethod @@ -415,6 +415,7 @@ def check_registration_has_ended(self): if self.event.end_registration_at < now(): raise ValidationError("Påmeldingsfristen har passert") + # noinspection PyShadowingBuiltins def get_submissions(self, type=None): from app.forms.models import EventForm, Submission diff --git a/app/content/models/short_link.py b/app/content/models/short_link.py index 22bc61a5..9cadea0d 100644 --- a/app/content/models/short_link.py +++ b/app/content/models/short_link.py @@ -29,7 +29,7 @@ def has_create_permission(cls, request): return super().has_write_permission(request) @classmethod - def has_destroy_permission(cls, request): + def has_destroy_permission(cls, _request): return True def has_object_write_permission(self, request): diff --git a/app/content/models/strike.py b/app/content/models/strike.py index 696dc446..872c1651 100644 --- a/app/content/models/strike.py +++ b/app/content/models/strike.py @@ -10,7 +10,7 @@ from app.communication.enums import UserNotificationSettingType from app.content.models import Event from app.util.models import BaseModel -from app.util.utils import getTimezone, now +from app.util.utils import get_timezone, now class Holiday: @@ -117,10 +117,10 @@ def expires_at(self): end = holiday.end start_date = datetime( - self.created_at.year, start[0], start[1], tzinfo=getTimezone() + self.created_at.year, start[0], start[1], tzinfo=get_timezone() ) end_date = datetime( - self.created_at.year, end[0], end[1], tzinfo=getTimezone() + self.created_at.year, end[0], end[1], tzinfo=get_timezone() ) if end_date < start_date: @@ -133,7 +133,7 @@ def expires_at(self): expired_date += smallest_difference + timedelta(days=1) break - return expired_date.astimezone(getTimezone()) + return expired_date.astimezone(get_timezone()) @classmethod def has_destroy_permission(cls, request): diff --git a/app/content/models/user.py b/app/content/models/user.py index 1c4ce58b..1230fb7a 100644 --- a/app/content/models/user.py +++ b/app/content/models/user.py @@ -209,7 +209,7 @@ def has_write_permission(cls, request): ) @classmethod - def has_create_permission(cls, request): + def has_create_permission(cls, _request): return True def has_object_write_permission(self, request): @@ -234,9 +234,10 @@ def has_object_get_user_detail_strikes_permission(self, request): ) +# noinspection PyUnusedLocal @receiver(post_save, sender=settings.AUTH_USER_MODEL) @disable_for_loaddata -def create_auth_token(sender, instance=None, created=False, **kwargs): +def create_auth_token(sender, instance=None, created=False, **_kwargs): """Generate token at creation of user""" if created: Token.objects.create(user=instance) diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index 0a731a4c..c97704df 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -282,7 +282,7 @@ class Meta: "allow_photo_count", ) - def get_has_attended_count(self, obj, *args, **kwargs): + def get_has_attended_count(self, obj, *_args, **_kwargs): return obj.registrations.filter(is_on_wait=False, has_attended=True).count() def get_has_allergy_count(self, obj, *args, **kwargs): @@ -307,7 +307,7 @@ def get_studyyears(self, obj, *args, **kwargs): ), ) - def get_studies(self, obj, *args, **kwargs): + def get_studies(self, obj, *_args, **_kwargs): return filter( lambda study: study["amount"] > 0, map( diff --git a/app/content/tasks/event.py b/app/content/tasks/event.py index 12da3245..ac2e6c8a 100644 --- a/app/content/tasks/event.py +++ b/app/content/tasks/event.py @@ -19,7 +19,7 @@ @app.task(bind=True, base=BaseTask) -def run_sign_off_deadline_reminder(self, *args, **kwargs): +def run_sign_off_deadline_reminder(self, *_args, **_kwargs): from app.content.models.event import Event try: @@ -41,7 +41,7 @@ def run_sign_off_deadline_reminder(self, *args, **kwargs): @app.task(bind=True, base=BaseTask) -def run_post_event_actions(self, *args, **kwargs): +def run_post_event_actions(self, *_args, **_kwargs): from app.content.models.event import Event try: @@ -61,7 +61,7 @@ def run_post_event_actions(self, *args, **kwargs): @app.task(bind=True, base=BaseTask) -def run_sign_up_start_notifier(self, *args, **kwargs): +def run_sign_up_start_notifier(self, *_args, **_kwargs): from app.content.models.event import Event try: @@ -82,7 +82,7 @@ def run_sign_up_start_notifier(self, *args, **kwargs): capture_exception(e) -def __sign_off_deadline_reminder(event, *args, **kwargs): +def __sign_off_deadline_reminder(event, *_args, **_kwargs): from app.content.models import User users_not_on_wait = User.objects.filter( @@ -117,7 +117,7 @@ def __sign_off_deadline_reminder(event, *args, **kwargs): event.save(update_fields=["runned_sign_off_deadline_reminder"]) -def __post_event_actions(event, *args, **kwargs): +def __post_event_actions(event, *_args, **_kwargs): from app.content.models import User if event.can_cause_strikes: @@ -149,7 +149,7 @@ def __post_event_actions(event, *args, **kwargs): event.save(update_fields=["runned_post_event_actions"]) -def __sign_up_start_notifier(event, *args, **kwargs): +def __sign_up_start_notifier(event, *_args, **_kwargs): description = f'Påmelding til "{event.title}" har nå åpnet! 🏃 Arrangementet starter {datetime_format(event.start_date)} og har {event.limit} plasser. Påmeldingen er åpen frem til {datetime_format(event.end_registration_at)}, men husk at det kan bli fullt før det. ⏲️' CHANNEL_ID = ( diff --git a/app/content/tests/test_strike_model.py b/app/content/tests/test_strike_model.py index de90371c..64afee29 100644 --- a/app/content/tests/test_strike_model.py +++ b/app/content/tests/test_strike_model.py @@ -5,7 +5,7 @@ from app.content.factories import StrikeFactory from app.content.models.strike import STRIKE_DURATION_IN_DAYS -from app.util.utils import getTimezone +from app.util.utils import get_timezone @pytest.mark.django_db @@ -30,8 +30,8 @@ def test_strike_is_active_or_not_with_freeze_through_winter_holiday( mock_now, today, created_at, expected_result ): """Strikes are frozen in winter from the 29th of November to the 1st of January.""" - today = today.replace(tzinfo=getTimezone()) - created_at = created_at.replace(tzinfo=getTimezone()) + today = today.replace(tzinfo=get_timezone()) + created_at = created_at.replace(tzinfo=get_timezone()) mock_now.return_value = today strike = StrikeFactory.build(created_at=created_at) @@ -63,8 +63,8 @@ def test_strike_is_active_or_not_with_freeze_through_summer_holiday( mock_now, today, created_at, expected_result ): """Strikes are frozen in summer from the 10th of May to 15th of August.""" - today = today.replace(tzinfo=getTimezone()) - created_at = created_at.replace(tzinfo=getTimezone()) + today = today.replace(tzinfo=get_timezone()) + created_at = created_at.replace(tzinfo=get_timezone()) mock_now.return_value = today strike = StrikeFactory.build(created_at=created_at) @@ -89,7 +89,7 @@ def test_strike_is_active_or_not_with_freeze_through_summer_holiday( ) def test_active_days_of_a_strike_with_freeze_through_holidays(created_at, days_active): """Days active is the amount of days a strike is active which is at least 20 days""" - created_at = created_at.replace(tzinfo=getTimezone()) + created_at = created_at.replace(tzinfo=get_timezone()) strike = StrikeFactory.build(created_at=created_at) @@ -117,7 +117,7 @@ def test_year_of_expire_date_different_than_created_year_with_freeze_through_win ): """A strike should have a different year of expired date if created less 20 days before the winter holiday""" - created_at = created_at.replace(tzinfo=getTimezone()) + created_at = created_at.replace(tzinfo=get_timezone()) strike = StrikeFactory.build(created_at=created_at) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index d6343190..e611adc9 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -1,14 +1,11 @@ -import os -from datetime import datetime - from sentry_sdk import capture_exception from app.content.exceptions import RefundFailedError from app.payment.tasks import check_if_has_paid from app.payment.util.payment_utils import ( - get_new_access_token, initiate_payment, refund_payment, + check_access_token ) @@ -45,13 +42,7 @@ def create_vipps_order(order_id, event, transaction_text, fallback): Creates vipps order, and returns the url. """ - access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") - expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") - - if not access_token or datetime.now() >= datetime.fromtimestamp(int(expires_at)): - (expires_at, access_token) = get_new_access_token() - os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) - os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) + access_token = check_access_token() event_price = int(event.paid_information.price * 100) @@ -71,13 +62,7 @@ def refund_vipps_order(order_id, event, transaction_text): Refunds vipps order. """ - access_token = os.environ.get("PAYMENT_ACCESS_TOKEN") - expires_at = os.environ.get("PAYMENT_ACCESS_TOKEN_EXPIRES_AT") - - if not access_token or datetime.now() >= datetime.fromtimestamp(int(expires_at)): - (expires_at, access_token) = get_new_access_token() - os.environ.update({"PAYMENT_ACCESS_TOKEN": access_token}) - os.environ.update({"PAYMENT_ACCESS_TOKEN_EXPIRES_AT": str(expires_at)}) + access_token = check_access_token() event_price = int(event.paid_information.price) * 100 diff --git a/app/content/util/feide_utils.py b/app/content/util/feide_utils.py index bc38cc96..bbce707c 100644 --- a/app/content/util/feide_utils.py +++ b/app/content/util/feide_utils.py @@ -52,7 +52,7 @@ def get_feide_tokens(code: str) -> tuple[str, str]: if "access_token" not in json or "id_token" not in json: raise FeideTokenNotFoundError() - return (json["access_token"], json["id_token"]) + return json["access_token"], json["id_token"] def get_feide_user_info_from_jwt(jwt_token: str) -> tuple[str, str]: @@ -73,7 +73,7 @@ def get_feide_user_info_from_jwt(jwt_token: str) -> tuple[str, str]: if not feide_username: raise FeideUsernameNotFoundError() - return (user_info["name"], feide_username) + return user_info["name"], feide_username def get_feide_user_groups(access_token: str) -> list[str]: diff --git a/app/content/views/accept_form.py b/app/content/views/accept_form.py index 74a3bd98..44e4fe6f 100644 --- a/app/content/views/accept_form.py +++ b/app/content/views/accept_form.py @@ -23,7 +23,9 @@ def accept_form(request): if len(types) == 1 and types[0].lower() == "annonse" else MAIL_NOK_LEADER ) - title = f"{body['info']['bedrift']} vil ha {', '.join(types[:-1])}{' og ' if len(types) > 1 else ''}{', '.join(types[-1:])}, {', '.join(times[:-1])}{' og ' if len(times) > 1 else ''}{', '.join(times[-1:])}" + title = (f"{body['info']['bedrift']} vil ha {', '.join(types[:-1])}" + f"{' og ' if len(types) > 1 else ''}{', '.join(types[-1:])}, " + f"{', '.join(times[:-1])}{' og ' if len(times) > 1 else ''}{', '.join(times[-1:])}") is_success = send_html_email( to_mails=[to_mail], diff --git a/app/content/views/event.py b/app/content/views/event.py index ecb52257..9827120c 100644 --- a/app/content/views/event.py +++ b/app/content/views/event.py @@ -170,7 +170,7 @@ def destroy(self, request, *args, **kwargs): super().destroy(request, *args, **kwargs) return Response( - {"detail": ("Arrangementet ble slettet")}, status=status.HTTP_200_OK + {"detail": "Arrangementet ble slettet"}, status=status.HTTP_200_OK ) @action( @@ -179,7 +179,7 @@ def destroy(self, request, *args, **kwargs): url_path="favorite", permission_classes=(IsMember,), ) - def user_favorite(self, request, pk, *args, **kwargs): + def user_favorite(self, request, pk, *_args, **_kwargs): event = get_object_or_404(Event, id=pk) if request.method == "PUT": @@ -202,7 +202,7 @@ def user_favorite(self, request, pk, *args, **kwargs): methods=["get"], url_path="public_registrations", ) - def get_public_event_registrations(self, request, pk, *args, **kwargs): + def get_public_event_registrations(self, request, pk, *_args, **_kwargs): event = get_object_or_404(Event, id=pk) registrations = event.get_participants() return self.paginate_response( @@ -216,7 +216,7 @@ def get_public_event_registrations(self, request, pk, *args, **kwargs): methods=["post"], url_path="notify", ) - def notify_registered_users(self, request, *args, **kwargs): + def notify_registered_users(self, request, *_args, **_kwargs): try: title = request.data["title"] message = request.data["message"] @@ -242,7 +242,7 @@ def notify_registered_users(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="admin") - def get_events_where_is_admin(self, request, *args, **kwargs): + def get_events_where_is_admin(self, request, *_args, **_kwargs): if not self.request.user: return Response( {"detail": "Du har ikke tilgang til å opprette/redigere arrangementer"}, @@ -271,7 +271,7 @@ def get_events_where_is_admin(self, request, *args, **kwargs): ) @action(detail=True, methods=["get"], url_path="statistics") - def statistics(self, request, *args, **kwargs): + def statistics(self, request, *_args, **_kwargs): event = self.get_object() serializer = EventStatisticsSerializer(event, context={"request": request}) return Response(serializer.data, status=status.HTTP_200_OK) @@ -285,7 +285,7 @@ def statistics(self, request, *args, **kwargs): FormParser, ), ) - def mail_gift_cards(self, request, *args, **kwargs): + def mail_gift_cards(self, request, *_args, **_kwargs): event = self.get_object() dispatcher = request.user diff --git a/app/content/views/page.py b/app/content/views/page.py index a9f02b98..d509a53c 100644 --- a/app/content/views/page.py +++ b/app/content/views/page.py @@ -175,7 +175,7 @@ def destroy(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"]) - def tree(self, request, *args, **kwargs): + def tree(self, _request, *_args, **_kwargs): root = Page.objects.get(parent=None) serializer = PageTreeSerializer(root) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 0aa13b3a..4aa3bf68 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -154,7 +154,7 @@ def _admin_unregister(self, registration): ) @action(detail=False, methods=["post"], url_path="add") - def add_registration(self, request, *args, **kwargs): + def add_registration(self, request, *_args, **_kwargs): """Add registration to event for admins""" if not is_admin_group_user(request): diff --git a/app/content/views/upload.py b/app/content/views/upload.py index 669c81d1..b9816e43 100644 --- a/app/content/views/upload.py +++ b/app/content/views/upload.py @@ -27,7 +27,7 @@ def upload(request): key = list(request.FILES.keys())[0] blob = request.FILES[key] - url = AzureFileHandler(blob).uploadBlob() + url = AzureFileHandler(blob).upload_blob() return Response( {"url": url}, status=status.HTTP_200_OK, @@ -41,14 +41,14 @@ def upload(request): @api_view(["DELETE"]) @permission_classes([IsMember]) -def delete(request, container_name, blob_name): +def delete(_request, container_name, blob_name): """Method for deleting files from Azure Blob Storage, only allowed for members""" try: handler = AzureFileHandler() handler.blobName = blob_name handler.containerName = container_name - handler.deleteBlob() + handler.delete_blob() return Response( {"detail": "Filen ble slettet"}, status=status.HTTP_200_OK, diff --git a/app/content/views/user.py b/app/content/views/user.py index bb3e67c4..fd9bf1ff 100644 --- a/app/content/views/user.py +++ b/app/content/views/user.py @@ -148,7 +148,7 @@ def _get_user(self, request, pk): return get_object_or_404(User, user_id=pk) @action(detail=False, methods=["post"], url_path="me/slack") - def connect_to_slack(self, request, *args, **kwargs): + def connect_to_slack(self, request, *_args, **_kwargs): user = self.request.user self.check_object_permissions(self.request, user) @@ -171,7 +171,7 @@ def connect_to_slack(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="me/permissions") - def get_user_permissions(self, request, *args, **kwargs): + def get_user_permissions(self, request, *_args, **_kwargs): try: serializer = UserPermissionsSerializer( request.user, context={"request": request} @@ -184,7 +184,7 @@ def get_user_permissions(self, request, *args, **kwargs): ) @action(detail=True, methods=["get"], url_path="memberships") - def get_user_memberships(self, request, pk, *args, **kwargs): + def get_user_memberships(self, request, pk, *_args, **_kwargs): user = self._get_user(request, pk) self.check_object_permissions(self.request, user) @@ -198,7 +198,7 @@ def get_user_memberships(self, request, pk, *args, **kwargs): ) @action(detail=True, methods=["get"], url_path="memberships-with-fines") - def get_user_memberships_with_fines(self, request, pk, *args, **kwargs): + def get_user_memberships_with_fines(self, request, pk, *_args, **_kwargs): user = self._get_user(request, pk) self.check_object_permissions(self.request, user) @@ -212,7 +212,7 @@ def get_user_memberships_with_fines(self, request, pk, *args, **kwargs): ) @action(detail=True, methods=["get"], url_path="membership-histories") - def get_user_membership_histories(self, request, pk, *args, **kwargs): + def get_user_membership_histories(self, request, pk, *_args, **_kwargs): user = self._get_user(request, pk) self.check_object_permissions(self.request, user) @@ -225,7 +225,7 @@ def get_user_membership_histories(self, request, pk, *args, **kwargs): context={"request": request}, ) - def post_user_badges(self, request, *args, **kwargs): + def post_user_badges(self, request, *_args, **_kwargs): import uuid user = self.request.user @@ -265,7 +265,7 @@ def post_user_badges(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST, ) - def get_user_detail_badges(self, request, *args, **kwargs): + def get_user_detail_badges(self, request, *_args, **kwargs): user = self._get_user(request, kwargs["pk"]) user_badges = user.user_badges.order_by("-created_at") badges = [ @@ -289,7 +289,7 @@ def get_or_post_detail_user_badges(self, request, *args, **kwargs): return self.post_user_badges(request, *args, **kwargs) @action(detail=False, methods=["get"], url_path="me/strikes") - def get_user_strikes(self, request, *args, **kwargs): + def get_user_strikes(self, request, *_args, **_kwargs): strikes = request.user.strikes.active() serializer = UserInfoStrikeSerializer( instance=strikes, many=True, context={"request": request} @@ -297,7 +297,7 @@ def get_user_strikes(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_200_OK) @action(detail=True, methods=["get"], url_path="strikes") - def get_user_detail_strikes(self, request, *args, **kwargs): + def get_user_detail_strikes(self, request, *_args, **_kwargs): user = self.get_object() strikes = user.strikes.active() serializer = UserInfoStrikeSerializer( @@ -306,7 +306,7 @@ def get_user_detail_strikes(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_200_OK) @action(detail=False, methods=["get"], url_path="me/events") - def get_user_events(self, request, *args, **kwargs): + def get_user_events(self, request, *_args, **_kwargs): filter_field = self.request.query_params.get("expired") event_has_ended = CaseInsensitiveBooleanQueryParam(filter_field).value @@ -326,7 +326,7 @@ def get_user_events(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="me/forms") - def get_user_forms(self, request, *args, **kwargs): + def get_user_forms(self, request, *_args, **_kwargs): forms = request.user.forms filter_field = request.query_params.get("unanswered") @@ -347,7 +347,7 @@ def get_user_forms(self, request, *args, **kwargs): url_path="activate", permission_classes=(IsHS | IsDev,), ) - def makeTIHLDEMember(self, request, *args, **kwargs): + def makeTIHLDEMember(self, request, *_args, **_kwargs): TIHLDE = Group.objects.get(slug=Groups.TIHLDE) user_id = request.data["user_id"] user = get_object_or_404(User, user_id=user_id) @@ -372,7 +372,7 @@ def makeTIHLDEMember(self, request, *args, **kwargs): url_path="decline", permission_classes=(IsHS | IsDev,), ) - def declineTIHLDEMember(self, request, *args, **kwargs): + def declineTIHLDEMember(self, request, *_args, **_kwargs): user_id = request.data["user_id"] try: reason = request.data["reason"] @@ -397,7 +397,7 @@ def declineTIHLDEMember(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="me/data") - def export_user_data(self, request, *args, **kwargs): + def export_user_data(self, request, *_args, **_kwargs): export_successfull = export_user_data(request, request.user) if export_successfull: @@ -414,7 +414,7 @@ def export_user_data(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="me/reservations") - def get_user_reservations(self, request, *args, **kwargs): + def get_user_reservations(self, request, *_args, **_kwargs): user = request.user reservations = Reservation.objects.filter(author=user).order_by("start_time") serializer = ReservationSerializer( diff --git a/app/content/views/user_bio.py b/app/content/views/user_bio.py index fcb7da4c..5eeb1081 100644 --- a/app/content/views/user_bio.py +++ b/app/content/views/user_bio.py @@ -50,5 +50,5 @@ def update(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): super().destroy(request, *args, **kwargs) return Response( - {"detail": ("Brukerbio ble slettet")}, status=status.HTTP_200_OK + {"detail": "Brukerbio ble slettet"}, status=status.HTTP_200_OK ) diff --git a/app/content/views/user_calendar_events.py b/app/content/views/user_calendar_events.py index fd480a5b..3c26c72f 100644 --- a/app/content/views/user_calendar_events.py +++ b/app/content/views/user_calendar_events.py @@ -22,7 +22,8 @@ def __call__(self, request, *args, **kwargs): if not self.user.public_event_registrations: return JsonResponse( { - "detail": "Denne brukeren har skrudd av offentlig deling av påmeldinger til arrangementer. Du kan derfor ikke hente ut brukerens arrangementer som .ics-fil" + "detail": "Denne brukeren har skrudd av offentlig deling av påmeldinger til arrangementer. " + "Du kan derfor ikke hente ut brukerens arrangementer som .ics-fil" }, status=status.HTTP_403_FORBIDDEN, ) diff --git a/app/emoji/views/reaction.py b/app/emoji/views/reaction.py index 518364df..38c19f2e 100644 --- a/app/emoji/views/reaction.py +++ b/app/emoji/views/reaction.py @@ -31,7 +31,7 @@ def update(self, request, *args, **kwargs): reaction, data=request.data, context={"request": request} ) if serializer.is_valid(): - reaction = super().perform_update(serializer) + super().perform_update(serializer) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, *args, **kwargs): diff --git a/app/forms/models/forms.py b/app/forms/models/forms.py index 642654ec..737fb84e 100644 --- a/app/forms/models/forms.py +++ b/app/forms/models/forms.py @@ -64,7 +64,7 @@ def has_retrieve_permission(cls, request): return True @classmethod - def has_statistics_permission(cls, request): + def has_statistics_permission(cls, _request): return True def has_object_statistics_permission(self, request): diff --git a/app/forms/views/form.py b/app/forms/views/form.py index 09ec32a4..a5b75cfd 100644 --- a/app/forms/views/form.py +++ b/app/forms/views/form.py @@ -52,7 +52,7 @@ def destroy(self, request, *args, **kwargs): return Response({"detail": "Skjemaet ble slettet"}, status=status.HTTP_200_OK) @action(detail=True, methods=["get"], url_path="statistics") - def statistics(self, request, *args, **kwargs): + def statistics(self, _request, *_args, **_kwargs): form = self.get_object() serializer = FormStatisticsSerializer(form) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/app/forms/views/submission.py b/app/forms/views/submission.py index b148a212..0333d71c 100644 --- a/app/forms/views/submission.py +++ b/app/forms/views/submission.py @@ -68,6 +68,6 @@ def create(self, request, *args, **kwargs): return super().create(request, *args, **kwargs) @action(detail=False, methods=["get"], url_path="download") - def download(self, request, *args, **kwargs): + def download(self, _request, *_args, **_kwargs): """To return the response as csv, include header 'Accept: text/csv.""" return SubmissionsCsvWriter(self.get_queryset()).write_csv() diff --git a/app/gallery/views/picture.py b/app/gallery/views/picture.py index fa3cf540..7f395e5b 100644 --- a/app/gallery/views/picture.py +++ b/app/gallery/views/picture.py @@ -36,7 +36,7 @@ def create(self, request, *args, **kwargs): errors = 0 for file in files: try: - url = AzureFileHandler(file).uploadBlob() + url = AzureFileHandler(file).upload_blob() Picture.objects.create(image=url, album=album) except Exception: errors += 1 diff --git a/app/group/filters/membership.py b/app/group/filters/membership.py index b975bb73..f04548c6 100644 --- a/app/group/filters/membership.py +++ b/app/group/filters/membership.py @@ -16,7 +16,7 @@ class Meta: model = Membership fields = ["onlyMembers"] - def filter_membership_type(self, queryset, name, value): + def filter_membership_type(self, queryset, _name, value): if value: return queryset.filter(membership_type=MembershipType.MEMBER) return queryset diff --git a/app/group/serializers/group.py b/app/group/serializers/group.py index b3ac0214..02c56144 100644 --- a/app/group/serializers/group.py +++ b/app/group/serializers/group.py @@ -95,7 +95,7 @@ class Meta: model = Group fields = ("studyyears", "studies") - def get_studyyears(self, obj, *args, **kwargs): + def get_studyyears(self, obj, *_args, **_kwargs): return filter( lambda studyyear: studyyear["amount"] > 0, map( @@ -109,7 +109,7 @@ def get_studyyears(self, obj, *args, **kwargs): ), ) - def get_studies(self, obj, *args, **kwargs): + def get_studies(self, obj, *_args, **_kwargs): return filter( lambda study: study["amount"] > 0, map( diff --git a/app/group/views/fine.py b/app/group/views/fine.py index 8b443eaa..de15f638 100644 --- a/app/group/views/fine.py +++ b/app/group/views/fine.py @@ -42,6 +42,7 @@ def get_queryset(self): group__slug=self.kwargs["slug"], group__fines_activated=True ) + # noinspection PyShadowingNames def create(self, request, *args, **kwargs): context = { "group_slug": kwargs["slug"], @@ -85,7 +86,7 @@ def destroy(self, request, *args, **kwargs): return Response({"detail": ("Boten ble slettet")}, status=status.HTTP_200_OK) @action(detail=False, methods=["get"], url_path=r"users/(?P[^/.]+)") - def get_user_fines(self, request, *args, **kwargs): + def get_user_fines(self, _request, *_args, **kwargs): """Get the fines of a specific user in a group""" fines = ( @@ -96,7 +97,7 @@ def get_user_fines(self, request, *args, **kwargs): return self.paginate_response(data=fines, serializer=FineNoUserSerializer) @action(detail=False, methods=["get"], url_path="users") - def get_fine_users(self, request, *args, **kwargs): + def get_fine_users(self, _request, *_args, **_kwargs): """Get the users in a group which has fines and how many""" users = self.get_fine_filter_query() return self.paginate_response(data=users, serializer=UserFineSerializer) @@ -119,7 +120,7 @@ def get_fine_filter_query(self): ) @action(detail=False, methods=["put"], url_path="batch-update") - def batch_update_fines(self, request, *args, **kwargs): + def batch_update_fines(self, request, *_args, **_kwargs): """Update a batch of fines at once""" assert request.data["data"] fines = self.get_queryset().filter(id__in=request.data["fine_ids"]) @@ -140,7 +141,7 @@ def batch_update_fines(self, request, *args, **kwargs): ) @action(detail=False, methods=["put"], url_path=r"batch-update/(?P[^/.]+)") - def batch_update_user_fines(self, request, *args, **kwargs): + def batch_update_user_fines(self, request, *_args, **kwargs): """Update all the fines of a user in a specific group""" fines = self.get_queryset().filter(user__user_id=kwargs["user_id"]) serializer = FineUpdateCreateSerializer( @@ -160,12 +161,12 @@ def batch_update_user_fines(self, request, *args, **kwargs): ) @action(detail=False, methods=["get"], url_path="statistics") - def get_group_fine_statistics(self, request, *args, **kwargs): + def get_group_fine_statistics(self, _request, *_args, **kwargs): group = Group.objects.get(slug=kwargs["slug"]) return Response(FineStatisticsSerializer(group).data, status=status.HTTP_200_OK) @action(detail=True, methods=["put"], url_path="defense") - def update_defense(self, request, *args, **kwargs): + def update_defense(self, request, *_args, **_kwargs): fine = self.get_object() serializer = FineUpdateDefenseSerializer( fine, data=request.data, context={"request": request} diff --git a/app/group/views/group.py b/app/group/views/group.py index 80e24f39..a96f19ce 100644 --- a/app/group/views/group.py +++ b/app/group/views/group.py @@ -80,7 +80,7 @@ def create(self, request, *args, **kwargs): ) @action(detail=True, methods=["get"], url_path="statistics") - def statistics(self, request, *args, **kwargs): + def statistics(self, request, *_args, **_kwargs): group = self.get_object() serializer = GroupStatisticsSerializer(group, context={"request": request}) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/app/group/views/law.py b/app/group/views/law.py index 62786c35..83228bf9 100644 --- a/app/group/views/law.py +++ b/app/group/views/law.py @@ -32,4 +32,4 @@ def create(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): super().destroy(request, *args, **kwargs) - return Response({"detail": ("Loven ble slettet")}, status=status.HTTP_200_OK) + return Response({"detail": "Loven ble slettet"}, status=status.HTTP_200_OK) diff --git a/app/group/views/membership.py b/app/group/views/membership.py index 8b7ff041..1e4356a8 100644 --- a/app/group/views/membership.py +++ b/app/group/views/membership.py @@ -57,9 +57,14 @@ def update(self, request, *args, **kwargs): f"Du er nå leder i {membership.group.name}", UserNotificationSettingType.GROUP_MEMBERSHIP, ).add_paragraph(f"Hei, {membership.user.first_name}!").add_paragraph( - f'Du har blitt gjort til leder i gruppen "{membership.group.name}". Som leder får du tilgang til diverse funksjonalitet på nettsiden. Du kan finne administrasjonspanelene du har tilgang til under "Admin" i din profil.' + f'Du har blitt gjort til leder i gruppen "{membership.group.name}". Som leder får du tilgang til ' + f'diverse funksjonalitet på nettsiden. Du kan finne administrasjonspanelene du har tilgang til ' + f'under "Admin" i din profil.' ).add_paragraph( - f'Som leder har du også fått administratorrettigheter i "{membership.group.name}". Det innebærer at du kan legge til og fjerne medlemmer, endre tidligere medlemskap og administrere gruppens spørreskjemaer. I gruppens innstillinger kan du endre gruppens beskrivelse og logo, samt aktivere botsystemet og velge en botsjef.' + f'Som leder har du også fått administratorrettigheter i "{membership.group.name}". Det innebærer ' + f'at du kan legge til og fjerne medlemmer, endre tidligere medlemskap og administrere gruppens ' + f'spørreskjemaer. I gruppens innstillinger kan du endre gruppens beskrivelse og logo, samt ' + f'aktivere botsystemet og velge en botsjef.' ).add_paragraph( "Gratulerer så mye og lykke til med ledervervet!" ).add_link( @@ -87,7 +92,9 @@ def create(self, request, *args, **kwargs): admin_text = " " if group.type in [GroupType.BOARD, GroupType.SUBGROUP]: - admin_text = f'Som medlem av "{group.name}" har du også fått tilgang til diverse funksjonalitet på nettsiden. Du kan finne administrasjonspanelene du har tilgang til under "Admin" i din profil. ' + admin_text = (f'Som medlem av "{group.name}" har du også fått tilgang til diverse funksjonalitet på ' + f'nettsiden. Du kan finne administrasjonspanelene du har tilgang til under "Admin" ' + f'i din profil. ') Notify( [membership.user], diff --git a/app/payment/models/order.py b/app/payment/models/order.py index dd221467..e82fc55e 100644 --- a/app/payment/models/order.py +++ b/app/payment/models/order.py @@ -75,10 +75,10 @@ def has_list_permission(cls, request): def has_read_all_permission(cls, request): return is_admin_user(request) - def has_object_update_permission(self, request): + def has_object_update_permission(self, _request): return False - def has_object_destroy_permission(self, request): + def has_object_destroy_permission(self, _request): return False def has_object_retrieve_permission(self, request): diff --git a/app/payment/tasks.py b/app/payment/tasks.py index 26e13fab..f41386c2 100644 --- a/app/payment/tasks.py +++ b/app/payment/tasks.py @@ -7,7 +7,7 @@ @app.task(bind=True, base=BaseTask) -def check_if_has_paid(self, event_id, registration_id): +def check_if_has_paid(_self, event_id, registration_id): registration = Registration.objects.filter(registration_id=registration_id).first() event = Event.objects.filter(id=event_id).first() diff --git a/app/payment/util/payment_utils.py b/app/payment/util/payment_utils.py index e7e2f611..b3bd9367 100644 --- a/app/payment/util/payment_utils.py +++ b/app/payment/util/payment_utils.py @@ -26,7 +26,7 @@ def get_new_access_token(): response = response.json() - return (response["expires_on"], response["access_token"]) + return response["expires_on"], response["access_token"] def check_access_token(): diff --git a/app/payment/views/vipps_callback.py b/app/payment/views/vipps_callback.py index 754564d7..f0224ff4 100644 --- a/app/payment/views/vipps_callback.py +++ b/app/payment/views/vipps_callback.py @@ -19,4 +19,4 @@ def force_payment(order_id): res = requests.post(url, headers=headers) status_code = res.status_code json = res.json() - return (json, status_code) + return json, status_code diff --git a/app/util/export_user_data.py b/app/util/export_user_data.py index b8fb2f74..cdfda0fb 100644 --- a/app/util/export_user_data.py +++ b/app/util/export_user_data.py @@ -104,8 +104,6 @@ def export_user_data(request, user): mails = MailGDPRSerializer(user.emails, many=True, context={"request": request}) data["eposter"] = mails.data - is_success = False - with tempfile.NamedTemporaryFile() as tmp: with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as archive: for model, json in data.items(): diff --git a/app/util/migrations.py b/app/util/migrations.py index 99dd1698..087c25ab 100644 --- a/app/util/migrations.py +++ b/app/util/migrations.py @@ -22,7 +22,7 @@ class UpdateContentType(migrations.RunPython): def _update_contenttype_func( self, old_app: str, old_model: str, new_app: str, new_model: str ): - def func(apps, schema_editor): + def func(_apps, _schema_editor): ContentType.objects.filter(app_label=old_app, model=old_model).update( app_label=new_app, model=new_model ) diff --git a/app/util/utils.py b/app/util/utils.py index 4260acac..19908b34 100644 --- a/app/util/utils.py +++ b/app/util/utils.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -def getTimezone(): +def get_timezone(): return pytz_timezone(settings.TIME_ZONE) @@ -18,7 +18,7 @@ def yesterday(): def now(): - return datetime.now(tz=getTimezone()) + return datetime.now(tz=get_timezone()) def tomorrow(): From 2bff09ac34c0a8bd4322108fe9ab2671d998044e Mon Sep 17 00:00:00 2001 From: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:15:01 +0100 Subject: [PATCH 09/16] Add default detail to custom exceptions (#916) Signed-off-by: Tmpecho Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- app/codex/exceptions.py | 8 ++++++-- app/codex/util/event.py | 8 ++------ app/communication/exceptions.py | 12 ++++++------ app/content/exceptions.py | 30 ++++++++++++++++-------------- app/content/models/registration.py | 2 +- app/content/util/event_utils.py | 2 +- app/forms/exceptions.py | 6 +++--- app/forms/models/forms.py | 17 ++++------------- app/group/exceptions.py | 4 ++-- 9 files changed, 41 insertions(+), 48 deletions(-) diff --git a/app/codex/exceptions.py b/app/codex/exceptions.py index fd68fcce..bcb72ba3 100644 --- a/app/codex/exceptions.py +++ b/app/codex/exceptions.py @@ -17,8 +17,12 @@ class APICodexEventEndRegistrationDateBeforeStartRegistrationDate(APIException): class CodexEventEndRegistrationDateAfterStartDate(ValueError): - pass + default_detail = ( + "Sluttdatoen for påmelding kan ikke være etter startdatoen for kurset" + ) class CodexEventEndRegistrationDateBeforeStartRegistrationDate(ValueError): - pass + default_detail = ( + "Sluttdatoen for påmelding kan ikke være før startdatoen for påmelding" + ) diff --git a/app/codex/util/event.py b/app/codex/util/event.py index 00b3979f..2248a1ee 100644 --- a/app/codex/util/event.py +++ b/app/codex/util/event.py @@ -6,11 +6,7 @@ def validate_event_dates(data: dict): if data["end_registration_at"] > data["start_date"]: - raise CodexEventEndRegistrationDateAfterStartDate( - "Påmeldingsslutt kan ikke være etter kursstart" - ) + raise CodexEventEndRegistrationDateAfterStartDate() if data["end_registration_at"] < data["start_registration_at"]: - raise CodexEventEndRegistrationDateBeforeStartRegistrationDate( - "Påmeldingsslutt kan ikke være før påmeldingsstart" - ) + raise CodexEventEndRegistrationDateBeforeStartRegistrationDate() diff --git a/app/communication/exceptions.py b/app/communication/exceptions.py index 26f6b387..7ffc5dea 100644 --- a/app/communication/exceptions.py +++ b/app/communication/exceptions.py @@ -8,17 +8,17 @@ class APIAnotherVisibleBannerException(APIException): default_detail = "Det finnes allerede et banner som er synlig i samme tidsrom" +class AnotherVisibleBannerError(ValidationError): + default_detail = "Det finnes allerede et banner som er synlig i samme tidsrom" + + class APIDatesMixedException(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = "Datoen banneret er synlig til er satt etter datoen banneret for synlig fra. Bytt om disse to" -class AnotherVisibleBannerError(ValidationError): - pass - - class DatesMixedError(ValidationError): - pass + default_detail = "Datoen banneret er synlig til er satt etter datoen banneret for synlig fra. Bytt om disse to" class APIAllChannelsUnselected(APIException): @@ -27,4 +27,4 @@ class APIAllChannelsUnselected(APIException): class AllChannelsUnselected(ValueError): - pass + default_detail = "Du må velge minst en kommunikasjonsmetode" diff --git a/app/content/exceptions.py b/app/content/exceptions.py index 73f07dfe..31ca909d 100644 --- a/app/content/exceptions.py +++ b/app/content/exceptions.py @@ -22,6 +22,10 @@ class APIEventSignOffDeadlineHasPassed(APIException): default_detail = "Du kan ikke melde deg av etter avmeldingsfristen" +class EventSignOffDeadlineHasPassed(ValueError): + default_detail = "Du kan ikke melde deg av etter avmeldingsfristen" + + class APIUnansweredFormException(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = ( @@ -29,34 +33,32 @@ class APIUnansweredFormException(APIException): ) +class UnansweredFormError(ValueError): + default_detail = ( + "Du har ubesvarte evalueringsskjemaer som må besvares før du kan melde deg på" + ) + + class APIHasStrikeException(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = "Kan ikke melde deg på fordi du har en eller flere prikker" -class APIEventIsFullException(APIException): - status_code = status.HTTP_400_BAD_REQUEST - default_detail = "Du kan ikke flytte opp en fra ventelisten når arrangementet er fullt. Flytt en bruker ned først." - - -class EventSignOffDeadlineHasPassed(ValueError): - pass - - class StrikeError(ValueError): - pass + default_detail = "Kan ikke melde deg på fordi du har en eller flere prikker" -class UnansweredFormError(ValueError): - pass +class APIEventIsFullException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Du kan ikke flytte opp en fra ventelisten når arrangementet er fullt. Flytt en bruker ned først." class EventIsFullError(ValueError): - pass + default_detail = "Du kan ikke flytte opp en fra ventelisten når arrangementet er fullt. Flytt en bruker ned først." class RefundFailedError(ValueError): - pass + default_detail = "Tilbakebetaling feilet" class FeideError(ValueError): diff --git a/app/content/models/registration.py b/app/content/models/registration.py index 22006637..20449987 100644 --- a/app/content/models/registration.py +++ b/app/content/models/registration.py @@ -165,7 +165,7 @@ def save(self, *args, **kwargs): and not self.is_on_wait and self in self.event.get_waiting_list() ): - raise EventIsFullError + raise EventIsFullError() self.send_notification_and_mail() return super().save(*args, **kwargs) diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index e611adc9..304431a7 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -76,4 +76,4 @@ def refund_vipps_order(order_id, event, transaction_text): except Exception as refund_error: capture_exception(refund_error) - raise RefundFailedError("Tilbakebetaling feilet") + raise RefundFailedError() diff --git a/app/forms/exceptions.py b/app/forms/exceptions.py index c5ad930d..932613f8 100644 --- a/app/forms/exceptions.py +++ b/app/forms/exceptions.py @@ -8,7 +8,7 @@ class APIDuplicateSubmissionException(APIException): class DuplicateSubmission(ValueError): - pass + default_detail = "Spørreskjemaet tillater kun én innsending" class APIFormNotOpenForSubmissionException(APIException): @@ -17,7 +17,7 @@ class APIFormNotOpenForSubmissionException(APIException): class FormNotOpenForSubmission(ValueError): - pass + default_detail = "Spørreskjemaet er ikke åpent for innsending" class APIGroupFormOnlyForMembersException(APIException): @@ -26,4 +26,4 @@ class APIGroupFormOnlyForMembersException(APIException): class GroupFormOnlyForMembers(ValueError): - pass + default_detail = "Spørreskjemaet er kun åpent for medlemmer av gruppen" diff --git a/app/forms/models/forms.py b/app/forms/models/forms.py index 737fb84e..2385a499 100644 --- a/app/forms/models/forms.py +++ b/app/forms/models/forms.py @@ -112,7 +112,6 @@ def has_object_read_permission(self, request): class EventForm(Form): - event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="forms") type = models.CharField( max_length=40, choices=EventFormType.choices, default=EventFormType.SURVEY @@ -160,7 +159,6 @@ def has_object_read_permission(self, request): class GroupForm(Form): - read_access = [Groups.TIHLDE] email_receiver_on_submit = models.EmailField(max_length=200, null=True, blank=True) group = models.ForeignKey(Group, on_delete=models.CASCADE, related_name="forms") @@ -212,7 +210,6 @@ def has_object_write_permission(self, request): class Field(OrderedModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=400) form = models.ForeignKey(Form, on_delete=models.CASCADE, related_name="fields") @@ -234,7 +231,6 @@ class Meta(OrderedModel.Meta): class Option(OrderedModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=400, default="") field = models.ForeignKey(Field, on_delete=models.CASCADE, related_name="options") @@ -298,7 +294,7 @@ def check_multiple_submissions(self): elif isinstance(self.form, GroupForm): self.check_group_form_can_submit_multiple() else: - raise DuplicateSubmission("Spørreskjemaet tillater kun én innsending") + raise DuplicateSubmission() def check_event_form_has_registration(self): user_has_registration = self.form.event.registrations.filter( @@ -311,21 +307,17 @@ def check_event_form_has_registration(self): def check_group_form_can_submit_multiple(self): if not self.form.can_submit_multiple: - raise DuplicateSubmission("Spørreskjemaet tillater kun én innsending") + raise DuplicateSubmission() def check_group_form_open_for_submissions(self): if not self.form.is_open_for_submissions: - raise FormNotOpenForSubmission( - "Spørreskjemaet er ikke åpent for innsending" - ) + raise FormNotOpenForSubmission() def check_group_form_only_for_members(self): if self.form.only_for_group_members and not self.user.is_member_of( self.form.group ): - raise GroupFormOnlyForMembers( - "Spørreskjemaet er kun åpent for medlemmer av gruppen" - ) + raise GroupFormOnlyForMembers() @classmethod def _get_form_from_request(cls, request): @@ -388,7 +380,6 @@ def has_download_permission(cls, request): class Answer(BaseModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) submission = models.ForeignKey( Submission, on_delete=models.CASCADE, related_name="answers" diff --git a/app/group/exceptions.py b/app/group/exceptions.py index c0ca1867..6c1c8a80 100644 --- a/app/group/exceptions.py +++ b/app/group/exceptions.py @@ -8,7 +8,7 @@ class APIUserIsNotInGroupException(APIException): class UserIsNotInGroup(ValidationError): - pass + default_detail = "En av brukerne er ikke medlem av gruppen" class APIGroupTypeNotInPublicGroupsException(APIException): @@ -17,4 +17,4 @@ class APIGroupTypeNotInPublicGroupsException(APIException): class GroupTypeNotInPublicGroups(ValueError): - pass + default_detail = "Ikke gylde gruppetype" From ca5eb3af16e12ab8fc9679be4fb5377cfb4f0025 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:04:21 +0100 Subject: [PATCH 10/16] chore(deps): bump uvicorn from 0.30.6 to 0.32.0 (#908) Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.30.6 to 0.32.0. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.30.6...0.32.0) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a2c8be27..20aa30f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ celery == 5.4.0 azure-storage-blob == 12.23.1 python-dotenv ~= 1.0.1 gunicorn == 23.0.0 -uvicorn == 0.30.6 +uvicorn == 0.32.0 whitenoise == 6.7.0 django-ical == 1.9.2 slack-sdk == 3.33.1 From 95965cbfb1d61a8b74730307b40ca16ac995963c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:08:27 +0100 Subject: [PATCH 11/16] chore(deps): Bump pre-commit from 3.8.0 to 4.0.1 (#899) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.8.0 to 4.0.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.8.0...v4.0.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 20aa30f0..7f1d547f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -44,7 +44,7 @@ isort flake8 flake8-django flake8-black -pre-commit == 3.8.0 +pre-commit == 4.0.1 # Testing # ------------------------------------------------------------------------------ From b5670f00c8326e225ae7a453780f715844b72471 Mon Sep 17 00:00:00 2001 From: Yazan Zarka Date: Tue, 29 Oct 2024 15:37:06 +0100 Subject: [PATCH 12/16] Feat(registration)/filter participants (#917) Fixed bugs introduced in an earlier pr --------- Co-authored-by: Harry Linrui XU Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Co-authored-by: haruixu <114171733+haruixu@users.noreply.github.com> --- app/career/views/weekly_business.py | 4 ++-- app/content/serializers/event.py | 6 +++--- app/content/views/upload.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/career/views/weekly_business.py b/app/career/views/weekly_business.py index 657c3af0..80baeffc 100644 --- a/app/career/views/weekly_business.py +++ b/app/career/views/weekly_business.py @@ -59,7 +59,7 @@ def create(self, request, *args, **kwargs): except ValueError: return Response( {"detail": "En feil oppstod under behandlingen av forespørselen."}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) def update(self, request, pk): @@ -81,7 +81,7 @@ def update(self, request, pk): except ValueError: return Response( {"detail": "En feil oppstod under behandlingen av forespørselen."}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) def destroy(self, request, *args, **kwargs): diff --git a/app/content/serializers/event.py b/app/content/serializers/event.py index c97704df..66182916 100644 --- a/app/content/serializers/event.py +++ b/app/content/serializers/event.py @@ -1,4 +1,3 @@ -from django.db.models import Q from rest_framework import serializers from dry_rest_permissions.generics import DRYPermissionsField @@ -326,6 +325,7 @@ def get_allow_photo_count(self, obj, *args, **kwargs): def get_has_not_paid_count(self, obj, *args, **kwargs): if obj.is_paid_event: - orders = obj.orders.filter(~Q(status=OrderStatus.SALE), event=obj).count() - return orders + registrations = obj.registrations.filter(is_on_wait=False).count() + orders = obj.orders.filter(status=OrderStatus.SALE, event=obj).count() + return registrations - orders return 0 diff --git a/app/content/views/upload.py b/app/content/views/upload.py index b9816e43..d5c9d8b2 100644 --- a/app/content/views/upload.py +++ b/app/content/views/upload.py @@ -35,7 +35,7 @@ def upload(request): except ValueError: return Response( {"detail": "En feil oppstod under behandlingen av forespørselen."}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) @@ -56,5 +56,5 @@ def delete(_request, container_name, blob_name): except ValueError: return Response( {"detail": "En feil oppstod under behandlingen av forespørselen."}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) From 246ad2a1d589f4994171ad887b7fa67ec24bdbdf Mon Sep 17 00:00:00 2001 From: Emil Johnsen <111747340+EmilJohns1@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:08:21 +0100 Subject: [PATCH 13/16] Upload files (#906) * Create files app to manage user uploaded files Signed-off-by: Tmpecho * created serializers for file and gallery * Move file upload endpoint from content/ to files/, create file and gallery views, add file and gallery model methods Signed-off-by: Tmpecho * updating progress, needed to rename 'Gallery' to 'UserGallery' as a Gallery model already exists * finished mvp for file project * updating progress * mvp for file uploading/deletion * forgot changelog, oopsie * fixed error in permissions from allowing non-admins to delete files, fixed 'security threat' * Minor fixes to file serializer Signed-off-by: Tmpecho * Fix whitespace Signed-off-by: Tmpecho * Add custom exeption with mixins for files class Signed-off-by: Tmpecho * Update custom file exceptions, set max gallery size as global constant, throw custom exceptions Signed-off-by: Tmpecho * create test for creating file when not having a gallery, format * removed duplicate method * small refactoring and removed 'url' from file model, changed it to 'OptionalFile' * fix lint --------- Signed-off-by: Tmpecho Co-authored-by: Tmpecho Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Co-authored-by: Johannes Aamot-Skeidsvoll <82368148+Tmpecho@users.noreply.github.com> --- CHANGELOG.md | 3 + app/common/serializers.py | 2 + app/communication/models/mail.py | 6 +- app/constants.py | 2 + app/content/urls.py | 4 - app/content/util/event_utils.py | 2 +- app/content/views/__init__.py | 1 - app/content/views/accept_form.py | 8 +- app/content/views/user_bio.py | 4 +- app/content/views/user_calendar_events.py | 2 +- app/files/__init__.py | 0 app/files/admin.py | 6 + app/files/app.py | 5 + app/files/enums.py | 0 app/files/exceptions.py | 22 ++ app/files/factories/__init__.py | 0 app/files/filters/__init__.py | 0 app/files/migrations/0001_initial.py | 68 +++++ .../0002_remove_file_url_file_file.py | 22 ++ app/files/migrations/__init__.py | 0 app/files/mixins.py | 17 ++ app/files/models/__init__.py | 2 + app/files/models/file.py | 68 +++++ app/files/models/user_gallery.py | 77 +++++ app/files/serializers/__init__.py | 6 + app/files/serializers/file.py | 68 +++++ app/files/serializers/user_gallery.py | 22 ++ app/files/tasks/__init__.py | 0 app/files/tests/__init__.py | 0 app/files/urls.py | 17 ++ app/files/util/__init__.py | 0 app/files/views/__init__.py | 2 + app/files/views/file.py | 56 ++++ app/{content => files}/views/upload.py | 0 app/files/views/user_gallery.py | 41 +++ app/group/views/group.py | 3 +- app/group/views/membership.py | 16 +- app/settings.py | 1 + app/tests/conftest.py | 13 + app/tests/files/__init__.py | 0 app/tests/files/test_file_integration.py | 272 ++++++++++++++++++ .../files/test_user_gallery_integration.py | 52 ++++ app/urls.py | 1 + app/util/models.py | 9 + app/util/test_utils.py | 33 +++ 45 files changed, 909 insertions(+), 24 deletions(-) create mode 100644 app/files/__init__.py create mode 100644 app/files/admin.py create mode 100644 app/files/app.py create mode 100644 app/files/enums.py create mode 100644 app/files/exceptions.py create mode 100644 app/files/factories/__init__.py create mode 100644 app/files/filters/__init__.py create mode 100644 app/files/migrations/0001_initial.py create mode 100644 app/files/migrations/0002_remove_file_url_file_file.py create mode 100644 app/files/migrations/__init__.py create mode 100644 app/files/mixins.py create mode 100644 app/files/models/__init__.py create mode 100644 app/files/models/file.py create mode 100644 app/files/models/user_gallery.py create mode 100644 app/files/serializers/__init__.py create mode 100644 app/files/serializers/file.py create mode 100644 app/files/serializers/user_gallery.py create mode 100644 app/files/tasks/__init__.py create mode 100644 app/files/tests/__init__.py create mode 100644 app/files/urls.py create mode 100644 app/files/util/__init__.py create mode 100644 app/files/views/__init__.py create mode 100644 app/files/views/file.py rename app/{content => files}/views/upload.py (100%) create mode 100644 app/files/views/user_gallery.py create mode 100644 app/tests/files/__init__.py create mode 100644 app/tests/files/test_file_integration.py create mode 100644 app/tests/files/test_user_gallery_integration.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb3d57b..83fb1c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ - 🎨**Overordnet**. Endret variabel og funksjonsnavn til å følge konvensjoner og andre små endringer. - ✨ **Filtrering**. Admin kan nå filtere deltakere av et arrangement på studie, studieår, om deltakere har allergier, (om deltakere godtar å bli tatt bilde av, om deltakere har ankommet), i tillegg til søk på fornavn og etternavn. +- ✨ **Filopplasting**. Det er nå mulig for admin brukere å laste opp- og slette filer. +- ✨ **Mail endepunkt**. Det er nå laget et endepunkt for å sende mailer. + ## Versjon 2024.10.11 - ✨ **Tilbakemelding-funksjon**. Man kan nå opprette tilbakemeldinger for bugs og idé. - 🦟 **Påmelding**. Det vil nå ikke være mulig med flere påmeldinger på et arrangement enn maksgrensen. diff --git a/app/common/serializers.py b/app/common/serializers.py index 8f8a386e..f91f26e6 100644 --- a/app/common/serializers.py +++ b/app/common/serializers.py @@ -7,4 +7,6 @@ class BaseModelSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): if hasattr(instance, "image") and "image" in validated_data: replace_file(instance.image, validated_data.get("image", None)) + if hasattr(instance, "file") and "file" in validated_data: + replace_file(instance.file, validated_data.get("file", None)) return super().update(instance, validated_data) diff --git a/app/communication/models/mail.py b/app/communication/models/mail.py index 692fdd00..6a4e13ea 100644 --- a/app/communication/models/mail.py +++ b/app/communication/models/mail.py @@ -40,5 +40,7 @@ def send(self, connection): return is_success def __str__(self): - return (f"\"{self.subject}\", to {self.users.all()[0] if self.users.count() == 1 else f'{self.users.count()} users'}, " - f"{'sent' if self.sent else 'eta'} {self.eta}") + return ( + f"\"{self.subject}\", to {self.users.all()[0] if self.users.count() == 1 else f'{self.users.count()} users'}, " + f"{'sent' if self.sent else 'eta'} {self.eta}" + ) diff --git a/app/constants.py b/app/constants.py index 638b4d67..773570e1 100644 --- a/app/constants.py +++ b/app/constants.py @@ -20,4 +20,6 @@ SLACK_BEDPRES_OG_KURS_CHANNEL_ID = "C01DCSJ8X2Q" SLACK_ARRANGEMENTER_CHANNEL_ID = "C01LFEFJFV3" +MAX_GALLERY_SIZE = 50 + # TODO: Create api-urls as constants which then can be used in for example tests and urls.py files diff --git a/app/content/urls.py b/app/content/urls.py index 26911fd6..536f8fb7 100644 --- a/app/content/urls.py +++ b/app/content/urls.py @@ -18,10 +18,8 @@ UserCalendarEvents, UserViewSet, accept_form, - delete, register_with_feide, send_email, - upload, ) router = routers.DefaultRouter() @@ -53,9 +51,7 @@ urlpatterns = [ re_path(r"", include(router.urls)), path("accept-form/", accept_form), - path("upload/", upload), path("send-email/", send_email), - path("delete-file///", delete), path("feide/", register_with_feide), re_path(r"users/(?P[^/.]+)/events.ics", UserCalendarEvents()), ] diff --git a/app/content/util/event_utils.py b/app/content/util/event_utils.py index 304431a7..ad423954 100644 --- a/app/content/util/event_utils.py +++ b/app/content/util/event_utils.py @@ -3,9 +3,9 @@ from app.content.exceptions import RefundFailedError from app.payment.tasks import check_if_has_paid from app.payment.util.payment_utils import ( + check_access_token, initiate_payment, refund_payment, - check_access_token ) diff --git a/app/content/views/__init__.py b/app/content/views/__init__.py index 5a9a5c3f..8cdc2bdf 100644 --- a/app/content/views/__init__.py +++ b/app/content/views/__init__.py @@ -8,7 +8,6 @@ from app.content.views.news import NewsViewSet from app.content.views.page import PageViewSet from app.content.views.short_link import ShortLinkViewSet -from app.content.views.upload import upload, delete from app.content.views.strike import StrikeViewSet from app.content.views.toddel import ToddelViewSet from app.content.views.qr_code import QRCodeViewSet diff --git a/app/content/views/accept_form.py b/app/content/views/accept_form.py index 44e4fe6f..545e1d35 100644 --- a/app/content/views/accept_form.py +++ b/app/content/views/accept_form.py @@ -23,9 +23,11 @@ def accept_form(request): if len(types) == 1 and types[0].lower() == "annonse" else MAIL_NOK_LEADER ) - title = (f"{body['info']['bedrift']} vil ha {', '.join(types[:-1])}" - f"{' og ' if len(types) > 1 else ''}{', '.join(types[-1:])}, " - f"{', '.join(times[:-1])}{' og ' if len(times) > 1 else ''}{', '.join(times[-1:])}") + title = ( + f"{body['info']['bedrift']} vil ha {', '.join(types[:-1])}" + f"{' og ' if len(types) > 1 else ''}{', '.join(types[-1:])}, " + f"{', '.join(times[:-1])}{' og ' if len(times) > 1 else ''}{', '.join(times[-1:])}" + ) is_success = send_html_email( to_mails=[to_mail], diff --git a/app/content/views/user_bio.py b/app/content/views/user_bio.py index 5eeb1081..b4c0902d 100644 --- a/app/content/views/user_bio.py +++ b/app/content/views/user_bio.py @@ -49,6 +49,4 @@ def update(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): super().destroy(request, *args, **kwargs) - return Response( - {"detail": "Brukerbio ble slettet"}, status=status.HTTP_200_OK - ) + return Response({"detail": "Brukerbio ble slettet"}, status=status.HTTP_200_OK) diff --git a/app/content/views/user_calendar_events.py b/app/content/views/user_calendar_events.py index 3c26c72f..59e5d499 100644 --- a/app/content/views/user_calendar_events.py +++ b/app/content/views/user_calendar_events.py @@ -23,7 +23,7 @@ def __call__(self, request, *args, **kwargs): return JsonResponse( { "detail": "Denne brukeren har skrudd av offentlig deling av påmeldinger til arrangementer. " - "Du kan derfor ikke hente ut brukerens arrangementer som .ics-fil" + "Du kan derfor ikke hente ut brukerens arrangementer som .ics-fil" }, status=status.HTTP_403_FORBIDDEN, ) diff --git a/app/files/__init__.py b/app/files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/admin.py b/app/files/admin.py new file mode 100644 index 00000000..b4aaec23 --- /dev/null +++ b/app/files/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from app.files import models + +admin.site.register(models.UserGallery) +admin.site.register(models.File) diff --git a/app/files/app.py b/app/files/app.py new file mode 100644 index 00000000..2449379c --- /dev/null +++ b/app/files/app.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FilesConfig(AppConfig): + name = "app.files" diff --git a/app/files/enums.py b/app/files/enums.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/exceptions.py b/app/files/exceptions.py new file mode 100644 index 00000000..83490cc2 --- /dev/null +++ b/app/files/exceptions.py @@ -0,0 +1,22 @@ +from rest_framework import status +from rest_framework.exceptions import APIException, ValidationError + +from app.constants import MAX_GALLERY_SIZE + + +class APINoGalleryFoundForUser(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Ingen galleri ble funnet for brukeren." + + +class NoGalleryFoundForUser(ValidationError): + default_detail = "Ingen galleri ble funnet for brukeren." + + +class APIGalleryIsFull(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = f"Galleriet er fullt med {MAX_GALLERY_SIZE} filer." + + +class GalleryIsFull(ValidationError): + default_detail = f"Galleriet er fullt med {MAX_GALLERY_SIZE} filer." diff --git a/app/files/factories/__init__.py b/app/files/factories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/filters/__init__.py b/app/files/filters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/migrations/0001_initial.py b/app/files/migrations/0001_initial.py new file mode 100644 index 00000000..60e432dc --- /dev/null +++ b/app/files/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.16 on 2024-10-07 16:45 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="UserGallery", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "author", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="user_galleries", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="File", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=80)), + ("url", models.URLField()), + ("description", models.TextField(blank=True)), + ( + "gallery", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="files", + to="files.usergallery", + ), + ), + ], + ), + ] diff --git a/app/files/migrations/0002_remove_file_url_file_file.py b/app/files/migrations/0002_remove_file_url_file_file.py new file mode 100644 index 00000000..5f5ac0f3 --- /dev/null +++ b/app/files/migrations/0002_remove_file_url_file_file.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.1 on 2024-10-31 15:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="file", + name="url", + ), + migrations.AddField( + model_name="file", + name="file", + field=models.URLField(blank=True, max_length=600, null=True), + ), + ] diff --git a/app/files/migrations/__init__.py b/app/files/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/mixins.py b/app/files/mixins.py new file mode 100644 index 00000000..1e36f0e4 --- /dev/null +++ b/app/files/mixins.py @@ -0,0 +1,17 @@ +from app.files.exceptions import ( + APIGalleryIsFull, + APINoGalleryFoundForUser, + GalleryIsFull, + NoGalleryFoundForUser, +) +from app.util.mixins import APIErrorsMixin + + +class FileErrorMixin(APIErrorsMixin): + @property + def expected_exceptions(self): + return { + **super().expected_exceptions, + NoGalleryFoundForUser: APINoGalleryFoundForUser, + GalleryIsFull: APIGalleryIsFull, + } diff --git a/app/files/models/__init__.py b/app/files/models/__init__.py new file mode 100644 index 00000000..8952713d --- /dev/null +++ b/app/files/models/__init__.py @@ -0,0 +1,2 @@ +from app.files.models.user_gallery import UserGallery +from app.files.models.file import File diff --git a/app/files/models/file.py b/app/files/models/file.py new file mode 100644 index 00000000..8f813bf2 --- /dev/null +++ b/app/files/models/file.py @@ -0,0 +1,68 @@ +from django.db import models +from django.db.models import PROTECT + +from app.common.enums import AdminGroup, Groups +from app.common.permissions import BasePermissionModel, check_has_access +from app.files.models.user_gallery import UserGallery +from app.util.models import BaseModel, OptionalFile + + +class File(BaseModel, BasePermissionModel, OptionalFile): + read_access = AdminGroup.admin() + write_access = AdminGroup.admin() + + title = models.CharField(max_length=80) + + description = models.TextField(blank=True) + gallery = models.ForeignKey( + UserGallery, on_delete=PROTECT, related_name="files", blank=False + ) + + class Meta: + pass + + def __str__(self): + return self.title + + @classmethod + def has_read_permission(cls, request): + return super().has_read_permission(request) + + @classmethod + def has_write_permission(cls, request): + return check_has_access(Groups.TIHLDE, request) + + @classmethod + def has_retrieve_permission(cls, request): + return cls.has_read_permission(request) + + @classmethod + def has_create_permission(cls, request): + return check_has_access(cls.write_access, request) + + @classmethod + def has_update_permission(cls, request): + return check_has_access(cls.write_access, request) + + @classmethod + def has_destroy_permission(cls, request): + return check_has_access(Groups.TIHLDE, request) + + @classmethod + def has_list_permission(cls, request): + return cls.has_read_permission(request) + + def has_object_read_permission(self, request): + return self.has_read_permission(request) + + def has_object_write_permission(self, request): + return self.has_write_permission(request) + + def has_object_retrieve_permission(self, request): + return self.has_object_read_permission(request) + + def has_object_update_permission(self, request): + return self.gallery.author == request.user + + def has_object_destroy_permission(self, request): + return self.gallery.author == request.user diff --git a/app/files/models/user_gallery.py b/app/files/models/user_gallery.py new file mode 100644 index 00000000..238cc4c7 --- /dev/null +++ b/app/files/models/user_gallery.py @@ -0,0 +1,77 @@ +from django.db import models +from django.db.models import PROTECT + +from app.common.enums import AdminGroup +from app.common.permissions import BasePermissionModel +from app.content.models.user import User +from app.util.models import BaseModel + + +class UserGallery(BaseModel, BasePermissionModel): + read_access = AdminGroup.admin() + write_access = AdminGroup.admin() + + author = models.OneToOneField( + User, on_delete=PROTECT, related_name="user_galleries" + ) + + class Meta: + pass + + def __str__(self): + return f"Gallery by {self.author.first_name} {self.author.last_name}" + + @classmethod + def has_read_permission(cls, request): + return super().has_read_permission(request) + + @classmethod + def has_write_permission(cls, request): + return super().has_write_permission(request) + + @classmethod + def has_retrieve_permission(cls, request): + return cls.has_read_permission(request) + + @classmethod + def has_create_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_update_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_destroy_permission(cls, request): + return cls.has_write_permission(request) + + @classmethod + def has_list_permission(cls, request): + return cls.has_read_permission(request) + + def has_object_read_permission(self, request): + return self.has_read_permission(request) + + def has_object_write_permission(self, request): + return self.has_write_permission(request) + + def has_object_retrieve_permission(self, request): + return self.has_object_read_permission(request) + + def has_object_update_permission(self, request): + return self.author == request.user + + def has_object_destroy_permission(self, request): + return self.author == request.user + + @classmethod + def get_all_files(cls, user): + return cls.objects.get(author=user).files.all() + + @classmethod + def has_gallery(cls, user): + return cls.objects.filter(author=user).exists() + + @classmethod + def create_gallery(cls, user): + return cls.objects.create(author=user) diff --git a/app/files/serializers/__init__.py b/app/files/serializers/__init__.py new file mode 100644 index 00000000..e44eb26c --- /dev/null +++ b/app/files/serializers/__init__.py @@ -0,0 +1,6 @@ +from app.files.serializers.user_gallery import UserGallerySerializer +from app.files.serializers.file import ( + FileSerializer, + CreateFileSerializer, + DeleteFileSerializer, +) diff --git a/app/files/serializers/file.py b/app/files/serializers/file.py new file mode 100644 index 00000000..d12dc675 --- /dev/null +++ b/app/files/serializers/file.py @@ -0,0 +1,68 @@ +from app.common.azure_file_handler import AzureFileHandler +from app.common.serializers import BaseModelSerializer +from app.constants import MAX_GALLERY_SIZE +from app.files.exceptions import GalleryIsFull, NoGalleryFoundForUser +from app.files.models import File +from app.files.models.user_gallery import UserGallery + + +class FileSerializer(BaseModelSerializer): + class Meta: + model = File + fields = ( + "id", + "title", + "description", + "file", + "created_at", + "updated_at", + ) + + +class CreateFileSerializer(BaseModelSerializer): + class Meta: + model = File + fields = ( + "title", + "description", + "file", + ) + + def create(self, validated_data): + user = self.context["request"].user + + gallery = UserGallery.objects.filter(author=user).first() + + if not gallery: + raise NoGalleryFoundForUser() + + if gallery.files.count() >= MAX_GALLERY_SIZE: + raise GalleryIsFull() + + validated_data["gallery"] = gallery + + file_instance = super().create(validated_data) + + return file_instance + + +class UpdateFileSerializer(BaseModelSerializer): + + class Meta: + model = File + fields = ( + "title", + "description", + "file", + ) + + +class DeleteFileSerializer(BaseModelSerializer): + class Meta: + model = File + + def delete(self, instance): + azure_handler = AzureFileHandler(url=instance.url) + azure_handler.deleteBlob() + + return super().delete(instance) diff --git a/app/files/serializers/user_gallery.py b/app/files/serializers/user_gallery.py new file mode 100644 index 00000000..8dd6f64a --- /dev/null +++ b/app/files/serializers/user_gallery.py @@ -0,0 +1,22 @@ +from app.common.serializers import BaseModelSerializer +from app.content.serializers.user import DefaultUserSerializer +from app.files.models.user_gallery import UserGallery + + +class UserGallerySerializer(BaseModelSerializer): + author = DefaultUserSerializer(read_only=True) + + class Meta: + model = UserGallery + fields = ( + "id", + "created_at", + "updated_at", + "author", + ) + + def create(self, validated_data): + user = self.context["request"].user + validated_data["author"] = user + + return super().create(validated_data) diff --git a/app/files/tasks/__init__.py b/app/files/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/tests/__init__.py b/app/files/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/urls.py b/app/files/urls.py new file mode 100644 index 00000000..02655adc --- /dev/null +++ b/app/files/urls.py @@ -0,0 +1,17 @@ +from django.urls import include, path +from rest_framework import routers + +from app.files.views.file import FileViewSet +from app.files.views.upload import delete, upload +from app.files.views.user_gallery import UserGalleryViewSet + +router = routers.DefaultRouter() + +router.register("file", FileViewSet, basename="file") +router.register("user_gallery", UserGalleryViewSet, basename="user_gallery") + +urlpatterns = [ + path("upload/", upload), + path("delete-file///", delete), + path("files/", include(router.urls)), +] diff --git a/app/files/util/__init__.py b/app/files/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/files/views/__init__.py b/app/files/views/__init__.py new file mode 100644 index 00000000..3a2ba8b4 --- /dev/null +++ b/app/files/views/__init__.py @@ -0,0 +1,2 @@ +from app.files.views.user_gallery import UserGalleryViewSet +from app.files.views.file import FileViewSet diff --git a/app/files/views/file.py b/app/files/views/file.py new file mode 100644 index 00000000..d99070bf --- /dev/null +++ b/app/files/views/file.py @@ -0,0 +1,56 @@ +from rest_framework import status +from rest_framework.response import Response + +from app.common.permissions import BasicViewPermission +from app.common.viewsets import BaseViewSet +from app.files.mixins import FileErrorMixin +from app.files.models.file import File +from app.files.serializers.file import ( + CreateFileSerializer, + FileSerializer, + UpdateFileSerializer, +) + + +class FileViewSet(FileErrorMixin, BaseViewSet): + serializer_class = FileSerializer + permission_classes = [BasicViewPermission] + queryset = File.objects.select_related("gallery").all() + + def retrieve(self, request, *_args, **_kwargs): + """Retrieves a specific file by id""" + file = self.get_object() + serializer = FileSerializer(file, context={"request": request}, many=False) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + def update(self, request, *_args, **_kwargs): + """Updates a specific file by id""" + file = self.get_object() + + serializer = UpdateFileSerializer( + file, data=request.data, partial=True, context={"request": request} + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def create(self, request, *args, **kwargs): + """Creates a file""" + serializer = CreateFileSerializer( + data=request.data, context={"request": request} + ) + if serializer.is_valid(): + file = super().perform_create(serializer) + return_serializer = CreateFileSerializer(file) + return Response(data=return_serializer.data, status=status.HTTP_201_CREATED) + return Response( + {"detail": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def delete(self, request, *_args, **_kwargs): + """Deletes a specific file by id""" + file = self.get_object() + file.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/app/content/views/upload.py b/app/files/views/upload.py similarity index 100% rename from app/content/views/upload.py rename to app/files/views/upload.py diff --git a/app/files/views/user_gallery.py b/app/files/views/user_gallery.py new file mode 100644 index 00000000..c362dcc8 --- /dev/null +++ b/app/files/views/user_gallery.py @@ -0,0 +1,41 @@ +from rest_framework import status +from rest_framework.response import Response + +from app.common.permissions import BasicViewPermission +from app.common.viewsets import BaseViewSet +from app.files.models.user_gallery import UserGallery +from app.files.serializers.user_gallery import UserGallerySerializer + + +class UserGalleryViewSet(BaseViewSet): + serializer_class = UserGallerySerializer + permission_classes = [BasicViewPermission] + queryset = UserGallery.objects.all() + + def retrieve(self, request, *_args, **_kwargs): + """Retrieve all files in gallery""" + try: + files = UserGallery.get_all_files(request.user) + serializer = UserGallerySerializer(files, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except UserGallery.DoesNotExist: + return Response( + {"detail": "Galleriet finnes ikke"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def create(self, request, *args, **kwargs): + """Create a gallery for the current user""" + serializer = UserGallerySerializer( + data=request.data, context={"request": request} + ) + + if serializer.is_valid(): + user_gallery = super().perform_create(serializer) + return_serializer = UserGallerySerializer(user_gallery) + return Response(return_serializer.data, status=status.HTTP_201_CREATED) + + return Response( + {"detail": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/app/group/views/group.py b/app/group/views/group.py index a96f19ce..3533e33c 100644 --- a/app/group/views/group.py +++ b/app/group/views/group.py @@ -2,7 +2,6 @@ from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework.status import HTTP_201_CREATED from app.common.mixins import ActionMixin from app.common.permissions import BasicViewPermission @@ -73,7 +72,7 @@ def create(self, request, *args, **kwargs): if serializer.is_valid(): group = super().perform_create(serializer) return_serializer = SimpleGroupSerializer(group) - return Response(data=return_serializer.data, status=HTTP_201_CREATED) + return Response(data=return_serializer.data, status=status.HTTP_201_CREATED) return Response( {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST, diff --git a/app/group/views/membership.py b/app/group/views/membership.py index 1e4356a8..f98db0ab 100644 --- a/app/group/views/membership.py +++ b/app/group/views/membership.py @@ -58,13 +58,13 @@ def update(self, request, *args, **kwargs): UserNotificationSettingType.GROUP_MEMBERSHIP, ).add_paragraph(f"Hei, {membership.user.first_name}!").add_paragraph( f'Du har blitt gjort til leder i gruppen "{membership.group.name}". Som leder får du tilgang til ' - f'diverse funksjonalitet på nettsiden. Du kan finne administrasjonspanelene du har tilgang til ' + f"diverse funksjonalitet på nettsiden. Du kan finne administrasjonspanelene du har tilgang til " f'under "Admin" i din profil.' ).add_paragraph( f'Som leder har du også fått administratorrettigheter i "{membership.group.name}". Det innebærer ' - f'at du kan legge til og fjerne medlemmer, endre tidligere medlemskap og administrere gruppens ' - f'spørreskjemaer. I gruppens innstillinger kan du endre gruppens beskrivelse og logo, samt ' - f'aktivere botsystemet og velge en botsjef.' + f"at du kan legge til og fjerne medlemmer, endre tidligere medlemskap og administrere gruppens " + f"spørreskjemaer. I gruppens innstillinger kan du endre gruppens beskrivelse og logo, samt " + f"aktivere botsystemet og velge en botsjef." ).add_paragraph( "Gratulerer så mye og lykke til med ledervervet!" ).add_link( @@ -92,9 +92,11 @@ def create(self, request, *args, **kwargs): admin_text = " " if group.type in [GroupType.BOARD, GroupType.SUBGROUP]: - admin_text = (f'Som medlem av "{group.name}" har du også fått tilgang til diverse funksjonalitet på ' - f'nettsiden. Du kan finne administrasjonspanelene du har tilgang til under "Admin" ' - f'i din profil. ') + admin_text = ( + f'Som medlem av "{group.name}" har du også fått tilgang til diverse funksjonalitet på ' + f'nettsiden. Du kan finne administrasjonspanelene du har tilgang til under "Admin" ' + f"i din profil. " + ) Notify( [membership.user], diff --git a/app/settings.py b/app/settings.py index 26e8c1fe..f9b3bcf6 100644 --- a/app/settings.py +++ b/app/settings.py @@ -108,6 +108,7 @@ "app.emoji", "app.feedback", "app.codex", + "app.files", ] # Django rest framework diff --git a/app/tests/conftest.py b/app/tests/conftest.py index fbdccddf..eb48220e 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -33,6 +33,7 @@ NewsReactionFactory, ) from app.feedback.factories import BugFactory, IdeaFactory +from app.files.models.user_gallery import UserGallery from app.forms.tests.form_factories import FormFactory, SubmissionFactory from app.group.factories import GroupFactory, MembershipFactory from app.group.factories.fine_factory import FineFactory @@ -314,6 +315,18 @@ def codex_event_registration(): return CodexEventRegistrationFactory() +@pytest.fixture +def user_gallery(member): + """Creates a gallery for the member.""" + return UserGallery.objects.create(author=member) + + +@pytest.fixture +def admin_gallery(admin_user): + """Creates a gallery for the admin user.""" + return UserGallery.objects.create(author=admin_user) + + @pytest.fixture() def new_admin_user(): admin = UserFactory() diff --git a/app/tests/files/__init__.py b/app/tests/files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/tests/files/test_file_integration.py b/app/tests/files/test_file_integration.py new file mode 100644 index 00000000..20660c7b --- /dev/null +++ b/app/tests/files/test_file_integration.py @@ -0,0 +1,272 @@ +from rest_framework import status + +import pytest + +from app.common.enums import AdminGroup, Groups +from app.constants import MAX_GALLERY_SIZE +from app.files.models.file import File +from app.files.models.user_gallery import UserGallery +from app.util.test_utils import ( + add_user_to_group_with_name, + get_api_client, + remove_user_from_group_with_name, +) + +FILE_URL = "/files/file/" + + +def _get_file_url(file=None): + return f"{FILE_URL}{file.id}/" if file else f"{FILE_URL}" + + +def _get_file_post_data(): + return { + "title": "Sample File", + "url": "https://example.com/file.pdf", + "description": "This is a sample file.", + } + + +def _get_file_put_data(file): + return {"title": file.title, "description": "Updated description."} + + +def _create_file(user, gallery=None): + """Helper function to create a file in the database.""" + if gallery is None: + gallery, _ = UserGallery.objects.get_or_create(author=user) + + return File.objects.create( + title="Sample File", + description="This is a sample file.", + gallery=gallery, + ) + + +@pytest.mark.django_db +def test_list_files_as_anonymous_user(default_client): + """Tests if an anonymous user cannot list files""" + url = _get_file_url() + response = default_client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_list_files_as_member(member): + """Tests if a member can list files""" + client = get_api_client(user=member) + url = _get_file_url() + response = client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_list_files_as_admin(admin_user): + """Tests if an admin user can list files""" + client = get_api_client(user=admin_user) + url = _get_file_url() + response = client.get(url) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_create_file_as_member(member): + """Tests if a member can create a file""" + client = get_api_client(user=member) + url = _get_file_url() + data = _get_file_post_data() + + response = client.post(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_create_file_as_admin(admin_user, admin_gallery): + """Tests if an admin can create a file""" + client = get_api_client(user=admin_user) + url = _get_file_url() + data = _get_file_post_data() + + response = client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.filter(title=data["title"]).exists() + file = File.objects.get(title=data["title"]) + assert file.gallery == admin_gallery + + +@pytest.mark.django_db +def test_create_file_as_admin_no_gallery(admin_user): + """Tests if an admin can create a file without a gallery""" + client = get_api_client(user=admin_user) + url = _get_file_url() + data = _get_file_post_data() + + response = client.post(url, data) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_create_file_gallery_full(admin_user, admin_gallery): + """Tests if the admin cannot create a file when gallery is full (>=50 files)""" + for _ in range(MAX_GALLERY_SIZE): + _create_file(admin_user, admin_gallery) + + client = get_api_client(user=admin_user) + url = _get_file_url() + data = _get_file_post_data() + + response = client.post(url, data) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +# noinspection PyTypeChecker +@pytest.mark.django_db +def test_retrieve_file_does_not_exist(admin_user): + """Tests retrieving a non-existent file""" + client = get_api_client(user=admin_user) + url = _get_file_url(file=None) + "999/" + + response = client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + +@pytest.mark.django_db +def test_delete_file_as_admin(admin_user, admin_gallery): + """Tests if an admin user can delete their file""" + file = _create_file(admin_user, admin_gallery) + + client = get_api_client(user=admin_user) + url = _get_file_url(file) + + response = client.delete(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not File.objects.filter(id=file.id).exists() + + +@pytest.mark.django_db +def test_delete_file_as_non_author(member, new_admin_user, admin_gallery): + """Tests if a non-author cannot delete another user's file""" + add_user_to_group_with_name(member, AdminGroup.HS) + + admin_gallery = UserGallery.objects.create(author=new_admin_user) + + file = _create_file(new_admin_user, admin_gallery) + + client = get_api_client(user=member) + url = _get_file_url(file) + + response = client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert File.objects.filter(id=file.id).exists() + + +@pytest.mark.django_db +def test_update_file_as_admin(admin_user, admin_gallery): + """Tests if an admin can update a file without providing a new file.""" + file = _create_file(admin_user, admin_gallery) + client = get_api_client(user=admin_user) + url = _get_file_url(file) + + data = { + "title": "Updated Sample File", + "description": "This is an updated sample file.", + } + + response = client.put(url, data) + + assert response.status_code == status.HTTP_200_OK + file.refresh_from_db() + assert file.title == "Updated Sample File" + assert file.description == "This is an updated sample file." + + +@pytest.mark.django_db +def test_update_file_as_member(member, user_gallery): + """Tests if a member cannot update a file.""" + file = _create_file(member, user_gallery) + client = get_api_client(user=member) + url = _get_file_url(file) + + data = { + "title": "Attempted Update by Member", + } + + response = client.put(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_update_file_invalid_data(admin_user, admin_gallery): + """Tests if an admin cannot update a file with invalid data.""" + file = _create_file(admin_user, admin_gallery) + client = get_api_client(user=admin_user) + url = _get_file_url(file) + + data = { + "title": "", + } + + response = client.put(url, data) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "title" in response.data + + +@pytest.mark.django_db +def test_update_non_existent_file(admin_user): + """Tests if an admin cannot update a non-existent file.""" + client = get_api_client(user=admin_user) + url = _get_file_url() + "999/" + + data = { + "title": "Non-Existent File", + } + + response = client.put(url, data) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + +@pytest.mark.django_db +def test_partial_update_file(admin_user, admin_gallery): + """Tests if an admin can partially update a file.""" + file = _create_file(admin_user, admin_gallery) + client = get_api_client(user=admin_user) + url = _get_file_url(file) + + data = { + "description": "This is a partially updated description.", + } + + response = client.patch(url, data) + + assert response.status_code == status.HTTP_200_OK + file.refresh_from_db() + assert file.description == "This is a partially updated description." + + +@pytest.mark.django_db +def test_create_file_as_admin_and_delete_as_tihlde_user(admin_user, admin_gallery): + """Tests if an admin can create a file and delete it after being removed as an admin.""" + file = _create_file(admin_user, admin_gallery) + url = _get_file_url(file) + + remove_user_from_group_with_name(admin_user, AdminGroup.HS) + add_user_to_group_with_name(admin_user, Groups.TIHLDE) + + client = get_api_client(user=admin_user) + + delete_response = client.delete(url) + + assert delete_response.status_code == status.HTTP_204_NO_CONTENT diff --git a/app/tests/files/test_user_gallery_integration.py b/app/tests/files/test_user_gallery_integration.py new file mode 100644 index 00000000..5b9c1bdf --- /dev/null +++ b/app/tests/files/test_user_gallery_integration.py @@ -0,0 +1,52 @@ +from rest_framework import status + +import pytest + +from app.files.models.user_gallery import UserGallery +from app.util.test_utils import get_api_client + + +def _get_user_gallery_url(user_gallery=None): + return ( + f"/files/user_gallery/{user_gallery.id}/" + if user_gallery + else "/files/user_gallery/" + ) + + +@pytest.mark.django_db +def test_create_user_gallery(admin_user): + """Tests if an admin can create a gallery""" + client = get_api_client(user=admin_user) + url = _get_user_gallery_url() + + assert not UserGallery.has_gallery(admin_user) + + response = client.post(url) + + assert response.status_code == status.HTTP_201_CREATED + assert UserGallery.objects.filter(author=admin_user).exists() + + user_gallery = UserGallery.objects.get(author=admin_user) + assert user_gallery.author == admin_user + + +@pytest.mark.django_db +def test_delete_admin_gallery(admin_user): + """Tests if an admin can delete their gallery""" + client = get_api_client(user=admin_user) + + post_url = _get_user_gallery_url() + response = client.post(post_url) + + assert response.status_code == status.HTTP_201_CREATED + + user_gallery_id = response.data["id"] + user_gallery = UserGallery.objects.get(id=user_gallery_id) + + delete_url = _get_user_gallery_url(user_gallery) + + response = client.delete(delete_url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not UserGallery.objects.filter(id=user_gallery.id).exists() diff --git a/app/urls.py b/app/urls.py index 301f2342..74acacb4 100644 --- a/app/urls.py +++ b/app/urls.py @@ -47,6 +47,7 @@ path("", include("app.content.urls")), path("", include("app.group.urls")), path("", include("app.payment.urls")), + path("", include("app.files.urls")), path("auth/", include("app.authentication.urls")), path("badges/", include("app.badge.urls")), path("forms/", include("app.forms.urls")), diff --git a/app/util/models.py b/app/util/models.py index 65d4589a..4d303477 100644 --- a/app/util/models.py +++ b/app/util/models.py @@ -17,3 +17,12 @@ class OptionalImage(models.Model): class Meta: abstract = True + + +class OptionalFile(models.Model): + """Abstract model for models containing a file""" + + file = models.URLField(max_length=600, null=True, blank=True) + + class Meta: + abstract = True diff --git a/app/util/test_utils.py b/app/util/test_utils.py index 78682ec2..2ab0818f 100644 --- a/app/util/test_utils.py +++ b/app/util/test_utils.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ObjectDoesNotExist from django.db.transaction import atomic from rest_framework.authtoken.models import Token from rest_framework.test import APIClient @@ -24,6 +25,9 @@ def get_api_client(user=None, group_name=None): def add_user_to_group_with_name( user, group_name, group_type=None, membership_type=MembershipType.MEMBER ): + """ + Adds a user to a group with the given name. + """ if not group_type: group_type = get_group_type_from_group_name(group_name) group = Group.objects.get_or_create(name=group_name, type=group_type)[0] @@ -33,6 +37,35 @@ def add_user_to_group_with_name( return group +@atomic +def remove_user_from_group_with_name(user, group_name, group_type=None): + """ + Removes a user from a group with the given name. + """ + if not group_type: + group_type = get_group_type_from_group_name(group_name) + + try: + group = Group.objects.get(name=group_name, type=group_type) + except Group.DoesNotExist: + raise ObjectDoesNotExist( + f"Group with name '{group_name}' and type '{group_type}' does not exist." + ) + + try: + membership = Membership.objects.get(group=group, user=user) + membership.delete() + except Membership.DoesNotExist: + raise ObjectDoesNotExist( + f"User '{user}' is not a member of the group '{group_name}'." + ) + + if not Membership.objects.filter(group=group).exists(): + group.delete() + + return group + + def get_group_type_from_group_name(group_name): if group_name == AdminGroup.HS: return GroupType.BOARD From 0a177b090bc54afc0f6f6e9f0990a38d0389a37a Mon Sep 17 00:00:00 2001 From: LeMiTam <145369879+LeMiTam@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:41:10 +0100 Subject: [PATCH 14/16] Refactor/ Fixed sorting in albums (#910) * Created factory for album * Fixed the order method in albums * Fixed linting * run migrations --------- Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- app/gallery/factories/_init_.py | 0 app/gallery/factories/album_factory.py | 15 +++++++++++++++ .../migrations/0003_alter_album_options.py | 17 +++++++++++++++++ app/gallery/models/album.py | 3 +++ app/tests/gallery/_init_.py | 0 5 files changed, 35 insertions(+) create mode 100644 app/gallery/factories/_init_.py create mode 100644 app/gallery/factories/album_factory.py create mode 100644 app/gallery/migrations/0003_alter_album_options.py create mode 100644 app/tests/gallery/_init_.py diff --git a/app/gallery/factories/_init_.py b/app/gallery/factories/_init_.py new file mode 100644 index 00000000..e69de29b diff --git a/app/gallery/factories/album_factory.py b/app/gallery/factories/album_factory.py new file mode 100644 index 00000000..e2abd659 --- /dev/null +++ b/app/gallery/factories/album_factory.py @@ -0,0 +1,15 @@ +import factory +from factory.django import DjangoModelFactory + +from app.gallery.models.album import Album + + +class AlbumFactory(DjangoModelFactory): + class Meta: + model = Album + + id = factory.Sequence(lambda n: f"picture_{n}") + image = factory.Faker("image") + title = factory.Faker("title") + image_alt = factory.Faker("image_alt") + description = factory.Faker("description") diff --git a/app/gallery/migrations/0003_alter_album_options.py b/app/gallery/migrations/0003_alter_album_options.py new file mode 100644 index 00000000..5d8f724b --- /dev/null +++ b/app/gallery/migrations/0003_alter_album_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2024-10-21 17:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("gallery", "0002_remove_picture_album_album_id_alter_album_slug"), + ] + + operations = [ + migrations.AlterModelOptions( + name="album", + options={"ordering": ["-created_at"]}, + ), + ] diff --git a/app/gallery/models/album.py b/app/gallery/models/album.py index 43dfe672..b3caf682 100644 --- a/app/gallery/models/album.py +++ b/app/gallery/models/album.py @@ -18,6 +18,9 @@ class Album(BaseModel, BasePermissionModel, OptionalImage): slug = models.SlugField(max_length=50, primary_key=False) write_access = AdminGroup.all() + class Meta: + ordering = ["-created_at"] + def __str__(self): return self.title diff --git a/app/tests/gallery/_init_.py b/app/tests/gallery/_init_.py new file mode 100644 index 00000000..e69de29b From fc4b78bbbd4de9ddf22c429cb9e85ef23e1c911f Mon Sep 17 00:00:00 2001 From: Emil Johnsen <111747340+EmilJohns1@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:56:56 +0100 Subject: [PATCH 15/16] made it possible for members and leader of the organizing group to add members to event (#921) Co-authored-by: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> --- app/content/views/registration.py | 24 ++-- .../content/test_registration_integration.py | 106 +++++++++++++++++- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/app/content/views/registration.py b/app/content/views/registration.py index 4aa3bf68..c41a8682 100644 --- a/app/content/views/registration.py +++ b/app/content/views/registration.py @@ -13,6 +13,7 @@ from app.common.pagination import BasePagination from app.common.permissions import ( BasicViewPermission, + check_has_access, is_admin_group_user, is_admin_user, ) @@ -157,7 +158,22 @@ def _admin_unregister(self, registration): def add_registration(self, request, *_args, **_kwargs): """Add registration to event for admins""" - if not is_admin_group_user(request): + event_id = self.kwargs.get("event_id", None) + user_id = request.data["user"] + + event = get_object_or_404(Event, id=event_id) + user = get_object_or_404(User, user_id=user_id) + + organizing_group = event.organizer + + is_member_or_leader_of_organizing_group = check_has_access( + [organizing_group], request + ) + + if ( + not is_admin_group_user(request) + and not is_member_or_leader_of_organizing_group + ): return Response( { "detail": "Du har ikke tillatelse til å opprette en påmelding på dette arrangementet" @@ -165,12 +181,6 @@ def add_registration(self, request, *_args, **_kwargs): status=status.HTTP_403_FORBIDDEN, ) - event_id = self.kwargs.get("event_id", None) - user_id = request.data["user"] - - event = get_object_or_404(Event, id=event_id) - user = get_object_or_404(User, user_id=user_id) - if not user.accepts_event_rules: return Response( { diff --git a/app/tests/content/test_registration_integration.py b/app/tests/content/test_registration_integration.py index 86d11115..eb7a3990 100644 --- a/app/tests/content/test_registration_integration.py +++ b/app/tests/content/test_registration_integration.py @@ -4,7 +4,7 @@ import pytest -from app.common.enums import AdminGroup +from app.common.enums import AdminGroup, Groups from app.common.enums import NativeGroupType as GroupType from app.common.enums import NativeMembershipType as MembershipType from app.common.enums import NativeUserStudy as StudyType @@ -1038,6 +1038,110 @@ def test_add_registration_to_event_as_member(member, event): assert response.status_code == status.HTTP_403_FORBIDDEN +@pytest.mark.django_db +@pytest.mark.parametrize( + "group_name", + [ + Groups.JUBKOM, + Groups.REDAKSJONEN, + Groups.FONDET, + Groups.PLASK, + Groups.DRIFT, + ], +) +def test_add_registration_to_event_as_group_member(event, member, group_name): + """ + A member of a specific group (not part of AdminGroup) should be able to add a + registration to an event if their group organized it. + """ + + member_group = add_user_to_group_with_name( + member, group_name, GroupType.SUBGROUP, MembershipType.MEMBER + ) + + event.organizer = member_group + event.save() + + data = {"user": member.user_id, "event": event.id} + url = f"{_get_registration_url(event=event)}add/" + + client = get_api_client(user=member) + response = client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "group_name", + [ + Groups.JUBKOM, + Groups.REDAKSJONEN, + Groups.FONDET, + Groups.PLASK, + Groups.DRIFT, + ], +) +def test_add_registration_to_event_as_group_member_of_non_organizing_group( + event, member, group_name +): + """ + A member of a specific group (not part of AdminGroup) should NOT be able to add a + registration to an event if their group did not organize it. + """ + add_user_to_group_with_name( + member, group_name, GroupType.SUBGROUP, MembershipType.MEMBER + ) + + event.organizer = GroupFactory(name="Different Organizer") + event.save() + + data = {"user": member.user_id, "event": event.id} + url = f"{_get_registration_url(event=event)}add/" + + client = get_api_client(user=member) + response = client.post(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "group_name", + [ + Groups.JUBKOM, + Groups.REDAKSJONEN, + Groups.FONDET, + Groups.PLASK, + Groups.DRIFT, + ], +) +def test_add_registration_when_event_is_full(event, member, group_name): + """ + A member of the organizing group should be able to add a registration to an event + for another member even when the event is full, and the registration should be added to the waitlist. + """ + + member_group = add_user_to_group_with_name( + member, group_name, GroupType.SUBGROUP, MembershipType.MEMBER + ) + + event.organizer = member_group + event.limit = 1 + event.save() + + RegistrationFactory(event=event) + + data = {"user": member.user_id, "event": event.id} + url = f"{_get_registration_url(event=event)}add/" + + client = get_api_client(user=member) + response = client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + assert event.registrations.get(user=member).is_on_wait + + @pytest.mark.django_db @pytest.mark.parametrize( ("order_status", "status_code"), From a9f32b483f202a2fd367df1ab1c8c34c1eb347ae Mon Sep 17 00:00:00 2001 From: Mads Nylund <73914541+MadsNyl@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:54:36 +0100 Subject: [PATCH 16/16] Fix feeedback serializer on list (#927) added description to serializer for feedback list --- app/feedback/serializers/feedback.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/feedback/serializers/feedback.py b/app/feedback/serializers/feedback.py index 588fc680..ae184264 100644 --- a/app/feedback/serializers/feedback.py +++ b/app/feedback/serializers/feedback.py @@ -21,6 +21,7 @@ class Meta: "status", "created_at", "author", + "description", )