Compare commits
No commits in common. "main" and "old" have entirely different histories.
322 changed files with 76176 additions and 48056 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,9 +1,5 @@
|
|||
urlaubsauktion/database.py
|
||||
*.swp
|
||||
*.pyc
|
||||
__pycache__/
|
||||
migrations/
|
||||
venv/
|
||||
db.sqlite3
|
||||
localsettings.py
|
||||
uwsgi.local.sh
|
||||
config.ini
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
|||
[submodule "locale"]
|
||||
path = locale
|
||||
url = git@kumig.it:journeyjoker/journeyjoker-locale.git
|
||||
[submodule "dbsettings"]
|
||||
path = dbsettings
|
||||
url = git@kumig.it:kumisystems/dbsettings.git
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from urlaubsauktion.admin import joker_admin as admin
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Inquiry, Offer
|
||||
from auction.models import Inquiry
|
||||
|
||||
admin.register(Inquiry)
|
||||
admin.register(Offer)
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(Inquiry)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuctionConfig(AppConfig):
|
||||
name = 'auction'
|
||||
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
from django import forms
|
||||
from django.forms import ModelForm, DateField, DateInput
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
from profiles.models import ContactProfile
|
||||
from auction.models import Inquiry
|
||||
|
||||
from .models import Inquiry
|
||||
|
||||
class InquiryProcessForm(forms.ModelForm):
|
||||
first_name = forms.CharField(max_length=64, required=True)
|
||||
last_name = forms.CharField(max_length=64, required=True)
|
||||
street = forms.CharField(max_length=64, required=True)
|
||||
city = forms.CharField(max_length=64, required=True)
|
||||
zip = forms.CharField(max_length=16)
|
||||
state = forms.CharField(max_length=64)
|
||||
country = CountryField().formfield(required=True)
|
||||
terms = forms.BooleanField(required=True)
|
||||
class PostPaymentForm(ModelForm):
|
||||
class Meta:
|
||||
model = ContactProfile
|
||||
fields = ["first_name", "last_name", "address", "address2", "zipcode", "city", "country", "phone", "email"]
|
||||
|
||||
class InquiryForm(ModelForm):
|
||||
class Meta:
|
||||
model = Inquiry
|
||||
fields = ["gateway"]
|
||||
fields = ["amount", "first_date", "last_date", "destination_name", "adults", "children"]
|
||||
|
||||
class OfferSelectionForm(forms.Form):
|
||||
terms = forms.BooleanField(required=True)
|
||||
offer = forms.UUIDField(required=True)
|
||||
first_date = DateField(
|
||||
widget=DateInput(format='%d.%m.%Y'),
|
||||
input_formats=('%d.%m.%Y', )
|
||||
)
|
||||
|
||||
last_date = DateField(
|
||||
widget=DateInput(format='%d.%m.%Y'),
|
||||
input_formats=('%d.%m.%Y', )
|
||||
)
|
|
@ -1,110 +1,34 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.gis.db.models.functions import Distance
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models import Model, ForeignKey, UUIDField, DateTimeField, DecimalField, PositiveIntegerField, DateField, CharField, ForeignKey, CASCADE, SET_NULL
|
||||
from django.contrib.gis.db.models import PointField
|
||||
from django.urls import reverse_lazy
|
||||
from django.conf import settings
|
||||
|
||||
from clients.models import ClientProfile
|
||||
from partners.models import Establishment, RoomCategory
|
||||
from uuid import uuid4
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
from profiles.models import ClientProfile, ContactProfile
|
||||
from offers.models import Offer
|
||||
|
||||
from datetime import timedelta
|
||||
class Inquiry(Model):
|
||||
uuid = UUIDField(default=uuid4, primary_key=True)
|
||||
user = ForeignKey(ClientProfile, null=True, on_delete=SET_NULL)
|
||||
amount = DecimalField(max_digits=25, decimal_places=2)
|
||||
currency = CharField(max_length=3, choices=settings.CURRENCIES)
|
||||
first_date = DateField()
|
||||
last_date = DateField()
|
||||
destination_name = CharField(max_length=128)
|
||||
destination_geo = PointField()
|
||||
adults = PositiveIntegerField()
|
||||
children = PositiveIntegerField(default=0)
|
||||
posted = DateTimeField(auto_now_add=True)
|
||||
contact = ForeignKey(ContactProfile, null=True, on_delete=SET_NULL)
|
||||
|
||||
import uuid
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("auction:payment", kwargs={'pk': self.uuid})
|
||||
|
||||
class LengthChoices(models.IntegerChoices):
|
||||
ANY = 0
|
||||
SHORT = 1
|
||||
LONG = 2
|
||||
class Offer(Model):
|
||||
uuid = UUIDField(default=uuid4, primary_key=True)
|
||||
inquiry = ForeignKey(Inquiry, on_delete=CASCADE)
|
||||
offer = ForeignKey(Offer, on_delete=CASCADE)
|
||||
arrival_date = DateField()
|
||||
departure_date = DateField()
|
||||
|
||||
class Inquiry(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
client = models.ForeignKey(ClientProfile, models.PROTECT, null=True, blank=True)
|
||||
destination_name = models.CharField(max_length=128)
|
||||
destination_coords = models.PointField()
|
||||
destination_radius = models.IntegerField()
|
||||
arrival = models.DateField()
|
||||
min_nights = models.IntegerField(default=0, choices=LengthChoices.choices)
|
||||
budget = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
adults = models.IntegerField()
|
||||
children = models.IntegerField()
|
||||
comment = models.TextField(null=True, blank=True)
|
||||
activated = models.DateTimeField(null=True, blank=True)
|
||||
bidding_end = models.DateTimeField(null=True, blank=True)
|
||||
auction_end = models.DateTimeField(null=True, blank=True)
|
||||
expiry = models.DateTimeField(null=True, blank=True)
|
||||
gateway = models.CharField(max_length=128, null=True, blank=True)
|
||||
|
||||
@property
|
||||
def is_paid(self):
|
||||
if not self.invoice:
|
||||
return False
|
||||
|
||||
return self.invoice.is_paid
|
||||
|
||||
def activate(self):
|
||||
if self.activated:
|
||||
return False
|
||||
|
||||
self.activated = timezone.now()
|
||||
self.bidding_end = self.activated + timedelta(hours=int(getValue("auction.bidding_period", 24)))
|
||||
self.auction_end = self.bidding_end + timedelta(hours=int(getValue("auction.selection_period", 24)))
|
||||
self.save()
|
||||
|
||||
def process_payment(self):
|
||||
if self.invoice.is_paid:
|
||||
self.activate()
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
return self.expiry and (self.expiry < timezone.now())
|
||||
|
||||
@property
|
||||
def in_bidding(self):
|
||||
return self.active and (self.bidding_end > timezone.now())
|
||||
|
||||
@property
|
||||
def in_selection(self):
|
||||
return self.active and (self.auction_end > timezone.now()) and not self.in_bidding
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
try:
|
||||
return bool(self.activated) and not self.expired
|
||||
except:
|
||||
return False
|
||||
|
||||
@property
|
||||
def accepted(self):
|
||||
try:
|
||||
return Offer.objects.get(inquiry=self, accepted__isnull=False)
|
||||
except Offer.DoesNotExist:
|
||||
return False
|
||||
|
||||
class Offer(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
inquiry = models.ForeignKey(Inquiry, models.PROTECT)
|
||||
roomcategory = models.ForeignKey(RoomCategory, models.PROTECT)
|
||||
departure = models.DateField()
|
||||
comment = models.TextField(null=True, blank=True)
|
||||
accepted = models.DateTimeField(null=True, blank=True)
|
||||
hidden = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def establishment(self):
|
||||
return self.roomcategory.establishment
|
||||
|
||||
@property
|
||||
def nights(self):
|
||||
return (self.departure - self.inquiry.arrival).days
|
||||
|
||||
@property
|
||||
def distance(self):
|
||||
return self.inquiry.destination_coords.distance(self.roomcategory.establishment.coords)
|
||||
|
||||
@receiver(post_save, sender=Inquiry)
|
||||
def inquiry_expiry(sender, instance, created, **kwargs):
|
||||
if not instance.expiry:
|
||||
instance.expiry = min(instance.arrival, (timezone.now() + timedelta(hours=int(getValue("auction.payment_period", 168)))).date())
|
||||
instance.save()
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
from django import template
|
||||
|
||||
from geopy.distance import geodesic
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def distance(pointa, pointb):
|
||||
return geodesic((pointa.x, pointa.y), (pointb.x, pointb.y)).m
|
|
@ -1,18 +1,11 @@
|
|||
from django.urls import path
|
||||
|
||||
from frontend.views import HomeView
|
||||
|
||||
from .views import InquiryCreateView, InquiryProcessView, InquiryPaymentView, OfferSelectionView, OfferSelectionTableView, BiddingListView, OfferCreationView
|
||||
from auction.views import InquiryView, PaymentView, PostPaymentView
|
||||
|
||||
app_name = "auction"
|
||||
|
||||
urlpatterns = [
|
||||
path('create_inquiry/', InquiryCreateView.as_view(), name="create_inquiry"),
|
||||
path('process_inquiry/<slug:uuid>/', InquiryProcessView.as_view(), name="process_inquiry"),
|
||||
path('create_payment/<slug:uuid>/', InquiryPaymentView.as_view(), name="inquiry_payment"),
|
||||
path('offers/<slug:uuid>/', OfferSelectionView.as_view(), name="offer_selection"),
|
||||
path('offers/<slug:uuid>/table.js', OfferSelectionTableView.as_view(), name="offer_table"),
|
||||
path('bidding/<int:id>/', BiddingListView.as_view(), name="bidding"),
|
||||
path('bidding/<int:establishment>/<slug:inquiry>/', OfferCreationView.as_view(), name="offer_create"),
|
||||
path('bidding/', BiddingListView.as_view(), name="bidding"),
|
||||
path('create_inquiry/', InquiryView.as_view(), name="create_inquiry"),
|
||||
path('<uuid:pk>/payment/', PaymentView.as_view(), name="payment"),
|
||||
path('<uuid:pk>/post_payment/', PostPaymentView.as_view(), name="post_payment")
|
||||
]
|
296
auction/views.py
296
auction/views.py
|
@ -1,222 +1,118 @@
|
|||
from django.views.generic import CreateView, UpdateView, View, ListView, DetailView, FormView
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.shortcuts import render, redirect
|
||||
from django.views.generic import CreateView, DetailView, FormView
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.gis.db.models.functions import Distance
|
||||
from django.contrib.messages import error
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from frontend.mixins import InConstructionMixin
|
||||
from partners.mixins import PartnerProfileRequiredMixin
|
||||
from clients.mixins import ClientBaseMixin
|
||||
from localauth.helpers import name_to_coords
|
||||
from partners.models import Establishment
|
||||
from payment.models import BillingAddress, Invoice, InvoiceItem, InvoicePayment
|
||||
from clients.models import ClientProfile
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
from payment.demo.models import DemoInvoicePayment # TODO: Remove when no longer needed
|
||||
from auction.models import Inquiry
|
||||
from profiles.models import ClientProfile, ContactProfile
|
||||
from auction.forms import PostPaymentForm, InquiryForm
|
||||
from payment.models import KlarnaPayment, PaypalPayment, StripePayment, DummyPayment
|
||||
|
||||
from .models import Inquiry, Offer
|
||||
from .forms import InquiryProcessForm, OfferSelectionForm
|
||||
# Create your views here.
|
||||
|
||||
class InquiryCreateView(ClientBaseMixin, CreateView):
|
||||
model = Inquiry
|
||||
fields = ["destination_name", "budget", "arrival", "min_nights", "adults", "children"]
|
||||
class InquiryView(FormView):
|
||||
form_class = InquiryForm
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect("/")
|
||||
return redirect(reverse_lazy("frontend:index"))
|
||||
|
||||
def form_invalid(self, form):
|
||||
print(repr(form.errors))
|
||||
return redirect(reverse_lazy("frontend:index") + "?invalid=true")
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.destination_coords = self.clean_destination_coords()
|
||||
form.instance.destination_radius = 5000
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form, *args, **kwargs):
|
||||
for field in form:
|
||||
for error in field.errors:
|
||||
messages.error(self.request, f"{field.name}: {error}")
|
||||
|
||||
return redirect("/")
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("auction:process_inquiry", args=(self.object.uuid,))
|
||||
|
||||
def clean_destination_coords(self):
|
||||
lat, lon = name_to_coords(self.request.POST.get("destination_name"))
|
||||
return Point(lon, lat)
|
||||
|
||||
class InquiryProcessView(ClientBaseMixin, UpdateView):
|
||||
form_class = InquiryProcessForm
|
||||
model = Inquiry
|
||||
template_name = "auction/process.html"
|
||||
|
||||
def get_object(self):
|
||||
return Inquiry.objects.get(uuid=self.kwargs["uuid"])
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
|
||||
try:
|
||||
initial["country"] = self.request.user.clientprofile.country.code
|
||||
user = ClientProfile.objects.get(user=self.request.user)
|
||||
except:
|
||||
pass
|
||||
user = None
|
||||
|
||||
return initial
|
||||
lat, lon = self.request.POST.get("destination_lat", None), self.request.POST.get("destination_lon", None)
|
||||
|
||||
if (not lat) or (not lon):
|
||||
location = Nominatim(user_agent="UrlaubsAuktion 1.0").geocode(form.instance.destination_name, country_codes="at")
|
||||
lat, lon = location.latitude, location.longitude
|
||||
|
||||
inquiry = Inquiry.objects.create(
|
||||
user = user,
|
||||
amount = form.cleaned_data["amount"],
|
||||
first_date = form.cleaned_data["first_date"],
|
||||
last_date = form.cleaned_data["last_date"],
|
||||
destination_name = form.cleaned_data["destination_name"],
|
||||
adults = form.cleaned_data["adults"],
|
||||
children = form.cleaned_data["children"],
|
||||
currency = "eur",
|
||||
destination_geo = Point(lon, lat)
|
||||
)
|
||||
return redirect(inquiry.get_absolute_url())
|
||||
|
||||
class PaymentView(DetailView):
|
||||
model = Inquiry
|
||||
template_name = "auction/payment.html"
|
||||
|
||||
class PostPaymentView(FormView):
|
||||
form_class = PostPaymentForm
|
||||
|
||||
def form_invalid(self, form):
|
||||
#super().form_invalid(form)
|
||||
for _dumbo, errormsg in form.errors:
|
||||
error(self.request, errormsg)
|
||||
return redirect(reverse_lazy("auction:payment", kwargs={'pk': self.kwargs["pk"]}))
|
||||
|
||||
def form_valid(self, form):
|
||||
profile, _ = ClientProfile.objects.get_or_create(user=self.request.user)
|
||||
profile.first_name = form.cleaned_data["first_name"]
|
||||
profile.last_name = form.cleaned_data["last_name"]
|
||||
profile.street = form.cleaned_data["street"]
|
||||
profile.city = form.cleaned_data["city"]
|
||||
profile.zip = form.cleaned_data["zip"]
|
||||
profile.state = form.cleaned_data["state"]
|
||||
profile.country = form.cleaned_data["country"]
|
||||
profile.save()
|
||||
#super().form_valid(form)
|
||||
|
||||
form.instance.client = profile
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form, *args, **kwargs):
|
||||
for field in form:
|
||||
for error in field.errors:
|
||||
messages.error(self.request, f"{field.name}: {error}")
|
||||
|
||||
return redirect("/")
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("auction:inquiry_payment", args=(self.object.uuid,))
|
||||
|
||||
class InquiryPaymentView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
inquiry = Inquiry.objects.get(uuid=kwargs["uuid"])
|
||||
# ClientProfile
|
||||
try:
|
||||
invoice = inquiry.invoice
|
||||
except Invoice.DoesNotExist:
|
||||
invoice = Invoice.from_inquiry(inquiry)
|
||||
# payment_url = InvoicePayment.from_invoice(invoice, inquiry.gateway) # TODO: Make this work again
|
||||
payment_url = DemoInvoicePayment.initiate(invoice)
|
||||
client = ClientProfile.objects.get(user=self.request.user)
|
||||
except ClientProfile.DoesNotExist: # pylint: disable=no-member
|
||||
client = ClientProfile.objects.create(
|
||||
user = self.request.user,
|
||||
first_name = form.cleaned_data["first_name"],
|
||||
last_name = form.cleaned_data["last_name"],
|
||||
address = form.cleaned_data["address"],
|
||||
address2 = form.cleaned_data["address2"],
|
||||
zipcode = form.cleaned_data["zipcode"],
|
||||
city = form.cleaned_data["city"],
|
||||
country = form.cleaned_data["country"],
|
||||
phone = form.cleaned_data["phone"]
|
||||
)
|
||||
self.request.user.email = form.cleaned_data["email"]
|
||||
self.request.user.save()
|
||||
|
||||
if not payment_url:
|
||||
messages.error(request, "Die Zahlung ist leider fehlgeschlagen. Versuche es bitte nochmals!")
|
||||
return redirect(reverse("auction:process_inquiry", args=(kwargs["uuid"],)))
|
||||
# ContactProfile
|
||||
contact = ContactProfile.objects.create(
|
||||
user = self.request.user,
|
||||
first_name = form.cleaned_data["first_name"],
|
||||
last_name = form.cleaned_data["last_name"],
|
||||
address = form.cleaned_data["address"],
|
||||
address2 = form.cleaned_data["address2"],
|
||||
zipcode = form.cleaned_data["zipcode"],
|
||||
city = form.cleaned_data["city"],
|
||||
country = form.cleaned_data["country"],
|
||||
phone = form.cleaned_data["phone"],
|
||||
email = form.cleaned_data["email"]
|
||||
)
|
||||
|
||||
return redirect(payment_url)
|
||||
# Inquiry
|
||||
inquiry = Inquiry.objects.get(uuid=self.kwargs["pk"]) # pylint: disable=no-member
|
||||
inquiry.user = client
|
||||
inquiry.contact = contact
|
||||
inquiry.save()
|
||||
|
||||
class OfferSelectionView(ClientBaseMixin, FormView, DetailView):
|
||||
model = Inquiry
|
||||
form_class = OfferSelectionForm
|
||||
|
||||
def get_template_names(self):
|
||||
inquiry = self.get_object()
|
||||
|
||||
if inquiry.in_bidding:
|
||||
return ["auction/offer_noselect.html"]
|
||||
|
||||
return ["auction/offer_select.html"]
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(Inquiry, uuid=self.kwargs["uuid"], client=self.request.user.clientprofile)
|
||||
|
||||
def form_valid(self, form):
|
||||
inquiry = self.get_object()
|
||||
offer = get_object_or_404(Offer, inquiry=inquiry, uuid=form.cleaned_data["offer"])
|
||||
offer.accepted = timezone.now()
|
||||
offer.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("clients:booking_view", args=[self.kwargs["uuid"]])
|
||||
|
||||
class OfferSelectionTableView(ClientBaseMixin, ListView):
|
||||
model = Offer
|
||||
content_type = "text/javascript"
|
||||
template_name = "auction/offer_table.js"
|
||||
|
||||
def get_queryset(self):
|
||||
inquiry = get_object_or_404(Inquiry, uuid=self.kwargs["uuid"], client=self.request.user.clientprofile)
|
||||
return inquiry.offer_set.all()
|
||||
|
||||
class OfferCreationView(InConstructionMixin, PartnerProfileRequiredMixin, CreateView):
|
||||
model = Offer
|
||||
template_name = "auction/offer_create.html"
|
||||
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!")
|
||||
return redirect("partners:establishment_register")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_establishment(self):
|
||||
establishment = self.kwargs.get("establishment", None)
|
||||
kwargs = {"owner": self.request.user.partnerprofile}
|
||||
|
||||
if establishment:
|
||||
kwargs["id"] = establishment
|
||||
return get_object_or_404(Establishment, **kwargs)
|
||||
# Payment
|
||||
gateway = self.request.POST.get("gateway").lower()
|
||||
if gateway == "paypal":
|
||||
handler = PaypalPayment
|
||||
elif gateway == "dummy" and settings.DEBUG:
|
||||
handler = DummyPayment
|
||||
elif gateway == "klarna":
|
||||
handler = KlarnaPayment
|
||||
else:
|
||||
return Establishment.objects.filter(**kwargs).first()
|
||||
handler = StripePayment
|
||||
|
||||
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"] = 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
|
||||
template_name = "auction/bidding_list.html"
|
||||
|
||||
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.get_establishment()
|
||||
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)
|
||||
context["establishment"] = self.establishment
|
||||
return context
|
||||
|
||||
class BookingView(ClientBaseMixin, DetailView):
|
||||
model = Inquiry
|
||||
template_name = "auction/booking.html"
|
||||
|
||||
def get_object(self):
|
||||
return Inquiry.objects.get(uuid=self.kwargs["uuid"], client=self.request.user.clientprofile)
|
||||
payment = handler.objects.create(invoice=inquiry)
|
||||
return payment.start()
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ClientsConfig(AppConfig):
|
||||
name = 'clients'
|
|
@ -1,17 +0,0 @@
|
|||
from django.urls import reverse_lazy
|
||||
|
||||
from localauth.mixins import UserPassesTestMixin, MultiPermissionMixin, LoginRequiredMixin
|
||||
|
||||
class ClientProfileRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
try:
|
||||
assert self.request.user.clientprofile
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_login_url(self):
|
||||
return reverse_lazy("clients:register")
|
||||
|
||||
class ClientBaseMixin(MultiPermissionMixin):
|
||||
MIXINS = [LoginRequiredMixin, ClientProfileRequiredMixin]
|
|
@ -1,9 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from localauth.models import Profile
|
||||
|
||||
class ClientProfile(Profile):
|
||||
@property
|
||||
def balance(self):
|
||||
return 0.0
|
|
@ -1,17 +0,0 @@
|
|||
from django.urls import path, reverse_lazy
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from .views import ClientRegistrationView, ClientProfileView, ClientDashboardView, ClientBookingsView
|
||||
|
||||
from auction.views import BookingView
|
||||
|
||||
app_name = "clients"
|
||||
|
||||
urlpatterns = [
|
||||
path('dashboard/', ClientDashboardView.as_view(), name="dashboard"),
|
||||
path('register/', ClientRegistrationView.as_view(), name="register"),
|
||||
path('profile/', ClientProfileView.as_view(), name="profile"),
|
||||
path('bookings/', ClientBookingsView.as_view(), name="bookings"),
|
||||
path('bookings/<slug:uuid>/', BookingView.as_view(), name="booking_view"),
|
||||
path('', RedirectView.as_view(url=reverse_lazy("clients:dashboard"))),
|
||||
]
|
|
@ -1,86 +0,0 @@
|
|||
from django.views.generic import CreateView, UpdateView, TemplateView, ListView, DetailView
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
|
||||
from .models import ClientProfile
|
||||
from .mixins import ClientBaseMixin
|
||||
|
||||
from localauth.mixins import LoginRequiredMixin, RedirectToNextMixin
|
||||
from frontend.mixins import InConstructionMixin
|
||||
from auction.models import Inquiry
|
||||
|
||||
class ClientRegistrationView(LoginRequiredMixin, RedirectToNextMixin, CreateView):
|
||||
model = ClientProfile
|
||||
exclude = ["user"]
|
||||
template_name = "clients/signup.html"
|
||||
fields = ["company", "vat_id", "first_name", "last_name", "street", "city", "zip", "state", "country", "phone"]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
ClientProfile.objects.get(user=request.user)
|
||||
return redirect(reverse_lazy("clients:profile"))
|
||||
except (ClientProfile.DoesNotExist, TypeError):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, "Profil erfolgreich angelegt!")
|
||||
return self.get_redirect_url() if self.get_redirect_url() else reverse_lazy("clients:profile")
|
||||
|
||||
def get_initial(self):
|
||||
try:
|
||||
partner = self.request.user.partnerprofile
|
||||
return {
|
||||
"company": partner.company,
|
||||
"vat_id": partner.vat_id,
|
||||
"first_name": partner.first_name,
|
||||
"last_name": partner.last_name,
|
||||
"street": partner.street,
|
||||
"city": partner.city,
|
||||
"zip": partner.zip,
|
||||
"state": partner.state,
|
||||
"country": partner.country,
|
||||
"phone": partner.phone,
|
||||
}
|
||||
|
||||
except:
|
||||
return {
|
||||
"country": "AT",
|
||||
"phone": "+43"
|
||||
}
|
||||
|
||||
class ClientProfileView(ClientBaseMixin, UpdateView):
|
||||
model = ClientProfile
|
||||
exclude = ["user"]
|
||||
template_name = "clients/profile.html"
|
||||
fields = ["company", "vat_id", "first_name", "last_name", "street", "city", "zip", "state", "country"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("clients:profile")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user.clientprofile
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().get(request, *args, **kwargs)
|
||||
except ClientProfile.DoesNotExist:
|
||||
return redirect("clients:register")
|
||||
|
||||
class ClientDashboardView(ClientBaseMixin, DetailView):
|
||||
model = ClientProfile
|
||||
template_name = "clients/dashboard.html"
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user.clientprofile
|
||||
|
||||
class ClientBookingsView(ClientBaseMixin, ListView):
|
||||
model = Inquiry
|
||||
template_name = "clients/bookings.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return Inquiry.objects.filter(client=self.request.user.clientprofile)
|
|
@ -1,39 +0,0 @@
|
|||
[JOURNEYJOKER]
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
Debug = 0
|
||||
Host = journeyjoker.lan
|
||||
Countries = ["AT"]
|
||||
|
||||
CurrencySymbol = "€"
|
||||
CurrencyCode = "EUR"
|
||||
CurrencyName = "Euro"
|
||||
|
||||
[SMTP]
|
||||
Host = "mail.server"
|
||||
# Port = 25
|
||||
Username = "mail_username"
|
||||
Password = "mail_password"
|
||||
StartTLS = 0
|
||||
SSL = 1
|
||||
|
||||
From = "noreply@journeyjoker.lan"
|
||||
BCC = []
|
||||
|
||||
[ADMINS]
|
||||
admin@example.com = Demo Admin
|
||||
|
||||
[MANAGERS]
|
||||
manager@example.com = Demo Manager
|
||||
|
||||
# [MySQL]
|
||||
# Database = journeyjoker
|
||||
# Username = journeyjoker
|
||||
# Password = secret123!
|
||||
# Host = localhost
|
||||
# Port = 3306
|
||||
|
||||
# [S3]
|
||||
# AccessKey = journeyjoker
|
||||
# SecretKey = !!!verysecret!!!
|
||||
# Bucket = journeyjoker
|
||||
# Endpoint = https://minio.journeyjoker.lan
|
1
dbsettings
Submodule
1
dbsettings
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 47c7a84781ea1314733189e0b47adf751e889394
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/bash
|
||||
sudo apt install libpq-dev build-essential libpython3-dev libmariadb-dev python3-pip python3-venv libgdal-dev wkhtmltopdf gettext -y
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -Ur requirements.txt
|
||||
./manage.py
|
||||
./manage.py compilemessages -i venv
|
|
@ -1,8 +1,7 @@
|
|||
from urlaubsauktion.admin import joker_admin as admin
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Inspiration, InspirationRegion, InspirationSponsor, Testimonial
|
||||
from frontend.models import Testimonial
|
||||
|
||||
admin.register(Testimonial)
|
||||
admin.register(Inspiration)
|
||||
admin.register(InspirationRegion)
|
||||
admin.register(InspirationSponsor)
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(Testimonial)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
def demo(request):
|
||||
return {
|
||||
"DEBUG": settings.DEBUG,
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from .validators import LanguageValidator
|
||||
|
||||
class LanguageField(models.CharField):
|
||||
default_validators = [LanguageValidator()]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("max_length", 16)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
value = getattr(model_instance, self.attname, None)
|
||||
if value:
|
||||
value = value.lower()
|
||||
if "_" in value:
|
||||
lang, country = value.split("_")
|
||||
value = "_".join([lang, country.upper()])
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
else:
|
||||
return super().pre_save(model_instance, add)
|
|
@ -1,12 +0,0 @@
|
|||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
|
||||
class DemoMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if settings.DEBUG:
|
||||
messages.warning(request, "Sie befinden sich auf der Demo-Instanz von JourneyJoker. Diese ist nur zu Testzwecken gedacht und möglicherweise nicht stabil.")
|
||||
|
||||
return self.get_response(request)
|
|
@ -1,13 +0,0 @@
|
|||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class InConstructionMixin:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
messages.warning(request, _("Die aufgerufene Seite wird aktuell bearbeitet und funktioniert möglicherweise nicht wie erwartet. Versuchen Sie es bitte später wieder oder wenden Sie sich an den Administrator, wenn dieses Problem länger besteht."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
class TitleMixin:
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = self.title
|
||||
return context
|
|
@ -1,61 +1,16 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.db.models import Model, CharField, TextField, ImageField, BooleanField, PositiveIntegerField
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from localauth.models import ImageMixin
|
||||
# Create your models here.
|
||||
|
||||
from .fields import LanguageField
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
class ClassProperty(property):
|
||||
def __get__(self, cls, owner):
|
||||
return self.fget.__get__(None, owner)()
|
||||
|
||||
class Testimonial(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
text = models.TextField()
|
||||
stars = models.PositiveIntegerField(
|
||||
class Testimonial(Model):
|
||||
name = CharField(max_length=128)
|
||||
text = TextField()
|
||||
stars = PositiveIntegerField(
|
||||
validators=[
|
||||
MaxValueValidator(5),
|
||||
MinValueValidator(1)
|
||||
])
|
||||
language = LanguageField()
|
||||
public = models.BooleanField(default=False)
|
||||
|
||||
class InspirationRegion(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
is_state = models.BooleanField(default=False)
|
||||
country = CountryField()
|
||||
|
||||
@ClassProperty
|
||||
@classmethod
|
||||
def country_set(cls):
|
||||
return cls.objects.all().values_list("country").distinct()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class InspirationSponsor(ImageMixin):
|
||||
user = models.ForeignKey(get_user_model(), models.SET_NULL, null=True, blank=True)
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Inspiration(ImageMixin):
|
||||
title = models.CharField(max_length=256)
|
||||
subtitle = models.CharField(max_length=256, null=True, blank=True)
|
||||
region = models.ForeignKey(InspirationRegion, models.PROTECT)
|
||||
sponsor = models.ForeignKey(InspirationSponsor, models.PROTECT)
|
||||
content = models.TextField()
|
||||
destination_name = models.CharField(max_length=128)
|
||||
destination_coords = models.PointField()
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.sponsor.user
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
language = CharField(max_length=12, choices=settings.LANGUAGES)
|
||||
public = BooleanField(default=False)
|
|
@ -1,15 +0,0 @@
|
|||
from django.utils.translation import get_language
|
||||
from django.conf import settings
|
||||
|
||||
import googlemaps
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
class GoogleAPI:
|
||||
def __init__(self, api_key=None):
|
||||
api_key = api_key or getValue("google.api.key")
|
||||
self.api = googlemaps.Client(key=api_key)
|
||||
|
||||
def autocomplete(self, term, types="(cities)", language=get_language(), countries=settings.JOKER_COUNTRIES):
|
||||
response = self.api.places_autocomplete(term, types=types, language=language, components={"country": countries})
|
||||
return [result["description"] for result in response]
|
|
@ -1,9 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import PlacesAutocompleteView
|
||||
|
||||
app_name = "places"
|
||||
|
||||
urlpatterns = [
|
||||
path('autocomplete', PlacesAutocompleteView.as_view(), name="autocomplete"),
|
||||
]
|
|
@ -1,13 +0,0 @@
|
|||
from django.views.generic import View
|
||||
from django.http import JsonResponse
|
||||
|
||||
from .api import GoogleAPI
|
||||
|
||||
class PlacesAutocompleteView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if term := request.GET.get("term"):
|
||||
api = GoogleAPI()
|
||||
results = api.autocomplete(term)
|
||||
else:
|
||||
results = []
|
||||
return JsonResponse(results, safe=False)
|
|
@ -1,30 +0,0 @@
|
|||
from django import template
|
||||
from django.utils import timezone
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def seconds_from_date(date, seconds):
|
||||
return date + timedelta(seconds=seconds)
|
||||
|
||||
@register.simple_tag
|
||||
def hours_from_date(date, hours):
|
||||
return date + timedelta(hours=hours)
|
||||
|
||||
@register.simple_tag
|
||||
def days_from_date(date, days):
|
||||
return date + timedelta(days=days)
|
||||
|
||||
@register.simple_tag
|
||||
def seconds_from_now(seconds):
|
||||
return seconds_from_date(timezone.now(), seconds)
|
||||
|
||||
@register.simple_tag
|
||||
def hours_from_now(hours):
|
||||
return hours_from_date(timezone.now(), hours)
|
||||
|
||||
@register.simple_tag
|
||||
def days_from_now(days):
|
||||
return days_from_date(timezone.now(), days)
|
|
@ -1,22 +1,27 @@
|
|||
from django import template
|
||||
|
||||
import requests
|
||||
import base64
|
||||
import io
|
||||
|
||||
from staticmap import StaticMap, CircleMarker
|
||||
|
||||
from dbsettings.models import Setting
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def mapimage(location, zoom=7, width=348, height=250):
|
||||
smap = StaticMap(width, height, url_template="https://tile.openstreetmap.de/{z}/{x}/{y}.png")
|
||||
marker = CircleMarker((location.x, location.y), color="orange", width=20)
|
||||
smap.add_marker(marker)
|
||||
image = smap.render(zoom)
|
||||
bio = io.BytesIO()
|
||||
image.save(bio, format="JPEG")
|
||||
def mapimage(location, zoom=8, width=348, height=250):
|
||||
payload = {
|
||||
'center': location,
|
||||
'zoom': str(zoom),
|
||||
"size": "%ix%i" % (width, height),
|
||||
'sensor': "false",
|
||||
'key': Setting.objects.get(key="google.api.key").value # pylint: disable=no-member
|
||||
}
|
||||
r = requests.get('https://maps.googleapis.com/maps/api/staticmap', params=payload)
|
||||
image = r.content
|
||||
data_uri = 'data:image/jpg;base64,'
|
||||
data_uri += base64.b64encode(bio.getvalue()).decode().replace('\n', '')
|
||||
data_uri += base64.b64encode(image).decode().replace('\n', '')
|
||||
return data_uri
|
||||
|
||||
@register.simple_tag
|
||||
def mapimage_coords(lat, lon, zoom=8, width=348, height=250):
|
||||
return mapimage("%s,%s" % (str(lat), str(lon)), zoom, width, height)
|
17
frontend/templatetags/offeroptions.py
Normal file
17
frontend/templatetags/offeroptions.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django import template
|
||||
from frontend.models import Testimonial
|
||||
from random import SystemRandom
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def stars(number, classes=""):
|
||||
if not 1 <= number <= 5:
|
||||
raise ValueError("Number of stars must be between 1 and 5.")
|
||||
|
||||
output = ""
|
||||
|
||||
for i in range(5):
|
||||
output += '<span><i class="fa%s fa-star %s"></i></span>' % (("" if i < number else "r"), classes)
|
||||
|
||||
return output
|
|
@ -1,7 +0,0 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def splitter(l, c=3):
|
||||
return [l[i:i+c] for i in range(0, len(l), c)]
|
|
@ -1,7 +0,0 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def startswith(text, string):
|
||||
return text.startswith(string)
|
|
@ -1,8 +1,6 @@
|
|||
from django import template
|
||||
|
||||
from random import SystemRandom
|
||||
|
||||
from frontend.models import Testimonial
|
||||
from random import SystemRandom
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
from django.urls import path, include
|
||||
from django.urls import path
|
||||
|
||||
from .views import HomeView, DemoTemplateView, ImpressumView, PrivacyNoticeView, TOSView, InspirationsView, LanguageChoiceView, InspirationsCountryAPIView, InspirationsRegionAPIView, InspirationsAPIView, LocaleVariableView
|
||||
from frontend.views import IndexView, change_language
|
||||
|
||||
app_name = "frontend"
|
||||
|
||||
urlpatterns = [
|
||||
path('', HomeView.as_view(), name="home"),
|
||||
path('api/places/', include("frontend.places.urls"), name="places"),
|
||||
path('demo/template/', DemoTemplateView.as_view()),
|
||||
path('impressum/', ImpressumView.as_view(), name="impressum"),
|
||||
path('privacy/', PrivacyNoticeView.as_view(), name="privacy"),
|
||||
path('tos/', TOSView.as_view(), name="tos"),
|
||||
path('inspirations/', InspirationsView.as_view(), name="inspirations"),
|
||||
path('api/setlang/<slug:code>/', LanguageChoiceView.as_view(), name="languagechoice"),
|
||||
path('api/getvars/', LocaleVariableView.as_view(), name="getvars"),
|
||||
path('api/inspirations/country/', InspirationsCountryAPIView.as_view(), name="inspirationscountriesapi"),
|
||||
path('api/inspirations/country/<slug:country>/', InspirationsRegionAPIView.as_view(), name="inspirationsregionsapi"),
|
||||
path('api/inspirations/region/<int:region>/', InspirationsAPIView.as_view(), name="inspirationsapi"),
|
||||
path('no_js/', HomeView.as_view(), name="nojs"),
|
||||
path('', IndexView.as_view(), name="index"),
|
||||
path('change_language/', change_language, name="change_language")
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class LanguageValidator:
|
||||
def __call__(self, value):
|
||||
for language, _ in settings.LANGUAGES:
|
||||
if language.startswith(value):
|
||||
return
|
||||
raise ValidationError(_("This is not a valid language code supported by this project."), code='invalid', params={'value': value})
|
|
@ -1,83 +1,28 @@
|
|||
from django.views.generic import TemplateView, View
|
||||
from django.conf import settings
|
||||
from django.http.response import HttpResponse, JsonResponse
|
||||
from django.contrib import messages
|
||||
from django.utils.formats import get_format
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||
|
||||
from .models import InspirationRegion, Inspiration
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
import django_countries
|
||||
# Create your views here.
|
||||
|
||||
class HomeView(TemplateView):
|
||||
class IndexView(TemplateView):
|
||||
template_name = "frontend/index.html"
|
||||
|
||||
class Error404View(TemplateView):
|
||||
template_name = "frontend/404.html"
|
||||
def change_language(request):
|
||||
url = request.GET.get('url', '/')
|
||||
language = request.GET.get('language', 'de')
|
||||
if "://" in url:
|
||||
raise ValueError("This is not a de-referer.")
|
||||
request.session[LANGUAGE_SESSION_KEY] = language
|
||||
return redirect(url)
|
||||
|
||||
class DemoTemplateView(TemplateView):
|
||||
template_name = "partners/calendar.html"
|
||||
|
||||
class ImpressumView(TemplateView):
|
||||
template_name = "frontend/impressum.html"
|
||||
|
||||
class PrivacyNoticeView(TemplateView):
|
||||
template_name = "frontend/privacy.html"
|
||||
|
||||
class TOSView(TemplateView):
|
||||
template_name = "frontend/terms.html"
|
||||
|
||||
class InspirationsView(TemplateView):
|
||||
template_name = "frontend/inspirations.html"
|
||||
|
||||
class InspirationsCountryAPIView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
countries = [{"code": country[0], "name": django_countries.countries.name(country[0])} for country in InspirationRegion.country_set.all()]
|
||||
return JsonResponse(countries, safe=False)
|
||||
|
||||
class InspirationsRegionAPIView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
regions = [{"id": region.id, "name": region.name, "state": region.is_state} for region in InspirationRegion.objects.filter(country=kwargs["country"])]
|
||||
return JsonResponse(regions, safe=False)
|
||||
|
||||
class InspirationsAPIView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
inspirations = [
|
||||
{
|
||||
"id": inspiration.id,
|
||||
"title": inspiration.title,
|
||||
"subtitle": inspiration.subtitle,
|
||||
"image": inspiration.image.url,
|
||||
"sponsor": {
|
||||
"id": inspiration.sponsor.id,
|
||||
"name": inspiration.sponsor.name,
|
||||
"image": inspiration.sponsor.image.url
|
||||
},
|
||||
"content": inspiration.content,
|
||||
"destination": {
|
||||
"name": inspiration.destination_name,
|
||||
"coords": {
|
||||
"lat": inspiration.destination_coords.y,
|
||||
"lon": inspiration.destination_coords.x
|
||||
}
|
||||
}
|
||||
} for inspiration in Inspiration.objects.filter(region__id=kwargs["region"])]
|
||||
|
||||
return JsonResponse(inspirations, safe=False)
|
||||
|
||||
class LanguageChoiceView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = HttpResponse()
|
||||
for language, _ in settings.LANGUAGES:
|
||||
if language.startswith(kwargs["code"]):
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
||||
if request.user.is_authenticated:
|
||||
request.user.language = language
|
||||
request.user.save()
|
||||
def errorhandler(request, exception, status):
|
||||
response = render(request, "frontend/error.html", {"status_code": status})
|
||||
response.status_code = status
|
||||
return response
|
||||
|
||||
class LocaleVariableView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
variables = {
|
||||
"date_format": get_format('SHORT_DATE_FORMAT', use_l10n=True).replace("Y", "yyyy")
|
||||
}
|
||||
return JsonResponse(variables)
|
||||
def handler404(request, exception):
|
||||
return errorhandler(request, exception, 404)
|
||||
|
||||
def handler500(request):
|
||||
return errorhandler(request, None, 500)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
from urlaubsauktion.admin import joker_admin as admin
|
||||
|
||||
from .models import Image
|
||||
|
||||
admin.register(Image)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GalleryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'gallery'
|
|
@ -1,4 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
return instance.upload_path or settings.GALLERY_UPLOAD_PATH
|
|
@ -1,13 +0,0 @@
|
|||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
|
||||
from .models import Image
|
||||
|
||||
class GalleryMixin(models.Model):
|
||||
image_set = GenericRelation(Image)
|
||||
|
||||
def add_image(self, image, upload_path=None):
|
||||
Image.objects.create(image=image, content_object=self, upload_path=upload_path)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
|
@ -1,20 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from .helpers import get_upload_path
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to=get_upload_path)
|
||||
title = models.CharField(max_length=128, null=True, blank=True)
|
||||
comment = models.TextField(null=True, blank=True)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
upload_path = models.CharField(max_length=256, null=True, blank=True)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.image.url
|
|
@ -1,5 +0,0 @@
|
|||
from urlaubsauktion.admin import joker_admin as admin
|
||||
|
||||
from .models import User
|
||||
|
||||
admin.register(User)
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LocalauthConfig(AppConfig):
|
||||
name = 'localauth'
|
|
@ -1,25 +0,0 @@
|
|||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django import forms
|
||||
|
||||
from .models import User
|
||||
|
||||
from partners.models import PartnerProfile
|
||||
from clients.models import ClientProfile
|
||||
|
||||
class RegistrationForm(UserCreationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop("request")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["email", "password1", "password2"]
|
||||
|
||||
class VerificationForm(forms.Form):
|
||||
def get_choices():
|
||||
for client in ClientProfile.objects.filter(verified=False):
|
||||
yield ("C%i" % client.id, "C%i – %s" % (client.id, client.full_name))
|
||||
for partner in PartnerProfile.objects.filter(verified=False):
|
||||
yield ("P%i" % partner.id, "P%i – %s" % (partner.id, partner.full_name))
|
||||
|
||||
profile = forms.ChoiceField(choices=get_choices)
|
|
@ -1,30 +0,0 @@
|
|||
from django.conf import settings
|
||||
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
import uuid
|
||||
import string
|
||||
|
||||
from random import SystemRandom
|
||||
|
||||
def name_to_coords(name):
|
||||
geocoder = Nominatim(user_agent="JourneyJoker.at")
|
||||
|
||||
result = geocoder.geocode(name, exactly_one=True)
|
||||
|
||||
return result.latitude, result.longitude
|
||||
|
||||
def profile_to_coords(profile):
|
||||
return name_to_coords("%s, %s, %s, %s" % (profile.street, profile.city, profile.zip, profile.country))
|
||||
|
||||
def upload_path(instance, filename):
|
||||
try:
|
||||
user_id = instance.user.id
|
||||
except:
|
||||
user_id = "global"
|
||||
|
||||
return f'userfiles/{user_id}/{uuid.uuid4()}/{filename}'
|
||||
|
||||
def generate_token(length=6, characters=string.digits):
|
||||
return "".join([SystemRandom().choice(characters) for _ in range(length)])
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
from django.utils.decorators import method_decorator
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.views import redirect_to_login, RedirectURLMixin as SuccessURLAllowedHostsMixin
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
class SuperUserRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
try:
|
||||
return self.request.user.is_superuser
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_login_url(self):
|
||||
return reverse_lazy("localauth:login")
|
||||
|
||||
class LoginRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
try:
|
||||
return self.request.user.is_authenticated
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_login_url(self):
|
||||
return reverse_lazy("localauth:login")
|
||||
|
||||
class MultiPermissionMixin:
|
||||
MIXINS = []
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
for mixin in self.MIXINS:
|
||||
if not mixin.test_func(self):
|
||||
return redirect_to_login(request.get_full_path(), mixin.get_login_url(self), REDIRECT_FIELD_NAME)
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
class RedirectToNextMixin(SuccessURLAllowedHostsMixin):
|
||||
def get_redirect_url(self):
|
||||
"""Return the user-originating redirect URL if it's safe."""
|
||||
redirect_to = self.request.POST.get(
|
||||
REDIRECT_FIELD_NAME,
|
||||
self.request.GET.get(REDIRECT_FIELD_NAME, '')
|
||||
)
|
||||
url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=redirect_to,
|
||||
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
return redirect_to if url_is_safe else ''
|
|
@ -1,160 +0,0 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from .helpers import profile_to_coords, upload_path
|
||||
|
||||
from frontend.fields import LanguageField
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
use_in_migrations = True
|
||||
|
||||
def _create_user(self, email, password, **extra_fields):
|
||||
values = [email]
|
||||
field_value_map = dict(zip(self.model.REQUIRED_FIELDS, values))
|
||||
for field_name, value in field_value_map.items():
|
||||
if not value:
|
||||
raise ValueError('The {} value must be set'.format(field_name))
|
||||
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(
|
||||
email=email,
|
||||
**extra_fields
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
return self._create_user(email, password, **extra_fields)
|
||||
|
||||
def create_superuser(self, email, password=None, **extra_fields):
|
||||
extra_fields.setdefault('is_superuser', True)
|
||||
return self._create_user(email, password, **extra_fields)
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
email = models.EmailField(unique=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
date_joined = models.DateTimeField(default=timezone.now)
|
||||
last_login = models.DateTimeField(null=True)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
language = LanguageField(max_length=16, default="de")
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
|
||||
def get_full_name(self):
|
||||
return self.email
|
||||
|
||||
def get_short_name(self):
|
||||
return self.email
|
||||
|
||||
def has_permission(self, *args, **kwargs):
|
||||
return self.is_superuser
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
return self.is_superuser
|
||||
|
||||
has_module_perms = has_permission
|
||||
has_perm = has_permission
|
||||
|
||||
@property
|
||||
def phone(self):
|
||||
try:
|
||||
return self.clientprofile.phone
|
||||
except:
|
||||
try:
|
||||
return self.partnerprofile.phone
|
||||
except:
|
||||
return None
|
||||
|
||||
class AddressMixin(models.Model):
|
||||
street = models.CharField("Straße", max_length=64)
|
||||
city = models.CharField("Stadt", max_length=64)
|
||||
zip = models.CharField("PLZ", max_length=16)
|
||||
state = models.CharField("Bundesland", max_length=64, null=True, blank=True)
|
||||
country = CountryField("Staat")
|
||||
|
||||
@property
|
||||
def full_address(self):
|
||||
return f"{self.street}, {self.city}, {self.zip}, {self.state}, {self.country}"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class LocationMixin(AddressMixin):
|
||||
coords = models.PointField()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.coords:
|
||||
lat, lon = profile_to_coords(self)
|
||||
self.coords = Point(lon, lat)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class ImageMixin(models.Model):
|
||||
image = models.ImageField(upload_to=upload_path, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class PhoneMixin(models.Model):
|
||||
phone = PhoneNumberField("Mobiltelefon")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class PersonMixin(models.Model):
|
||||
company = models.CharField("Firma", max_length=64, null=True, blank=True)
|
||||
vat_id = models.CharField("UID-Nummer", max_length=32, null=True, blank=True)
|
||||
first_name = models.CharField("Vorname", max_length=64)
|
||||
last_name = models.CharField("Nachname", max_length=64)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
name = self.full_name_only
|
||||
|
||||
if self.company:
|
||||
name += f" ({self.company})"
|
||||
|
||||
return name
|
||||
|
||||
@property
|
||||
def full_name_only(self):
|
||||
name = " ".join([self.first_name, self.last_name])
|
||||
|
||||
return name
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class Profile(PersonMixin, AddressMixin, PhoneMixin):
|
||||
user = models.OneToOneField(get_user_model(), models.CASCADE)
|
||||
verified = models.BooleanField(default=False)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class TwoFactor(PolymorphicModel):
|
||||
user = models.ForeignKey(User, models.CASCADE)
|
||||
|
||||
@classmethod
|
||||
def initiate(cls, user):
|
||||
raise NotImplementedError("%s does not implement initiate()" % cls.__name__)
|
||||
|
||||
def send_token(self, description=""):
|
||||
return True
|
||||
|
||||
def validate_token(self, token):
|
||||
raise NotImplementedError("%s does not implement validate_token()" % cls.__name__)
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,12 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import LoginView, LogoutView, RegistrationView, VerificationView
|
||||
|
||||
app_name = "localauth"
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', LoginView.as_view(), name="login"),
|
||||
path('logout/', LogoutView.as_view(), name="logout"),
|
||||
path('register/', RegistrationView.as_view(), name="register"),
|
||||
path('verify/', VerificationView.as_view(), name="verify"),
|
||||
]
|
|
@ -1,60 +0,0 @@
|
|||
from django.contrib.auth.views import LoginView as Login, LogoutView as Logout
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.contrib.auth import login
|
||||
from django.shortcuts import resolve_url
|
||||
from django.conf import settings
|
||||
from django.views.generic import FormView
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .forms import RegistrationForm, VerificationForm
|
||||
from .models import User
|
||||
from .mixins import SuperUserRequiredMixin
|
||||
|
||||
from frontend.mixins import TitleMixin
|
||||
from clients.models import ClientProfile
|
||||
from partners.models import PartnerProfile
|
||||
from mail.views import MailView
|
||||
|
||||
class LoginView(TitleMixin, Login):
|
||||
title = _("Login")
|
||||
template_name = "localauth/login.html"
|
||||
|
||||
class LogoutView(Logout):
|
||||
next_page = "/"
|
||||
|
||||
class RegistrationView(TitleMixin, Login):
|
||||
title = _("Registrieren")
|
||||
form_class = RegistrationForm
|
||||
template_name = "localauth/register.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
user = User.objects.create_user(form.cleaned_data["email"])
|
||||
user.set_password(form.cleaned_data["password1"])
|
||||
user.save()
|
||||
|
||||
login(self.request, user)
|
||||
|
||||
messages.success(self.request, _("Erfolgreich registriert!"))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_default_redirect_url(self):
|
||||
return resolve_url(self.next_page or settings.REGISTER_REDIRECT_URL)
|
||||
|
||||
class VerificationView(SuperUserRequiredMixin, FormView):
|
||||
form_class = VerificationForm
|
||||
template_name = "localauth/verify.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
pid = form.cleaned_data["profile"]
|
||||
ptype = ClientProfile if profile.startswith("C") else PartnerProfile
|
||||
pobj = ptype.objects.get(id=profile[1:])
|
||||
pobj.update(verified=True)
|
||||
|
||||
messages.success(self.request, _("Benutzer %s bestätigt!") % pobj.full_name)
|
||||
|
||||
return HttpResponseRedirect(resolve_url("localauth:verify"))
|
||||
|
||||
class EmailVerificationMailView(MailView):
|
||||
template_name = "localauth/mail/verify.html"
|
||||
|
1
locale
1
locale
|
@ -1 +0,0 @@
|
|||
Subproject commit 5a8fa82926cc81b5f4bea697b3d568ee1c71dfe3
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MailConfig(AppConfig):
|
||||
name = 'mail'
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,29 +0,0 @@
|
|||
from django import template
|
||||
from django.templatetags.static import static
|
||||
|
||||
from urllib.request import urlopen
|
||||
from io import BytesIO
|
||||
|
||||
import base64
|
||||
|
||||
import magic
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def to_data(binary):
|
||||
with BytesIO(binary) as bio:
|
||||
mime = magic.from_buffer(bio.read(), mime=True)
|
||||
|
||||
encoded = b"".join(base64.encodestring(binary).splitlines()).decode()
|
||||
|
||||
return f"data:{ mime };base64,{ encoded }"
|
||||
|
||||
@register.simple_tag
|
||||
def url_to_data(url):
|
||||
binary = urlopen(url).read()
|
||||
return to_data(binary)
|
||||
|
||||
@register.simple_tag
|
||||
def static_to_data(path):
|
||||
url = static(path)
|
||||
return url_to_data(url)
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,70 +0,0 @@
|
|||
from django.views.generic.base import ContextMixin
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.exceptions import TemplateDoesNotExist
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.conf import settings
|
||||
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class MailView(ContextMixin):
|
||||
template_name = None
|
||||
|
||||
@property
|
||||
def html_template_name(self):
|
||||
if self.template_name:
|
||||
if self.template_name.split("/")[-1].split(".")[-1] in ("html", "txt"):
|
||||
basename = template_name.rsplit(".", 1)[0]
|
||||
else:
|
||||
basename = template_name
|
||||
|
||||
try:
|
||||
path = f"{basename}.html"
|
||||
render_to_string(path)
|
||||
return path
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def text_template_name(self):
|
||||
if self.template_name:
|
||||
if self.template_name.split("/")[-1].split(".")[-1] in ("html", "txt"):
|
||||
basename = template_name.rsplit(".", 1)[0]
|
||||
else:
|
||||
basename = template_name
|
||||
|
||||
try:
|
||||
path = f"{basename}.txt"
|
||||
render_to_string(path)
|
||||
return path
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def render_to_html(self, **kwargs):
|
||||
if self.html_template_name:
|
||||
context = self.get_context_data(**kwargs)
|
||||
return render_to_string(self.html_template_name, context)
|
||||
else:
|
||||
return None
|
||||
|
||||
def render_to_text(self, from_html=False, **kwargs):
|
||||
if self.text_template_name:
|
||||
context = self.get_context_data(**kwargs)
|
||||
return render_to_string(self.text_template_name, context)
|
||||
else:
|
||||
if from_html and (html := self.render_to_html(**kwargs)):
|
||||
return BeautifulSoup(html).get_text()
|
||||
|
||||
return None
|
||||
|
||||
def send(self, subject, recipient, context={}, attachments=[], sender=None, cc=[], bcc=[], text_from_html=False):
|
||||
text = self.render_to_text(text_from_html, **context)
|
||||
email = EmailMultiAlternatives(subject, text, sender, [recipient], cc=cc, bcc=bcc + DEFAULT_BCC_EMAILS, attachments=attachments)
|
||||
if html := self.render_to_html(**context):
|
||||
email.attach_alternative(html, "text/html")
|
||||
email.send()
|
|
@ -1,11 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'urlaubsauktion.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
|
|
5
offers/apps.py
Normal file
5
offers/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OffersConfig(AppConfig):
|
||||
name = 'offers'
|
67
offers/models.py
Normal file
67
offers/models.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
from django.db.models import Model, CharField, ImageField, ForeignKey, ManyToManyField, TimeField, OneToOneField, CASCADE, IntegerField, BooleanField, TextField
|
||||
from django.contrib.gis.db.models import PointField
|
||||
from django.conf import settings
|
||||
|
||||
from vies.models import VATINField
|
||||
from django_countries.fields import CountryField
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from multiselectfield import MultiSelectField
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from profiles.models import PartnerProfile
|
||||
from offers.utils import WEEKDAY_CHOICES, WIFI_AVAILABILITY_CHOICES, PETS_CHOICES, ACTIVITIES_CHOICES, DESCRIPTION_LANGUAGE_CHOICES
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Hours(Model):
|
||||
day = MultiSelectField(choices=WEEKDAY_CHOICES)
|
||||
start = TimeField()
|
||||
end = TimeField()
|
||||
|
||||
class Offer(PolymorphicModel):
|
||||
name = CharField(max_length=128)
|
||||
address = CharField(max_length=128)
|
||||
address2 = CharField(max_length=128, blank=True, null=True)
|
||||
zipcode = CharField(max_length=15)
|
||||
city = CharField(max_length=128)
|
||||
country = CountryField()
|
||||
phone = PhoneNumberField()
|
||||
logo = ImageField(null=True)
|
||||
location = PointField()
|
||||
partners = ManyToManyField(PartnerProfile)
|
||||
|
||||
class Hotel(Offer):
|
||||
pass
|
||||
|
||||
class Descriptions(Model):
|
||||
offer = OneToOneField(Offer, on_delete=CASCADE)
|
||||
default = CharField(max_length=12, choices=DESCRIPTION_LANGUAGE_CHOICES)
|
||||
de = TextField(max_length=2048, null=True, blank=True)
|
||||
en = TextField(max_length=2048, null=True, blank=True)
|
||||
|
||||
class HotelOptions(Model):
|
||||
hotel = OneToOneField(Hotel, on_delete=CASCADE)
|
||||
|
||||
# Reception
|
||||
|
||||
reception = ManyToManyField(Hours)
|
||||
checkout = TimeField(blank=True, null=True)
|
||||
checkin = TimeField(null=True, blank=True)
|
||||
|
||||
# Furry guests
|
||||
|
||||
pets = IntegerField(choices=PETS_CHOICES, blank=True, null=True)
|
||||
|
||||
# WiFi
|
||||
wifi = IntegerField(choices=WIFI_AVAILABILITY_CHOICES, blank=True, null=True)
|
||||
wifi_cost = BooleanField(null=True)
|
||||
wifi_notes = TextField()
|
||||
|
||||
# Activities
|
||||
|
||||
activities = MultiSelectField(choices=ACTIVITIES_CHOICES)
|
||||
|
||||
class OfferImage(Model):
|
||||
offer = ForeignKey(Offer, on_delete=CASCADE)
|
||||
image = ImageField()
|
||||
is_primary = BooleanField(default=False)
|
29
offers/utils.py
Normal file
29
offers/utils.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from django.utils.translation import gettext as _
|
||||
|
||||
WEEKDAY_CHOICES = [
|
||||
(0, _("Montag")),
|
||||
(1, _("Dienstag")),
|
||||
(2, _("Mittwoch")),
|
||||
(3, _("Donnerstag")),
|
||||
(4, _("Freitag")),
|
||||
(5, _("Samstag")),
|
||||
(6, _("Sonntag"))
|
||||
]
|
||||
|
||||
PETS_CHOICES = [
|
||||
(0, _("Keine Haustiere erlaubt")),
|
||||
(1, _("Haustiere erlaubt"))
|
||||
]
|
||||
|
||||
WIFI_AVAILABILITY_CHOICES = [
|
||||
(0, _("Kein WLAN verfügbar")),
|
||||
(1, _("WLAN in öffentlichen Bereichen")),
|
||||
(2, _("WLAN im Zimmer")),
|
||||
]
|
||||
|
||||
ACTIVITIES_CHOICES = []
|
||||
|
||||
DESCRIPTION_LANGUAGE_CHOICES = [
|
||||
("de", _("Deutsch")),
|
||||
("en", _("Englisch"))
|
||||
]
|
2
packages.txt
Normal file
2
packages.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
python3-pip
|
||||
gdal-bin
|
|
@ -1,7 +0,0 @@
|
|||
from urlaubsauktion.admin import joker_admin as admin
|
||||
|
||||
from .models import PartnerProfile, Establishment, RoomCategory
|
||||
|
||||
admin.register(PartnerProfile)
|
||||
admin.register(Establishment)
|
||||
admin.register(RoomCategory)
|
|
@ -1,5 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PartnersConfig(AppConfig):
|
||||
name = 'partners'
|
|
@ -1,54 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
class FeatureSet(models.Model):
|
||||
pass
|
||||
|
||||
class Feature(PolymorphicModel):
|
||||
featureset = models.OneToOneField(FeatureSet, models.CASCADE)
|
||||
comment = models.CharField(max_length=128)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
raise NotImplementedError("%s does not implement name" % self.__class__)
|
||||
|
||||
def icon(self):
|
||||
return ""
|
||||
|
||||
class TimeFeature(Feature):
|
||||
time = models.TimeField()
|
||||
|
||||
class TimeRangeFeature(Feature):
|
||||
time_from = models.TimeField()
|
||||
time_to = models.TimeField()
|
||||
|
||||
class IncludedStatus(models.IntegerChoices):
|
||||
UNAVAILABLE = 0
|
||||
AVAILABLE = 1
|
||||
INCLUDED = 2
|
||||
|
||||
class IncludedFeature(Feature):
|
||||
status = models.IntegerField(choices=IncludedStatus.choices)
|
||||
|
||||
class BedsFeature(Feature):
|
||||
single = models.IntegerField()
|
||||
double = models.IntegerField()
|
||||
queen = models.IntegerField()
|
||||
king = models.IntegerField()
|
||||
couch = models.IntegerField()
|
||||
|
||||
class AvailableFeature(Feature):
|
||||
status = models.BooleanField()
|
||||
|
||||
class CountFeature(Feature):
|
||||
count = models.IntegerField()
|
||||
|
||||
class InRoomStatus(models.IntegerChoices):
|
||||
UNAVAILABLE = 0
|
||||
COMMON = 1
|
||||
ROOM = 2
|
||||
|
||||
class InRoomFeature(Feature):
|
||||
status = models.IntegerField(choices=InRoomStatus.choices)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django import forms
|
||||
|
||||
from .models import Establishment
|
||||
|
||||
class VerificationForm(forms.Form):
|
||||
def get_choices():
|
||||
for establishment in Establishment.objects.filter(verified=False):
|
||||
yield ("%i" % establishment.id, "%i – %s" % (establishment.id, establishment.name))
|
||||
|
||||
establishment = forms.ChoiceField(choices=get_choices)
|
|
@ -1,12 +0,0 @@
|
|||
from localauth.mixins import UserPassesTestMixin
|
||||
|
||||
class PartnerProfileRequiredMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
try:
|
||||
assert self.request.user.partnerprofile
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_login_url(self):
|
||||
return reverse_lazy("partners:register")
|
|
@ -1,107 +0,0 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .features import *
|
||||
|
||||
from localauth.models import User, Profile, LocationMixin, ImageMixin, PhoneMixin
|
||||
from gallery.mixins import GalleryMixin
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
class PartnerProfile(Profile):
|
||||
@property
|
||||
def roomcategory_set(self):
|
||||
return RoomCategory.objects.filter(establishment__in=self.establishment_set.all())
|
||||
|
||||
class Establishment(LocationMixin, ImageMixin, PhoneMixin, GalleryMixin):
|
||||
owner = models.ForeignKey(PartnerProfile, models.CASCADE)
|
||||
name = models.CharField(max_length=64)
|
||||
stars = models.IntegerField("Sterne", null=True, blank=True)
|
||||
superior = models.BooleanField(default=False)
|
||||
verified = models.BooleanField(default=False)
|
||||
active = models.BooleanField(default=True)
|
||||
featureset = models.OneToOneField(FeatureSet, models.PROTECT, null=True, blank=True)
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.owner.user
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.verified and self.active
|
||||
|
||||
@property
|
||||
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(GalleryMixin):
|
||||
establishment = models.ForeignKey(Establishment, models.CASCADE)
|
||||
name = models.CharField(max_length=64)
|
||||
rooms = models.IntegerField(default=0)
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
# TODO: Pricing via Pricing objects
|
||||
|
||||
average_price = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
minimum_price = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
|
||||
# def average_price(self, date=None):
|
||||
# dobj = timezone.datetime.strptime(date, "%Y-%m-%d") if date else timezone.now()
|
||||
#
|
||||
# return RoomCategoryPricing.for_date(self, dobj).average_price
|
||||
#
|
||||
# def minimum_price(self, date=None):
|
||||
# dobj = timezone.datetime.strptime(date, "%Y-%m-%d") if date else timezone.now()
|
||||
#
|
||||
# return RoomCategoryPricing.for_date(self, dobj).minimum_price
|
||||
|
||||
class RoomCategoryPricing(models.Model):
|
||||
roomcategory = models.ForeignKey(RoomCategory, models.CASCADE)
|
||||
date = models.DateField()
|
||||
average_price = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
minimum_price = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
|
||||
class Meta:
|
||||
unique_together = [("roomcategory", "date")]
|
||||
|
||||
@classmethod
|
||||
def for_date(cls, category: RoomCategory, date: timezone.datetime):
|
||||
try:
|
||||
pricing = cls.objects.get(roomcategory=category, date=date)
|
||||
return (pricing.average_price, pricing.minimum_price)
|
||||
|
||||
except cls.DoesNotExist:
|
||||
return RoomCategoryDefaultPricing.for_date(category, date)
|
||||
|
||||
|
||||
class RoomCategoryDefaultPricing(models.Model):
|
||||
class WeekdayChoices(models.IntegerChoices):
|
||||
MONDAY = (0, _("Montag"))
|
||||
TUESDAY = (1, _("Dienstag"))
|
||||
WEDNESDAY = (2, _("Mittwoch"))
|
||||
THURSDAY = (3, _("Donnerstag"))
|
||||
FRIDAY = (4, _("Freitag"))
|
||||
SATURDAY = (5, _("Samstag"))
|
||||
SUNDAY = (6, _("Sonntag"))
|
||||
|
||||
roomcategory = models.OneToOneField(RoomCategory, models.CASCADE)
|
||||
start_date = models.DateField()
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
weekday = models.IntegerField(choices=WeekdayChoices.choices)
|
||||
|
||||
average_price = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
minimum_price = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
|
||||
@classmethod
|
||||
def for_date(cls, category: RoomCategory, date: timezone.datetime):
|
||||
pricing = cls.objects.get(roomcategory=category)
|
||||
return (pricing[date.weekday()]["average"], pricing[date.weekday()]["minimum"])
|
|
@ -1,32 +0,0 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def stars(number, superior=False, color=""):
|
||||
number = int(number)
|
||||
if not 0 <= number <= 5:
|
||||
raise ValueError("Number of stars must be between 0 and 5.")
|
||||
|
||||
output = ""
|
||||
|
||||
for i in range(5):
|
||||
output += '<span><i class="fa%s fa-star" style="color:%s;"></i></span>' % (("" if i < number else "r"), color)
|
||||
|
||||
if superior:
|
||||
output += '<span style="color:%s;">S</span>' % color
|
||||
|
||||
return output
|
||||
|
||||
@register.simple_tag
|
||||
def hearts(number, color=""):
|
||||
number = int(number)
|
||||
if not 1 <= number <= 5:
|
||||
raise ValueError("Number of hearts must be between 0 and 5.")
|
||||
|
||||
output = ""
|
||||
|
||||
for i in range(5):
|
||||
output += '<span><i class="fa%s fa-heart" style="color:%s;"></i></span>' % (("" if i < number else "r"), color)
|
||||
|
||||
return output
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,22 +0,0 @@
|
|||
from django.urls import path, reverse_lazy
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from .views import PartnerRegistrationView, PartnerProfileView, OffersListView, EstablishmentsListView, EstablishmentRequestView, PartnerDashboardView, EstablishmentVerificationView, RoomCategoryListView, EstablishmentGalleryManagementView, RoomCategoryGalleryManagementView, RoomCategoryDefaultPricingView
|
||||
|
||||
app_name = "partners"
|
||||
|
||||
urlpatterns = [
|
||||
path('register/', PartnerRegistrationView.as_view(), name="register"),
|
||||
path('profile/', PartnerProfileView.as_view(), name="profile"),
|
||||
path('establishments/', EstablishmentsListView.as_view(), name="establishments"),
|
||||
path('establishments/<int:id>/', RoomCategoryListView.as_view(), name="roomcategories"),
|
||||
path('establishments/<int:id>/gallery/', EstablishmentGalleryManagementView.as_view(), name="establishment_gallery"),
|
||||
path('establishments/<int:eid>/<int:cid>/gallery/', RoomCategoryGalleryManagementView.as_view(), name="roomcategory_gallery"),
|
||||
path('establishments/<int:eid>/<int:cid>/prices/', RoomCategoryDefaultPricingView.as_view(), name="roomcategory_prices"),
|
||||
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"),
|
||||
path('dashboard/', PartnerDashboardView.as_view(), name="dashboard"),
|
||||
path('bookings/', PartnerDashboardView.as_view(), name="bookings"),
|
||||
path('', RedirectView.as_view(url=reverse_lazy("partners:dashboard"))),
|
||||
]
|
|
@ -1,279 +0,0 @@
|
|||
from django.views.generic import CreateView, UpdateView, ListView, DetailView, FormView, View
|
||||
from django.urls import reverse_lazy
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_list_or_404, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.http.response import JsonResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import PartnerProfile, Establishment, RoomCategory
|
||||
from .mixins import PartnerProfileRequiredMixin
|
||||
from .forms import VerificationForm
|
||||
|
||||
from auction.models import Inquiry, Offer
|
||||
from frontend.mixins import InConstructionMixin
|
||||
from localauth.mixins import LoginRequiredMixin, SuperUserRequiredMixin
|
||||
from gallery.models import Image
|
||||
|
||||
from django_starfield import Stars
|
||||
|
||||
import uuid
|
||||
|
||||
class PartnerRegistrationView(InConstructionMixin, LoginRequiredMixin, CreateView):
|
||||
model = PartnerProfile
|
||||
exclude = ["user"]
|
||||
template_name = "partners/signup.html"
|
||||
fields = ["company", "vat_id", "first_name", "last_name", "street", "city", "zip", "state", "country", "phone"]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
PartnerProfile.objects.get(user=request.user)
|
||||
return HttpResponseRedirect(reverse_lazy("partners:profile"))
|
||||
except (PartnerProfile.DoesNotExist, TypeError):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, _("Profil erfolgreich angelegt!"))
|
||||
return reverse_lazy("partners:profile")
|
||||
|
||||
def get_initial(self):
|
||||
try:
|
||||
client = self.request.user.clientprofile
|
||||
|
||||
return {
|
||||
"company": client.company,
|
||||
"vat_id": client.vat_id,
|
||||
"first_name": client.first_name,
|
||||
"last_name": client.last_name,
|
||||
"street": client.street,
|
||||
"city": client.city,
|
||||
"zip": client.zip,
|
||||
"state": client.state,
|
||||
"country": client.country,
|
||||
"phone": client.phone
|
||||
}
|
||||
|
||||
except:
|
||||
return {
|
||||
"country": "AT",
|
||||
"phone": "+43"
|
||||
}
|
||||
|
||||
class PartnerProfileView(InConstructionMixin, PartnerProfileRequiredMixin, UpdateView):
|
||||
model = PartnerProfile
|
||||
exclude = ["user"]
|
||||
template_name = "partners/profile.html"
|
||||
fields = ["company", "vat_id", "first_name", "last_name", "street", "city", "zip", "state", "country"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("partners:profile")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user.partnerprofile
|
||||
|
||||
class OffersListView(InConstructionMixin, PartnerProfileRequiredMixin, ListView):
|
||||
model = Offer
|
||||
template_name = "partners/offer_list.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return Offer.objects.filter(roomcategory__in=self.request.user.partnerprofile.roomcategory_set.all())
|
||||
|
||||
class EstablishmentsListView(InConstructionMixin, PartnerProfileRequiredMixin, ListView):
|
||||
model = Establishment
|
||||
template_name = "partners/establishment_list.html"
|
||||
|
||||
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"
|
||||
fields = ["name", "stars", "superior", "street", "city", "zip", "state", "country", "image"]
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.owner = self.request.user.partnerprofile
|
||||
retval = super().form_valid(form)
|
||||
|
||||
RoomCategory.objects.create(name=_("Einzelzimmer"), average_price=100, active=False, establishment=form.instance) # TODO: Move somewhere better
|
||||
RoomCategory.objects.create(name=_("Doppelzimmer"), average_price=100, active=False, establishment=form.instance)
|
||||
|
||||
return retval
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("partners:establishments")
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.fields['stars'].widget = Stars()
|
||||
return form
|
||||
|
||||
class PartnerDashboardView(InConstructionMixin, PartnerProfileRequiredMixin, DetailView):
|
||||
model = PartnerProfile
|
||||
template_name = "partners/dashboard.html"
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user.partnerprofile
|
||||
|
||||
class EstablishmentVerificationView(SuperUserRequiredMixin, FormView):
|
||||
form_class = VerificationForm
|
||||
template_name = "partners/establishment_verify.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
eid = form.cleaned_data["establishment"]
|
||||
eobj = Establishment.objects.filter(id=eid)
|
||||
eobj.update(verified=True)
|
||||
|
||||
messages.success(self.request, _("Unterkunft %s bestätigt!") % eobj[0].name)
|
||||
|
||||
return HttpResponseRedirect(reverse_lazy("partners:establishment_verify"))
|
||||
|
||||
class EstablishmentGalleryManagementView(PartnerProfileRequiredMixin, CreateView, ListView):
|
||||
model = Image
|
||||
template_name = "partners/establishment_gallery_manage.html"
|
||||
fields = ["image", "title", "comment"]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.establishment = self.get_establishment()
|
||||
self.object_list = self.get_queryset()
|
||||
|
||||
if not self.establishment:
|
||||
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 establishment.image_set.all()
|
||||
|
||||
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.content_object = self.establishment
|
||||
|
||||
for filename, file in self.request.FILES.items():
|
||||
name = self.request.FILES[filename].name
|
||||
|
||||
form.instance.upload_path = f"userfiles/{self.request.user.id}/{uuid.uuid4()}/{name}"
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("partners:establishment_gallery", args=[self.establishment.id])
|
||||
|
||||
class RoomCategoryGalleryManagementView(PartnerProfileRequiredMixin, CreateView, ListView):
|
||||
model = Image
|
||||
template_name = "partners/roomcategory_gallery_manage.html"
|
||||
fields = ["image", "title", "comment"]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.roomcategory = self.get_roomcategory()
|
||||
self.object_list = self.get_queryset()
|
||||
|
||||
if not self.roomcategory:
|
||||
return redirect("partners:establishment_register")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_roomcategory(self):
|
||||
roomcategory = self.kwargs.get("cid", None)
|
||||
kwargs = {"establishment_owner": self.request.user}
|
||||
|
||||
if roomcategory:
|
||||
kwargs["id"] = roomcategory
|
||||
return get_object_or_404(RoomCategory, **kwargs)
|
||||
else:
|
||||
return RoomCategory.objects.filter(**kwargs).first()
|
||||
|
||||
def get_queryset(self):
|
||||
roomcategory = self.roomcategory
|
||||
return roomcategory.image_set.all()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["roomcategory"] = self.roomcategory
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.content_object = self.roomcategory
|
||||
|
||||
for filename, file in self.request.FILES.items():
|
||||
name = self.request.FILES[filename].name
|
||||
|
||||
form.instance.upload_path = f"userfiles/{self.request.user.id}/{uuid.uuid4()}/{name}"
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("partners:roomcategory_gallery", args=[self.roomcategory.establishment.id, self.roomcategory.id])
|
||||
|
||||
class RoomCategoryCalendarAPIView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
rc = get_object_or_404(RoomCategory, id=kwargs["room"], establishment__id=kwargs["establishment"], establishment__owner=request.user)
|
||||
|
||||
events = []
|
||||
|
||||
|
||||
|
||||
return JsonResponse(events, safe=False)
|
||||
|
||||
class RoomCategoryDefaultPricingView(FormView):
|
||||
form_class = None
|
||||
template_name = "partners/default_pricing.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
pass
|
|
@ -1,16 +1,10 @@
|
|||
from urlaubsauktion.admin import joker_admin as admin
|
||||
|
||||
from .models import InvoicePayment, Invoice, InvoiceItem
|
||||
from .paypal.models import PaypalInvoicePayment
|
||||
from .sepa.models import SepaInvoicePayment
|
||||
from .demo.models import DemoInvoicePayment
|
||||
from django.contrib import admin
|
||||
from payment.models import Payment, KlarnaPayment, PaypalPayment, StripePayment, DummyPayment
|
||||
|
||||
# Register your models here.
|
||||
|
||||
admin.register(InvoicePayment)
|
||||
admin.register(Invoice)
|
||||
admin.register(InvoiceItem)
|
||||
|
||||
admin.register(PaypalInvoicePayment)
|
||||
admin.register(SepaInvoicePayment)
|
||||
admin.register(DemoInvoicePayment)
|
||||
admin.site.register(Payment)
|
||||
admin.site.register(KlarnaPayment)
|
||||
admin.site.register(PaypalPayment)
|
||||
admin.site.register(StripePayment)
|
||||
admin.site.register(DummyPayment)
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
# Payment Status Constants
|
||||
|
||||
PAYMENT_STATUS_INITIATED = -100
|
||||
|
||||
PAYMENT_STATUS_AUTHORIZED = -1
|
||||
PAYMENT_STATUS_SUCCESS = 0
|
||||
|
||||
PAYMENT_STATUS_FAILED = 1
|
||||
PAYMENT_STATUS_REFUNDED = 2
|
||||
|
||||
PAYMENT_STATUS_CANCELLED = 100
|
|
@ -1,30 +0,0 @@
|
|||
from payment.models import InvoicePayment, Invoice
|
||||
from payment.signals import initiate_payment
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.dispatch import receiver
|
||||
|
||||
import uuid
|
||||
|
||||
class DemoInvoicePayment(InvoicePayment):
|
||||
@property
|
||||
def gateway(self):
|
||||
return "Demo"
|
||||
|
||||
@classmethod
|
||||
def initiate(cls, invoice):
|
||||
payment = cls.objects.create(invoice=invoice, amount=invoice.balance * -1, gateway_id=uuid.uuid4())
|
||||
invoice.finalize()
|
||||
return reverse_lazy("payment:status", args=[payment.uuid])
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return 0
|
||||
|
||||
@receiver(initiate_payment)
|
||||
def from_signal(sender, **kwargs):
|
||||
if kwargs["gateway"] == "demo":
|
||||
return {"redirect": DemoInvoicePayment.initiate(kwargs["invoice"])}
|
||||
else:
|
||||
return {}
|
|
@ -1,6 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
app_name = "demo"
|
||||
|
||||
urlpatterns = [
|
||||
]
|
|
@ -1,4 +0,0 @@
|
|||
import uuid
|
||||
|
||||
def invoice_upload_path(instance, filename):
|
||||
return "/".join(["userfiles", str(instance.user.id), str(uuid.uuid4()), filename])
|
163
payment/models.py
Normal file
163
payment/models.py
Normal file
|
@ -0,0 +1,163 @@
|
|||
from django.db.models import Model, ForeignKey, DecimalField, CharField, DecimalField, UUIDField, BooleanField, CASCADE
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.conf import settings
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy, reverse
|
||||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from dbsettings.models import Setting
|
||||
|
||||
import stripe
|
||||
import uuid
|
||||
import paypalrestsdk
|
||||
|
||||
from auction.models import Inquiry
|
||||
|
||||
PAYMENT_STATUS_AUTHORIZED = -1
|
||||
PAYMENT_STATUS_SUCCESS = 0
|
||||
PAYMENT_STATUS_PENDING = 1
|
||||
PAYMENT_STATUS_FAILURE = 2
|
||||
PAYMENT_STATUS_REFUND = 3
|
||||
|
||||
stripe.api_key = Setting.objects.get(key="stripe.key.secret").value # pylint: disable=no-member
|
||||
|
||||
paypal_config = {
|
||||
"mode": Setting.objects.get(key="paypal.api.mode").value, # pylint: disable=no-member
|
||||
"client_id": Setting.objects.get(key="paypal.api.id").value, # pylint: disable=no-member
|
||||
"client_secret": Setting.objects.get(key="paypal.api.secret").value # pylint: disable=no-member
|
||||
}
|
||||
|
||||
paypalrestsdk.configure(paypal_config)
|
||||
|
||||
class Payment(PolymorphicModel):
|
||||
uuid = UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
content_type = ForeignKey(ContentType, on_delete=CASCADE)
|
||||
object_id = CharField(max_length=255)
|
||||
invoice = GenericForeignKey()
|
||||
active = BooleanField(default=True)
|
||||
|
||||
def start(self):
|
||||
raise NotImplementedError(
|
||||
"start() not implemented in %s!" % type(self).__name__)
|
||||
|
||||
def status(self):
|
||||
raise NotImplementedError(
|
||||
"status() not implemented in %s!" % type(self).__name__)
|
||||
|
||||
def capture(self):
|
||||
return self.status()
|
||||
|
||||
def cancel(self):
|
||||
invoice = self.invoice
|
||||
self.active = False
|
||||
self.save()
|
||||
return redirect(invoice.get_absolute_url() + "?status=cancelled")
|
||||
|
||||
def refund(self):
|
||||
return False
|
||||
|
||||
|
||||
class PaypalPayment(Payment):
|
||||
paypal_id = CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def start(self):
|
||||
payment = paypalrestsdk.Payment({
|
||||
"intent": "sale",
|
||||
"payer": {
|
||||
"payment_method": "paypal"},
|
||||
"redirect_urls": {
|
||||
"return_url": settings.BASE_URL +
|
||||
reverse("payment:callback", args=[self.uuid]),
|
||||
"cancel_url": settings.BASE_URL +
|
||||
reverse("payment:callback", args=[self.uuid])},
|
||||
"transactions": [{
|
||||
"item_list": {
|
||||
"items": [{
|
||||
"name": "Einzahlung",
|
||||
"price": float(self.invoice.amount),
|
||||
"currency": self.invoice.currency.upper(),
|
||||
"quantity": 1}]},
|
||||
"amount": {
|
||||
"total": float(self.invoice.amount),
|
||||
"currency": self.invoice.currency.upper()},
|
||||
"description": "Einzahlung"}]})
|
||||
|
||||
payment.create()
|
||||
|
||||
self.paypal_id = payment.id
|
||||
self.save()
|
||||
|
||||
print(repr(payment))
|
||||
|
||||
for link in payment.links:
|
||||
if link.rel == "approval_url":
|
||||
return redirect(str(link.href))
|
||||
|
||||
def status(self):
|
||||
payment = paypalrestsdk.Payment.find(self.paypal_id)
|
||||
print(repr(payment))
|
||||
return PAYMENT_STATUS_FAILURE
|
||||
|
||||
def capture(self):
|
||||
payment = paypalrestsdk.Payment.find(self.paypal_id)
|
||||
payer = payment.payer.payer_info.payer_id
|
||||
if payment.execute(payer):
|
||||
return PAYMENT_STATUS_SUCCESS
|
||||
else:
|
||||
self.active = False
|
||||
self.save()
|
||||
return PAYMENT_STATUS_FAILURE
|
||||
|
||||
class StripePayment(Payment):
|
||||
session = CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def start(self):
|
||||
self.session = stripe.checkout.Session.create(
|
||||
customer_email=self.invoice.user.user.email,
|
||||
payment_method_types=['card'],
|
||||
line_items=[{
|
||||
'name': 'Urlaubsauktion',
|
||||
'description': 'Einzahlung',
|
||||
'amount': int(self.invoice.amount * 100),
|
||||
'currency': self.invoice.currency,
|
||||
'quantity': 1,
|
||||
}],
|
||||
success_url=settings.BASE_URL +
|
||||
reverse("payment:callback", args=[self.uuid]),
|
||||
cancel_url=settings.BASE_URL +
|
||||
reverse("payment:callback", args=[self.uuid]),
|
||||
payment_intent_data={"capture_method": "manual", },
|
||||
).id
|
||||
self.save()
|
||||
return redirect(reverse("payment:redirect_stripe", args=[self.uuid]))
|
||||
|
||||
def capture(self):
|
||||
session = stripe.checkout.Session.retrieve(self.session)
|
||||
payment_intent = session.payment_intent
|
||||
capture = stripe.PaymentIntent.capture(payment_intent)
|
||||
return PAYMENT_STATUS_SUCCESS if capture.status == "succeeded" else PAYMENT_STATUS_FAILURE
|
||||
|
||||
def status(self):
|
||||
session = stripe.checkout.Session.retrieve(self.session)
|
||||
payment_intent = stripe.PaymentIntent.retrieve(session.payment_intent)
|
||||
print(payment_intent.status)
|
||||
if payment_intent.status == "processing":
|
||||
return PAYMENT_STATUS_PENDING
|
||||
elif payment_intent.status == "succeeded":
|
||||
return PAYMENT_STATUS_SUCCESS
|
||||
elif payment_intent.status == "requires_capture":
|
||||
return PAYMENT_STATUS_AUTHORIZED
|
||||
return PAYMENT_STATUS_FAILURE
|
||||
|
||||
|
||||
class KlarnaPayment(Payment):
|
||||
pass
|
||||
|
||||
|
||||
class DummyPayment(Payment):
|
||||
def start(self):
|
||||
return redirect(reverse_lazy("payment:status"))
|
||||
|
||||
def status(self):
|
||||
return PAYMENT_STATUS_SUCCESS
|
|
@ -1,4 +0,0 @@
|
|||
from .billingaddress import BillingAddress
|
||||
from .invoice import Invoice
|
||||
from .invoiceitem import InvoiceItem
|
||||
from .invoicepayment import InvoicePayment
|
|
@ -1,22 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from localauth.models import PersonMixin, AddressMixin
|
||||
|
||||
class BillingAddress(PersonMixin, AddressMixin):
|
||||
user = models.ForeignKey(get_user_model(), models.CASCADE)
|
||||
|
||||
@classmethod
|
||||
def from_profile(cls, profile):
|
||||
return cls.objects.create(
|
||||
company = profile.company,
|
||||
vat_id = profile.vat_id,
|
||||
first_name = profile.first_name,
|
||||
last_name = profile.last_name,
|
||||
street = profile.street,
|
||||
city = profile.city,
|
||||
zip = profile.zip,
|
||||
state = profile.state,
|
||||
country = profile.country,
|
||||
user = profile.user
|
||||
)
|
|
@ -1,117 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
import uuid
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
from auction.models import Inquiry
|
||||
|
||||
from .billingaddress import BillingAddress
|
||||
|
||||
from ..functions import invoice_upload_path
|
||||
from ..pdfviews import InvoicePDFView
|
||||
|
||||
class InvoiceTypeChoices(models.IntegerChoices):
|
||||
INVOICE = (0, "Rechnung")
|
||||
DEPOSIT = (1, "Sicherheitsleistung")
|
||||
|
||||
class Invoice(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
user = models.ForeignKey(get_user_model(), models.PROTECT)
|
||||
billing_address = models.ForeignKey(BillingAddress, models.PROTECT)
|
||||
currency = models.CharField(max_length=3)
|
||||
tax_rate = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
invoice = models.FileField(null=True, blank=True, upload_to=invoice_upload_path)
|
||||
inquiry = models.OneToOneField(Inquiry, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
type = models.IntegerField(choices=InvoiceTypeChoices.choices, default=1)
|
||||
|
||||
@property
|
||||
def price_net(self):
|
||||
price = 0
|
||||
|
||||
for item in self.invoiceitem_set.all():
|
||||
price += item.net_total
|
||||
|
||||
return price
|
||||
|
||||
@property
|
||||
def tax(self):
|
||||
return round(self.price_net * self.tax_rate / 100, 2)
|
||||
|
||||
@property
|
||||
def price_gross(self):
|
||||
return self.price_net + self.tax
|
||||
|
||||
@property
|
||||
def payment_instructions(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
try:
|
||||
return self.invoice.url
|
||||
except:
|
||||
return False
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
paid_amount = 0
|
||||
|
||||
for payment in self.invoicepayment_set.all():
|
||||
paid_amount += payment.amount
|
||||
|
||||
return paid_amount - self.price_gross
|
||||
|
||||
@property
|
||||
def is_paid(self):
|
||||
return self.balance >= 0
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
if self.is_paid:
|
||||
try:
|
||||
self.inquiry.process_payment(*args, **kwargs)
|
||||
except Inquiry.DoesNotExist:
|
||||
pass
|
||||
|
||||
self.generate_invoice()
|
||||
|
||||
def generate_invoice(self):
|
||||
view = InvoicePDFView()
|
||||
|
||||
bottom_tip = (f"{ self.payment_instructions }<br /><br />" if self.payment_instructions else "") + getValue("billing.bottom_tip", "")
|
||||
bottom_tip += "<br />" if bottom_tip else ""
|
||||
bottom_tip += "Dokument erstellt: %s" % str(timezone.now())
|
||||
|
||||
args = {
|
||||
# "type": InvoiceTypeChoices._value2label_map_[self.type], # TODO: Make this work again
|
||||
"type": "Rechnung",
|
||||
"object": self,
|
||||
"bottom_tip": bottom_tip
|
||||
}
|
||||
|
||||
self.invoice = ContentFile(view.render(**args), "invoice.pdf")
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def from_inquiry(cls, inquiry):
|
||||
invoice = cls.objects.create(
|
||||
user = inquiry.client.user,
|
||||
billing_address = BillingAddress.from_profile(inquiry.client),
|
||||
currency = settings.CURRENCY_CODE,
|
||||
tax_rate = 0,
|
||||
inquiry = inquiry
|
||||
)
|
||||
|
||||
invoice.invoiceitem_set.create(
|
||||
name = "SL",
|
||||
description = "Rückzahlbare Sicherheitsleistung zu JourneyJoker-Anfrage #%i" % inquiry.id,
|
||||
count = 1,
|
||||
net_each = inquiry.budget
|
||||
)
|
||||
|
||||
return invoice
|
|
@ -1,14 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
from .invoice import Invoice
|
||||
|
||||
class InvoiceItem(models.Model):
|
||||
invoice = models.ForeignKey(Invoice, models.CASCADE)
|
||||
name = models.CharField(max_length=64)
|
||||
description = models.CharField(max_length=256, null=True, blank=True)
|
||||
count = models.IntegerField()
|
||||
net_each = models.DecimalField(max_digits=11, decimal_places=2)
|
||||
|
||||
@property
|
||||
def net_total(self):
|
||||
return self.net_each * self.count
|
|
@ -1,43 +0,0 @@
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
import uuid
|
||||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from .invoice import Invoice
|
||||
|
||||
from ..signals import initiate_payment
|
||||
|
||||
class InvoicePayment(PolymorphicModel):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
invoice = models.ForeignKey(Invoice, models.PROTECT)
|
||||
amount = models.DecimalField(max_digits=9, decimal_places=2)
|
||||
gateway_id = models.CharField(max_length=256)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
@property
|
||||
def gateway(self):
|
||||
raise NotImplementedError("%s does not implement gateway" % type(self))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
raise NotImplementedError("%s does not implement status" % type(self))
|
||||
|
||||
@classmethod
|
||||
def initiate(cls, invoice):
|
||||
raise NotImplementedError("%s does not implement initiate()" % cls.__name__)
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
return self.invoice.finalize(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_invoice(cls, invoice, gateway):
|
||||
if not invoice.is_paid:
|
||||
responses = initiate_payment.send_robust(sender=cls, invoice=invoice, gateway=gateway)
|
||||
for handler, response in responses:
|
||||
try:
|
||||
return response["redirect"]
|
||||
except:
|
||||
continue
|
||||
return False
|
|
@ -1,9 +0,0 @@
|
|||
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment, LiveEnvironment
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
class PaypalAPI:
|
||||
def __init__(self, client_id=None, client_secret=None, mode=None):
|
||||
mode = SandboxEnvironment if (client_secret == "sandbox" or getValue("paypal.mode") == "sandbox") else LiveEnvironment
|
||||
environment = mode(client_id=(client_id or getValue("paypal.client_id")), client_secret=(client_secret or getValue("paypal.client_secret")))
|
||||
self.client = PayPalHttpClient(environment)
|
|
@ -1,64 +0,0 @@
|
|||
from payment.models import InvoicePayment, Invoice
|
||||
|
||||
from .api import PaypalAPI
|
||||
|
||||
from paypalcheckoutsdk.orders import OrdersCreateRequest
|
||||
from paypalhttp import HttpError
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PaypalOrder(models.Model):
|
||||
invoice = models.ForeignKey(Invoice, models.CASCADE)
|
||||
order_id = models.CharField(max_length=64)
|
||||
|
||||
class PaypalInvoicePayment(InvoicePayment):
|
||||
@property
|
||||
def gateway(self):
|
||||
return "Paypal"
|
||||
|
||||
@staticmethod
|
||||
def initiate(invoice):
|
||||
request = OrdersCreateRequest()
|
||||
request.prefer('return=representation')
|
||||
|
||||
request.request_body (
|
||||
{
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [
|
||||
{
|
||||
"amount": {
|
||||
"currency_code": invoice.currency,
|
||||
"value": float(invoice.price_gross)
|
||||
}
|
||||
}
|
||||
],
|
||||
"application_context": {
|
||||
"return_url": getValue("application.base_url") + reverse_lazy("this_sucks"),
|
||||
"cancel_url": getValue("application.base_url"),
|
||||
"brand_name": getValue("application.name", "JourneyJoker"),
|
||||
"landing_page": "BILLING",
|
||||
"user_action": "CONTINUE"
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
client = PaypalAPI().client
|
||||
response = client.execute(request)
|
||||
PaypalOrder.objects.create(subscription=subscription, order_id=response.result.id)
|
||||
|
||||
for link in response.result.links:
|
||||
if link.rel == "approve":
|
||||
return link.href
|
||||
|
||||
except IOError as ioe:
|
||||
logger.error(ioe)
|
||||
if isinstance(ioe, HttpError):
|
||||
logger.error(ioe.status_code)
|
|
@ -1,6 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
app_name = "paypal"
|
||||
|
||||
urlpatterns = [
|
||||
]
|
|
@ -1,9 +0,0 @@
|
|||
from pdf.views import PDFView
|
||||
|
||||
class InvoicePDFView(PDFView):
|
||||
template_name = "payment/invoice.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('type', "Rechnung")
|
||||
kwargs.setdefault('title', f'{kwargs["type"]} #{kwargs["object"].id}')
|
||||
return super().get_context_data(**kwargs)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue