diff --git a/docs/conf.py b/docs/conf.py index 5b21ed137..01cf10ada 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,8 +48,8 @@ master_doc = "index" # General information about the project. -project = u"LEGO" -copyright = u"2014 - 2020, Abakus Webkom" +project = "LEGO" +copyright = "2014 - 2020, Abakus Webkom" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -197,7 +197,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ("index", "LEGO.tex", u"LEGO Documentation", u"Abakus Webkom", "manual") + ("index", "LEGO.tex", "LEGO Documentation", "Abakus Webkom", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -224,7 +224,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "lego", u"LEGO Documentation", [u"Abakus Webkom"], 1)] +man_pages = [("index", "lego", "LEGO Documentation", ["Abakus Webkom"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -238,8 +238,8 @@ ( "index", "LEGO", - u"LEGO Documentation", - u"Abakus Webkom", + "LEGO Documentation", + "Abakus Webkom", "LEGO", "One line description of project.", "Miscellaneous", diff --git a/lego/api/v1.py b/lego/api/v1.py index a34af4be0..e15d48810 100644 --- a/lego/api/v1.py +++ b/lego/api/v1.py @@ -40,6 +40,8 @@ from lego.apps.gallery.views import GalleryPictureViewSet, GalleryViewSet from lego.apps.ical.viewsets import ICalTokenViewset, ICalViewset from lego.apps.joblistings.views import JoblistingViewSet +from lego.apps.lending.urls import urlpatterns as lending_urls +from lego.apps.lending.views import LendableObjectViewSet, LendingInstanceViewSet from lego.apps.meetings.views import ( MeetingInvitationTokenViewSet, MeetingInvitationViewSet, @@ -78,8 +80,6 @@ from lego.apps.users.views.registration import UserRegistrationRequestViewSet from lego.apps.users.views.user_delete import UserDeleteViewSet from lego.apps.users.views.users import UsersViewSet -from lego.apps.lending.views import LendableObjectViewSet, LendingInstanceViewSet - from lego.utils.views import SiteMetaViewSet router = routers.DefaultRouter() @@ -215,4 +215,5 @@ urlpatterns = [ path("", include(router.urls)), path("forums/", include((forums_urls, "forums"))), + path("", include((lending_urls, "lending"))), ] diff --git a/lego/apps/comments/models.py b/lego/apps/comments/models.py index e2c04de53..321bdf2f6 100644 --- a/lego/apps/comments/models.py +++ b/lego/apps/comments/models.py @@ -1,9 +1,10 @@ -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from lego.apps.comments.permissions import CommentPermissionHandler from lego.apps.content.fields import ContentField +from lego.apps.reactions.models import Reaction from lego.utils.managers import BasisModelManager from lego.utils.models import BasisModel @@ -19,6 +20,7 @@ class Comment(BasisModel): object_id = models.PositiveIntegerField(db_index=True) content_object = GenericForeignKey() parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE) + reactions = GenericRelation(Reaction) objects = CommentManager() # type: ignore @@ -34,5 +36,29 @@ def delete(self): else: super(Comment, self).delete(force=True) + def get_reactions_grouped(self, user): + grouped = {} + for reaction in self.reactions.all(): + if reaction.emoji.pk not in grouped: + grouped[reaction.emoji.pk] = { + "emoji": reaction.emoji.pk, + "unicode_string": reaction.emoji.unicode_string, + "count": 0, + "has_reacted": False, + "reaction_id": None, + } + + grouped[reaction.emoji.pk]["count"] += 1 + + if reaction.created_by == user: + grouped[reaction.emoji.pk]["has_reacted"] = True + grouped[reaction.emoji.pk]["reaction_id"] = reaction.id + + return sorted(grouped.values(), key=lambda kv: kv["count"], reverse=True) + def __str__(self): return self.text + + @property + def content_target_self(self): + return f"{self._meta.app_label}.{self._meta.model_name}-{self.pk}" diff --git a/lego/apps/comments/serializers.py b/lego/apps/comments/serializers.py index 5eee62b6f..81f7d21ff 100644 --- a/lego/apps/comments/serializers.py +++ b/lego/apps/comments/serializers.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType +from rest_framework import serializers from rest_framework.exceptions import ValidationError -from rest_framework.fields import DateTimeField +from rest_framework.fields import CharField, DateTimeField from lego.apps.comments.models import Comment from lego.apps.content.fields import ContentSerializerField @@ -14,6 +15,8 @@ class CommentSerializer(BasisModelSerializer): updated_at = DateTimeField(read_only=True) content_target = GenericRelationField(source="content_object") text = ContentSerializerField() + reactions_grouped = serializers.SerializerMethodField() + content_target_self = CharField(read_only=True) class Meta: model = Comment @@ -22,11 +25,17 @@ class Meta: "text", "author", "content_target", + "content_target_self", "created_at", "updated_at", "parent", + "reactions_grouped", ) + def get_reactions_grouped(self, obj): + user = self.context["request"].user + return obj.get_reactions_grouped(user) + def validate(self, attrs): content_target = attrs.get("content_object") diff --git a/lego/apps/comments/tests/test_reactions.py b/lego/apps/comments/tests/test_reactions.py new file mode 100644 index 000000000..6db2824b8 --- /dev/null +++ b/lego/apps/comments/tests/test_reactions.py @@ -0,0 +1,56 @@ +from lego.apps.articles.models import Article +from lego.apps.comments.models import Comment +from lego.apps.emojis.models import Emoji +from lego.apps.reactions.models import Reaction +from lego.apps.users.models import User +from lego.utils.test_utils import BaseTestCase + + +class CommentReactionsTestCase(BaseTestCase): + fixtures = [ + "test_abakus_groups.yaml", + "test_users.yaml", + "test_emojis.yaml", + "test_articles.yaml", + ] + + def setUp(self): + self.comment = Comment.objects.create( + created_by_id=0, + text="first comment", + content_object=Article.objects.all().first(), + ) + self.user = User.objects.all().first() + self.comment.created_by = self.user + self.comment.save() + + self.emoji = Emoji.objects.first() + + def test_add_reaction(self): + test_reaction = Reaction.objects.create( + content_object=self.comment, emoji=self.emoji + ) + test_reaction.created_by = self.user + test_reaction.save() + + reactions_grouped = self.comment.get_reactions_grouped(self.user) + self.assertEqual(len(reactions_grouped), 1) + self.assertEqual(reactions_grouped[0]["count"], 1) + self.assertEqual(reactions_grouped[0]["has_reacted"], True) + self.assertEqual( + reactions_grouped[0]["unicode_string"], self.emoji.unicode_string + ) + + def test_remove_reaction(self): + test_reaction = Reaction.objects.create( + content_object=self.comment, emoji=self.emoji + ) + + reactions_grouped = self.comment.get_reactions_grouped(self.user) + self.assertEqual(len(reactions_grouped), 1) + self.assertEqual(reactions_grouped[0]["count"], 1) + + test_reaction.delete() + + reactions_grouped = self.comment.get_reactions_grouped(self.user) + self.assertEqual(len(reactions_grouped), 0) diff --git a/lego/apps/email/filters.py b/lego/apps/email/filters.py index 7666a142b..bd77b98b8 100644 --- a/lego/apps/email/filters.py +++ b/lego/apps/email/filters.py @@ -3,7 +3,14 @@ from django_filters.rest_framework import BooleanFilter, CharFilter, FilterSet from lego.apps.email.models import EmailList -from lego.apps.users.constants import GROUP_COMMITTEE, GROUP_GRADE +from lego.apps.users.constants import ( + GROUP_BOARD, + GROUP_COMMITTEE, + GROUP_GRADE, + GROUP_ORDAINED, + GROUP_SUB, +) +from lego.apps.users.filters import CharInFilter from lego.apps.users.models import User @@ -21,8 +28,8 @@ class EmailUserFilterSet(FilterSet): email = CharFilter(field_name="internal_email__email", lookup_expr="icontains") userUsername = CharFilter(field_name="username", lookup_expr="icontains") userFullname = CharFilter(field_name="userFullname", method="fullname") - userCommittee = CharFilter(field_name="userCommitee", method="commitee") userGrade = CharFilter(field_name="abakus_groups", method="grade") + userGroups = CharInFilter(field_name="userGroups", method="groups") enabled = BooleanFilter(field_name="internal_email_enabled") def grade(self, queryset, name, value): @@ -34,13 +41,15 @@ def grade(self, queryset, name, value): ) return queryset - def commitee(self, queryset, name, value): - if value == "-": - return queryset.exclude(abakus_groups__type=GROUP_COMMITTEE) + def groups(self, queryset, name, value): + relevant_types = [GROUP_COMMITTEE, GROUP_BOARD, GROUP_ORDAINED, GROUP_SUB] + + if value == ["-"]: + return queryset.exclude(abakus_groups__type__in=relevant_types) if value: return queryset.filter( - abakus_groups__name__icontains=value, - abakus_groups__type=GROUP_COMMITTEE, + abakus_groups__name__in=value, + abakus_groups__type__in=relevant_types, ) return queryset @@ -59,5 +68,5 @@ class Meta: "userUsername", "userFullname", "userGrade", - "userCommittee", + "userGroups", ) diff --git a/lego/apps/email/serializers.py b/lego/apps/email/serializers.py index 97ef9df2f..e5209b771 100644 --- a/lego/apps/email/serializers.py +++ b/lego/apps/email/serializers.py @@ -1,3 +1,5 @@ +from typing import Any + from django.core.exceptions import ValidationError from rest_framework import exceptions, serializers @@ -7,7 +9,7 @@ PublicUserListField, PublicUserWithGroupsField, ) -from lego.apps.users.models import User +from lego.apps.users.models import Membership, User from .fields import EmailAddressField @@ -44,6 +46,33 @@ class Meta: "additional_emails", ) + def validate(self, attrs: Any) -> Any: + # Use existing values where missing to support patch requests + get = ( + lambda index: attrs[index] + if index in attrs + else getattr(self.instance, index) + ) + length = ( + lambda value: len(value) if hasattr(value, "__len__") else value.count() + ) + # Require at least one receiver of the email list + users_len, emails_len = length(get("users")), length(get("additional_emails")) + if users_len == 0 and emails_len == 0: + groups = ( + attrs["groups"] if "groups" in attrs else self.instance.groups.all() + ) + group_roles = get("group_roles") + memberships = Membership.objects.filter(abakus_group__in=groups) + + if length(group_roles) > 0: + memberships = memberships.filter(role__in=group_roles) + + if not memberships.exists(): + raise ValidationError("Cannot create a mail list without receivers") + + return super().validate(attrs) + class EmailListDetailSerializer(EmailListSerializer): users = PublicUserListField({"read_only": True}) diff --git a/lego/apps/email/tests/test_views.py b/lego/apps/email/tests/test_views.py index 61562740d..c8058b0d2 100644 --- a/lego/apps/email/tests/test_views.py +++ b/lego/apps/email/tests/test_views.py @@ -1,7 +1,7 @@ from rest_framework import status -from lego.apps.email.models import EmailList -from lego.apps.users.models import AbakusGroup, User +from lego.apps.email.models import EmailAddress, EmailList +from lego.apps.users.models import AbakusGroup, Membership, User from lego.utils.test_utils import BaseAPITestCase @@ -116,6 +116,21 @@ def test_create_list_invalid_additional_email(self): ) self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + def test_create_list_no_recipients(self): + """Bad request when user tries to create list with no recipients""" + response = self.client.post( + self.url, + { + "name": "Webkom", + "email": "webbers", + "users": [], + "groups": [self.admin_group.id], + "group_roles": ["recruiting"], + "additional_emails": [], + }, + ) + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + def test_edit_additional_email(self): """Test ability to remove an additional email""" response = self.client.patch( @@ -138,7 +153,9 @@ def test_edit_additional_email(self): def test_delete_additional_email(self): """Test ability to set additional emails to an empty array""" - response = self.client.patch(f"{self.url}1/", {"additional_emails": []}) + response = self.client.patch( + f"{self.url}1/", {"additional_emails": [], "users": [1]} + ) self.assertEqual(status.HTTP_200_OK, response.status_code) self.assertEqual([], EmailList.objects.get(id=1).additional_emails) @@ -149,8 +166,31 @@ def test_change_assigned_email(self): self.assertEqual("address", EmailList.objects.get(id=1).email_id) + def test_edit_list_no_recipients(self): + """Bad request when user tries to edit list with no recipients""" + response = self.client.patch( + f"{self.url}1/", + { + "users": [], + "groups": [self.admin_group.id], + "group_roles": ["recruiting"], + "additional_emails": [], + }, + ) + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + + def test_edit_list_no_recipients_partial(self): + """Bad request when user tries to edit list with no recipients""" + response = self.client.patch( + f"{self.url}1/", + { + "additional_emails": [], + }, + ) + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + def test_delete_endpoint_not_available(self): - """The delete endpoint is'nt available.""" + """The delete endpoint isn't available.""" response = self.client.delete(f"{self.url}1/") self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) @@ -178,6 +218,36 @@ def test_list(self): response = self.client.get(self.url) self.assertEqual(status.HTTP_200_OK, response.status_code) + def test_list_filter_groups(self): + """The list endpoint can be filtered by group memberships""" + webkom = AbakusGroup.objects.get(name="Webkom") + webber = User.objects.create( + username="Webber", + email="webber", + internal_email=EmailAddress.objects.create(email="webber"), + ) + Membership.objects.create(abakus_group=webkom, user=webber) + User.objects.create( + username="Pleb", + email="pleb", + internal_email=EmailAddress.objects.create(email="pleb"), + ) + response = self.client.get(self.url) + self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual(3, len(response.json()["results"])) + + response = self.client.get(f"{self.url}?userGroups=Webkom") + self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual(1, len(response.json()["results"])) + + response = self.client.get(f"{self.url}?userGroups=Webkom,EmailAdminTest") + self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual(1, len(response.json()["results"])) + + response = self.client.get(f"{self.url}?userGroups=-") + self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual(2, len(response.json()["results"])) + def test_retrieve(self): """It is possible to retrieve the user""" response = self.client.get(f"{self.url}1/") diff --git a/lego/apps/events/constants.py b/lego/apps/events/constants.py index 2753b7b83..1f5281116 100644 --- a/lego/apps/events/constants.py +++ b/lego/apps/events/constants.py @@ -70,6 +70,7 @@ class PRESENCE_CHOICES(models.TextChoices): UNKNOWN = "UNKNOWN" PRESENT = "PRESENT" + LATE = "LATE" NOT_PRESENT = "NOT_PRESENT" diff --git a/lego/apps/events/migrations/0040_alter_registration_presence.py b/lego/apps/events/migrations/0040_alter_registration_presence.py new file mode 100644 index 000000000..5c960e230 --- /dev/null +++ b/lego/apps/events/migrations/0040_alter_registration_presence.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.10 on 2024-03-06 22:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("events", "0039_remove_event_use_contact_tracing"), + ] + + operations = [ + migrations.AlterField( + model_name="registration", + name="presence", + field=models.CharField( + choices=[ + ("UNKNOWN", "Unknown"), + ("PRESENT", "Present"), + ("LATE", "Late"), + ("NOT_PRESENT", "Not Present"), + ], + default="UNKNOWN", + max_length=20, + ), + ), + ] diff --git a/lego/apps/events/models.py b/lego/apps/events/models.py index 9400fa54b..20699d892 100644 --- a/lego/apps/events/models.py +++ b/lego/apps/events/models.py @@ -30,7 +30,7 @@ from lego.apps.files.models import FileField from lego.apps.followers.models import FollowEvent from lego.apps.permissions.models import ObjectPermissionsModel -from lego.apps.users.constants import AUTUMN, SPRING +from lego.apps.users.constants import AUTUMN, PENALTY_TYPES, PENALTY_WEIGHTS, SPRING from lego.apps.users.models import AbakusGroup, Membership, Penalty, User from lego.utils.models import BasisModel from lego.utils.youtube_validator import youtube_validator @@ -934,26 +934,47 @@ def set_presence(self, presence: constants.PRESENCE_CHOICES) -> None: """Wrap this method in a transaction""" if presence not in constants.PRESENCE_CHOICES: raise ValueError("Illegal presence choice") + self.presence = presence self.handle_user_penalty(presence) self.save() + def delete_presence_penalties_for_event(self) -> None: + for penalty in self.user.penalties.filter( + source_event=self.event, type=PENALTY_TYPES.PRESENCE + ): + penalty.delete() + def handle_user_penalty(self, presence: constants.PRESENCE_CHOICES) -> None: + """ + Previous penalties related to the event are deleted since the + newest presence is the only one that matters + """ + if ( self.event.heed_penalties and presence == constants.PRESENCE_CHOICES.NOT_PRESENT and self.event.penalty_weight_on_not_present ): - if not self.user.penalties.filter(source_event=self.event).exists(): - Penalty.objects.create( - user=self.user, - reason=f"Møtte ikke opp på {self.event.title}.", - weight=self.event.penalty_weight_on_not_present, - source_event=self.event, - ) + self.delete_presence_penalties_for_event() + Penalty.objects.create( + user=self.user, + reason=f"Møtte ikke opp på {self.event.title}.", + weight=self.event.penalty_weight_on_not_present, + source_event=self.event, + type=PENALTY_TYPES.PRESENCE, + ) + elif self.event.heed_penalties and presence == constants.PRESENCE_CHOICES.LATE: + self.delete_presence_penalties_for_event() + Penalty.objects.create( + user=self.user, + reason=f"Møtte for sent opp på {self.event.title}.", + weight=PENALTY_WEIGHTS.LATE_PRESENCE, + source_event=self.event, + type=PENALTY_TYPES.PRESENCE, + ) else: - for penalty in self.user.penalties.filter(source_event=self.event): - penalty.delete() + self.delete_presence_penalties_for_event() def add_to_pool(self, pool: Pool) -> Registration: allowed: bool = False diff --git a/lego/apps/events/serializers/registrations.py b/lego/apps/events/serializers/registrations.py index a3ed6dde4..3ce2220c3 100644 --- a/lego/apps/events/serializers/registrations.py +++ b/lego/apps/events/serializers/registrations.py @@ -1,4 +1,3 @@ -from django.db import transaction from rest_framework import serializers from rest_framework_jwt.serializers import ImpersonateAuthTokenSerializer @@ -65,13 +64,12 @@ class Meta: ) def update(self, instance, validated_data): - with transaction.atomic(): - presence = validated_data.pop("presence", None) - super().update(instance, validated_data) - if presence: - instance.set_presence(presence) + presence = validated_data.pop("presence", None) + super().update(instance, validated_data) + if presence: + instance.set_presence(presence) - return instance + return instance class RegistrationAnonymizedReadSerializer(BasisModelSerializer): diff --git a/lego/apps/events/tests/test_penalties.py b/lego/apps/events/tests/test_penalties.py index f518ea092..dc7d998a3 100644 --- a/lego/apps/events/tests/test_penalties.py +++ b/lego/apps/events/tests/test_penalties.py @@ -4,6 +4,7 @@ from lego.apps.events import constants from lego.apps.events.models import Event, Registration +from lego.apps.users.constants import LATE_PRESENCE_PENALTY_WEIGHT from lego.apps.users.models import AbakusGroup, Penalty from lego.utils.test_utils import BaseTestCase @@ -398,6 +399,19 @@ def test_penalties_created_when_not_present(self): self.assertEqual(penalties_before, 0) self.assertEqual(penalties_after, event.penalty_weight_on_not_present) + def test_penalties_created_when_late_present(self): + """Test that user gets penalties when late present""" + event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") + + registration = event.registrations.first() + penalties_before = registration.user.number_of_penalties() + + registration.set_presence(constants.PRESENCE_CHOICES.LATE) + + penalties_after = registration.user.number_of_penalties() + self.assertEqual(penalties_before, 0) + self.assertEqual(penalties_after, LATE_PRESENCE_PENALTY_WEIGHT) + def test_penalties_removed_when_not_present_changes(self): """Test that penalties for not_present gets removed when resetting presence""" event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") @@ -411,8 +425,23 @@ def test_penalties_removed_when_not_present_changes(self): self.assertEqual(penalties_before, event.penalty_weight_on_not_present) self.assertEqual(penalties_after, 0) + def test_penalties_removed_when_late_present_changes(self): + """Test that penalties for late presence gets removed when changing to present""" + event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") + registration = event.registrations.first() + registration.set_presence(constants.PRESENCE_CHOICES.LATE) + + penalties_before = registration.user.number_of_penalties() + registration.set_presence(constants.PRESENCE_CHOICES.PRESENT) + + penalties_after = registration.user.number_of_penalties() + self.assertEqual(penalties_before, LATE_PRESENCE_PENALTY_WEIGHT) + self.assertEqual(penalties_after, 0) + def test_only_correct_penalties_are_removed_on_presence_change(self): - """Test that only penalties for given event are removed when changing presence""" + """ + Test that only penalties of type presence for given event are removed when changing presence + """ event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") other_event = Event.objects.get(title="POOLS_NO_REGISTRATIONS") registration = event.registrations.first() @@ -447,6 +476,41 @@ def test_only_correct_penalties_are_removed_on_presence_change(self): ) self.assertEqual(penalties_after, other_event.penalty_weight_on_not_present) + def test_only_correct_penalties_are_removed_on_presence_change_on_same_event(self): + """Test that only penalties of type presence are removed when changing presence""" + event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") + registration = event.registrations.first() + + registration.set_presence(constants.PRESENCE_CHOICES.NOT_PRESENT) + penalties_before = registration.user.number_of_penalties() + penalties_object_before = list(registration.user.penalties.all()) + + # Default penalty type is other + Penalty.objects.create( + user=registration.user, + reason="SAME EVENT", + weight=2, + source_event=event, + ) + penalties_during = registration.user.number_of_penalties() + penalties_objects_during = list(registration.user.penalties.all()) + + registration.set_presence(constants.PRESENCE_CHOICES.UNKNOWN) + penalties_after = registration.user.number_of_penalties() + penalties_object_after = list(registration.user.penalties.all()) + + self.assertEqual(penalties_object_before[0].source_event, event) + self.assertEqual(penalties_object_after[0].source_event, event) + self.assertEqual(len(penalties_object_before), 1) + self.assertEqual(len(penalties_objects_during), 2) + self.assertEqual(len(penalties_object_after), 1) + self.assertEqual(penalties_before, event.penalty_weight_on_not_present) + self.assertEqual( + penalties_during, + event.penalty_weight_on_not_present + event.penalty_weight_on_not_present, + ) + self.assertEqual(penalties_after, event.penalty_weight_on_not_present) + def test_able_to_register_when_not_heed_penalties_with_penalties(self): """Test that user is able to register when heed_penalties is false and user has penalties""" event = Event.objects.get(title="POOLS_WITH_REGISTRATIONS") diff --git a/lego/apps/joblistings/tests/test_joblistings_api.py b/lego/apps/joblistings/tests/test_joblistings_api.py index 3844373a6..968ebbb86 100644 --- a/lego/apps/joblistings/tests/test_joblistings_api.py +++ b/lego/apps/joblistings/tests/test_joblistings_api.py @@ -98,12 +98,12 @@ def setUp(self): def test_with_abakus_user(self): AbakusGroup.objects.get(name="Abakus").add_user(self.abakus_user) self.client.force_authenticate(self.abakus_user) - joblisting_response = self.client.get(_get_list_url()) + joblisting_response = self.client.get(_get_list_url(), {"timeFilter": True}) self.assertEqual(joblisting_response.status_code, status.HTTP_200_OK) self.assertEqual(len(joblisting_response.json()["results"]), 4) def test_without_user(self): - joblisting_response = self.client.get(_get_list_url()) + joblisting_response = self.client.get(_get_list_url(), {"timeFilter": True}) self.assertEqual(joblisting_response.status_code, status.HTTP_200_OK) self.assertEqual(len(joblisting_response.json()["results"]), 4) @@ -111,7 +111,7 @@ def test_list_after_visible_to(self): joblisting = Joblisting.objects.all().first() joblisting.visible_to = timezone.now() - timedelta(days=2) joblisting.save() - joblisting_response = self.client.get(_get_list_url()) + joblisting_response = self.client.get(_get_list_url(), {"timeFilter": True}) self.assertEqual(joblisting_response.status_code, status.HTTP_200_OK) self.assertEqual(len(joblisting_response.json()["results"]), 3) @@ -119,9 +119,23 @@ def test_before_visible(self): joblisting = Joblisting.objects.all().first() joblisting.visible_from = timezone.now() + timedelta(days=2) joblisting.save() - joblisting_response = self.client.get(_get_detail_url(1)) + joblisting_response = self.client.get(_get_detail_url(1), {"timeFilter": True}) self.assertEqual(joblisting_response.status_code, status.HTTP_404_NOT_FOUND) + def test_with_company_query_param(self): + company_pk = 1 + joblisting_response = self.client.get(_get_list_url(), {"company": company_pk}) + self.assertEqual(joblisting_response.status_code, status.HTTP_200_OK) + self.assertEqual(len(joblisting_response.json()["results"]), 1) + self.assertEqual( + joblisting_response.json()["results"][0]["company"]["id"], company_pk + ) + + def test_without_time_filter(self): + joblisting_response = self.client.get(_get_list_url()) + self.assertEqual(joblisting_response.status_code, status.HTTP_200_OK) + self.assertEqual(len(joblisting_response.json()["results"]), 5) + class RetrieveJoblistingsTestCase(BaseAPITestCase): fixtures = [ diff --git a/lego/apps/joblistings/views.py b/lego/apps/joblistings/views.py index 3aa041626..94c731834 100644 --- a/lego/apps/joblistings/views.py +++ b/lego/apps/joblistings/views.py @@ -31,8 +31,10 @@ def get_object(self) -> Joblisting: self.check_object_permissions(self.request, obj) except PermissionError: raise Http404 from None + if obj.visible_from > timezone.now() and self.request.user != obj.created_by: raise Http404 from None + return obj def get_serializer_class(self): @@ -45,8 +47,12 @@ def get_serializer_class(self): return JoblistingSerializer def get_queryset(self): - if self.action == "list": - return Joblisting.objects.filter( + queryset = Joblisting.objects.all() + + time_filter = self.request.query_params.get("timeFilter", False) + if time_filter: + queryset = queryset.filter( visible_from__lte=timezone.now(), visible_to__gte=timezone.now() ) - return Joblisting.objects.all() + + return queryset diff --git a/lego/apps/lending/filters.py b/lego/apps/lending/filters.py index 798503433..215d56564 100644 --- a/lego/apps/lending/filters.py +++ b/lego/apps/lending/filters.py @@ -15,5 +15,5 @@ class Meta: fields = [ "lendable_object", "start_date", - "pending", + "status", ] diff --git a/lego/apps/lending/fixtures/development_lendable_objects.yaml b/lego/apps/lending/fixtures/development_lendable_objects.yaml index 473c9a05f..cbf72d649 100644 --- a/lego/apps/lending/fixtures/development_lendable_objects.yaml +++ b/lego/apps/lending/fixtures/development_lendable_objects.yaml @@ -5,8 +5,8 @@ image: test_event_cover.png has_contract: False max_lending_period: "2 days" - # responsible_groups: - # - - Webkom + responsible_groups: + - - Webkom # responsible_roles: # - member created_by: 3 @@ -17,6 +17,7 @@ description: "En soundboks til å sondbokse" location: "It lageret" # image: "test_event_cover.png" + created_by: 3 model: lending.LendableObject pk: 2 - fields: @@ -24,5 +25,6 @@ description: "" location: "" # image: "test_event_covet.png" + created_by: 3 model: lending.LendableObject pk: 3 \ No newline at end of file diff --git a/lego/apps/lending/fixtures/development_lending_requests.yaml b/lego/apps/lending/fixtures/development_lending_requests.yaml index 9724fe3b3..7063cc881 100644 --- a/lego/apps/lending/fixtures/development_lending_requests.yaml +++ b/lego/apps/lending/fixtures/development_lending_requests.yaml @@ -3,8 +3,8 @@ created_by: 3 start_date: '2024-02-02T23:16:00+00:00' end_date: '2024-02-02T23:18:00+00:00' - comment: "Jeg vil låne grill :)" - pending: True # Should be a status field with multiple options + message: "Jeg vil låne grill :)" + status: "PENDING" model: lending.LendingInstance pk: 1 - fields: @@ -12,6 +12,6 @@ created_by: 3 start_date: '2024-02-02T23:16:00+00:00' end_date: '2024-02-02T23:18:00+00:00' - pending: True + status: "PENDING" model: lending.LendingInstance pk: 2 diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index f131aa6cf..179357324 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -3,8 +3,8 @@ class LendingInstanceManager(BasisModelManager): def create(self, *args, **kwargs): + from lego.apps.lending.notifications import LendingInstanceCreateNotification from lego.apps.users.models import Membership, User - from lego.apps.lending.notifications import LendingInstanceNotification lending_instance = super().create(*args, **kwargs) abakus_groups = lending_instance.lendable_object.responsible_groups.all() @@ -19,7 +19,7 @@ def create(self, *args, **kwargs): ).values_list("user", flat=True) for user_id in users_to_be_notified: user = User.objects.get(pk=user_id) - notification = LendingInstanceNotification( + notification = LendingInstanceCreateNotification( lending_instance=lending_instance, user=user, ) @@ -27,20 +27,5 @@ def create(self, *args, **kwargs): return lending_instance - #TODO: We probably need another template for accepting instances - def update(self, *args, **kwargs): - from lego.apps.lending.notifications import LendingInstanceNotification - - lending_instance = super().update(*args, **kwargs) - notification = LendingInstanceNotification( - lending_instance=lending_instance, - user=lending_instance.created_by, - ) - notification.notify() - - return lending_instance - - def get_queryset(self): return super().get_queryset().select_related("created_by") - diff --git a/lego/apps/lending/migrations/0001_initial.py b/lego/apps/lending/migrations/0001_initial.py index f226fc45a..26c675ded 100644 --- a/lego/apps/lending/migrations/0001_initial.py +++ b/lego/apps/lending/migrations/0001_initial.py @@ -1,60 +1,260 @@ -# Generated by Django 4.0.10 on 2023-10-04 19:54 +# Generated by Django 4.0.10 on 2024-05-24 11:03 -from django.conf import settings -from django.db import migrations, models +import datetime + +import django.contrib.postgres.fields import django.db.models.deletion import django.utils.timezone +from django.conf import settings +from django.db import migrations, models +import lego.apps.files.models +import lego.apps.lending.validators -class Migration(migrations.Migration): +class Migration(migrations.Migration): initial = True dependencies = [ - ('users', '0041_user_linkedin_id_alter_user_github_username'), + ("users", "0043_alter_abakusgroup_type"), + ("files", "0005_file_save_for_use"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='LendableObject', + name="LendableObject", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now, editable=False)), - ('updated_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), - ('deleted', models.BooleanField(db_index=True, default=False, editable=False)), - ('title', models.CharField(max_length=128)), - ('description', models.TextField(blank=True)), - ('has_contract', models.BooleanField(default=False)), - ('max_lending_period', models.DurationField(null=True)), - ('responsible_role', models.CharField(choices=[('member', 'member'), ('leader', 'leader'), ('co-leader', 'co-leader'), ('treasurer', 'treasurer'), ('recruiting', 'recruiting'), ('development', 'development'), ('editor', 'editor'), ('retiree', 'retiree'), ('media_relations', 'media_relations'), ('active_retiree', 'active_retiree'), ('alumni', 'alumni'), ('webmaster', 'webmaster'), ('interest_group_admin', 'interest_group_admin'), ('alumni_admin', 'alumni_admin'), ('retiree_email', 'retiree_email'), ('company_admin', 'company_admin'), ('dugnad_admin', 'dugnad_admin'), ('trip_admin', 'trip_admin'), ('sponsor_admin', 'sponsor_admin'), ('social_admin', 'social_admin')], default='member', max_length=30)), - ('created_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), - ('responsible_groups', models.ManyToManyField(to='users.abakusgroup')), - ('updated_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "updated_at", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "deleted", + models.BooleanField(db_index=True, default=False, editable=False), + ), + ("require_auth", models.BooleanField(default=True)), + ("title", models.CharField(max_length=128)), + ("description", models.TextField(blank=True)), + ("has_contract", models.BooleanField(default=False)), + ( + "max_lending_period", + models.DurationField(default=datetime.timedelta(days=7), null=True), + ), + ( + "responsible_roles", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("member", "member"), + ("leader", "leader"), + ("co-leader", "co-leader"), + ("treasurer", "treasurer"), + ("recruiting", "recruiting"), + ("development", "development"), + ("editor", "editor"), + ("retiree", "retiree"), + ("media_relations", "media_relations"), + ("active_retiree", "active_retiree"), + ("alumni", "alumni"), + ("webmaster", "webmaster"), + ("interest_group_admin", "interest_group_admin"), + ("alumni_admin", "alumni_admin"), + ("retiree_email", "retiree_email"), + ("company_admin", "company_admin"), + ("dugnad_admin", "dugnad_admin"), + ("trip_admin", "trip_admin"), + ("sponsor_admin", "sponsor_admin"), + ("social_admin", "social_admin"), + ], + max_length=30, + ), + default=["member"], + size=None, + validators=[ + lego.apps.lending.validators.responsible_roles_validator + ], + ), + ), + ("location", models.CharField(blank=True, max_length=128)), + ( + "can_edit_groups", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "can_edit_users", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "can_view_groups", + models.ManyToManyField( + blank=True, + related_name="can_view_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "image", + lego.apps.files.models.FileField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="lendable_object_images", + to="files.file", + ), + ), + ("responsible_groups", models.ManyToManyField(to="users.abakusgroup")), + ( + "updated_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, - 'default_manager_name': 'objects', + "abstract": False, }, ), migrations.CreateModel( - name='LendingInstance', + name="LendingInstance", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(db_index=True, default=django.utils.timezone.now, editable=False)), - ('updated_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), - ('deleted', models.BooleanField(db_index=True, default=False, editable=False)), - ('start_date', models.DateTimeField(null=True)), - ('end_date', models.DateTimeField(null=True)), - ('pending', models.BooleanField(default=True)), - ('created_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), - ('lendable_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lending.lendableobject')), - ('updated_by', models.ForeignKey(default=None, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated', to=settings.AUTH_USER_MODEL)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "updated_at", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "deleted", + models.BooleanField(db_index=True, default=False, editable=False), + ), + ("require_auth", models.BooleanField(default=True)), + ("start_date", models.DateTimeField(null=True)), + ("end_date", models.DateTimeField(null=True)), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("ACCEPTED", "Accepted"), + ("REJECTED", "Rejected"), + ], + default="PENDING", + max_length=8, + ), + ), + ("message", models.TextField(blank=True)), + ( + "can_edit_groups", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "can_edit_users", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "can_view_groups", + models.ManyToManyField( + blank=True, + related_name="can_view_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "lendable_object", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="lending.lendableobject", + ), + ), + ( + "updated_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, - 'default_manager_name': 'objects', + "abstract": False, }, ), ] diff --git a/lego/apps/lending/migrations/0002_lendableobject_location.py b/lego/apps/lending/migrations/0002_lendableobject_location.py deleted file mode 100644 index c7c411312..000000000 --- a/lego/apps/lending/migrations/0002_lendableobject_location.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.10 on 2023-10-19 18:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('lending', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='lendableobject', - name='location', - field=models.CharField(blank=True, max_length=128), - ), - ] diff --git a/lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py b/lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py deleted file mode 100644 index 0f4c1ab1a..000000000 --- a/lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.0.10 on 2024-02-06 20:54 - -import datetime -from django.conf import settings -import django.contrib.postgres.fields -from django.db import migrations, models -import django.db.models.deletion -import lego.apps.lending.validators - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('lending', '0002_lendableobject_location'), - ] - - operations = [ - migrations.RemoveField( - model_name='lendableobject', - name='responsible_role', - ), - migrations.AddField( - model_name='lendableobject', - name='responsible_roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('member', 'member'), ('leader', 'leader'), ('co-leader', 'co-leader'), ('treasurer', 'treasurer'), ('recruiting', 'recruiting'), ('development', 'development'), ('editor', 'editor'), ('retiree', 'retiree'), ('media_relations', 'media_relations'), ('active_retiree', 'active_retiree'), ('alumni', 'alumni'), ('webmaster', 'webmaster'), ('interest_group_admin', 'interest_group_admin'), ('alumni_admin', 'alumni_admin'), ('retiree_email', 'retiree_email'), ('company_admin', 'company_admin'), ('dugnad_admin', 'dugnad_admin'), ('trip_admin', 'trip_admin'), ('sponsor_admin', 'sponsor_admin'), ('social_admin', 'social_admin')], max_length=30), default=['member'], size=None, validators=[lego.apps.lending.validators.responsible_roles_validator]), - ), - migrations.AlterField( - model_name='lendableobject', - name='max_lending_period', - field=models.DurationField(default=datetime.timedelta(days=7), null=True), - ), - migrations.AlterField( - model_name='lendinginstance', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lendinginstances', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/lego/apps/lending/migrations/0004_lendableobject_image.py b/lego/apps/lending/migrations/0004_lendableobject_image.py deleted file mode 100644 index 9f3f05798..000000000 --- a/lego/apps/lending/migrations/0004_lendableobject_image.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.0.10 on 2024-02-20 19:45 - -from django.db import migrations -import django.db.models.deletion -import lego.apps.files.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('files', '0005_file_save_for_use'), - ('lending', '0003_remove_lendableobject_responsible_role_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='lendableobject', - name='image', - field=lego.apps.files.models.FileField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendable_object_images', to='files.file'), - ), - ] diff --git a/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py b/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py deleted file mode 100644 index f3e8d528e..000000000 --- a/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.0.10 on 2024-02-27 18:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('lending', '0004_lendableobject_image'), - ] - - operations = [ - migrations.RemoveField( - model_name='lendinginstance', - name='user', - ), - ] diff --git a/lego/apps/lending/migrations/0006_lendinginstance_comment.py b/lego/apps/lending/migrations/0006_lendinginstance_comment.py deleted file mode 100644 index 460831b78..000000000 --- a/lego/apps/lending/migrations/0006_lendinginstance_comment.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.10 on 2024-03-05 20:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('lending', '0005_remove_lendinginstance_user'), - ] - - operations = [ - migrations.AddField( - model_name='lendinginstance', - name='comment', - field=models.TextField(blank=True), - ), - ] diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index 4867851f8..827d5b935 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -1,20 +1,19 @@ -from datetime import datetime, timedelta, timezone, tzinfo +from datetime import timedelta, timezone +from django.contrib.postgres.fields import ArrayField from django.db import models -from lego.apps.files.fields import ImageField + from lego.apps.files.models import FileField -from lego.apps.lending.permissions import LendingInstancePermissionHandler +from lego.apps.lending.managers import LendingInstanceManager +from lego.apps.lending.permissions import ( + LendableObjectPermissionHandler, + LendingInstancePermissionHandler, +) from lego.apps.lending.validators import responsible_roles_validator -from django.contrib.postgres.fields import ArrayField from lego.apps.permissions.models import ObjectPermissionsModel from lego.apps.users import constants -from lego.apps.lending.managers import LendingInstanceManager -from lego.apps.users.models import User - - from lego.utils.models import BasisModel - # Create your models here. @@ -31,9 +30,9 @@ class LendableObject(BasisModel, ObjectPermissionsModel): max_length=30, choices=constants.ROLES, ), - default=list([constants.MEMBER]), + default=[constants.MEMBER], validators=[responsible_roles_validator], - ) + ) image = FileField(related_name="lendable_object_images") location = models.CharField(max_length=128, null=False, blank=True) @@ -43,16 +42,30 @@ def get_furthest_booking_date(self): class Meta: abstract = False + permission_handler = LendableObjectPermissionHandler() class LendingInstance(BasisModel, ObjectPermissionsModel): - lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) + lendable_object = models.ForeignKey( + LendableObject, on_delete=models.CASCADE, unique=False + ) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) - pending = models.BooleanField(default=True) - comment = models.TextField(null=False, blank=True) - objects = LendingInstanceManager() # type: ignore + class LendingInstanceStatus(models.TextChoices): + PENDING = "PENDING" + ACCEPTED = "ACCEPTED" + REJECTED = "REJECTED" + + status = models.CharField( + max_length=8, + choices=LendingInstanceStatus.choices, + default=LendingInstanceStatus.PENDING, + ) + + message = models.TextField(null=False, blank=True) + + objects = LendingInstanceManager() @property def active(self): diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py index ab5bb5d57..b6530c950 100644 --- a/lego/apps/lending/notifications.py +++ b/lego/apps/lending/notifications.py @@ -4,12 +4,11 @@ from lego.apps.users.models import User -class LendingInstanceNotification(Notification): +class LendingInstanceCreateNotification(Notification): def __init__(self, lending_instance: LendingInstance, user: User): self.lending_instance = lending_instance self.lender = lending_instance.user - # TODO: Might not work super().__init__(user=user) name = LENDING_INSTANCE_CREATION diff --git a/lego/apps/lending/permissions.py b/lego/apps/lending/permissions.py index 7bf9e0cef..197a25669 100644 --- a/lego/apps/lending/permissions.py +++ b/lego/apps/lending/permissions.py @@ -1,12 +1,14 @@ +from django.db.models import Q + from lego.apps.permissions.constants import CREATE, DELETE, EDIT, LIST, VIEW from lego.apps.permissions.permissions import PermissionHandler -from django.db.models import Q -class LendingInstancePermissionHandler(PermissionHandler): - force_object_permission_check = True +class LendableObjectPermissionHandler(PermissionHandler): + force_object_permission_check = False authentication_map = {LIST: False, VIEW: False} default_require_auth = True + default_keyword_permission = "/sudo/admin/lendingobject/{perm}/" def has_perm( self, @@ -17,11 +19,54 @@ def has_perm( check_keyword_permissions=True, **kwargs ): + if not user.is_authenticated: + return False + if perm == LIST: return True + # Check object permissions before keywork perms + if obj is not None: + return self.has_object_permissions(user, perm, obj) + + has_perm = super().has_perm( + user, perm, obj, queryset, check_keyword_permissions, **kwargs + ) + + if has_perm: + return True + + return False + + def has_object_permissions(self, user, perm, obj): + if perm == DELETE: + return False + if perm == EDIT and obj.created_by == user: + return True + return True + + +class LendingInstancePermissionHandler(PermissionHandler): + force_object_permission_check = True + authentication_map = {LIST: False, VIEW: False} + default_require_auth = True + force_queryset_filtering = True + default_keyword_permission = "/sudo/admin/lendinginstance/{perm}/" + + def has_perm( + self, + user, + perm, + obj=None, + queryset=None, + check_keyword_permissions=True, + **kwargs + ): if not user.is_authenticated: return False + if perm == LIST: + return True + # Check object permissions before keywork perms if obj is not None: if self.has_object_permissions(user, perm, obj): @@ -47,14 +92,13 @@ def has_object_permissions(self, user, perm, obj): return False if perm == EDIT and obj.created_by == user: return True - if perm == CREATE: - return True return not (perm == DELETE or perm == EDIT) def filter_queryset(self, user, queryset, **kwargs): if user.is_authenticated: + user_groups = user.abakus_groups.all() return queryset.filter( - Q(created_by=user) | - Q(lendable_object__responsible_groups__in=user.abakus_groups) + Q(created_by=user) + | Q(lendable_object__responsible_groups__in=user_groups) ).distinct() return queryset.none() diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 922deb493..baef61fca 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,17 +1,33 @@ from rest_framework import serializers -from lego.apps.files.fields import ImageField - +from lego.apps.files.fields import ImageField from lego.apps.lending.models import LendableObject, LendingInstance from lego.apps.users.serializers.users import PublicUserSerializer -from lego.utils.serializers import BasisModelSerializer, ObjectPermissionsSerializerMixin +from lego.utils.serializers import ( + BasisModelSerializer, + ObjectPermissionsSerializerMixin, +) class DetailedLendableObjectSerializer(BasisModelSerializer): image = ImageField(required=False, options={"height": 500}) + class Meta: model = LendableObject - fields = "__all__" + fields = ( + "id", + "title", + "description", + "has_contract", + "max_lending_period", + "responsible_groups", + "responsible_roles", + "image", + "location", + "created_by", + "updated_by", + ) + class DetailedAdminLendableObjectSerializer( ObjectPermissionsSerializerMixin, DetailedLendableObjectSerializer @@ -24,8 +40,44 @@ class Meta: ) +class LendingInstanceCreateAndUpdateSerializer(BasisModelSerializer): + class Meta: + model = LendingInstance + fields = ( + "id", + "lendable_object", + "start_date", + "end_date", + "message", + ) + + def validate(self, data): + # Check if 'lendable_object', 'start_date', and 'end_date' are provided in the data. + lendable_object = data.get("lendable_object") + start_date = data.get("start_date") + end_date = data.get("end_date") + + # Ensure all necessary fields are present + if lendable_object and start_date and end_date: + user = self.context["request"].user + # Check if the user is in one of the responsible groups + if not user.abakus_groups.filter( + id__in=lendable_object.responsible_groups.values_list("id", flat=True) + ).exists(): + # Calculate the lending period and compare + lending_period = end_date - start_date + max_lending_period = lendable_object.max_lending_period + if lending_period > max_lending_period: + raise serializers.ValidationError( + "Lending period exceeds maximum allowed duration" + ) + + return data + + class DetailedLendingInstanceSerializer(BasisModelSerializer): author = PublicUserSerializer(read_only=True, source="created_by") + lendable_object = DetailedLendableObjectSerializer() class Meta: model = LendingInstance @@ -35,8 +87,8 @@ class Meta: "start_date", "end_date", "author", - "pending", - "comment", + "status", + "message", "created_at", ) @@ -63,6 +115,7 @@ def validate(self, data): return data + class DetailedAdminLendingInstanceSerializer( ObjectPermissionsSerializerMixin, DetailedLendingInstanceSerializer ): diff --git a/lego/apps/lending/tests.py b/lego/apps/lending/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/lego/apps/lending/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/lego/apps/lending/urls.py b/lego/apps/lending/urls.py new file mode 100644 index 000000000..16fb4d2bf --- /dev/null +++ b/lego/apps/lending/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from .views import LendableObjectViewSet + +urlpatterns = [ + path( + "lendableobject//lendinginstances/", + LendableObjectViewSet.as_view({"get": "lendingInstance"}), + name="lendableobject-lendinginstance", + ), +] diff --git a/lego/apps/lending/validators.py b/lego/apps/lending/validators.py index be10e994d..c2d0d80d2 100644 --- a/lego/apps/lending/validators.py +++ b/lego/apps/lending/validators.py @@ -3,4 +3,4 @@ def responsible_roles_validator(value): if len(value) != len(set(value)): - raise ValidationError("Duplicate values are not allowed.") \ No newline at end of file + raise ValidationError("Duplicate values are not allowed.") diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index c1f0df2c9..bb78437c4 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -1,22 +1,13 @@ -from rest_framework import ( - decorators, - exceptions, - mixins, - permissions, - renderers, - status, - viewsets, -) +from rest_framework import decorators, permissions, status, viewsets from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet from lego.apps.lending.filters import LendableObjectFilterSet, LendingInstanceFilterSet from lego.apps.lending.models import LendableObject, LendingInstance from lego.apps.lending.serializers import ( + DetailedAdminLendableObjectSerializer, DetailedLendableObjectSerializer, DetailedLendingInstanceSerializer, - DetailedAdminLendableObjectSerializer, - DetailedAdminLendingInstanceSerializer + LendingInstanceCreateAndUpdateSerializer, ) from lego.apps.permissions.api.views import AllowedPermissionsMixin from lego.apps.permissions.utils import get_permission_handler @@ -36,37 +27,53 @@ def get_serializer_class(self): return DetailedAdminLendableObjectSerializer return super().get_serializer_class() + @decorators.action( + detail=True, methods=["get"], permission_classes=[permissions.IsAuthenticated] + ) + def lendingInstance(self, request, pk=None): + try: + lendable_object = LendableObject.objects.get(pk=pk) + except LendableObject.DoesNotExist: + return Response( + {"error": "LendableObject not found"}, status=status.HTTP_404_NOT_FOUND + ) + permission_handler = get_permission_handler(LendingInstance) + lending_instances = permission_handler.filter_queryset( + self.request.user, + LendingInstance.objects.filter(lendable_object=lendable_object), + ) + serializer = DetailedLendingInstanceSerializer( + lending_instances, many=True, context={"request": request} + ) + return Response(serializer.data) + class LendingInstanceViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): - queryset = LendingInstance.objects.all() serializer_class = DetailedLendingInstanceSerializer filterset_class = LendingInstanceFilterSet http_method_names = ["get", "post", "patch", "delete", "head", "options"] permission_classes = [ permissions.IsAuthenticated, ] - + def get_queryset(self): if self.request is None: return LendingInstance.objects.none() - permission_handler = get_permission_handler(LendingInstance) return permission_handler.filter_queryset( self.request.user, + LendingInstance.objects.prefetch_related("lendable_object"), ) def get_serializer_class(self): if self.request and self.request.user.is_authenticated: - return DetailedAdminLendingInstanceSerializer + return DetailedLendingInstanceSerializer return super().get_serializer_class() - def create(self, request): - serializer = DetailedLendingInstanceSerializer( + serializer = LendingInstanceCreateAndUpdateSerializer( data=request.data, context={"request": request} ) if serializer.is_valid(raise_exception=True): serializer.save() return Response(data=serializer.data, status=status.HTTP_201_CREATED) - - return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py b/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py index eb004aa0f..fb8f8f944 100644 --- a/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py +++ b/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py @@ -4,15 +4,37 @@ class Migration(migrations.Migration): - dependencies = [ - ('notifications', '0014_alter_notificationsetting_notification_type'), + ("notifications", "0014_alter_notificationsetting_notification_type"), ] operations = [ migrations.AlterField( - model_name='notificationsetting', - name='notification_type', - field=models.CharField(choices=[('announcement', 'announcement'), ('restricted_mail_sent', 'restricted_mail_sent'), ('weekly_mail', 'weekly_mail'), ('event_bump', 'event_bump'), ('event_admin_registration', 'event_admin_registration'), ('event_admin_unregistration', 'event_admin_unregistration'), ('event_payment_overdue', 'event_payment_overdue'), ('meeting_invite', 'meeting_invite'), ('meeting_invitation_reminder', 'meeting_invitation_reminder'), ('penalty_creation', 'penalty_creation'), ('comment', 'comment'), ('comment_reply', 'comment_reply'), ('registration_reminder', 'registration_reminder'), ('survey_created', 'survey_created'), ('event_payment_overdue_creator', 'event_payment_overdue_creator'), ('company_interest_created', 'company_interest_created'), ('inactive_warning', 'inactive_warning'), ('deleted_warning', 'deleted_warning'), ('lending_instance_creation', 'lending_instance_creation')], max_length=64), + model_name="notificationsetting", + name="notification_type", + field=models.CharField( + choices=[ + ("announcement", "announcement"), + ("restricted_mail_sent", "restricted_mail_sent"), + ("weekly_mail", "weekly_mail"), + ("event_bump", "event_bump"), + ("event_admin_registration", "event_admin_registration"), + ("event_admin_unregistration", "event_admin_unregistration"), + ("event_payment_overdue", "event_payment_overdue"), + ("meeting_invite", "meeting_invite"), + ("meeting_invitation_reminder", "meeting_invitation_reminder"), + ("penalty_creation", "penalty_creation"), + ("comment", "comment"), + ("comment_reply", "comment_reply"), + ("registration_reminder", "registration_reminder"), + ("survey_created", "survey_created"), + ("event_payment_overdue_creator", "event_payment_overdue_creator"), + ("company_interest_created", "company_interest_created"), + ("inactive_warning", "inactive_warning"), + ("deleted_warning", "deleted_warning"), + ("lending_instance_creation", "lending_instance_creation"), + ], + max_length=64, + ), ), ] diff --git a/lego/apps/notifications/migrations/0016_alter_notificationsetting_channels.py b/lego/apps/notifications/migrations/0016_alter_notificationsetting_channels.py new file mode 100644 index 000000000..c1105b13c --- /dev/null +++ b/lego/apps/notifications/migrations/0016_alter_notificationsetting_channels.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.10 on 2024-03-12 18:14 + +import django.contrib.postgres.fields +from django.db import migrations, models + +import lego.apps.notifications.models + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0015_announcement_exclude_waiting_list_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="notificationsetting", + name="channels", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[("email", "email"), ("push", "push")], max_length=64 + ), + blank=True, + default=lego.apps.notifications.models._default_channels, + null=True, + size=None, + ), + ), + ] diff --git a/lego/apps/notifications/migrations/0016_merge_20240312_1850.py b/lego/apps/notifications/migrations/0016_merge_20240312_1850.py new file mode 100644 index 000000000..05ad2ea8e --- /dev/null +++ b/lego/apps/notifications/migrations/0016_merge_20240312_1850.py @@ -0,0 +1,12 @@ +# Generated by Django 4.0.10 on 2024-03-12 18:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0015_alter_notificationsetting_notification_type"), + ("notifications", "0015_announcement_exclude_waiting_list_and_more"), + ] + + operations = [] diff --git a/lego/apps/notifications/migrations/0017_merge_20240423_1728.py b/lego/apps/notifications/migrations/0017_merge_20240423_1728.py new file mode 100644 index 000000000..111aea846 --- /dev/null +++ b/lego/apps/notifications/migrations/0017_merge_20240423_1728.py @@ -0,0 +1,12 @@ +# Generated by Django 4.0.10 on 2024-04-23 17:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0016_alter_notificationsetting_channels"), + ("notifications", "0016_merge_20240312_1850"), + ] + + operations = [] diff --git a/lego/apps/notifications/models.py b/lego/apps/notifications/models.py index 87e7598df..96ff2ecf6 100644 --- a/lego/apps/notifications/models.py +++ b/lego/apps/notifications/models.py @@ -31,6 +31,7 @@ class NotificationSetting(models.Model): models.CharField(max_length=64, choices=CHANNEL_CHOICES), default=_default_channels, null=True, + blank=True, ) class Meta: diff --git a/lego/apps/notifications/notification.py b/lego/apps/notifications/notification.py index 38cbf8887..abc62d03a 100644 --- a/lego/apps/notifications/notification.py +++ b/lego/apps/notifications/notification.py @@ -1,6 +1,6 @@ from structlog import get_logger -from lego.apps.users.models import User +from lego.apps.users.models import User from lego.utils.content_types import instance_to_string from lego.utils.tasks import send_email, send_push diff --git a/lego/apps/search/backend.py b/lego/apps/search/backend.py index 394fef96c..33451ebde 100644 --- a/lego/apps/search/backend.py +++ b/lego/apps/search/backend.py @@ -1,7 +1,7 @@ from . import registry -class SearchBacked: +class SearchBackend: """ Base class for search backends. A backend needs to implement all methods on this class to work like it should. diff --git a/lego/apps/search/backends/elasticsearch.py b/lego/apps/search/backends/elasticsearch.py index 212480136..86b14104d 100644 --- a/lego/apps/search/backends/elasticsearch.py +++ b/lego/apps/search/backends/elasticsearch.py @@ -6,10 +6,10 @@ from elasticsearch import Elasticsearch, NotFoundError from elasticsearch.helpers import bulk -from lego.apps.search.backend import SearchBacked +from lego.apps.search.backend import SearchBackend -class ElasticsearchBackend(SearchBacked): +class ElasticsearchBackend(SearchBackend): name = "elasticsearch" connection = None diff --git a/lego/apps/search/backends/postgres.py b/lego/apps/search/backends/postgres.py index 98cec12fd..bd0a39a1a 100644 --- a/lego/apps/search/backends/postgres.py +++ b/lego/apps/search/backends/postgres.py @@ -1,9 +1,9 @@ -from lego.apps.search.backend import SearchBacked +from lego.apps.search.backend import SearchBackend from .. import registry -class PostgresBackend(SearchBacked): +class PostgresBackend(SearchBackend): name = "postgres" max_results = 10 diff --git a/lego/apps/stats/analytics_client.py b/lego/apps/stats/analytics_client.py index e7da44a12..0a20c1026 100644 --- a/lego/apps/stats/analytics_client.py +++ b/lego/apps/stats/analytics_client.py @@ -20,6 +20,9 @@ def setup_analytics(): write_key = getattr(settings, "ANALYTICS_WRITE_KEY", "") host = getattr(settings, "ANALYTICS_HOST", "https://api.segment.io") + if write_key == "": + return + production = getattr(settings, "ENVIRONMENT_NAME", None) == "production" send = not (development or getattr(settings, "TESTING", False)) or production @@ -36,6 +39,9 @@ def setup_analytics(): def _proxy(method, user, *args, **kwargs): global default_client, development + if default_client is None: + return + fn = getattr(default_client, method) kwargs["context"] = { diff --git a/lego/apps/tags/tests/test_views.py b/lego/apps/tags/tests/test_views.py index a9f22313c..16e3d1ec8 100644 --- a/lego/apps/tags/tests/test_views.py +++ b/lego/apps/tags/tests/test_views.py @@ -13,9 +13,7 @@ def test_fetch_popular(self): def test_fetch_list(self): response = self.client.get("/api/v1/tags/") self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertDictEqual( - response.json()["results"][0], {"tag": "ababrygg", "usages": 0} - ) + self.assertDictEqual(response.json()[0], {"tag": "ababrygg", "usages": 0}) def test_fetch_detail(self): response = self.client.get("/api/v1/tags/ababrygg/") diff --git a/lego/apps/tags/views.py b/lego/apps/tags/views.py index ea54def28..7fe3d4829 100644 --- a/lego/apps/tags/views.py +++ b/lego/apps/tags/views.py @@ -12,10 +12,11 @@ class TagViewSet( RetrieveModelMixin, viewsets.GenericViewSet, ): - queryset = Tag.objects.all() + queryset = Tag.objects.all().order_by("tag") ordering = "tag" serializer_class = TagListSerializer permission_classes = [permissions.AllowAny] + pagination_class = None def get_serializer_class(self): if self.action != "list": diff --git a/lego/apps/users/constants.py b/lego/apps/users/constants.py index 7f10738de..899ff7242 100644 --- a/lego/apps/users/constants.py +++ b/lego/apps/users/constants.py @@ -1,5 +1,7 @@ from enum import Enum +from django.db import models + MALE = "male" FEMALE = "female" OTHER = "other" @@ -95,13 +97,12 @@ def values(cls) -> list[str]: FSGroup.MSSECCLO: FOURTH_GRADE_KOMTEK, } -STUDENT_EMAIL_DOMAIN = "stud.ntnu.no" - GROUP_COMMITTEE = "komite" GROUP_INTEREST = "interesse" GROUP_BOARD = "styre" GROUP_REVUE = "revy" GROUP_SUB = "under" +GROUP_ORDAINED = "ordenen" GROUP_GRADE = "klasse" GROUP_OTHER = "annen" GROUP_TYPES = ( @@ -112,6 +113,7 @@ def values(cls) -> list[str]: (GROUP_GRADE, GROUP_GRADE), (GROUP_OTHER, GROUP_OTHER), (GROUP_SUB, GROUP_SUB), + (GROUP_ORDAINED, GROUP_ORDAINED), ) OPEN_GROUPS = (GROUP_INTEREST,) @@ -142,3 +144,16 @@ def values(cls) -> list[str]: (LIGHT_THEME, LIGHT_THEME), (DARK_THEME, DARK_THEME), ) + + +LATE_PRESENCE_PENALTY_WEIGHT = 1 + + +class PENALTY_WEIGHTS(models.TextChoices): + LATE_PRESENCE = 1 + + +class PENALTY_TYPES(models.TextChoices): + PRESENCE = "presence" + PAYMENT = "payment" + OTHER = "other" diff --git a/lego/apps/users/filters.py b/lego/apps/users/filters.py index 0435a1379..575224c08 100644 --- a/lego/apps/users/filters.py +++ b/lego/apps/users/filters.py @@ -37,6 +37,8 @@ def user__fullname(self, queryset, name, value): class AbakusGroupFilterSet(FilterSet): + type = CharInFilter(field_name="type", lookup_expr="in") + class Meta: model = AbakusGroup fields = ("type",) diff --git a/lego/apps/users/fixtures/initial_abakus_groups.py b/lego/apps/users/fixtures/initial_abakus_groups.py index e2394af7b..12a077b3d 100644 --- a/lego/apps/users/fixtures/initial_abakus_groups.py +++ b/lego/apps/users/fixtures/initial_abakus_groups.py @@ -30,11 +30,12 @@ { "description": "Medlemmer av Abakus", "permissions": [ - "/sudo/admin/meetings/create", - "/sudo/admin/meetinginvitations/create", + "/sudo/admin/meetings/create/", + "/sudo/admin/meetinginvitations/create/", "/sudo/admin/registrations/create/", "/sudo/admin/events/payment/", - "/sudo/admin/comments/create", + "/sudo/admin/comments/create/", + "/sudo/admin/meetings/list/", ], }, { diff --git a/lego/apps/users/fixtures/initial_abakus_groups.yaml b/lego/apps/users/fixtures/initial_abakus_groups.yaml index daf18097f..2ec2811f6 100644 --- a/lego/apps/users/fixtures/initial_abakus_groups.yaml +++ b/lego/apps/users/fixtures/initial_abakus_groups.yaml @@ -30,8 +30,8 @@ logo: null type: annen text: '' - permissions: '["/sudo/admin/meetings/create", "/sudo/admin/meetinginvitations/create", - "/sudo/admin/registrations/create/", "/sudo/admin/events/payment/", "/sudo/admin/comments/create"]' + permissions: '["/sudo/admin/meetings/create/", "/sudo/admin/meetinginvitations/create/", + "/sudo/admin/registrations/create/", "/sudo/admin/events/payment/", "/sudo/admin/comments/create/", "/sudo/admin/meetings/list/"]' show_badge: true active: true lft: 1 diff --git a/lego/apps/users/fixtures/test_abakus_groups.yaml b/lego/apps/users/fixtures/test_abakus_groups.yaml index ef363cd84..89c6ebd34 100644 --- a/lego/apps/users/fixtures/test_abakus_groups.yaml +++ b/lego/apps/users/fixtures/test_abakus_groups.yaml @@ -325,7 +325,7 @@ logo: null type: annen text: '' - permissions: '["/sudo/admin/meetings/create", "/sudo/admin/meetinginvitations/create", + permissions: '["/sudo/admin/meetings/create/", "/sudo/admin/meetinginvitations/create/", "/sudo/admin/registrations/create/", "/sudo/admin/events/payment/"]' show_badge: true active: true diff --git a/lego/apps/users/migrations/0042_penalty_type.py b/lego/apps/users/migrations/0042_penalty_type.py new file mode 100644 index 000000000..7b4f6af4f --- /dev/null +++ b/lego/apps/users/migrations/0042_penalty_type.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.10 on 2024-03-14 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0041_user_linkedin_id_alter_user_github_username"), + ] + + operations = [ + migrations.AddField( + model_name="penalty", + name="type", + field=models.CharField( + choices=[ + ("presence", "Presence"), + ("payment", "Payment"), + ("other", "Other"), + ], + default="other", + max_length=50, + ), + ), + ] diff --git a/lego/apps/users/migrations/0043_alter_abakusgroup_type.py b/lego/apps/users/migrations/0043_alter_abakusgroup_type.py new file mode 100644 index 000000000..bbb5ca237 --- /dev/null +++ b/lego/apps/users/migrations/0043_alter_abakusgroup_type.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.10 on 2024-03-20 00:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0042_penalty_type"), + ] + + operations = [ + migrations.AlterField( + model_name="abakusgroup", + name="type", + field=models.CharField( + choices=[ + ("komite", "komite"), + ("interesse", "interesse"), + ("styre", "styre"), + ("revy", "revy"), + ("klasse", "klasse"), + ("annen", "annen"), + ("under", "under"), + ("ordenen", "ordenen"), + ], + default="annen", + max_length=10, + ), + ), + ] diff --git a/lego/apps/users/models.py b/lego/apps/users/models.py index d5797c3f9..7752a8b8a 100644 --- a/lego/apps/users/models.py +++ b/lego/apps/users/models.py @@ -537,6 +537,11 @@ class Penalty(BasisModel): source_event = models.ForeignKey( "events.Event", related_name="penalties", on_delete=models.CASCADE ) + type = models.CharField( + max_length=50, + choices=constants.PENALTY_TYPES.choices, + default=constants.PENALTY_TYPES.OTHER, + ) objects = UserPenaltyManager() # type: ignore diff --git a/lego/apps/users/tests/test_abakusgroup_api.py b/lego/apps/users/tests/test_abakusgroup_api.py index aedd5272a..f5db93824 100644 --- a/lego/apps/users/tests/test_abakusgroup_api.py +++ b/lego/apps/users/tests/test_abakusgroup_api.py @@ -2,7 +2,7 @@ from rest_framework import status from lego.apps.users import constants -from lego.apps.users.constants import LEADER +from lego.apps.users.constants import GROUP_COMMITTEE, GROUP_INTEREST, LEADER from lego.apps.users.models import AbakusGroup, User from lego.apps.users.serializers.abakus_groups import PublicAbakusGroupSerializer from lego.utils.test_utils import BaseAPITestCase @@ -67,6 +67,15 @@ def test_without_auth(self): def test_with_auth(self): self.successful_list(self.user) + def test_with_filter_type(self): + """Groups can be filtered on multiple types""" + self.client.force_authenticate(self.user) + response = self.client.get( + f"{_get_list_url()}?type={GROUP_COMMITTEE},{GROUP_INTEREST}" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(7, len(response.json()["results"])) + class RetrieveAbakusGroupAPITestCase(BaseAPITestCase): fixtures = ["test_abakus_groups.yaml", "test_users.yaml"] diff --git a/lego/apps/users/tests/test_password_change.py b/lego/apps/users/tests/test_password_change.py index fb610739f..cc94de75c 100644 --- a/lego/apps/users/tests/test_password_change.py +++ b/lego/apps/users/tests/test_password_change.py @@ -47,6 +47,7 @@ def test_new_password_not_valid(self): self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) def test_new_password_success(self): + old_password_hash = self.user.crypt_password_hash self.client.force_authenticate(self.user) response = self.client.post( self.url, @@ -60,3 +61,4 @@ def test_new_password_success(self): self.assertTrue( authenticate(username="test1", password="new_secret_password123") ) + self.assertNotEqual(self.user.crypt_password_hash, old_password_hash) diff --git a/lego/apps/users/tests/test_users_api.py b/lego/apps/users/tests/test_users_api.py index 098a683c3..235a03c45 100644 --- a/lego/apps/users/tests/test_users_api.py +++ b/lego/apps/users/tests/test_users_api.py @@ -232,6 +232,7 @@ def test_with_valid_data(self): self.assertEqual(new_user.is_superuser, False) self.assertEqual(new_user.is_abakus_member, False) self.assertEqual(new_user.is_abakom_member, False) + self.assertNotEqual(new_user.crypt_password_hash, "") # Test member groups user_group = AbakusGroup.objects.get(name=constants.USER_GROUP) diff --git a/lego/apps/users/views/users.py b/lego/apps/users/views/users.py index d7d5a8994..aba848973 100644 --- a/lego/apps/users/views/users.py +++ b/lego/apps/users/views/users.py @@ -116,6 +116,8 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) user = User.objects.create_user(email=token_email, **serializer.validated_data) + user.set_password(serializer.validated_data["password"]) + user.save() user_group = AbakusGroup.objects.get(name=constants.USER_GROUP) user_group.add_user(user) diff --git a/lego/utils/management/commands/load_fixtures.py b/lego/utils/management/commands/load_fixtures.py index 6d955a5b7..09830e126 100644 --- a/lego/utils/management/commands/load_fixtures.py +++ b/lego/utils/management/commands/load_fixtures.py @@ -79,7 +79,7 @@ def run(self, *args, **options): "surveys/fixtures/development_surveys.yaml", "users/fixtures/development_photo_consents.yaml", "lending/fixtures/development_lendable_objects.yaml", - "lending/fixtures/development_lending_requests", + "lending/fixtures/development_lending_requests.yaml", ] )