From 81f7d6eb667d8e77c6ec30e90dcbec1ac25fc8af Mon Sep 17 00:00:00 2001 From: Klaus-Uwe Mitterer Date: Tue, 20 Apr 2021 08:36:59 +0200 Subject: [PATCH] Flow improvements Dynamic displaying of received offers --- auction/admin.py | 7 ++- auction/models.py | 31 ++++++++-- auction/views.py | 22 +++++-- localsettings.dist.py | 6 +- partners/admin.py | 8 ++- partners/models.py | 15 ++++- partners/templatetags/offeroptions.py | 6 +- partners/urls.py | 3 +- partners/views.py | 44 +++++++++++++- requirements.txt | 1 + templates/auction/bidding_list.html | 6 +- templates/auction/offer_create.html | 13 +++- templates/auction/offer_select.html | 6 ++ templates/auction/offer_table.js | 26 +++++--- templates/auction/process.html | 6 +- templates/clients/bookings.html | 22 +++++-- templates/partners/establishment_list.html | 4 +- templates/partners/profile.html | 2 +- templates/partners/roomcategory_list.html | 71 ++++++++++++++++++++++ urlaubsauktion/settings.py | 2 + 20 files changed, 256 insertions(+), 45 deletions(-) create mode 100644 templates/partners/roomcategory_list.html diff --git a/auction/admin.py b/auction/admin.py index 8c38f3f..bf85077 100644 --- a/auction/admin.py +++ b/auction/admin.py @@ -1,3 +1,6 @@ -from django.contrib import admin +from urlaubsauktion.admin import joker_admin as admin -# Register your models here. +from .models import Inquiry, Offer + +admin.register(Inquiry) +admin.register(Offer) \ No newline at end of file diff --git a/auction/models.py b/auction/models.py index 270ece7..2d61669 100644 --- a/auction/models.py +++ b/auction/models.py @@ -1,8 +1,11 @@ from django.contrib.gis.db import models from django.utils import timezone +from django.conf import settings from clients.models import ClientProfile -from partners.models import Establishment +from partners.models import Establishment, RoomCategory + +from dateutil.relativedelta import relativedelta import uuid @@ -37,10 +40,19 @@ class Inquiry(models.Model): self.activated = timezone.now() self.save() + @property + def expiry(self): + if self.activated: + return self.activated + relativedelta(days=settings.INQUIRY_RUNTIME) + + @property + def expired(self): + return self.expiry and (self.expiry < timezone.now()) + @property def active(self): try: - return bool(self.activated) + return bool(self.activated) and not self.expired except: return False @@ -52,9 +64,18 @@ class Inquiry(models.Model): return qset[0] class Offer(models.Model): + uuid = models.UUIDField(default=uuid.uuid4, unique=True) inquiry = models.ForeignKey(Inquiry, models.PROTECT) - establishment = models.ForeignKey(Establishment, models.PROTECT) - nights = models.IntegerField() + roomcategory = models.ForeignKey(RoomCategory, models.PROTECT) + departure = models.DateField() comment = models.TextField(null=True, blank=True) accepted = models.BooleanField(default=False) - hidden = models.BooleanField(default=False) \ No newline at end of file + hidden = models.BooleanField(default=False) + + @property + def establishment(self): + return self.roomcategory.establishment + + @property + def nights(self): + return (self.departure - self.inquiry.arrival).days \ No newline at end of file diff --git a/auction/views.py b/auction/views.py index f3e1de4..6d76645 100644 --- a/auction/views.py +++ b/auction/views.py @@ -1,7 +1,7 @@ from django.views.generic import CreateView, UpdateView, View, ListView, DetailView from django.shortcuts import redirect, get_object_or_404 from django.contrib import messages -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.contrib.gis.geos import Point from django.contrib.gis.db.models.functions import Distance from django.conf import settings @@ -110,14 +110,16 @@ class OfferSelectionTableView(InConstructionMixin, ListView): def get_queryset(self): inquiry = get_object_or_404(Inquiry, uuid=self.kwargs["uuid"]) + return inquiry.offer_set.all() class OfferCreationView(InConstructionMixin, PartnerProfileRequiredMixin, CreateView): model = Offer template_name = "auction/offer_create.html" - fields = [] + fields = ["roomcategory", "departure", "comment"] def dispatch(self, request, *args, **kwargs): self.establishment = self.get_establishment() + self.inquiry = self.get_inquiry() if not self.establishment: messages.warning(request, "Um bieten zu können, muss zuerst eine Unterkunft im System hinterlegt werden!") @@ -135,11 +137,22 @@ class OfferCreationView(InConstructionMixin, PartnerProfileRequiredMixin, Create else: return Establishment.objects.filter(**kwargs).first() + def get_inquiry(self): + return get_object_or_404(Inquiry, uuid=self.kwargs.get("inquiry")) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["establishment"] = self.establishment - context["inquiry"] = get_object_or_404(Inquiry, uuid=self.kwargs.get("inquiry")) + context["inquiry"] = self.inquiry return context + + def form_valid(self, form): + form.instance.inquiry = self.inquiry + return super().form_valid(form) + + def get_success_url(self): + messages.success(self.request, "Angebot erfolgreich erstellt! Viel Erfolg!") + return reverse_lazy("auction:bidding", args=[self.establishment.id]) class BiddingListView(InConstructionMixin, PartnerProfileRequiredMixin, ListView): model = Inquiry @@ -166,7 +179,8 @@ class BiddingListView(InConstructionMixin, PartnerProfileRequiredMixin, ListView def get_queryset(self): establishment = self.get_establishment() - return Inquiry.objects.annotate(distance=Distance("destination_coords", establishment.coords)).exclude(activated__isnull=True) + excluded = [offer.inquiry.id for offer in establishment.offer_set.all()] + return Inquiry.objects.annotate(distance=Distance("destination_coords", establishment.coords)).exclude(activated__isnull=True).exclude(id__in=excluded) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/localsettings.dist.py b/localsettings.dist.py index 81167a4..0140300 100644 --- a/localsettings.dist.py +++ b/localsettings.dist.py @@ -34,4 +34,8 @@ JOKER_COUNTRIES = ["AT"] CURRENCY_SYMBOL = "€" CURRENCY_CODE = "EUR" -CURRENCY_NAME = "Euro" \ No newline at end of file +CURRENCY_NAME = "Euro" + +# Run time of inquiries in days + +INQUIRY_RUNTIME = 7 \ No newline at end of file diff --git a/partners/admin.py b/partners/admin.py index 8c38f3f..88a9da2 100644 --- a/partners/admin.py +++ b/partners/admin.py @@ -1,3 +1,7 @@ -from django.contrib import admin +from urlaubsauktion.admin import joker_admin as admin -# Register your models here. +from .models import PartnerProfile, Establishment, RoomCategory + +admin.register(PartnerProfile) +admin.register(Establishment) +admin.register(RoomCategory) \ No newline at end of file diff --git a/partners/models.py b/partners/models.py index 33458a3..a7a2083 100644 --- a/partners/models.py +++ b/partners/models.py @@ -18,7 +18,7 @@ class Establishment(LocationMixin, ImageMixin, PhoneMixin): superior = models.BooleanField("Superior", default=False) verified = models.BooleanField(default=False) active = models.BooleanField(default=True) - featureset = models.OneToOneField(FeatureSet, models.PROTECT, null=True) + featureset = models.OneToOneField(FeatureSet, models.PROTECT, null=True, blank=True) @property def user(self): @@ -32,7 +32,16 @@ class Establishment(LocationMixin, ImageMixin, PhoneMixin): def booking_set(self): return self.offer_set.filter(accepted=True) + @property + def offer_set(self): + querysets = [] + for roomcategory in self.roomcategory_set.all(): + querysets.append(roomcategory.offer_set.all()) + + return querysets[0].union(*querysets[1:]) + class RoomCategory(ImageMixin): establishment = models.ForeignKey(Establishment, models.CASCADE) - name = models.CharField(max_length=64) - average_price = models.DecimalField(max_digits=10, decimal_places=2) + name = models.CharField("Name", max_length=64) + average_price = models.DecimalField("Durchschnittspreis / Nacht", max_digits=10, decimal_places=2) + active = models.BooleanField(default=True) \ No newline at end of file diff --git a/partners/templatetags/offeroptions.py b/partners/templatetags/offeroptions.py index 1810f07..f68e64a 100644 --- a/partners/templatetags/offeroptions.py +++ b/partners/templatetags/offeroptions.py @@ -5,8 +5,8 @@ register = template.Library() @register.simple_tag def stars(number, superior=False, color=""): number = int(number) - if not 1 <= number <= 5: - raise ValueError("Number of stars must be between 1 and 5.") + if not 0 <= number <= 5: + raise ValueError("Number of stars must be between 0 and 5.") output = "" @@ -22,7 +22,7 @@ def stars(number, superior=False, color=""): def hearts(number, color=""): number = int(number) if not 1 <= number <= 5: - raise ValueError("Number of hearts must be between 1 and 5.") + raise ValueError("Number of hearts must be between 0 and 5.") output = "" diff --git a/partners/urls.py b/partners/urls.py index b249180..27dc31d 100644 --- a/partners/urls.py +++ b/partners/urls.py @@ -1,7 +1,7 @@ from django.urls import path, reverse_lazy from django.views.generic import RedirectView -from .views import PartnerRegistrationView, PartnerProfileView, OffersListView, EstablishmentsListView, EstablishmentRequestView, PartnerDashboardView, EstablishmentVerificationView +from .views import PartnerRegistrationView, PartnerProfileView, OffersListView, EstablishmentsListView, EstablishmentRequestView, PartnerDashboardView, EstablishmentVerificationView, RoomCategoryListView app_name = "partners" @@ -9,6 +9,7 @@ urlpatterns = [ path('register/', PartnerRegistrationView.as_view(), name="register"), path('profile/', PartnerProfileView.as_view(), name="profile"), path('establishments/', EstablishmentsListView.as_view(), name="establishments"), + path('establishments//', RoomCategoryListView.as_view(), name="roomcategories"), path('establishments/validate/', EstablishmentVerificationView.as_view(), name="establishment_verify"), path('establishments/register/', EstablishmentRequestView.as_view(), name="establishment_register"), path('offers/', OffersListView.as_view(), name="offers"), diff --git a/partners/views.py b/partners/views.py index b1525e6..602b700 100644 --- a/partners/views.py +++ b/partners/views.py @@ -4,7 +4,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import get_list_or_404, redirect, get_object_or_404 from django.contrib import messages -from .models import PartnerProfile, Establishment +from .models import PartnerProfile, Establishment, RoomCategory from .mixins import PartnerProfileRequiredMixin from .forms import VerificationForm @@ -82,6 +82,46 @@ class EstablishmentsListView(InConstructionMixin, PartnerProfileRequiredMixin, L def get_queryset(self): return self.request.user.partnerprofile.establishment_set.all() +class RoomCategoryListView(InConstructionMixin, PartnerProfileRequiredMixin, CreateView, ListView): + model = RoomCategory + template_name = "partners/roomcategory_list.html" + fields = ["name", "average_price"] + + def dispatch(self, request, *args, **kwargs): + self.establishment = self.get_establishment() + + if not self.establishment: + messages.warning(request, "Um bieten zu können, muss zuerst eine Unterkunft im System hinterlegt werden!") + return redirect("partners:establishment_register") + + return super().dispatch(request, *args, **kwargs) + + def get_establishment(self): + establishment = self.kwargs.get("id", None) + kwargs = {"owner": self.request.user.partnerprofile} + + if establishment: + kwargs["id"] = establishment + return get_object_or_404(Establishment, **kwargs) + else: + return Establishment.objects.filter(**kwargs).first() + + def get_queryset(self): + establishment = self.establishment + return RoomCategory.objects.filter(establishment=establishment) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["establishment"] = self.establishment + return context + + def form_valid(self, form): + form.instance.establishment = self.establishment + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy("partners:roomcategories", args=[self.establishment.id]) + class EstablishmentRequestView(InConstructionMixin, PartnerProfileRequiredMixin, CreateView): model = Establishment template_name = "partners/establishment_register.html" @@ -112,4 +152,4 @@ class EstablishmentVerificationView(SuperUserRequiredMixin, FormView): messages.success(self.request, "Unterkunft %s bestätigt!" % eobj[0].name) - return HttpResponseRedirect(resolve_url("partners:establishment_verify")) \ No newline at end of file + return HttpResponseRedirect(reverse_lazy("partners:establishment_verify")) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6d99644..8a9a05d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ phonenumbers googlemaps Babel staticmap +django-mathfilters git+https://kumig.it/kumisystems/PyInvoice \ No newline at end of file diff --git a/templates/auction/bidding_list.html b/templates/auction/bidding_list.html index bd8ceac..7e861a4 100644 --- a/templates/auction/bidding_list.html +++ b/templates/auction/bidding_list.html @@ -15,7 +15,11 @@

