diff --git a/django_web_galleries/web_galleries/forms.py b/django_web_galleries/web_galleries/forms.py index dee0cfe..7821d31 100644 --- a/django_web_galleries/web_galleries/forms.py +++ b/django_web_galleries/web_galleries/forms.py @@ -1,7 +1,10 @@ from django import forms from django.utils.translation import gettext as _ -from .models import Image +from .models import ( + Image, + Gallery +) class ImageUploadForm(forms.ModelForm): class Meta: @@ -10,3 +13,21 @@ class ImageUploadForm(forms.ModelForm): labels = { 'private': 'Make this image private' } + +class GalleryCreatingForm(forms.ModelForm): + class Meta: + model = Gallery + fields = ['title', 'private'] + labels = { + 'private': 'Make this gallery private' + } + + images = forms.ImageField( + widget=forms.ClearableFileInput(attrs={'multiple': True}) + ) + +class AccessCodeForm(forms.Form): + access_code = forms.CharField(max_length=32) + +class VisitorNameForm(forms.Form): + name = forms.CharField(max_length=50) \ No newline at end of file diff --git a/django_web_galleries/web_galleries/models.py b/django_web_galleries/web_galleries/models.py index 78cedef..e6aed26 100644 --- a/django_web_galleries/web_galleries/models.py +++ b/django_web_galleries/web_galleries/models.py @@ -1,4 +1,5 @@ import uuid +import names from django.db import models from django.core.exceptions import ValidationError @@ -6,6 +7,8 @@ from django.utils.translation import gettext as _ def get_uuid(): return str(uuid.uuid4()) +def get_visitor_name(): + return names.get_first_name() class Visitor(models.Model): """ @@ -21,11 +24,14 @@ class Visitor(models.Model): name = models.CharField( _('Human readable, self assigned name of the visitor'), null=True, - default=None, + default=get_visitor_name, max_length=50, unique=True ) + def __str__(self): + return self.name + class Gallery(models.Model): """ @@ -45,6 +51,7 @@ class Gallery(models.Model): access_code = models.CharField( _('Code to access private galleries'), null=True, + blank=True, max_length=32 ) @@ -57,11 +64,15 @@ class Gallery(models.Model): visitors = models.ManyToManyField( Visitor, + blank=True, verbose_name=_('Visitors that a part of this gallery'), related_name='linked_galleries' ) - def clean(self): + def __str__(self): + return self.title + + def mettwurst(self): if self.private and access_code is None: raise ValidationErrorr('Private gallery needs an access code') if len(self.visitors.all()) > 10: @@ -97,6 +108,7 @@ class Image(models.Model): access_code = models.CharField( _('Code to access private images'), null=True, + blank=True, max_length=32 ) @@ -114,15 +126,24 @@ class Image(models.Model): galleries = models.ManyToManyField( Gallery, + blank=True, verbose_name=_('What galleries this image is in'), related_name='images' ) + visible_to = models.ManyToManyField( + Visitor, + blank=True, + verbose_name=_('Visitors that can see this picture if it is marked private'), + related_name='visible_images' + ) + def __str__(self): if self.title: return self.title elif self.description: return self.description[:100] else: - return super.__str__() + return super().__str__() + diff --git a/django_web_galleries/web_galleries/static/web-galleries.css b/django_web_galleries/web_galleries/static/web-galleries.css index 7381f51..c770a26 100644 --- a/django_web_galleries/web_galleries/static/web-galleries.css +++ b/django_web_galleries/web_galleries/static/web-galleries.css @@ -39,6 +39,10 @@ } .RV-Image__link { + display: flex; + flex-direction: row; + flex-wrap: wrap; + transition: transform 300ms ease-in-out; } @@ -46,6 +50,15 @@ transform: scale(1.1); } +.RV-Image__title { + flex-basis: 100%; + display: block; +} + +.RV-Image { + max-width: 100%; +} + .RV-Fieldset { margin: 50px 200px; display: flex; @@ -53,6 +66,14 @@ gap: 30px; } +.RV-Navigation { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; +} + .RV-Input { display: flex; flex-direction: column; diff --git a/django_web_galleries/web_galleries/templates/gallery/create_gallery.html b/django_web_galleries/web_galleries/templates/gallery/create_gallery.html new file mode 100644 index 0000000..2578528 --- /dev/null +++ b/django_web_galleries/web_galleries/templates/gallery/create_gallery.html @@ -0,0 +1,15 @@ +{% extends '../global.html' %} + +{% block content %} +
+ {% csrf_token %} +
+ {% include 'partials/form_input.html' with field=form.title %} + {% include 'partials/form_input.html' with field=form.private classes='RV-Input--compact RV-Input--reverse' %} + {% include 'partials/form_input.html' with field=form.images %} +
+
+ +
+
+{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/gallery/gallery_detail.html b/django_web_galleries/web_galleries/templates/gallery/gallery_detail.html new file mode 100644 index 0000000..ff4aea2 --- /dev/null +++ b/django_web_galleries/web_galleries/templates/gallery/gallery_detail.html @@ -0,0 +1,26 @@ +{% extends '../global.html' %} + +{% load responsive_images %} + +{% block content %} + {% if gallery.title %} +

{{gallery.title}}

+ {% endif %} +
+
  • + {% for image in gallery.images.all %} + + + + {% endfor %} +
  • + {% if gallery.private and gallery.visitors.all|length > 0 and gallery.created_by == visitor %} +
    +

    You have gained access to the following people

    + {% for visitor in gallery.visitors.all %} + {{visitor.name}} Revoke access + {% endfor %} +
    + {% endif %} +
    +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/gallery/gallery_no_access.html b/django_web_galleries/web_galleries/templates/gallery/gallery_no_access.html new file mode 100644 index 0000000..436fa3d --- /dev/null +++ b/django_web_galleries/web_galleries/templates/gallery/gallery_no_access.html @@ -0,0 +1,17 @@ +{% extends '../global.html' %} + +{% block content %} +
    + {% csrf_token %} +
    +

    You do not have access to this private gallery

    + But you can gain access by entering the access code below: +
    +
    + {% include 'partials/form_input.html' with field=form.access_code %} +
    +
    + +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/gallery/list_galleries.html b/django_web_galleries/web_galleries/templates/gallery/list_galleries.html new file mode 100644 index 0000000..4c86e1c --- /dev/null +++ b/django_web_galleries/web_galleries/templates/gallery/list_galleries.html @@ -0,0 +1,40 @@ +{% extends '../global.html' %} + +{% load responsive_images %} + +{% block content %} +
    +
  • + {% for gallery in galleries %} + + {% if gallery.images.all|length <= 0 %} + no images + {% endif %} + {% if gallery.images.all|length == 1 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length == 2 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length == 3 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length >= 4 %} + {% for image in gallery.images.all|slice:":4" %} + + {% endfor %} + {% endif %} + {% if gallery.title %} +

    {{gallery.title}}

    + {% endif %} +
    + {% endfor %} +
  • +
    +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/gallery/my_galleries.html b/django_web_galleries/web_galleries/templates/gallery/my_galleries.html new file mode 100644 index 0000000..b0e5c51 --- /dev/null +++ b/django_web_galleries/web_galleries/templates/gallery/my_galleries.html @@ -0,0 +1,80 @@ +{% extends '../global.html' %} + +{% load responsive_images %} + +{% block content %} +{% if created_galleries|length > 0 %} +
    +

    Your galleries

    +
  • + {% for gallery in created_galleries %} + + {% if gallery.images.all|length <= 0 %} + no images + {% endif %} + {% if gallery.images.all|length == 1 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length == 2 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length == 3 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length >= 4 %} + {% for image in gallery.images.all|slice:":4" %} + + {% endfor %} + {% endif %} + {% if gallery.title %} +

    {{gallery.title}}

    + {% endif %} +
    + {% endfor %} +
  • +
    +{% endif %} +{% if invited_galleries|length > 0 %} +
    +

    Galleries you were invited to

    +
  • + {% for gallery in invited_galleries %} + + {% if gallery.images.all|length <= 0 %} + no images + {% endif %} + {% if gallery.images.all|length == 1 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length == 2 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length == 3 %} + {% for image in gallery.images.all %} + + {% endfor %} + {% endif %} + {% if gallery.images.all|length >= 4 %} + {% for image in gallery.images.all|slice:":4" %} + + {% endfor %} + {% endif %} + {% if gallery.title %} +

    {{gallery.title}}

    + {% endif %} +
    + {% endfor %} +
  • +
    +{% endif %} +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/global.html b/django_web_galleries/web_galleries/templates/global.html index aa6fbb5..21c8b68 100644 --- a/django_web_galleries/web_galleries/templates/global.html +++ b/django_web_galleries/web_galleries/templates/global.html @@ -30,18 +30,34 @@
  • - + My galleries
  • - + Create gallery
  • +
    +
    + Hello there, {{ visitor.name }} +
    + +
    + + {% if request.META.HTTP_REFERER %} + back + {% endif %}
    {% block content %} {% endblock content %} diff --git a/django_web_galleries/web_galleries/templates/image/image_detail.html b/django_web_galleries/web_galleries/templates/image/image_detail.html new file mode 100644 index 0000000..5545b22 --- /dev/null +++ b/django_web_galleries/web_galleries/templates/image/image_detail.html @@ -0,0 +1,7 @@ +{% extends '../global.html' %} + +{% load responsive_images %} + +{% block content %} + +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/image/image_no_access.html b/django_web_galleries/web_galleries/templates/image/image_no_access.html new file mode 100644 index 0000000..dc699a2 --- /dev/null +++ b/django_web_galleries/web_galleries/templates/image/image_no_access.html @@ -0,0 +1,7 @@ +{% extends '../global.html' %} + +{% block content %} +

    You have no access to this private image

    + But you can gain access by entering the access code below: + +{% endlbock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/image/my_images.html b/django_web_galleries/web_galleries/templates/image/my_images.html new file mode 100644 index 0000000..96d9fe2 --- /dev/null +++ b/django_web_galleries/web_galleries/templates/image/my_images.html @@ -0,0 +1,30 @@ +{% extends '../global.html' %} + +{% load responsive_images %} + +{% block content %} + {% if uploaded_images|length > 0 %} +
    +

    Images you uploaded

    +
  • + {% for image in uploaded_images %} + + + + {% endfor %} +
  • +
    + {% endif %} + {% if shared_images|length > 0 %} +
    +

    Images shared with you

    +
  • + {% for image in shared_images %} + + + + {% endfor %} +
  • +
    + {% endif %} +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/list_images/image_list.html b/django_web_galleries/web_galleries/templates/list_images/image_list.html index 4227257..30b5224 100644 --- a/django_web_galleries/web_galleries/templates/list_images/image_list.html +++ b/django_web_galleries/web_galleries/templates/list_images/image_list.html @@ -3,13 +3,13 @@ {% load responsive_images %} {% block content %} - +
  • {% for image in images %} - + {% endfor %}
  • - +
    {% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/templates/visitor/visitor_settings.html b/django_web_galleries/web_galleries/templates/visitor/visitor_settings.html new file mode 100644 index 0000000..d83eecb --- /dev/null +++ b/django_web_galleries/web_galleries/templates/visitor/visitor_settings.html @@ -0,0 +1,13 @@ +{% extends '../global.html' %} + +{% block content %} +
    + {% csrf_token %} +
    + {% include 'partials/form_input.html' with field=name_form.name %} +
    +
    + +
    +
    +{% endblock content %} \ No newline at end of file diff --git a/django_web_galleries/web_galleries/urls.py b/django_web_galleries/web_galleries/urls.py index 5f4897c..1adf16b 100644 --- a/django_web_galleries/web_galleries/urls.py +++ b/django_web_galleries/web_galleries/urls.py @@ -3,11 +3,25 @@ from django.urls import path from .views import ( ImageUploadView, PublicImageListView, - MyImagesListView + MyImagesListView, + GalleryCreateView, + MyGalleriesListView, + GalleryDetailView, + ImageDetailView, + GainAccessToGalleryView, + VisitorSettingsView, + RevokeAccessToGallery ) urlpatterns = [ path('', PublicImageListView.as_view(), name='home'), path('upload/', ImageUploadView.as_view(), name='upload_image'), - path('my_images/', MyImagesListView.as_view(), name='my_images') + path('my_images/', MyImagesListView.as_view(), name='my_images'), + path('create_gallery/', GalleryCreateView.as_view(), name='create_gallery'), + path('my_galleries/', MyGalleriesListView.as_view(), name='my_galleries'), + path('gallery/', GalleryDetailView.as_view(), name='gallery'), + path('image/', ImageDetailView.as_view(), name='image'), + path('gallery/gain_access//', GainAccessToGalleryView.as_view(), name='gallery_gain_access'), + path('visitor/my_profile', VisitorSettingsView.as_view(), name='visitor_profile'), + path('gallery/revoke_access//', RevokeAccessToGallery.as_view(), name='gallery_revoke_access'), ] diff --git a/django_web_galleries/web_galleries/views.py b/django_web_galleries/web_galleries/views.py index f18792f..c641bc2 100644 --- a/django_web_galleries/web_galleries/views.py +++ b/django_web_galleries/web_galleries/views.py @@ -2,13 +2,25 @@ import secrets from django.views import View from django.views.generic.list import ListView -from django.shortcuts import render, redirect +from django.views.generic.detail import DetailView +from django.contrib.auth.mixins import UserPassesTestMixin from django.urls import reverse +from django.shortcuts import ( + render, + redirect, + get_object_or_404 +) -from .forms import ImageUploadForm +from .forms import ( + ImageUploadForm, + GalleryCreatingForm, + AccessCodeForm, + VisitorNameForm +) from .models import ( Image, - Visitor + Visitor, + Gallery ) class VisitorSessionMixin(View): @@ -24,6 +36,23 @@ class VisitorSessionMixin(View): else: return None + def can_access_image(self, image): + if image.private: + return self.get_visitor() == image.uploaded_by or self.get_visitor() in image.visible_to.all() + else: + return True + + def can_access_gallery(self, gallery): + if gallery.private: + return self.get_visitor() == gallery.created_by or self.get_visitor() in gallery.visitors.all() + else: + return True + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['visitor'] = self.get_visitor() + return context + class ImageUploadView(VisitorSessionMixin, View): def get(self, request): @@ -32,7 +61,8 @@ class ImageUploadView(VisitorSessionMixin, View): request, 'upload_image/upload.html', { - 'form': form + 'form': form, + 'visitor': self.get_visitor() } ) @@ -46,10 +76,9 @@ class ImageUploadView(VisitorSessionMixin, View): image = form.save(commit=False) if image.private: - image.access_code = secrets.token_hex(32) + image.access_code = secrets.token_hex(16) image.uploaded_by = self.get_visitor() - print(image.uploaded_by) image.save() return redirect( reverse('home') @@ -60,7 +89,8 @@ class ImageUploadView(VisitorSessionMixin, View): request, 'upload_image/upload.html', { - 'form': form + 'form': form, + 'visitor': self.get_visitor() } ) @@ -71,14 +101,253 @@ class PublicImageListView(ListView): template_name = 'list_images/image_list.html' def get_queryset(self): - return Image.objects.all().filter(private=False) + return Image.objects.all().filter(private=False).order_by('-uploaded_when')[:10] -class MyImagesListView(VisitorSessionMixin, ListView): +class MyImagesListView(VisitorSessionMixin, View): + def get(self, request): + visitor = self.get_visitor() + uploaded_images = Image.objects.all().filter( + uploaded_by=self.get_visitor() + ) + shared_images = Image.objects.all().filter( + visible_to__in=(visitor, ) + ) + + return render( + request, + 'image/my_images.html', + { + 'uploaded_images': uploaded_images, + 'shared_images': shared_images, + 'visitor': self.get_visitor() + } + ) + + +class GalleryCreateView(VisitorSessionMixin, View): + + def get(self, request): + form = GalleryCreatingForm() + return render( + request, + 'gallery/create_gallery.html', + { + 'form': form, + 'visitor': self.get_visitor() + } + ) + + def post(self, request): + form = GalleryCreatingForm( + request.POST, + request.FILES + ) + + if form.is_valid(): + gallery = form.save(commit=False) + + if gallery.private: + gallery.access_code = secrets.token_hex(16) + + gallery.created_by = self.get_visitor() + + gallery.save() + for image in request.FILES.getlist('images'): + imageObject = Image.objects.create( + image_file=image, + uploaded_by=gallery.created_by, + private=gallery.private, + access_code=gallery.access_code + + ) + imageObject.galleries.add(gallery) + imageObject.save() + + gallery.save() + return redirect( + reverse('home') + ) + else: + form = ImageUploadForm() + return render( + request, + 'upload_image/upload.html', + { + 'form': form, + 'visitor': self.get_visitor() + } + ) + +class MyGalleriesListView(VisitorSessionMixin, View): + + def get(self, request): + visitor = self.get_visitor() + created_galleries = Gallery.objects.all().filter( + created_by=self.get_visitor() + ) + invited_galleries = Gallery.objects.all().filter( + visitors__in=(visitor, ) + ) + + return render( + request, + 'gallery/my_galleries.html', + { + 'created_galleries': created_galleries, + 'invited_galleries': invited_galleries, + 'visitor': self.get_visitor() + } + ) + +class GalleryDetailView(VisitorSessionMixin, UserPassesTestMixin, DetailView): + model = Gallery + template_name = 'gallery/gallery_detail.html' + context_object_name = 'gallery' + + def test_func(self): + return self.can_access_gallery( + self.get_object() + ) + + def handle_no_permission(self): + return redirect( + reverse( + 'gallery_gain_access', + kwargs={ + 'gallery_id': self.get_object().id + } + ) + ) + +class ImageDetailView(VisitorSessionMixin, UserPassesTestMixin, DetailView): model = Image - paginated_by = 20 - context_object_name = 'images' - template_name = 'list_images/image_list.html' + template_name = 'image/image_detail.html' + context_object_name = 'image' - def get_queryset(self): - return Image.objects.all().filter(uploaded_by=self.get_visitor()) - \ No newline at end of file + def test_func(self): + return self.can_access_image( + self.get_object() + ) + + def handle_no_permission(self): + return redirect( + reverse('home') + ) + +class GainAccessToGalleryView(VisitorSessionMixin, View): + def _redirect_to_gallery(self, gallery): + return redirect( + reverse( + 'gallery', + kwargs={ + 'pk': gallery.id + } + ) + ) + + def get(self, request, gallery_id): + gallery = get_object_or_404( + Gallery, + id=gallery_id + ) + if self.can_access_gallery(gallery): + return self._redirect_to_gallery(gallery) + else: + return render( + self.request, + 'gallery/gallery_no_access.html', + { + 'form': AccessCodeForm(), + 'visitor': self.get_visitor() + } + ) + + def post(self, request, gallery_id): + gallery = get_object_or_404( + Gallery, + id=gallery_id + ) + if self.can_access_gallery(gallery): + return self._redirect_to_gallery(gallery) + else: + form = AccessCodeForm( + request.POST + ) + if form.is_valid(): + access_code = form.cleaned_data['access_code'] + if access_code == gallery.access_code: + gallery.visitors.add(self.get_visitor()) + gallery.save() + self.get_visitor().save() + return self._redirect_to_gallery(gallery) + + return render( + self.request, + 'gallery/gallery_no_access.html', + { + 'form': AccessCodeForm(), + 'visitor': self.get_visitor() + } + ) + +class RevokeAccessToGallery(VisitorSessionMixin, View): + def get(self, request, gallery_id, visitor_id): + gallery = get_object_or_404(Gallery, id=gallery_id) + visitor = self.get_visitor() + + if visitor == gallery.created_by: + visitor_to_remove = get_object_or_404(Visitor, id=visitor_id) + if visitor_to_remove in gallery.visitors.all(): + gallery.visitors.remove(visitor_to_remove) + gallery.access_code = secrets.token_hex(16) + gallery.save() + + return redirect( + reverse( + 'gallery', + kwargs={ + 'pk': gallery.id + } + ) + ) + +class VisitorSettingsView(VisitorSessionMixin, View): + def get(self, request): + name_form = VisitorNameForm( + initial={ + 'name': self.get_visitor().name + } + ) + + return render( + request, + 'visitor/visitor_settings.html', + { + 'name_form': name_form, + 'visitor': self.get_visitor() + } + ) + + def post(self, request): + name_form = VisitorNameForm( + request.POST + ) + + if name_form.is_valid(): + visitor = self.get_visitor() + visitor.name = name_form.cleaned_data['name'] + visitor.save() + + name_form = VisitorNameForm( + initial={ + 'name': self.get_visitor().name + } + ) + return render( + request, + 'visitor/visitor_settings.html', + { + 'name_form': name_form, + 'visitor': self.get_visitor() + } + )