Merge branch 'photograph_upload' into 'master'

Photograph interface

See merge request bde/photo21!11
This commit is contained in:
erdnaxe 2021-10-17 20:33:47 +02:00
commit 29e9dba141
17 changed files with 351 additions and 55 deletions

View file

@ -17,7 +17,7 @@ class CustomSignupForm(SignupForm):
"""
Check that the email address ends with a trusted domain.
"""
email = self.cleaned_data.get("email")
email = super().clean_email()
if not email.endswith("@crans.org") and not email.endswith("@ens-paris-saclay.fr"):
raise forms.ValidationError(
_("Must end with `@crans.org` or `@ens-paris-saclay.fr`.")

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-13 13:52+0000\n"
"POT-Creation-Date: 2021-10-15 12:15+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -42,19 +42,19 @@ msgstr ""
msgid "hash"
msgstr ""
#: photo21/settings.py:153
#: photo21/settings.py:163
msgid "German"
msgstr ""
#: photo21/settings.py:154
#: photo21/settings.py:164
msgid "English"
msgstr ""
#: photo21/settings.py:155
#: photo21/settings.py:165
msgid "Spanish"
msgstr ""
#: photo21/settings.py:156
#: photo21/settings.py:166
msgid "French"
msgstr ""
@ -108,7 +108,7 @@ msgstr ""
msgid "E-mail Addresses"
msgstr ""
#: photo21/templates/account/email.html:9 photo21/templates/base.html:51
#: photo21/templates/account/email.html:9 photo21/templates/base.html:58
#: photo21/templates/socialaccount/connections.html:9
msgid "Account"
msgstr "Compte"
@ -225,27 +225,34 @@ msgstr ""
msgid "The ENS Paris-Saclay pictures server."
msgstr ""
#: photo21/templates/base.html:35
#: photo21/templates/base.html:36
msgid "Galleries"
msgstr "Galeries"
#: photo21/templates/base.html:39
#: photo21/templates/base.html:41
#: photologue_custom/templates/photologue/upload.html:6
#: photologue_custom/templates/photologue/upload.html:54
#: photologue_custom/templates/photologue/upload.html:65
msgid "Upload"
msgstr "Téléversement"
#: photo21/templates/base.html:46
msgid "Manage"
msgstr "Gestion"
#: photo21/templates/base.html:60
#: photo21/templates/base.html:67
msgid "Log out"
msgstr ""
#: photo21/templates/base.html:70
#: photo21/templates/base.html:77
msgid "Log in"
msgstr ""
#: photo21/templates/base.html:79
#: photo21/templates/base.html:86
msgid "Sign up"
msgstr "Inscription"
#: photo21/templates/index.html:50
#: photo21/templates/index.html:49
msgid "Connected as"
msgstr "Connecté en tant que"
@ -268,6 +275,40 @@ msgstr ""
msgid "Add a 3rd Party Account"
msgstr ""
#: photologue_custom/admin.py:45 photologue_custom/models.py:51
msgid "owner"
msgstr "propriétaire"
#: photologue_custom/forms.py:22
msgid "Gallery"
msgstr "Galerie"
#: photologue_custom/forms.py:24
msgid ""
"Select a gallery to add these images to. Leave this empty to create a new "
"gallery from the supplied title."
msgstr ""
#: photologue_custom/forms.py:28
msgid "New gallery title"
msgstr "Titre de la nouvelle galerie"
#: photologue_custom/forms.py:33
msgid "New gallery event start date"
msgstr "Date de début de l'évènement de la nouvelle galerie"
#: photologue_custom/forms.py:38
msgid "New gallery event end date"
msgstr "Date de fin de l'évènement de la nouvelle galerie"
#: photologue_custom/forms.py:46
msgid "A gallery with that title already exists."
msgstr ""
#: photologue_custom/forms.py:55
msgid "Select an existing gallery, or enter a title for a new gallery."
msgstr ""
#: photologue_custom/models.py:23
msgid "start date"
msgstr "date de début"
@ -276,53 +317,57 @@ msgstr "date de début"
msgid "end date"
msgstr "date de fin"
#: photologue_custom/models.py:51
msgid "owner"
msgstr "propriétaire"
#: photologue_custom/templates/photologue/gallery_archive.html:4
#: photologue_custom/templates/photologue/gallery_archive.html:9
#: photologue_custom/templates/photologue/gallery_archive.html:7
#: photologue_custom/templates/photologue/gallery_archive.html:12
msgid "Latest photo galleries"
msgstr ""
#: photologue_custom/templates/photologue/gallery_archive.html:15
#: photologue_custom/templates/photologue/gallery_archive.html:18
msgid "Filter by year"
msgstr ""
#: photologue_custom/templates/photologue/gallery_archive.html:32
#: photologue_custom/templates/photologue/gallery_archive.html:35
msgid "No galleries were found"
msgstr ""
#: photologue_custom/templates/photologue/gallery_archive_year.html:4
#: photologue_custom/templates/photologue/gallery_archive_year.html:9
#: photologue_custom/templates/photologue/gallery_archive_year.html:7
#: photologue_custom/templates/photologue/gallery_archive_year.html:12
#, python-format
msgid "Galleries for %(show_year)s"
msgstr ""
#: photologue_custom/templates/photologue/gallery_archive_year.html:14
#: photologue_custom/templates/photologue/gallery_archive_year.html:17
msgid "View all galleries"
msgstr ""
#: photologue_custom/templates/photologue/gallery_archive_year.html:26
#: photologue_custom/templates/photologue/gallery_archive_year.html:29
msgid "No galleries were found."
msgstr ""
#: photologue_custom/templates/photologue/gallery_detail.html:35
#: photologue_custom/templates/photologue/gallery_detail.html:38
msgid "to"
msgstr "au"
#: photologue_custom/templates/photologue/gallery_detail.html:49
#: photologue_custom/templates/photologue/gallery_detail.html:54
msgid "All pictures"
msgstr "Toutes les photos"
#: photologue_custom/templates/photologue/gallery_detail.html:63
#: photologue_custom/templates/photologue/gallery_detail.html:75
msgid "Download all gallery"
msgstr "Télécharger toute la galerie"
#: photologue_custom/templates/photologue/photo_detail.html:10
#: photologue_custom/templates/photologue/photo_detail.html:13
msgid "Published"
msgstr ""
#: photologue_custom/templates/photologue/photo_detail.html:22
#: photologue_custom/templates/photologue/photo_detail.html:25
msgid "This photo is found in the following galleries"
msgstr ""
#: photologue_custom/templates/photologue/upload.html:59
msgid "Drag and drop photos here"
msgstr "Glissez et déposez les photos ici"
#: photologue_custom/templates/photologue/upload.html:63
msgid "Owner will be"
msgstr "Le propriétaire sera"

View file

@ -35,6 +35,16 @@ ALLOWED_HOSTS = [
"photos-dev.crans.org",
]
# Admins receive server errors, this is useful to be notified of potential bugs
ADMINS = [
('erdnaxe', 'a+photo21@crans.org'),
]
# Managers receive notifications about new photos upload
MANAGERS = [
('moderation', 'photos@crans.org'),
]
# Application definition
@ -193,6 +203,7 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', None)
# Mail will be sent from this address
SERVER_EMAIL = "photos@crans.org"
DEFAULT_FROM_EMAIL = f"Serveur photos <{SERVER_EMAIL}>"
EMAIL_SUBJECT_PREFIX = '[Serveur photos] '
# After login redirect user to transfer page
LOGIN_REDIRECT_URL = '/'

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</li>
</ul>
</div>
<div class="card-body">
<div class="card-body">
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>

View file

@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<link rel="stylesheet" href="{% static "bootstrap5/css/bootstrap.min.css" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "favicon-16x16.png" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "favicon-32x32.png" %}">
<meta name="theme-color" content="#212529">
{% block extracss %}{% endblock %}
</head>
<body class="bg-light">
@ -34,10 +35,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% url 'photologue:pl-gallery-archive' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">{% trans 'Galleries' %}</a>
</li>
{% if perms.photologue.add_gallery %}
<li class="nav-item">
{% url 'gallery-upload' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">{% trans 'Upload' %}</a>
</li>
{% endif %}
{% if request.user.is_staff %}
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">{% trans 'Manage' %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %}">{% trans 'Manage' %}</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">

View file

@ -46,9 +46,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<hr/>
{% if request.user.is_authenticated %}
{% trans "Connected as" %} <code>{{ request.user.username }}</code>.
{% endif %}
{% trans "Connected as" %} <code>{{ request.user.username }}</code>.
<form action="{% url 'set_language' %}" method="post" style="max-width: 10em;">
{% csrf_token %}
Changer la langue :

View file

@ -1,4 +1,5 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from photologue.admin import GalleryAdmin as GalleryAdminDefault
from photologue.admin import PhotoAdmin as PhotoAdminDefault
from photologue.models import Gallery, Photo, PhotoEffect, PhotoSize, Watermark
@ -32,6 +33,16 @@ class PhotoAdmin(PhotoAdminDefault):
model.
"""
inlines = [PhotoExtendedInline, ]
list_display = ('title', 'date_taken', 'date_added',
'is_public', 'view_count', 'admin_thumbnail', 'get_owner')
list_filter = ['date_added', 'is_public', 'extended__owner']
def get_owner(self, obj):
if not hasattr(obj, 'extended'):
return "No owner"
return obj.extended.owner.username
get_owner.admin_order_field = 'owner'
get_owner.short_description = _('owner')
admin.site.unregister(Gallery)

View file

@ -0,0 +1,73 @@
import datetime
from django import forms
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from photologue.models import Gallery
from .models import GalleryExtended
class UploadForm(forms.Form):
file_field = forms.FileField(
label="",
widget=forms.FileInput(attrs={
'accept': 'image/*',
'multiple': True,
'class': 'mb-3',
}),
)
gallery = forms.ModelChoiceField(
Gallery.objects.all(),
label=_('Gallery'),
required=False,
help_text=_('Select a gallery to add these images to. Leave this empty to '
'create a new gallery from the supplied title.')
)
new_gallery_title = forms.CharField(
label=_('New gallery title'),
max_length=250,
required=False,
)
date_start = forms.DateField(
label=_('New gallery event start date'),
initial=datetime.date.today,
required=False,
)
date_end = forms.DateField(
label=_('New gallery event end date'),
initial=datetime.date.today,
required=False,
)
def clean_new_gallery_title(self):
title = self.cleaned_data['new_gallery_title']
if title and Gallery.objects.filter(title=title).exists():
raise forms.ValidationError(_('A gallery with that title already exists.'))
return title
def clean(self):
cleaned_data = super().clean()
# Check that either an existing gallery is chosen, or new_gallery_title is filled
if not (bool(cleaned_data['gallery']) ^ bool(cleaned_data.get('new_gallery_title', None))):
raise forms.ValidationError(
_('Select an existing gallery, or enter a title for a new gallery.'))
return cleaned_data
def get_or_create_gallery(self):
"""
Get or create gallery
"""
gallery = self.cleaned_data['gallery']
if not gallery:
# Create new gallery
title = self.cleaned_data.get('new_gallery_title')
gallery = Gallery.objects.create(title=title, slug=slugify(title))
GalleryExtended.objects.create(
gallery=gallery,
date_start=self.cleaned_data['date_start'],
date_end=self.cleaned_data['date_end'],
)
return gallery

View file

@ -0,0 +1,3 @@
{% extends "admin/change_list.html" %}
{% load i18n %}
{# Hide upload as zip #}

View file

@ -1,4 +1,7 @@
{% extends "photologue/root.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block title %}{% trans "Latest photo galleries" %}{% endblock %}

View file

@ -1,4 +1,7 @@
{% extends "photologue/root.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block title %}{% blocktrans with show_year=year|date:"Y" %}Galleries for {{ show_year }}{% endblocktrans %}{% endblock %}

View file

@ -1,4 +1,7 @@
{% extends "photologue/root.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load static i18n %}
{% block title %}{{ gallery.title }}{% endblock %}
@ -61,14 +64,12 @@
{% endfor %}
</ul>
</div>
<div class="card-body">
<div id="lightgallery">
{% for photo in photos %}
<a href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url }}" data-download-url="{{ photo.image.url }}">
<img src="{{ photo.get_thumbnail_url }}" class="img-thumbnail" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %} - {{ photo.extended.owner.get_full_name }}">
</a>
{% endfor %}
</div>
<div class="card-body row" id="lightgallery">
{% for photo in photos %}
<a class="col-6 col-md-3 mb-2 text-center" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url }}" data-download-url="{{ photo.image.url }}">
<img src="{{ photo.get_thumbnail_url }}" class="img-thumbnail" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %} - {{ photo.extended.owner.get_full_name }}">
</a>
{% endfor %}
</div>
<div class="card-footer">
<a href="{% url 'gallery-download' gallery.slug %}" class="btn btn-secondary btn-sm">{% trans 'Download all gallery' %}</a>

View file

@ -7,7 +7,6 @@
<div class="card-body">
<h5 class="card-title">{{ gallery.title }}</h5>
{% if gallery.extended.date_start %}<p class="card-text text-muted small mb-0">{{ gallery.extended.date_start }}{% if gallery.extended.date_end and gallery.extended.date_end != gallery.extended.date_start %} - {{ gallery.extended.date_end }}{% endif %}</p>{% endif %}
{% if gallery.description %}<p class="card-text small mb-0">{{ gallery.description|safe }}</p>{% endif %}
<a href="{{ gallery.get_absolute_url }}" class="stretched-link"></a>
</div>
</div>

View file

@ -1,4 +1,7 @@
{% extends "photologue/root.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load photologue_tags i18n %}
{% block title %}{{ object.title }}{% endblock %}

View file

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block title %}{% trans "Upload" %}{% endblock %}
{% block extracss %}
<style>
.upload-drop-zone {
height: 10em;
line-height: 10em;
border-width: 2px;
color: #a3a3a3;
border-style: dashed;
border-color: #a3a3a3;
border-radius: 0.5em;
text-align: center;
margin-bottom: 0.5em;
}
.upload-drop-zone.drop {
color: #222;
border-color: #222;
background-color: rgba(163, 163, 163, 0.274);
}
</style>
{% endblock %}
{% block extrajs %}
<script>
const dropZone = document.getElementById('drop-zone');
const uploadInput = document.getElementById('id_file_field');
dropZone.ondrop = function(e) {
e.preventDefault();
this.className = 'upload-drop-zone';
console.log(e.dataTransfer.files)
uploadInput.files = e.dataTransfer.files;
}
dropZone.ondragover = function() {
this.className = 'upload-drop-zone drop';
return false;
}
dropZone.ondragleave = function() {
this.className = 'upload-drop-zone';
return false;
}
</script>
{% endblock %}
{% block content %}
<h1>{% trans "Upload" %}</h1>
<div class="card">
<div class="card-body">
<form method="post" enctype="multipart/form-data">{% csrf_token %}
<div class="upload-drop-zone" id="drop-zone">
{% trans "Drag and drop photos here" %}
</div>
{{ form|crispy }}
<p class="mt-3">
{% trans "Owner will be" %} <code>{{ request.user.get_full_name }} ({{ request.user.username}})</code>.
</p>
<button type="submit" class="btn btn-success">{% trans "Upload" %}</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -1,8 +1,8 @@
from django.urls import path, re_path
from .views import (CustomGalleryArchiveIndexView,
CustomGalleryYearArchiveView, CustomGalleryDetailView,
GalleryDownload, TagDetail)
from .views import (CustomGalleryArchiveIndexView, CustomGalleryDetailView,
CustomGalleryYearArchiveView, GalleryDownload,
GalleryUpload, TagDetail)
urlpatterns = [
path('tag/<slug:slug>/', TagDetail.as_view(), name='tag-detail'),
@ -11,4 +11,5 @@ urlpatterns = [
path('gallery/<slug:slug>/', CustomGalleryDetailView.as_view(), name='pl-gallery'),
path('gallery/<slug:slug>/<int:owner>/', CustomGalleryDetailView.as_view(), name='pl-gallery-owner'),
path('gallery/<slug:slug>/download/', GalleryDownload.as_view(), name='gallery-download'),
path('upload/', GalleryUpload.as_view(), name='gallery-upload'),
]

View file

@ -5,13 +5,24 @@ import os
import zipfile
from io import BytesIO
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.contrib.auth.mixins import (LoginRequiredMixin,
PermissionRequiredMixin)
from django.core.mail import mail_managers
from django.db import IntegrityError
from django.http import HttpResponse
from django.views.generic import DetailView
from photologue.models import Gallery
from django.urls import reverse_lazy
from django.utils.text import slugify
from django.views.generic.detail import DetailView
from django.views.generic.edit import FormView
from photologue.models import Gallery, Photo
from photologue.views import GalleryArchiveIndexView, GalleryYearArchiveView
from PIL import Image
from taggit.models import Tag
from .forms import UploadForm
from .models import PhotoExtended
class TagDetail(LoginRequiredMixin, DetailView):
model = Tag
@ -23,7 +34,8 @@ class TagDetail(LoginRequiredMixin, DetailView):
current_tag = self.get_object().slug
context = super().get_context_data(**kwargs)
context['galleries'] = Gallery.objects.on_site().is_public() \
.filter(extended__tags__slug=current_tag)
.filter(extended__tags__slug=current_tag) \
.order_by('-extended__date_start')
return context
@ -51,12 +63,14 @@ class CustomGalleryDetailView(DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['photos'] = self.object.public()
# Query with extended and owner to reduce database lag
context['photos'] = self.object.public().select_related('extended__owner')
# List owners
context['owners'] = []
for photo in context['photos']:
if photo.extended.owner not in context['owners']:
if hasattr(photo, 'extended') and photo.extended.owner not in context['owners']:
context['owners'].append(photo.extended.owner)
# Filter on owner
@ -86,3 +100,58 @@ class GalleryDownload(LoginRequiredMixin, DetailView):
response = HttpResponse(byte_data.getvalue(), content_type='application/x-zip-compressed')
response['Content-Disposition'] = f"attachment; filename={gallery.slug}.zip"
return response
class GalleryUpload(PermissionRequiredMixin, FormView):
"""
Form to upload new photos in a gallery
"""
form_class = UploadForm
template_name = "photologue/upload.html"
success_url = reverse_lazy("gallery-upload")
permission_required = 'photologue.add_gallery'
def form_valid(self, form):
# Upload photos
# We take files from the request to support multiple upload
files = self.request.FILES.getlist('file_field')
gallery = form.get_or_create_gallery()
failed_upload = 0
for photo_file in files:
# Check that we have a valid image
print(photo_file, type(photo_file))
try:
opened = Image.open(photo_file)
opened.verify()
except Exception:
# Pillow doesn't recognize it as an image, skip it
messages.error(self.request, f"{photo_file.name} was not recognized as an image")
failed_upload += 1
continue
title = f"{gallery.title} - {photo_file.name}"
try:
photo = Photo(title=title, slug=slugify(title))
photo.image.save(photo_file.name, photo_file)
photo.save()
photo.galleries.set([gallery])
PhotoExtended.objects.create(photo=photo, owner=self.request.user)
except IntegrityError:
messages.error(self.request, f"{photo_file.name} was not uploaded. Maybe the photo was already uploaded.")
failed_upload += 1
# Notify user then managers
if not failed_upload:
messages.success(self.request, "All photos has been successfully uploaded.")
else:
n_success = len(files) - failed_upload
messages.warning(self.request, f"Only {n_success} photos were successfully uploaded !")
gallery_title = form.cleaned_data['gallery'] or form.cleaned_data.get('new_gallery_title', '')
photos = ", ".join(f.name for f in files)
mail_managers(
subject="New photos upload",
message=f"{self.request.user.username} has uploaded in `{gallery_title}`: {photos}",
)
return super().form_valid(form)