diff --git a/.envs/.local b/.envs/.local index a873515cd..69592b857 100644 --- a/.envs/.local +++ b/.envs/.local @@ -4,4 +4,4 @@ DATABASE_HOST=db DATABASE_NAME=nettside-dev DATABASE_PASSWORD=password DATABASE_PORT=3306 -DATABASE_USER=root +DATABASE_USER=root \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 17dd794be..d9ed4dcf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ## Neste versjon +## Versjon 2024.04.16 +- ✨ **Brukerbio**. Bruker kan nå opprette bio. + ## Versjon 2023.04.08 - ✨ **Codex** Index brukere kan nå opprette dokumenter og møtereferater i Codex. diff --git a/app/communication/enums.py b/app/communication/enums.py index 60cb68ac1..d9c6fc606 100644 --- a/app/communication/enums.py +++ b/app/communication/enums.py @@ -15,3 +15,6 @@ class UserNotificationSettingType(models.TextChoices): FINE = "FINE", "Grupper - bot" GROUP_MEMBERSHIP = "GROUP_MEMBERSHIP", "Grupper - medlemsskap" OTHER = "OTHER", "Andre" + RESERVATION_NEW = "RESERVATION NEW", "Ny reservasjon" + RESERVATION_APPROVED = "RESERVATION APPROVED", "Godkjent reservasjon" + RESERVATION_CANCELLED = "RESERVATION CANCELLED", "Avslått reservasjon" diff --git a/app/communication/migrations/0010_alter_usernotificationsetting_notification_type.py b/app/communication/migrations/0010_alter_usernotificationsetting_notification_type.py new file mode 100644 index 000000000..87c08c700 --- /dev/null +++ b/app/communication/migrations/0010_alter_usernotificationsetting_notification_type.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.5 on 2024-04-10 16:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("communication", "0009_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"), + ], + max_length=30, + ), + ), + ] diff --git a/app/content/factories/__init__.py b/app/content/factories/__init__.py index 1a28282eb..6dd452ef5 100644 --- a/app/content/factories/__init__.py +++ b/app/content/factories/__init__.py @@ -10,5 +10,6 @@ from app.content.factories.toddel_factory import ToddelFactory from app.content.factories.priority_pool_factory import PriorityPoolFactory from app.content.factories.qr_code_factory import QRCodeFactory +from app.content.factories.user_bio_factory import UserBioFactory from app.content.factories.logentry_factory import LogEntryFactory from app.content.factories.minute_factory import MinuteFactory diff --git a/app/content/factories/user_bio_factory.py b/app/content/factories/user_bio_factory.py new file mode 100644 index 000000000..56967709c --- /dev/null +++ b/app/content/factories/user_bio_factory.py @@ -0,0 +1,12 @@ +import factory +from factory.django import DjangoModelFactory + +from app.content.factories.user_factory import UserFactory +from app.content.models.user_bio import UserBio + + +class UserBioFactory(DjangoModelFactory): + class Meta: + model = UserBio + + user = factory.SubFactory(UserFactory) diff --git a/app/content/filters/user.py b/app/content/filters/user.py index d44c492de..69ffd2e63 100644 --- a/app/content/filters/user.py +++ b/app/content/filters/user.py @@ -20,6 +20,10 @@ class UserFilter(FilterSet): ) in_group = CharFilter(method="filter_is_in_group", label="Only list users in group") + has_allowed_photo = BooleanFilter( + method="filter_has_allowed_photo", label="Has allowed photo" + ) + class Meta: model: User fields = [ @@ -52,3 +56,6 @@ 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): + return queryset.filter(allows_photo_by_default=value) diff --git a/app/content/migrations/0059_userbio.py b/app/content/migrations/0059_userbio.py new file mode 100644 index 000000000..efe3561cf --- /dev/null +++ b/app/content/migrations/0059_userbio.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.5 on 2024-01-29 17:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0058_merge_20231217_2155"), + ] + + operations = [ + migrations.CreateModel( + name="UserBio", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(max_length=50)), + ("gitHub_link", models.URLField(blank=True, max_length=300, null=True)), + ( + "linkedIn_link", + models.URLField(blank=True, max_length=300, null=True), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bio", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/app/content/migrations/0060_alter_userbio_description.py b/app/content/migrations/0060_alter_userbio_description.py new file mode 100644 index 000000000..1f945a9a6 --- /dev/null +++ b/app/content/migrations/0060_alter_userbio_description.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2024-02-01 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0059_userbio"), + ] + + operations = [ + migrations.AlterField( + model_name="userbio", + name="description", + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/app/content/migrations/0061_userbio_created_at_userbio_updated_at.py b/app/content/migrations/0061_userbio_created_at_userbio_updated_at.py new file mode 100644 index 000000000..8d70b8703 --- /dev/null +++ b/app/content/migrations/0061_userbio_created_at_userbio_updated_at.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.5 on 2024-02-19 16:09 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0060_alter_userbio_description"), + ] + + operations = [ + migrations.AddField( + model_name="userbio", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="userbio", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/app/content/migrations/0062_alter_userbio_user.py b/app/content/migrations/0062_alter_userbio_user.py new file mode 100644 index 000000000..3697c9a23 --- /dev/null +++ b/app/content/migrations/0062_alter_userbio_user.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.5 on 2024-02-21 13:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0061_userbio_created_at_userbio_updated_at"), + ] + + operations = [ + migrations.AlterField( + model_name="userbio", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bio", + to=settings.AUTH_USER_MODEL, + unique=True, + ), + ), + ] diff --git a/app/content/migrations/0063_alter_userbio_user.py b/app/content/migrations/0063_alter_userbio_user.py new file mode 100644 index 000000000..c9cb9f2d2 --- /dev/null +++ b/app/content/migrations/0063_alter_userbio_user.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.5 on 2024-02-21 13:39 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0062_alter_userbio_user"), + ] + + operations = [ + migrations.AlterField( + model_name="userbio", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="bio", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/app/content/migrations/0064_alter_userbio_description.py b/app/content/migrations/0064_alter_userbio_description.py new file mode 100644 index 000000000..3e0461076 --- /dev/null +++ b/app/content/migrations/0064_alter_userbio_description.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2024-02-26 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0063_alter_userbio_user"), + ] + + operations = [ + migrations.AlterField( + model_name="userbio", + name="description", + field=models.CharField(blank=True, max_length=500, null=True), + ), + ] diff --git a/app/content/migrations/0065_merge_0060_minute_tag_0064_alter_userbio_description.py b/app/content/migrations/0065_merge_0060_minute_tag_0064_alter_userbio_description.py new file mode 100644 index 000000000..311ba8460 --- /dev/null +++ b/app/content/migrations/0065_merge_0060_minute_tag_0064_alter_userbio_description.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.5 on 2024-04-16 06:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("content", "0060_minute_tag"), + ("content", "0064_alter_userbio_description"), + ] + + operations = [] diff --git a/app/content/models/user_bio.py b/app/content/models/user_bio.py new file mode 100644 index 000000000..bca02474a --- /dev/null +++ b/app/content/models/user_bio.py @@ -0,0 +1,45 @@ +from django.db import models + +from app.common.enums import Groups +from app.common.permissions import BasePermissionModel, check_has_access +from app.content.models.user import User +from app.util.models import BaseModel + + +class UserBio(BaseModel, BasePermissionModel): + read_access = (Groups.TIHLDE,) + write_access = (Groups.TIHLDE,) + + description = models.CharField(max_length=500, blank=True, null=True) + + gitHub_link = models.URLField(max_length=300, blank=True, null=True) + + linkedIn_link = models.URLField(max_length=300, blank=True, null=True) + + user = models.OneToOneField( + User, on_delete=models.CASCADE, unique=True, related_name="bio" + ) + + def __str__(self): + bio_str = f"{self.user}" + if self.description: + bio_str += f" - {self.description}" + if self.gitHub_link: + bio_str += f" - {self.gitHub_link}" + if self.linkedIn_link: + bio_str += f" - {self.linkedIn_link}" + return bio_str + + @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(cls.write_access, request) + + def has_object_update_permission(self, request): + return self.user == request.user + + def has_object_destroy_permission(self, request): + return self.user == request.user diff --git a/app/content/serializers/user.py b/app/content/serializers/user.py index d52aed014..d1bd1c544 100644 --- a/app/content/serializers/user.py +++ b/app/content/serializers/user.py @@ -6,6 +6,7 @@ from app.common.enums import GroupType from app.common.serializers import BaseModelSerializer from app.content.models import User +from app.content.serializers.user_bio import UserBioSerializer from app.group.models import Group, Membership @@ -50,6 +51,7 @@ def get_studyyear(self, obj): class UserSerializer(DefaultUserSerializer): unread_notifications = serializers.SerializerMethodField() unanswered_evaluations_count = serializers.SerializerMethodField() + bio = UserBioSerializer(read_only=True, required=False) class Meta: model = User @@ -63,6 +65,7 @@ class Meta: "slack_user_id", "allows_photo_by_default", "accepts_event_rules", + "bio", ) read_only_fields = ("user_id",) diff --git a/app/content/serializers/user_bio.py b/app/content/serializers/user_bio.py new file mode 100644 index 000000000..fd701ffba --- /dev/null +++ b/app/content/serializers/user_bio.py @@ -0,0 +1,23 @@ +from app.common.serializers import BaseModelSerializer +from app.content.models.user_bio import UserBio + + +class UserBioSerializer(BaseModelSerializer): + class Meta: + model = UserBio + fields = ["id", "description", "gitHub_link", "linkedIn_link"] + + +class UserBioCreateSerializer(BaseModelSerializer): + class Meta: + model = UserBio + fields = ["description", "gitHub_link", "linkedIn_link"] + + def create(self, validated_data): + return super().create(validated_data) + + +class UserBioUpdateSerializer(BaseModelSerializer): + class Meta: + model = UserBio + fields = ["description", "gitHub_link", "linkedIn_link"] diff --git a/app/content/urls.py b/app/content/urls.py index 1a783c067..f710aad06 100644 --- a/app/content/urls.py +++ b/app/content/urls.py @@ -14,6 +14,7 @@ ShortLinkViewSet, StrikeViewSet, ToddelViewSet, + UserBioViewset, UserCalendarEvents, UserViewSet, accept_form, @@ -30,6 +31,7 @@ router.register("short-links", ShortLinkViewSet, basename="short-link") router.register("qr-codes", QRCodeViewSet, basename="qr-code") router.register("users", UserViewSet, basename="user") +router.register("user-bios", UserBioViewset, basename="user-bio") router.register( r"events/(?P\d+)/registrations", RegistrationViewSet, diff --git a/app/content/views/__init__.py b/app/content/views/__init__.py index 517d59b3c..bc0bd030a 100644 --- a/app/content/views/__init__.py +++ b/app/content/views/__init__.py @@ -12,5 +12,6 @@ from app.content.views.strike import StrikeViewSet from app.content.views.toddel import ToddelViewSet from app.content.views.qr_code import QRCodeViewSet +from app.content.views.user_bio import UserBioViewset from app.content.views.logentry import LogEntryViewSet from app.content.views.minute import MinuteViewSet diff --git a/app/content/views/user.py b/app/content/views/user.py index e0edc6770..90d787b2c 100644 --- a/app/content/views/user.py +++ b/app/content/views/user.py @@ -63,15 +63,23 @@ def get_serializer_class(self): return super().get_serializer_class() def retrieve(self, request, pk, *args, **kwargs): - user = self._get_user(request, pk) - self.check_object_permissions(self.request, user) + try: + user = self._get_user(request, pk) - serializer = DefaultUserSerializer(user) - if is_admin_user(self.request) or user == request.user: - serializer = UserSerializer(user, context={"request": self.request}) + self.check_object_permissions(self.request, user) - return Response(serializer.data, status=status.HTTP_200_OK) + serializer = DefaultUserSerializer(user) + + if is_admin_user(self.request) or user == request.user: + serializer = UserSerializer(user, context={"request": self.request}) + + return Response(serializer.data, status=status.HTTP_200_OK) + + except Exception: + return Response( + {"message": "Error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) def create(self, request, *args, **kwargs): serializer = UserCreateSerializer(data=self.request.data) @@ -181,6 +189,20 @@ def get_user_memberships(self, request, pk, *args, **kwargs): context={"request": request}, ) + @action(detail=True, methods=["get"], url_path="memberships-with-fines") + def get_user_memberships_with_fines(self, request, pk, *args, **kwargs): + user = self._get_user(request, pk) + self.check_object_permissions(self.request, user) + + memberships = user.memberships.filter( + group__type__in=GroupType.public_groups(), group__fines_activated=True + ).order_by("-created_at") + return self.paginate_response( + data=memberships, + serializer=MembershipSerializer, + context={"request": request}, + ) + @action(detail=True, methods=["get"], url_path="membership-histories") def get_user_membership_histories(self, request, pk, *args, **kwargs): user = self._get_user(request, pk) diff --git a/app/content/views/user_bio.py b/app/content/views/user_bio.py new file mode 100644 index 000000000..fcb7da4c0 --- /dev/null +++ b/app/content/views/user_bio.py @@ -0,0 +1,54 @@ +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.content.models.user_bio import UserBio +from app.content.serializers.user_bio import ( + UserBioCreateSerializer, + UserBioSerializer, + UserBioUpdateSerializer, +) + + +class UserBioViewset(BaseViewSet): + queryset = UserBio.objects.all() + serializer_class = UserBioSerializer + permission_classes = [BasicViewPermission] + + def create(self, request, *args, **kwargs): + data = request.data + + serializer = UserBioCreateSerializer(data=data, context={"request": request}) + + if not serializer.is_valid(): + return Response( + {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST + ) + + user_bio = super().perform_create(serializer, user=request.user) + + user_bio_serializer = UserBioSerializer( + user_bio, context={"user": user_bio.user} + ) + + return Response(user_bio_serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, *args, **kwargs): + bio = self.get_object() + serializer = UserBioUpdateSerializer( + bio, data=request.data, context={"request": request} + ) + if serializer.is_valid(): + bio = super().perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response( + {"detail": serializer.errors}, status=status.HTTP_400_BAD_REQUEST + ) + + def destroy(self, request, *args, **kwargs): + super().destroy(request, *args, **kwargs) + return Response( + {"detail": ("Brukerbio ble slettet")}, status=status.HTTP_200_OK + ) diff --git a/app/group/serializers/group.py b/app/group/serializers/group.py index 406cfa733..fd45dd6ce 100644 --- a/app/group/serializers/group.py +++ b/app/group/serializers/group.py @@ -22,6 +22,7 @@ class Meta: "viewer_is_member", "image", "image_alt", + "fines_activated", ) def get_viewer_is_member(self, obj): diff --git a/app/kontres/models/reservation.py b/app/kontres/models/reservation.py index f73fbc0a1..22f544cae 100644 --- a/app/kontres/models/reservation.py +++ b/app/kontres/models/reservation.py @@ -2,10 +2,12 @@ from django.db import models -from app.common.enums import AdminGroup, Groups +from app.common.enums import AdminGroup, Groups, MembershipType from app.common.permissions import BasePermissionModel, check_has_access +from app.communication.enums import UserNotificationSettingType +from app.communication.notifier import Notify from app.content.models import User -from app.group.models import Group +from app.group.models import Group, Membership from app.kontres.enums import ReservationStateEnum from app.kontres.models.bookable_item import BookableItem from app.util.models import BaseModel @@ -110,3 +112,44 @@ def has_object_update_permission(self, request): def is_own_reservation(self, request): return self.author == request.user + + def notify_admins_new_reservation(self): + formatted_start_time = self.start_time.strftime("%d/%m %H:%M") + + leader_membership = Membership.objects.filter( + group=Group.objects.get(pk="kontkom"), membership_type=MembershipType.LEADER + ).first() + + if leader_membership is None: + return + + notification_message = ( + f"En ny reservasjon er opprettet for {self.bookable_item.name}, " + f"planlagt til {formatted_start_time}." + ) + + Notify( + users=[leader_membership.user], + title="Ny Reservasjon Laget", + notification_type=UserNotificationSettingType.RESERVATION_NEW, + ).add_paragraph(notification_message).send() + + def notify_approved(self): + formatted_date_time = self.start_time.strftime("%d/%m %H:%M") + Notify( + [self.author], + f'Reservasjonssøknad for "{self.bookable_item.name} er godkjent."', + UserNotificationSettingType.RESERVATION_APPROVED, + ).add_paragraph( + f"Hei, {self.author.first_name}! Din søknad for å reservere {self.bookable_item.name}, den {formatted_date_time} har blitt godkjent." + ).send() + + def notify_denied(self): + formatted_date_time = self.start_time.strftime("%d/%m %H:%M") + Notify( + [self.author], + f'Reservasjonssøknad for "{self.bookable_item.name}" er avslått.', + UserNotificationSettingType.RESERVATION_CANCELLED, + ).add_paragraph( + f"Hei, {self.author.first_name}! Din søknad for å reservere {self.bookable_item.name}, den {formatted_date_time} har blitt avslått. Du kan ta kontakt med Kontor og Kiosk dersom du lurer på noe ifm. dette." + ).send() diff --git a/app/kontres/views/reservation.py b/app/kontres/views/reservation.py index d3ab071fe..1a0165fdc 100644 --- a/app/kontres/views/reservation.py +++ b/app/kontres/views/reservation.py @@ -59,17 +59,28 @@ def update(self, request, *args, **kwargs): serializer = self.get_serializer(reservation, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - # Check if the state is being updated to CONFIRMED and set approved_by - if ( - "state" in serializer.validated_data - and serializer.validated_data["state"] == ReservationStateEnum.CONFIRMED - and reservation.state != ReservationStateEnum.CONFIRMED - ): - serializer.save(approved_by=request.user) - else: - serializer.save() + if serializer.is_valid(): + previous_state = reservation.state + new_state = serializer.validated_data.get("state") + + # Check if the state is being updated to CONFIRMED and set approved_by + if ( + new_state == ReservationStateEnum.CONFIRMED + and previous_state != ReservationStateEnum.CONFIRMED + ): + serializer.save(approved_by=request.user) + else: + serializer.save() + + if new_state and new_state != previous_state: + if new_state == ReservationStateEnum.CONFIRMED: + serializer.instance.notify_approved() + elif new_state == ReservationStateEnum.CANCELLED: + serializer.instance.notify_denied() - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, *args, **kwargs): super().destroy(self, request, *args, **kwargs) diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 3d864bb04..14aae4af4 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -22,6 +22,7 @@ QRCodeFactory, RegistrationFactory, ShortLinkFactory, + UserBioFactory, UserFactory, ) from app.content.factories.toddel_factory import ToddelFactory @@ -290,6 +291,11 @@ def event_with_priority_pool(priority_group): return event +@pytest.fixture() +def user_bio(): + return UserBioFactory() + + @pytest.fixture() def minute(user): return MinuteFactory(author=user) diff --git a/app/tests/content/test_user_bio_integration.py b/app/tests/content/test_user_bio_integration.py new file mode 100644 index 000000000..a55978d3c --- /dev/null +++ b/app/tests/content/test_user_bio_integration.py @@ -0,0 +1,124 @@ +from rest_framework import status + +import pytest + +from app.content.models.user_bio import UserBio +from app.util.test_utils import get_api_client + +pytestmark = pytest.mark.django_db + +API_USER_BIO_BASE_URL = "/user-bios/" + + +def _get_bio_url(user_bio): + return f"{API_USER_BIO_BASE_URL}{user_bio.id}/" + + +def _get_user_bio_post_data(): + return { + "description": "this is my description", + "gitHub_link": "https://www.github.com", + "linkedIn_link": "https://www.linkedIn.com", + } + + +def _get_user_bio_put_data(): + return { + "description": "New description", + } + + +@pytest.mark.django_db +def test_create_user_bio(member, api_client): + """A user should be able to create a user bio""" + data = _get_user_bio_post_data() + client = api_client(user=member) + response = client.post(API_USER_BIO_BASE_URL, data) + + assert response.status_code == status.HTTP_201_CREATED + + +@pytest.mark.django_db +def test_create_duplicate_user_bio(member, api_client, user_bio): + """A user should not be able to create a duplicate user bio""" + user_bio.user = member + user_bio.save() + + data = _get_user_bio_post_data() + client = api_client(user=member) + response = client.post(API_USER_BIO_BASE_URL, data) + + assert response.status_code == status.HTTP_409_CONFLICT + + +@pytest.mark.django_db +def test_update_bio_as_anonymous_user(default_client, user_bio): + """An anonymous user should not be able to update a user's bio""" + url = _get_bio_url(user_bio) + data = _get_user_bio_put_data() + response = default_client.put(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + user_bio.refresh_from_db() + assert user_bio.description != data["description"] + + +@pytest.mark.django_db +def test_update_own_bio_as_user(member, user_bio): + """An user should be able to update their own bio""" + user_bio.user = member + user_bio.save() + url = _get_bio_url(user_bio) + client = get_api_client(user=member) + data = _get_user_bio_put_data() + response = client.put(url, data) + + assert response.status_code == status.HTTP_200_OK + user_bio.refresh_from_db() + assert user_bio.description == data["description"] + + +@pytest.mark.django_db +def test_update_another_users_bio(member, user_bio): + """An user should not be able to update another user's bio""" + url = _get_bio_url(user_bio) + client = get_api_client(user=member) + data = _get_user_bio_put_data() + response = client.put(url, data) + + assert response.status_code == status.HTTP_403_FORBIDDEN + user_bio.refresh_from_db() + assert user_bio.description != data["description"] + + +@pytest.mark.django_db +def test_destroy_bio_as_anonymous_user(default_client, user_bio): + """An anonymous user should not be able to destroy a user's bio""" + url = _get_bio_url(user_bio) + response = default_client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert len(UserBio.objects.filter(id=user_bio.id)) + + +@pytest.mark.django_db +def test_destroy_own_bio(user_bio, member): + """An user should be able to destroy their own user's bio""" + user_bio.user = member + user_bio.save() + url = _get_bio_url(user_bio) + client = get_api_client(user=member) + response = client.delete(url) + assert response.status_code == status.HTTP_200_OK + assert not len(UserBio.objects.filter(id=user_bio.id)) + + +@pytest.mark.django_db +def test_destroy_other_bios(member, user_bio): + """An user should not be able to delete another user's bio""" + url = _get_bio_url(user_bio) + client = get_api_client(user=member) + response = client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert len(UserBio.objects.filter(id=user_bio.id)) diff --git a/app/tests/content/test_user_integration.py b/app/tests/content/test_user_integration.py index 139f3bd9a..bd36027e7 100644 --- a/app/tests/content/test_user_integration.py +++ b/app/tests/content/test_user_integration.py @@ -180,6 +180,7 @@ def test_filter_only_users_with_active_strikes( [ ("/", status.HTTP_200_OK), ("/memberships/", status.HTTP_200_OK), + ("/memberships-with-fines/", status.HTTP_200_OK), ("/membership-histories/", status.HTTP_200_OK), ("/badges/", status.HTTP_200_OK), ("/events/", status.HTTP_200_OK), diff --git a/app/tests/kontres/test_reservation_integration.py b/app/tests/kontres/test_reservation_integration.py index 7668c9968..d78dc9770 100644 --- a/app/tests/kontres/test_reservation_integration.py +++ b/app/tests/kontres/test_reservation_integration.py @@ -659,6 +659,9 @@ def test_retrieve_specific_reservation_within_its_date_range(member, bookable_it @pytest.mark.skip @pytest.mark.django_db +@pytest.mark.skip( + "This test is only working sometimes, needs to be fixed. Kontres backend team's responsibility." +) def test_retrieve_subset_of_reservations(member, bookable_item): client = get_api_client(user=member)