diff --git a/fet2020/fet2020/settings.py b/fet2020/fet2020/settings.py index df2d89ac..a48b9eac 100644 --- a/fet2020/fet2020/settings.py +++ b/fet2020/fet2020/settings.py @@ -61,6 +61,7 @@ INSTALLED_APPS = [ "gallery.apps.GalleryConfig", "intern.apps.InternConfig", "finance.apps.FinanceConfig", + "rental.apps.RentalConfig", ] diff --git a/fet2020/fet2020/urls.py b/fet2020/fet2020/urls.py index d5b0f952..598ddee2 100644 --- a/fet2020/fet2020/urls.py +++ b/fet2020/fet2020/urls.py @@ -38,6 +38,7 @@ urlpatterns = [ path("intern/", include("intern.urls")), path("jobs/", include("blackboard.urls")), path("posts/", include("posts.urls")), + path("rental/", include("rental.urls")), path("search/", include("search.urls")), path( "discord/", diff --git a/fet2020/rental/__init__.py b/fet2020/rental/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fet2020/rental/admin.py b/fet2020/rental/admin.py new file mode 100644 index 00000000..c8729b05 --- /dev/null +++ b/fet2020/rental/admin.py @@ -0,0 +1,84 @@ +from django.contrib import admin + +from .forms import RentalAdminForm, RentalItemAdminForm +from .models import Rental, RentalItem + + +@admin.register(Rental) +class RentalAdmin(admin.ModelAdmin): + form = RentalAdminForm + model = Rental + + list_display = [ + "id", + "firstname", + "surname", + "status", + "date_start", + "date_end", + ] + fieldsets = ( + ( + "Persönliche Daten", + { + "fields": ( + ("firstname", "surname"), + ("organization", "matriculation_number"), + ("email", "phone"), + ), + }, + ), + ( + "Verleih", + { + "fields": ( + ("date_start", "date_end"), + "reason", + "rentalitems", + ), + }, + ), + ( + "Sonstiges", + { + "fields": ( + "comment", + "status", + ), + }, + ), + ) + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view(request, form_url, extra_context=extra_context) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().change_view(request, object_id, form_url, extra_context=extra_context) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) + + +@admin.register(RentalItem) +class RentalItemAdmin(admin.ModelAdmin): + form = RentalItemAdminForm + model = RentalItem + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view(request, form_url, extra_context=extra_context) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().change_view(request, object_id, form_url, extra_context=extra_context) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) diff --git a/fet2020/rental/apps.py b/fet2020/rental/apps.py new file mode 100644 index 00000000..a87f86ec --- /dev/null +++ b/fet2020/rental/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig +from django.db.models.signals import post_migrate + +from fet2020.utils import create_perms + + +class RentalConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "rental" + + def ready(self): + post_migrate.connect(create_perms, sender=self) diff --git a/fet2020/rental/forms.py b/fet2020/rental/forms.py new file mode 100644 index 00000000..12b90ee6 --- /dev/null +++ b/fet2020/rental/forms.py @@ -0,0 +1,60 @@ +from django import forms +from django.forms import DateInput + +from .models import Rental, RentalItem + + +class DateInput(DateInput): + input_type = "date" + + +class RentalCreateForm(forms.ModelForm): + # Conformation + conformation = forms.BooleanField( + required=True, + label=( + "1. Fachschaft Elektrotechnik (FET) hat stets Vorrecht. 2. Bereits bestätigte Objekte " + "können storniert werden, falls FET sie selber braucht. 3. Sachen dürfen nur über das " + "Verleihsystem verborgt werden. 4. Objekte müssen pünktlich und gereinigt retouniert " + "werden. 5. Verleihpersonen entscheiden über Kaution (lediglich für Pünktlichkeit und " + "Reinheit) 6. Geht was kaputt, muss dies umgehend mitgeteilt werden und finanziell " + "dafür aufgekommen werden. " + ), + initial=False, + ) + + class Meta: + model = Rental + + fields = [ + "firstname", + "surname", + "matriculation_number", + "email", + "phone", + "organization", + "date_start", + "date_end", + "reason", + "comment", + "rentalitems", + ] + + widgets = { + "date_start": DateInput(format=("%Y-%m-%d")), + "date_end": DateInput(format=("%Y-%m-%d")), + } + + +class RentalAdminForm(forms.ModelForm): + class Meta: + model = Rental + fields = "__all__" + + widgets = {"rentalitems": forms.CheckboxSelectMultiple()} + + +class RentalItemAdminForm(forms.ModelForm): + class Meta: + model = RentalItem + fields = "__all__" diff --git a/fet2020/rental/models.py b/fet2020/rental/models.py new file mode 100644 index 00000000..b3ec61bb --- /dev/null +++ b/fet2020/rental/models.py @@ -0,0 +1,71 @@ +from django.db import models +from django.forms import ValidationError + +from .validators import PhoneNumberValidator + + +class RentalItem(models.Model): + name = models.CharField(verbose_name="Name", max_length=128) + description = models.CharField( + verbose_name="Beschreibung", max_length=128, blank=True, default="" + ) + + class Meta: + verbose_name = "Verleihgegenstand" + verbose_name_plural = "Verleihgegenstände" + + def __str__(self): + return self.name + + +class Rental(models.Model): + firstname = models.CharField(max_length=128, verbose_name="Vorname") + surname = models.CharField(max_length=128, verbose_name="Nachname") + + matriculation_number = models.CharField(verbose_name="Matrikelnummer", max_length=8) + email = models.EmailField(verbose_name="E-Mail") + phone = models.CharField( + verbose_name="Telefonnummer", max_length=32, validators=[PhoneNumberValidator()] + ) + + organization = models.CharField(verbose_name="Organisation", max_length=128) + + date_start = models.DateField(verbose_name="Abholdatum") + date_end = models.DateField(verbose_name="Rückgabedatum") + + reason = models.TextField(verbose_name="Grund der Ausleihe", max_length=500) + + comment = models.TextField(verbose_name="Kommentar", max_length=500, blank=True, default="") + + class Status(models.TextChoices): + SUBMITTED = "S", "Eingereicht" + APPROVED = "A", "Verleih genehmigt" + REJECTED = "J", "Verleih abgelehnt" + ISSUED = "I", "Verleihgegenstände ausgegeben" + RETURNED = "R", "Verleihgegenstände zurückgegeben" + + status = models.CharField( + verbose_name="Status", + max_length=1, + choices=Status.choices, + default=Status.SUBMITTED, + ) + + rentalitems = models.ManyToManyField( + RentalItem, + verbose_name="Verleihgegenstände", + ) + + class Meta: + verbose_name = "Verleih" + verbose_name_plural = "Verleih" + + def __str__(self): + return f"Verleih #{self.id}: {self.firstname} {self.surname}" + + def clean(self): + if not self.date_end: + self.date_end = self.date_start + + if self.date_start > self.date_end: + raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.") diff --git a/fet2020/rental/tests.py b/fet2020/rental/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/fet2020/rental/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/fet2020/rental/urls.py b/fet2020/rental/urls.py new file mode 100644 index 00000000..f350689e --- /dev/null +++ b/fet2020/rental/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from . import apps +from .views import RentalCreateDoneView, RentalCreateView, RentalListView + +app_name = apps.RentalConfig.name + +urlpatterns = [ + path("rental/", RentalListView.as_view(), name="index"), + path("request-rental/", RentalCreateView.as_view(), name="rental_create"), + path( + "request-rental//done/", + RentalCreateDoneView.as_view(), + name="rental_create_done", + ), +] diff --git a/fet2020/rental/validators.py b/fet2020/rental/validators.py new file mode 100644 index 00000000..486e044d --- /dev/null +++ b/fet2020/rental/validators.py @@ -0,0 +1,11 @@ +from django.core.validators import RegexValidator +from django.utils.deconstruct import deconstructible + + +@deconstructible +class PhoneNumberValidator(RegexValidator): + regex = r"^\+?1?\d{9,15}$" + message = ( + "Telefonnummer muss in diesem Format +999999999999 eingegeben werden. Bis zu 15 Zahlen " + "sind erlaubt." + ) diff --git a/fet2020/rental/views.py b/fet2020/rental/views.py new file mode 100644 index 00000000..3dc3a047 --- /dev/null +++ b/fet2020/rental/views.py @@ -0,0 +1,152 @@ +import calendar +import datetime + +from django.db.models import Q +from django.urls import reverse +from django.views.generic import ListView, TemplateView +from django.views.generic.edit import CreateView + +from .forms import RentalCreateForm +from .models import Rental, RentalItem + + +class RentalListView(ListView): + model = Rental + template_name = "rental/calendar.html" + + # Month is the month displayed in the calendar (and should be the first day of the month) + month = None + # Rental items to filter + rentalitem_filters = [] + + def get(self, request, *args, **kwargs): + # Get the rental items from the filter + self.rentalitem_filters = request.GET.getlist("rentalitems", []) + if not self.rentalitem_filters: + for rentalitem in RentalItem.objects.all(): + self.rentalitem_filters.append(rentalitem.name) + + # Get the displayed month from the request + _date_str = request.GET.get("month", "") + if _date_str: + self.month = ( + datetime.datetime.strptime(_date_str, "%Y-%m").replace(tzinfo=datetime.UTC).date() + ) + else: + self.month = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) + + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # Calculate the day of the previous month from Monday + days_of_prev_month = [] + + if self.month.weekday() != calendar.MONDAY: + for i in range(1, 7): + day = self.month + datetime.timedelta(days=-i) + days_of_prev_month.append(day) + if day.weekday() == calendar.MONDAY: + break + + # Calculate the days of the next month until Sunday + last_day_of_month = self.month.replace( + day=calendar.monthrange(self.month.year, self.month.month)[1] + ) + days_of_next_month = [] + + if last_day_of_month.weekday() != calendar.SUNDAY: + for i in range(1, 7): + day = last_day_of_month + datetime.timedelta(days=i) + days_of_next_month.append(day) + if day.weekday() == calendar.SUNDAY: + break + + # Calculate the days of the displayed month + days_of_month = [ + self.month + datetime.timedelta(days=i) + for i in range((last_day_of_month - self.month).days + 1) + ] + + # Create a dictionary with the rental items for each day + rental_dict = {} + for rental in self.get_queryset(): + for day in days_of_month: + if rental["date_start"] <= day and rental["date_end"] >= day: + if day not in rental_dict: + rental_dict[day] = [] + + if rental["rentalitems__name"] not in rental_dict[day]: + rental_dict[day].append(rental["rentalitems__name"]) + + # Add the displayed, previous and next month + context["month"] = self.month + context["prev_month"] = self.month + datetime.timedelta(days=-1) + context["next_month"] = self.month + datetime.timedelta( + days=calendar.monthrange(self.month.year, self.month.month)[1] + 1 + ) + + # Add the days of the displayed, previous and next month + context["days_of_month"] = days_of_month + context["days_of_prev_month"] = sorted(days_of_prev_month) + context["days_of_next_month"] = days_of_next_month + + # Get the current date for the calendar + context["today"] = datetime.datetime.now(tz=datetime.UTC).date() + + # Add rental items to the context for the filter + context["rentalitems"] = RentalItem.objects.all() + + # Add the (max. 4) selected rental items to the context for the filter + context["rentalitem_filters"] = {"rentalitems": self.rentalitem_filters[:4]} + + context["rental_dict"] = rental_dict + + return context + + def get_queryset(self): + qs = ( + super() + .get_queryset() + .filter( + Q(status=Rental.Status.APPROVED) + | Q(status=Rental.Status.ISSUED) + | Q(status=Rental.Status.RETURNED) + ) + ) + + last_day_of_month = self.month.replace( + day=calendar.monthrange(self.month.year, self.month.month)[1] + ) + + # Filter by date + qs_new = qs.filter(date_start__gte=self.month, date_start__lte=last_day_of_month) + qs_new |= qs.filter(date_end__gte=self.month, date_end__lte=last_day_of_month) + + # Filter by rental items + qs = qs.filter(rentalitems__name__in=self.rentalitem_filters).distinct() + + qs = qs.values("id", "date_start", "date_end", "rentalitems__name").distinct() + + return qs + + +class RentalCreateView(CreateView): + form_class = RentalCreateForm + model = Rental + template_name = "rental/create.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context["rentalitems_addinfo"] = RentalItem.objects.all() + + return context + + def get_success_url(self): + return reverse("rental:rental_create_done", kwargs={"pk": self.object.pk}) + + +class RentalCreateDoneView(TemplateView): + template_name = "rental/create_done.html" diff --git a/fet2020/templates/base.html b/fet2020/templates/base.html index 3eef34f7..46bc2980 100644 --- a/fet2020/templates/base.html +++ b/fet2020/templates/base.html @@ -111,6 +111,7 @@
  • News
  • Fachschaft
  • Galerie
  • +
  • Verleih
  • Jobs
  • {% get_flatpages '/kontakt/' as pages %} diff --git a/fet2020/templates/baseform/email.html b/fet2020/templates/baseform/email.html new file mode 100644 index 00000000..f96cc4ac --- /dev/null +++ b/fet2020/templates/baseform/email.html @@ -0,0 +1,20 @@ + diff --git a/fet2020/templates/rental/calendar.html b/fet2020/templates/rental/calendar.html new file mode 100644 index 00000000..e10e60c5 --- /dev/null +++ b/fet2020/templates/rental/calendar.html @@ -0,0 +1,187 @@ +{% extends 'base.html' %} + +{% load static %} + +{% block title %}Verleih{% endblock %} + +{% block content %} +
    +

    Verleih

    + +
    +

    + Willkommen bei unserem Verleih! +

    + + + Verleih anfragen + +
    + +
    +
    +
    + + + + +
    +
    + +
    +
    +
    + {{ month|date:'F Y' }} + +
    + + +
    +
    + + + + + + + + + + + + + + + + {% for day in days_of_prev_month %} + {% if day.weekday == 0 %} + + {% endif %} + + + {% endfor %} + + {% for day in days_of_month %} + {% if day.weekday == 0 %} + + {% endif %} + + + + {% if day.weekday == 6 %} + + {% endif %} + {% endfor %} + + {% for day in days_of_next_month %} + + + {% if day.weekday == 6 %} + + {% endif %} + {% endfor %} + + +
    + + Mon + + + Tue + + + Wed + + + Thu + + + Fri + + + Sat + + + Sun +
    +
    +
    + {{ day.day }} +
    +
    +
    +
    +
    +
    + {% if day == today %} + {{ day.day }} + {% else %} + {{ day.day }} + {% endif %} +
    + + {% for key, names in rental_dict.items %} + {% if key == day %} + {% for name in names %} +
    +
    + + + + + {{ name|truncatechars:3 }} +
    +
    + {% empty %} +
    + {% endfor %} + {% endif %} + {% endfor %} +
    +
    +
    +
    + {{ day.day }} +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock content %} diff --git a/fet2020/templates/rental/create.html b/fet2020/templates/rental/create.html new file mode 100644 index 00000000..b9de3dcc --- /dev/null +++ b/fet2020/templates/rental/create.html @@ -0,0 +1,112 @@ +{% extends 'base.html' %} + +{% block title %}Verleih Anfrage{% endblock %} + +{% block content %} + +
    +

    Verleih Anfrage

    + +
    + {% csrf_token %} + {% include "baseform/non_field_errors.html" %} + +
    +

    Persönliche Daten

    + Bitte geben Sie Ihre persönlichen Daten ein. + +
    +
    + {% include "baseform/text.html" with field=form.firstname %} +
    +
    + {% include "baseform/text.html" with field=form.surname %} +
    +
    + {% include "baseform/text.html" with field=form.organization %} +
    +
    + {% include "baseform/text.html" with field=form.matriculation_number %} +
    +
    + {% include "baseform/email.html" with field=form.email %} +
    +
    + {% include "baseform/text.html" with field=form.phone %} +
    +
    +
    + +
    +

    Verleihgegenstände

    + Wählen Sie die gewünschten Verleihgegenstände aus. + +
    + {% if form.rentalitems.errors %} +
    +
    {{ form.rentalitems.errors }}
    +
    + {% endif %} + +
    + {% for elem in form.rentalitems %} +
    + + + + {% for item in rentalitems_addinfo %} + {% if item.name == elem.choice_label and item.description %} +

    {{ item.description }}

    + {% endif %} + {% endfor %} +
    + {% endfor %} +
    +
    +
    +
    + {% include "baseform/date.html" with field=form.date_start %} +
    +
    + {% include "baseform/date.html" with field=form.date_end %} +
    +
    + {% include "baseform/textarea.html" with field=form.reason %} +
    +
    +
    + +
    +

    Zusätzliche Informationen

    + Hier können Sie zusätzliche Informationen, Anliegen und Sonstiges angeben. + +
    +
    + {% include "baseform/textarea.html" with field=form.comment %} +
    +
    +
    + +
    +
    +
    + {% include "baseform/checkbox.html" with field=form.conformation %} +
    +
    +
    + +
    + +
    +
    +
    +{% endblock content %} diff --git a/fet2020/templates/rental/create_done.html b/fet2020/templates/rental/create_done.html new file mode 100644 index 00000000..a90b7c03 --- /dev/null +++ b/fet2020/templates/rental/create_done.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %} + +{% block content %} + +
    +
    +

    + Die Verleihanfrage mit der Nummer #{{ pk }} wurde erfolgreich eingereicht. +

    + +
    +
    +{% endblock content %}