Compare commits

..

No commits in common. "old" and "main" have entirely different histories.
old ... main

322 changed files with 48052 additions and 76172 deletions

6
.gitignore vendored
View file

@ -1,5 +1,9 @@
urlaubsauktion/database.py
*.swp
*.pyc
__pycache__/
migrations/
venv/
db.sqlite3
localsettings.py
uwsgi.local.sh
config.ini

6
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "dbsettings"]
path = dbsettings
url = git@kumig.it:kumisystems/dbsettings.git
[submodule "locale"]
path = locale
url = git@kumig.it:journeyjoker/journeyjoker-locale.git

View file

@ -1,7 +1,6 @@
from django.contrib import admin
from urlaubsauktion.admin import joker_admin as admin
from auction.models import Inquiry
from .models import Inquiry, Offer
# Register your models here.
admin.site.register(Inquiry)
admin.register(Inquiry)
admin.register(Offer)

View file

@ -1,5 +1,5 @@
from django.apps import AppConfig
class AuctionConfig(AppConfig):
name = 'auction'

View file

@ -1,24 +1,23 @@
from django.forms import ModelForm, DateField, DateInput
from django import forms
from profiles.models import ContactProfile
from auction.models import Inquiry
from django_countries.fields import CountryField
class PostPaymentForm(ModelForm):
class Meta:
model = ContactProfile
fields = ["first_name", "last_name", "address", "address2", "zipcode", "city", "country", "phone", "email"]
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 InquiryForm(ModelForm):
class Meta:
model = Inquiry
fields = ["amount", "first_date", "last_date", "destination_name", "adults", "children"]
fields = ["gateway"]
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', )
)
class OfferSelectionForm(forms.Form):
terms = forms.BooleanField(required=True)
offer = forms.UUIDField(required=True)

View file

@ -1,34 +1,110 @@
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 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 uuid import uuid4
from clients.models import ClientProfile
from partners.models import Establishment, RoomCategory
from profiles.models import ClientProfile, ContactProfile
from offers.models import Offer
from dbsettings.functions import getValue
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)
from datetime import timedelta
def get_absolute_url(self):
return reverse_lazy("auction:payment", kwargs={'pk': self.uuid})
import uuid
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 LengthChoices(models.IntegerChoices):
ANY = 0
SHORT = 1
LONG = 2
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()

View file

@ -0,0 +1,9 @@
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

View file

@ -1,11 +1,18 @@
from django.urls import path
from auction.views import InquiryView, PaymentView, PostPaymentView
from frontend.views import HomeView
from .views import InquiryCreateView, InquiryProcessView, InquiryPaymentView, OfferSelectionView, OfferSelectionTableView, BiddingListView, OfferCreationView
app_name = "auction"
urlpatterns = [
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")
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"),
]

View file

