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.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 localauth.models import User, Profile, LocationMixin, ImageMixin, PhoneMixin
|
||||
from gallery.mixins import GalleryMixin
|
||||
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
|
@ -11,7 +12,7 @@ class PartnerProfile(Profile):
|
|||
def roomcategory_set(self):
|
||||
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)
|
||||
name = models.CharField("Name", max_length=64)
|
||||
stars = models.IntegerField("Sterne", null=True, blank=True)
|
||||
|
@ -40,7 +41,7 @@ class Establishment(LocationMixin, ImageMixin, PhoneMixin):
|
|||
|
||||
return querysets[0].union(*querysets[1:])
|
||||
|
||||
class RoomCategory(ImageMixin):
|
||||
class RoomCategory(GalleryMixin):
|
||||
establishment = models.ForeignKey(Establishment, models.CASCADE)
|
||||
name = models.CharField("Name", max_length=64)
|
||||
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.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"
|
||||
|
||||
|
@ -10,6 +10,7 @@ urlpatterns = [
|
|||
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/validate/', EstablishmentVerificationView.as_view(), name="establishment_verify"),
|
||||
path('establishments/register/', EstablishmentRequestView.as_view(), name="establishment_register"),
|
||||
path('offers/', OffersListView.as_view(), name="offers"),
|
||||
|
|
|
@ -11,6 +11,7 @@ from .forms import VerificationForm
|
|||
from auction.models import Inquiry, Offer
|
||||
from public.mixins import InConstructionMixin
|
||||
from localauth.mixins import LoginRequiredMixin, SuperUserRequiredMixin
|
||||
from gallery.models import Image
|
||||
|
||||
class PartnerRegistrationView(InConstructionMixin, LoginRequiredMixin, CreateView):
|
||||
model = PartnerProfile
|
||||
|
@ -152,4 +153,49 @@ class EstablishmentVerificationView(SuperUserRequiredMixin, FormView):
|
|||
|
||||
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 %}
|
|
@ -133,4 +133,6 @@ STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage' if ENABLE_S3_S
|
|||
|
||||
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