Implement image gallery for establishments
This commit is contained in:
parent
4ccd5784c4
commit
553b6fae23
10 changed files with 221 additions and 8 deletions
|
@ -1,3 +1,5 @@
|
||||||
from django.contrib import admin
|
from urlaubsauktion.admin import joker_admin as admin
|
||||||
|
|
||||||
# Register your models here.
|
from .models import Image
|
||||||
|
|
||||||
|
admin.register(Image)
|
4
gallery/helpers.py
Normal file
4
gallery/helpers.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
def get_upload_path(instance, filename):
|
||||||
|
return instance.upload_path or settings.GALLERY_UPLOAD_PATH
|
13
gallery/mixins.py
Normal file
13
gallery/mixins.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .models import Image
|
||||||
|
|
||||||
|
class GalleryMixin(models.Model):
|
||||||
|
image_set = GenericRelation(Image)
|
||||||
|
|
||||||
|
def add_image(self, image, upload_path=None):
|
||||||
|
Image.objects.create(image=image, content_object=self, upload_path=upload_path)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
|
@ -1,3 +1,20 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
# Create your models here.
|
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
|
|
@ -3,6 +3,7 @@ from django.contrib.gis.db import models
|
||||||
from .features import *
|
from .features import *
|
||||||
|
|
||||||
from localauth.models import User, Profile, LocationMixin, ImageMixin, PhoneMixin
|
from localauth.models import User, Profile, LocationMixin, ImageMixin, PhoneMixin
|
||||||
|
from gallery.mixins import GalleryMixin
|
||||||
|
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ class PartnerProfile(Profile):
|
||||||
def roomcategory_set(self):
|
def roomcategory_set(self):
|
||||||
return RoomCategory.objects.filter(establishment__in=self.establishment_set.all())
|
return RoomCategory.objects.filter(establishment__in=self.establishment_set.all())
|
||||||
|
|
||||||
class Establishment(LocationMixin, ImageMixin, PhoneMixin):
|
class Establishment(LocationMixin, ImageMixin, PhoneMixin, GalleryMixin):
|
||||||
owner = models.ForeignKey(PartnerProfile, models.CASCADE)
|
owner = models.ForeignKey(PartnerProfile, models.CASCADE)
|
||||||
name = models.CharField("Name", max_length=64)
|
name = models.CharField("Name", max_length=64)
|
||||||
stars = models.IntegerField("Sterne", null=True, blank=True)
|
stars = models.IntegerField("Sterne", null=True, blank=True)
|
||||||
|
@ -40,7 +41,7 @@ class Establishment(LocationMixin, ImageMixin, PhoneMixin):
|
||||||
|
|
||||||
return querysets[0].union(*querysets[1:])
|
return querysets[0].union(*querysets[1:])
|
||||||
|
|
||||||
class RoomCategory(ImageMixin):
|
class RoomCategory(GalleryMixin):
|
||||||
establishment = models.ForeignKey(Establishment, models.CASCADE)
|
establishment = models.ForeignKey(Establishment, models.CASCADE)
|
||||||
name = models.CharField("Name", max_length=64)
|
name = models.CharField("Name", max_length=64)
|
||||||
average_price = models.DecimalField("Durchschnittspreis / Nacht", max_digits=10, decimal_places=2)
|
average_price = models.DecimalField("Durchschnittspreis / Nacht", max_digits=10, decimal_places=2)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.urls import path, reverse_lazy
|
from django.urls import path, reverse_lazy
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from .views import PartnerRegistrationView, PartnerProfileView, OffersListView, EstablishmentsListView, EstablishmentRequestView, PartnerDashboardView, EstablishmentVerificationView, RoomCategoryListView
|
from .views import PartnerRegistrationView, PartnerProfileView, OffersListView, EstablishmentsListView, EstablishmentRequestView, PartnerDashboardView, EstablishmentVerificationView, RoomCategoryListView, EstablishmentGalleryManagementView
|
||||||
|
|
||||||
app_name = "partners"
|
app_name = "partners"
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ urlpatterns = [
|
||||||
path('profile/', PartnerProfileView.as_view(), name="profile"),
|
path('profile/', PartnerProfileView.as_view(), name="profile"),
|
||||||
path('establishments/', EstablishmentsListView.as_view(), name="establishments"),
|
path('establishments/', EstablishmentsListView.as_view(), name="establishments"),
|
||||||
path('establishments/<int:id>/', RoomCategoryListView.as_view(), name="roomcategories"),
|
path('establishments/<int:id>/', RoomCategoryListView.as_view(), name="roomcategories"),
|
||||||
|
path('establishments/<int:id>/gallery/', EstablishmentGalleryManagementView.as_view(), name="establishment_gallery"),
|
||||||
path('establishments/validate/', EstablishmentVerificationView.as_view(), name="establishment_verify"),
|
path('establishments/validate/', EstablishmentVerificationView.as_view(), name="establishment_verify"),
|
||||||
path('establishments/register/', EstablishmentRequestView.as_view(), name="establishment_register"),
|
path('establishments/register/', EstablishmentRequestView.as_view(), name="establishment_register"),
|
||||||
path('offers/', OffersListView.as_view(), name="offers"),
|
path('offers/', OffersListView.as_view(), name="offers"),
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .forms import VerificationForm
|
||||||
from auction.models import Inquiry, Offer
|
from auction.models import Inquiry, Offer
|
||||||
from public.mixins import InConstructionMixin
|
from public.mixins import InConstructionMixin
|
||||||
from localauth.mixins import LoginRequiredMixin, SuperUserRequiredMixin
|
from localauth.mixins import LoginRequiredMixin, SuperUserRequiredMixin
|
||||||
|
from gallery.models import Image
|
||||||
|
|
||||||
class PartnerRegistrationView(InConstructionMixin, LoginRequiredMixin, CreateView):
|
class PartnerRegistrationView(InConstructionMixin, LoginRequiredMixin, CreateView):
|
||||||
model = PartnerProfile
|
model = PartnerProfile
|
||||||
|
@ -153,3 +154,48 @@ class EstablishmentVerificationView(SuperUserRequiredMixin, FormView):
|
||||||
messages.success(self.request, "Unterkunft %s bestätigt!" % eobj[0].name)
|
messages.success(self.request, "Unterkunft %s bestätigt!" % eobj[0].name)
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse_lazy("partners:establishment_verify"))
|
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}/{name}"
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("partners:establishment_gallery", args=[self.establishment.id])
|
7
public/templatetags/splitter.py
Normal file
7
public/templatetags/splitter.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def splitter(l, c=3):
|
||||||
|
return [l[i:i+c] for i in range(0, len(l), c)]
|
120
templates/partners/establishment_gallery_manage.html
Normal file
120
templates/partners/establishment_gallery_manage.html
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{% extends "partners/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap4 %}
|
||||||
|
{% load splitter %}
|
||||||
|
{% block "dashboardcontent" %}
|
||||||
|
<div class="col-12 col-md-10 col-lg-10 dashboard-content booking-trips">
|
||||||
|
<h2 class="dash-content-title">Deine Bilder</h2>
|
||||||
|
<div class="dash-content-title"><small>für {{ establishment.name }}</small></div>
|
||||||
|
<button
|
||||||
|
class="position-relative btn-primary text-center"
|
||||||
|
type="button"
|
||||||
|
data-target="#create-image" data-toggle="modal"
|
||||||
|
>Bild hochladen</button>
|
||||||
|
|
||||||
|
<!-- Carousel wrapper -->
|
||||||
|
<div
|
||||||
|
id="carouselMultiItemExample"
|
||||||
|
class="carousel slide carousel-dark text-center"
|
||||||
|
data-ride="carousel"
|
||||||
|
>
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="d-flex justify-content-center mb-4">
|
||||||
|
<button
|
||||||
|
class="carousel-control-prev position-relative btn-secondary"
|
||||||
|
type="button"
|
||||||
|
data-target="#carouselMultiItemExample"
|
||||||
|
data-slide="prev"
|
||||||
|
>
|
||||||
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Zurück</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="carousel-control-next position-relative btn-secondary"
|
||||||
|
type="button"
|
||||||
|
data-target="#carouselMultiItemExample"
|
||||||
|
data-slide="next"
|
||||||
|
>
|
||||||
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Weiter</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Inner -->
|
||||||
|
<div class="carousel-inner py-4">
|
||||||
|
{% splitter object_list 3 as chunks %}
|
||||||
|
{% for chunk in chunks %}
|
||||||
|
<!-- Single item -->
|
||||||
|
<div class="carousel-item {% if forloop.first %}active{% endif %}">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
{% for image in chunk %}
|
||||||
|
<div class="col-lg-4 {% if forloop.first %}d-none d-lg-block{% endif %}">
|
||||||
|
<div class="card">
|
||||||
|
<img src="{{ image.url }}" class="card-img-top" alt="{{ image.title }}" />
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ image.title }}</h5>
|
||||||
|
<p class="card-text">{{ image.comment }}</p>
|
||||||
|
<a href="#!" class="btn btn-primary">Löschen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Inner -->
|
||||||
|
</div>
|
||||||
|
<!-- Carousel wrapper -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block "modal" %}
|
||||||
|
<div id="create-image" class="modal custom-modal fade" role="dialog">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title">Bild hochladen</h3>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
</div><!-- end modal-header -->
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form enctype="multipart/form-data" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
<button class="btn btn-orange btn-block">Hochladen...</button>
|
||||||
|
</form>
|
||||||
|
</div><!-- end modal-bpdy -->
|
||||||
|
</div><!-- end modal-content -->
|
||||||
|
</div><!-- end modal-dialog -->
|
||||||
|
</div><!-- end edit-profile -->
|
||||||
|
|
||||||
|
<!-- Modal 1 -->
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="exampleModal1"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="exampleModal1Label"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="ratio ratio-16x9">
|
||||||
|
<iframe
|
||||||
|
src="https://www.youtube.com/embed/A3PDXmYoF5U"
|
||||||
|
title="YouTube video"
|
||||||
|
allowfullscreen width="100%" height="100%"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center py-3">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -134,3 +134,5 @@ STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage' if ENABLE_S3_S
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
PHONENUMBER_DEFAULT_REGION = JOKER_COUNTRIES[0]
|
PHONENUMBER_DEFAULT_REGION = JOKER_COUNTRIES[0]
|
||||||
|
|
||||||
|
GALLERY_UPLOAD_PATH = "/userfiles/"
|
Loading…
Reference in a new issue