diff --git a/README.md b/README.md
index 7bda3bdfc030281cc211b27b09373e021abfb046..52dccb321f9648dc948c920cbfc9eb10ba164120 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@
 
 [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](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