{% trans "Verfügbare Anfragen" %}

-
für {{ establishment.name }}
+
für
diff --git a/templates/auction/offer_create.html b/templates/auction/offer_create.html index 50b5f1f..7570e59 100644 --- a/templates/auction/offer_create.html +++ b/templates/auction/offer_create.html @@ -3,6 +3,7 @@ {% load static %} {% load mapimage %} {% load dbsetting %} +{% load mathfilters %} {% block "content" %}
@@ -51,6 +52,10 @@
+ + + + @@ -112,7 +117,9 @@
@@ -145,10 +152,10 @@
- +
- + diff --git a/templates/auction/offer_select.html b/templates/auction/offer_select.html index 71ef4ea..525bf31 100644 --- a/templates/auction/offer_select.html +++ b/templates/auction/offer_select.html @@ -14,7 +14,13 @@

Angebote vergleichen

+ {% if object.offer_set.all %}
+ {% else %} + + {% endif %}
diff --git a/templates/auction/offer_table.js b/templates/auction/offer_table.js index a297333..4594427 100644 --- a/templates/auction/offer_table.js +++ b/templates/auction/offer_table.js @@ -6,30 +6,40 @@ { from: "", tableHeader: [ - ['Habbo Hotel
{% autoescape off %}{% stars 3 1 orange %}{% endautoescape %}
'], - ['Kumi Systems
{% autoescape off %}{% stars 1 0 orange %}{% endautoescape %}
'], + {% for offer in object_list %}['{{ offer.roomcategory.establishment.name }}
{% autoescape off %}{% stars offer.roomcategory.establishment.stars offer.roomcategory.establishment.superior orange %}{% endautoescape %}{% if offer.roomcategory.establishment.image %}
{% endif %}'],{% endfor %} ], rows : [ { - rowVal: [["1. Mai 2021", "1. Mai 2021"],["3. Mai 2021", "10. Mai 2021"],["2 Nächte", "9 Nächte"]], + rowVal: [ + [{% for offer in object_list %}'{{ offer.inquiry.arrival }}',{% endfor %}], + [{% for offer in object_list %}'{{ offer.departure }}',{% endfor %}], + [{% for offer in object_list %}'{{ offer.nights }} Nächte',{% endfor %}] + ], rowDesc: ["Anreise", "Abreise", "Dauer"] }, { - rowVal: [[{% autoescape off %}'{% hearts 3 red %} (304)', '{% hearts 5 red %} (1)'{% endautoescape %}]], + rowVal: [ + [{% for offer in object_list %}{% autoescape off %}'{% hearts 3 red %} (304)'{% endautoescape %},{% endfor %}] + ], rowDesc: ["Bewertungen"] }, { - rowVal: [['Doppelzimmer Luxus', 'Wohnzimmer'], ["1 King-Size Bett", "1 Couch"]], + rowVal: [ + [{% for offer in object_list %}'{{ offer.roomcategory.name }}',{% endfor %}], + [{% for offer in object_list %}' ',{% endfor %}] + ], rowDesc: ["Zimmerart", "Betten"] }, { - rowVal: [['Romantikpaket inkl. Frühstück am Bett, 2x 60 Minuten Hot-Stone-Massage, 0,75l Prosecco, Obstschale, Zugang zum Spa', 'Gratis Arbeitsgelegenheit inklusive!']], + rowVal: [ + [{% for offer in object_list %}'{{ offer.comment }}',{% endfor %}] + ], rowDesc: ["Zusätzliche Angaben"] }, { rowVal: [ - [200, 200], - ['', ''] + [{% for offer in object_list %}'€ {{ offer.inquiry.budget }}',{% endfor %}], + [{% for offer in object_list %}'Zahlungspflichtig buchen!',{% endfor %}] ], rowDesc: ["Fixpreis", ""] } diff --git a/templates/auction/process.html b/templates/auction/process.html index 229e73d..beeeecf 100644 --- a/templates/auction/process.html +++ b/templates/auction/process.html @@ -55,6 +55,10 @@ + + + + @@ -254,7 +258,7 @@ stripe

{% trans "Die Zahlung per Kreditkarte wird durch unseren Zahlungsdienstleister Stripe abgewickelt." %}

-

{% trans "Der gebotene Betrag wird autorisiert und erst bei verbindlicher Buchung einer Reise tatsächlich von Ihrer Kreditkarte abgebucht." %}

+

{% trans "Der gebotene Betrag wird autorisiert und erst bei verbindlicher Buchung einer Reise tatsächlich von Ihrer Kreditkarte abgebucht. Bei Auslaufen oder Stornierung Ihres Angebots wird der Betrag ohne Abzug wieder freigegeben." %}

diff --git a/templates/clients/bookings.html b/templates/clients/bookings.html index 06becac..5c83474 100644 --- a/templates/clients/bookings.html +++ b/templates/clients/bookings.html @@ -28,19 +28,29 @@
ID {% trans "Anreisedatum" %} {{ inquiry.arrival|date:"SHORT_DATE_FORMAT" }}
{% trans "Aufenthaltsdauer" %}{% if inquiry.min_nights == 0 %}Egal!{% elif inquiry.min_nights == 1 %}Kurztrip (2 – 3 Nächte){% else %}min. 3 Nächte{% endif %}
{% trans "Erwachsene" %} {{ inquiry.adults }} {% trans "Anreisedatum" %} {{ object.arrival|date:"SHORT_DATE_FORMAT" }}
{% trans "Aufenthaltsdauer" %}{% if object.min_nights == 0 %}Egal!{% elif object.min_nights == 1 %}Kurztrip (2 – 3 Nächte){% else %}min. 3 Nächte{% endif %}
{% trans "Erwachsene" %} {{ object.adults }}
+ {% for inquiry in object_list %} - + + {% endfor %}

12

April 2021

{{ inquiry.arrival.day }}

{{ inquiry.arrival|date:"E Y" }}

-

Habbo Hotel

+

{% if inquiry.accepted %}{{ inquiry.accepted.name }} ({{ inquiry.destination_name }}){% else %}{{ inquiry.destination_name }}{% endif %}

    -
  • Ankunft: 12. April 2021
  • -
  • Abreise: 19. April 2021
  • -
  • Preis: € 500,00
  • +
  • Ankunft: {{ inquiry.arrival }}
  • +
  • {% if inquiry.accepted %}Abreise{% else %}Dauer{% endif %}: {% if inquiry.accepted %}{{ inquiry.accepted.departure }}{% elif inquiry.min_nights == 0 %}Egal!{% elif inquiry.min_nights == 1 %}Kurztrip (2 – 3 Nächte){% elif inquiry.min_nights == 2 %}min. 3 Nächte{% endif %}
  • +
  • Preis: € {{ inquiry.budget }}
  • +
  • Status: {% if inquiry.accepted %}Fixiert{% elif inquiry.active %}In Gebotsphase ({{ inquiry.offer_set.all|length }} Gebote){% else %}Inaktiv{% endif %}
- + {% if inquiry.accepted %} + + {% elif inquiry.active %} + Gebote ansehen + {% elif inquiry.expired %} + {% else %} + Zahlung abschließen + {% endif %}
diff --git a/templates/partners/establishment_list.html b/templates/partners/establishment_list.html index 6c5acda..a33974b 100644 --- a/templates/partners/establishment_list.html +++ b/templates/partners/establishment_list.html @@ -35,9 +35,9 @@
  • Buchungen: {{ establishment.bookings | length }}
  • Status: {% if establishment.is_active %}aktiv{% else %}inaktiv{% endif %}
  • - +
    Zimmer und Ausstattung bearbeiten - Bieten + Bieten {% endfor %} diff --git a/templates/partners/profile.html b/templates/partners/profile.html index 828f63f..29e3e10 100644 --- a/templates/partners/profile.html +++ b/templates/partners/profile.html @@ -1,4 +1,4 @@ -{% extends "clients/base.html" %} +{% extends "partners/base.html" %} {% load i18n %} {% load bootstrap4 %} {% block "dashboardcontent" %} diff --git a/templates/partners/roomcategory_list.html b/templates/partners/roomcategory_list.html new file mode 100644 index 0000000..abfae59 --- /dev/null +++ b/templates/partners/roomcategory_list.html @@ -0,0 +1,71 @@ +{% extends "partners/base.html" %} +{% load i18n %} +{% load bootstrap4 %} +{% block "dashboardcontent" %} +
    +

    Deine Zimmerkategorien

    +
    für {{ establishment.name }}
    +
    +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + + {% for roomcategory in object_list %} + + + + + {% endfor %} + +
    +

    {{ roomcategory.name }}

    +
      +
    • Durchschnittspreis: {{ roomcategory.average_price }}
    • +
    • Buchungen: {{ roomcategory.bookings | length }}
    • +
    • Status: {% if roomcategory.active %}aktiv{% else %}inaktiv{% endif %}
    • +
    + Zimmer und Ausstattung bearbeiten +
    Bieten
    +
    +
    + +
    +{% endblock %} +{% block "modal" %} + +{% endblock %} \ No newline at end of file diff --git a/urlaubsauktion/settings.py b/urlaubsauktion/settings.py index c171d64..4cfd41f 100644 --- a/urlaubsauktion/settings.py +++ b/urlaubsauktion/settings.py @@ -14,6 +14,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.gis', 'phonenumber_field', 'localauth', 'public', @@ -24,6 +25,7 @@ INSTALLED_APPS = [ 'auction', 'bootstrap4', 'django_countries', + 'mathfilters', ] MIDDLEWARE = [