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