@ -1,118 +1,222 @@
from django.shortcuts import render, redirect
from django.views.generic import CreateView, DetailView, FormView
from django.urls import reverse_lazy
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.contrib.gis.geos import Point
from django.contrib.messages import error
from django.contrib.gis.db.models.functions import Distance
from django.conf import settings
from django.utils import timezone
from geopy.geocoders import Nominatim
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 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 payment.demo.models import DemoInvoicePayment # TODO: Remove when no longer needed
# Create your views here.
from .models import Inquiry, Offer
from .forms import InquiryProcessForm, OfferSelectionForm
class InquiryView(FormView):
form_class = InquiryForm
class InquiryCreateView(ClientBaseMixin, CreateView):
model = Inquiry
fields = ["destination_name", "budget", "arrival", "min_nights", "adults", "children"]
def get(self, request, *args, **kwargs):
return redirect(reverse_lazy("frontend:index"))
def form_invalid(self, form):
print(repr(form.errors))
return redirect(reverse_lazy("frontend:index") + "?invalid=true")
return redirect("/")
def form_valid(self, form):
try:
user = ClientProfile.objects.get(user=self.request.user)
except:
user = None
form.instance.destination_coords = self.clean_destination_coords()
form.instance.destination_radius = 5000
return super().form_valid(form)
lat, lon = self.request.POST.get("destination_lat", None), self.request.POST.get("destination_lon", None)
def form_invalid(self, form, *args, **kwargs):
for field in form:
for error in field.errors:
messages.error(self.request, f"{field.name}: {error}")
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
return redirect("/")
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())
def get_success_url(self):
return reverse("auction:process_inquiry", args=(self.object.uuid,))
class PaymentView(DetailView):
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/payment.html"
template_name = "auction/process.html"
class PostPaymentView(FormView):
form_class = PostPaymentForm
def get_object(self):
return Inquiry.objects.get(uuid=self.kwargs["uuid"])
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 get_initial(self):
initial = super().get_initial()
try:
initial["country"] = self.request.user.clientprofile.country.code
except:
pass
return initial
def form_valid(self, form):
#super().form_valid(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()
# ClientProfile
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"])
try:
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()
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)
# 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"]
)
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"],)))
# Inquiry
inquiry = Inquiry.objects.get(uuid=self.kwargs["pk"]) # pylint: disable=no-member
inquiry.user = client
inquiry.contact = contact
inquiry.save()
return redirect(payment_url)
# 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
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)
else:
handler = StripePayment
return Establishment.objects.filter(**kwargs).first()
payment = handler.objects.create(invoice=inquiry)
return payment.start()
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)

5
clients/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ClientsConfig(AppConfig):
name = 'clients'

17
clients/mixins.py Normal file
View file

@ -0,0 +1,17 @@
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]

9
clients/models.py Normal file
View file

@ -0,0 +1,9 @@
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

17
clients/urls.py Normal file
View file

@ -0,0 +1,17 @@
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"))),
]

86
clients/views.py Normal file
View file

@ -0,0 +1,86 @@
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)

39
config.dist.ini Normal file
View file

@ -0,0 +1,39 @@
[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 +0,0 @@
Subproject commit 47c7a84781ea1314733189e0b47adf751e889394

7
debian_setup.sh Normal file
View file

@ -0,0 +1,7 @@
#!/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

View file

@ -1,7 +1,8 @@
from django.contrib import admin
from urlaubsauktion.admin import joker_admin as admin
from frontend.models import Testimonial
from .models import Inspiration, InspirationRegion, InspirationSponsor, Testimonial
# Register your models here.
admin.site.register(Testimonial)
admin.register(Testimonial)
admin.register(Inspiration)
admin.register(InspirationRegion)
admin.register(InspirationSponsor)

View file

@ -0,0 +1,6 @@
from django.conf import settings
def demo(request):
return {
"DEBUG": settings.DEBUG,
}

23
frontend/fields.py Normal file
View file

@ -0,0 +1,23 @@
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)

12
frontend/middleware.py Normal file
View file

@ -0,0 +1,12 @@
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)

13
frontend/mixins.py Normal file
View file

@ -0,0 +1,13 @@
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

View file

@ -1,16 +1,61 @@
from django.db.models import Model, CharField, TextField, ImageField, BooleanField, PositiveIntegerField
from django.contrib.gis.db import models
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.auth import get_user_model
# Create your models here.
from localauth.models import ImageMixin
class Testimonial(Model):
name = CharField(max_length=128)
text = TextField()
stars = PositiveIntegerField(
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(
validators=[
MaxValueValidator(5),
MinValueValidator(1)
])
language = CharField(max_length=12, choices=settings.LANGUAGES)
public = BooleanField(default=False)
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

View file

15
frontend/places/api.py Normal file
View file

@ -0,0 +1,15 @@
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]

9
frontend/places/urls.py Normal file
View file

@ -0,0 +1,9 @@
from django.urls import path
from .views import PlacesAutocompleteView
app_name = "places"
urlpatterns = [
path('autocomplete', PlacesAutocompleteView.as_view(), name="autocomplete"),
]

