From d9db172a920b7a30b55a2933ca695f8e2622a2b3 Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Wed, 4 Oct 2023 22:54:07 +0200 Subject: [PATCH 01/29] Implement lending system --- lego/api/v1.py | 5 ++ lego/apps/lending/__init__.py | 0 lego/apps/lending/apps.py | 6 ++ lego/apps/lending/filters.py | 20 +++++++ lego/apps/lending/managers.py | 25 ++++++++ lego/apps/lending/migrations/0001_initial.py | 60 +++++++++++++++++++ lego/apps/lending/migrations/__init__.py | 0 lego/apps/lending/models.py | 42 +++++++++++++ lego/apps/lending/notifications.py | 40 +++++++++++++ lego/apps/lending/serializers.py | 37 ++++++++++++ lego/apps/lending/tests.py | 3 + lego/apps/lending/views.py | 60 +++++++++++++++++++ .../users/email/lending_instance.html | 31 ++++++++++ .../users/email/lending_instance.txt | 14 +++++ .../templates/users/push/lending_instance.txt | 5 ++ lego/settings/base.py | 1 + 16 files changed, 349 insertions(+) create mode 100644 lego/apps/lending/__init__.py create mode 100644 lego/apps/lending/apps.py create mode 100644 lego/apps/lending/filters.py create mode 100644 lego/apps/lending/managers.py create mode 100644 lego/apps/lending/migrations/0001_initial.py create mode 100644 lego/apps/lending/migrations/__init__.py create mode 100644 lego/apps/lending/models.py create mode 100644 lego/apps/lending/notifications.py create mode 100644 lego/apps/lending/serializers.py create mode 100644 lego/apps/lending/tests.py create mode 100644 lego/apps/lending/views.py create mode 100644 lego/apps/users/templates/users/email/lending_instance.html create mode 100644 lego/apps/users/templates/users/email/lending_instance.txt create mode 100644 lego/apps/users/templates/users/push/lending_instance.txt diff --git a/lego/api/v1.py b/lego/api/v1.py index c038b597c..6571bd741 100644 --- a/lego/api/v1.py +++ b/lego/api/v1.py @@ -75,6 +75,8 @@ 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() @@ -141,6 +143,9 @@ basename="abakusgroup-memberships", ) router.register(r"joblistings", JoblistingViewSet, basename="joblisting") +router.register(r"lendableobject", LendableObjectViewSet, basename="lendableobject") +router.register(r"lendinginstance", LendingInstanceViewSet, basename="lendinginstance") + router.register( r"meeting-token", MeetingInvitationTokenViewSet, basename="meeting-token" ) diff --git a/lego/apps/lending/__init__.py b/lego/apps/lending/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/lending/apps.py b/lego/apps/lending/apps.py new file mode 100644 index 000000000..608fc9e03 --- /dev/null +++ b/lego/apps/lending/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LendingConfig(AppConfig): + name = "lego.apps.lending" + verbose_name = "Lending" diff --git a/lego/apps/lending/filters.py b/lego/apps/lending/filters.py new file mode 100644 index 000000000..53773c7fd --- /dev/null +++ b/lego/apps/lending/filters.py @@ -0,0 +1,20 @@ +from django_filters.rest_framework import FilterSet + +from lego.apps.lending.models import LendableObject, LendingInstance + + +class LendableObjectFilterSet(FilterSet): + class Meta: + model = LendableObject + fields = ["title"] + + +class LendingInstanceFilterSet(FilterSet): + class Meta: + model = LendingInstance + fields = [ + "user", + "lendable_object", + "start_date", + "pending", + ] diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py new file mode 100644 index 000000000..e32eea456 --- /dev/null +++ b/lego/apps/lending/managers.py @@ -0,0 +1,25 @@ +from lego.utils.managers import PersistentModelManager + + +class LendingInstanceManager(PersistentModelManager): + def create(self, *args, **kwargs): + 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() + role = lending_instance.lendable_object.responsible_role + + users_to_be_notified = Membership.objects.filter( + abakus_group__in=abakus_groups, role=role + ).values_list("user", flat=True) + + for user_id in users_to_be_notified: + user = User.objects.get(pk=user_id) + notification = LendingInstanceNotification( + lending_instance=lending_instance, + user_email=user, + ) + notification.notify() + + return lending_instance diff --git a/lego/apps/lending/migrations/0001_initial.py b/lego/apps/lending/migrations/0001_initial.py new file mode 100644 index 000000000..f226fc45a --- /dev/null +++ b/lego/apps/lending/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 4.0.10 on 2023-10-04 19:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0041_user_linkedin_id_alter_user_github_username'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + options={ + 'abstract': False, + 'default_manager_name': 'objects', + }, + ), + migrations.CreateModel( + 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)), + ], + options={ + 'abstract': False, + 'default_manager_name': 'objects', + }, + ), + ] diff --git a/lego/apps/lending/migrations/__init__.py b/lego/apps/lending/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py new file mode 100644 index 000000000..9a9d448c3 --- /dev/null +++ b/lego/apps/lending/models.py @@ -0,0 +1,42 @@ +from datetime import datetime, timedelta, timezone, tzinfo + +from django.db import models + +from lego.apps.users import constants +from lego.apps.lending.managers import LendingInstanceManager + + +from lego.utils.models import BasisModel + + +# Create your models here. + + +class LendableObject(BasisModel): + title = models.CharField(max_length=128, null=False, blank=False) + description = models.TextField(null=False, blank=True) + has_contract = models.BooleanField(default=False, null=False, blank=False) + max_lending_period = models.DurationField(null=True, blank=False) + responsible_groups = models.ManyToManyField("users.AbakusGroup") + responsible_role = models.CharField( + max_length=30, choices=constants.ROLES, default=constants.MEMBER + ) + location = models.CharField(max_length=128, null=False, blank=False) + + @property + def get_furthest_booking_date(self): + return timezone.now() + timedelta(days=14) + + +class LendingInstance(BasisModel): + user = models.ForeignKey("users.User", on_delete=models.CASCADE) + lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) + start_date = models.DateTimeField(null=True) + end_date = models.DateTimeField(null=True) + pending = models.BooleanField(default=True) + + objects = LendingInstanceManager() # type: ignore + + @property + def active(self): + return timezone.now() < self.end_date and timezone.now() > self.start_date diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py new file mode 100644 index 000000000..93d956b9c --- /dev/null +++ b/lego/apps/lending/notifications.py @@ -0,0 +1,40 @@ +from lego.apps.lending.models import LendingInstance +from lego.apps.notifications.notification import Notification +from lego.apps.users.models import User + + +class LendingInstanceNotification(Notification): + def __init__(self, lending_instance: LendingInstance, user: User): + self.lending_instance = lending_instance + self.user = user + + # TODO: Might not work + super().__init__(user=lending_instance.user) + + name = "lending_instance_creation" + + def generate_mail(self): + return self._delay_mail( + to_email=self.user.email_address, + context={ + "user": self.user.full_name, + "lendable_object": self.lending_instance.lendable_object.title, + "start_date": self.lending_instance.start_date, + "end_date": self.lending_instance.end_date, + }, + subject="Utlån forespørsel", + plain_template="users/email/lending_instance.txt", + html_template="users/email/lending_instance.html", + ) + + def generate_push(self): + return self._delay_push( + template="users/push/lending_instance.txt", + context={ + "user": self.user.full_name, + "lendable_object": self.lending_instance.lendable_object.title, + "start_date": self.lending_instance.start_date, + "end_date": self.lending_instance.end_date, + }, + instance=self.lending_instance, + ) diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py new file mode 100644 index 000000000..9adc9935f --- /dev/null +++ b/lego/apps/lending/serializers.py @@ -0,0 +1,37 @@ +from rest_framework import serializers + + +from lego.apps.lending.models import LendableObject, LendingInstance + + +class LendableObjectSerializer(serializers.ModelSerializer): + class Meta: + model = LendableObject + fields = "__all__" + + +class LendingInstanceSerializer(serializers.ModelSerializer): + class Meta: + model = LendingInstance + fields = "__all__" + + def validate(self, data): + lendable_object_id = data["lendable_object"].id + lendable_object = LendableObject.objects.get(id=lendable_object_id) + user = self.request.user + + if not user.abakus_groups.filter( + id__in=lendable_object.responsible_groups.all().values_list("id", flat=True) + ).exists(): + if ( + data["end_date"] - data["start_date"] + > lendable_object.max_lending_period + ): + raise serializers.ValidationError( + "Lending period exceeds maximum allowed duration" + ) + + # Add additional validation logic as per your use case + # ... + + return data diff --git a/lego/apps/lending/tests.py b/lego/apps/lending/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/lego/apps/lending/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py new file mode 100644 index 000000000..9f9ef333b --- /dev/null +++ b/lego/apps/lending/views.py @@ -0,0 +1,60 @@ +from rest_framework import ( + decorators, + exceptions, + mixins, + permissions, + renderers, + 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 ( + LendableObjectSerializer, + LendingInstanceSerializer, +) +from lego.apps.permissions.api.views import AllowedPermissionsMixin + + +class LendableObjectViewSet( + AllowedPermissionsMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): + queryset = LendableObject.objects.all() + serializer_class = LendableObjectSerializer + filterset_class = LendableObjectFilterSet + http_method_names = ["get", "post", "patch", "delete", "head", "options"] + permission_classes = [ + permissions.IsAuthenticated, + ] + + +class LendingInstanceViewSet( + AllowedPermissionsMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): + queryset = LendingInstance.objects.all() + serializer_class = LendingInstanceSerializer + filterset_class = LendingInstanceFilterSet + http_method_names = ["get", "post", "patch", "delete", "head", "options"] + permission_classes = [ + permissions.IsAuthenticated, + ] + + def create(self, request): + serializer = LendingInstanceSerializer(request, data=request.data) + + 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/users/templates/users/email/lending_instance.html b/lego/apps/users/templates/users/email/lending_instance.html new file mode 100644 index 000000000..e37243ddc --- /dev/null +++ b/lego/apps/users/templates/users/email/lending_instance.html @@ -0,0 +1,31 @@ +{% extends "email/base.html" %} + +{% block alert %} + + + + Det er en ny forespørsel om å låne {{ lendable_object }}! + + + +{% endblock %} + +{% block content %} + + + + Bruker: {{ user }}! + + + + + + + + Fra: {{ start_date }}, Til: {{ end_date }} + + + + + +{% endblock %} \ No newline at end of file diff --git a/lego/apps/users/templates/users/email/lending_instance.txt b/lego/apps/users/templates/users/email/lending_instance.txt new file mode 100644 index 000000000..1dccb1bd8 --- /dev/null +++ b/lego/apps/users/templates/users/email/lending_instance.txt @@ -0,0 +1,14 @@ +{% extends "email/base.txt" %} + +{% block content %} + +Det er en ny forespørsel om å låne {{ lendable_object }}! + +Bruker: {{ user }}! + +Fra: {{ start_date }}, Til: {{ end_date }} + + + + +{% endblock %} diff --git a/lego/apps/users/templates/users/push/lending_instance.txt b/lego/apps/users/templates/users/push/lending_instance.txt new file mode 100644 index 000000000..f35482044 --- /dev/null +++ b/lego/apps/users/templates/users/push/lending_instance.txt @@ -0,0 +1,5 @@ +Det er en ny forespørsel om å låne {{ lendable_object }}! + +Bruker: {{ user }}! + +Fra: {{ start_date }}, Til: {{ end_date }} diff --git a/lego/settings/base.py b/lego/settings/base.py index b1d301370..d3451c226 100644 --- a/lego/settings/base.py +++ b/lego/settings/base.py @@ -65,6 +65,7 @@ "lego.apps.tags", "lego.apps.users", "lego.apps.websockets", + "lego.apps.lending", ] DEFAULT_AUTO_FIELD = "django.db.models.AutoField" From 10278d809157b3ee8c85004ff5d142afdf0106e0 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Thu, 19 Oct 2023 20:31:09 +0200 Subject: [PATCH 02/29] Fix location field bug --- .../migrations/0002_lendableobject_location.py | 18 ++++++++++++++++++ lego/apps/lending/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 lego/apps/lending/migrations/0002_lendableobject_location.py diff --git a/lego/apps/lending/migrations/0002_lendableobject_location.py b/lego/apps/lending/migrations/0002_lendableobject_location.py new file mode 100644 index 000000000..c7c411312 --- /dev/null +++ b/lego/apps/lending/migrations/0002_lendableobject_location.py @@ -0,0 +1,18 @@ +# 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/models.py b/lego/apps/lending/models.py index 9a9d448c3..475b8feaf 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -21,7 +21,7 @@ class LendableObject(BasisModel): responsible_role = models.CharField( max_length=30, choices=constants.ROLES, default=constants.MEMBER ) - location = models.CharField(max_length=128, null=False, blank=False) + location = models.CharField(max_length=128, null=False, blank=True) @property def get_furthest_booking_date(self): From 69ee849f898858baf20e7b8a8f3424f63a3ef173 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Thu, 19 Oct 2023 20:51:49 +0200 Subject: [PATCH 03/29] Fix other thing --- lego/apps/lending/managers.py | 2 +- lego/apps/lending/models.py | 4 ++++ lego/apps/lending/notifications.py | 8 ++++---- lego/apps/lending/serializers.py | 6 +----- lego/apps/lending/views.py | 3 +-- lego/apps/notifications/notification.py | 3 ++- .../users/templates/users/email/lending_instance.html | 2 +- .../apps/users/templates/users/email/lending_instance.txt | 2 +- lego/apps/users/templates/users/push/lending_instance.txt | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index e32eea456..0ba896719 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -18,7 +18,7 @@ def create(self, *args, **kwargs): user = User.objects.get(pk=user_id) notification = LendingInstanceNotification( lending_instance=lending_instance, - user_email=user, + user=user, ) notification.notify() diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index 475b8feaf..3125c275f 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -21,6 +21,10 @@ class LendableObject(BasisModel): responsible_role = models.CharField( max_length=30, choices=constants.ROLES, default=constants.MEMBER ) + # TODO: options should be changed + image = models.ImageField( + source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} + ) location = models.CharField(max_length=128, null=False, blank=True) @property diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py index 93d956b9c..489cc509c 100644 --- a/lego/apps/lending/notifications.py +++ b/lego/apps/lending/notifications.py @@ -6,10 +6,10 @@ class LendingInstanceNotification(Notification): def __init__(self, lending_instance: LendingInstance, user: User): self.lending_instance = lending_instance - self.user = user + self.lender = lending_instance.user # TODO: Might not work - super().__init__(user=lending_instance.user) + super().__init__(user=user) name = "lending_instance_creation" @@ -17,7 +17,7 @@ def generate_mail(self): return self._delay_mail( to_email=self.user.email_address, context={ - "user": self.user.full_name, + "lender": self.lender, "lendable_object": self.lending_instance.lendable_object.title, "start_date": self.lending_instance.start_date, "end_date": self.lending_instance.end_date, @@ -31,7 +31,7 @@ def generate_push(self): return self._delay_push( template="users/push/lending_instance.txt", context={ - "user": self.user.full_name, + "lender": self.lender, "lendable_object": self.lending_instance.lendable_object.title, "start_date": self.lending_instance.start_date, "end_date": self.lending_instance.end_date, diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 9adc9935f..cd6dc4ccc 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -18,8 +18,7 @@ class Meta: def validate(self, data): lendable_object_id = data["lendable_object"].id lendable_object = LendableObject.objects.get(id=lendable_object_id) - user = self.request.user - + user = self.context['request'].user if not user.abakus_groups.filter( id__in=lendable_object.responsible_groups.all().values_list("id", flat=True) ).exists(): @@ -31,7 +30,4 @@ def validate(self, data): "Lending period exceeds maximum allowed duration" ) - # Add additional validation logic as per your use case - # ... - return data diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index 9f9ef333b..0c29e7601 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -51,8 +51,7 @@ class LendingInstanceViewSet( ] def create(self, request): - serializer = LendingInstanceSerializer(request, data=request.data) - + serializer = LendingInstanceSerializer(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) diff --git a/lego/apps/notifications/notification.py b/lego/apps/notifications/notification.py index 0cdec3bbe..38cbf8887 100644 --- a/lego/apps/notifications/notification.py +++ b/lego/apps/notifications/notification.py @@ -1,4 +1,5 @@ from structlog import get_logger +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 @@ -15,7 +16,7 @@ class Notification: name = None - def __init__(self, user, *args, **kwargs): + def __init__(self, user: User, *args, **kwargs): self.user = user self.args = (args,) self.kwargs = kwargs diff --git a/lego/apps/users/templates/users/email/lending_instance.html b/lego/apps/users/templates/users/email/lending_instance.html index e37243ddc..06bdbd27d 100644 --- a/lego/apps/users/templates/users/email/lending_instance.html +++ b/lego/apps/users/templates/users/email/lending_instance.html @@ -14,7 +14,7 @@ - Bruker: {{ user }}! + Bruker: {{ lender }}! diff --git a/lego/apps/users/templates/users/email/lending_instance.txt b/lego/apps/users/templates/users/email/lending_instance.txt index 1dccb1bd8..59b087f55 100644 --- a/lego/apps/users/templates/users/email/lending_instance.txt +++ b/lego/apps/users/templates/users/email/lending_instance.txt @@ -4,7 +4,7 @@ Det er en ny forespørsel om å låne {{ lendable_object }}! -Bruker: {{ user }}! +Bruker: {{ lender }}! Fra: {{ start_date }}, Til: {{ end_date }} diff --git a/lego/apps/users/templates/users/push/lending_instance.txt b/lego/apps/users/templates/users/push/lending_instance.txt index f35482044..a7d604c5c 100644 --- a/lego/apps/users/templates/users/push/lending_instance.txt +++ b/lego/apps/users/templates/users/push/lending_instance.txt @@ -1,5 +1,5 @@ Det er en ny forespørsel om å låne {{ lendable_object }}! -Bruker: {{ user }}! +Bruker: {{ lender }}! Fra: {{ start_date }}, Til: {{ end_date }} From 79a86201221551cdff1f52205339ad25d4f29064 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Thu, 1 Feb 2024 21:56:38 +0100 Subject: [PATCH 04/29] Fix more --- lego/apps/lending/models.py | 12 +++++++--- lego/apps/lending/notifications.py | 3 ++- lego/apps/lending/serializers.py | 36 ++++++++++++++++++---------- lego/apps/lending/views.py | 20 ++++------------ lego/apps/notifications/constants.py | 5 ++++ 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index 3125c275f..38913babc 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -1,9 +1,11 @@ from datetime import datetime, timedelta, timezone, tzinfo from django.db import models +from lego.apps.files.fields import ImageField 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 @@ -16,13 +18,15 @@ class LendableObject(BasisModel): title = models.CharField(max_length=128, null=False, blank=False) description = models.TextField(null=False, blank=True) has_contract = models.BooleanField(default=False, null=False, blank=False) - max_lending_period = models.DurationField(null=True, blank=False) + max_lending_period = models.DurationField( + null=True, blank=False, default=timedelta(days=7) + ) responsible_groups = models.ManyToManyField("users.AbakusGroup") responsible_role = models.CharField( max_length=30, choices=constants.ROLES, default=constants.MEMBER ) # TODO: options should be changed - image = models.ImageField( + image = ImageField( source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} ) location = models.CharField(max_length=128, null=False, blank=True) @@ -33,7 +37,9 @@ def get_furthest_booking_date(self): class LendingInstance(BasisModel): - user = models.ForeignKey("users.User", on_delete=models.CASCADE) + user = models.ForeignKey( + User, related_name="lendinginstances", on_delete=models.CASCADE + ) lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py index 489cc509c..ab5bb5d57 100644 --- a/lego/apps/lending/notifications.py +++ b/lego/apps/lending/notifications.py @@ -1,4 +1,5 @@ from lego.apps.lending.models import LendingInstance +from lego.apps.notifications.constants import LENDING_INSTANCE_CREATION from lego.apps.notifications.notification import Notification from lego.apps.users.models import User @@ -11,7 +12,7 @@ def __init__(self, lending_instance: LendingInstance, user: User): # TODO: Might not work super().__init__(user=user) - name = "lending_instance_creation" + name = LENDING_INSTANCE_CREATION def generate_mail(self): return self._delay_mail( diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index cd6dc4ccc..8882fdee4 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,7 +1,9 @@ +from datetime import timedelta from rest_framework import serializers from lego.apps.lending.models import LendableObject, LendingInstance +from lego.apps.users.serializers.users import PublicUserSerializer class LendableObjectSerializer(serializers.ModelSerializer): @@ -11,23 +13,31 @@ class Meta: class LendingInstanceSerializer(serializers.ModelSerializer): + user = PublicUserSerializer(read_only=True) + class Meta: model = LendingInstance fields = "__all__" def validate(self, data): - lendable_object_id = data["lendable_object"].id - lendable_object = LendableObject.objects.get(id=lendable_object_id) - user = self.context['request'].user - if not user.abakus_groups.filter( - id__in=lendable_object.responsible_groups.all().values_list("id", flat=True) - ).exists(): - if ( - data["end_date"] - data["start_date"] - > lendable_object.max_lending_period - ): - raise serializers.ValidationError( - "Lending period exceeds maximum allowed duration" - ) + # 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 = timedelta(days=lendable_object.max_lending_period) + if lending_period > max_lending_period: + raise serializers.ValidationError( + "Lending period exceeds maximum allowed duration" + ) return data diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index 0c29e7601..8675e4548 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -19,13 +19,7 @@ from lego.apps.permissions.api.views import AllowedPermissionsMixin -class LendableObjectViewSet( - AllowedPermissionsMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - GenericViewSet, -): +class LendableObjectViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendableObject.objects.all() serializer_class = LendableObjectSerializer filterset_class = LendableObjectFilterSet @@ -35,13 +29,7 @@ class LendableObjectViewSet( ] -class LendingInstanceViewSet( - AllowedPermissionsMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - GenericViewSet, -): +class LendingInstanceViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendingInstance.objects.all() serializer_class = LendingInstanceSerializer filterset_class = LendingInstanceFilterSet @@ -51,7 +39,9 @@ class LendingInstanceViewSet( ] def create(self, request): - serializer = LendingInstanceSerializer(data=request.data, context={'request': request}) + serializer = LendingInstanceSerializer( + 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) diff --git a/lego/apps/notifications/constants.py b/lego/apps/notifications/constants.py index fdf9a5363..2840025ae 100644 --- a/lego/apps/notifications/constants.py +++ b/lego/apps/notifications/constants.py @@ -47,6 +47,10 @@ # Followers REGISTRATION_REMINDER = "registration_reminder" +# Lending + +LENDING_INSTANCE_CREATION = "lending_instance_creation" + NOTIFICATION_TYPES = [ ANNOUNCEMENT, RESTRICTED_MAIL_SENT, @@ -66,6 +70,7 @@ COMPANY_INTEREST_CREATED, INACTIVE_WARNING, DELETED_WARNING, + LENDING_INSTANCE_CREATION, ] NOTIFICATION_CHOICES = [ From eb0ce166cbf13e4897d3920725ce6d0cbcafb77b Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 6 Feb 2024 21:54:34 +0100 Subject: [PATCH 05/29] Change responsible_role to responsible_roles --- ...endableobject_responsible_role_and_more.py | 38 +++++++++++++++++++ lego/apps/lending/models.py | 15 +++++--- lego/apps/lending/validators.py | 6 +++ ...r_notificationsetting_notification_type.py | 18 +++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py create mode 100644 lego/apps/lending/validators.py create mode 100644 lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py 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 new file mode 100644 index 000000000..0f4c1ab1a --- /dev/null +++ b/lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py @@ -0,0 +1,38 @@ +# 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/models.py b/lego/apps/lending/models.py index 38913babc..119bf8c64 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -2,7 +2,8 @@ from django.db import models from lego.apps.files.fields import ImageField - +from lego.apps.lending.validators import responsible_roles_validator +from django.contrib.postgres.fields import ArrayField from lego.apps.users import constants from lego.apps.lending.managers import LendingInstanceManager from lego.apps.users.models import User @@ -22,10 +23,14 @@ class LendableObject(BasisModel): null=True, blank=False, default=timedelta(days=7) ) responsible_groups = models.ManyToManyField("users.AbakusGroup") - responsible_role = models.CharField( - max_length=30, choices=constants.ROLES, default=constants.MEMBER - ) - # TODO: options should be changed + responsible_roles = ArrayField( + models.CharField( + max_length=30, + choices=constants.ROLES, + ), + default=list([constants.MEMBER]), + validators=[responsible_roles_validator], + ) image = ImageField( source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} ) diff --git a/lego/apps/lending/validators.py b/lego/apps/lending/validators.py new file mode 100644 index 000000000..be10e994d --- /dev/null +++ b/lego/apps/lending/validators.py @@ -0,0 +1,6 @@ +from django.forms import ValidationError + + +def responsible_roles_validator(value): + if len(value) != len(set(value)): + raise ValidationError("Duplicate values are not allowed.") \ No newline at end of file diff --git a/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py b/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py new file mode 100644 index 000000000..eb004aa0f --- /dev/null +++ b/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.10 on 2024-02-06 20:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('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), + ), + ] From 3583477376b05e4edf7b03020fd67b10576a830f Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Sun, 18 Feb 2024 19:07:25 +0100 Subject: [PATCH 06/29] Add fixtures --- .../development_lendable_objects.yaml | 28 +++++++++++++++++++ .../development_lending_requests.yaml | 17 +++++++++++ .../management/commands/load_fixtures.py | 2 ++ 3 files changed, 47 insertions(+) create mode 100644 lego/apps/lending/fixtures/development_lendable_objects.yaml create mode 100644 lego/apps/lending/fixtures/development_lending_requests.yaml diff --git a/lego/apps/lending/fixtures/development_lendable_objects.yaml b/lego/apps/lending/fixtures/development_lendable_objects.yaml new file mode 100644 index 000000000..95ea3b281 --- /dev/null +++ b/lego/apps/lending/fixtures/development_lendable_objects.yaml @@ -0,0 +1,28 @@ +- fields: + title: "Grill" + description: "En grill til å grille" + location: "A3" + # image: "test_event_cover.png" Could not get this to work + has_contract: False + max_lending_period: "2 days" + responsible_groups: + - - Webkom + responsible_roles: + - member + created_by: 3 + model: lending.LendableObject + pk: 1 +- fields: + title: "Soundboks" + description: "En soundboks til å sondbokse" + location: "It lageret" + # image: "test_event_cover.png" + model: lending.LendableObject + pk: 2 +- fields: + title: "Prinsessekjole" + description: "" + location: "" + # image: "test_event_covet.png" + 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 new file mode 100644 index 000000000..a6b3fcfc2 --- /dev/null +++ b/lego/apps/lending/fixtures/development_lending_requests.yaml @@ -0,0 +1,17 @@ +- fields: + lendable_object: 1 + user: 3 + start_date: '2024-02-02T23:16:00+00:00' + end_date: '2024-02-02T23:18:00+00:00' + # message: "Jeg vil låne grill :)" + pending: True # Should be a status field with multiple options + model: lending.LendingInstance + pk: 1 +- fields: + lendable_object: 2 + user: 3 + start_date: '2024-02-02T23:16:00+00:00' + end_date: '2024-02-02T23:18:00+00:00' + pending: True + model: lending.LendingInstance + pk: 2 diff --git a/lego/utils/management/commands/load_fixtures.py b/lego/utils/management/commands/load_fixtures.py index 264d8608a..6d955a5b7 100644 --- a/lego/utils/management/commands/load_fixtures.py +++ b/lego/utils/management/commands/load_fixtures.py @@ -78,6 +78,8 @@ def run(self, *args, **options): "joblistings/fixtures/development_joblistings.yaml", "surveys/fixtures/development_surveys.yaml", "users/fixtures/development_photo_consents.yaml", + "lending/fixtures/development_lendable_objects.yaml", + "lending/fixtures/development_lending_requests", ] ) From 0934b6f5e9f5fef2cfddef69f8e6d22011b835ae Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 20 Feb 2024 21:11:21 +0100 Subject: [PATCH 07/29] Fix image field on lending --- .../development_lendable_objects.yaml | 10 ++++----- .../migrations/0004_lendableobject_image.py | 21 +++++++++++++++++++ lego/apps/lending/models.py | 5 ++--- lego/apps/lending/serializers.py | 2 ++ 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 lego/apps/lending/migrations/0004_lendableobject_image.py diff --git a/lego/apps/lending/fixtures/development_lendable_objects.yaml b/lego/apps/lending/fixtures/development_lendable_objects.yaml index 95ea3b281..473c9a05f 100644 --- a/lego/apps/lending/fixtures/development_lendable_objects.yaml +++ b/lego/apps/lending/fixtures/development_lendable_objects.yaml @@ -2,13 +2,13 @@ title: "Grill" description: "En grill til å grille" location: "A3" - # image: "test_event_cover.png" Could not get this to work + image: test_event_cover.png has_contract: False max_lending_period: "2 days" - responsible_groups: - - - Webkom - responsible_roles: - - member + # responsible_groups: + # - - Webkom + # responsible_roles: + # - member created_by: 3 model: lending.LendableObject pk: 1 diff --git a/lego/apps/lending/migrations/0004_lendableobject_image.py b/lego/apps/lending/migrations/0004_lendableobject_image.py new file mode 100644 index 000000000..9f3f05798 --- /dev/null +++ b/lego/apps/lending/migrations/0004_lendableobject_image.py @@ -0,0 +1,21 @@ +# 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/models.py b/lego/apps/lending/models.py index 119bf8c64..e19e8e6a1 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -2,6 +2,7 @@ from django.db import models from lego.apps.files.fields import ImageField +from lego.apps.files.models import FileField from lego.apps.lending.validators import responsible_roles_validator from django.contrib.postgres.fields import ArrayField from lego.apps.users import constants @@ -31,9 +32,7 @@ class LendableObject(BasisModel): default=list([constants.MEMBER]), validators=[responsible_roles_validator], ) - image = ImageField( - source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} - ) + image = FileField(related_name="lendable_object_images") location = models.CharField(max_length=128, null=False, blank=True) @property diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 8882fdee4..5003c4f4b 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,5 +1,6 @@ from datetime import timedelta from rest_framework import serializers +from lego.apps.files.fields import ImageField from lego.apps.lending.models import LendableObject, LendingInstance @@ -7,6 +8,7 @@ class LendableObjectSerializer(serializers.ModelSerializer): + image = ImageField(required=False, options={"height": 500}) class Meta: model = LendableObject fields = "__all__" From cd8db093d031dae9ae267c57327d94eeda63ccc3 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 20 Feb 2024 21:34:47 +0100 Subject: [PATCH 08/29] Update notified users --- lego/apps/lending/managers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 0ba896719..93a05edd1 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -9,11 +9,14 @@ def create(self, *args, **kwargs): lending_instance = super().create(*args, **kwargs) abakus_groups = lending_instance.lendable_object.responsible_groups.all() role = lending_instance.lendable_object.responsible_role - - users_to_be_notified = Membership.objects.filter( - abakus_group__in=abakus_groups, role=role - ).values_list("user", flat=True) - + if role: + users_to_be_notified = Membership.objects.filter( + abakus_group__in=abakus_groups, role=role + ).values_list("user", flat=True) + else: + users_to_be_notified = Membership.objects.filter( + abakus_group__in=abakus_groups + ).values_list("user", flat=True) for user_id in users_to_be_notified: user = User.objects.get(pk=user_id) notification = LendingInstanceNotification( From 78dc3a1fa2af38382128f21cdf0c97f00a1f4679 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 20 Feb 2024 22:03:38 +0100 Subject: [PATCH 09/29] Fix conflict bug --- lego/apps/lending/models.py | 3 --- lego/apps/lending/serializers.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index e19e8e6a1..09d929337 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -41,9 +41,6 @@ def get_furthest_booking_date(self): class LendingInstance(BasisModel): - user = models.ForeignKey( - User, related_name="lendinginstances", on_delete=models.CASCADE - ) lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 5003c4f4b..eebee8c0f 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -15,7 +15,7 @@ class Meta: class LendingInstanceSerializer(serializers.ModelSerializer): - user = PublicUserSerializer(read_only=True) + user = PublicUserSerializer(read_only=True, source="created_by") class Meta: model = LendingInstance From 10db26d779afe8fa267253a74deb0b3f7f8a6cc5 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 27 Feb 2024 20:02:47 +0100 Subject: [PATCH 10/29] Add remove user migration --- lego/apps/lending/filters.py | 1 - .../0005_remove_lendinginstance_user.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 lego/apps/lending/migrations/0005_remove_lendinginstance_user.py diff --git a/lego/apps/lending/filters.py b/lego/apps/lending/filters.py index 53773c7fd..798503433 100644 --- a/lego/apps/lending/filters.py +++ b/lego/apps/lending/filters.py @@ -13,7 +13,6 @@ class LendingInstanceFilterSet(FilterSet): class Meta: model = LendingInstance fields = [ - "user", "lendable_object", "start_date", "pending", diff --git a/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py b/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py new file mode 100644 index 000000000..f3e8d528e --- /dev/null +++ b/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py @@ -0,0 +1,17 @@ +# 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', + ), + ] From 651e70a620d33589e8753023689b89a612b8c9bc Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 5 Mar 2024 19:46:23 +0100 Subject: [PATCH 11/29] Fix roles and validation for LendingInstance --- lego/apps/lending/managers.py | 10 +++++++--- lego/apps/lending/serializers.py | 12 ++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 93a05edd1..05aeb8086 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -8,10 +8,10 @@ def create(self, *args, **kwargs): lending_instance = super().create(*args, **kwargs) abakus_groups = lending_instance.lendable_object.responsible_groups.all() - role = lending_instance.lendable_object.responsible_role - if role: + roles = lending_instance.lendable_object.responsible_roles + if roles: users_to_be_notified = Membership.objects.filter( - abakus_group__in=abakus_groups, role=role + abakus_group__in=abakus_groups, role__in=roles ).values_list("user", flat=True) else: users_to_be_notified = Membership.objects.filter( @@ -26,3 +26,7 @@ def create(self, *args, **kwargs): notification.notify() return lending_instance + + def get_queryset(self): + return super().get_queryset().select_related("created_by") + diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index eebee8c0f..8d2348f20 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -19,7 +19,15 @@ class LendingInstanceSerializer(serializers.ModelSerializer): class Meta: model = LendingInstance - fields = "__all__" + fields = ( + "id", + "lendable_object", + "start_date", + "end_date", + "user", + "pending", + "created_at", + ) def validate(self, data): # Check if 'lendable_object', 'start_date', and 'end_date' are provided in the data. @@ -36,7 +44,7 @@ def validate(self, data): ).exists(): # Calculate the lending period and compare lending_period = end_date - start_date - max_lending_period = timedelta(days=lendable_object.max_lending_period) + max_lending_period = lendable_object.max_lending_period if lending_period > max_lending_period: raise serializers.ValidationError( "Lending period exceeds maximum allowed duration" From dcf7b07a0996330c06ab469076a279f16645213a Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 5 Mar 2024 20:39:12 +0100 Subject: [PATCH 12/29] Change to basismodel for lendinginstance --- lego/apps/lending/managers.py | 4 ++-- lego/apps/lending/serializers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 05aeb8086..375865db2 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -1,7 +1,7 @@ -from lego.utils.managers import PersistentModelManager +from lego.utils.managers import BasisModelManager -class LendingInstanceManager(PersistentModelManager): +class LendingInstanceManager(BasisModelManager): def create(self, *args, **kwargs): from lego.apps.users.models import Membership, User from lego.apps.lending.notifications import LendingInstanceNotification diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 8d2348f20..42938bc96 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,10 +1,10 @@ -from datetime import timedelta from rest_framework import serializers 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 class LendableObjectSerializer(serializers.ModelSerializer): @@ -14,7 +14,7 @@ class Meta: fields = "__all__" -class LendingInstanceSerializer(serializers.ModelSerializer): +class LendingInstanceSerializer(BasisModelSerializer): user = PublicUserSerializer(read_only=True, source="created_by") class Meta: From e549314f1e525267d10c2c89be9980067908cb5e Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 5 Mar 2024 21:20:51 +0100 Subject: [PATCH 13/29] Rename user to author, add comment to lendinginstance --- .../fixtures/development_lending_requests.yaml | 6 +++--- .../migrations/0006_lendinginstance_comment.py | 18 ++++++++++++++++++ lego/apps/lending/models.py | 1 + lego/apps/lending/serializers.py | 5 +++-- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 lego/apps/lending/migrations/0006_lendinginstance_comment.py diff --git a/lego/apps/lending/fixtures/development_lending_requests.yaml b/lego/apps/lending/fixtures/development_lending_requests.yaml index a6b3fcfc2..9724fe3b3 100644 --- a/lego/apps/lending/fixtures/development_lending_requests.yaml +++ b/lego/apps/lending/fixtures/development_lending_requests.yaml @@ -1,15 +1,15 @@ - fields: lendable_object: 1 - user: 3 + created_by: 3 start_date: '2024-02-02T23:16:00+00:00' end_date: '2024-02-02T23:18:00+00:00' - # message: "Jeg vil låne grill :)" + comment: "Jeg vil låne grill :)" pending: True # Should be a status field with multiple options model: lending.LendingInstance pk: 1 - fields: lendable_object: 2 - user: 3 + created_by: 3 start_date: '2024-02-02T23:16:00+00:00' end_date: '2024-02-02T23:18:00+00:00' pending: True diff --git a/lego/apps/lending/migrations/0006_lendinginstance_comment.py b/lego/apps/lending/migrations/0006_lendinginstance_comment.py new file mode 100644 index 000000000..460831b78 --- /dev/null +++ b/lego/apps/lending/migrations/0006_lendinginstance_comment.py @@ -0,0 +1,18 @@ +# 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 09d929337..d08ead6d3 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -45,6 +45,7 @@ class LendingInstance(BasisModel): 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 diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 42938bc96..eeb167360 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -15,7 +15,7 @@ class Meta: class LendingInstanceSerializer(BasisModelSerializer): - user = PublicUserSerializer(read_only=True, source="created_by") + author = PublicUserSerializer(read_only=True, source="created_by") class Meta: model = LendingInstance @@ -24,8 +24,9 @@ class Meta: "lendable_object", "start_date", "end_date", - "user", + "author", "pending", + "comment", "created_at", ) From 2e0dd154b6c26e739901d47ab5ccfb0dbda0496c Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Wed, 4 Oct 2023 22:54:07 +0200 Subject: [PATCH 14/29] Implement lending system --- lego/api/v1.py | 5 ++ lego/apps/lending/__init__.py | 0 lego/apps/lending/apps.py | 6 ++ lego/apps/lending/filters.py | 20 +++++++ lego/apps/lending/managers.py | 25 ++++++++ lego/apps/lending/migrations/0001_initial.py | 60 +++++++++++++++++++ lego/apps/lending/migrations/__init__.py | 0 lego/apps/lending/models.py | 42 +++++++++++++ lego/apps/lending/notifications.py | 40 +++++++++++++ lego/apps/lending/serializers.py | 37 ++++++++++++ lego/apps/lending/tests.py | 3 + lego/apps/lending/views.py | 60 +++++++++++++++++++ .../users/email/lending_instance.html | 31 ++++++++++ .../users/email/lending_instance.txt | 14 +++++ .../templates/users/push/lending_instance.txt | 5 ++ lego/settings/base.py | 1 + 16 files changed, 349 insertions(+) create mode 100644 lego/apps/lending/__init__.py create mode 100644 lego/apps/lending/apps.py create mode 100644 lego/apps/lending/filters.py create mode 100644 lego/apps/lending/managers.py create mode 100644 lego/apps/lending/migrations/0001_initial.py create mode 100644 lego/apps/lending/migrations/__init__.py create mode 100644 lego/apps/lending/models.py create mode 100644 lego/apps/lending/notifications.py create mode 100644 lego/apps/lending/serializers.py create mode 100644 lego/apps/lending/tests.py create mode 100644 lego/apps/lending/views.py create mode 100644 lego/apps/users/templates/users/email/lending_instance.html create mode 100644 lego/apps/users/templates/users/email/lending_instance.txt create mode 100644 lego/apps/users/templates/users/push/lending_instance.txt diff --git a/lego/api/v1.py b/lego/api/v1.py index 37b606863..a34af4be0 100644 --- a/lego/api/v1.py +++ b/lego/api/v1.py @@ -78,6 +78,8 @@ 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() @@ -145,6 +147,9 @@ basename="abakusgroup-memberships", ) router.register(r"joblistings", JoblistingViewSet, basename="joblisting") +router.register(r"lendableobject", LendableObjectViewSet, basename="lendableobject") +router.register(r"lendinginstance", LendingInstanceViewSet, basename="lendinginstance") + router.register( r"meeting-token", MeetingInvitationTokenViewSet, basename="meeting-token" ) diff --git a/lego/apps/lending/__init__.py b/lego/apps/lending/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/lending/apps.py b/lego/apps/lending/apps.py new file mode 100644 index 000000000..608fc9e03 --- /dev/null +++ b/lego/apps/lending/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LendingConfig(AppConfig): + name = "lego.apps.lending" + verbose_name = "Lending" diff --git a/lego/apps/lending/filters.py b/lego/apps/lending/filters.py new file mode 100644 index 000000000..53773c7fd --- /dev/null +++ b/lego/apps/lending/filters.py @@ -0,0 +1,20 @@ +from django_filters.rest_framework import FilterSet + +from lego.apps.lending.models import LendableObject, LendingInstance + + +class LendableObjectFilterSet(FilterSet): + class Meta: + model = LendableObject + fields = ["title"] + + +class LendingInstanceFilterSet(FilterSet): + class Meta: + model = LendingInstance + fields = [ + "user", + "lendable_object", + "start_date", + "pending", + ] diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py new file mode 100644 index 000000000..e32eea456 --- /dev/null +++ b/lego/apps/lending/managers.py @@ -0,0 +1,25 @@ +from lego.utils.managers import PersistentModelManager + + +class LendingInstanceManager(PersistentModelManager): + def create(self, *args, **kwargs): + 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() + role = lending_instance.lendable_object.responsible_role + + users_to_be_notified = Membership.objects.filter( + abakus_group__in=abakus_groups, role=role + ).values_list("user", flat=True) + + for user_id in users_to_be_notified: + user = User.objects.get(pk=user_id) + notification = LendingInstanceNotification( + lending_instance=lending_instance, + user_email=user, + ) + notification.notify() + + return lending_instance diff --git a/lego/apps/lending/migrations/0001_initial.py b/lego/apps/lending/migrations/0001_initial.py new file mode 100644 index 000000000..f226fc45a --- /dev/null +++ b/lego/apps/lending/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 4.0.10 on 2023-10-04 19:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0041_user_linkedin_id_alter_user_github_username'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + options={ + 'abstract': False, + 'default_manager_name': 'objects', + }, + ), + migrations.CreateModel( + 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)), + ], + options={ + 'abstract': False, + 'default_manager_name': 'objects', + }, + ), + ] diff --git a/lego/apps/lending/migrations/__init__.py b/lego/apps/lending/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py new file mode 100644 index 000000000..9a9d448c3 --- /dev/null +++ b/lego/apps/lending/models.py @@ -0,0 +1,42 @@ +from datetime import datetime, timedelta, timezone, tzinfo + +from django.db import models + +from lego.apps.users import constants +from lego.apps.lending.managers import LendingInstanceManager + + +from lego.utils.models import BasisModel + + +# Create your models here. + + +class LendableObject(BasisModel): + title = models.CharField(max_length=128, null=False, blank=False) + description = models.TextField(null=False, blank=True) + has_contract = models.BooleanField(default=False, null=False, blank=False) + max_lending_period = models.DurationField(null=True, blank=False) + responsible_groups = models.ManyToManyField("users.AbakusGroup") + responsible_role = models.CharField( + max_length=30, choices=constants.ROLES, default=constants.MEMBER + ) + location = models.CharField(max_length=128, null=False, blank=False) + + @property + def get_furthest_booking_date(self): + return timezone.now() + timedelta(days=14) + + +class LendingInstance(BasisModel): + user = models.ForeignKey("users.User", on_delete=models.CASCADE) + lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) + start_date = models.DateTimeField(null=True) + end_date = models.DateTimeField(null=True) + pending = models.BooleanField(default=True) + + objects = LendingInstanceManager() # type: ignore + + @property + def active(self): + return timezone.now() < self.end_date and timezone.now() > self.start_date diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py new file mode 100644 index 000000000..93d956b9c --- /dev/null +++ b/lego/apps/lending/notifications.py @@ -0,0 +1,40 @@ +from lego.apps.lending.models import LendingInstance +from lego.apps.notifications.notification import Notification +from lego.apps.users.models import User + + +class LendingInstanceNotification(Notification): + def __init__(self, lending_instance: LendingInstance, user: User): + self.lending_instance = lending_instance + self.user = user + + # TODO: Might not work + super().__init__(user=lending_instance.user) + + name = "lending_instance_creation" + + def generate_mail(self): + return self._delay_mail( + to_email=self.user.email_address, + context={ + "user": self.user.full_name, + "lendable_object": self.lending_instance.lendable_object.title, + "start_date": self.lending_instance.start_date, + "end_date": self.lending_instance.end_date, + }, + subject="Utlån forespørsel", + plain_template="users/email/lending_instance.txt", + html_template="users/email/lending_instance.html", + ) + + def generate_push(self): + return self._delay_push( + template="users/push/lending_instance.txt", + context={ + "user": self.user.full_name, + "lendable_object": self.lending_instance.lendable_object.title, + "start_date": self.lending_instance.start_date, + "end_date": self.lending_instance.end_date, + }, + instance=self.lending_instance, + ) diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py new file mode 100644 index 000000000..9adc9935f --- /dev/null +++ b/lego/apps/lending/serializers.py @@ -0,0 +1,37 @@ +from rest_framework import serializers + + +from lego.apps.lending.models import LendableObject, LendingInstance + + +class LendableObjectSerializer(serializers.ModelSerializer): + class Meta: + model = LendableObject + fields = "__all__" + + +class LendingInstanceSerializer(serializers.ModelSerializer): + class Meta: + model = LendingInstance + fields = "__all__" + + def validate(self, data): + lendable_object_id = data["lendable_object"].id + lendable_object = LendableObject.objects.get(id=lendable_object_id) + user = self.request.user + + if not user.abakus_groups.filter( + id__in=lendable_object.responsible_groups.all().values_list("id", flat=True) + ).exists(): + if ( + data["end_date"] - data["start_date"] + > lendable_object.max_lending_period + ): + raise serializers.ValidationError( + "Lending period exceeds maximum allowed duration" + ) + + # Add additional validation logic as per your use case + # ... + + return data diff --git a/lego/apps/lending/tests.py b/lego/apps/lending/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/lego/apps/lending/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py new file mode 100644 index 000000000..9f9ef333b --- /dev/null +++ b/lego/apps/lending/views.py @@ -0,0 +1,60 @@ +from rest_framework import ( + decorators, + exceptions, + mixins, + permissions, + renderers, + 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 ( + LendableObjectSerializer, + LendingInstanceSerializer, +) +from lego.apps.permissions.api.views import AllowedPermissionsMixin + + +class LendableObjectViewSet( + AllowedPermissionsMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): + queryset = LendableObject.objects.all() + serializer_class = LendableObjectSerializer + filterset_class = LendableObjectFilterSet + http_method_names = ["get", "post", "patch", "delete", "head", "options"] + permission_classes = [ + permissions.IsAuthenticated, + ] + + +class LendingInstanceViewSet( + AllowedPermissionsMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): + queryset = LendingInstance.objects.all() + serializer_class = LendingInstanceSerializer + filterset_class = LendingInstanceFilterSet + http_method_names = ["get", "post", "patch", "delete", "head", "options"] + permission_classes = [ + permissions.IsAuthenticated, + ] + + def create(self, request): + serializer = LendingInstanceSerializer(request, data=request.data) + + 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/users/templates/users/email/lending_instance.html b/lego/apps/users/templates/users/email/lending_instance.html new file mode 100644 index 000000000..e37243ddc --- /dev/null +++ b/lego/apps/users/templates/users/email/lending_instance.html @@ -0,0 +1,31 @@ +{% extends "email/base.html" %} + +{% block alert %} + + + + Det er en ny forespørsel om å låne {{ lendable_object }}! + + + +{% endblock %} + +{% block content %} + + + + Bruker: {{ user }}! + + + + + + + + Fra: {{ start_date }}, Til: {{ end_date }} + + + + + +{% endblock %} \ No newline at end of file diff --git a/lego/apps/users/templates/users/email/lending_instance.txt b/lego/apps/users/templates/users/email/lending_instance.txt new file mode 100644 index 000000000..1dccb1bd8 --- /dev/null +++ b/lego/apps/users/templates/users/email/lending_instance.txt @@ -0,0 +1,14 @@ +{% extends "email/base.txt" %} + +{% block content %} + +Det er en ny forespørsel om å låne {{ lendable_object }}! + +Bruker: {{ user }}! + +Fra: {{ start_date }}, Til: {{ end_date }} + + + + +{% endblock %} diff --git a/lego/apps/users/templates/users/push/lending_instance.txt b/lego/apps/users/templates/users/push/lending_instance.txt new file mode 100644 index 000000000..f35482044 --- /dev/null +++ b/lego/apps/users/templates/users/push/lending_instance.txt @@ -0,0 +1,5 @@ +Det er en ny forespørsel om å låne {{ lendable_object }}! + +Bruker: {{ user }}! + +Fra: {{ start_date }}, Til: {{ end_date }} diff --git a/lego/settings/base.py b/lego/settings/base.py index b340b8772..2480c4afc 100644 --- a/lego/settings/base.py +++ b/lego/settings/base.py @@ -66,6 +66,7 @@ "lego.apps.tags", "lego.apps.users", "lego.apps.websockets", + "lego.apps.lending", ] DEFAULT_AUTO_FIELD = "django.db.models.AutoField" From e2b7ca072be4b87d261db05afc8354e2bf6ba739 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Thu, 19 Oct 2023 20:31:09 +0200 Subject: [PATCH 15/29] Fix location field bug --- .../migrations/0002_lendableobject_location.py | 18 ++++++++++++++++++ lego/apps/lending/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 lego/apps/lending/migrations/0002_lendableobject_location.py diff --git a/lego/apps/lending/migrations/0002_lendableobject_location.py b/lego/apps/lending/migrations/0002_lendableobject_location.py new file mode 100644 index 000000000..c7c411312 --- /dev/null +++ b/lego/apps/lending/migrations/0002_lendableobject_location.py @@ -0,0 +1,18 @@ +# 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/models.py b/lego/apps/lending/models.py index 9a9d448c3..475b8feaf 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -21,7 +21,7 @@ class LendableObject(BasisModel): responsible_role = models.CharField( max_length=30, choices=constants.ROLES, default=constants.MEMBER ) - location = models.CharField(max_length=128, null=False, blank=False) + location = models.CharField(max_length=128, null=False, blank=True) @property def get_furthest_booking_date(self): From f36ffda7cc8f10e6af53787b36ce45150bf0ce7b Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Thu, 19 Oct 2023 20:51:49 +0200 Subject: [PATCH 16/29] Fix other thing --- lego/apps/lending/managers.py | 2 +- lego/apps/lending/models.py | 4 ++++ lego/apps/lending/notifications.py | 8 ++++---- lego/apps/lending/serializers.py | 6 +----- lego/apps/lending/views.py | 3 +-- lego/apps/notifications/notification.py | 3 ++- .../users/templates/users/email/lending_instance.html | 2 +- .../apps/users/templates/users/email/lending_instance.txt | 2 +- lego/apps/users/templates/users/push/lending_instance.txt | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index e32eea456..0ba896719 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -18,7 +18,7 @@ def create(self, *args, **kwargs): user = User.objects.get(pk=user_id) notification = LendingInstanceNotification( lending_instance=lending_instance, - user_email=user, + user=user, ) notification.notify() diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index 475b8feaf..3125c275f 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -21,6 +21,10 @@ class LendableObject(BasisModel): responsible_role = models.CharField( max_length=30, choices=constants.ROLES, default=constants.MEMBER ) + # TODO: options should be changed + image = models.ImageField( + source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} + ) location = models.CharField(max_length=128, null=False, blank=True) @property diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py index 93d956b9c..489cc509c 100644 --- a/lego/apps/lending/notifications.py +++ b/lego/apps/lending/notifications.py @@ -6,10 +6,10 @@ class LendingInstanceNotification(Notification): def __init__(self, lending_instance: LendingInstance, user: User): self.lending_instance = lending_instance - self.user = user + self.lender = lending_instance.user # TODO: Might not work - super().__init__(user=lending_instance.user) + super().__init__(user=user) name = "lending_instance_creation" @@ -17,7 +17,7 @@ def generate_mail(self): return self._delay_mail( to_email=self.user.email_address, context={ - "user": self.user.full_name, + "lender": self.lender, "lendable_object": self.lending_instance.lendable_object.title, "start_date": self.lending_instance.start_date, "end_date": self.lending_instance.end_date, @@ -31,7 +31,7 @@ def generate_push(self): return self._delay_push( template="users/push/lending_instance.txt", context={ - "user": self.user.full_name, + "lender": self.lender, "lendable_object": self.lending_instance.lendable_object.title, "start_date": self.lending_instance.start_date, "end_date": self.lending_instance.end_date, diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 9adc9935f..cd6dc4ccc 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -18,8 +18,7 @@ class Meta: def validate(self, data): lendable_object_id = data["lendable_object"].id lendable_object = LendableObject.objects.get(id=lendable_object_id) - user = self.request.user - + user = self.context['request'].user if not user.abakus_groups.filter( id__in=lendable_object.responsible_groups.all().values_list("id", flat=True) ).exists(): @@ -31,7 +30,4 @@ def validate(self, data): "Lending period exceeds maximum allowed duration" ) - # Add additional validation logic as per your use case - # ... - return data diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index 9f9ef333b..0c29e7601 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -51,8 +51,7 @@ class LendingInstanceViewSet( ] def create(self, request): - serializer = LendingInstanceSerializer(request, data=request.data) - + serializer = LendingInstanceSerializer(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) diff --git a/lego/apps/notifications/notification.py b/lego/apps/notifications/notification.py index 0cdec3bbe..38cbf8887 100644 --- a/lego/apps/notifications/notification.py +++ b/lego/apps/notifications/notification.py @@ -1,4 +1,5 @@ from structlog import get_logger +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 @@ -15,7 +16,7 @@ class Notification: name = None - def __init__(self, user, *args, **kwargs): + def __init__(self, user: User, *args, **kwargs): self.user = user self.args = (args,) self.kwargs = kwargs diff --git a/lego/apps/users/templates/users/email/lending_instance.html b/lego/apps/users/templates/users/email/lending_instance.html index e37243ddc..06bdbd27d 100644 --- a/lego/apps/users/templates/users/email/lending_instance.html +++ b/lego/apps/users/templates/users/email/lending_instance.html @@ -14,7 +14,7 @@ - Bruker: {{ user }}! + Bruker: {{ lender }}! diff --git a/lego/apps/users/templates/users/email/lending_instance.txt b/lego/apps/users/templates/users/email/lending_instance.txt index 1dccb1bd8..59b087f55 100644 --- a/lego/apps/users/templates/users/email/lending_instance.txt +++ b/lego/apps/users/templates/users/email/lending_instance.txt @@ -4,7 +4,7 @@ Det er en ny forespørsel om å låne {{ lendable_object }}! -Bruker: {{ user }}! +Bruker: {{ lender }}! Fra: {{ start_date }}, Til: {{ end_date }} diff --git a/lego/apps/users/templates/users/push/lending_instance.txt b/lego/apps/users/templates/users/push/lending_instance.txt index f35482044..a7d604c5c 100644 --- a/lego/apps/users/templates/users/push/lending_instance.txt +++ b/lego/apps/users/templates/users/push/lending_instance.txt @@ -1,5 +1,5 @@ Det er en ny forespørsel om å låne {{ lendable_object }}! -Bruker: {{ user }}! +Bruker: {{ lender }}! Fra: {{ start_date }}, Til: {{ end_date }} From 9bc279b2669408257b7dc2970d3e117fee21c3a3 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Thu, 1 Feb 2024 21:56:38 +0100 Subject: [PATCH 17/29] Fix more --- lego/apps/lending/models.py | 12 +++++++--- lego/apps/lending/notifications.py | 3 ++- lego/apps/lending/serializers.py | 36 ++++++++++++++++++---------- lego/apps/lending/views.py | 20 ++++------------ lego/apps/notifications/constants.py | 5 ++++ 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index 3125c275f..38913babc 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -1,9 +1,11 @@ from datetime import datetime, timedelta, timezone, tzinfo from django.db import models +from lego.apps.files.fields import ImageField 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 @@ -16,13 +18,15 @@ class LendableObject(BasisModel): title = models.CharField(max_length=128, null=False, blank=False) description = models.TextField(null=False, blank=True) has_contract = models.BooleanField(default=False, null=False, blank=False) - max_lending_period = models.DurationField(null=True, blank=False) + max_lending_period = models.DurationField( + null=True, blank=False, default=timedelta(days=7) + ) responsible_groups = models.ManyToManyField("users.AbakusGroup") responsible_role = models.CharField( max_length=30, choices=constants.ROLES, default=constants.MEMBER ) # TODO: options should be changed - image = models.ImageField( + image = ImageField( source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} ) location = models.CharField(max_length=128, null=False, blank=True) @@ -33,7 +37,9 @@ def get_furthest_booking_date(self): class LendingInstance(BasisModel): - user = models.ForeignKey("users.User", on_delete=models.CASCADE) + user = models.ForeignKey( + User, related_name="lendinginstances", on_delete=models.CASCADE + ) lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) diff --git a/lego/apps/lending/notifications.py b/lego/apps/lending/notifications.py index 489cc509c..ab5bb5d57 100644 --- a/lego/apps/lending/notifications.py +++ b/lego/apps/lending/notifications.py @@ -1,4 +1,5 @@ from lego.apps.lending.models import LendingInstance +from lego.apps.notifications.constants import LENDING_INSTANCE_CREATION from lego.apps.notifications.notification import Notification from lego.apps.users.models import User @@ -11,7 +12,7 @@ def __init__(self, lending_instance: LendingInstance, user: User): # TODO: Might not work super().__init__(user=user) - name = "lending_instance_creation" + name = LENDING_INSTANCE_CREATION def generate_mail(self): return self._delay_mail( diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index cd6dc4ccc..8882fdee4 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,7 +1,9 @@ +from datetime import timedelta from rest_framework import serializers from lego.apps.lending.models import LendableObject, LendingInstance +from lego.apps.users.serializers.users import PublicUserSerializer class LendableObjectSerializer(serializers.ModelSerializer): @@ -11,23 +13,31 @@ class Meta: class LendingInstanceSerializer(serializers.ModelSerializer): + user = PublicUserSerializer(read_only=True) + class Meta: model = LendingInstance fields = "__all__" def validate(self, data): - lendable_object_id = data["lendable_object"].id - lendable_object = LendableObject.objects.get(id=lendable_object_id) - user = self.context['request'].user - if not user.abakus_groups.filter( - id__in=lendable_object.responsible_groups.all().values_list("id", flat=True) - ).exists(): - if ( - data["end_date"] - data["start_date"] - > lendable_object.max_lending_period - ): - raise serializers.ValidationError( - "Lending period exceeds maximum allowed duration" - ) + # 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 = timedelta(days=lendable_object.max_lending_period) + if lending_period > max_lending_period: + raise serializers.ValidationError( + "Lending period exceeds maximum allowed duration" + ) return data diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index 0c29e7601..8675e4548 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -19,13 +19,7 @@ from lego.apps.permissions.api.views import AllowedPermissionsMixin -class LendableObjectViewSet( - AllowedPermissionsMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - GenericViewSet, -): +class LendableObjectViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendableObject.objects.all() serializer_class = LendableObjectSerializer filterset_class = LendableObjectFilterSet @@ -35,13 +29,7 @@ class LendableObjectViewSet( ] -class LendingInstanceViewSet( - AllowedPermissionsMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - GenericViewSet, -): +class LendingInstanceViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendingInstance.objects.all() serializer_class = LendingInstanceSerializer filterset_class = LendingInstanceFilterSet @@ -51,7 +39,9 @@ class LendingInstanceViewSet( ] def create(self, request): - serializer = LendingInstanceSerializer(data=request.data, context={'request': request}) + serializer = LendingInstanceSerializer( + 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) diff --git a/lego/apps/notifications/constants.py b/lego/apps/notifications/constants.py index fdf9a5363..2840025ae 100644 --- a/lego/apps/notifications/constants.py +++ b/lego/apps/notifications/constants.py @@ -47,6 +47,10 @@ # Followers REGISTRATION_REMINDER = "registration_reminder" +# Lending + +LENDING_INSTANCE_CREATION = "lending_instance_creation" + NOTIFICATION_TYPES = [ ANNOUNCEMENT, RESTRICTED_MAIL_SENT, @@ -66,6 +70,7 @@ COMPANY_INTEREST_CREATED, INACTIVE_WARNING, DELETED_WARNING, + LENDING_INSTANCE_CREATION, ] NOTIFICATION_CHOICES = [ From 097aeb4117753d99f7d627c304dd7c55ce0c93e8 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 6 Feb 2024 21:54:34 +0100 Subject: [PATCH 18/29] Change responsible_role to responsible_roles --- ...endableobject_responsible_role_and_more.py | 38 +++++++++++++++++++ lego/apps/lending/models.py | 15 +++++--- lego/apps/lending/validators.py | 6 +++ ...r_notificationsetting_notification_type.py | 18 +++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py create mode 100644 lego/apps/lending/validators.py create mode 100644 lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py 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 new file mode 100644 index 000000000..0f4c1ab1a --- /dev/null +++ b/lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py @@ -0,0 +1,38 @@ +# 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/models.py b/lego/apps/lending/models.py index 38913babc..119bf8c64 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -2,7 +2,8 @@ from django.db import models from lego.apps.files.fields import ImageField - +from lego.apps.lending.validators import responsible_roles_validator +from django.contrib.postgres.fields import ArrayField from lego.apps.users import constants from lego.apps.lending.managers import LendingInstanceManager from lego.apps.users.models import User @@ -22,10 +23,14 @@ class LendableObject(BasisModel): null=True, blank=False, default=timedelta(days=7) ) responsible_groups = models.ManyToManyField("users.AbakusGroup") - responsible_role = models.CharField( - max_length=30, choices=constants.ROLES, default=constants.MEMBER - ) - # TODO: options should be changed + responsible_roles = ArrayField( + models.CharField( + max_length=30, + choices=constants.ROLES, + ), + default=list([constants.MEMBER]), + validators=[responsible_roles_validator], + ) image = ImageField( source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} ) diff --git a/lego/apps/lending/validators.py b/lego/apps/lending/validators.py new file mode 100644 index 000000000..be10e994d --- /dev/null +++ b/lego/apps/lending/validators.py @@ -0,0 +1,6 @@ +from django.forms import ValidationError + + +def responsible_roles_validator(value): + if len(value) != len(set(value)): + raise ValidationError("Duplicate values are not allowed.") \ No newline at end of file diff --git a/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py b/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py new file mode 100644 index 000000000..eb004aa0f --- /dev/null +++ b/lego/apps/notifications/migrations/0015_alter_notificationsetting_notification_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.10 on 2024-02-06 20:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('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), + ), + ] From 235ceea7a85722ca2f30302fe7e7777921939a69 Mon Sep 17 00:00:00 2001 From: Isak Berg Endresen Date: Sun, 18 Feb 2024 19:07:25 +0100 Subject: [PATCH 19/29] Add fixtures --- .../development_lendable_objects.yaml | 28 +++++++++++++++++++ .../development_lending_requests.yaml | 17 +++++++++++ .../management/commands/load_fixtures.py | 2 ++ 3 files changed, 47 insertions(+) create mode 100644 lego/apps/lending/fixtures/development_lendable_objects.yaml create mode 100644 lego/apps/lending/fixtures/development_lending_requests.yaml diff --git a/lego/apps/lending/fixtures/development_lendable_objects.yaml b/lego/apps/lending/fixtures/development_lendable_objects.yaml new file mode 100644 index 000000000..95ea3b281 --- /dev/null +++ b/lego/apps/lending/fixtures/development_lendable_objects.yaml @@ -0,0 +1,28 @@ +- fields: + title: "Grill" + description: "En grill til å grille" + location: "A3" + # image: "test_event_cover.png" Could not get this to work + has_contract: False + max_lending_period: "2 days" + responsible_groups: + - - Webkom + responsible_roles: + - member + created_by: 3 + model: lending.LendableObject + pk: 1 +- fields: + title: "Soundboks" + description: "En soundboks til å sondbokse" + location: "It lageret" + # image: "test_event_cover.png" + model: lending.LendableObject + pk: 2 +- fields: + title: "Prinsessekjole" + description: "" + location: "" + # image: "test_event_covet.png" + 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 new file mode 100644 index 000000000..a6b3fcfc2 --- /dev/null +++ b/lego/apps/lending/fixtures/development_lending_requests.yaml @@ -0,0 +1,17 @@ +- fields: + lendable_object: 1 + user: 3 + start_date: '2024-02-02T23:16:00+00:00' + end_date: '2024-02-02T23:18:00+00:00' + # message: "Jeg vil låne grill :)" + pending: True # Should be a status field with multiple options + model: lending.LendingInstance + pk: 1 +- fields: + lendable_object: 2 + user: 3 + start_date: '2024-02-02T23:16:00+00:00' + end_date: '2024-02-02T23:18:00+00:00' + pending: True + model: lending.LendingInstance + pk: 2 diff --git a/lego/utils/management/commands/load_fixtures.py b/lego/utils/management/commands/load_fixtures.py index 264d8608a..6d955a5b7 100644 --- a/lego/utils/management/commands/load_fixtures.py +++ b/lego/utils/management/commands/load_fixtures.py @@ -78,6 +78,8 @@ def run(self, *args, **options): "joblistings/fixtures/development_joblistings.yaml", "surveys/fixtures/development_surveys.yaml", "users/fixtures/development_photo_consents.yaml", + "lending/fixtures/development_lendable_objects.yaml", + "lending/fixtures/development_lending_requests", ] ) From 6ce54bab6c1bbb774e8555e658bd027e099bd3f6 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 20 Feb 2024 21:11:21 +0100 Subject: [PATCH 20/29] Fix image field on lending --- .../development_lendable_objects.yaml | 10 ++++----- .../migrations/0004_lendableobject_image.py | 21 +++++++++++++++++++ lego/apps/lending/models.py | 5 ++--- lego/apps/lending/serializers.py | 2 ++ 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 lego/apps/lending/migrations/0004_lendableobject_image.py diff --git a/lego/apps/lending/fixtures/development_lendable_objects.yaml b/lego/apps/lending/fixtures/development_lendable_objects.yaml index 95ea3b281..473c9a05f 100644 --- a/lego/apps/lending/fixtures/development_lendable_objects.yaml +++ b/lego/apps/lending/fixtures/development_lendable_objects.yaml @@ -2,13 +2,13 @@ title: "Grill" description: "En grill til å grille" location: "A3" - # image: "test_event_cover.png" Could not get this to work + image: test_event_cover.png has_contract: False max_lending_period: "2 days" - responsible_groups: - - - Webkom - responsible_roles: - - member + # responsible_groups: + # - - Webkom + # responsible_roles: + # - member created_by: 3 model: lending.LendableObject pk: 1 diff --git a/lego/apps/lending/migrations/0004_lendableobject_image.py b/lego/apps/lending/migrations/0004_lendableobject_image.py new file mode 100644 index 000000000..9f3f05798 --- /dev/null +++ b/lego/apps/lending/migrations/0004_lendableobject_image.py @@ -0,0 +1,21 @@ +# 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/models.py b/lego/apps/lending/models.py index 119bf8c64..e19e8e6a1 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -2,6 +2,7 @@ from django.db import models from lego.apps.files.fields import ImageField +from lego.apps.files.models import FileField from lego.apps.lending.validators import responsible_roles_validator from django.contrib.postgres.fields import ArrayField from lego.apps.users import constants @@ -31,9 +32,7 @@ class LendableObject(BasisModel): default=list([constants.MEMBER]), validators=[responsible_roles_validator], ) - image = ImageField( - source="cover", required=False, options={"height": 50, "filters": ["blur(20)"]} - ) + image = FileField(related_name="lendable_object_images") location = models.CharField(max_length=128, null=False, blank=True) @property diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 8882fdee4..5003c4f4b 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,5 +1,6 @@ from datetime import timedelta from rest_framework import serializers +from lego.apps.files.fields import ImageField from lego.apps.lending.models import LendableObject, LendingInstance @@ -7,6 +8,7 @@ class LendableObjectSerializer(serializers.ModelSerializer): + image = ImageField(required=False, options={"height": 500}) class Meta: model = LendableObject fields = "__all__" From 264a16b5cedbcad78ef3b44e276d6a5f46e8aca7 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 20 Feb 2024 21:34:47 +0100 Subject: [PATCH 21/29] Update notified users --- lego/apps/lending/managers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 0ba896719..93a05edd1 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -9,11 +9,14 @@ def create(self, *args, **kwargs): lending_instance = super().create(*args, **kwargs) abakus_groups = lending_instance.lendable_object.responsible_groups.all() role = lending_instance.lendable_object.responsible_role - - users_to_be_notified = Membership.objects.filter( - abakus_group__in=abakus_groups, role=role - ).values_list("user", flat=True) - + if role: + users_to_be_notified = Membership.objects.filter( + abakus_group__in=abakus_groups, role=role + ).values_list("user", flat=True) + else: + users_to_be_notified = Membership.objects.filter( + abakus_group__in=abakus_groups + ).values_list("user", flat=True) for user_id in users_to_be_notified: user = User.objects.get(pk=user_id) notification = LendingInstanceNotification( From 7b9c0c8d6d72d05e5b8ebd3e873a0a1e8468888f Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 20 Feb 2024 22:03:38 +0100 Subject: [PATCH 22/29] Fix conflict bug --- lego/apps/lending/models.py | 3 --- lego/apps/lending/serializers.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index e19e8e6a1..09d929337 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -41,9 +41,6 @@ def get_furthest_booking_date(self): class LendingInstance(BasisModel): - user = models.ForeignKey( - User, related_name="lendinginstances", on_delete=models.CASCADE - ) lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 5003c4f4b..eebee8c0f 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -15,7 +15,7 @@ class Meta: class LendingInstanceSerializer(serializers.ModelSerializer): - user = PublicUserSerializer(read_only=True) + user = PublicUserSerializer(read_only=True, source="created_by") class Meta: model = LendingInstance From 024b0ca8244f85b999ffa9d14d447ce8b2000759 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 27 Feb 2024 20:02:47 +0100 Subject: [PATCH 23/29] Add remove user migration --- lego/apps/lending/filters.py | 1 - .../0005_remove_lendinginstance_user.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 lego/apps/lending/migrations/0005_remove_lendinginstance_user.py diff --git a/lego/apps/lending/filters.py b/lego/apps/lending/filters.py index 53773c7fd..798503433 100644 --- a/lego/apps/lending/filters.py +++ b/lego/apps/lending/filters.py @@ -13,7 +13,6 @@ class LendingInstanceFilterSet(FilterSet): class Meta: model = LendingInstance fields = [ - "user", "lendable_object", "start_date", "pending", diff --git a/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py b/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py new file mode 100644 index 000000000..f3e8d528e --- /dev/null +++ b/lego/apps/lending/migrations/0005_remove_lendinginstance_user.py @@ -0,0 +1,17 @@ +# 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', + ), + ] From 4a62d98014648a60d7e5df7d52667855c1c8d42f Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 5 Mar 2024 19:46:23 +0100 Subject: [PATCH 24/29] Fix roles and validation for LendingInstance --- lego/apps/lending/managers.py | 10 +++++++--- lego/apps/lending/serializers.py | 12 ++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 93a05edd1..05aeb8086 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -8,10 +8,10 @@ def create(self, *args, **kwargs): lending_instance = super().create(*args, **kwargs) abakus_groups = lending_instance.lendable_object.responsible_groups.all() - role = lending_instance.lendable_object.responsible_role - if role: + roles = lending_instance.lendable_object.responsible_roles + if roles: users_to_be_notified = Membership.objects.filter( - abakus_group__in=abakus_groups, role=role + abakus_group__in=abakus_groups, role__in=roles ).values_list("user", flat=True) else: users_to_be_notified = Membership.objects.filter( @@ -26,3 +26,7 @@ def create(self, *args, **kwargs): notification.notify() return lending_instance + + def get_queryset(self): + return super().get_queryset().select_related("created_by") + diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index eebee8c0f..8d2348f20 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -19,7 +19,15 @@ class LendingInstanceSerializer(serializers.ModelSerializer): class Meta: model = LendingInstance - fields = "__all__" + fields = ( + "id", + "lendable_object", + "start_date", + "end_date", + "user", + "pending", + "created_at", + ) def validate(self, data): # Check if 'lendable_object', 'start_date', and 'end_date' are provided in the data. @@ -36,7 +44,7 @@ def validate(self, data): ).exists(): # Calculate the lending period and compare lending_period = end_date - start_date - max_lending_period = timedelta(days=lendable_object.max_lending_period) + max_lending_period = lendable_object.max_lending_period if lending_period > max_lending_period: raise serializers.ValidationError( "Lending period exceeds maximum allowed duration" From 911630e4272d2d81b5647fcbbe7f17f183eb6b1f Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 5 Mar 2024 20:39:12 +0100 Subject: [PATCH 25/29] Change to basismodel for lendinginstance --- lego/apps/lending/managers.py | 4 ++-- lego/apps/lending/serializers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 05aeb8086..375865db2 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -1,7 +1,7 @@ -from lego.utils.managers import PersistentModelManager +from lego.utils.managers import BasisModelManager -class LendingInstanceManager(PersistentModelManager): +class LendingInstanceManager(BasisModelManager): def create(self, *args, **kwargs): from lego.apps.users.models import Membership, User from lego.apps.lending.notifications import LendingInstanceNotification diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 8d2348f20..42938bc96 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -1,10 +1,10 @@ -from datetime import timedelta from rest_framework import serializers 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 class LendableObjectSerializer(serializers.ModelSerializer): @@ -14,7 +14,7 @@ class Meta: fields = "__all__" -class LendingInstanceSerializer(serializers.ModelSerializer): +class LendingInstanceSerializer(BasisModelSerializer): user = PublicUserSerializer(read_only=True, source="created_by") class Meta: From ab09ba5aa671abe2304f11de6714ffe01a910826 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 5 Mar 2024 21:20:51 +0100 Subject: [PATCH 26/29] Rename user to author, add comment to lendinginstance --- .../fixtures/development_lending_requests.yaml | 6 +++--- .../migrations/0006_lendinginstance_comment.py | 18 ++++++++++++++++++ lego/apps/lending/models.py | 1 + lego/apps/lending/serializers.py | 5 +++-- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 lego/apps/lending/migrations/0006_lendinginstance_comment.py diff --git a/lego/apps/lending/fixtures/development_lending_requests.yaml b/lego/apps/lending/fixtures/development_lending_requests.yaml index a6b3fcfc2..9724fe3b3 100644 --- a/lego/apps/lending/fixtures/development_lending_requests.yaml +++ b/lego/apps/lending/fixtures/development_lending_requests.yaml @@ -1,15 +1,15 @@ - fields: lendable_object: 1 - user: 3 + created_by: 3 start_date: '2024-02-02T23:16:00+00:00' end_date: '2024-02-02T23:18:00+00:00' - # message: "Jeg vil låne grill :)" + comment: "Jeg vil låne grill :)" pending: True # Should be a status field with multiple options model: lending.LendingInstance pk: 1 - fields: lendable_object: 2 - user: 3 + created_by: 3 start_date: '2024-02-02T23:16:00+00:00' end_date: '2024-02-02T23:18:00+00:00' pending: True diff --git a/lego/apps/lending/migrations/0006_lendinginstance_comment.py b/lego/apps/lending/migrations/0006_lendinginstance_comment.py new file mode 100644 index 000000000..460831b78 --- /dev/null +++ b/lego/apps/lending/migrations/0006_lendinginstance_comment.py @@ -0,0 +1,18 @@ +# 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 09d929337..d08ead6d3 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -45,6 +45,7 @@ class LendingInstance(BasisModel): 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 diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index 42938bc96..eeb167360 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -15,7 +15,7 @@ class Meta: class LendingInstanceSerializer(BasisModelSerializer): - user = PublicUserSerializer(read_only=True, source="created_by") + author = PublicUserSerializer(read_only=True, source="created_by") class Meta: model = LendingInstance @@ -24,8 +24,9 @@ class Meta: "lendable_object", "start_date", "end_date", - "user", + "author", "pending", + "comment", "created_at", ) From 0c729d52560b915de9e855faa6b7be410758154a Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Tue, 12 Mar 2024 18:43:46 +0100 Subject: [PATCH 27/29] Add notification on update of lending instance --- lego/apps/lending/managers.py | 14 ++++++++ lego/apps/lending/models.py | 13 +++++-- lego/apps/lending/permissions.py | 60 ++++++++++++++++++++++++++++++++ lego/apps/lending/serializers.py | 26 ++++++++++++-- lego/apps/lending/views.py | 33 +++++++++++++++--- 5 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 lego/apps/lending/permissions.py diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 375865db2..6b3761584 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -26,6 +26,20 @@ def create(self, *args, **kwargs): notification.notify() 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/models.py b/lego/apps/lending/models.py index d08ead6d3..4867851f8 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -3,8 +3,10 @@ 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.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 @@ -16,7 +18,7 @@ # Create your models here. -class LendableObject(BasisModel): +class LendableObject(BasisModel, ObjectPermissionsModel): title = models.CharField(max_length=128, null=False, blank=False) description = models.TextField(null=False, blank=True) has_contract = models.BooleanField(default=False, null=False, blank=False) @@ -39,8 +41,11 @@ class LendableObject(BasisModel): def get_furthest_booking_date(self): return timezone.now() + timedelta(days=14) + class Meta: + abstract = False -class LendingInstance(BasisModel): + +class LendingInstance(BasisModel, ObjectPermissionsModel): lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) @@ -52,3 +57,7 @@ class LendingInstance(BasisModel): @property def active(self): return timezone.now() < self.end_date and timezone.now() > self.start_date + + class Meta: + abstract = False + permission_handler = LendingInstancePermissionHandler() diff --git a/lego/apps/lending/permissions.py b/lego/apps/lending/permissions.py new file mode 100644 index 000000000..7bf9e0cef --- /dev/null +++ b/lego/apps/lending/permissions.py @@ -0,0 +1,60 @@ +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 + authentication_map = {LIST: False, VIEW: False} + default_require_auth = True + + def has_perm( + self, + user, + perm, + obj=None, + queryset=None, + check_keyword_permissions=True, + **kwargs + ): + if perm == LIST: + return True + if not user.is_authenticated: + return False + + # Check object permissions before keywork perms + if obj is not None: + if self.has_object_permissions(user, perm, obj): + return True + + if perm == EDIT and self.created_by(user, obj): + return True + + if perm == CREATE: + return True + + 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 + if perm == CREATE: + return True + return not (perm == DELETE or perm == EDIT) + + def filter_queryset(self, user, queryset, **kwargs): + if user.is_authenticated: + return queryset.filter( + Q(created_by=user) | + Q(lendable_object__responsible_groups__in=user.abakus_groups) + ).distinct() + return queryset.none() diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index eeb167360..f7f87dad8 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -4,17 +4,27 @@ from lego.apps.lending.models import LendableObject, LendingInstance from lego.apps.users.serializers.users import PublicUserSerializer -from lego.utils.serializers import BasisModelSerializer +from lego.utils.serializers import BasisModelSerializer, ObjectPermissionsSerializerMixin -class LendableObjectSerializer(serializers.ModelSerializer): +class DetailedLendableObjectSerializer(BasisModelSerializer): image = ImageField(required=False, options={"height": 500}) class Meta: model = LendableObject fields = "__all__" +class DetailedAdminLendableObjectSerializer( + ObjectPermissionsSerializerMixin, DetailedLendableObjectSerializer +): + class Meta: + model = LendableObject + fields = ( + DetailedLendableObjectSerializer.Meta.fields + + ObjectPermissionsSerializerMixin.Meta.fields + ) + -class LendingInstanceSerializer(BasisModelSerializer): +class DetailedLendingInstanceSerializer(BasisModelSerializer): author = PublicUserSerializer(read_only=True, source="created_by") class Meta: @@ -52,3 +62,13 @@ def validate(self, data): ) return data + +class DetailedAdminLendingInstanceSerializer( + ObjectPermissionsSerializerMixin, DetailedLendingInstanceSerializer +): + class Meta: + model = LendingInstance + fields = ( + DetailedLendingInstanceSerializer.Meta.fields + + ObjectPermissionsSerializerMixin.Meta.fields + ) diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index 8675e4548..c1f0df2c9 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -13,33 +13,56 @@ from lego.apps.lending.filters import LendableObjectFilterSet, LendingInstanceFilterSet from lego.apps.lending.models import LendableObject, LendingInstance from lego.apps.lending.serializers import ( - LendableObjectSerializer, - LendingInstanceSerializer, + DetailedLendableObjectSerializer, + DetailedLendingInstanceSerializer, + DetailedAdminLendableObjectSerializer, + DetailedAdminLendingInstanceSerializer ) from lego.apps.permissions.api.views import AllowedPermissionsMixin +from lego.apps.permissions.utils import get_permission_handler class LendableObjectViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendableObject.objects.all() - serializer_class = LendableObjectSerializer + serializer_class = DetailedLendableObjectSerializer filterset_class = LendableObjectFilterSet http_method_names = ["get", "post", "patch", "delete", "head", "options"] permission_classes = [ permissions.IsAuthenticated, ] + def get_serializer_class(self): + if self.request and self.request.user.is_authenticated: + return DetailedAdminLendableObjectSerializer + return super().get_serializer_class() + class LendingInstanceViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendingInstance.objects.all() - serializer_class = LendingInstanceSerializer + 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, + ) + + def get_serializer_class(self): + if self.request and self.request.user.is_authenticated: + return DetailedAdminLendingInstanceSerializer + return super().get_serializer_class() + def create(self, request): - serializer = LendingInstanceSerializer( + serializer = DetailedLendingInstanceSerializer( data=request.data, context={"request": request} ) if serializer.is_valid(raise_exception=True): From afc32a32693b8ee6ce58ac938d3418d2249d6e48 Mon Sep 17 00:00:00 2001 From: Vebjorn Date: Tue, 12 Mar 2024 19:45:45 +0100 Subject: [PATCH 28/29] Implement lending system --- docs/conf.py | 12 +- lego/api/v1.py | 5 +- lego/apps/comments/models.py | 28 +- lego/apps/comments/serializers.py | 11 +- lego/apps/comments/tests/test_reactions.py | 56 ++++ lego/apps/email/filters.py | 25 +- lego/apps/email/serializers.py | 31 +- lego/apps/email/tests/test_views.py | 78 ++++- lego/apps/events/constants.py | 1 + .../0040_alter_registration_presence.py | 26 ++ lego/apps/events/models.py | 41 ++- lego/apps/events/serializers/registrations.py | 12 +- lego/apps/events/tests/test_penalties.py | 66 ++++- .../joblistings/tests/test_joblistings_api.py | 22 +- lego/apps/joblistings/views.py | 12 +- lego/apps/lending/filters.py | 2 +- .../development_lendable_objects.yaml | 6 +- .../development_lending_requests.yaml | 6 +- lego/apps/lending/managers.py | 19 +- lego/apps/lending/migrations/0001_initial.py | 268 +++++++++++++++--- .../0002_lendableobject_location.py | 18 -- ...endableobject_responsible_role_and_more.py | 38 --- .../migrations/0004_lendableobject_image.py | 21 -- .../0005_remove_lendinginstance_user.py | 17 -- .../0006_lendinginstance_comment.py | 18 -- lego/apps/lending/models.py | 43 ++- lego/apps/lending/notifications.py | 3 +- lego/apps/lending/permissions.py | 58 +++- lego/apps/lending/serializers.py | 65 ++++- lego/apps/lending/tests.py | 3 - lego/apps/lending/urls.py | 11 + lego/apps/lending/validators.py | 2 +- lego/apps/lending/views.py | 47 +-- ...r_notificationsetting_notification_type.py | 32 ++- ...0016_alter_notificationsetting_channels.py | 28 ++ .../migrations/0016_merge_20240312_1850.py | 12 + .../migrations/0017_merge_20240423_1728.py | 12 + lego/apps/notifications/models.py | 1 + lego/apps/notifications/notification.py | 2 +- lego/apps/search/backend.py | 2 +- lego/apps/search/backends/elasticsearch.py | 4 +- lego/apps/search/backends/postgres.py | 4 +- lego/apps/stats/analytics_client.py | 6 + lego/apps/tags/tests/test_views.py | 4 +- lego/apps/tags/views.py | 3 +- lego/apps/users/constants.py | 19 +- lego/apps/users/filters.py | 2 + .../users/fixtures/initial_abakus_groups.py | 7 +- .../users/fixtures/initial_abakus_groups.yaml | 4 +- .../users/fixtures/test_abakus_groups.yaml | 2 +- .../users/migrations/0042_penalty_type.py | 25 ++ .../migrations/0043_alter_abakusgroup_type.py | 30 ++ lego/apps/users/models.py | 5 + lego/apps/users/tests/test_abakusgroup_api.py | 11 +- lego/apps/users/tests/test_password_change.py | 2 + lego/apps/users/tests/test_users_api.py | 1 + lego/apps/users/views/users.py | 2 + .../management/commands/load_fixtures.py | 2 +- 58 files changed, 997 insertions(+), 296 deletions(-) create mode 100644 lego/apps/comments/tests/test_reactions.py create mode 100644 lego/apps/events/migrations/0040_alter_registration_presence.py delete mode 100644 lego/apps/lending/migrations/0002_lendableobject_location.py delete mode 100644 lego/apps/lending/migrations/0003_remove_lendableobject_responsible_role_and_more.py delete mode 100644 lego/apps/lending/migrations/0004_lendableobject_image.py delete mode 100644 lego/apps/lending/migrations/0005_remove_lendinginstance_user.py delete mode 100644 lego/apps/lending/migrations/0006_lendinginstance_comment.py delete mode 100644 lego/apps/lending/tests.py create mode 100644 lego/apps/lending/urls.py create mode 100644 lego/apps/notifications/migrations/0016_alter_notificationsetting_channels.py create mode 100644 lego/apps/notifications/migrations/0016_merge_20240312_1850.py create mode 100644 lego/apps/notifications/migrations/0017_merge_20240423_1728.py create mode 100644 lego/apps/users/migrations/0042_penalty_type.py create mode 100644 lego/apps/users/migrations/0043_alter_abakusgroup_type.py 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", ] ) From cd5814475a680b42d8ded8d74e85ef59ccc20352 Mon Sep 17 00:00:00 2001 From: Jonas de Luna Skulberg Date: Fri, 24 May 2024 13:17:23 +0200 Subject: [PATCH 29/29] Remove notifications --- lego/apps/lending/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index 827d5b935..83ed6c461 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -4,7 +4,6 @@ from django.db import models from lego.apps.files.models import FileField -from lego.apps.lending.managers import LendingInstanceManager from lego.apps.lending.permissions import ( LendableObjectPermissionHandler, LendingInstancePermissionHandler, @@ -65,8 +64,6 @@ class LendingInstanceStatus(models.TextChoices): message = models.TextField(null=False, blank=True) - objects = LendingInstanceManager() - @property def active(self): return timezone.now() < self.end_date and timezone.now() > self.start_date