From 51b526ccc9f12cb4b41e5f120cb3c909713a95ba Mon Sep 17 00:00:00 2001 From: Arash Farzaneh Taleghani Date: Wed, 4 Oct 2023 22:54:07 +0200 Subject: [PATCH] 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 c038b597cb..6571bd7417 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 0000000000..e69de29bb2 diff --git a/lego/apps/lending/apps.py b/lego/apps/lending/apps.py new file mode 100644 index 0000000000..608fc9e03f --- /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 0000000000..53773c7fd0 --- /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 0000000000..e32eea456b --- /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 0000000000..f226fc45ad --- /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 0000000000..e69de29bb2 diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py new file mode 100644 index 0000000000..9a9d448c31 --- /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 0000000000..93d956b9c6 --- /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 0000000000..9adc9935f4 --- /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 0000000000..7ce503c2dd --- /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 0000000000..9f9ef333b2 --- /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 0000000000..e37243ddc5 --- /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 0000000000..1dccb1bd8d --- /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 0000000000..f35482044c --- /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 b1d3013701..d3451c2267 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"