13
frontend/places/views.py Normal file
View file

@ -0,0 +1,13 @@
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)

View file

@ -0,0 +1,30 @@
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)

View file

@ -1,27 +1,22 @@
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=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
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")
data_uri = 'data:image/jpg;base64,'
data_uri += base64.b64encode(image).decode().replace('\n', '')
data_uri += base64.b64encode(bio.getvalue()).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)

View file

@ -1,17 +0,0 @@
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

View file

@ -0,0 +1,7 @@
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)]

View file

@ -0,0 +1,7 @@
from django import template
register = template.Library()
@register.filter
def startswith(text, string):
return text.startswith(string)

View file

@ -1,7 +1,9 @@
from django import template
from frontend.models import Testimonial
from random import SystemRandom
from frontend.models import Testimonial
register = template.Library()
@register.simple_tag

View file

@ -1,10 +1,21 @@
from django.urls import path
from django.urls import path, include
from frontend.views import IndexView, change_language
from .views import HomeView, DemoTemplateView, ImpressumView, PrivacyNoticeView, TOSView, InspirationsView, LanguageChoiceView, InspirationsCountryAPIView, InspirationsRegionAPIView, InspirationsAPIView, LocaleVariableView
app_name = "frontend"
urlpatterns = [
path('', IndexView.as_view(), name="index"),
path('change_language/', change_language, name="change_language")
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"),
]

10
frontend/validators.py Normal file
View file

@ -0,0 +1,10 @@
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})

View file

@ -1,28 +1,83 @@
from django.shortcuts import redirect, render
from django.utils.translation import LANGUAGE_SESSION_KEY
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.views.generic import TemplateView
from .models import InspirationRegion, Inspiration
# Create your views here.
import django_countries
class IndexView(TemplateView):
class HomeView(TemplateView):
template_name = "frontend/index.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 Error404View(TemplateView):
template_name = "frontend/404.html"
def errorhandler(request, exception, status):
response = render(request, "frontend/error.html", {"status_code": status})
response.status_code = status
return response
class DemoTemplateView(TemplateView):
template_name = "partners/calendar.html"
def handler404(request, exception):
return errorhandler(request, exception, 404)
class ImpressumView(TemplateView):
template_name = "frontend/impressum.html"
def handler500(request):
return errorhandler(request, None, 500)
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()
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)

0
gallery/__init__.py Normal file
View file

5
gallery/admin.py Normal file
View file

@ -0,0 +1,5 @@
from urlaubsauktion.admin import joker_admin as admin
from .models import Image
admin.register(Image)

6
gallery/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GalleryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gallery'

4
gallery/helpers.py Normal file
View file

@ -0,0 +1,4 @@
from django.conf import settings
def get_upload_path(instance, filename):
return instance.upload_path or settings.GALLERY_UPLOAD_PATH

13
gallery/mixins.py Normal file
View file

@ -0,0 +1,13 @@
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

20
gallery/models.py Normal file
View file

@ -0,0 +1,20 @@
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

0
localauth/__init__.py Normal file
View file

5
localauth/admin.py Normal file
View file

@ -0,0 +1,5 @@
from urlaubsauktion.admin import joker_admin as admin
from .models import User
admin.register(User)

5
localauth/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class LocalauthConfig(AppConfig):
name = 'localauth'

25
localauth/forms.py Normal file
View file

@ -0,0 +1,25 @@
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)

30
localauth/helpers.py Normal file
View file

@ -0,0 +1,30 @@
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)])

54
localauth/mixins.py Normal file
View file

@ -0,0 +1,54 @@
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 ''

160
localauth/models.py Normal file
View file

@ -0,0 +1,160 @@
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__)

3
localauth/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
localauth/urls.py Normal file
View file

@ -0,0 +1,12 @@
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"),
]

60
localauth/views.py Normal file
View file

@ -0,0 +1,60 @@
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 Submodule

