List Images and Galleries, Access restrictions

This commit is contained in:
reverend 2022-12-27 22:09:10 +01:00
parent a54ad8954c
commit 651218c48b
16 changed files with 623 additions and 26 deletions

View File

@ -1,7 +1,10 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from .models import Image from .models import (
Image,
Gallery
)
class ImageUploadForm(forms.ModelForm): class ImageUploadForm(forms.ModelForm):
class Meta: class Meta:
@ -10,3 +13,21 @@ class ImageUploadForm(forms.ModelForm):
labels = { labels = {
'private': 'Make this image private' '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)

View File

@ -1,4 +1,5 @@
import uuid import uuid
import names
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -6,6 +7,8 @@ from django.utils.translation import gettext as _
def get_uuid(): def get_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
def get_visitor_name():
return names.get_first_name()
class Visitor(models.Model): class Visitor(models.Model):
""" """
@ -21,11 +24,14 @@ class Visitor(models.Model):
name = models.CharField( name = models.CharField(
_('Human readable, self assigned name of the visitor'), _('Human readable, self assigned name of the visitor'),
null=True, null=True,
default=None, default=get_visitor_name,
max_length=50, max_length=50,
unique=True unique=True
) )
def __str__(self):
return self.name
class Gallery(models.Model): class Gallery(models.Model):
""" """
@ -45,6 +51,7 @@ class Gallery(models.Model):
access_code = models.CharField( access_code = models.CharField(
_('Code to access private galleries'), _('Code to access private galleries'),
null=True, null=True,
blank=True,
max_length=32 max_length=32
) )
@ -57,11 +64,15 @@ class Gallery(models.Model):
visitors = models.ManyToManyField( visitors = models.ManyToManyField(
Visitor, Visitor,
blank=True,
verbose_name=_('Visitors that a part of this gallery'), verbose_name=_('Visitors that a part of this gallery'),
related_name='linked_galleries' related_name='linked_galleries'
) )
def clean(self): def __str__(self):
return self.title
def mettwurst(self):
if self.private and access_code is None: if self.private and access_code is None:
raise ValidationErrorr('Private gallery needs an access code') raise ValidationErrorr('Private gallery needs an access code')
if len(self.visitors.all()) > 10: if len(self.visitors.all()) > 10:
@ -97,6 +108,7 @@ class Image(models.Model):
access_code = models.CharField( access_code = models.CharField(
_('Code to access private images'), _('Code to access private images'),
null=True, null=True,
blank=True,
max_length=32 max_length=32
) )
@ -114,15 +126,24 @@ class Image(models.Model):
galleries = models.ManyToManyField( galleries = models.ManyToManyField(
Gallery, Gallery,
blank=True,
verbose_name=_('What galleries this image is in'), verbose_name=_('What galleries this image is in'),
related_name='images' 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): def __str__(self):
if self.title: if self.title:
return self.title return self.title
elif self.description: elif self.description:
return self.description[:100] return self.description[:100]
else: else:
return super.__str__() return super().__str__()

View File

@ -39,6 +39,10 @@
} }
.RV-Image__link { .RV-Image__link {
display: flex;
flex-direction: row;
flex-wrap: wrap;
transition: transform 300ms ease-in-out; transition: transform 300ms ease-in-out;
} }
@ -46,6 +50,15 @@
transform: scale(1.1); transform: scale(1.1);
} }
.RV-Image__title {
flex-basis: 100%;
display: block;
}
.RV-Image {
max-width: 100%;
}
.RV-Fieldset { .RV-Fieldset {
margin: 50px 200px; margin: 50px 200px;
display: flex; display: flex;
@ -53,6 +66,14 @@
gap: 30px; gap: 30px;
} }
.RV-Navigation {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.RV-Input { .RV-Input {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -0,0 +1,15 @@
{% extends '../global.html' %}
{% block content %}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="RV-Fieldset">
{% 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 %}
</div>
<div class="RV-Fieldset">
<button type="submit">Create</button>
</div>
</form>
{% endblock content %}

View File

@ -0,0 +1,26 @@
{% extends '../global.html' %}
{% load responsive_images %}
{% block content %}
{% if gallery.title %}
<h1>{{gallery.title}}</h1>
{% endif %}
<section class="RV-Images">
<li class="RV-Images__list">
{% for image in gallery.images.all %}
<a href="{% url 'image' pk=image.id %}" class="RV-Image__link RV-Image__link--detail">
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
</a>
{% endfor %}
</li>
{% if gallery.private and gallery.visitors.all|length > 0 and gallery.created_by == visitor %}
<div>
<h2>You have gained access to the following people</h2>
{% for visitor in gallery.visitors.all %}
{{visitor.name}} <a href="{% url 'gallery_revoke_access' gallery_id=gallery.id visitor_id=visitor.id %}">Revoke access</a>
{% endfor %}
</div>
{% endif %}
</section>
{% endblock content %}

View File

@ -0,0 +1,17 @@
{% extends '../global.html' %}
{% block content %}
<form method="POST">
{% csrf_token %}
<div class="RV-Fieldset">
<h1>You do not have access to this private gallery</h1>
But you can gain access by entering the access code below:
</div>
<div class="RV-Fieldset">
{% include 'partials/form_input.html' with field=form.access_code %}
</div>
<div class="RV-Fieldset">
<button type="submit">Enter</button>
</div>
</form>
{% endblock content %}

View File

@ -0,0 +1,40 @@
{% extends '../global.html' %}
{% load responsive_images %}
{% block content %}
<section class="RV-Images">
<li class="RV-Images__list">
{% for gallery in galleries %}
<a href="{% url 'gallery' pk=gallery.pk %}" class="RV-Image__link RV-Image__link--detail">
{% if gallery.images.all|length <= 0 %}
no images
{% endif %}
{% if gallery.images.all|length == 1 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length == 2 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 100x200 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length == 3 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 100x100 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length >= 4 %}
{% for image in gallery.images.all|slice:":4" %}
<img class="RV-Image__source" src="{% src image.image_file 100x100 %}">
{% endfor %}
{% endif %}
{% if gallery.title %}
<p class="RV-Image__title">{{gallery.title}}</p>
{% endif %}
</a>
{% endfor %}
</li>
</section>
{% endblock content %}

View File

@ -0,0 +1,80 @@
{% extends '../global.html' %}
{% load responsive_images %}
{% block content %}
{% if created_galleries|length > 0 %}
<section class="RV-Images">
<h2>Your galleries</h2>
<li class="RV-Images__list">
{% for gallery in created_galleries %}
<a href="{% url 'gallery' pk=gallery.pk %}" class="RV-Image__link RV-Image__link--detail">
{% if gallery.images.all|length <= 0 %}
no images
{% endif %}
{% if gallery.images.all|length == 1 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length == 2 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 100x200 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length == 3 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 100x100 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length >= 4 %}
{% for image in gallery.images.all|slice:":4" %}
<img class="RV-Image__source" src="{% src image.image_file 100x100 %}">
{% endfor %}
{% endif %}
{% if gallery.title %}
<p class="RV-Image__title">{{gallery.title}}</p>
{% endif %}
</a>
{% endfor %}
</li>
</section>
{% endif %}
{% if invited_galleries|length > 0 %}
<section class="RV-Images">
<h2>Galleries you were invited to</h2>
<li class="RV-Images__list">
{% for gallery in invited_galleries %}
<a href="{% url 'gallery' pk=gallery.pk %}" class="RV-Image__link RV-Image__link--detail">
{% if gallery.images.all|length <= 0 %}
no images
{% endif %}
{% if gallery.images.all|length == 1 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length == 2 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 100x200 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length == 3 %}
{% for image in gallery.images.all %}
<img class="RV-Image__source" src="{% src image.image_file 100x100 %}">
{% endfor %}
{% endif %}
{% if gallery.images.all|length >= 4 %}
{% for image in gallery.images.all|slice:":4" %}
<img class="RV-Image__source" src="{% src image.image_file 100x100 %}">
{% endfor %}
{% endif %}
{% if gallery.title %}
<p class="RV-Image__title">{{gallery.title}}</p>
{% endif %}
</a>
{% endfor %}
</li>
</section>
{% endif %}
{% endblock content %}

View File

@ -30,18 +30,34 @@
</a> </a>
</li> </li>
<li class="RV-Navigation__item"> <li class="RV-Navigation__item">
<a href="#" class="RV-Navigation__link"> <a href="{% url 'my_galleries' %}" class="RV-Navigation__link">
My galleries My galleries
</a> </a>
</li> </li>
<li class="RV-Navigation__item"> <li class="RV-Navigation__item">
<a href="#" class="RV-Navigation__link"> <a href="{% url 'create_gallery' %}" class="RV-Navigation__link">
Create gallery Create gallery
</a> </a>
</li> </li>
</ul> </ul>
<div class="RV-UserInfo">
<div class="RV-UserInfo__name">
Hello there, {{ visitor.name }}
</div>
<ul class="RV-UserInfo__links">
<li class="RV-UserInfo__link">
<a href="{% url 'visitor_profile' %}">
My Profile
</a>
</li>
</ul>
</div>
</nav> </nav>
</header> </header>
{% if request.META.HTTP_REFERER %}
<a href="{{ request.META.HTTP_REFERER }}">back</a>
{% endif %}
<main class="RV-Content"> <main class="RV-Content">
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,7 @@
{% extends '../global.html' %}
{% load responsive_images %}
{% block content %}
<img class="RV-Image" src="{% src image.image_file 1920x1200 nocrop %}" />
{% endblock content %}

View File

@ -0,0 +1,7 @@
{% extends '../global.html' %}
{% block content %}
<h1>You have no access to this private image</h1>
But you can gain access by entering the access code below:
{% endlbock content %}

View File

@ -0,0 +1,30 @@
{% extends '../global.html' %}
{% load responsive_images %}
{% block content %}
{% if uploaded_images|length > 0 %}
<section class="RV-Images">
<h2>Images you uploaded</h2>
<li class="RV-Images__list">
{% for image in uploaded_images %}
<a href="{% url 'image' pk=image.id %}" class="RV-Image__link RV-Image__link--detail">
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
</a>
{% endfor %}
</li>
</section>
{% endif %}
{% if shared_images|length > 0 %}
<section class="RV-Images">
<h2>Images shared with you</h2>
<li class="RV-Images__list">
{% for image in shared_images %}
<a href="{% url 'image' pk=image.id %}" class="RV-Image__link RV-Image__link--detail">
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
</a>
{% endfor %}
</li>
</section>
{% endif %}
{% endblock content %}

View File

@ -3,13 +3,13 @@
{% load responsive_images %} {% load responsive_images %}
{% block content %} {% block content %}
<sectoin class="RV-Images"> <section class="RV-Images">
<li class="RV-Images__list"> <li class="RV-Images__list">
{% for image in images %} {% for image in images %}
<a href="#" class="RV-Image__link RV-Image__link--detail"> <a href="{% url 'image' pk=image.id %}" class="RV-Image__link RV-Image__link--detail">
<img class="RV-Image__source" src="{% src image.image_file 200x200 %}"> <img class="RV-Image__source" src="{% src image.image_file 200x200 %}">
</a> </a>
{% endfor %} {% endfor %}
</li> </li>
</sectoin> </section>
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,13 @@
{% extends '../global.html' %}
{% block content %}
<form method="POST">
{% csrf_token %}
<div class="RV-Fieldset">
{% include 'partials/form_input.html' with field=name_form.name %}
</div>
<div class="RV-Fieldset">
<button type="submit">Save</button>
</div>
</form>
{% endblock content %}

View File

@ -3,11 +3,25 @@ from django.urls import path
from .views import ( from .views import (
ImageUploadView, ImageUploadView,
PublicImageListView, PublicImageListView,
MyImagesListView MyImagesListView,
GalleryCreateView,
MyGalleriesListView,
GalleryDetailView,
ImageDetailView,
GainAccessToGalleryView,
VisitorSettingsView,
RevokeAccessToGallery
) )
urlpatterns = [ urlpatterns = [
path('', PublicImageListView.as_view(), name='home'), path('', PublicImageListView.as_view(), name='home'),
path('upload/', ImageUploadView.as_view(), name='upload_image'), 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/<int:pk>', GalleryDetailView.as_view(), name='gallery'),
path('image/<int:pk>', ImageDetailView.as_view(), name='image'),
path('gallery/gain_access/<int:gallery_id>/', GainAccessToGalleryView.as_view(), name='gallery_gain_access'),
path('visitor/my_profile', VisitorSettingsView.as_view(), name='visitor_profile'),
path('gallery/revoke_access/<int:gallery_id>/<int:visitor_id>', RevokeAccessToGallery.as_view(), name='gallery_revoke_access'),
] ]

View File

@ -2,13 +2,25 @@ import secrets
from django.views import View from django.views import View
from django.views.generic.list import ListView 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.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 ( from .models import (
Image, Image,
Visitor Visitor,
Gallery
) )
class VisitorSessionMixin(View): class VisitorSessionMixin(View):
@ -24,6 +36,23 @@ class VisitorSessionMixin(View):
else: else:
return None 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): class ImageUploadView(VisitorSessionMixin, View):
def get(self, request): def get(self, request):
@ -32,7 +61,8 @@ class ImageUploadView(VisitorSessionMixin, View):
request, request,
'upload_image/upload.html', '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) image = form.save(commit=False)
if image.private: if image.private:
image.access_code = secrets.token_hex(32) image.access_code = secrets.token_hex(16)
image.uploaded_by = self.get_visitor() image.uploaded_by = self.get_visitor()
print(image.uploaded_by)
image.save() image.save()
return redirect( return redirect(
reverse('home') reverse('home')
@ -60,7 +89,8 @@ class ImageUploadView(VisitorSessionMixin, View):
request, request,
'upload_image/upload.html', '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' template_name = 'list_images/image_list.html'
def get_queryset(self): 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 model = Image
paginated_by = 20 template_name = 'image/image_detail.html'
context_object_name = 'images' context_object_name = 'image'
template_name = 'list_images/image_list.html'
def get_queryset(self): def test_func(self):
return Image.objects.all().filter(uploaded_by=self.get_visitor()) 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()
}
)