Compare commits
No commits in common. "old" and "main" have entirely different histories.
322 changed files with 48052 additions and 76172 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,5 +1,9 @@
|
||||||
urlaubsauktion/database.py
|
|
||||||
*.swp
|
*.swp
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/
|
__pycache__/
|
||||||
migrations/
|
migrations/
|
||||||
|
venv/
|
||||||
|
db.sqlite3
|
||||||
|
localsettings.py
|
||||||
|
uwsgi.local.sh
|
||||||
|
config.ini
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
||||||
[submodule "dbsettings"]
|
[submodule "locale"]
|
||||||
path = dbsettings
|
path = locale
|
||||||
url = git@kumig.it:kumisystems/dbsettings.git
|
url = git@kumig.it:journeyjoker/journeyjoker-locale.git
|
||||||
|
|
|
@ -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.register(Inquiry)
|
||||||
|
admin.register(Offer)
|
||||||
admin.site.register(Inquiry)
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class AuctionConfig(AppConfig):
|
class AuctionConfig(AppConfig):
|
||||||
name = 'auction'
|
name = 'auction'
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
from django.forms import ModelForm, DateField, DateInput
|
from django import forms
|
||||||
|
|
||||||
from profiles.models import ContactProfile
|
from django_countries.fields import CountryField
|
||||||
from auction.models import Inquiry
|
|
||||||
|
|
||||||
class PostPaymentForm(ModelForm):
|
from .models import Inquiry
|
||||||
class Meta:
|
|
||||||
model = ContactProfile
|
class InquiryProcessForm(forms.ModelForm):
|
||||||
fields = ["first_name", "last_name", "address", "address2", "zipcode", "city", "country", "phone", "email"]
|
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:
|
class Meta:
|
||||||
model = Inquiry
|
model = Inquiry
|
||||||
fields = ["amount", "first_date", "last_date", "destination_name", "adults", "children"]
|
fields = ["gateway"]
|
||||||
|
|
||||||
first_date = DateField(
|
class OfferSelectionForm(forms.Form):
|
||||||
widget=DateInput(format='%d.%m.%Y'),
|
terms = forms.BooleanField(required=True)
|
||||||
input_formats=('%d.%m.%Y', )
|
offer = forms.UUIDField(required=True)
|
||||||
)
|
|
||||||
|
|
||||||
last_date = DateField(
|
|
||||||
widget=DateInput(format='%d.%m.%Y'),
|
|
||||||
input_formats=('%d.%m.%Y', )
|
|
||||||
)
|
|
|
@ -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 import models
|
||||||
from django.contrib.gis.db.models import PointField
|
from django.utils import timezone
|
||||||
from django.urls import reverse_lazy
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
from django.conf import settings
|
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 dbsettings.functions import getValue
|
||||||
from offers.models import Offer
|
|
||||||
|
|
||||||
class Inquiry(Model):
|
from datetime import timedelta
|
||||||
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)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
import uuid
|
||||||
return reverse_lazy("auction:payment", kwargs={'pk': self.uuid})
|
|
||||||
|
|
||||||
class Offer(Model):
|
class LengthChoices(models.IntegerChoices):
|
||||||
uuid = UUIDField(default=uuid4, primary_key=True)
|
ANY = 0
|
||||||
inquiry = ForeignKey(Inquiry, on_delete=CASCADE)
|
SHORT = 1
|
||||||
offer = ForeignKey(Offer, on_delete=CASCADE)
|
LONG = 2
|
||||||
arrival_date = DateField()
|
|
||||||
departure_date = DateField()
|
|
||||||
|
|
||||||
|
class Inquiry(models.Model):
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
|
client = models.ForeignKey(ClientProfile, models.PROTECT, null=True, blank=True)
|
||||||
|
destination_name = models.CharField(max_length=128)
|
||||||
|
destination_coords = models.PointField()
|
||||||
|
destination_radius = models.IntegerField()
|
||||||
|
arrival = models.DateField()
|
||||||
|
min_nights = models.IntegerField(default=0, choices=LengthChoices.choices)
|
||||||
|
budget = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
adults = models.IntegerField()
|
||||||
|
children = models.IntegerField()
|
||||||
|
comment = models.TextField(null=True, blank=True)
|
||||||
|
activated = models.DateTimeField(null=True, blank=True)
|
||||||
|
bidding_end = models.DateTimeField(null=True, blank=True)
|
||||||
|
auction_end = models.DateTimeField(null=True, blank=True)
|
||||||
|
expiry = models.DateTimeField(null=True, blank=True)
|
||||||
|
gateway = models.CharField(max_length=128, null=True, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_paid(self):
|
||||||
|
if not self.invoice:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.invoice.is_paid
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
if self.activated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.activated = timezone.now()
|
||||||
|
self.bidding_end = self.activated + timedelta(hours=int(getValue("auction.bidding_period", 24)))
|
||||||
|
self.auction_end = self.bidding_end + timedelta(hours=int(getValue("auction.selection_period", 24)))
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def process_payment(self):
|
||||||
|
if self.invoice.is_paid:
|
||||||
|
self.activate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return self.expiry and (self.expiry < timezone.now())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_bidding(self):
|
||||||
|
return self.active and (self.bidding_end > timezone.now())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_selection(self):
|
||||||
|
return self.active and (self.auction_end > timezone.now()) and not self.in_bidding
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active(self):
|
||||||
|
try:
|
||||||
|
return bool(self.activated) and not self.expired
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accepted(self):
|
||||||
|
try:
|
||||||
|
return Offer.objects.get(inquiry=self, accepted__isnull=False)
|
||||||
|
except Offer.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Offer(models.Model):
|
||||||
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
|
inquiry = models.ForeignKey(Inquiry, models.PROTECT)
|
||||||
|
roomcategory = models.ForeignKey(RoomCategory, models.PROTECT)
|
||||||
|
departure = models.DateField()
|
||||||
|
comment = models.TextField(null=True, blank=True)
|
||||||
|
accepted = models.DateTimeField(null=True, blank=True)
|
||||||
|
hidden = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def establishment(self):
|
||||||
|
return self.roomcategory.establishment
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nights(self):
|
||||||
|
return (self.departure - self.inquiry.arrival).days
|
||||||
|
|
||||||
|
@property
|
||||||
|
def distance(self):
|
||||||
|
return self.inquiry.destination_coords.distance(self.roomcategory.establishment.coords)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Inquiry)
|
||||||
|
def inquiry_expiry(sender, instance, created, **kwargs):
|
||||||
|
if not instance.expiry:
|
||||||
|
instance.expiry = min(instance.arrival, (timezone.now() + timedelta(hours=int(getValue("auction.payment_period", 168)))).date())
|
||||||
|
instance.save()
|
||||||
|
|
9
auction/templatetags/distance.py
Normal file
9
auction/templatetags/distance.py
Normal 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
|
|
@ -1,11 +1,18 @@
|
||||||
from django.urls import path
|
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"
|
app_name = "auction"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('create_inquiry/', InquiryView.as_view(), name="create_inquiry"),
|
path('create_inquiry/', InquiryCreateView.as_view(), name="create_inquiry"),
|
||||||
path('<uuid:pk>/payment/', PaymentView.as_view(), name="payment"),
|
path('process_inquiry/<slug:uuid>/', InquiryProcessView.as_view(), name="process_inquiry"),
|
||||||
path('<uuid:pk>/post_payment/', PostPaymentView.as_view(), name="post_payment")
|
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"),
|
||||||
]
|
]
|
288
auction/views.py
288
auction/views.py
|
@ -1,118 +1,222 @@
|
||||||
from django.shortcuts import render, redirect
|
from django.views.generic import CreateView, UpdateView, View, ListView, DetailView, FormView
|
||||||
from django.views.generic import CreateView, DetailView, FormView
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.contrib import messages
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.contrib.gis.geos import Point
|
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.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 payment.demo.models import DemoInvoicePayment # TODO: Remove when no longer needed
|
||||||
from profiles.models import ClientProfile, ContactProfile
|
|
||||||
from auction.forms import PostPaymentForm, InquiryForm
|
|
||||||
from payment.models import KlarnaPayment, PaypalPayment, StripePayment, DummyPayment
|
|
||||||
|
|
||||||
# Create your views here.
|
from .models import Inquiry, Offer
|
||||||
|
from .forms import InquiryProcessForm, OfferSelectionForm
|
||||||
|
|
||||||
class InquiryView(FormView):
|
class InquiryCreateView(ClientBaseMixin, CreateView):
|
||||||
form_class = InquiryForm
|
model = Inquiry
|
||||||
|
fields = ["destination_name", "budget", "arrival", "min_nights", "adults", "children"]
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
return redirect(reverse_lazy("frontend:index"))
|
return redirect("/")
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
print(repr(form.errors))
|
|
||||||
return redirect(reverse_lazy("frontend:index") + "?invalid=true")
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
try:
|
form.instance.destination_coords = self.clean_destination_coords()
|
||||||
user = ClientProfile.objects.get(user=self.request.user)
|
form.instance.destination_radius = 5000
|
||||||
except:
|
return super().form_valid(form)
|
||||||
user = None
|
|
||||||
|
|
||||||
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):
|
return redirect("/")
|
||||||
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(
|
def get_success_url(self):
|
||||||
user = user,
|
return reverse("auction:process_inquiry", args=(self.object.uuid,))
|
||||||
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):
|
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
|
model = Inquiry
|
||||||
template_name = "auction/payment.html"
|
template_name = "auction/process.html"
|
||||||
|
|
||||||
class PostPaymentView(FormView):
|
def get_object(self):
|
||||||
form_class = PostPaymentForm
|
return Inquiry.objects.get(uuid=self.kwargs["uuid"])
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def get_initial(self):
|
||||||
#super().form_invalid(form)
|
initial = super().get_initial()
|
||||||
for _dumbo, errormsg in form.errors:
|
|
||||||
error(self.request, errormsg)
|
try:
|
||||||
return redirect(reverse_lazy("auction:payment", kwargs={'pk': self.kwargs["pk"]}))
|
initial["country"] = self.request.user.clientprofile.country.code
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
def form_valid(self, form):
|
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:
|
try:
|
||||||
client = ClientProfile.objects.get(user=self.request.user)
|
invoice = inquiry.invoice
|
||||||
except ClientProfile.DoesNotExist: # pylint: disable=no-member
|
except Invoice.DoesNotExist:
|
||||||
client = ClientProfile.objects.create(
|
invoice = Invoice.from_inquiry(inquiry)
|
||||||
user = self.request.user,
|
# payment_url = InvoicePayment.from_invoice(invoice, inquiry.gateway) # TODO: Make this work again
|
||||||
first_name = form.cleaned_data["first_name"],
|
payment_url = DemoInvoicePayment.initiate(invoice)
|
||||||
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()
|
|
||||||
|
|
||||||
# ContactProfile
|
if not payment_url:
|
||||||
contact = ContactProfile.objects.create(
|
messages.error(request, "Die Zahlung ist leider fehlgeschlagen. Versuche es bitte nochmals!")
|
||||||
user = self.request.user,
|
return redirect(reverse("auction:process_inquiry", args=(kwargs["uuid"],)))
|
||||||
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"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Inquiry
|
return redirect(payment_url)
|
||||||
inquiry = Inquiry.objects.get(uuid=self.kwargs["pk"]) # pylint: disable=no-member
|
|
||||||
inquiry.user = client
|
|
||||||
inquiry.contact = contact
|
|
||||||
inquiry.save()
|
|
||||||
|
|
||||||
# Payment
|
class OfferSelectionView(ClientBaseMixin, FormView, DetailView):
|
||||||
gateway = self.request.POST.get("gateway").lower()
|
model = Inquiry
|
||||||
if gateway == "paypal":
|
form_class = OfferSelectionForm
|
||||||
handler = PaypalPayment
|
|
||||||
elif gateway == "dummy" and settings.DEBUG:
|
def get_template_names(self):
|
||||||
handler = DummyPayment
|
inquiry = self.get_object()
|
||||||
elif gateway == "klarna":
|
|
||||||
handler = KlarnaPayment
|
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:
|
else:
|
||||||
handler = StripePayment
|
return Establishment.objects.filter(**kwargs).first()
|
||||||
|
|
||||||
payment = handler.objects.create(invoice=inquiry)
|
def get_inquiry(self):
|
||||||
return payment.start()
|
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
5
clients/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ClientsConfig(AppConfig):
|
||||||
|
name = 'clients'
|
17
clients/mixins.py
Normal file
17
clients/mixins.py
Normal 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
9
clients/models.py
Normal 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
17
clients/urls.py
Normal 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
86
clients/views.py
Normal 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
39
config.dist.ini
Normal 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
7
debian_setup.sh
Normal 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
|
|
@ -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.register(Testimonial)
|
||||||
|
admin.register(Inspiration)
|
||||||
admin.site.register(Testimonial)
|
admin.register(InspirationRegion)
|
||||||
|
admin.register(InspirationSponsor)
|
6
frontend/context_processors.py
Normal file
6
frontend/context_processors.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
def demo(request):
|
||||||
|
return {
|
||||||
|
"DEBUG": settings.DEBUG,
|
||||||
|
}
|
23
frontend/fields.py
Normal file
23
frontend/fields.py
Normal 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
12
frontend/middleware.py
Normal 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
13
frontend/mixins.py
Normal 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
|
|
@ -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.conf import settings
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
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):
|
from .fields import LanguageField
|
||||||
name = CharField(max_length=128)
|
|
||||||
text = TextField()
|
from django_countries.fields import CountryField
|
||||||
stars = PositiveIntegerField(
|
|
||||||
|
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=[
|
validators=[
|
||||||
MaxValueValidator(5),
|
MaxValueValidator(5),
|
||||||
MinValueValidator(1)
|
MinValueValidator(1)
|
||||||
])
|
])
|
||||||
language = CharField(max_length=12, choices=settings.LANGUAGES)
|
language = LanguageField()
|
||||||
public = BooleanField(default=False)
|
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
|
0
frontend/places/__init__.py
Normal file
0
frontend/places/__init__.py
Normal file
15
frontend/places/api.py
Normal file
15
frontend/places/api.py
Normal 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
9
frontend/places/urls.py
Normal 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
13
frontend/places/views.py
Normal 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)
|
30
frontend/templatetags/datecalc.py
Normal file
30
frontend/templatetags/datecalc.py
Normal 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)
|
|
@ -1,27 +1,22 @@
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
import requests
|
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
|
from staticmap import StaticMap, CircleMarker
|
||||||
|
|
||||||
from dbsettings.models import Setting
|
from dbsettings.models import Setting
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def mapimage(location, zoom=8, width=348, height=250):
|
def mapimage(location, zoom=7, width=348, height=250):
|
||||||
payload = {
|
smap = StaticMap(width, height, url_template="https://tile.openstreetmap.de/{z}/{x}/{y}.png")
|
||||||
'center': location,
|
marker = CircleMarker((location.x, location.y), color="orange", width=20)
|
||||||
'zoom': str(zoom),
|
smap.add_marker(marker)
|
||||||
"size": "%ix%i" % (width, height),
|
image = smap.render(zoom)
|
||||||
'sensor': "false",
|
bio = io.BytesIO()
|
||||||
'key': Setting.objects.get(key="google.api.key").value # pylint: disable=no-member
|
image.save(bio, format="JPEG")
|
||||||
}
|
|
||||||
r = requests.get('https://maps.googleapis.com/maps/api/staticmap', params=payload)
|
|
||||||
image = r.content
|
|
||||||
data_uri = 'data:image/jpg;base64,'
|
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
|
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)
|
|
|
@ -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
|
|
7
frontend/templatetags/splitter.py
Normal file
7
frontend/templatetags/splitter.py
Normal 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)]
|
7
frontend/templatetags/startswith.py
Normal file
7
frontend/templatetags/startswith.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def startswith(text, string):
|
||||||
|
return text.startswith(string)
|
|
@ -1,7 +1,9 @@
|
||||||
from django import template
|
from django import template
|
||||||
from frontend.models import Testimonial
|
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
|
||||||
|
from frontend.models import Testimonial
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
|
|
@ -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"
|
app_name = "frontend"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', IndexView.as_view(), name="index"),
|
path('', HomeView.as_view(), name="home"),
|
||||||
path('change_language/', change_language, name="change_language")
|
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
10
frontend/validators.py
Normal 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})
|
|
@ -1,28 +1,83 @@
|
||||||
from django.shortcuts import redirect, render
|
from django.views.generic import TemplateView, View
|
||||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
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"
|
template_name = "frontend/index.html"
|
||||||
|
|
||||||
def change_language(request):
|
class Error404View(TemplateView):
|
||||||
url = request.GET.get('url', '/')
|
template_name = "frontend/404.html"
|
||||||
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)
|
|
||||||
|
|
||||||
def errorhandler(request, exception, status):
|
class DemoTemplateView(TemplateView):
|
||||||
response = render(request, "frontend/error.html", {"status_code": status})
|
template_name = "partners/calendar.html"
|
||||||
response.status_code = status
|
|
||||||
|
class ImpressumView(TemplateView):
|
||||||
|
template_name = "frontend/impressum.html"
|
||||||
|
|
||||||
|
class PrivacyNoticeView(TemplateView):
|
||||||
|
template_name = "frontend/privacy.html"
|
||||||
|
|
||||||
|
class TOSView(TemplateView):
|
||||||
|
template_name = "frontend/terms.html"
|
||||||
|
|
||||||
|
class InspirationsView(TemplateView):
|
||||||
|
template_name = "frontend/inspirations.html"
|
||||||
|
|
||||||
|
class InspirationsCountryAPIView(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
countries = [{"code": country[0], "name": django_countries.countries.name(country[0])} for country in InspirationRegion.country_set.all()]
|
||||||
|
return JsonResponse(countries, safe=False)
|
||||||
|
|
||||||
|
class InspirationsRegionAPIView(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
regions = [{"id": region.id, "name": region.name, "state": region.is_state} for region in InspirationRegion.objects.filter(country=kwargs["country"])]
|
||||||
|
return JsonResponse(regions, safe=False)
|
||||||
|
|
||||||
|
class InspirationsAPIView(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
inspirations = [
|
||||||
|
{
|
||||||
|
"id": inspiration.id,
|
||||||
|
"title": inspiration.title,
|
||||||
|
"subtitle": inspiration.subtitle,
|
||||||
|
"image": inspiration.image.url,
|
||||||
|
"sponsor": {
|
||||||
|
"id": inspiration.sponsor.id,
|
||||||
|
"name": inspiration.sponsor.name,
|
||||||
|
"image": inspiration.sponsor.image.url
|
||||||
|
},
|
||||||
|
"content": inspiration.content,
|
||||||
|
"destination": {
|
||||||
|
"name": inspiration.destination_name,
|
||||||
|
"coords": {
|
||||||
|
"lat": inspiration.destination_coords.y,
|
||||||
|
"lon": inspiration.destination_coords.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} for inspiration in Inspiration.objects.filter(region__id=kwargs["region"])]
|
||||||
|
|
||||||
|
return JsonResponse(inspirations, safe=False)
|
||||||
|
|
||||||
|
class LanguageChoiceView(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
response = HttpResponse()
|
||||||
|
for language, _ in settings.LANGUAGES:
|
||||||
|
if language.startswith(kwargs["code"]):
|
||||||
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
request.user.language = language
|
||||||
|
request.user.save()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def handler404(request, exception):
|
class LocaleVariableView(View):
|
||||||
return errorhandler(request, exception, 404)
|
def get(self, request, *args, **kwargs):
|
||||||
|
variables = {
|
||||||
def handler500(request):
|
"date_format": get_format('SHORT_DATE_FORMAT', use_l10n=True).replace("Y", "yyyy")
|
||||||
return errorhandler(request, None, 500)
|
}
|
||||||
|
return JsonResponse(variables)
|
||||||
|
|
0
gallery/__init__.py
Normal file
0
gallery/__init__.py
Normal file
5
gallery/admin.py
Normal file
5
gallery/admin.py
Normal 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
6
gallery/apps.py
Normal 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
4
gallery/helpers.py
Normal 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
13
gallery/mixins.py
Normal 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
20
gallery/models.py
Normal 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
0
localauth/__init__.py
Normal file
5
localauth/admin.py
Normal file
5
localauth/admin.py
Normal 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
5
localauth/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LocalauthConfig(AppConfig):
|
||||||
|
name = 'localauth'
|
25
localauth/forms.py
Normal file
25
localauth/forms.py
Normal 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
30
localauth/helpers.py
Normal 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
54
localauth/mixins.py
Normal 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
160
localauth/models.py
Normal 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
3
localauth/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
12
localauth/urls.py
Normal file
12
localauth/urls.py
Normal 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
60
localauth/views.py
Normal 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
1
locale
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5a8fa82926cc81b5f4bea697b3d568ee1c71dfe3
|
0
mail/__init__.py
Normal file
0
mail/__init__.py
Normal file
3
mail/admin.py
Normal file
3
mail/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
mail/apps.py
Normal file
5
mail/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MailConfig(AppConfig):
|
||||||
|
name = 'mail'
|
3
mail/models.py
Normal file
3
mail/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
0
mail/templatetags/__init__.py
Normal file
0
mail/templatetags/__init__.py
Normal file
29
mail/templatetags/file_to_data.py
Normal file
29
mail/templatetags/file_to_data.py
Normal 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
3
mail/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
70
mail/views.py
Normal file
70
mail/views.py
Normal 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()
|
|
@ -1,10 +1,11 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python
|
||||||
"""Django's command-line utility for administrative tasks."""
|
"""Django's command-line utility for administrative tasks."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'urlaubsauktion.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'urlaubsauktion.settings')
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class OffersConfig(AppConfig):
|
|
||||||
name = 'offers'
|
|
|
@ -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)
|
|
|
@ -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"))
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
python3-pip
|
|
||||||
gdal-bin
|
|
0
partners/__init__.py
Normal file
0
partners/__init__.py
Normal file
7
partners/admin.py
Normal file
7
partners/admin.py
Normal 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
5
partners/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PartnersConfig(AppConfig):
|
||||||
|
name = 'partners'
|
54
partners/features.py
Normal file
54
partners/features.py
Normal 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
11
partners/forms.py
Normal 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
12
partners/mixins.py
Normal 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
107
partners/models.py
Normal 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"])
|
0
partners/templatetags/__init__.py
Normal file
0
partners/templatetags/__init__.py
Normal file
32
partners/templatetags/offeroptions.py
Normal file
32
partners/templatetags/offeroptions.py
Normal 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
3
partners/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
22
partners/urls.py
Normal file
22
partners/urls.py
Normal 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
279
partners/views.py
Normal 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
|
|
@ -1,10 +1,16 @@
|
||||||
from django.contrib import admin
|
from urlaubsauktion.admin import joker_admin as admin
|
||||||
from payment.models import Payment, KlarnaPayment, PaypalPayment, StripePayment, DummyPayment
|
|
||||||
|
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.
|
# Register your models here.
|
||||||
|
|
||||||
admin.site.register(Payment)
|
admin.register(InvoicePayment)
|
||||||
admin.site.register(KlarnaPayment)
|
admin.register(Invoice)
|
||||||
admin.site.register(PaypalPayment)
|
admin.register(InvoiceItem)
|
||||||
admin.site.register(StripePayment)
|
|
||||||
admin.site.register(DummyPayment)
|
admin.register(PaypalInvoicePayment)
|
||||||
|
admin.register(SepaInvoicePayment)
|
||||||
|
admin.register(DemoInvoicePayment)
|
11
payment/const.py
Normal file
11
payment/const.py
Normal 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
0
payment/demo/__init__.py
Normal file
30
payment/demo/models.py
Normal file
30
payment/demo/models.py
Normal 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
6
payment/demo/urls.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
app_name = "demo"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
]
|
4
payment/functions.py
Normal file
4
payment/functions.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
def invoice_upload_path(instance, filename):
|
||||||
|
return "/".join(["userfiles", str(instance.user.id), str(uuid.uuid4()), filename])
|
|
@ -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
|
|
4
payment/models/__init__.py
Normal file
4
payment/models/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .billingaddress import BillingAddress
|
||||||
|
from .invoice import Invoice
|
||||||
|
from .invoiceitem import InvoiceItem
|
||||||
|
from .invoicepayment import InvoicePayment
|
22
payment/models/billingaddress.py
Normal file
22
payment/models/billingaddress.py
Normal 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
117
payment/models/invoice.py
Normal 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
|
14
payment/models/invoiceitem.py
Normal file
14
payment/models/invoiceitem.py
Normal 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
|
43
payment/models/invoicepayment.py
Normal file
43
payment/models/invoicepayment.py
Normal 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
|
0
payment/paypal/__init__.py
Normal file
0
payment/paypal/__init__.py
Normal file
9
payment/paypal/api.py
Normal file
9
payment/paypal/api.py
Normal 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
64
payment/paypal/models.py
Normal 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
6
payment/paypal/urls.py
Normal 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
Loading…
Reference in a new issue