@ -0,0 +1 @@
Subproject commit 5a8fa82926cc81b5f4bea697b3d568ee1c71dfe3

0
mail/__init__.py Normal file
View file

3
mail/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
mail/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MailConfig(AppConfig):
name = 'mail'

3
mail/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View file

View file

@ -0,0 +1,29 @@
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)

3
mail/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

70
mail/views.py Normal file
View file

@ -0,0 +1,70 @@
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()

View file

@ -1,10 +1,11 @@
#!/usr/bin/env python3
#!/usr/bin/env python
"""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

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class OffersConfig(AppConfig):
name = 'offers'

View file

@ -1,67 +0,0 @@
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)

View file

@ -1,29 +0,0 @@
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"))
]

View file

@ -1,2 +0,0 @@
python3-pip
gdal-bin

0
partners/__init__.py Normal file
View file

7
partners/admin.py Normal file
View file

@ -0,0 +1,7 @@
from urlaubsauktion.admin import joker_admin as admin
from .models import PartnerProfile, Establishment, RoomCategory
admin.register(PartnerProfile)
admin.register(Establishment)
admin.register(RoomCategory)

5
partners/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PartnersConfig(AppConfig):
name = 'partners'

54
partners/features.py Normal file
View file

@ -0,0 +1,54 @@
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)

11
partners/forms.py Normal file
View file

@ -0,0 +1,11 @@
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)

12
partners/mixins.py Normal file
View file

@ -0,0 +1,12 @@
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")

107
partners/models.py Normal file
View file

@ -0,0 +1,107 @@
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"])

View file

View file

@ -0,0 +1,32 @@
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

3
partners/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
partners/urls.py Normal file
View file

@ -0,0 +1,22 @@
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"))),
]

279
partners/views.py Normal file
View file

@ -0,0 +1,279 @@
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

View file

@ -1,10 +1,16 @@
from django.contrib import admin
from payment.models import Payment, KlarnaPayment, PaypalPayment, StripePayment, DummyPayment
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
# Register your models here.
admin.site.register(Payment)
admin.site.register(KlarnaPayment)
admin.site.register(PaypalPayment)
admin.site.register(StripePayment)
admin.site.register(DummyPayment)
admin.register(InvoicePayment)
admin.register(Invoice)
admin.register(InvoiceItem)
admin.register(PaypalInvoicePayment)
admin.register(SepaInvoicePayment)
admin.register(DemoInvoicePayment)

11
payment/const.py Normal file
View file

@ -0,0 +1,11 @@
# 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

0
payment/demo/__init__.py Normal file
View file

30
payment/demo/models.py Normal file
View file

@ -0,0 +1,30 @@
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 {}

6
payment/demo/urls.py Normal file
View file

@ -0,0 +1,6 @@
from django.urls import path
app_name = "demo"
urlpatterns = [
]

4
payment/functions.py Normal file
View file

@ -0,0 +1,4 @@
import uuid
def invoice_upload_path(instance, filename):
return "/".join(["userfiles", str(instance.user.id), str(uuid.uuid4()), filename])

View file

@ -1,163 +0,0 @@
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

View file

@ -0,0 +1,4 @@
from .billingaddress import BillingAddress
from .invoice import Invoice
from .invoiceitem import InvoiceItem
from .invoicepayment import InvoicePayment

View file

@ -0,0 +1,22 @@
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
)

117
payment/models/invoice.py Normal file
View file

@ -0,0 +1,117 @@
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

View file

@ -0,0 +1,14 @@
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

View file

@ -0,0 +1,43 @@
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

View file

9
payment/paypal/api.py Normal file
View file

@ -0,0 +1,9 @@
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)

64
payment/paypal/models.py Normal file
View file

@ -0,0 +1,64 @@
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)

6
payment/paypal/urls.py Normal file
View file

@ -0,0 +1,6 @@
from django.urls import path
app_name = "paypal"
urlpatterns = [
]

Some files were not shown because too many files have changed in this diff Show more