From 5368a51a76bf07f838e5f03bbb381f7f814d605b Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 30 Jan 2022 08:20:03 +0100 Subject: [PATCH] Bundle trimed down alternative to photologue --- README.md | 5 +- .../{es_ES => es}/LC_MESSAGES/django.po | 0 photo21/locale/fr/LC_MESSAGES/django.po | 26 +- photo21/settings.py | 5 - photo21/views.py | 2 +- photologue/__init__.py | 0 photologue/admin.py | 24 + photologue/apps.py | 6 + photologue/locale/de/LC_MESSAGES/django.po | 431 ++++++++++++ photologue/locale/es/LC_MESSAGES/django.po | 427 ++++++++++++ photologue/locale/fr/LC_MESSAGES/django.po | 433 ++++++++++++ photologue/management/commands/plcache.py | 43 ++ .../management/commands/plcreatesize.py | 57 ++ photologue/management/commands/plflush.py | 34 + photologue/migrations/0001_initial.py | 158 +++++ photologue/migrations/0002_photosize_data.py | 43 ++ .../migrations/0003_auto_20140822_1716.py | 19 + .../migrations/0004_auto_20140915_1259.py | 35 + .../migrations/0005_auto_20141027_1552.py | 20 + .../migrations/0006_auto_20141028_2005.py | 21 + .../migrations/0007_auto_20150404_1737.py | 30 + .../migrations/0008_auto_20150509_1557.py | 22 + .../migrations/0009_auto_20160102_0904.py | 20 + .../migrations/0010_auto_20160105_1307.py | 35 + .../migrations/0011_auto_20190223_2138.py | 18 + .../migrations/0012_auto_20220129_2207.py | 39 ++ .../migrations/0013_alter_gallery_photos.py | 18 + photologue/migrations/__init__.py | 0 photologue/models.py | 659 ++++++++++++++++++ photologue/views.py | 24 + photologue_custom/admin.py | 9 +- photologue_custom/apps.py | 1 + .../templates/photologue/gallery_archive.html | 2 +- .../photologue/gallery_archive_year.html | 2 +- .../templates/photologue/gallery_detail.html | 2 +- .../templates/photologue/photo_detail.html | 2 +- photologue_custom/urls.py | 11 +- photologue_custom/views.py | 26 +- requirements.txt | 9 +- tox.ini | 4 +- 40 files changed, 2652 insertions(+), 70 deletions(-) rename photo21/locale/{es_ES => es}/LC_MESSAGES/django.po (100%) create mode 100644 photologue/__init__.py create mode 100644 photologue/admin.py create mode 100644 photologue/apps.py create mode 100644 photologue/locale/de/LC_MESSAGES/django.po create mode 100644 photologue/locale/es/LC_MESSAGES/django.po create mode 100644 photologue/locale/fr/LC_MESSAGES/django.po create mode 100644 photologue/management/commands/plcache.py create mode 100644 photologue/management/commands/plcreatesize.py create mode 100644 photologue/management/commands/plflush.py create mode 100644 photologue/migrations/0001_initial.py create mode 100644 photologue/migrations/0002_photosize_data.py create mode 100644 photologue/migrations/0003_auto_20140822_1716.py create mode 100644 photologue/migrations/0004_auto_20140915_1259.py create mode 100644 photologue/migrations/0005_auto_20141027_1552.py create mode 100644 photologue/migrations/0006_auto_20141028_2005.py create mode 100644 photologue/migrations/0007_auto_20150404_1737.py create mode 100644 photologue/migrations/0008_auto_20150509_1557.py create mode 100644 photologue/migrations/0009_auto_20160102_0904.py create mode 100644 photologue/migrations/0010_auto_20160105_1307.py create mode 100644 photologue/migrations/0011_auto_20190223_2138.py create mode 100644 photologue/migrations/0012_auto_20220129_2207.py create mode 100644 photologue/migrations/0013_alter_gallery_photos.py create mode 100644 photologue/migrations/__init__.py create mode 100644 photologue/models.py create mode 100644 photologue/views.py diff --git a/README.md b/README.md index 9384fee..52dccb3 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ Bien que cela permette de créer une instance sur toutes les distributions, Pour initialiser la base de données avec de quoi travailler. ```bash - (env)$ ./manage.py collectstatic --noinput (env)$ ./manage.py compilemessages (env)$ ./manage.py migrate (env)$ ./manage.py loaddata initial @@ -69,10 +68,10 @@ de la note sur un téléphone ! ## Installation d'une instance de production -**En production on souhaite absolument utiliser les modules Python packagées +**En production on souhaite utiliser les modules Python packagées dans le gestionnaire de paquet.** Cela permet de mettre à jour facilement les dépendances critiques telles que Django. L'installation d'une instance de -production néccessite **une installation de Debian Bullseye**. +production néccessite **une installation de Debian Bullseye ou plus récent**. 1. **Installation des dépendances APT.** On tire les dépendances le plus possible à partir des dépôts de Debian. diff --git a/photo21/locale/es_ES/LC_MESSAGES/django.po b/photo21/locale/es/LC_MESSAGES/django.po similarity index 100% rename from photo21/locale/es_ES/LC_MESSAGES/django.po rename to photo21/locale/es/LC_MESSAGES/django.po diff --git a/photo21/locale/fr/LC_MESSAGES/django.po b/photo21/locale/fr/LC_MESSAGES/django.po index db95ab9..ac7970b 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: 2022-01-29 20:41+0000\n" +"POT-Creation-Date: 2022-01-30 07:09+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:164 +#: photo21/settings.py:162 msgid "German" msgstr "" -#: photo21/settings.py:165 +#: photo21/settings.py:163 msgid "English" msgstr "" -#: photo21/settings.py:166 +#: photo21/settings.py:164 msgid "Spanish" msgstr "" -#: photo21/settings.py:167 +#: photo21/settings.py:165 msgid "French" msgstr "" @@ -108,7 +108,7 @@ msgstr "" msgid "E-mail Addresses" msgstr "" -#: photo21/templates/account/email.html:9 photo21/templates/base.html:58 +#: photo21/templates/account/email.html:9 photo21/templates/base.html:59 #: photo21/templates/socialaccount/connections.html:9 msgid "Account" msgstr "Compte" @@ -225,31 +225,31 @@ msgstr "" msgid "The ENS Paris-Saclay pictures server." msgstr "" -#: photo21/templates/base.html:36 +#: photo21/templates/base.html:37 msgid "Galleries" msgstr "Galeries" -#: photo21/templates/base.html:41 +#: photo21/templates/base.html:42 msgid "Upload" msgstr "Téléversement" -#: photo21/templates/base.html:46 +#: photo21/templates/base.html:47 msgid "Manage" msgstr "Gestion" -#: photo21/templates/base.html:67 +#: photo21/templates/base.html:68 msgid "Log out" msgstr "" -#: photo21/templates/base.html:77 +#: photo21/templates/base.html:78 msgid "Log in" msgstr "" -#: photo21/templates/base.html:86 +#: photo21/templates/base.html:87 msgid "Sign up" msgstr "Inscription" -#: photo21/templates/base.html:108 +#: photo21/templates/base.html:109 msgid "Source code" msgstr "Code source" diff --git a/photo21/settings.py b/photo21/settings.py index 33d4a47..5c61f42 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -64,7 +64,6 @@ INSTALLED_APPS = [ 'crispy_forms', 'photologue_custom', 'photologue', - 'sortedm2m', 'taggit', ] @@ -235,7 +234,3 @@ SOCIALACCOUNT_PROVIDERS = { # Use Bootstrap forms CRISPY_TEMPLATE_PACK = 'bootstrap4' - -# Photologue -PHOTOLOGUE_GALLERY_SAMPLE_SIZE = 1 -PHOTOLOGUE_DIR = '.' diff --git a/photo21/views.py b/photo21/views.py index 90f4b55..5429901 100644 --- a/photo21/views.py +++ b/photo21/views.py @@ -17,6 +17,6 @@ class MediaAccess(LoginRequiredMixin, View): class IndexView(LoginRequiredMixin, ListView): - queryset = Gallery.objects.on_site().is_public() + queryset = Gallery.objects.filter(is_public=True) paginate_by = 4 template_name = "index.html" diff --git a/photologue/__init__.py b/photologue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/photologue/admin.py b/photologue/admin.py new file mode 100644 index 0000000..0c04abf --- /dev/null +++ b/photologue/admin.py @@ -0,0 +1,24 @@ +from django.contrib import admin + +from .models import Gallery, Photo + + +class GalleryAdmin(admin.ModelAdmin): + list_display = ('title', 'date_added', 'photo_count', 'is_public') + list_filter = ['date_added', 'is_public'] + date_hierarchy = 'date_added' + prepopulated_fields = {'slug': ('title',)} + model = Gallery + autocomplete_fields = ['photos', ] + search_fields = ['title', ] + + +class PhotoAdmin(admin.ModelAdmin): + list_display = ('title', 'date_taken', 'date_added', + 'is_public', 'view_count', 'admin_thumbnail') + list_filter = ['date_added', 'is_public'] + search_fields = ['title', 'slug', 'caption'] + list_per_page = 10 + prepopulated_fields = {'slug': ('title',)} + readonly_fields = ('date_taken',) + model = Photo diff --git a/photologue/apps.py b/photologue/apps.py new file mode 100644 index 0000000..19a451f --- /dev/null +++ b/photologue/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PhotologueConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' + name = 'photologue' diff --git a/photologue/locale/de/LC_MESSAGES/django.po b/photologue/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..f9a29ed --- /dev/null +++ b/photologue/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,431 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Translators: +# FIRST AUTHOR , 2009 +# Jannis Vajen, 2012-2016 +# Martin Darmüntzel , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Photologue\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-30 07:09+0000\n" +"PO-Revision-Date: 2017-12-03 14:47+0000\n" +"Last-Translator: Richard Barran \n" +"Language-Team: German (http://www.transifex.com/richardbarran/django-" +"photologue/language/de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: photologue/models.py:86 +msgid "Very Low" +msgstr "Sehr niedrig" + +#: photologue/models.py:87 +msgid "Low" +msgstr "Niedrig" + +#: photologue/models.py:88 +msgid "Medium-Low" +msgstr "Mittel-niedrig" + +#: photologue/models.py:89 +msgid "Medium" +msgstr "Mittel" + +#: photologue/models.py:90 +msgid "Medium-High" +msgstr "Mittel-hoch" + +#: photologue/models.py:91 +msgid "High" +msgstr "Hoch" + +#: photologue/models.py:92 +msgid "Very High" +msgstr "Sehr hoch" + +#: photologue/models.py:97 +msgid "Top" +msgstr "Oben" + +#: photologue/models.py:98 +msgid "Right" +msgstr "Rechts" + +#: photologue/models.py:99 +msgid "Bottom" +msgstr "Unten" + +#: photologue/models.py:100 +msgid "Left" +msgstr "Links" + +#: photologue/models.py:101 +msgid "Center (Default)" +msgstr "Mitte (Standard)" + +#: photologue/models.py:105 +msgid "Flip left to right" +msgstr "Horizontal spiegeln" + +#: photologue/models.py:106 +msgid "Flip top to bottom" +msgstr "Vertikal spiegeln" + +#: photologue/models.py:107 +msgid "Rotate 90 degrees counter-clockwise" +msgstr "Um 90° nach links drehen" + +#: photologue/models.py:108 +msgid "Rotate 90 degrees clockwise" +msgstr "Um 90° nach rechts drehen" + +#: photologue/models.py:109 +msgid "Rotate 180 degrees" +msgstr "Um 180° drehen" + +#: photologue/models.py:119 +#, python-format +msgid "" +"Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO-" +">FILTER_THREE\". Image filters will be applied in order. The following " +"filters are available: %s." +msgstr "" +"Verkette mehrere Filter in der Art \"FILTER_EINS->FILTER_ZWEI->FILTER_DREI" +"\". Bildfilter werden nach der Reihe angewendet. Folgende Filter sind " +"verfügbar: %s." + +#: photologue/models.py:141 +msgid "date published" +msgstr "Veröffentlichungsdatum" + +#: photologue/models.py:143 photologue/models.py:474 +msgid "title" +msgstr "Titel" + +#: photologue/models.py:146 +msgid "title slug" +msgstr "Kurztitel" + +#: photologue/models.py:149 photologue/models.py:480 +msgid "A \"slug\" is a unique URL-friendly title for an object." +msgstr "" +"Ein Kurztitel (\"slug\") ist ein eindeutiger, URL-geeigneter Titel für ein " +"Objekt." + +#: photologue/models.py:150 +msgid "description" +msgstr "Beschreibung" + +#: photologue/models.py:152 photologue/models.py:485 +msgid "is public" +msgstr "ist öffentlich" + +#: photologue/models.py:154 +msgid "Public galleries will be displayed in the default views." +msgstr "Öffentliche Galerien werden in den Standard-Views angezeigt." + +#: photologue/models.py:158 photologue/models.py:495 +msgid "photos" +msgstr "Fotos" + +#: photologue/models.py:166 +msgid "gallery" +msgstr "Galerie" + +#: photologue/models.py:167 +msgid "galleries" +msgstr "Galerien" + +#: photologue/models.py:202 +msgid "count" +msgstr "Anzahl" + +#: photologue/models.py:210 +msgid "image" +msgstr "Bild" + +#: photologue/models.py:213 +msgid "date taken" +msgstr "Aufnahmedatum" + +#: photologue/models.py:216 +msgid "Date image was taken; is obtained from the image EXIF data." +msgstr "" +"Datum, an dem das Foto geschossen wurde; ausgelesen aus den EXIF-Daten." + +#: photologue/models.py:217 +msgid "view count" +msgstr "Anzahl an Aufrufen" + +#: photologue/models.py:220 +msgid "crop from" +msgstr "Beschneiden von" + +#: photologue/models.py:243 +msgid "An \"admin_thumbnail\" photo size has not been defined." +msgstr "Es ist keine Fotogröße \"admin_thumbnail\" definiert." + +#: photologue/models.py:250 +msgid "Thumbnail" +msgstr "Vorschaubild" + +#: photologue/models.py:477 +msgid "slug" +msgstr "Kurztitel" + +#: photologue/models.py:481 +msgid "caption" +msgstr "Bildunterschrift" + +#: photologue/models.py:483 +msgid "date added" +msgstr "Datum des Eintrags" + +#: photologue/models.py:487 +msgid "Public photographs will be displayed in the default views." +msgstr "Öffentliche Fotos werden in den Standard-Views angezeigt." + +#: photologue/models.py:494 +msgid "photo" +msgstr "Foto" + +#: photologue/models.py:556 +msgid "name" +msgstr "Name" + +#: photologue/models.py:560 +msgid "" +"Photo size name should contain only letters, numbers and underscores. " +"Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"." +msgstr "" +"Der Name der Fotogröße darf nur Buchstaben, Zahlen und Unterstriche " +"enthalten. Beispiele: \"thumbnail\", \"display\", \"small\", " +"\"main_page_widget\"." + +#: photologue/models.py:567 +msgid "width" +msgstr "Breite" + +#: photologue/models.py:570 +msgid "" +"If width is set to \"0\" the image will be scaled to the supplied height." +msgstr "" +"Wenn die Breite auf \"0\" gesetzt ist, wird das Bild proportional auf die " +"angebene Höhe skaliert." + +#: photologue/models.py:571 +msgid "height" +msgstr "Höhe" + +#: photologue/models.py:574 +msgid "" +"If height is set to \"0\" the image will be scaled to the supplied width" +msgstr "" +"Wenn die Höhe auf \"0\" gesetzt ist, wird das Bild proportional auf die " +"angebene Breite skaliert." + +#: photologue/models.py:575 +msgid "quality" +msgstr "Qualität" + +#: photologue/models.py:578 +msgid "JPEG image quality." +msgstr "JPEG-Bildqualität" + +#: photologue/models.py:579 +msgid "upscale images?" +msgstr "Bilder hochskalieren?" + +#: photologue/models.py:581 +msgid "" +"If selected the image will be scaled up if necessary to fit the supplied " +"dimensions. Cropped sizes will be upscaled regardless of this setting." +msgstr "" +"Soll das Bild hochskaliert werden, um das angegebene Format zu erreichen? " +"Beschnittene Größen werden unabhängig von dieser Einstellung bei Bedarf " +"hochskaliert." + +#: photologue/models.py:585 +msgid "crop to fit?" +msgstr "Zuschneiden?" + +#: photologue/models.py:587 +msgid "" +"If selected the image will be scaled and cropped to fit the supplied " +"dimensions." +msgstr "" +"Soll das Bild auf das angegebene Format skaliert und beschnitten werden?" + +#: photologue/models.py:589 +msgid "pre-cache?" +msgstr "Vorausspeichern?" + +#: photologue/models.py:591 +msgid "If selected this photo size will be pre-cached as photos are added." +msgstr "" +"Soll diese Bildgröße im Voraus gespeichert (pre-cached) werden, wenn Fotos " +"hinzugefügt werden?" + +#: photologue/models.py:592 +msgid "increment view count?" +msgstr "Bildzähler?" + +#: photologue/models.py:594 +msgid "" +"If selected the image's \"view_count\" will be incremented when this photo " +"size is displayed." +msgstr "" +"Soll der Ansichts-Zähler (view-count) hochgezählt werden, wenn ein Foto " +"dieser Größe angezeigt wird?" + +#: photologue/models.py:599 +msgid "photo size" +msgstr "Foto-Größe" + +#: photologue/models.py:600 +msgid "photo sizes" +msgstr "Foto-Größen" + +#: photologue/models.py:617 +msgid "Can only crop photos if both width and height dimensions are set." +msgstr "" +"Fotos können nur zugeschnitten werden, wenn Breite und Höhe angegeben sind." + +#: photologue_custom/admin.py:43 photologue_custom/models.py:51 +msgid "owner" +msgstr "" + +#: photologue_custom/forms.py:34 +msgid "Gallery" +msgstr "Galerie" + +#: photologue_custom/forms.py:36 +msgid "-- Create a new gallery --" +msgstr "" + +#: photologue_custom/forms.py:37 +msgid "" +"Select a gallery to add these images to. Leave this empty to create a new " +"gallery from the supplied title." +msgstr "" +"Wähle eine Galerie aus, zu der diese Bilder hinzugefügt werden sollen. Lasse " +"dieses Feld leer, um eine neue Galerie mit dem angegeben Titel zu erzeugen." + +#: photologue_custom/forms.py:41 +#, fuzzy +#| msgid "View all galleries" +msgid "New gallery title" +msgstr "Zeige alle Galerien." + +#: photologue_custom/forms.py:46 +msgid "New gallery event start date" +msgstr "" + +#: photologue_custom/forms.py:51 +msgid "New gallery event end date" +msgstr "" + +#: photologue_custom/forms.py:57 +#, fuzzy +#| msgid "gallery" +msgid "New gallery tags" +msgstr "Galerie" + +#: photologue_custom/forms.py:59 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "" + +#: photologue_custom/forms.py:76 +#: photologue_custom/templates/photologue/upload.html:6 +#: photologue_custom/templates/photologue/upload.html:73 +msgid "Upload" +msgstr "Hochladen" + +#: photologue_custom/forms.py:82 +msgid "A gallery with that title already exists." +msgstr "Es existiert bereits eine Gallerie mit diesem Titel." + +#: photologue_custom/forms.py:91 +msgid "Select an existing gallery, or enter a title for a new gallery." +msgstr "" +"Wähle eine existierende Galerie aus oder gib einen Titel für eine neue " +"Galerie ein." + +#: photologue_custom/models.py:23 +msgid "start date" +msgstr "" + +#: photologue_custom/models.py:28 +msgid "end date" +msgstr "" + +#: photologue_custom/models.py:56 +msgid "license" +msgstr "" + +#: photologue_custom/templates/photologue/gallery_archive.html:7 +#: photologue_custom/templates/photologue/gallery_archive.html:12 +msgid "Latest photo galleries" +msgstr "Aktuelle Fotogalerien" + +#: photologue_custom/templates/photologue/gallery_archive.html:18 +msgid "Filter by year" +msgstr "Filtere nach Jahr" + +#: photologue_custom/templates/photologue/gallery_archive.html:35 +msgid "No galleries were found" +msgstr "Es wurden keine Galerien gefunden." + +#: 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 "Gallerien von %(show_year)s" + +#: photologue_custom/templates/photologue/gallery_archive_year.html:17 +msgid "View all galleries" +msgstr "Zeige alle Galerien." + +#: photologue_custom/templates/photologue/gallery_archive_year.html:29 +msgid "No galleries were found." +msgstr "Es wurden keine Galerien gefunden." + +#: photologue_custom/templates/photologue/gallery_detail.html:41 +msgid "to" +msgstr "" + +#: photologue_custom/templates/photologue/gallery_detail.html:57 +#, fuzzy +#| msgid "All photos" +msgid "All pictures" +msgstr "Alle Fotos" + +#: photologue_custom/templates/photologue/gallery_detail.html:78 +#, fuzzy +#| msgid "View all galleries" +msgid "Download all gallery" +msgstr "Zeige alle Galerien." + +#: photologue_custom/templates/photologue/photo_detail.html:13 +msgid "Published" +msgstr "Veröffentlicht" + +#: photologue_custom/templates/photologue/photo_detail.html:25 +msgid "This photo is found in the following galleries" +msgstr "Dieses Foto befindet sich in folgenden Galerien" + +#: photologue_custom/templates/photologue/upload.html:78 +msgid "Drag and drop photos here" +msgstr "" + +#: photologue_custom/templates/photologue/upload.html:82 +msgid "Owner will be" +msgstr "" diff --git a/photologue/locale/es/LC_MESSAGES/django.po b/photologue/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..8a9ca71 --- /dev/null +++ b/photologue/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,427 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Translators: +# dmalisani , 2014 +# dmalisani , 2014 +# Rafa Muñoz Cárdenas , 2009 +# salvador ortiz , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Photologue\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-30 07:09+0000\n" +"PO-Revision-Date: 2017-12-03 14:46+0000\n" +"Last-Translator: Richard Barran \n" +"Language-Team: Spanish (Spain) (http://www.transifex.com/richardbarran/" +"django-photologue/language/es_ES/)\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: photologue/models.py:86 +msgid "Very Low" +msgstr "Muy bajo" + +#: photologue/models.py:87 +msgid "Low" +msgstr "Bajo" + +#: photologue/models.py:88 +msgid "Medium-Low" +msgstr "Medio-bajo" + +#: photologue/models.py:89 +msgid "Medium" +msgstr "Medio" + +#: photologue/models.py:90 +msgid "Medium-High" +msgstr "Medio-alto" + +#: photologue/models.py:91 +msgid "High" +msgstr "Alto" + +#: photologue/models.py:92 +msgid "Very High" +msgstr "Muy alto" + +#: photologue/models.py:97 +msgid "Top" +msgstr "Arriba" + +#: photologue/models.py:98 +msgid "Right" +msgstr "Derecha" + +#: photologue/models.py:99 +msgid "Bottom" +msgstr "Abajo" + +#: photologue/models.py:100 +msgid "Left" +msgstr "Izquierda" + +#: photologue/models.py:101 +msgid "Center (Default)" +msgstr "Centro (por defecto)" + +#: photologue/models.py:105 +msgid "Flip left to right" +msgstr "Voltear de izquerda a derecha" + +#: photologue/models.py:106 +msgid "Flip top to bottom" +msgstr "Voltear de arriba a abajo" + +#: photologue/models.py:107 +msgid "Rotate 90 degrees counter-clockwise" +msgstr "Rotar 90 grados en sentido horario" + +#: photologue/models.py:108 +msgid "Rotate 90 degrees clockwise" +msgstr "Rotar 90 grados en sentido antihorario" + +#: photologue/models.py:109 +msgid "Rotate 180 degrees" +msgstr "Rotar 180 grados" + +#: photologue/models.py:119 +#, python-format +msgid "" +"Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO-" +">FILTER_THREE\". Image filters will be applied in order. The following " +"filters are available: %s." +msgstr "" +"Encadene múltiples filtros usando el siguiente patrón \"FILTRO_UNO-" +">FILTRO_DOS->FILTRO_TRES\". Los filtros de imagen se aplicarán en orden. Los " +"siguientes filtros están disponibles: %s." + +#: photologue/models.py:141 +msgid "date published" +msgstr "fecha de publicación" + +#: photologue/models.py:143 photologue/models.py:474 +msgid "title" +msgstr "título" + +#: photologue/models.py:146 +msgid "title slug" +msgstr "título slug" + +#: photologue/models.py:149 photologue/models.py:480 +msgid "A \"slug\" is a unique URL-friendly title for an object." +msgstr "Un \"slug\" es un único título URL-amigable para un objeto." + +#: photologue/models.py:150 +msgid "description" +msgstr "descripción" + +#: photologue/models.py:152 photologue/models.py:485 +msgid "is public" +msgstr "es público" + +#: photologue/models.py:154 +msgid "Public galleries will be displayed in the default views." +msgstr "Las galerías públicas serán mostradas en las vistas por defecto." + +#: photologue/models.py:158 photologue/models.py:495 +msgid "photos" +msgstr "fotos" + +#: photologue/models.py:166 +msgid "gallery" +msgstr "galería" + +#: photologue/models.py:167 +msgid "galleries" +msgstr "galerías" + +#: photologue/models.py:202 +msgid "count" +msgstr "contar" + +#: photologue/models.py:210 +msgid "image" +msgstr "imagen" + +#: photologue/models.py:213 +msgid "date taken" +msgstr "fecha en la que se tomó" + +#: photologue/models.py:216 +msgid "Date image was taken; is obtained from the image EXIF data." +msgstr "La fecha de la imagen fue obtenida por información EXIF de la imagen." + +#: photologue/models.py:217 +msgid "view count" +msgstr "Contador de visitas" + +#: photologue/models.py:220 +msgid "crop from" +msgstr "Recortar desde" + +#: photologue/models.py:243 +msgid "An \"admin_thumbnail\" photo size has not been defined." +msgstr "El tamaño de foto de \"miniatura de admin\" no ha sido definido." + +#: photologue/models.py:250 +msgid "Thumbnail" +msgstr "Miniatura" + +#: photologue/models.py:477 +msgid "slug" +msgstr "slug" + +#: photologue/models.py:481 +msgid "caption" +msgstr "pie de foto" + +#: photologue/models.py:483 +msgid "date added" +msgstr "fecha añadida" + +#: photologue/models.py:487 +msgid "Public photographs will be displayed in the default views." +msgstr "Las fotos públicas serán mostradas en las vistas por defecto." + +#: photologue/models.py:494 +msgid "photo" +msgstr "foto" + +#: photologue/models.py:556 +msgid "name" +msgstr "nombre" + +#: photologue/models.py:560 +msgid "" +"Photo size name should contain only letters, numbers and underscores. " +"Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"." +msgstr "" +"El nombre del tamaño solo puede contener letras, números y subrayados. Por " +"ejemplo:\"miniaturas\", \"muestra\", \"muestra_principal\"." + +#: photologue/models.py:567 +msgid "width" +msgstr "anchura" + +#: photologue/models.py:570 +msgid "" +"If width is set to \"0\" the image will be scaled to the supplied height." +msgstr "" +"Si la anchura se establece a \"0\" la imagen será escalada hasta la altura " +"proporcionada" + +#: photologue/models.py:571 +msgid "height" +msgstr "altura" + +#: photologue/models.py:574 +msgid "" +"If height is set to \"0\" the image will be scaled to the supplied width" +msgstr "" +"Si la altura se establece a \"0\" la imagen será escalada hasta la anchura " +"proporcionada" + +#: photologue/models.py:575 +msgid "quality" +msgstr "calidad" + +#: photologue/models.py:578 +msgid "JPEG image quality." +msgstr "Calidad de imagen JPEG." + +#: photologue/models.py:579 +msgid "upscale images?" +msgstr "¿Aumentar imágenes?" + +#: photologue/models.py:581 +msgid "" +"If selected the image will be scaled up if necessary to fit the supplied " +"dimensions. Cropped sizes will be upscaled regardless of this setting." +msgstr "" +"Si se selecciona la imagen será aumentada si es necesario para ajustarse a " +"las dimensiones proporcionadas. Los tamaños recortados serán aumentados de " +"acuerdo a esta opción." + +#: photologue/models.py:585 +msgid "crop to fit?" +msgstr "¿Recortar hasta ajustar?" + +#: photologue/models.py:587 +msgid "" +"If selected the image will be scaled and cropped to fit the supplied " +"dimensions." +msgstr "" +"Si se selecciona la imagen será escalada y recortada para ajustarse a las " +"dimensiones proporcionadas." + +#: photologue/models.py:589 +msgid "pre-cache?" +msgstr "¿pre-cachear?" + +#: photologue/models.py:591 +msgid "If selected this photo size will be pre-cached as photos are added." +msgstr "" +"Si se selecciona, este tamaño de foto será pre-cacheado cuando se añadan " +"nuevas fotos." + +#: photologue/models.py:592 +msgid "increment view count?" +msgstr "¿incrementar contador de visualizaciones?" + +#: photologue/models.py:594 +msgid "" +"If selected the image's \"view_count\" will be incremented when this photo " +"size is displayed." +msgstr "" +"Si se selecciona el \"contador de visualizaciones\" se incrementará cuando " +"esta foto sea visualizada." + +#: photologue/models.py:599 +msgid "photo size" +msgstr "tamaño de foto" + +#: photologue/models.py:600 +msgid "photo sizes" +msgstr "tamaños de foto" + +#: photologue/models.py:617 +msgid "Can only crop photos if both width and height dimensions are set." +msgstr "Solo puede recortar las fotos si ancho y alto están establecidos." + +#: photologue_custom/admin.py:43 photologue_custom/models.py:51 +msgid "owner" +msgstr "" + +#: photologue_custom/forms.py:34 +msgid "Gallery" +msgstr "Galería" + +#: photologue_custom/forms.py:36 +msgid "-- Create a new gallery --" +msgstr "" + +#: photologue_custom/forms.py:37 +msgid "" +"Select a gallery to add these images to. Leave this empty to create a new " +"gallery from the supplied title." +msgstr "" +"Seleccione una galería para agregarle estas imágenes. Déjelo vacío para " +"crear una nueva galería a partir de este título." + +#: photologue_custom/forms.py:41 +#, fuzzy +#| msgid "View all galleries" +msgid "New gallery title" +msgstr "Ver todas las galerías" + +#: photologue_custom/forms.py:46 +msgid "New gallery event start date" +msgstr "" + +#: photologue_custom/forms.py:51 +msgid "New gallery event end date" +msgstr "" + +#: photologue_custom/forms.py:57 +#, fuzzy +#| msgid "gallery" +msgid "New gallery tags" +msgstr "galería" + +#: photologue_custom/forms.py:59 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "" + +#: photologue_custom/forms.py:76 +#: photologue_custom/templates/photologue/upload.html:6 +#: photologue_custom/templates/photologue/upload.html:73 +msgid "Upload" +msgstr "Subir" + +#: photologue_custom/forms.py:82 +msgid "A gallery with that title already exists." +msgstr "Ya existe una galería con ese título." + +#: photologue_custom/forms.py:91 +msgid "Select an existing gallery, or enter a title for a new gallery." +msgstr "" +"Seleccione una galería existente o ingrese un nuevo nombre para la galería." + +#: photologue_custom/models.py:23 +msgid "start date" +msgstr "" + +#: photologue_custom/models.py:28 +msgid "end date" +msgstr "" + +#: photologue_custom/models.py:56 +msgid "license" +msgstr "" + +#: photologue_custom/templates/photologue/gallery_archive.html:7 +#: photologue_custom/templates/photologue/gallery_archive.html:12 +msgid "Latest photo galleries" +msgstr "Fotos de galerías mas recientes" + +#: photologue_custom/templates/photologue/gallery_archive.html:18 +msgid "Filter by year" +msgstr "Filtrar por año" + +#: photologue_custom/templates/photologue/gallery_archive.html:35 +msgid "No galleries were found" +msgstr "No se encontraron galerías" + +#: 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 "Galerías por %(show_year)s" + +#: photologue_custom/templates/photologue/gallery_archive_year.html:17 +msgid "View all galleries" +msgstr "Ver todas las galerías" + +#: photologue_custom/templates/photologue/gallery_archive_year.html:29 +msgid "No galleries were found." +msgstr "No se encontraron galerías" + +#: photologue_custom/templates/photologue/gallery_detail.html:41 +msgid "to" +msgstr "" + +#: photologue_custom/templates/photologue/gallery_detail.html:57 +#, fuzzy +#| msgid "All photos" +msgid "All pictures" +msgstr "Todas las fotos" + +#: photologue_custom/templates/photologue/gallery_detail.html:78 +#, fuzzy +#| msgid "View all galleries" +msgid "Download all gallery" +msgstr "Ver todas las galerías" + +#: photologue_custom/templates/photologue/photo_detail.html:13 +msgid "Published" +msgstr "Publicado" + +#: photologue_custom/templates/photologue/photo_detail.html:25 +msgid "This photo is found in the following galleries" +msgstr "Esta foto se encontró en las siguientes galerías" + +#: photologue_custom/templates/photologue/upload.html:78 +msgid "Drag and drop photos here" +msgstr "" + +#: photologue_custom/templates/photologue/upload.html:82 +msgid "Owner will be" +msgstr "" diff --git a/photologue/locale/fr/LC_MESSAGES/django.po b/photologue/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..f3b69fc --- /dev/null +++ b/photologue/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,433 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Translators: +# Matthieu Payet , 2017 +# Théophane Hufschmitt , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Photologue\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-30 07:09+0000\n" +"PO-Revision-Date: 2017-12-03 14:47+0000\n" +"Last-Translator: Richard Barran \n" +"Language-Team: French (http://www.transifex.com/richardbarran/django-" +"photologue/language/fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: photologue/models.py:86 +msgid "Very Low" +msgstr "Très Bas" + +#: photologue/models.py:87 +msgid "Low" +msgstr "Bas" + +#: photologue/models.py:88 +msgid "Medium-Low" +msgstr "Moyen-Bas" + +#: photologue/models.py:89 +msgid "Medium" +msgstr "Moyen" + +#: photologue/models.py:90 +msgid "Medium-High" +msgstr "Moyen-Haut" + +#: photologue/models.py:91 +msgid "High" +msgstr "Haut" + +#: photologue/models.py:92 +msgid "Very High" +msgstr "Très Haut" + +#: photologue/models.py:97 +msgid "Top" +msgstr "Sommet" + +#: photologue/models.py:98 +msgid "Right" +msgstr "Droite" + +#: photologue/models.py:99 +msgid "Bottom" +msgstr "Bas" + +#: photologue/models.py:100 +msgid "Left" +msgstr "Gauche" + +#: photologue/models.py:101 +msgid "Center (Default)" +msgstr "Centré (par défaut)" + +#: photologue/models.py:105 +msgid "Flip left to right" +msgstr "Inversion de gauche à droite" + +#: photologue/models.py:106 +msgid "Flip top to bottom" +msgstr "Inversion de haut en bas" + +#: photologue/models.py:107 +msgid "Rotate 90 degrees counter-clockwise" +msgstr "Rotation de 90 degrés dans le sens anti-horloger" + +#: photologue/models.py:108 +msgid "Rotate 90 degrees clockwise" +msgstr "Rotation de 90 degrés dans le sens horloger" + +#: photologue/models.py:109 +msgid "Rotate 180 degrees" +msgstr "Rotation de 180 degrés" + +#: photologue/models.py:119 +#, python-format +msgid "" +"Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO-" +">FILTER_THREE\". Image filters will be applied in order. The following " +"filters are available: %s." +msgstr "" +"Faite suivre de multiple filtres en utilisant la forme suivante \"FILTRE_UN-" +">FILTRE_DEUX->FILTRE_TROIS\". Les filtres d'image seront appliqués dans " +"l'ordre. Les filtres suivants sont disponibles: %s." + +#: photologue/models.py:141 +msgid "date published" +msgstr "date de publication" + +#: photologue/models.py:143 photologue/models.py:474 +msgid "title" +msgstr "titre" + +#: photologue/models.py:146 +msgid "title slug" +msgstr "version abrégée du titre" + +#: photologue/models.py:149 photologue/models.py:480 +msgid "A \"slug\" is a unique URL-friendly title for an object." +msgstr "" +"Un \"slug\" est un titre abrégé et unique, compatible avec les URL, pour un " +"objet." + +#: photologue/models.py:150 +msgid "description" +msgstr "description" + +#: photologue/models.py:152 photologue/models.py:485 +msgid "is public" +msgstr "est public" + +#: photologue/models.py:154 +msgid "Public galleries will be displayed in the default views." +msgstr "Les galeries publiques seront affichée dans les vues par défaut." + +#: photologue/models.py:158 photologue/models.py:495 +msgid "photos" +msgstr "photos" + +#: photologue/models.py:166 +msgid "gallery" +msgstr "galerie" + +#: photologue/models.py:167 +msgid "galleries" +msgstr "galleries" + +#: photologue/models.py:202 +msgid "count" +msgstr "nombre" + +#: photologue/models.py:210 +msgid "image" +msgstr "image" + +#: photologue/models.py:213 +msgid "date taken" +msgstr "date de prise de vue" + +#: photologue/models.py:216 +msgid "Date image was taken; is obtained from the image EXIF data." +msgstr "" +"La date à laquelle l'image a été prise ; obtenue à partir des données EXIF " +"de l'image." + +#: photologue/models.py:217 +msgid "view count" +msgstr "nombre" + +#: photologue/models.py:220 +msgid "crop from" +msgstr "découper à partir de" + +#: photologue/models.py:243 +msgid "An \"admin_thumbnail\" photo size has not been defined." +msgstr "Une taille de photo \"admin_thumbnail\" n'a pas encore été définie." + +#: photologue/models.py:250 +msgid "Thumbnail" +msgstr "Miniature" + +#: photologue/models.py:477 +msgid "slug" +msgstr "libellé court" + +#: photologue/models.py:481 +msgid "caption" +msgstr "légende" + +#: photologue/models.py:483 +msgid "date added" +msgstr "date d'ajout" + +#: photologue/models.py:487 +msgid "Public photographs will be displayed in the default views." +msgstr "Les photographies publique seront affichées dans les vues par défaut." + +#: photologue/models.py:494 +msgid "photo" +msgstr "photo" + +#: photologue/models.py:556 +msgid "name" +msgstr "nom" + +#: photologue/models.py:560 +msgid "" +"Photo size name should contain only letters, numbers and underscores. " +"Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"." +msgstr "" +"Le nom de la taille de la photo ne doit contenir que des lettres, des " +"chiffres et des caractères de soulignement. Exemples: \"miniature\", " +"\"affichage\", \"petit\", \"widget_page_principale\"." + +#: photologue/models.py:567 +msgid "width" +msgstr "largeur" + +#: photologue/models.py:570 +msgid "" +"If width is set to \"0\" the image will be scaled to the supplied height." +msgstr "" +"Si la largeur est réglée à \"0\" l l'image sera redimensionnée par rapport à " +"la hauteur fournie." + +#: photologue/models.py:571 +msgid "height" +msgstr "hauteur" + +#: photologue/models.py:574 +msgid "" +"If height is set to \"0\" the image will be scaled to the supplied width" +msgstr "" +"Si la hauteur est réglée à \"0\" l l'image sera redimensionnée par rapport à " +"la largeur fournie." + +#: photologue/models.py:575 +msgid "quality" +msgstr "qualité" + +#: photologue/models.py:578 +msgid "JPEG image quality." +msgstr "Qualité JPEG de l'image." + +#: photologue/models.py:579 +msgid "upscale images?" +msgstr "agrandir les images ?" + +#: photologue/models.py:581 +msgid "" +"If selected the image will be scaled up if necessary to fit the supplied " +"dimensions. Cropped sizes will be upscaled regardless of this setting." +msgstr "" +"Si sélectionné l'image sera agrandie si nécessaire pour coïncider avec les " +"dimensions fournies. Les dimensions ajustées seront agrandies sans prendre " +"en compte ce paramètre." + +#: photologue/models.py:585 +msgid "crop to fit?" +msgstr "découper pour adapter à la taille ?" + +#: photologue/models.py:587 +msgid "" +"If selected the image will be scaled and cropped to fit the supplied " +"dimensions." +msgstr "" +"Si sélectionné l'image sera redimensionnée et recadrée pour coïncider avec " +"les dimensions fournies." + +#: photologue/models.py:589 +msgid "pre-cache?" +msgstr "mise en cache ?" + +#: photologue/models.py:591 +msgid "If selected this photo size will be pre-cached as photos are added." +msgstr "" +"Si sélectionné cette taille de photo sera mise en cache au moment au les " +"photos sont ajoutées." + +#: photologue/models.py:592 +msgid "increment view count?" +msgstr "incrémenter le nombre d'affichages ?" + +#: photologue/models.py:594 +msgid "" +"If selected the image's \"view_count\" will be incremented when this photo " +"size is displayed." +msgstr "" +"Si sélectionné le \"view_count\" (nombre d'affichage) de l'image sera " +"incrémenté quand cette taille de photo sera affichée." + +#: photologue/models.py:599 +msgid "photo size" +msgstr "taille de la photo" + +#: photologue/models.py:600 +msgid "photo sizes" +msgstr "tailles des photos" + +#: photologue/models.py:617 +msgid "Can only crop photos if both width and height dimensions are set." +msgstr "" +"La hauteur et la largeur doivent être toutes les deux définies pour " +"retailler des photos." + +#: photologue_custom/admin.py:43 photologue_custom/models.py:51 +msgid "owner" +msgstr "" + +#: photologue_custom/forms.py:34 +msgid "Gallery" +msgstr "Galerie" + +#: photologue_custom/forms.py:36 +msgid "-- Create a new gallery --" +msgstr "" + +#: photologue_custom/forms.py:37 +msgid "" +"Select a gallery to add these images to. Leave this empty to create a new " +"gallery from the supplied title." +msgstr "" +"Sélectionner une galerie à laquelle ajouter ces images. Laisser ce champ " +"vide pour créer une nouvelle galerie à partir du titre indiqué." + +#: photologue_custom/forms.py:41 +#, fuzzy +#| msgid "View all galleries" +msgid "New gallery title" +msgstr "Afficher toutes les galeries" + +#: photologue_custom/forms.py:46 +msgid "New gallery event start date" +msgstr "" + +#: photologue_custom/forms.py:51 +msgid "New gallery event end date" +msgstr "" + +#: photologue_custom/forms.py:57 +#, fuzzy +#| msgid "gallery uploads" +msgid "New gallery tags" +msgstr "gallery uploads" + +#: photologue_custom/forms.py:59 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "" + +#: photologue_custom/forms.py:76 +#: photologue_custom/templates/photologue/upload.html:6 +#: photologue_custom/templates/photologue/upload.html:73 +msgid "Upload" +msgstr "Télécharger" + +#: photologue_custom/forms.py:82 +msgid "A gallery with that title already exists." +msgstr "Une galerie portant ce nom existe déjà." + +#: photologue_custom/forms.py:91 +msgid "Select an existing gallery, or enter a title for a new gallery." +msgstr "" +"Sélectionner une galerie existante ou entrer un titre pour une nouvelle " +"galerie." + +#: photologue_custom/models.py:23 +msgid "start date" +msgstr "" + +#: photologue_custom/models.py:28 +msgid "end date" +msgstr "" + +#: photologue_custom/models.py:56 +msgid "license" +msgstr "" + +#: photologue_custom/templates/photologue/gallery_archive.html:7 +#: photologue_custom/templates/photologue/gallery_archive.html:12 +msgid "Latest photo galleries" +msgstr "Dernières galeries de photos" + +#: photologue_custom/templates/photologue/gallery_archive.html:18 +msgid "Filter by year" +msgstr "Filtrer par année" + +#: photologue_custom/templates/photologue/gallery_archive.html:35 +msgid "No galleries were found" +msgstr "Aucune galerie trouvée" + +#: 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 "Galeries de %(show_year)s" + +#: photologue_custom/templates/photologue/gallery_archive_year.html:17 +msgid "View all galleries" +msgstr "Afficher toutes les galeries" + +#: photologue_custom/templates/photologue/gallery_archive_year.html:29 +msgid "No galleries were found." +msgstr "Aucune galerie trouvée." + +#: photologue_custom/templates/photologue/gallery_detail.html:41 +msgid "to" +msgstr "" + +#: photologue_custom/templates/photologue/gallery_detail.html:57 +#, fuzzy +#| msgid "All photos" +msgid "All pictures" +msgstr "Toutes les photos" + +#: photologue_custom/templates/photologue/gallery_detail.html:78 +#, fuzzy +#| msgid "View all galleries" +msgid "Download all gallery" +msgstr "Afficher toutes les galeries" + +#: photologue_custom/templates/photologue/photo_detail.html:13 +msgid "Published" +msgstr "Publiée le" + +#: photologue_custom/templates/photologue/photo_detail.html:25 +msgid "This photo is found in the following galleries" +msgstr "Cette photo se trouve dans les galeries suivantes" + +#: photologue_custom/templates/photologue/upload.html:78 +msgid "Drag and drop photos here" +msgstr "" + +#: photologue_custom/templates/photologue/upload.html:82 +msgid "Owner will be" +msgstr "" diff --git a/photologue/management/commands/plcache.py b/photologue/management/commands/plcache.py new file mode 100644 index 0000000..4884957 --- /dev/null +++ b/photologue/management/commands/plcache.py @@ -0,0 +1,43 @@ +# Based on https://github.com/richardbarran/django-photologue/ +# by Richard Barran, BSD-3 licensed + +from django.core.management.base import BaseCommand, CommandError +from photologue.models import ImageModel, PhotoSize + + +class Command(BaseCommand): + + help = 'Manages Photologue cache file for the given sizes.' + + def add_arguments(self, parser): + parser.add_argument('sizes', + nargs='*', + type=str, + help='Name of the photosize.') + parser.add_argument('--reset', + action='store_true', + default=False, + dest='reset', + help='Reset photo cache before generating.') + + def handle(self, *args, **options): + reset = options['reset'] + sizes = options['sizes'] + + if not sizes: + photosizes = PhotoSize.objects.all() + else: + photosizes = PhotoSize.objects.filter(name__in=sizes) + + if not len(photosizes): + raise CommandError('No photo sizes were found.') + + print('Caching photos, this may take a while...') + + for cls in ImageModel.__subclasses__(): + for photosize in photosizes: + print('Cacheing %s size images' % photosize.name) + for obj in cls.objects.all(): + if reset: + obj.remove_size(photosize) + obj.create_size(photosize) diff --git a/photologue/management/commands/plcreatesize.py b/photologue/management/commands/plcreatesize.py new file mode 100644 index 0000000..48d4e31 --- /dev/null +++ b/photologue/management/commands/plcreatesize.py @@ -0,0 +1,57 @@ +# Based on https://github.com/richardbarran/django-photologue/ +# by Richard Barran, BSD-3 licensed + +from django.core.management.base import BaseCommand +from photologue.models import PhotoSize + + +class Command(BaseCommand): + help = ('Creates a new Photologue photo size interactively.') + requires_model_validation = True + can_import_settings = True + + def add_arguments(self, parser): + parser.add_argument('name', + type=str, + help='Name of the new photo size') + + def handle(self, *args, **options): + create_photosize(options['name']) + + +def get_response(msg, func=int, default=None): + while True: + resp = input(msg) + if not resp and default is not None: + return default + try: + return func(resp) + except Exception: + print('Invalid input.') + + +def create_photosize(name, width=0, height=0, crop=False, pre_cache=False, increment_count=False): + try: + size = PhotoSize.objects.get(name=name) + exists = True + except PhotoSize.DoesNotExist: + size = PhotoSize(name=name) + exists = False + if exists: + msg = 'A "%s" photo size already exists. Do you want to replace it? (yes, no):' % name + if not get_response(msg, lambda inp: inp == 'yes', False): + return + print('\nWe will now define the "%s" photo size:\n' % size) + w = get_response('Width (in pixels):', lambda inp: int(inp), width) + h = get_response('Height (in pixels):', lambda inp: int(inp), height) + c = get_response('Crop to fit? (yes, no):', lambda inp: inp == 'yes', crop) + p = get_response('Pre-cache? (yes, no):', lambda inp: inp == 'yes', pre_cache) + i = get_response('Increment count? (yes, no):', lambda inp: inp == 'yes', increment_count) + size.width = w + size.height = h + size.crop = c + size.pre_cache = p + size.increment_count = i + size.save() + print('\nA "%s" photo size has been created.\n' % name) + return size diff --git a/photologue/management/commands/plflush.py b/photologue/management/commands/plflush.py new file mode 100644 index 0000000..9f902e6 --- /dev/null +++ b/photologue/management/commands/plflush.py @@ -0,0 +1,34 @@ +# Based on https://github.com/richardbarran/django-photologue/ +# by Richard Barran, BSD-3 licensed + +from django.core.management.base import BaseCommand, CommandError +from photologue.models import ImageModel, PhotoSize + + +class Command(BaseCommand): + help = 'Clears the Photologue cache for the given sizes.' + + def add_arguments(self, parser): + parser.add_argument('sizes', + nargs='*', + type=str, + help='Name of the photosize.') + + def handle(self, *args, **options): + sizes = options['sizes'] + + if not sizes: + photosizes = PhotoSize.objects.all() + else: + photosizes = PhotoSize.objects.filter(name__in=sizes) + + if not len(photosizes): + raise CommandError('No photo sizes were found.') + + print('Flushing cache...') + + for cls in ImageModel.__subclasses__(): + for photosize in photosizes: + print('Flushing %s size images' % photosize.name) + for obj in cls.objects.all(): + obj.remove_size(photosize) diff --git a/photologue/migrations/0001_initial.py b/photologue/migrations/0001_initial.py new file mode 100644 index 0000000..35d3b3d --- /dev/null +++ b/photologue/migrations/0001_initial.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import django.core.validators +import django.utils.timezone +import sortedm2m.fields +from django.db import migrations, models + +import photologue.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Gallery', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), + ('date_added', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date published')), + ('title', models.CharField(max_length=50, verbose_name='title', unique=True)), + ('slug', models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', verbose_name='title slug', unique=True)), + ('description', models.TextField(blank=True, verbose_name='description')), + ('is_public', models.BooleanField(help_text='Public galleries will be displayed in the default views.', verbose_name='is public', default=True)), + ('tags', photologue.models.TagField(max_length=255, help_text='Django-tagging was not found, tags will be treated as plain text.', blank=True, verbose_name='tags')), + ('sites', models.ManyToManyField(blank=True, verbose_name='sites', null=True, to='sites.Site')), + ], + options={ + 'get_latest_by': 'date_added', + 'verbose_name': 'gallery', + 'ordering': ['-date_added'], + 'verbose_name_plural': 'galleries', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='GalleryUpload', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), + ('zip_file', models.FileField(help_text='Select a .zip file of images to upload into a new Gallery.', verbose_name='images file (.zip)', upload_to='photologue/temp')), + ('title', models.CharField(max_length=50, help_text='All uploaded photos will be given a title made up of this title + a sequential number.', verbose_name='title')), + ('caption', models.TextField(help_text='Caption will be added to all photos.', blank=True, verbose_name='caption')), + ('description', models.TextField(help_text='A description of this Gallery.', blank=True, verbose_name='description')), + ('is_public', models.BooleanField(help_text='Uncheck this to make the uploaded gallery and included photographs private.', verbose_name='is public', default=True)), + ('tags', models.CharField(max_length=255, help_text='Django-tagging was not found, tags will be treated as plain text.', blank=True, verbose_name='tags')), + ('gallery', models.ForeignKey(blank=True, verbose_name='gallery', null=True, help_text='Select a gallery to add these images to. Leave this empty to create a new gallery from the supplied title.', to='photologue.Gallery', on_delete=models.CASCADE)), + ], + options={ + 'verbose_name': 'gallery upload', + 'verbose_name_plural': 'gallery uploads', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Photo', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), + ('image', models.ImageField(upload_to=photologue.models.get_storage_path, verbose_name='image')), + ('date_taken', models.DateTimeField(verbose_name='date taken', blank=True, editable=False, null=True)), + ('view_count', models.PositiveIntegerField(verbose_name='view count', default=0, editable=False)), + ('crop_from', models.CharField(max_length=10, default='center', blank=True, verbose_name='crop from', choices=[('top', 'Top'), ('right', 'Right'), ('bottom', 'Bottom'), ('left', 'Left'), ('center', 'Center (Default)')])), + ('title', models.CharField(max_length=50, verbose_name='title', unique=True)), + ('slug', models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', verbose_name='slug', unique=True)), + ('caption', models.TextField(blank=True, verbose_name='caption')), + ('date_added', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date added')), + ('is_public', models.BooleanField(help_text='Public photographs will be displayed in the default views.', verbose_name='is public', default=True)), + ('tags', photologue.models.TagField(max_length=255, help_text='Django-tagging was not found, tags will be treated as plain text.', blank=True, verbose_name='tags')), + ('sites', models.ManyToManyField(blank=True, verbose_name='sites', null=True, to='sites.Site')), + ], + options={ + 'get_latest_by': 'date_added', + 'verbose_name': 'photo', + 'ordering': ['-date_added'], + 'verbose_name_plural': 'photos', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='gallery', + name='photos', + field=sortedm2m.fields.SortedManyToManyField(blank=True, verbose_name='photos', null=True, to='photologue.Photo'), + preserve_default=True, + ), + migrations.CreateModel( + name='PhotoEffect', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), + ('name', models.CharField(max_length=30, verbose_name='name', unique=True)), + ('description', models.TextField(blank=True, verbose_name='description')), + ('transpose_method', models.CharField(max_length=15, blank=True, verbose_name='rotate or flip', choices=[('FLIP_LEFT_RIGHT', 'Flip left to right'), ('FLIP_TOP_BOTTOM', 'Flip top to bottom'), ('ROTATE_90', 'Rotate 90 degrees counter-clockwise'), ('ROTATE_270', 'Rotate 90 degrees clockwise'), ('ROTATE_180', 'Rotate 180 degrees')])), + ('color', models.FloatField(help_text='A factor of 0.0 gives a black and white image, a factor of 1.0 gives the original image.', verbose_name='color', default=1.0)), + ('brightness', models.FloatField(help_text='A factor of 0.0 gives a black image, a factor of 1.0 gives the original image.', verbose_name='brightness', default=1.0)), + ('contrast', models.FloatField(help_text='A factor of 0.0 gives a solid grey image, a factor of 1.0 gives the original image.', verbose_name='contrast', default=1.0)), + ('sharpness', models.FloatField(help_text='A factor of 0.0 gives a blurred image, a factor of 1.0 gives the original image.', verbose_name='sharpness', default=1.0)), + ('filters', models.CharField(max_length=200, help_text='Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE". Image filters will be applied in order. The following filters are available: BLUR, CONTOUR, DETAIL, EDGE_ENHANCE, EDGE_ENHANCE_MORE, EMBOSS, FIND_EDGES, SHARPEN, SMOOTH, SMOOTH_MORE.', blank=True, verbose_name='filters')), + ('reflection_size', models.FloatField(help_text='The height of the reflection as a percentage of the orignal image. A factor of 0.0 adds no reflection, a factor of 1.0 adds a reflection equal to the height of the orignal image.', verbose_name='size', default=0)), + ('reflection_strength', models.FloatField(help_text='The initial opacity of the reflection gradient.', verbose_name='strength', default=0.6)), + ('background_color', models.CharField(max_length=7, help_text='The background color of the reflection gradient. Set this to match the background color of your page.', verbose_name='color', default='#FFFFFF')), + ], + options={ + 'verbose_name': 'photo effect', + 'verbose_name_plural': 'photo effects', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='photo', + name='effect', + field=models.ForeignKey(blank=True, verbose_name='effect', null=True, to='photologue.PhotoEffect', on_delete=models.CASCADE), + preserve_default=True, + ), + migrations.CreateModel( + name='PhotoSize', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), + ('name', models.CharField(max_length=40, help_text='Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".', verbose_name='name', unique=True, validators=[django.core.validators.RegexValidator(regex='^[a-z0-9_]+$', message='Use only plain lowercase letters (ASCII), numbers and underscores.')])), + ('width', models.PositiveIntegerField(help_text='If width is set to "0" the image will be scaled to the supplied height.', verbose_name='width', default=0)), + ('height', models.PositiveIntegerField(help_text='If height is set to "0" the image will be scaled to the supplied width', verbose_name='height', default=0)), + ('quality', models.PositiveIntegerField(help_text='JPEG image quality.', verbose_name='quality', choices=[(30, 'Very Low'), (40, 'Low'), (50, 'Medium-Low'), (60, 'Medium'), (70, 'Medium-High'), (80, 'High'), (90, 'Very High')], default=70)), + ('upscale', models.BooleanField(help_text='If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting.', verbose_name='upscale images?', default=False)), + ('crop', models.BooleanField(help_text='If selected the image will be scaled and cropped to fit the supplied dimensions.', verbose_name='crop to fit?', default=False)), + ('pre_cache', models.BooleanField(help_text='If selected this photo size will be pre-cached as photos are added.', verbose_name='pre-cache?', default=False)), + ('increment_count', models.BooleanField(help_text='If selected the image\'s "view_count" will be incremented when this photo size is displayed.', verbose_name='increment view count?', default=False)), + ('effect', models.ForeignKey(blank=True, verbose_name='photo effect', null=True, to='photologue.PhotoEffect', on_delete=models.CASCADE)), + ], + options={ + 'verbose_name': 'photo size', + 'ordering': ['width', 'height'], + 'verbose_name_plural': 'photo sizes', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Watermark', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), + ('name', models.CharField(max_length=30, verbose_name='name', unique=True)), + ('description', models.TextField(blank=True, verbose_name='description')), + ('image', models.ImageField(upload_to='photologue/watermarks', verbose_name='image')), + ('style', models.CharField(max_length=5, default='scale', verbose_name='style', choices=[('tile', 'Tile'), ('scale', 'Scale')])), + ('opacity', models.FloatField(help_text='The opacity of the overlay.', verbose_name='opacity', default=1)), + ], + options={ + 'verbose_name': 'watermark', + 'verbose_name_plural': 'watermarks', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='photosize', + name='watermark', + field=models.ForeignKey(blank=True, verbose_name='watermark image', null=True, to='photologue.Watermark', on_delete=models.CASCADE), + preserve_default=True, + ), + ] diff --git a/photologue/migrations/0002_photosize_data.py b/photologue/migrations/0002_photosize_data.py new file mode 100644 index 0000000..7bb9229 --- /dev/null +++ b/photologue/migrations/0002_photosize_data.py @@ -0,0 +1,43 @@ +# encoding: utf8 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def initial_photosizes(apps, schema_editor): + + PhotoSize = apps.get_model('photologue', 'PhotoSize') + + # If there are already Photosizes, then we are upgrading an existing + # installation, we don't want to auto-create some PhotoSizes. + if PhotoSize.objects.all().count() > 0: + return + PhotoSize.objects.create(name='admin_thumbnail', + width=100, + height=75, + crop=True, + pre_cache=True, + increment_count=False) + PhotoSize.objects.create(name='thumbnail', + width=100, + height=75, + crop=True, + pre_cache=True, + increment_count=False) + PhotoSize.objects.create(name='display', + width=400, + crop=False, + pre_cache=True, + increment_count=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.RunPython(initial_photosizes), + ] diff --git a/photologue/migrations/0003_auto_20140822_1716.py b/photologue/migrations/0003_auto_20140822_1716.py new file mode 100644 index 0000000..16d1942 --- /dev/null +++ b/photologue/migrations/0003_auto_20140822_1716.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0002_photosize_data'), + ] + + operations = [ + migrations.AlterField( + model_name='galleryupload', + name='title', + field=models.CharField(null=True, help_text='All uploaded photos will be given a title made up of this title + a sequential number.', max_length=50, verbose_name='title', blank=True), + ), + ] diff --git a/photologue/migrations/0004_auto_20140915_1259.py b/photologue/migrations/0004_auto_20140915_1259.py new file mode 100644 index 0000000..0202044 --- /dev/null +++ b/photologue/migrations/0004_auto_20140915_1259.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import sortedm2m.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0003_auto_20140822_1716'), + ] + + operations = [ + migrations.AlterField( + model_name='gallery', + name='photos', + field=sortedm2m.fields.SortedManyToManyField(to='photologue.Photo', related_name='galleries', null=True, verbose_name='photos', blank=True, help_text=None), + ), + migrations.AlterField( + model_name='photo', + name='effect', + field=models.ForeignKey(to='photologue.PhotoEffect', blank=True, related_name='photo_related', verbose_name='effect', null=True, on_delete=models.CASCADE), + ), + migrations.AlterField( + model_name='photosize', + name='effect', + field=models.ForeignKey(to='photologue.PhotoEffect', blank=True, related_name='photo_sizes', verbose_name='photo effect', null=True, on_delete=models.CASCADE), + ), + migrations.AlterField( + model_name='photosize', + name='watermark', + field=models.ForeignKey(to='photologue.Watermark', blank=True, related_name='photo_sizes', verbose_name='watermark image', null=True, on_delete=models.CASCADE), + ), + ] diff --git a/photologue/migrations/0005_auto_20141027_1552.py b/photologue/migrations/0005_auto_20141027_1552.py new file mode 100644 index 0000000..9f3d862 --- /dev/null +++ b/photologue/migrations/0005_auto_20141027_1552.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0004_auto_20140915_1259'), + ] + + operations = [ + migrations.AlterField( + model_name='photo', + name='title', + field=models.CharField(unique=True, max_length=60, verbose_name='title'), + preserve_default=True, + ), + ] diff --git a/photologue/migrations/0006_auto_20141028_2005.py b/photologue/migrations/0006_auto_20141028_2005.py new file mode 100644 index 0000000..583c3b8 --- /dev/null +++ b/photologue/migrations/0006_auto_20141028_2005.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0005_auto_20141027_1552'), + ] + + operations = [ + migrations.RemoveField( + model_name='galleryupload', + name='gallery', + ), + migrations.DeleteModel( + name='GalleryUpload', + ), + ] diff --git a/photologue/migrations/0007_auto_20150404_1737.py b/photologue/migrations/0007_auto_20150404_1737.py new file mode 100644 index 0000000..b41490c --- /dev/null +++ b/photologue/migrations/0007_auto_20150404_1737.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import sortedm2m.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0006_auto_20141028_2005'), + ] + + operations = [ + migrations.AlterField( + model_name='gallery', + name='photos', + field=sortedm2m.fields.SortedManyToManyField(help_text=None, related_name='galleries', verbose_name='photos', to='photologue.Photo', blank=True), + ), + migrations.AlterField( + model_name='gallery', + name='sites', + field=models.ManyToManyField(to='sites.Site', verbose_name='sites', blank=True), + ), + migrations.AlterField( + model_name='photo', + name='sites', + field=models.ManyToManyField(to='sites.Site', verbose_name='sites', blank=True), + ), + ] diff --git a/photologue/migrations/0008_auto_20150509_1557.py b/photologue/migrations/0008_auto_20150509_1557.py new file mode 100644 index 0000000..0baafd2 --- /dev/null +++ b/photologue/migrations/0008_auto_20150509_1557.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0007_auto_20150404_1737'), + ] + + operations = [ + migrations.RemoveField( + model_name='gallery', + name='tags', + ), + migrations.RemoveField( + model_name='photo', + name='tags', + ), + ] diff --git a/photologue/migrations/0009_auto_20160102_0904.py b/photologue/migrations/0009_auto_20160102_0904.py new file mode 100644 index 0000000..4c64f11 --- /dev/null +++ b/photologue/migrations/0009_auto_20160102_0904.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-01-02 09:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0008_auto_20150509_1557'), + ] + + operations = [ + migrations.AlterField( + model_name='photo', + name='date_taken', + field=models.DateTimeField(blank=True, help_text='Date image was taken; is obtained from the image EXIF data.', null=True, verbose_name='date taken'), + ), + ] diff --git a/photologue/migrations/0010_auto_20160105_1307.py b/photologue/migrations/0010_auto_20160105_1307.py new file mode 100644 index 0000000..d466cae --- /dev/null +++ b/photologue/migrations/0010_auto_20160105_1307.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-01-05 13:07 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0009_auto_20160102_0904'), + ] + + operations = [ + migrations.AlterField( + model_name='gallery', + name='slug', + field=models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', max_length=250, unique=True, verbose_name='title slug'), + ), + migrations.AlterField( + model_name='gallery', + name='title', + field=models.CharField(max_length=250, unique=True, verbose_name='title'), + ), + migrations.AlterField( + model_name='photo', + name='slug', + field=models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', max_length=250, unique=True, verbose_name='slug'), + ), + migrations.AlterField( + model_name='photo', + name='title', + field=models.CharField(max_length=250, unique=True, verbose_name='title'), + ), + ] diff --git a/photologue/migrations/0011_auto_20190223_2138.py b/photologue/migrations/0011_auto_20190223_2138.py new file mode 100644 index 0000000..7bee4eb --- /dev/null +++ b/photologue/migrations/0011_auto_20190223_2138.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-02-23 21:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0010_auto_20160105_1307'), + ] + + operations = [ + migrations.AlterField( + model_name='photoeffect', + name='filters', + field=models.CharField(blank=True, help_text='Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE". Image filters will be applied in order. The following filters are available: BLUR, CONTOUR, DETAIL, EDGE_ENHANCE, EDGE_ENHANCE_MORE, EMBOSS, FIND_EDGES, Kernel, SHARPEN, SMOOTH, SMOOTH_MORE.', max_length=200, verbose_name='filters'), + ), + ] diff --git a/photologue/migrations/0012_auto_20220129_2207.py b/photologue/migrations/0012_auto_20220129_2207.py new file mode 100644 index 0000000..7ed2b9a --- /dev/null +++ b/photologue/migrations/0012_auto_20220129_2207.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.11 on 2022-01-29 22:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0011_auto_20190223_2138'), + ] + + operations = [ + migrations.RemoveField( + model_name='gallery', + name='sites', + ), + migrations.RemoveField( + model_name='photo', + name='effect', + ), + migrations.RemoveField( + model_name='photo', + name='sites', + ), + migrations.RemoveField( + model_name='photosize', + name='effect', + ), + migrations.RemoveField( + model_name='photosize', + name='watermark', + ), + migrations.DeleteModel( + name='PhotoEffect', + ), + migrations.DeleteModel( + name='Watermark', + ), + ] diff --git a/photologue/migrations/0013_alter_gallery_photos.py b/photologue/migrations/0013_alter_gallery_photos.py new file mode 100644 index 0000000..ab5ecbc --- /dev/null +++ b/photologue/migrations/0013_alter_gallery_photos.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-01-30 07:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0012_auto_20220129_2207'), + ] + + operations = [ + migrations.AlterField( + model_name='gallery', + name='photos', + field=models.ManyToManyField(blank=True, related_name='galleries', to='photologue.Photo', verbose_name='photos'), + ), + ] diff --git a/photologue/migrations/__init__.py b/photologue/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/photologue/models.py b/photologue/models.py new file mode 100644 index 0000000..f38fbc5 --- /dev/null +++ b/photologue/models.py @@ -0,0 +1,659 @@ +# Based on https://github.com/richardbarran/django-photologue/ +# by Richard Barran, BSD-3 licensed + +import logging +import os +import random +import unicodedata +from datetime import datetime +from functools import partial +from importlib import import_module +from inspect import isclass +from io import BytesIO + +import exifread +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.core.validators import RegexValidator +from django.db import models +from django.template.defaultfilters import slugify +from django.urls import reverse +from django.utils.encoding import filepath_to_uri, force_str, smart_str +from django.utils.safestring import mark_safe +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from PIL import Image, ImageFile, ImageFilter + +logger = logging.getLogger('photologue.models') + +# Default limit for gallery.latest +LATEST_LIMIT = getattr(settings, 'PHOTOLOGUE_GALLERY_LATEST_LIMIT', None) + +# max_length setting for the ImageModel ImageField +IMAGE_FIELD_MAX_LENGTH = getattr(settings, 'PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH', 100) + +# Modify image file buffer size. +ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10) + +# Look for user function to define file paths +PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None) +if PHOTOLOGUE_PATH is not None: + if callable(PHOTOLOGUE_PATH): + get_storage_path = PHOTOLOGUE_PATH + else: + parts = PHOTOLOGUE_PATH.split('.') + module_name = '.'.join(parts[:-1]) + module = import_module(module_name) + get_storage_path = getattr(module, parts[-1]) +else: + def get_storage_path(instance, filename): + fn = unicodedata.normalize('NFKD', force_str(filename)).encode('ascii', 'ignore').decode('ascii') + return os.path.join('photos', fn) + +# Support CACHEDIR.TAG spec for backups for ignoring cache dir. +# See http://www.brynosaurus.com/cachedir/spec.html +PHOTOLOGUE_CACHEDIRTAG = os.path.join("photos", "cache", "CACHEDIR.TAG") +if not default_storage.exists(PHOTOLOGUE_CACHEDIRTAG): + default_storage.save(PHOTOLOGUE_CACHEDIRTAG, ContentFile( + b"Signature: 8a477f597d28d172789f06886806bc55")) + +# Exif Orientation values +# Value 0thRow 0thColumn +# 1 top left +# 2 top right +# 3 bottom right +# 4 bottom left +# 5 left top +# 6 right top +# 7 right bottom +# 8 left bottom + +# Image Orientations (according to EXIF informations) that needs to be +# transposed and appropriate action +IMAGE_EXIF_ORIENTATION_MAP = { + 2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 6: Image.ROTATE_270, + 8: Image.ROTATE_90, +} + +# Quality options for JPEG images +JPEG_QUALITY_CHOICES = ( + (30, _('Very Low')), + (40, _('Low')), + (50, _('Medium-Low')), + (60, _('Medium')), + (70, _('Medium-High')), + (80, _('High')), + (90, _('Very High')), +) + +# choices for new crop_anchor field in Photo +CROP_ANCHOR_CHOICES = ( + ('top', _('Top')), + ('right', _('Right')), + ('bottom', _('Bottom')), + ('left', _('Left')), + ('center', _('Center (Default)')), +) + +IMAGE_TRANSPOSE_CHOICES = ( + ('FLIP_LEFT_RIGHT', _('Flip left to right')), + ('FLIP_TOP_BOTTOM', _('Flip top to bottom')), + ('ROTATE_90', _('Rotate 90 degrees counter-clockwise')), + ('ROTATE_270', _('Rotate 90 degrees clockwise')), + ('ROTATE_180', _('Rotate 180 degrees')), +) + +# Prepare a list of image filters +filter_names = [] +for n in dir(ImageFilter): + klass = getattr(ImageFilter, n) + if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \ + hasattr(klass, 'name'): + filter_names.append(klass.__name__) +IMAGE_FILTERS_HELP_TEXT = _('Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE"' + '. Image filters will be applied in order. The following filters are available: %s.' + % (', '.join(filter_names))) + +size_method_map = {} + + +class TagField(models.CharField): + """Tags have been removed from Photologue, but the migrations still refer to them so this + Tagfield definition is left here. + """ + + def __init__(self, **kwargs): + default_kwargs = {'max_length': 255, 'blank': True} + default_kwargs.update(kwargs) + super().__init__(**default_kwargs) + + def get_internal_type(self): + return 'CharField' + + +class Gallery(models.Model): + date_added = models.DateTimeField(_('date published'), + default=now) + title = models.CharField(_('title'), + max_length=250, + unique=True) + slug = models.SlugField(_('title slug'), + unique=True, + max_length=250, + help_text=_('A "slug" is a unique URL-friendly title for an object.')) + description = models.TextField(_('description'), + blank=True) + is_public = models.BooleanField(_('is public'), + default=True, + help_text=_('Public galleries will be displayed ' + 'in the default views.')) + photos = models.ManyToManyField('photologue.Photo', + related_name='galleries', + verbose_name=_('photos'), + blank=True) + + class Meta: + ordering = ['-date_added'] + get_latest_by = 'date_added' + verbose_name = _('gallery') + verbose_name_plural = _('galleries') + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('photologue:pl-gallery', args=[self.slug]) + + def latest(self, limit=LATEST_LIMIT, public=True): + if not limit: + limit = self.photo_count() + if public: + return self.public()[:limit] + else: + return self.photos[:limit] + + def sample(self, count=None, public=True): + """Return a sample of photos, ordered at random.""" + if not count: + count = 1 + if count > self.photo_count(): + count = self.photo_count() + if public: + photo_set = self.public() + else: + photo_set = self.photos + return random.sample(set(photo_set), count) + + def photo_count(self, public=True): + """Return a count of all the photos in this gallery.""" + if public: + return self.public().count() + else: + return self.photos.count() + + photo_count.short_description = _('count') + + def public(self): + """Return a queryset of all the public photos in this gallery.""" + return self.photos.filter(is_public=True) + + +class ImageModel(models.Model): + image = models.ImageField(_('image'), + max_length=IMAGE_FIELD_MAX_LENGTH, + upload_to=get_storage_path) + date_taken = models.DateTimeField(_('date taken'), + null=True, + blank=True, + help_text=_('Date image was taken; is obtained from the image EXIF data.')) + view_count = models.PositiveIntegerField(_('view count'), + default=0, + editable=False) + crop_from = models.CharField(_('crop from'), + blank=True, + max_length=10, + default='center', + choices=CROP_ANCHOR_CHOICES) + + class Meta: + abstract = True + + def exif(self, file=None): + try: + if file: + tags = exifread.process_file(file) + else: + with self.image.storage.open(self.image.name, 'rb') as file: + tags = exifread.process_file(file, details=False) + return tags + except Exception: + return {} + + def admin_thumbnail(self): + func = getattr(self, 'get_admin_thumbnail_url', None) + if func is None: + return _('An "admin_thumbnail" photo size has not been defined.') + else: + if hasattr(self, 'get_absolute_url'): + return mark_safe(''.format(self.get_absolute_url(), func())) + else: + return mark_safe(''.format(self.image.url, func())) + + admin_thumbnail.short_description = _('Thumbnail') + admin_thumbnail.allow_tags = True + + def cache_path(self): + return os.path.join(os.path.dirname(self.image.name), "cache") + + def cache_url(self): + return '/'.join([os.path.dirname(self.image.url), "cache"]) + + def image_filename(self): + return os.path.basename(force_str(self.image.name)) + + def _get_filename_for_size(self, size): + size = getattr(size, 'name', size) + base, ext = os.path.splitext(self.image_filename()) + return ''.join([base, '_', size, ext]) + + def _get_size_photosize(self, size): + return PhotoSizeCache().sizes.get(size) + + def _get_size_size(self, size): + photosize = PhotoSizeCache().sizes.get(size) + if not self.size_exists(photosize): + self.create_size(photosize) + try: + return Image.open(self.image.storage.open( + self._get_size_filename(size))).size + except Exception: + return None + + def _get_size_url(self, size): + photosize = PhotoSizeCache().sizes.get(size) + if not self.size_exists(photosize): + self.create_size(photosize) + if photosize.increment_count: + self.increment_count() + return '/'.join([ + self.cache_url(), + filepath_to_uri(self._get_filename_for_size(photosize.name))]) + + def _get_size_filename(self, size): + photosize = PhotoSizeCache().sizes.get(size) + return smart_str(os.path.join(self.cache_path(), + self._get_filename_for_size(photosize.name))) + + def increment_count(self): + self.view_count += 1 + models.Model.save(self) + + def __getattr__(self, name): + global size_method_map + if not size_method_map: + init_size_method_map() + di = size_method_map.get(name, None) + if di is not None: + result = partial(getattr(self, di['base_name']), di['size']) + setattr(self, name, result) + return result + else: + raise AttributeError + + def size_exists(self, photosize): + func = getattr(self, "get_%s_filename" % photosize.name, None) + if func is not None: + if self.image.storage.exists(func()): + return True + return False + + def resize_image(self, im, photosize): + cur_width, cur_height = im.size + new_width, new_height = photosize.size + if photosize.crop: + ratio = max(float(new_width) / cur_width, float(new_height) / cur_height) + x = (cur_width * ratio) + y = (cur_height * ratio) + xd = abs(new_width - x) + yd = abs(new_height - y) + x_diff = int(xd / 2) + y_diff = int(yd / 2) + if self.crop_from == 'top': + box = (int(x_diff), 0, int(x_diff + new_width), new_height) + elif self.crop_from == 'left': + box = (0, int(y_diff), new_width, int(y_diff + new_height)) + elif self.crop_from == 'bottom': + # y - yd = new_height + box = (int(x_diff), int(yd), int(x_diff + new_width), int(y)) + elif self.crop_from == 'right': + # x - xd = new_width + box = (int(xd), int(y_diff), int(x), int(y_diff + new_height)) + else: + box = (int(x_diff), int(y_diff), int(x_diff + new_width), int(y_diff + new_height)) + im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box) + else: + if not new_width == 0 and not new_height == 0: + ratio = min(float(new_width) / cur_width, + float(new_height) / cur_height) + else: + if new_width == 0: + ratio = float(new_height) / cur_height + else: + ratio = float(new_width) / cur_width + new_dimensions = (int(round(cur_width * ratio)), + int(round(cur_height * ratio))) + if new_dimensions[0] > cur_width or \ + new_dimensions[1] > cur_height: + if not photosize.upscale: + return im + im = im.resize(new_dimensions, Image.ANTIALIAS) + return im + + def create_size(self, photosize, recreate=False): + if self.size_exists(photosize) and not recreate: + return + try: + im = Image.open(self.image.storage.open(self.image.name)) + except OSError: + return + # Save the original format + im_format = im.format + # Apply effect if found + if self.effect is not None: + im = self.effect.pre_process(im) + elif photosize.effect is not None: + im = photosize.effect.pre_process(im) + # Rotate if found & necessary + if 'Image Orientation' in self.exif() and \ + self.exif().get('Image Orientation').values[0] in IMAGE_EXIF_ORIENTATION_MAP: + im = im.transpose( + IMAGE_EXIF_ORIENTATION_MAP[self.EXIF().get('Image Orientation').values[0]]) + # Resize/crop image + if (im.size != photosize.size and photosize.size != (0, 0)) or recreate: + im = self.resize_image(im, photosize) + # Apply effect if found + if self.effect is not None: + im = self.effect.post_process(im) + elif photosize.effect is not None: + im = photosize.effect.post_process(im) + # Save file + im_filename = getattr(self, "get_%s_filename" % photosize.name)() + try: + buffer = BytesIO() + if im_format != 'JPEG': + im.save(buffer, im_format) + else: + # Issue #182 - test fix from https://github.com/bashu/django-watermark/issues/31 + if im.mode.endswith('A'): + im = im.convert(im.mode[:-1]) + im.save(buffer, 'JPEG', quality=int(photosize.quality), + optimize=True) + buffer_contents = ContentFile(buffer.getvalue()) + self.image.storage.save(im_filename, buffer_contents) + except OSError as e: + if self.image.storage.exists(im_filename): + self.image.storage.delete(im_filename) + raise e + + def remove_size(self, photosize, remove_dirs=True): + if not self.size_exists(photosize): + return + filename = getattr(self, "get_%s_filename" % photosize.name)() + if self.image.storage.exists(filename): + self.image.storage.delete(filename) + + def clear_cache(self): + cache = PhotoSizeCache() + for photosize in cache.sizes.values(): + self.remove_size(photosize, False) + + def pre_cache(self, recreate=False): + cache = PhotoSizeCache() + if recreate: + self.clear_cache() + for photosize in cache.sizes.values(): + if photosize.pre_cache: + self.create_size(photosize, recreate) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._old_image = self.image + + def save(self, *args, **kwargs): + recreate = kwargs.pop('recreate', False) + image_has_changed = False + if self._get_pk_val() and (self._old_image != self.image): + image_has_changed = True + # If we have changed the image, we need to clear from the cache all instances of the old + # image; clear_cache() works on the current (new) image, and in turn calls several other methods. + # Changing them all to act on the old image was a lot of changes, so instead we temporarily swap old + # and new images. + new_image = self.image + self.image = self._old_image + self.clear_cache() + self.image = new_image # Back to the new image. + self._old_image.storage.delete(self._old_image.name) # Delete (old) base image. + if self.date_taken is None or image_has_changed: + # Attempt to get the date the photo was taken from the EXIF data. + try: + exif_date = self.exif(self.image.file).get('EXIF DateTimeOriginal', None) + if exif_date is not None: + d, t = exif_date.values.split() + year, month, day = d.split(':') + hour, minute, second = t.split(':') + self.date_taken = datetime(int(year), int(month), int(day), + int(hour), int(minute), int(second)) + except Exception: + logger.error('Failed to read EXIF DateTimeOriginal', exc_info=True) + super().save(*args, **kwargs) + self.pre_cache(recreate) + + def delete(self): + assert self._get_pk_val() is not None, \ + "%s object can't be deleted because its %s attribute is set to None." % \ + (self._meta.object_name, self._meta.pk.attname) + self.clear_cache() + # Files associated to a FileField have to be manually deleted: + # https://docs.djangoproject.com/en/dev/releases/1.3/#deleting-a-model-doesn-t-delete-associated-files + # http://haineault.com/blog/147/ + # The data loss scenarios mentioned in the docs hopefully do not apply + # to Photologue! + super().delete() + self.image.storage.delete(self.image.name) + + +class Photo(ImageModel): + title = models.CharField(_('title'), + max_length=250, + unique=True) + slug = models.SlugField(_('slug'), + unique=True, + max_length=250, + help_text=_('A "slug" is a unique URL-friendly title for an object.')) + caption = models.TextField(_('caption'), + blank=True) + date_added = models.DateTimeField(_('date added'), + default=now) + is_public = models.BooleanField(_('is public'), + default=True, + help_text=_('Public photographs will be displayed in the default views.')) + + class Meta: + ordering = ['-date_added'] + get_latest_by = 'date_added' + verbose_name = _("photo") + verbose_name_plural = _("photos") + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + # If crop_from or effect property has been changed on existing image, + # update kwargs to force image recreation in parent class + current = Photo.objects.get(pk=self.pk) if self.pk else None + if current and (current.crop_from != self.crop_from or current.effect != self.effect): + kwargs.update(recreate=True) + + if self.slug is None: + self.slug = slugify(self.title) + super().save(*args, **kwargs) + + def get_absolute_url(self): + return reverse('photologue:pl-photo', args=[self.slug]) + + def public_galleries(self): + """Return the public galleries to which this photo belongs.""" + return self.galleries.filter(is_public=True) + + def get_previous_in_gallery(self, gallery): + """Find the neighbour of this photo in the supplied gallery. + We assume that the gallery and all its photos are on the same site. + """ + if not self.is_public: + raise ValueError('Cannot determine neighbours of a non-public photo.') + photos = gallery.photos.filter(is_public=True) + if self not in photos: + raise ValueError('Photo does not belong to gallery.') + previous = None + for photo in photos: + if photo == self: + return previous + previous = photo + + def get_next_in_gallery(self, gallery): + """Find the neighbour of this photo in the supplied gallery. + We assume that the gallery and all its photos are on the same site. + """ + if not self.is_public: + raise ValueError('Cannot determine neighbours of a non-public photo.') + photos = gallery.photos.filter(is_public=True) + if self not in photos: + raise ValueError('Photo does not belong to gallery.') + matched = False + for photo in photos: + if matched: + return photo + if photo == self: + matched = True + return None + + +class PhotoSize(models.Model): + """About the Photosize name: it's used to create get_PHOTOSIZE_url() methods, + so the name has to follow the same restrictions as any Python method name, + e.g. no spaces or non-ascii characters.""" + + name = models.CharField(_('name'), + max_length=40, + unique=True, + help_text=_( + 'Photo size name should contain only letters, numbers and underscores. Examples: ' + '"thumbnail", "display", "small", "main_page_widget".'), + validators=[RegexValidator(regex='^[a-z0-9_]+$', + message='Use only plain lowercase letters (ASCII), numbers and ' + 'underscores.' + )] + ) + width = models.PositiveIntegerField(_('width'), + default=0, + help_text=_( + 'If width is set to "0" the image will be scaled to the supplied height.')) + height = models.PositiveIntegerField(_('height'), + default=0, + help_text=_( + 'If height is set to "0" the image will be scaled to the supplied width')) + quality = models.PositiveIntegerField(_('quality'), + choices=JPEG_QUALITY_CHOICES, + default=70, + help_text=_('JPEG image quality.')) + upscale = models.BooleanField(_('upscale images?'), + default=False, + help_text=_('If selected the image will be scaled up if necessary to fit the ' + 'supplied dimensions. Cropped sizes will be upscaled regardless of this ' + 'setting.') + ) + crop = models.BooleanField(_('crop to fit?'), + default=False, + help_text=_('If selected the image will be scaled and cropped to fit the supplied ' + 'dimensions.')) + pre_cache = models.BooleanField(_('pre-cache?'), + default=False, + help_text=_('If selected this photo size will be pre-cached as photos are added.')) + increment_count = models.BooleanField(_('increment view count?'), + default=False, + help_text=_('If selected the image\'s "view_count" will be incremented when ' + 'this photo size is displayed.')) + + class Meta: + ordering = ['width', 'height'] + verbose_name = _('photo size') + verbose_name_plural = _('photo sizes') + + def __str__(self): + return self.name + + def clear_cache(self): + for cls in ImageModel.__subclasses__(): + for obj in cls.objects.all(): + obj.remove_size(self) + if self.pre_cache: + obj.create_size(self) + PhotoSizeCache().reset() + + def clean(self): + if self.crop is True: + if self.width == 0 or self.height == 0: + raise ValidationError( + _("Can only crop photos if both width and height dimensions are set.")) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + PhotoSizeCache().reset() + self.clear_cache() + + def delete(self): + assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." \ + % (self._meta.object_name, self._meta.pk.attname) + self.clear_cache() + super().delete() + + def _get_size(self): + return (self.width, self.height) + + def _set_size(self, value): + self.width, self.height = value + + size = property(_get_size, _set_size) + + +class PhotoSizeCache: + __state = {"sizes": {}} + + def __init__(self): + self.__dict__ = self.__state + if not len(self.sizes): + sizes = PhotoSize.objects.all() + for size in sizes: + self.sizes[size.name] = size + + def reset(self): + global size_method_map + size_method_map = {} + self.sizes = {} + + +def init_size_method_map(): + global size_method_map + for size in PhotoSizeCache().sizes.keys(): + size_method_map['get_%s_size' % size] = \ + {'base_name': '_get_size_size', 'size': size} + size_method_map['get_%s_photosize' % size] = \ + {'base_name': '_get_size_photosize', 'size': size} + size_method_map['get_%s_url' % size] = \ + {'base_name': '_get_size_url', 'size': size} + size_method_map['get_%s_filename' % size] = \ + {'base_name': '_get_size_filename', 'size': size} diff --git a/photologue/views.py b/photologue/views.py new file mode 100644 index 0000000..93fc8b5 --- /dev/null +++ b/photologue/views.py @@ -0,0 +1,24 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic.dates import ArchiveIndexView, YearArchiveView +from django.views.generic.detail import DetailView + +from .models import Gallery, Photo + + +class GalleryDateView(LoginRequiredMixin): + queryset = Gallery.objects.filter(is_public=True) + date_field = 'extended__date_start' + uses_datetime_field = False # Fix related object access + allow_empty = True + + +class GalleryArchiveIndexView(GalleryDateView, ArchiveIndexView): + pass + + +class GalleryYearArchiveView(GalleryDateView, YearArchiveView): + make_object_list = True + + +class PhotoDetailView(LoginRequiredMixin, DetailView): + queryset = Photo.objects.filter(is_public=True) diff --git a/photologue_custom/admin.py b/photologue_custom/admin.py index 41b967f..e8f0130 100644 --- a/photologue_custom/admin.py +++ b/photologue_custom/admin.py @@ -2,7 +2,7 @@ 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 +from photologue.models import Gallery, Photo from .models import GalleryExtended, PhotoExtended @@ -18,8 +18,6 @@ class GalleryAdmin(GalleryAdminDefault): model. """ inlines = [GalleryExtendedInline, ] - autocomplete_fields = ['photos', ] - search_fields = ['title', ] class PhotoExtendedInline(admin.StackedInline): @@ -45,10 +43,5 @@ class PhotoAdmin(PhotoAdminDefault): get_owner.short_description = _('owner') -admin.site.unregister(Gallery) -admin.site.unregister(Photo) -admin.site.unregister(PhotoEffect) -admin.site.unregister(PhotoSize) -admin.site.unregister(Watermark) admin.site.register(Gallery, GalleryAdmin) admin.site.register(Photo, PhotoAdmin) diff --git a/photologue_custom/apps.py b/photologue_custom/apps.py index b08e032..55acba8 100644 --- a/photologue_custom/apps.py +++ b/photologue_custom/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class PhotologueCustomConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'photologue_custom' diff --git a/photologue_custom/templates/photologue/gallery_archive.html b/photologue_custom/templates/photologue/gallery_archive.html index 364ae4e..188101b 100644 --- a/photologue_custom/templates/photologue/gallery_archive.html +++ b/photologue_custom/templates/photologue/gallery_archive.html @@ -1,4 +1,4 @@ -{% extends "photologue/root.html" %} +{% extends "base.html" %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} diff --git a/photologue_custom/templates/photologue/gallery_archive_year.html b/photologue_custom/templates/photologue/gallery_archive_year.html index 6a895bb..580731a 100644 --- a/photologue_custom/templates/photologue/gallery_archive_year.html +++ b/photologue_custom/templates/photologue/gallery_archive_year.html @@ -1,4 +1,4 @@ -{% extends "photologue/root.html" %} +{% extends "base.html" %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} diff --git a/photologue_custom/templates/photologue/gallery_detail.html b/photologue_custom/templates/photologue/gallery_detail.html index dcbb43b..21bc348 100644 --- a/photologue_custom/templates/photologue/gallery_detail.html +++ b/photologue_custom/templates/photologue/gallery_detail.html @@ -1,4 +1,4 @@ -{% extends "photologue/root.html" %} +{% extends "base.html" %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} diff --git a/photologue_custom/templates/photologue/photo_detail.html b/photologue_custom/templates/photologue/photo_detail.html index 9aff5b9..c0d4c1b 100644 --- a/photologue_custom/templates/photologue/photo_detail.html +++ b/photologue_custom/templates/photologue/photo_detail.html @@ -1,4 +1,4 @@ -{% extends "photologue/root.html" %} +{% extends "base.html" %} {% comment %} SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} diff --git a/photologue_custom/urls.py b/photologue_custom/urls.py index 6000e73..037a354 100644 --- a/photologue_custom/urls.py +++ b/photologue_custom/urls.py @@ -1,19 +1,18 @@ from django.urls import path, re_path +from photologue.views import GalleryArchiveIndexView, GalleryYearArchiveView, PhotoDetailView -from .views import (CustomGalleryArchiveIndexView, CustomGalleryDetailView, - CustomGalleryYearArchiveView, CustomPhotoDetailView, - GalleryDownload, GalleryUpload, TagDetail) +from .views import CustomGalleryDetailView, GalleryDownload, GalleryUpload, TagDetail # Rather than using photologue default router, we redefine our own router # with login and permission checks. app_name = 'photologue' urlpatterns = [ path('tag//', TagDetail.as_view(), name='tag-detail'), - path('gallery/', CustomGalleryArchiveIndexView.as_view(), name='pl-gallery-archive'), - re_path(r'^gallery/(?P\d{4})/$', CustomGalleryYearArchiveView.as_view(), name='pl-gallery-archive-year'), + path('gallery/', GalleryArchiveIndexView.as_view(), name='pl-gallery-archive'), + re_path(r'^gallery/(?P\d{4})/$', GalleryYearArchiveView.as_view(), name='pl-gallery-archive-year'), path('gallery//', CustomGalleryDetailView.as_view(), name='pl-gallery'), path('gallery///', CustomGalleryDetailView.as_view(), name='pl-gallery-owner'), path('gallery//download/', GalleryDownload.as_view(), name='pl-gallery-download'), - path('photo//', CustomPhotoDetailView.as_view(), name='pl-photo'), + path('photo//', PhotoDetailView.as_view(), name='pl-photo'), path('upload/', GalleryUpload.as_view(), name='pl-gallery-upload'), ] diff --git a/photologue_custom/views.py b/photologue_custom/views.py index 04bfa69..8a683f0 100644 --- a/photologue_custom/views.py +++ b/photologue_custom/views.py @@ -17,8 +17,6 @@ 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, - PhotoDetailView) from PIL import Image from taggit.models import Tag @@ -35,33 +33,17 @@ class TagDetail(LoginRequiredMixin, DetailView): """ current_tag = self.get_object().slug context = super().get_context_data(**kwargs) - context['galleries'] = Gallery.objects.on_site().is_public() \ + context['galleries'] = Gallery.objects.filter(is_public=True) \ .filter(extended__tags__slug=current_tag) \ .order_by('-extended__date_start') return context -class CustomGalleryArchiveIndexView(LoginRequiredMixin, GalleryArchiveIndexView): - """ - Override to use event date - """ - date_field = 'extended__date_start' - uses_datetime_field = False # Fix related object access - - -class CustomGalleryYearArchiveView(LoginRequiredMixin, GalleryYearArchiveView): - """ - Override to use event date - """ - date_field = 'extended__date_start' - uses_datetime_field = False # Fix related object access - - class CustomGalleryDetailView(LoginRequiredMixin, DetailView): """ Custom gallery detail view to filter on photo owner """ - queryset = Gallery.objects.on_site().is_public() + queryset = Gallery.objects.filter(is_public=True) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -104,10 +86,6 @@ class GalleryDownload(LoginRequiredMixin, DetailView): return response -class CustomPhotoDetailView(LoginRequiredMixin, PhotoDetailView): - pass - - class GalleryUpload(PermissionRequiredMixin, FormView): """ Form to upload new photos in a gallery diff --git a/requirements.txt b/requirements.txt index d6c2685..9f86e8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -Django>=2.2.20 -django-photologue~=3.13 -django-taggit>=1.5.0 -django-crispy-forms~=1.7 django-allauth>=0.44 +django-crispy-forms~=1.7 +django-taggit>=1.5.0 +Django>=2.2.20 +ExifRead>=2.1.2 git+https://gitlab.crans.org/bde/allauth-note-kfet.git +Pillow>=6.0.0 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 007b0c9..f69e975 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -r{toxinidir}/requirements.txt coverage commands = - coverage run --omit='photo21/wsgi.py' --source=photo21,photologue_custom ./manage.py test + coverage run --omit='photo21/wsgi.py' --source=photo21,photologue,photologue_custom ./manage.py test coverage report -m [testenv:linters] @@ -26,7 +26,7 @@ deps = pep8-naming pyflakes commands = - flake8 photo21 photologue_custom + flake8 photo21 photologue photologue_custom [flake8] ignore = W503, I100, I101