Compare commits

...

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

322 changed files with 76176 additions and 48056 deletions

6
.gitignore vendored
View file

@ -1,9 +1,5 @@
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 "locale"]
path = locale
url = git@kumig.it:journeyjoker/journeyjoker-locale.git
[submodule "dbsettings"]
path = dbsettings
url = git@kumig.it:kumisystems/dbsettings.git

View file

@ -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)

View file

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

View file

@ -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', )
)

View file

@ -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 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()
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()

View file

@ -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

View file

@ -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")
]

View file

@ -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()
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])
handler = StripePayment
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()

View file

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

View file

@ -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]

View file

@ -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

View file

@ -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"))),
]

View file

@ -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)

View file

@ -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

@ -0,0 +1 @@
Subproject commit 47c7a84781ea1314733189e0b47adf751e889394

View file

@ -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

View file

@ -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)

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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]

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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)]

View file

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

View file

@ -1,8 +1,6 @@
from django import template
from random import SystemRandom
from frontend.models import Testimonial
from random import SystemRandom
register = template.Library()
@ -10,4 +8,4 @@ register = template.Library()
def testimonials(count=5, language="de"):
objects = list(Testimonial.objects.filter(language=language)) # pylint: disable=no-member
SystemRandom().shuffle(objects)
return objects[:min(count, len(objects))]
return objects[:min(count, len(objects))]

View file

@ -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")
]

View file

@ -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})

View file

@ -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"
def errorhandler(request, exception, status):
response = render(request, "frontend/error.html", {"status_code": status})
response.status_code = status
return response
class ImpressumView(TemplateView):
template_name = "frontend/impressum.html"
def handler404(request, exception):
return errorhandler(request, exception, 404)
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)
def handler500(request):
return errorhandler(request, None, 500)

View file

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

View file

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

View file

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

View file

@ -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)

View file

@ -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)])

View file

@ -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 ''

View file

@ -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__)

View file

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

View file

@ -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"),
]

View file

@ -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 +0,0 @@
Subproject commit 5a8fa82926cc81b5f4bea697b3d568ee1c71dfe3

View file

View file

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

View file

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

View file

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

View file

@ -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)

View file

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

View file

@ -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()

View file

@ -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
View file

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

67
offers/models.py Normal file
View 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
View 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
View file

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

View file

View file

@ -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)

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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"])

View file

@ -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

View file

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

View file

@ -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"))),
]

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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 {}

View file

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

View file

@ -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
View 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

View file

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

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

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

View file

@ -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