diff --git a/photo21/forms.py b/photo21/forms.py index 3ab1880..a4cb52b 100644 --- a/photo21/forms.py +++ b/photo21/forms.py @@ -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`.") diff --git a/photo21/locale/fr/LC_MESSAGES/django.po b/photo21/locale/fr/LC_MESSAGES/django.po index 6ea60a9..3f8956a 100644 --- a/photo21/locale/fr/LC_MESSAGES/django.po +++ b/photo21/locale/fr/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \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" diff --git a/photo21/settings.py b/photo21/settings.py index 08250a8..5128667 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -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 = '/' diff --git a/photo21/templates/account/email.html b/photo21/templates/account/email.html index d7c472f..9329cab 100644 --- a/photo21/templates/account/email.html +++ b/photo21/templates/account/email.html @@ -18,7 +18,7 @@ SPDX-License-Identifier: GPL-2.0-or-later -
+
{% if user.emailaddress_set.all %}

{% trans 'The following e-mail addresses are associated with your account:' %}

diff --git a/photo21/templates/base.html b/photo21/templates/base.html index 7d09a26..44af685 100644 --- a/photo21/templates/base.html +++ b/photo21/templates/base.html @@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-3.0-or-later + {% block extracss %}{% endblock %} @@ -34,10 +35,16 @@ SPDX-License-Identifier: GPL-3.0-or-later {% url 'photologue:pl-gallery-archive' as url %} {% trans 'Galleries' %} + {% if perms.photologue.add_gallery %} + + {% endif %} {% if request.user.is_staff %} - + {% endif %}
-
-
- {% for photo in photos %} - - {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %} - {{ photo.extended.owner.get_full_name }} - - {% endfor %} -
+
+ {% for photo in photos %} + + {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %} - {{ photo.extended.owner.get_full_name }} + + {% endfor %}
diff --git a/photologue_custom/templates/photologue/photo_detail.html b/photologue_custom/templates/photologue/photo_detail.html index af7ebe8..9aff5b9 100644 --- a/photologue_custom/templates/photologue/photo_detail.html +++ b/photologue_custom/templates/photologue/photo_detail.html @@ -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 %} diff --git a/photologue_custom/templates/photologue/upload.html b/photologue_custom/templates/photologue/upload.html new file mode 100644 index 0000000..fbe1ee0 --- /dev/null +++ b/photologue_custom/templates/photologue/upload.html @@ -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 %} + +{% endblock %} + +{% block extrajs %} + +{% endblock %} + +{% block content %} +

{% trans "Upload" %}

+
+
+ {% csrf_token %} +
+ {% trans "Drag and drop photos here" %} +
+ {{ form|crispy }} +

+ {% trans "Owner will be" %} {{ request.user.get_full_name }} ({{ request.user.username}}). +

+ + +
+
+{% endblock %} diff --git a/photologue_custom/urls.py b/photologue_custom/urls.py index 6ada0f6..c5fbc20 100644 --- a/photologue_custom/urls.py +++ b/photologue_custom/urls.py @@ -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//', TagDetail.as_view(), name='tag-detail'), @@ -11,4 +11,5 @@ urlpatterns = [ path('gallery//', CustomGalleryDetailView.as_view(), name='pl-gallery'), path('gallery///', CustomGalleryDetailView.as_view(), name='pl-gallery-owner'), path('gallery//download/', GalleryDownload.as_view(), name='gallery-download'), + path('upload/', GalleryUpload.as_view(), name='gallery-upload'), ] diff --git a/photologue_custom/views.py b/photologue_custom/views.py index 509f6b8..5355a28 100644 --- a/photologue_custom/views.py +++ b/photologue_custom/views.py @@ -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)