diff --git a/README.md b/README.md index 7bda3bdfc030281cc211b27b09373e021abfb046..52dccb321f9648dc948c920cbfc9eb10ba164120 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [](https://www.gnu.org/licenses/gpl-3.0.txt) +Le serveur photos est un projet Django permettant de gérer les photos de la vie +associative de l'ENS Paris-Saclay. + ## Table des matières - [Installation d'une instance de développement](#installation-dune-instance-de-développement) @@ -44,14 +47,16 @@ 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 (env)$ ./manage.py createsuperuser # Création d'un utilisateur initial ``` -6. Enjoy : +6. **Activation du mode déboguage.** + Dans `photo21/settings.py`, changer `DEBUG` à `True`. + +7. Enjoy : ```bash (env)$ ./manage.py runserver 0.0.0.0:8000 @@ -63,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/de/LC_MESSAGES/django.po b/photo21/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000000000000000000000000000000000000..1cff8066dd069ed9d677916be71597ecfcb4f8b1 --- /dev/null +++ b/photo21/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,315 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-29 21:58+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \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" + +#: photo21/forms.py:12 +msgid "" +"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" +"saclay.fr`." +msgstr "" + +#: photo21/forms.py:23 +msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." +msgstr "" + +#: photo21/hashers.py:42 +msgid "algorithm" +msgstr "" + +#: photo21/hashers.py:43 +msgid "salt" +msgstr "" + +#: photo21/hashers.py:44 +msgid "hash" +msgstr "" + +#: photo21/settings.py:162 +msgid "German" +msgstr "" + +#: photo21/settings.py:163 +msgid "English" +msgstr "" + +#: photo21/settings.py:164 +msgid "Spanish" +msgstr "" + +#: photo21/settings.py:165 +msgid "French" +msgstr "" + +#: photo21/templates/400.html:10 +msgid "Bad request" +msgstr "" + +#: photo21/templates/400.html:14 +msgid "" +"Sorry, your request was bad. Don't know what could be wrong. An email has " +"been sent to webmasters with the details of the error. You can now drink a " +"coke." +msgstr "" + +#: photo21/templates/403.html:10 +msgid "Permission denied" +msgstr "" + +#: photo21/templates/403.html:13 +msgid "You don't have the right to perform this request." +msgstr "" + +#: photo21/templates/403.html:15 photo21/templates/404.html:19 +msgid "Exception message:" +msgstr "" + +#: photo21/templates/404.html:10 +msgid "Page not found" +msgstr "" + +#: photo21/templates/404.html:14 +#, python-format +msgid "" +"The requested path <code>%(request_path)s</code> was not found on the server." +msgstr "" + +#: photo21/templates/500.html:10 +msgid "Server error" +msgstr "" + +#: photo21/templates/500.html:14 +msgid "" +"Sorry, an error occurred when processing your request. An email has been " +"sent to webmasters with the detail of the error, and this will be fixed " +"soon. You can now drink a beer." +msgstr "" + +#: photo21/templates/account/email.html:6 +#: photo21/templates/account/email.html:14 +#: photo21/templates/socialaccount/connections.html:14 +msgid "E-mail Addresses" +msgstr "" + +#: photo21/templates/account/email.html:9 photo21/templates/base.html:59 +#: photo21/templates/socialaccount/connections.html:9 +msgid "Account" +msgstr "" + +#: photo21/templates/account/email.html:17 +#: photo21/templates/socialaccount/connections.html:17 +msgid "Social connections" +msgstr "" + +#: photo21/templates/account/email.html:23 +msgid "The following e-mail addresses are associated with your account:" +msgstr "" + +#: photo21/templates/account/email.html:34 +msgid "Verified" +msgstr "" + +#: photo21/templates/account/email.html:36 +msgid "Unverified" +msgstr "" + +#: photo21/templates/account/email.html:38 +msgid "Primary" +msgstr "" + +#: photo21/templates/account/email.html:44 +msgid "Make Primary" +msgstr "" + +#: photo21/templates/account/email.html:45 +msgid "Re-send Verification" +msgstr "" + +#: photo21/templates/account/email.html:46 +#: photo21/templates/socialaccount/connections.html:45 +msgid "Remove" +msgstr "" + +#: photo21/templates/account/email.html:51 +msgid "Warning:" +msgstr "" + +#: photo21/templates/account/email.html:51 +msgid "" +"You currently do not have any e-mail address set up. You should really add " +"an e-mail address so you can receive notifications, reset your password, etc." +msgstr "" + +#: photo21/templates/account/email.html:55 +msgid "Add E-mail Address" +msgstr "" + +#: photo21/templates/account/email.html:60 +msgid "Add E-mail" +msgstr "" + +#: photo21/templates/account/email.html:68 +msgid "Do you really want to remove the selected e-mail address?" +msgstr "" + +#: photo21/templates/account/login.html:6 +#: photo21/templates/account/login.html:11 +#: photo21/templates/account/login.html:39 +msgid "Sign In" +msgstr "" + +#: photo21/templates/account/login.html:16 +#, python-format +msgid "" +"Please sign in with one of your existing third party accounts. Or, <a href=" +"\"%(signup_url)s\">sign up</a> for a %(site_name)s account and sign in below:" +msgstr "" + +#: photo21/templates/account/login.html:24 +msgid "or" +msgstr "" + +#: photo21/templates/account/login.html:30 +#, python-format +msgid "" +"If you have not created an account yet, then please <a href=\"%(signup_url)s" +"\">sign up</a> first." +msgstr "" + +#: photo21/templates/account/login.html:40 +msgid "Forgot Password?" +msgstr "" + +#: photo21/templates/account/login.html:44 +msgid "If any problem, please contact the server owners at" +msgstr "" + +#: photo21/templates/account/signup.html:6 +msgid "Signup" +msgstr "" + +#: photo21/templates/account/signup.html:11 +#: photo21/templates/account/signup.html:22 +msgid "Sign Up" +msgstr "" + +#: photo21/templates/account/signup.html:14 +#, python-format +msgid "" +"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a>." +msgstr "" + +#: photo21/templates/base.html:12 +msgid "The ENS Paris-Saclay pictures server." +msgstr "" + +#: photo21/templates/base.html:37 +msgid "Galleries" +msgstr "" + +#: photo21/templates/base.html:42 +msgid "Upload" +msgstr "" + +#: photo21/templates/base.html:47 +msgid "Manage" +msgstr "" + +#: photo21/templates/base.html:68 +msgid "Log out" +msgstr "" + +#: photo21/templates/base.html:78 +msgid "Log in" +msgstr "" + +#: photo21/templates/base.html:87 +msgid "Sign up" +msgstr "" + +#: photo21/templates/base.html:109 +msgid "Source code" +msgstr "" + +#: photo21/templates/index.html:6 +msgid "Home" +msgstr "" + +#: photo21/templates/index.html:9 +msgid "Welcome to the pictures server!" +msgstr "" + +#: photo21/templates/index.html:11 +msgid "" +"This website aims to collect the pictures and movies taken in the student " +"life of ENS Paris-Saclay-Saclay or involving its students." +msgstr "" + +#: photo21/templates/index.html:18 +#, python-format +msgid "" +"The pictures are visible in <a href=\"%(gallery_archive_url)s\">the " +"galleries</a> and are downloadable. <b>However, the agreement of the " +"photographer and the persons present on the photo is necessary before any " +"republication on another platform. </b>" +msgstr "" + +#: photo21/templates/index.html:27 +msgid "" +"If you want a photo to be deleted, please let us know: <a href=\"mailto:" +"photos@crans.org?subject=[ABUS] Nouvelle requête\" class=\"btn btn-dark btn-" +"sm\">Abuse request</a>" +msgstr "" + +#: photo21/templates/index.html:33 +msgid "" +"If you want to obtain the right to upload pictures, please let us know: <a " +"href=\"mailto:photos@crans.org?subject=[Photographe] Demande de droits " +"photographe\" class=\"btn btn-dark btn-sm\">Become a photograph</a>" +msgstr "" + +#: photo21/templates/index.html:39 +msgid "Last galleries" +msgstr "" + +#: photo21/templates/index.html:50 +msgid "Connected as" +msgstr "" + +#: photo21/templates/index.html:53 +msgid "Select another language:" +msgstr "" + +#: photo21/templates/socialaccount/connections.html:6 +msgid "Account Connections" +msgstr "" + +#: photo21/templates/socialaccount/connections.html:23 +msgid "" +"You can sign in to your account using any of the following third party " +"accounts:" +msgstr "" + +#: photo21/templates/socialaccount/connections.html:51 +msgid "" +"You currently have no social network accounts connected to this account." +msgstr "" + +#: photo21/templates/socialaccount/connections.html:54 +msgid "Add a 3rd Party Account" +msgstr "" diff --git a/photo21/locale/es/LC_MESSAGES/django.po b/photo21/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000000000000000000000000000000000000..a9413d9859ec28fab5b5de5874fe696285a4db44 --- /dev/null +++ b/photo21/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,314 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-01-29 21:58+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: photo21/forms.py:12 +msgid "" +"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" +"saclay.fr`." +msgstr "" + +#: photo21/forms.py:23 +msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." +msgstr "" + +#: photo21/hashers.py:42 +msgid "algorithm" +msgstr "" + +#: photo21/hashers.py:43 +msgid "salt" +msgstr "" + +#: photo21/hashers.py:44 +msgid "hash" +msgstr "" + +#: photo21/settings.py:162 +msgid "German" +msgstr "" + +#: photo21/settings.py:163 +msgid "English" +msgstr "" + +#: photo21/settings.py:164 +msgid "Spanish" +msgstr "" + +#: photo21/settings.py:165 +msgid "French" +msgstr "" + +#: photo21/templates/400.html:10 +msgid "Bad request" +msgstr "" + +#: photo21/templates/400.html:14 +msgid "" +"Sorry, your request was bad. Don't know what could be wrong. An email has " +"been sent to webmasters with the details of the error. You can now drink a " +"coke." +msgstr "" + +#: photo21/templates/403.html:10 +msgid "Permission denied" +msgstr "" + +#: photo21/templates/403.html:13 +msgid "You don't have the right to perform this request." +msgstr "" + +#: photo21/templates/403.html:15 photo21/templates/404.html:19 +msgid "Exception message:" +msgstr "" + +#: photo21/templates/404.html:10 +msgid "Page not found" +msgstr "" + +#: photo21/templates/404.html:14 +#, python-format +msgid "" +"The requested path <code>%(request_path)s</code> was not found on the server." +msgstr "" + +#: photo21/templates/500.html:10 +msgid "Server error" +msgstr "" + +#: photo21/templates/500.html:14 +msgid "" +"Sorry, an error occurred when processing your request. An email has been " +"sent to webmasters with the detail of the error, and this will be fixed " +"soon. You can now drink a beer." +msgstr "" + +#: photo21/templates/account/email.html:6 +#: photo21/templates/account/email.html:14 +#: photo21/templates/socialaccount/connections.html:14 +msgid "E-mail Addresses" +msgstr "" + +#: photo21/templates/account/email.html:9 photo21/templates/base.html:59 +#: photo21/templates/socialaccount/connections.html:9 +msgid "Account" +msgstr "" + +#: photo21/templates/account/email.html:17 +#: photo21/templates/socialaccount/connections.html:17 +msgid "Social connections" +msgstr "" + +#: photo21/templates/account/email.html:23 +msgid "The following e-mail addresses are associated with your account:" +msgstr "" + +#: photo21/templates/account/email.html:34 +msgid "Verified" +msgstr "" + +#: photo21/templates/account/email.html:36 +msgid "Unverified" +msgstr "" + +#: photo21/templates/account/email.html:38 +msgid "Primary" +msgstr "" + +#: photo21/templates/account/email.html:44 +msgid "Make Primary" +msgstr "" + +#: photo21/templates/account/email.html:45 +msgid "Re-send Verification" +msgstr "" + +#: photo21/templates/account/email.html:46 +#: photo21/templates/socialaccount/connections.html:45 +msgid "Remove" +msgstr "" + +#: photo21/templates/account/email.html:51 +msgid "Warning:" +msgstr "" + +#: photo21/templates/account/email.html:51 +msgid "" +"You currently do not have any e-mail address set up. You should really add " +"an e-mail address so you can receive notifications, reset your password, etc." +msgstr "" + +#: photo21/templates/account/email.html:55 +msgid "Add E-mail Address" +msgstr "" + +#: photo21/templates/account/email.html:60 +msgid "Add E-mail" +msgstr "" + +#: photo21/templates/account/email.html:68 +msgid "Do you really want to remove the selected e-mail address?" +msgstr "" + +#: photo21/templates/account/login.html:6 +#: photo21/templates/account/login.html:11 +#: photo21/templates/account/login.html:39 +msgid "Sign In" +msgstr "" + +#: photo21/templates/account/login.html:16 +#, python-format +msgid "" +"Please sign in with one of your existing third party accounts. Or, <a href=" +"\"%(signup_url)s\">sign up</a> for a %(site_name)s account and sign in below:" +msgstr "" + +#: photo21/templates/account/login.html:24 +msgid "or" +msgstr "" + +#: photo21/templates/account/login.html:30 +#, python-format +msgid "" +"If you have not created an account yet, then please <a href=\"%(signup_url)s" +"\">sign up</a> first." +msgstr "" + +#: photo21/templates/account/login.html:40 +msgid "Forgot Password?" +msgstr "" + +#: photo21/templates/account/login.html:44 +msgid "If any problem, please contact the server owners at" +msgstr "" + +#: photo21/templates/account/signup.html:6 +msgid "Signup" +msgstr "" + +#: photo21/templates/account/signup.html:11 +#: photo21/templates/account/signup.html:22 +msgid "Sign Up" +msgstr "" + +#: photo21/templates/account/signup.html:14 +#, python-format +msgid "" +"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a>." +msgstr "" + +#: photo21/templates/base.html:12 +msgid "The ENS Paris-Saclay pictures server." +msgstr "" + +#: photo21/templates/base.html:37 +msgid "Galleries" +msgstr "" + +#: photo21/templates/base.html:42 +msgid "Upload" +msgstr "" + +#: photo21/templates/base.html:47 +msgid "Manage" +msgstr "" + +#: photo21/templates/base.html:68 +msgid "Log out" +msgstr "" + +#: photo21/templates/base.html:78 +msgid "Log in" +msgstr "" + +#: photo21/templates/base.html:87 +msgid "Sign up" +msgstr "" + +#: photo21/templates/base.html:109 +msgid "Source code" +msgstr "" + +#: photo21/templates/index.html:6 +msgid "Home" +msgstr "" + +#: photo21/templates/index.html:9 +msgid "Welcome to the pictures server!" +msgstr "" + +#: photo21/templates/index.html:11 +msgid "" +"This website aims to collect the pictures and movies taken in the student " +"life of ENS Paris-Saclay-Saclay or involving its students." +msgstr "" + +#: photo21/templates/index.html:18 +#, python-format +msgid "" +"The pictures are visible in <a href=\"%(gallery_archive_url)s\">the " +"galleries</a> and are downloadable. <b>However, the agreement of the " +"photographer and the persons present on the photo is necessary before any " +"republication on another platform. </b>" +msgstr "" + +#: photo21/templates/index.html:27 +msgid "" +"If you want a photo to be deleted, please let us know: <a href=\"mailto:" +"photos@crans.org?subject=[ABUS] Nouvelle requête\" class=\"btn btn-dark btn-" +"sm\">Abuse request</a>" +msgstr "" + +#: photo21/templates/index.html:33 +msgid "" +"If you want to obtain the right to upload pictures, please let us know: <a " +"href=\"mailto:photos@crans.org?subject=[Photographe] Demande de droits " +"photographe\" class=\"btn btn-dark btn-sm\">Become a photograph</a>" +msgstr "" + +#: photo21/templates/index.html:39 +msgid "Last galleries" +msgstr "" + +#: photo21/templates/index.html:50 +msgid "Connected as" +msgstr "" + +#: photo21/templates/index.html:53 +msgid "Select another language:" +msgstr "" + +#: photo21/templates/socialaccount/connections.html:6 +msgid "Account Connections" +msgstr "" + +#: photo21/templates/socialaccount/connections.html:23 +msgid "" +"You can sign in to your account using any of the following third party " +"accounts:" +msgstr "" + +#: photo21/templates/socialaccount/connections.html:51 +msgid "" +"You currently have no social network accounts connected to this account." +msgstr "" + +#: photo21/templates/socialaccount/connections.html:54 +msgid "Add a 3rd Party Account" +msgstr "" diff --git a/photo21/locale/fr/LC_MESSAGES/django.po b/photo21/locale/fr/LC_MESSAGES/django.po index fb7f80c606d06d4342a31337ab770a389f4ef551..ac7970b1c2438380699a5a99f4296d0ca3b30525 100644 --- a/photo21/locale/fr/LC_MESSAGES/django.po +++ b/photo21/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-10-23 15:29+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 <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -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,169 +225,156 @@ 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 photologue_custom/forms.py:76 -#: photologue_custom/templates/photologue/upload.html:6 -#: photologue_custom/templates/photologue/upload.html:54 +#: 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:107 +#: photo21/templates/base.html:109 msgid "Source code" msgstr "Code source" -#: photo21/templates/index.html:49 -msgid "Connected as" -msgstr "Connecté en tant que" +#: photo21/templates/index.html:6 +msgid "Home" +msgstr "Accueil" -#: photo21/templates/socialaccount/connections.html:6 -msgid "Account Connections" -msgstr "" +#: photo21/templates/index.html:9 +msgid "Welcome to the pictures server!" +msgstr "Bienvenue sur le serveur photos !" -#: photo21/templates/socialaccount/connections.html:23 +#: photo21/templates/index.html:11 msgid "" -"You can sign in to your account using any of the following third party " -"accounts:" +"This website aims to collect the pictures and movies taken in the student " +"life of ENS Paris-Saclay-Saclay or involving its students." msgstr "" +"Ce site à pour objectif de recenser les photos et films pris dans la vie " +"associative de l'ENS Paris-Saclay ou impliquant des <b>K</b>chanais." -#: photo21/templates/socialaccount/connections.html:51 +#: photo21/templates/index.html:18 +#, python-format msgid "" -"You currently have no social network accounts connected to this account." +"The pictures are visible in <a href=\"%(gallery_archive_url)s\">the " +"galleries</a> and are downloadable. <b>However, the agreement of the " +"photographer and the persons present on the photo is necessary before any " +"republication on another platform. </b>" msgstr "" +"Les photos sont visibles dans <a href=\"%(gallery_archive_url)s\">les " +"galeries</a> et sont téléchargeables. <b>Toutefois, l'accord de la ou du " +"photographe et des personnes présentes sur la photo est nécessaire avant " +"toute republication sur un autre site.</b>" -#: photo21/templates/socialaccount/connections.html:54 -msgid "Add a 3rd Party Account" +#: photo21/templates/index.html:27 +msgid "" +"If you want a photo to be deleted, please let us know: <a href=\"mailto:" +"photos@crans.org?subject=[ABUS] Nouvelle requête\" class=\"btn btn-dark btn-" +"sm\">Abuse request</a>" msgstr "" +"Si vous souhaitez qu'une photo soit supprimée, signalez le nous : <a href=" +"\"mailto:photos@crans.org?subject=[ABUS] Nouvelle requête\" class=\"btn btn-" +"dark btn-sm\">Signaler un abus</a>" -#: photologue_custom/admin.py:45 photologue_custom/models.py:51 -msgid "owner" -msgstr "propriétaire" - -#: photologue_custom/forms.py:34 -msgid "Gallery" -msgstr "Galerie" - -#: photologue_custom/forms.py:36 -msgid "-- Create a new gallery --" -msgstr "-- Créer une nouvelle galerie --" - -#: photologue_custom/forms.py:37 +#: photo21/templates/index.html:33 msgid "" -"Select a gallery to add these images to. Leave this empty to create a new " -"gallery from the supplied title." +"If you want to obtain the right to upload pictures, please let us know: <a " +"href=\"mailto:photos@crans.org?subject=[Photographe] Demande de droits " +"photographe\" class=\"btn btn-dark btn-sm\">Become a photograph</a>" msgstr "" +"Si vous souhaitez obtenir les droits photographes pour téléverser vos " +"photos, signalez le nous : <a href=\"mailto:photos@crans.org?" +"subject=[Photographe] Demande de droits photographe\" class=\"btn btn-dark " +"btn-sm\">Devenir photographe</a>" -#: photologue_custom/forms.py:41 -msgid "New gallery title" -msgstr "Titre de la nouvelle galerie" +#: photo21/templates/index.html:39 +msgid "Last galleries" +msgstr "Galeries récentes" -#: photologue_custom/forms.py:46 -msgid "New gallery event start date" -msgstr "Date de début de l'évènement de la nouvelle galerie" +#: photo21/templates/index.html:50 +msgid "Connected as" +msgstr "Connecté en tant que" -#: photologue_custom/forms.py:51 -msgid "New gallery event end date" -msgstr "Date de fin de l'évènement de la nouvelle galerie" +#: photo21/templates/index.html:53 +msgid "Select another language:" +msgstr "Changer de langue :" -#: photologue_custom/forms.py:57 -msgid "New gallery tags" -msgstr "Tags de la nouvelle galerie" +#: photo21/templates/socialaccount/connections.html:6 +msgid "Account Connections" +msgstr "" -#: photologue_custom/forms.py:59 +#: photo21/templates/socialaccount/connections.html:23 msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +"You can sign in to your account using any of the following third party " +"accounts:" msgstr "" -#: photologue_custom/forms.py:82 -msgid "A gallery with that title already exists." +#: photo21/templates/socialaccount/connections.html:51 +msgid "" +"You currently have no social network accounts connected to this account." msgstr "" -#: photologue_custom/forms.py:91 -msgid "Select an existing gallery, or enter a title for a new gallery." +#: photo21/templates/socialaccount/connections.html:54 +msgid "Add a 3rd Party Account" msgstr "" -#: photologue_custom/models.py:23 -msgid "start date" -msgstr "date de début" +#~ msgid "owner" +#~ msgstr "propriétaire" -#: photologue_custom/models.py:28 -msgid "end date" -msgstr "date de fin" +#~ msgid "Gallery" +#~ msgstr "Galerie" -#: photologue_custom/models.py:56 -msgid "license" -msgstr "licence" +#~ msgid "-- Create a new gallery --" +#~ msgstr "-- Créer une nouvelle galerie --" -#: photologue_custom/templates/photologue/gallery_archive.html:7 -#: photologue_custom/templates/photologue/gallery_archive.html:12 -msgid "Latest photo galleries" -msgstr "" +#~ msgid "New gallery title" +#~ msgstr "Titre de la nouvelle galerie" -#: photologue_custom/templates/photologue/gallery_archive.html:18 -msgid "Filter by year" -msgstr "" +#~ msgid "New gallery event start date" +#~ msgstr "Date de début de l'évènement de la nouvelle galerie" -#: photologue_custom/templates/photologue/gallery_archive.html:35 -msgid "No galleries were found" -msgstr "" - -#: 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 "" +#~ msgid "New gallery event end date" +#~ msgstr "Date de fin de l'évènement de la nouvelle galerie" -#: photologue_custom/templates/photologue/gallery_archive_year.html:17 -msgid "View all galleries" -msgstr "" +#~ msgid "New gallery tags" +#~ msgstr "Tags de la nouvelle galerie" -#: photologue_custom/templates/photologue/gallery_archive_year.html:29 -msgid "No galleries were found." -msgstr "" +#~ msgid "start date" +#~ msgstr "date de début" -#: photologue_custom/templates/photologue/gallery_detail.html:38 -msgid "to" -msgstr "au" +#~ msgid "end date" +#~ msgstr "date de fin" -#: photologue_custom/templates/photologue/gallery_detail.html:54 -msgid "All pictures" -msgstr "Toutes les photos" +#~ msgid "license" +#~ msgstr "licence" -#: photologue_custom/templates/photologue/gallery_detail.html:75 -msgid "Download all gallery" -msgstr "Télécharger toute la galerie" +#~ msgid "to" +#~ msgstr "au" -#: photologue_custom/templates/photologue/photo_detail.html:13 -msgid "Published" -msgstr "" +#~ msgid "All pictures" +#~ msgstr "Toutes les photos" -#: photologue_custom/templates/photologue/photo_detail.html:25 -msgid "This photo is found in the following galleries" -msgstr "" +#~ msgid "Download all gallery" +#~ msgstr "Télécharger toute la galerie" -#: photologue_custom/templates/photologue/upload.html:59 -msgid "Drag and drop photos here" -msgstr "Glissez et déposez les photos ici" +#~ msgid "Drag and drop photos here" +#~ msgstr "Glissez et déposez les photos ici" -#: photologue_custom/templates/photologue/upload.html:63 -msgid "Owner will be" -msgstr "Le propriétaire sera" +#~ msgid "Owner will be" +#~ msgstr "Le propriétaire sera" diff --git a/photo21/settings.py b/photo21/settings.py index 33d4a47ee16c34251e5710a73feabc00fc27e0a1..5c61f4242143544482c5abf870dfc3246731b2d8 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/static/bootstrap5/css/bootstrap-dark-plugin.min.css b/photo21/static/bootstrap5/css/bootstrap-dark-plugin.min.css new file mode 100644 index 0000000000000000000000000000000000000000..e8bb4e2d9e9b32d4eb41990f427a59553d896f1b --- /dev/null +++ b/photo21/static/bootstrap5/css/bootstrap-dark-plugin.min.css @@ -0,0 +1,12 @@ +/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * + * Bootstrap-Dark-plugin v1.1.3 (https://vinorodrigues.github.io/bootstrap-dark-5/) + * Copyright 2020-2021 Vino Rodrigues + * This version is a dual color theme, but as a plugin + * Include both bootstrap.css and this file and darkmode is optioned in by a + * `prefers-color-scheme: dark` media query + */:root{color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--bs-blue:#375a7f;--bs-indigo:#673ab7;--bs-purple:#654ea3;--bs-pink:#e83e8c;--bs-red:#e74c3c;--bs-orange:#fd7e14;--bs-yellow:#f39c12;--bs-green:#00bc8c;--bs-teal:#45b5aa;--bs-cyan:#17a2b8;--bs-white:#fafafa;--bs-black:#111;--bs-gray:#7e7e7e;--bs-gray-dark:#121212;--bs-gray-100:#e1e1e1;--bs-gray-200:#cfcfcf;--bs-gray-300:#b1b1b1;--bs-gray-400:#9e9e9e;--bs-gray-500:#7e7e7e;--bs-gray-600:#626262;--bs-gray-700:#515151;--bs-gray-800:#3b3b3b;--bs-gray-900:#222;--bs-primary:#375a7f;--bs-secondary:#626262;--bs-success:#00bc8c;--bs-info:#17a2b8;--bs-warning:#f39c12;--bs-danger:#e74c3c;--bs-light:#9e9e9e;--bs-dark:#3b3b3b;--bs-primary-rgb:55,90,127;--bs-secondary-rgb:98,98,98;--bs-success-rgb:0,188,140;--bs-info-rgb:23,162,184;--bs-warning-rgb:243,156,18;--bs-danger-rgb:231,76,60;--bs-light-rgb:158,158,158;--bs-dark-rgb:59,59,59;--bs-white-rgb:250,250,250;--bs-black-rgb:17,17,17;--bs-body-color-rgb:225,225,225;--bs-body-bg-rgb:34,34,34;--bs-body-color:#e1e1e1;--bs-body-bg:#222;--bs-gradient:linear-gradient(180deg, rgba(17, 17, 17, 0.15), rgba(17, 17, 17, 0))}hr{color:#fafafa;background-color:currentColor;opacity:.1}mark{background-color:rgba(243,156,18,.5)}a{color:#557392}a:hover{color:#778fa8}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit}pre{color:#e1e1e1}pre code{color:inherit}code{color:#45b5aa}a>code{color:inherit}kbd{color:#e1e1e1;background-color:#3b3b3b}caption{color:#9e9e9e}.blockquote-footer{color:#626262}.img-thumbnail{background-color:#222;border:1px solid #515151}.figure-caption{color:#9e9e9e}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#e1e1e1;--bs-table-striped-bg:rgba(250, 250, 250, 0.05);--bs-table-active-color:#e1e1e1;--bs-table-active-bg:rgba(250, 250, 250, 0.1);--bs-table-hover-color:#e1e1e1;--bs-table-hover-bg:rgba(250, 250, 250, 0.075);color:#e1e1e1;border-color:#515151}.table>:not(:first-child){border-top:2px solid currentColor}.table-primary{--bs-table-bg:#1c2d40;--bs-table-striped-bg:#27384a;--bs-table-striped-color:#fff;--bs-table-active-bg:#334253;--bs-table-active-color:#fff;--bs-table-hover-bg:#2d3d4e;--bs-table-hover-color:#fff;color:#fff;border-color:#334253}.table-secondary{--bs-table-bg:#313131;--bs-table-striped-bg:#3b3b3b;--bs-table-striped-color:#fff;--bs-table-active-bg:#464646;--bs-table-active-color:#fff;--bs-table-hover-bg:#404040;--bs-table-hover-color:#fff;color:#fff;border-color:#464646}.table-success{--bs-table-bg:#005e46;--bs-table-striped-bg:#0d664f;--bs-table-striped-color:#fff;--bs-table-active-bg:#1a6e59;--bs-table-active-color:#fff;--bs-table-hover-bg:#136a54;--bs-table-hover-color:#fff;color:#fff;border-color:#1a6e59}.table-info{--bs-table-bg:#0c515c;--bs-table-striped-bg:#185a64;--bs-table-striped-color:#fff;--bs-table-active-bg:#24626c;--bs-table-active-color:#fff;--bs-table-hover-bg:#1e5e68;--bs-table-hover-color:#fff;color:#fff;border-color:#24626c}.table-warning{--bs-table-bg:#7a4e09;--bs-table-striped-bg:#815715;--bs-table-striped-color:#fff;--bs-table-active-bg:#876022;--bs-table-active-color:#fff;--bs-table-hover-bg:#845b1b;--bs-table-hover-color:#fff;color:#fff;border-color:#876022}.table-danger{--bs-table-bg:#74261e;--bs-table-striped-bg:#7b3129;--bs-table-striped-color:#fff;--bs-table-active-bg:#823c35;--bs-table-active-color:#fff;--bs-table-hover-bg:#7e362f;--bs-table-hover-color:#fff;color:#fff;border-color:#823c35}.table-light{--bs-table-bg:#9e9e9e;--bs-table-striped-bg:#969696;--bs-table-striped-color:#000;--bs-table-active-bg:#8e8e8e;--bs-table-active-color:#000;--bs-table-hover-bg:#929292;--bs-table-hover-color:#000;color:#000;border-color:#8e8e8e}.table-dark{--bs-table-bg:#3b3b3b;--bs-table-striped-bg:#454545;--bs-table-striped-color:#fff;--bs-table-active-bg:#4f4f4f;--bs-table-active-color:#fff;--bs-table-hover-bg:#4a4a4a;--bs-table-hover-color:#fff;color:#fff;border-color:#4f4f4f}.form-text{color:#9e9e9e}.form-control{color:#b1b1b1;background-color:#222;border:1px solid #515151}.form-control:focus{color:#b1b1b1;background-color:#222;border-color:#9badbf;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.form-control::-moz-placeholder{color:#5a5a5a}.form-control:-ms-input-placeholder{color:#5a5a5a}.form-control::placeholder{color:#5a5a5a}.form-control:disabled,.form-control[readonly]{background-color:#222}.form-control::-webkit-file-upload-button{color:#b1b1b1;background-color:#3b3b3b;border-color:inherit}.form-control::file-selector-button{color:#b1b1b1;background-color:#3b3b3b;border-color:inherit}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#383838}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#383838}.form-control::-webkit-file-upload-button{color:#b1b1b1;background-color:#3b3b3b;border-color:inherit}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#383838}.form-control-plaintext{color:#e1e1e1;background-color:transparent;border:solid transparent}.form-select{color:#b1b1b1;background-color:#222;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23cfcfcf' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");border:1px solid #515151}.form-select:focus{border-color:#9badbf;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){background-image:none}.form-select:disabled{background-color:#3b3b3b}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #b1b1b1}.form-check-input{background-color:#222;border:1px solid rgba(255,255,255,.25)}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#9badbf;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.form-check-input:checked{background-color:#375a7f;border-color:#375a7f}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fafafa' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fafafa'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#375a7f;border-color:#375a7f;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fafafa' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch .form-check-input{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28250, 250, 250, 0.25%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%239badbf'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fafafa'/%3e%3c/svg%3e")}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{opacity:.65}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #222,0 0 0 .25rem rgba(55,90,127,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 .25rem rgba(55,90,127,.25)}.form-range::-webkit-slider-thumb{background-color:#375a7f;border:0}.form-range::-webkit-slider-thumb:active{background-color:#c3ced9}.form-range::-webkit-slider-runnable-track{background-color:#515151}.form-range::-moz-range-thumb{background-color:#375a7f;border:0}.form-range::-moz-range-thumb:active{background-color:#c3ced9}.form-range::-moz-range-track{background-color:#515151}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#7e7e7e}.form-range:disabled::-moz-range-thumb{background-color:#7e7e7e}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control:-ms-input-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65}.form-floating>.form-control:not(:-ms-input-placeholder)~label{opacity:.65}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65}.form-floating>.form-control:-webkit-autofill~label{opacity:.65}.input-group-text{color:#b1b1b1;background-color:#3b3b3b;border:1px solid #515151}.valid-feedback{color:#00bc8c}.valid-tooltip{color:#111;background-color:rgba(0,188,140,.9)}.form-control.is-valid,.was-validated .form-control:valid{border-color:#00bc8c;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e")}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#00bc8c;box-shadow:0 0 0 .25rem rgba(0,188,140,.25)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#00bc8c}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23cfcfcf' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e")}.form-select.is-valid[multiple],.form-select.is-valid[size]:not([size="1"]),.was-validated .form-select:valid[multiple],.was-validated .form-select:valid[size]:not([size="1"]){background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e")}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#00bc8c;box-shadow:0 0 0 .25rem rgba(0,188,140,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#00bc8c}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#00bc8c}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(0,188,140,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#00bc8c}.invalid-feedback{color:#e74c3c}.invalid-tooltip{color:#fafafa;background-color:rgba(231,76,60,.9)}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#e74c3c;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23e74c3c'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e")}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#e74c3c;box-shadow:0 0 0 .25rem rgba(231,76,60,.25)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#e74c3c}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23cfcfcf' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23e74c3c'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e")}.form-select.is-invalid[multiple],.form-select.is-invalid[size]:not([size="1"]),.was-validated .form-select:invalid[multiple],.was-validated .form-select:invalid[size]:not([size="1"]){background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23e74c3c'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e")}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#e74c3c;box-shadow:0 0 0 .25rem rgba(231,76,60,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#e74c3c}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#e74c3c}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(231,76,60,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#e74c3c}.btn{color:#e1e1e1;background-color:transparent;border:1px solid transparent}.btn:hover{color:#e1e1e1}.btn-check:focus+.btn,.btn:focus{box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{opacity:.65}.btn-primary{color:#fafafa;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fafafa;background-color:#2f4d6c;border-color:#2c4866}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fafafa;background-color:#2f4d6c;border-color:#2c4866;box-shadow:0 0 0 .25rem rgba(84,114,145,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fafafa;background-color:#2c4866;border-color:#29445f}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(84,114,145,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fafafa;background-color:#375a7f;border-color:#375a7f}.btn-secondary{color:#fafafa;background-color:#626262;border-color:#626262}.btn-secondary:hover{color:#fafafa;background-color:#535353;border-color:#4e4e4e}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fafafa;background-color:#535353;border-color:#4e4e4e;box-shadow:0 0 0 .25rem rgba(121,121,121,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fafafa;background-color:#4e4e4e;border-color:#4a4a4a}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(121,121,121,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fafafa;background-color:#626262;border-color:#626262}.btn-success{color:#111;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#111;background-color:#26c69d;border-color:#1ac398}.btn-check:focus+.btn-success,.btn-success:focus{color:#111;background-color:#26c69d;border-color:#1ac398;box-shadow:0 0 0 .25rem rgba(3,162,122,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#111;background-color:#33c9a3;border-color:#1ac398}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(3,162,122,.5)}.btn-success.disabled,.btn-success:disabled{color:#111;background-color:#00bc8c;border-color:#00bc8c}.btn-info{color:#fafafa;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fafafa;background-color:#148a9c;border-color:#128293}.btn-check:focus+.btn-info,.btn-info:focus{color:#fafafa;background-color:#148a9c;border-color:#128293;box-shadow:0 0 0 .25rem rgba(57,175,194,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fafafa;background-color:#128293;border-color:#117a8a}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(57,175,194,.5)}.btn-info.disabled,.btn-info:disabled{color:#fafafa;background-color:#17a2b8;border-color:#17a2b8}.btn-warning{color:#111;background-color:#f39c12;border-color:#f39c12}.btn-warning:hover{color:#111;background-color:#f5ab36;border-color:#f4a62a}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#111;background-color:#f5ab36;border-color:#f4a62a;box-shadow:0 0 0 .25rem rgba(209,135,18,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#111;background-color:#f5b041;border-color:#f4a62a}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(209,135,18,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#111;background-color:#f39c12;border-color:#f39c12}.btn-danger{color:#fafafa;background-color:#e74c3c;border-color:#e74c3c}.btn-danger:hover{color:#fafafa;background-color:#c44133;border-color:#b93d30}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fafafa;background-color:#c44133;border-color:#b93d30;box-shadow:0 0 0 .25rem rgba(234,102,89,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fafafa;background-color:#b93d30;border-color:#ad392d}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(234,102,89,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fafafa;background-color:#e74c3c;border-color:#e74c3c}.btn-light{color:#fafafa;background-color:#9e9e9e;border-color:#9e9e9e}.btn-light:hover{color:#fafafa;background-color:#868686;border-color:#7e7e7e}.btn-check:focus+.btn-light,.btn-light:focus{color:#fafafa;background-color:#868686;border-color:#7e7e7e;box-shadow:0 0 0 .25rem rgba(172,172,172,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#fafafa;background-color:#7e7e7e;border-color:#777}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(172,172,172,.5)}.btn-light.disabled,.btn-light:disabled{color:#fafafa;background-color:#9e9e9e;border-color:#9e9e9e}.btn-dark{color:#fafafa;background-color:#3b3b3b;border-color:#3b3b3b}.btn-dark:hover{color:#fafafa;background-color:#323232;border-color:#2f2f2f}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fafafa;background-color:#323232;border-color:#2f2f2f;box-shadow:0 0 0 .25rem rgba(88,88,88,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fafafa;background-color:#2f2f2f;border-color:#2c2c2c}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(88,88,88,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fafafa;background-color:#3b3b3b;border-color:#3b3b3b}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(55,90,127,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(55,90,127,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-secondary{color:#626262;border-color:#626262}.btn-outline-secondary:hover{color:#fff;background-color:#626262;border-color:#626262}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(98,98,98,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#626262;border-color:#626262}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(98,98,98,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#626262;background-color:transparent}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#000;background-color:#00bc8c;border-color:#00bc8c}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(0,188,140,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#000;background-color:#00bc8c;border-color:#00bc8c}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(0,188,140,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#000;background-color:#17a2b8;border-color:#17a2b8}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(23,162,184,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#17a2b8;border-color:#17a2b8}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-warning{color:#f39c12;border-color:#f39c12}.btn-outline-warning:hover{color:#000;background-color:#f39c12;border-color:#f39c12}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(243,156,18,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#f39c12;border-color:#f39c12}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(243,156,18,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f39c12;background-color:transparent}.btn-outline-danger{color:#e74c3c;border-color:#e74c3c}.btn-outline-danger:hover{color:#000;background-color:#e74c3c;border-color:#e74c3c}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(231,76,60,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#000;background-color:#e74c3c;border-color:#e74c3c}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(231,76,60,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#e74c3c;background-color:transparent}.btn-outline-light{color:#9e9e9e;border-color:#9e9e9e}.btn-outline-light:hover{color:#000;background-color:#9e9e9e;border-color:#9e9e9e}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(158,158,158,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#9e9e9e;border-color:#9e9e9e}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(158,158,158,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#9e9e9e;background-color:transparent}.btn-outline-dark{color:#3b3b3b;border-color:#3b3b3b}.btn-outline-dark:hover{color:#fff;background-color:#3b3b3b;border-color:#3b3b3b}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(59,59,59,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#3b3b3b;border-color:#3b3b3b}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(59,59,59,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#3b3b3b;background-color:transparent}.btn-link{color:#557392}.btn-link:hover{color:#778fa8}.btn-link.disabled,.btn-link:disabled{color:#626262}.dropdown-menu{color:#e1e1e1;background-color:#111;border:1px solid rgba(250,250,250,.15)}.dropdown-divider{border-top:1px solid rgba(250,250,250,.15)}.dropdown-item{color:#f8f9fa;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#dfe0e1;background-color:#343a40}.dropdown-item.active,.dropdown-item:active{color:#fafafa;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#7e7e7e;background-color:transparent}.dropdown-header{color:#9e9e9e}.dropdown-item-text{color:#f8f9fa}.dropdown-menu-dark{color:#515151;background-color:#cfcfcf;border-color:rgba(250,250,250,.15)}.dropdown-menu-dark .dropdown-item{color:#515151}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#111;background-color:rgba(17,17,17,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fafafa;background-color:#375a7f}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#7e7e7e}.dropdown-menu-dark .dropdown-divider{border-color:rgba(250,250,250,.15)}.dropdown-menu-dark .dropdown-item-text{color:#515151}.dropdown-menu-dark .dropdown-header{color:#7e7e7e}.nav-link{color:#557392}.nav-link:focus,.nav-link:hover{color:#778fa8}.nav-link.disabled{color:#9e9e9e}.nav-tabs{border-bottom:1px solid #515151}.nav-tabs .nav-link{border:1px solid transparent}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#3b3b3b #3b3b3b #515151}.nav-tabs .nav-link.disabled{color:#9e9e9e;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#b1b1b1;background-color:#222;border-color:#515151 #515151 #222}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fafafa;background-color:#375a7f}.navbar-toggler{background-color:transparent;border:1px solid transparent}.navbar-light .navbar-brand{color:rgba(250,250,250,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(250,250,250,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(250,250,250,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(250,250,250,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(250,250,250,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(250,250,250,.9)}.navbar-light .navbar-toggler{color:rgba(250,250,250,.55);border-color:rgba(250,250,250,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28250, 250, 250, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(250,250,250,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(250,250,250,.9)}.navbar-dark .navbar-brand{color:#fafafa}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fafafa}.navbar-dark .navbar-nav .nav-link{color:rgba(250,250,250,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(250,250,250,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(250,250,250,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fafafa}.navbar-dark .navbar-toggler{color:rgba(250,250,250,.55);border-color:rgba(250,250,250,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28250, 250, 250, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(250,250,250,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fafafa}.card{background-color:#222;border:1px solid rgba(250,250,250,.125)}.card-header{background-color:rgba(250,250,250,.03);border-bottom:1px solid rgba(250,250,250,.125)}.card-footer{background-color:rgba(250,250,250,.03);border-top:1px solid rgba(250,250,250,.125)}.accordion-button{color:#e1e1e1;background-color:#222}.accordion-button:not(.collapsed){color:#879cb2;background-color:#1c2d40;box-shadow:inset 0 -1px 0 rgba(250,250,250,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23879cb2'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.accordion-button::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23e1e1e1'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.accordion-button:hover{z-index:2}.accordion-button:focus{border-color:#9badbf;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.accordion-item{background-color:#222;border:1px solid rgba(250,250,250,.125)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.breadcrumb-item+.breadcrumb-item::before{color:#9e9e9e}.breadcrumb-item.active{color:#9e9e9e}.page-link{color:#557392;background-color:#3b3b3b;border:1px solid #515151}.page-link:hover{color:#778fa8;background-color:#515151;border-color:#515151}.page-link:focus{color:#778fa8;background-color:#3b3b3b;outline:0;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.page-item.active .page-link{color:#fafafa;background-color:#375a7f;border-color:#375a7f}.page-item.disabled .page-link{color:#5a5a5a;background-color:#222;border-color:#515151}.badge{color:#fafafa}.alert-heading{color:inherit}.alert-primary{color:#738ca5;background-color:#1c2d40;border-color:#21364c}.alert-primary .alert-link{color:#5c7084}.alert-secondary{color:#919191;background-color:#313131;border-color:#3b3b3b}.alert-secondary .alert-link{color:#747474}.alert-success{color:#4dd0af;background-color:#005e46;border-color:#007154}.alert-success .alert-link{color:#3ea68c}.alert-info{color:#5dbecd;background-color:#0c515c;border-color:#0e616e}.alert-info .alert-link{color:#4a98a4}.alert-warning{color:#f7ba59;background-color:#7a4e09;border-color:#925e0b}.alert-warning .alert-link{color:#c69547}.alert-danger{color:#ee8277;background-color:#74261e;border-color:#8b2e24}.alert-danger .alert-link{color:#be685f}.alert-light{color:#bbb;background-color:#4f4f4f;border-color:#5f5f5f}.alert-light .alert-link{color:#969696}.alert-dark{color:#767676;background-color:#1e1e1e;border-color:#232323}.alert-dark .alert-link{color:#5e5e5e}.progress{background-color:#3b3b3b}.progress-bar{color:#111;background-color:#375a7f}.list-group-item-action{color:#b1b1b1}.list-group-item-action:focus,.list-group-item-action:hover{color:#b1b1b1;background-color:#2f2f2f}.list-group-item-action:active{color:#e1e1e1;background-color:#3b3b3b}.list-group-item{color:#e1e1e1;background-color:#222;border:1px solid rgba(250,250,250,.125)}.list-group-item.disabled,.list-group-item:disabled{color:#9e9e9e;background-color:#222}.list-group-item.active{color:#fafafa;background-color:#375a7f;border-color:#375a7f}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#738ca5;background-color:#1c2d40}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#738ca5;background-color:#19293a}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#738ca5;border-color:#738ca5}.list-group-item-secondary{color:#919191;background-color:#313131}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#919191;background-color:#2c2c2c}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#919191;border-color:#919191}.list-group-item-success{color:#4dd0af;background-color:#005e46}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#4dd0af;background-color:#00553f}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#4dd0af;border-color:#4dd0af}.list-group-item-info{color:#5dbecd;background-color:#0c515c}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#5dbecd;background-color:#0b4953}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#5dbecd;border-color:#5dbecd}.list-group-item-warning{color:#f7ba59;background-color:#7a4e09}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#f7ba59;background-color:#6e4608}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#f7ba59;border-color:#f7ba59}.list-group-item-danger{color:#ee8277;background-color:#74261e}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#ee8277;background-color:#68221b}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#ee8277;border-color:#ee8277}.list-group-item-light{color:#bbb;background-color:#4f4f4f}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#bbb;background-color:#474747}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#bbb;border-color:#bbb}.list-group-item-dark{color:#767676;background-color:#1e1e1e}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#767676;background-color:#1b1b1b}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#767676;border-color:#767676}.btn-close{color:#fafafa;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fafafa'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;opacity:.5}.btn-close:hover{color:#fafafa;opacity:.75}.btn-close:focus{box-shadow:0 0 0 .25rem rgba(55,90,127,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{background-color:rgba(17,17,17,.85);border:1px solid rgba(250,250,250,.1);box-shadow:0 .5rem 1rem rgba(17,17,17,.15)}.toast-header{color:#9e9e9e;background-color:rgba(17,17,17,.85);border-bottom:1px solid rgba(250,250,250,.05)}.modal-content{background-color:#2f2f2f;border:1px solid rgba(250,250,250,.2)}.modal-backdrop{background-color:#111}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.85}.modal-header{border-bottom:1px solid #515151}.modal-footer{border-top:1px solid #515151}.tooltip{opacity:0}.tooltip.show{opacity:.9}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{border-top-color:#fafafa}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{border-right-color:#fafafa}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{border-bottom-color:#fafafa}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{border-left-color:#fafafa}.tooltip-inner{color:#111;background-color:#fafafa}.popover{background-color:#111;border:1px solid rgba(250,250,250,.2)}.popover .popover-arrow::after,.popover .popover-arrow::before{border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{border-top-color:rgba(250,250,250,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{border-top-color:#111}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{border-right-color:rgba(250,250,250,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{border-right-color:#111}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{border-bottom-color:rgba(250,250,250,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{border-bottom-color:#111}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{border-bottom:1px solid #1f1f1f}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{border-left-color:rgba(250,250,250,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{border-left-color:#111}.popover-header{background-color:#1f1f1f;border-bottom:1px solid rgba(250,250,250,.2)}.popover-body{color:#e1e1e1}.carousel-control-next,.carousel-control-prev{color:#fafafa;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fafafa;opacity:.9}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fafafa'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fafafa'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators [data-bs-target]{background-color:#111;opacity:.5}.carousel-indicators .active{opacity:1}.carousel-caption{color:#111}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#fafafa}.carousel-dark .carousel-caption{color:#fafafa}.offcanvas{background-color:#2f2f2f}.offcanvas-backdrop{background-color:#111}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.85}.offcanvas-start{border-right:1px solid rgba(250,250,250,.2)}.offcanvas-end{border-left:1px solid rgba(250,250,250,.2)}.offcanvas-top{border-bottom:1px solid rgba(250,250,250,.2)}.offcanvas-bottom{border-top:1px solid rgba(250,250,250,.2)}.placeholder{background-color:currentColor;opacity:.5}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#fafafa 55%,rgba(255,255,255,0.8) 75%,#fafafa 95%);mask-image:linear-gradient(130deg,#fafafa 55%,rgba(255,255,255,0.8) 75%,#fafafa 95%)}.link-primary{color:#375a7f}.link-primary:focus,.link-primary:hover{color:#2c4866}.link-secondary{color:#626262}.link-secondary:focus,.link-secondary:hover{color:#4e4e4e}.link-success{color:#00bc8c}.link-success:focus,.link-success:hover{color:#33c9a3}.link-info{color:#17a2b8}.link-info:focus,.link-info:hover{color:#128293}.link-warning{color:#f39c12}.link-warning:focus,.link-warning:hover{color:#f5b041}.link-danger{color:#e74c3c}.link-danger:focus,.link-danger:hover{color:#b93d30}.link-light{color:#9e9e9e}.link-light:focus,.link-light:hover{color:#7e7e7e}.link-dark{color:#3b3b3b}.link-dark:focus,.link-dark:hover{color:#2f2f2f}.vr{background-color:currentColor;opacity:.1}.shadow{box-shadow:0 .5rem 1rem rgba(17,17,17,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(17,17,17,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(17,17,17,.175)!important}.border{border:1px solid #515151!important}.border-top{border-top:1px solid #515151!important}.border-end{border-right:1px solid #515151!important}.border-bottom{border-bottom:1px solid #515151!important}.border-start{border-left:1px solid #515151!important}.border-primary{border-color:#375a7f!important}.border-secondary{border-color:#626262!important}.border-success{border-color:#00bc8c!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#f39c12!important}.border-danger{border-color:#e74c3c!important}.border-light{border-color:#9e9e9e!important}.border-dark{border-color:#3b3b3b!important}.border-white{border-color:#fafafa!important}.border-black{border-color:#111!important}.text-muted{color:#9e9e9e!important}.text-white-50{color:rgba(250,250,250,.5)!important}.text-black-50{color:rgba(17,17,17,.5)!important}.bg-black{background-color:#111!important}body::-moz-selection{color:#cfcfcf;background:rgba(23,162,184,.5)}body::selection{color:#cfcfcf;background:rgba(23,162,184,.5)}}@media (prefers-color-scheme:light){.d-light-inline{display:inline!important}.d-light-inline-block{display:inline-block!important}.d-light-block{display:block!important}.d-light-grid{display:grid!important}.d-light-table{display:table!important}.d-light-table-row{display:table-row!important}.d-light-table-cell{display:table-cell!important}.d-light-flex{display:flex!important}.d-light-inline-flex{display:inline-flex!important}.d-light-none{display:none!important}}@media (prefers-color-scheme:dark){.d-dark-inline{display:inline!important}.d-dark-inline-block{display:inline-block!important}.d-dark-block{display:block!important}.d-dark-grid{display:grid!important}.d-dark-table{display:table!important}.d-dark-table-row{display:table-row!important}.d-dark-table-cell{display:table-cell!important}.d-dark-flex{display:flex!important}.d-dark-inline-flex{display:inline-flex!important}.d-dark-none{display:none!important}} \ No newline at end of file diff --git a/photo21/templates/account/login.html b/photo21/templates/account/login.html index 02422ceee9d3a0b22347260102273742508460a7..5e2af591a1a34a1a2a0f72b2a46294b7fa71dc2b 100644 --- a/photo21/templates/account/login.html +++ b/photo21/templates/account/login.html @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later {% block head_title %}{% trans "Sign In" %}{% endblock %} {% block content %} -<div class="card bg-light mx-auto" style="max-width: 35rem;"> +<div class="card mx-auto" style="max-width: 35rem;"> <h3 class="card-header text-center"> {% trans "Sign In" %} </h3> diff --git a/photo21/templates/account/signup.html b/photo21/templates/account/signup.html index 16df34736b0c5b9c305b64451e4a0c4c06e341bc..380b639d10b8dc24c80ea2046a3d70eb2c8d0f6d 100644 --- a/photo21/templates/account/signup.html +++ b/photo21/templates/account/signup.html @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later {% block head_title %}{% trans "Signup" %}{% endblock %} {% block content %} -<div class="card bg-light"> +<div class="card"> <h3 class="card-header text-center"> {% trans "Sign Up" %} </h3> diff --git a/photo21/templates/base.html b/photo21/templates/base.html index 707280c21c2dc49325d331998177053ce06080b7..ff7070a11744cb9801fd99f142f24dd82902f5aa 100644 --- a/photo21/templates/base.html +++ b/photo21/templates/base.html @@ -11,12 +11,13 @@ SPDX-License-Identifier: GPL-3.0-or-later <title>{% block title %}{{ title }}{% endblock title %} - {{ request.site.name }}</title> <meta name="description" content="{% trans "The ENS Paris-Saclay pictures server." %}"> <link rel="stylesheet" href="{% static "bootstrap5/css/bootstrap.min.css" %}"> + <link rel="stylesheet" href="{% static "bootstrap5/css/bootstrap-dark-plugin.min.css" %}"> <link rel="icon" type="image/png" sizes="16x16" href="{% static "favicon-16x16.png" %}"> <link rel="icon" type="image/png" sizes="32x32" href="{% static "favicon-32x32.png" %}"> <meta name="theme-color" content="#212529"> {% block extracss %}{% endblock %} </head> -<body class="bg-light"> +<body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark py-0"> <div class="container"> <a class="navbar-brand" href="{% url 'index' %}"> diff --git a/photo21/templates/index.html b/photo21/templates/index.html index e9fd5fa81dd67971c161466635237d3b76b7b0e1..2fc6a5dd2166f6a95ed0912c563b3b60ada7d4c3 100644 --- a/photo21/templates/index.html +++ b/photo21/templates/index.html @@ -3,31 +3,40 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endcomment %} {% load i18n %} -{% block title %}Accueil{% endblock %} +{% block title %}{% trans "Home" %}{% endblock %} {% block content %} - <h2>Bienvenue sur le serveur photos !</h2> + <h2>{% trans "Welcome to the pictures server!" %}</h2> <p> - Ce site à pour objectif de recenser les photos et films pris dans la vie - associative de l'ENS Paris-Saclay ou impliquant des - <b>K</b>chanais. + {% blocktranslate trimmed %} + This website aims to collect the pictures and movies taken in the student + life of ENS Paris-Saclay-Saclay or involving its students. + {% endblocktranslate %} </p> <p> - Les photos sont visibles dans <a href="{% url 'photologue:pl-gallery-archive' %}">les galeries</a> et téléchargeables, toutefois, - <b>l'accord de la ou du photographe et des personnes présentes sur la - photo est nécessaire avant toute republication sur un autre site.</b> + {% url 'photologue:pl-gallery-archive' as gallery_archive_url %} + {% blocktranslate trimmed %} + The pictures are visible in <a href="{{ gallery_archive_url }}">the galleries</a> + and are downloadable. + <b>However, the agreement of the photographer and the persons present + on the photo is necessary before any republication on another platform. </b> + {% endblocktranslate %} </p> <p> - Si vous souhaitez qu'une photo soit supprimée, signalez le nous : - <a href="mailto:photos@crans.org?subject=[ABUS] Nouvelle requête" class="btn btn-dark btn-sm">Signaler un abus</a> + {% blocktranslate trimmed %} + If you want a photo to be deleted, please let us know: + <a href="mailto:photos@crans.org?subject=[ABUS] Nouvelle requête" class="btn btn-dark btn-sm">Abuse request</a> + {% endblocktranslate %} </p> <p> - Si vous souhaitez obtenir les droits photographes pour téléverser vos photos, signalez le nous : - <a href="mailto:photos@crans.org?subject=[Photographe] Demande de droits photographe" class="btn btn-dark btn-sm">Devenir photographe</a> + {% blocktranslate trimmed %} + If you want to obtain the right to upload pictures, please let us know: + <a href="mailto:photos@crans.org?subject=[Photographe] Demande de droits photographe" class="btn btn-dark btn-sm">Become a photograph</a> + {% endblocktranslate %} </p> - <h3>Dernières galeries</h3> + <h3>{% trans "Last galleries" %}</h3> <div class="row mb-2"> {% for gallery in object_list %} <div class="col-md-3"> @@ -36,20 +45,12 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endfor %} </div> - <h3>Participez et faites vivre le serveur photos :</h3> - <ul> - <li>Devenez photographe et téléversez vos photos</li> - <li>Reportez-nous toute anomalie dans la nouvelle interface ou toute difficulté rencontrée : fautes d'orthographes, bugs, mauvaise ergonomie…</li> - <li>Envoyez-nous vos suggestions à <a href="mailto:photos@crans.org">photos@crans.org</a>.</li> - <li>Si l'amélioration ou la gestion du serveur vous intéresse et pour en savoir plus, <a href="mailto:photos@crans.org">contactez-nous</a>.</li> - </ul> - <hr/> {% trans "Connected as" %} <code>{{ request.user.username }}</code>. - <form action="{% url 'set_language' %}" method="post" style="max-width: 10em;"> + <form action="{% url 'set_language' %}" method="post" style="max-width: 15em;"> {% csrf_token %} - Changer la langue : + {% trans "Select another language:" %} <select title="language" name="language" class="form-control form-control-sm" onchange="this.form.submit()"> {% get_current_language as LANGUAGE_CODE %} {% get_available_languages as LANGUAGES %} diff --git a/photo21/views.py b/photo21/views.py index 90f4b55d06016e9c0f487894beb3dbd30273a12a..54299010d784dea4902c6e4cf85e7662769cc31e 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/photologue/admin.py b/photologue/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..0c04abf5fe91a0d87f9e5ba01d5d98d19d9a0e0f --- /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 0000000000000000000000000000000000000000..19a451f9ee65270dde169e5d5c9ee7f5a442caf9 --- /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 0000000000000000000000000000000000000000..f9a29ed88c4ab83f70d57587781d76cc7724cc37 --- /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 <EMAIL@ADDRESS>, 2009 +# Jannis Vajen, 2012-2016 +# Martin Darmüntzel <martin@trivialanalog.de>, 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 <richard@arbee-design.co.uk>\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 0000000000000000000000000000000000000000..8a9ca7147505a44b03393bd3f6bd9bfbadf5c39a --- /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 <dmalisani@gmail.com>, 2014 +# dmalisani <dmalisani@gmail.com>, 2014 +# Rafa Muñoz Cárdenas <bymenda@gmail.com>, 2009 +# salvador ortiz <chava.door@gmail.com>, 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 <richard@arbee-design.co.uk>\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 0000000000000000000000000000000000000000..f3b69fc0137eac88fcd81b6ab6ac1ecf095ebb5f --- /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 <matthieu.payet4@gmail.com>, 2017 +# Théophane Hufschmitt <huf31@gmx.fr>, 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 <richard@arbee-design.co.uk>\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 0000000000000000000000000000000000000000..4884957c4e6c899f738e17d908bfc156e007f838 --- /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 0000000000000000000000000000000000000000..48d4e31e46c7bfa4902e2f072662f446f34534d4 --- /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 0000000000000000000000000000000000000000..9f902e6005242ceb49f70ea07b61062b09ee672c --- /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 0000000000000000000000000000000000000000..35d3b3d8f35f00493cece7a116fe4ce22b6b30e7 --- /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 0000000000000000000000000000000000000000..7bb9229197ba5264aec75e5f7aab738aef7af20e --- /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 0000000000000000000000000000000000000000..16d1942b04893b762bf25e6b99e58e9b8e5d50f1 --- /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 0000000000000000000000000000000000000000..0202044113a2eeab893ec3ee6399a5b9f5a9021b --- /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 0000000000000000000000000000000000000000..9f3d862340caf49c9bc5c1bd8764429fb9361877 --- /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 0000000000000000000000000000000000000000..583c3b800d38522f3b26b48c447b373836b43dc3 --- /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 0000000000000000000000000000000000000000..b41490c28bac4d9ef60bf3601ea2cb050bff54ac --- /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 0000000000000000000000000000000000000000..0baafd2f63d02aadd582d7b3b23e77cee695ff64 --- /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 0000000000000000000000000000000000000000..4c64f11f1c95606cbd8da2285c03296ba68537ae --- /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 0000000000000000000000000000000000000000..d466cae53fcfd255d7331e5ebef5ff49dacaf7e5 --- /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 0000000000000000000000000000000000000000..7bee4ebc31355b9c47c9ddd3248da3997fea57cc --- /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 0000000000000000000000000000000000000000..7ed2b9ab1e621274ec9d89d2c1433ebf465c1f01 --- /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 0000000000000000000000000000000000000000..ab5ecbc65114e5b707404a26d9e50c7189a79858 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/photologue/models.py b/photologue/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f38fbc556102ad38e7f961fd94ae75e761304752 --- /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('<a href="{}"><img src="{}"></a>'.format(self.get_absolute_url(), func())) + else: + return mark_safe('<a href="{}"><img src="{}"></a>'.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 0000000000000000000000000000000000000000..93fc8b5233a5a400130ac6e9d383b79b725500d8 --- /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 41b967f4b724195394fe96a78f0e65a1af6bf5a8..e8f0130b42b2b67fe7bbda360c6360c91638f1a3 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 b08e03258894fa7374ea427189f2d6e4a2ab9032..55acba8b2213ed038c85d8fcd566e942475ce156 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/migrations/0001_initial.py b/photologue_custom/migrations/0001_initial.py index 4148b406eb142bfd83d409ea00250eb68b32867c..9fb5a1f0d5907794686422ebfdc20022e18e1c45 100644 --- a/photologue_custom/migrations/0001_initial.py +++ b/photologue_custom/migrations/0001_initial.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')), - ('photo', models.OneToOneField(on_delete='cascade', related_name='extented', to='photologue.Photo')), + ('photo', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='extented', to='photologue.Photo')), ], options={ 'verbose_name': 'Extra fields', @@ -33,7 +33,7 @@ class Migration(migrations.Migration): name='GalleryExtended', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('gallery', models.OneToOneField(on_delete='cascade', related_name='extended', to='photologue.Gallery')), + ('gallery', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='extended', to='photologue.Gallery')), ('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), ], options={ diff --git a/photologue_custom/templates/photologue/gallery_archive.html b/photologue_custom/templates/photologue/gallery_archive.html index 364ae4e0dbc2944bc9b1bda5e386ba152814e3d2..188101b0f376e6bfa80cda7243d761e1a4a5df47 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 6a895bbe53f1d2e09fc3d4ecb0d716a794208be7..580731aa59da00934359cb9b6139b8e65d66ecea 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 dcbb43bf046405aa208016d1b7e58d8e4e1abe15..21bc348c84de84aa8b6b0f82c15f6dd7d0f26d7f 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 9aff5b9b87a894c3f80351550e1836eb776ba525..c0d4c1b675ce710c8254ddab271b93f7faf39e32 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 6000e73817cb212bc150346c0393995834497bb7..037a354eb39f0afab3c7cd1b0ee00f0c915dd0ff 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/<slug:slug>/', TagDetail.as_view(), name='tag-detail'), - path('gallery/', CustomGalleryArchiveIndexView.as_view(), name='pl-gallery-archive'), - re_path(r'^gallery/(?P<year>\d{4})/$', CustomGalleryYearArchiveView.as_view(), name='pl-gallery-archive-year'), + path('gallery/', GalleryArchiveIndexView.as_view(), name='pl-gallery-archive'), + re_path(r'^gallery/(?P<year>\d{4})/$', GalleryYearArchiveView.as_view(), name='pl-gallery-archive-year'), path('gallery/<slug:slug>/', CustomGalleryDetailView.as_view(), name='pl-gallery'), path('gallery/<slug:slug>/<int:owner>/', CustomGalleryDetailView.as_view(), name='pl-gallery-owner'), path('gallery/<slug:slug>/download/', GalleryDownload.as_view(), name='pl-gallery-download'), - path('photo/<slug:slug>/', CustomPhotoDetailView.as_view(), name='pl-photo'), + path('photo/<slug:slug>/', 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 04bfa69cdb15077a976760b4cb804922886ade91..8a683f0a016883a05c6913eefaf11aeff1315c5a 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 d6c268546a673b96c7e56755c39f10c142bd7fb0..9f86e8dea72f2a9565a5589f46bc47c9102ce0f7 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 007b0c9cd48376d236854ff5ed21a2fdadc4a982..f69e975e890d04d797059dbee807486c2a8d04e8 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