From 696bc4d5c11031efeecad3617e208fe647fc0e75 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 12:29:27 +0100
Subject: [PATCH 01/15] Staff members can inspect private pictures

---
 photologue/models.py                          | 42 +++--------------
 .../admin/photologue/photo/change_list.html   |  3 --
 .../templates/photologue/gallery_detail.html  |  1 +
 .../photologue/includes/gallery_sample.html   |  3 +-
 .../templates/photologue/photo_detail.html    |  2 +-
 photologue/urls.py                            |  6 +--
 photologue/views.py                           | 45 +++++++++++++++----
 7 files changed, 49 insertions(+), 53 deletions(-)
 delete mode 100644 photologue/templates/admin/photologue/photo/change_list.html

diff --git a/photologue/models.py b/photologue/models.py
index b7e975c..889be06 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -52,13 +52,6 @@ else:
         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
@@ -181,22 +174,13 @@ class Gallery(models.Model):
     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):
+    def sample(self, public=True):
         """Return a sample of photos, ordered at random."""
-        if not count:
-            count = 1
+        count = 1
         if count > self.photo_count():
             count = self.photo_count()
         if public:
-            photo_set = self.public()
+            photo_set = self.photos.filter(is_public=True)
         else:
             photo_set = self.photos
         return random.sample(set(photo_set), count)
@@ -204,16 +188,12 @@ class Gallery(models.Model):
     def photo_count(self, public=True):
         """Return a count of all the photos in this gallery."""
         if public:
-            return self.public().count()
+            return self.photos.filter(is_public=True).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'),
@@ -375,11 +355,6 @@ class ImageModel(models.Model):
             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:
@@ -388,11 +363,6 @@ class ImageModel(models.Model):
         # 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:
@@ -515,10 +485,10 @@ class Photo(ImageModel):
         return self.title
 
     def save(self, *args, **kwargs):
-        # If crop_from or effect property has been changed on existing image,
+        # If crop_from 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):
+        if current and (current.crop_from != self.crop_from):
             kwargs.update(recreate=True)
 
         if self.slug is None:
diff --git a/photologue/templates/admin/photologue/photo/change_list.html b/photologue/templates/admin/photologue/photo/change_list.html
deleted file mode 100644
index 22a98d1..0000000
--- a/photologue/templates/admin/photologue/photo/change_list.html
+++ /dev/null
@@ -1,3 +0,0 @@
-{% extends "admin/change_list.html" %}
-{% load i18n %}
-{# Hide upload as zip #}
\ No newline at end of file
diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index 1df68af..1263266 100644
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -39,6 +39,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
     {% endif %}
 </h1>
 {% if gallery.date_start %}<p class="text-muted small">{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} {% trans "to" %} {{ gallery.date_end }}{% endif %}</p>{% endif %}
+{% if not gallery.is_public %}<p><span class="badge rounded-pill bg-danger">PRIVATE</span></p>{% endif %}
 {% if gallery.tags.all %}
 <p class="text-muted">
     Tags : {% for tag in gallery.tags.all %}
diff --git a/photologue/templates/photologue/includes/gallery_sample.html b/photologue/templates/photologue/includes/gallery_sample.html
index 8282074..694fda4 100644
--- a/photologue/templates/photologue/includes/gallery_sample.html
+++ b/photologue/templates/photologue/includes/gallery_sample.html
@@ -1,12 +1,13 @@
 {% load i18n %}
 
-<div class="card text-white bg-dark">
+<div class="card text-white {% if not gallery.is_public %}bg-danger{% else %}bg-dark{% endif %}">
     {% for photo in gallery.sample %}
     <img src="{{ photo.get_thumbnail_url }}" class="card-img-top" alt="{{ photo.title }}">
     {% endfor %}
     <div class="card-body">
         <h5 class="card-title">{{ gallery.title }}</h5>
         {% if gallery.date_start %}<p class="card-text text-muted small mb-0">{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} - {{ gallery.date_end }}{% endif %}</p>{% endif %}
+        {% if not gallery.is_public %}<p class="card-text small mb-0">(private)</p>{% endif %}
         <a href="{{ gallery.get_absolute_url }}" class="stretched-link"></a>
     </div>
 </div>
diff --git a/photologue/templates/photologue/photo_detail.html b/photologue/templates/photologue/photo_detail.html
index c0d4c1b..24ec51b 100644
--- a/photologue/templates/photologue/photo_detail.html
+++ b/photologue/templates/photologue/photo_detail.html
@@ -2,7 +2,7 @@
 {% comment %}
 SPDX-License-Identifier: GPL-3.0-or-later
 {% endcomment %}
-{% load photologue_tags i18n %}
+{% load i18n %}
 
 {% block title %}{{ object.title }}{% endblock %}
 
diff --git a/photologue/urls.py b/photologue/urls.py
index e4949f8..ff49533 100644
--- a/photologue/urls.py
+++ b/photologue/urls.py
@@ -1,6 +1,6 @@
 from django.urls import path, re_path
 
-from .views import (CustomGalleryDetailView, GalleryArchiveIndexView,
+from .views import (GalleryDetailView, GalleryArchiveIndexView,
                     GalleryDownload, GalleryUpload, GalleryYearArchiveView,
                     PhotoDetailView, TagDetail)
 
@@ -9,8 +9,8 @@ urlpatterns = [
     path('tag/<slug:slug>/', TagDetail.as_view(), name='tag-detail'),
     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>/', GalleryDetailView.as_view(), name='pl-gallery'),
+    path('gallery/<slug:slug>/<int:owner>/', GalleryDetailView.as_view(), name='pl-gallery-owner'),
     path('gallery/<slug:slug>/download/', GalleryDownload.as_view(), name='pl-gallery-download'),
     path('photo/<slug:slug>/', PhotoDetailView.as_view(), name='pl-photo'),
     path('upload/', GalleryUpload.as_view(), name='pl-gallery-upload'),
diff --git a/photologue/views.py b/photologue/views.py
index 77953a8..cf10e0b 100644
--- a/photologue/views.py
+++ b/photologue/views.py
@@ -24,11 +24,19 @@ from .models import Gallery, Photo, Tag
 
 
 class GalleryDateView(LoginRequiredMixin):
-    queryset = Gallery.objects.filter(is_public=True)
+    model = Gallery
     date_field = 'date_start'
     uses_datetime_field = False  # Fix related object access
     allow_empty = True
 
+    def get_queryset(self):
+        """Non-staff members only see public galleries"""
+        qs = super().get_queryset()
+        if self.request.user.is_staff:
+            return qs
+        else:
+            return qs.filter(is_public=True)
+
 
 class GalleryArchiveIndexView(GalleryDateView, ArchiveIndexView):
     pass
@@ -39,7 +47,15 @@ class GalleryYearArchiveView(GalleryDateView, YearArchiveView):
 
 
 class PhotoDetailView(LoginRequiredMixin, DetailView):
-    queryset = Photo.objects.filter(is_public=True)
+    model = Photo
+
+    def get_queryset(self):
+        """Non-staff members only see public photos"""
+        qs = super().get_queryset()
+        if self.request.user.is_staff:
+            return qs
+        else:
+            return qs.filter(is_public=True)
 
 
 class TagDetail(LoginRequiredMixin, DetailView):
@@ -57,17 +73,28 @@ class TagDetail(LoginRequiredMixin, DetailView):
         return context
 
 
-class CustomGalleryDetailView(LoginRequiredMixin, DetailView):
+class GalleryDetailView(LoginRequiredMixin, DetailView):
     """
-    Custom gallery detail view to filter on photo owner
+    Gallery detail view to filter on photo owner
     """
-    queryset = Gallery.objects.filter(is_public=True)
+    model = Gallery
+
+    def get_queryset(self):
+        """Non-staff members only see public galleries"""
+        qs = super().get_queryset()
+        if self.request.user.is_staff:
+            return qs
+        else:
+            return qs.filter(is_public=True)
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
-        # Query with owner to reduce database lag
-        context['photos'] = self.object.public().select_related('owner')
+        # Non-staff members only see public photos
+        if self.request.user.is_staff:
+            context['photos'] = self.object.photos.all()
+        else:
+            context['photos'] = self.object.photos.filter(is_public=True)
 
         # List owners
         context['owners'] = []
@@ -83,7 +110,7 @@ class CustomGalleryDetailView(LoginRequiredMixin, DetailView):
 
 
 class GalleryDownload(LoginRequiredMixin, DetailView):
-    model = Gallery
+    queryset = Gallery.objects.filter(is_public=True)
 
     def get(self, request, *args, **kwargs):
         """
@@ -93,7 +120,7 @@ class GalleryDownload(LoginRequiredMixin, DetailView):
         gallery = self.get_object()
         byte_data = BytesIO()
         zip_file = zipfile.ZipFile(byte_data, "w")
-        for photo in gallery.public():
+        for photo in gallery.photos.filter(is_public=True):
             filename = os.path.basename(os.path.normpath(photo.image.path))
             zip_file.write(photo.image.path, filename)
         zip_file.close()
-- 
GitLab


From 8b768108d2a211f5f633c44b04cd8ec8d876a427 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 12:36:11 +0100
Subject: [PATCH 02/15] Red border for private pricture

---
 photologue/templates/photologue/gallery_detail.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index 1263266..88d2cf4 100644
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -71,7 +71,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
     <div class="card-body row" id="lightgallery">
         {% for photo in photos %}
         <a class="col-6 col-md-3 mb-2 text-center" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url }}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}">
-            <img src="{{ photo.get_thumbnail_url }}" loading="lazy" class="img-thumbnail p-0" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %} - {{ photo.owner.get_full_name }}{% if photo.license %} - {{ photo.license }}{% endif %}">
+            <img src="{{ photo.get_thumbnail_url }}" loading="lazy" class="img-thumbnail p-0{% if not photo.is_public %} border-danger border-5{% endif %}" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %} - {{ photo.owner.get_full_name }}{% if photo.license %} - {{ photo.license }}{% endif %}">
         </a>
         {% endfor %}
     </div>
-- 
GitLab


From 4b3cf831813ac01f6fdd12b6f738d643ac875682 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 13:13:53 +0100
Subject: [PATCH 03/15] Make all galleries public

---
 photo21/views.py                              |  2 +-
 photologue/admin.py                           |  4 ++--
 .../0003_remove_gallery_is_public.py          | 17 +++++++++++++++
 photologue/models.py                          |  7 +------
 .../templates/photologue/gallery_detail.html  |  1 -
 .../photologue/includes/gallery_sample.html   |  3 +--
 photologue/views.py                           | 21 ++-----------------
 7 files changed, 24 insertions(+), 31 deletions(-)
 create mode 100644 photologue/migrations/0003_remove_gallery_is_public.py

diff --git a/photo21/views.py b/photo21/views.py
index 5429901..90d71d5 100644
--- a/photo21/views.py
+++ b/photo21/views.py
@@ -17,6 +17,6 @@ class MediaAccess(LoginRequiredMixin, View):
 
 
 class IndexView(LoginRequiredMixin, ListView):
-    queryset = Gallery.objects.filter(is_public=True)
+    queryset = Gallery.objects.all()
     paginate_by = 4
     template_name = "index.html"
diff --git a/photologue/admin.py b/photologue/admin.py
index 774ed8e..e58d1c1 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -5,8 +5,8 @@ from .models import Gallery, Photo, Tag
 
 
 class GalleryAdmin(admin.ModelAdmin):
-    list_display = ('title', 'date_start', 'photo_count', 'is_public')
-    list_filter = ['date_start', 'is_public']
+    list_display = ('title', 'date_start', 'photo_count')
+    list_filter = ['date_start']
     date_hierarchy = 'date_start'
     prepopulated_fields = {'slug': ('title',)}
     model = Gallery
diff --git a/photologue/migrations/0003_remove_gallery_is_public.py b/photologue/migrations/0003_remove_gallery_is_public.py
new file mode 100644
index 0000000..8092038
--- /dev/null
+++ b/photologue/migrations/0003_remove_gallery_is_public.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.11 on 2022-01-30 12:14
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('photologue', '0002_auto_20220130_1020'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='gallery',
+            name='is_public',
+        ),
+    ]
diff --git a/photologue/models.py b/photologue/models.py
index 889be06..ede27c3 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -15,7 +15,6 @@ 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
@@ -153,10 +152,6 @@ class Gallery(models.Model):
         verbose_name=_('tags'),
         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'),
@@ -500,7 +495,7 @@ class Photo(ImageModel):
 
     def public_galleries(self):
         """Return the public galleries to which this photo belongs."""
-        return self.galleries.filter(is_public=True)
+        return self.galleries
 
     def get_previous_in_gallery(self, gallery):
         """Find the neighbour of this photo in the supplied gallery.
diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index 88d2cf4..4975d54 100644
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -39,7 +39,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
     {% endif %}
 </h1>
 {% if gallery.date_start %}<p class="text-muted small">{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} {% trans "to" %} {{ gallery.date_end }}{% endif %}</p>{% endif %}
-{% if not gallery.is_public %}<p><span class="badge rounded-pill bg-danger">PRIVATE</span></p>{% endif %}
 {% if gallery.tags.all %}
 <p class="text-muted">
     Tags : {% for tag in gallery.tags.all %}
diff --git a/photologue/templates/photologue/includes/gallery_sample.html b/photologue/templates/photologue/includes/gallery_sample.html
index 694fda4..8282074 100644
--- a/photologue/templates/photologue/includes/gallery_sample.html
+++ b/photologue/templates/photologue/includes/gallery_sample.html
@@ -1,13 +1,12 @@
 {% load i18n %}
 
-<div class="card text-white {% if not gallery.is_public %}bg-danger{% else %}bg-dark{% endif %}">
+<div class="card text-white bg-dark">
     {% for photo in gallery.sample %}
     <img src="{{ photo.get_thumbnail_url }}" class="card-img-top" alt="{{ photo.title }}">
     {% endfor %}
     <div class="card-body">
         <h5 class="card-title">{{ gallery.title }}</h5>
         {% if gallery.date_start %}<p class="card-text text-muted small mb-0">{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} - {{ gallery.date_end }}{% endif %}</p>{% endif %}
-        {% if not gallery.is_public %}<p class="card-text small mb-0">(private)</p>{% endif %}
         <a href="{{ gallery.get_absolute_url }}" class="stretched-link"></a>
     </div>
 </div>
diff --git a/photologue/views.py b/photologue/views.py
index cf10e0b..01c2abd 100644
--- a/photologue/views.py
+++ b/photologue/views.py
@@ -29,14 +29,6 @@ class GalleryDateView(LoginRequiredMixin):
     uses_datetime_field = False  # Fix related object access
     allow_empty = True
 
-    def get_queryset(self):
-        """Non-staff members only see public galleries"""
-        qs = super().get_queryset()
-        if self.request.user.is_staff:
-            return qs
-        else:
-            return qs.filter(is_public=True)
-
 
 class GalleryArchiveIndexView(GalleryDateView, ArchiveIndexView):
     pass
@@ -67,8 +59,7 @@ class TagDetail(LoginRequiredMixin, DetailView):
         """
         current_tag = self.get_object().slug
         context = super().get_context_data(**kwargs)
-        context['galleries'] = Gallery.objects.filter(is_public=True) \
-            .filter(tags__slug=current_tag) \
+        context['galleries'] = Gallery.objects.filter(tags__slug=current_tag) \
             .order_by('-date_start')
         return context
 
@@ -79,14 +70,6 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
     """
     model = Gallery
 
-    def get_queryset(self):
-        """Non-staff members only see public galleries"""
-        qs = super().get_queryset()
-        if self.request.user.is_staff:
-            return qs
-        else:
-            return qs.filter(is_public=True)
-
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
@@ -110,7 +93,7 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
 
 
 class GalleryDownload(LoginRequiredMixin, DetailView):
-    queryset = Gallery.objects.filter(is_public=True)
+    model = Gallery
 
     def get(self, request, *args, **kwargs):
         """
-- 
GitLab


From 3c8e34db927e311e00d3419088fe62cb29181920 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 13:23:57 +0100
Subject: [PATCH 04/15] Hide empty galleries

---
 photologue/views.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/photologue/views.py b/photologue/views.py
index 01c2abd..8d75d87 100644
--- a/photologue/views.py
+++ b/photologue/views.py
@@ -26,8 +26,14 @@ from .models import Gallery, Photo, Tag
 class GalleryDateView(LoginRequiredMixin):
     model = Gallery
     date_field = 'date_start'
-    uses_datetime_field = False  # Fix related object access
-    allow_empty = True
+
+    def get_queryset(self):
+        """Hide galleries with only private photos"""
+        qs = super().get_queryset()
+        if self.request.user.is_staff:
+            return qs
+        else:
+            return qs.filter(photos__is_public=True).distinct()
 
 
 class GalleryArchiveIndexView(GalleryDateView, ArchiveIndexView):
-- 
GitLab


From 3a24fbdc289f9e592ead9e0eef8bdf51144a0f58 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 13:45:55 +0100
Subject: [PATCH 05/15] Autocomplete slug for tag

---
 photologue/admin.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/photologue/admin.py b/photologue/admin.py
index e58d1c1..eae1eaa 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -19,7 +19,7 @@ class PhotoAdmin(admin.ModelAdmin):
                     'is_public', 'view_count', 'admin_thumbnail', 'get_owner')
     list_filter = ['date_added', 'is_public', 'owner']
     search_fields = ['title', 'slug', 'caption']
-    list_per_page = 10
+    list_per_page = 25
     prepopulated_fields = {'slug': ('title',)}
     readonly_fields = ('date_taken',)
     model = Photo
@@ -33,6 +33,7 @@ class PhotoAdmin(admin.ModelAdmin):
 class TagAdmin(admin.ModelAdmin):
     list_display = ('name',)
     search_fields = ('name',)
+    prepopulated_fields = {'slug': ('name',)}
     model = Tag
 
 
-- 
GitLab


From e94436c79cbea5bf3e3ac2029da63a28da81fbbd Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 13:51:18 +0100
Subject: [PATCH 06/15] Add tags support in admin

---
 photologue/admin.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/photologue/admin.py b/photologue/admin.py
index eae1eaa..0d1db81 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -5,14 +5,18 @@ from .models import Gallery, Photo, Tag
 
 
 class GalleryAdmin(admin.ModelAdmin):
-    list_display = ('title', 'date_start', 'photo_count')
-    list_filter = ['date_start']
+    list_display = ('title', 'date_start', 'photo_count', 'get_tags')
+    list_filter = ['date_start', 'tags']
     date_hierarchy = 'date_start'
     prepopulated_fields = {'slug': ('title',)}
     model = Gallery
     autocomplete_fields = ['photos', 'tags']
     search_fields = ['title', ]
 
+    def get_tags(self, obj):
+        return ", ".join([t.name for t in obj.tags.all()])
+    get_tags.short_description = _('tags')
+
 
 class PhotoAdmin(admin.ModelAdmin):
     list_display = ('title', 'date_taken', 'date_added',
-- 
GitLab


From b7a78cea12efdd36bdc0038a6152f0cb2e22441f Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 14:53:19 +0100
Subject: [PATCH 07/15] Simplify photo display

---
 .../templates/photologue/photo_detail.html    | 22 ++++---------------
 1 file changed, 4 insertions(+), 18 deletions(-)

diff --git a/photologue/templates/photologue/photo_detail.html b/photologue/templates/photologue/photo_detail.html
index 24ec51b..ef1082b 100644
--- a/photologue/templates/photologue/photo_detail.html
+++ b/photologue/templates/photologue/photo_detail.html
@@ -13,22 +13,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
         <p class="text-muted small">{% trans "Published" %} {{ object.date_added }}</p>
     </div>
 </div>
-<div class="row">
-    <div class="col-md-8">
-        {% if object.caption %}<p>{{ object.caption|safe }}</p>{% endif %}
-        <a href="{{ object.image.url }}">
-            <img src="{{ object.get_display_url }}" class="img-thumbnail" alt="{{ object.title }}">
-        </a>
-    </div>
-    <div class="col-md-4">
-        {% if object.public_galleries %}
-        <p>{% trans "This photo is found in the following galleries" %}:</p>
-        <ul>
-            {% for gallery in object.public_galleries %}
-                <li><a href="{{ gallery.get_absolute_url }}">{{ gallery.title }}</a></li>
-            {% endfor %}
-        </ul>    
-        {% endif %}
-    </div>
-</div>
+{% if object.caption %}<p>{{ object.caption|safe }}</p>{% endif %}
+<a href="{{ object.image.url }}">
+    <img src="{{ object.get_display_url }}" class="img-thumbnail" alt="{{ object.title }}">
+</a>
 {% endblock %}
\ No newline at end of file
-- 
GitLab


From 648cae81d53b3cc98c00151d58530b0fd298dd03 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 19:13:42 +0100
Subject: [PATCH 08/15] Use secure cookies

---
 photo21/settings.py | 26 ++++++--------------------
 1 file changed, 6 insertions(+), 20 deletions(-)

diff --git a/photo21/settings.py b/photo21/settings.py
index e6773ec..92965ca 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -23,7 +23,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'CHANGE_ME'
+SECRET_KEY = ''
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = False
@@ -36,15 +36,13 @@ ALLOWED_HOSTS = [
 ]
 
 # Admins receive server errors, this is useful to be notified of potential bugs
+# By default MANAGERS=ADMINS, so admins also receive upload notifications
 ADMINS = [
     ('admin', 'photos-admin@lists.crans.org'),
 ]
 
-# Managers receive notifications about new photos upload
-MANAGERS = [
-    ('moderation', 'photos-admin@lists.crans.org'),
-]
-
+# Use secure cookies in production
+SESSION_COOKIE_SECURE = not DEBUG
 
 # Application definition
 
@@ -144,14 +142,8 @@ PASSWORD_HASHERS = [
 # Internationalization
 # https://docs.djangoproject.com/en/2.2/topics/i18n/
 
-LANGUAGE_CODE = 'en-us'
-
 TIME_ZONE = 'UTC'
 
-USE_I18N = True
-
-USE_L10N = True
-
 USE_TZ = True
 
 # Limit available languages to this subset
@@ -185,16 +177,10 @@ LOCALE_PATHS = [os.path.join(BASE_DIR, 'photo21/locale')]
 
 FIXTURE_DIRS = [os.path.join(BASE_DIR, 'photo21/fixtures')]
 
-# Email settings
+# Do not send email during debug
+# By default Django sends mails to localhost:25 without authentification
 if DEBUG:
     EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
-else:
-    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', False)
-EMAIL_HOST = os.getenv('EMAIL_HOST', 'localhost')
-EMAIL_PORT = os.getenv('EMAIL_PORT', 25)
-EMAIL_HOST_USER = os.getenv('EMAIL_USER', None)
-EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', None)
 
 # Mail will be sent from this address
 SERVER_EMAIL = "photos@crans.org"
-- 
GitLab


From 13f5111d7a55de13545a4dbbdc399bbc198a36da Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 19:51:58 +0100
Subject: [PATCH 09/15] Mark CSRF cookies as secure

---
 photo21/settings.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/photo21/settings.py b/photo21/settings.py
index 92965ca..f46c675 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -43,6 +43,7 @@ ADMINS = [
 
 # Use secure cookies in production
 SESSION_COOKIE_SECURE = not DEBUG
+CSRF_COOKIE_SECURE = not DEBUG
 
 # Application definition
 
-- 
GitLab


From df7f46425e1f478afa579b6ce3ec82fce5916ded Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 19:55:46 +0100
Subject: [PATCH 10/15] Enable HSTS

---
 photo21/settings.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/photo21/settings.py b/photo21/settings.py
index f46c675..216bc96 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -45,6 +45,9 @@ ADMINS = [
 SESSION_COOKIE_SECURE = not DEBUG
 CSRF_COOKIE_SECURE = not DEBUG
 
+# Remember HTTPS for 24h
+SECURE_HSTS_SECONDS = 86400
+
 # Application definition
 
 INSTALLED_APPS = [
-- 
GitLab


From a719203ee0d018c432575107d56bbe3d6b9ff0b5 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 20:00:04 +0100
Subject: [PATCH 11/15] Include subdomains and preload in HSTS

---
 photo21/settings.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/photo21/settings.py b/photo21/settings.py
index 216bc96..41a845b 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -45,8 +45,10 @@ ADMINS = [
 SESSION_COOKIE_SECURE = not DEBUG
 CSRF_COOKIE_SECURE = not DEBUG
 
-# Remember HTTPS for 24h
-SECURE_HSTS_SECONDS = 86400
+# Remember HTTPS for 1 year
+SECURE_HSTS_SECONDS = 31536000
+SECURE_HSTS_INCLUDE_SUBDOMAINS = True
+SECURE_HSTS_PRELOAD = True
 
 # Application definition
 
-- 
GitLab


From 167c9cb45b4770b86f1bd0fc696d3e963d787dce Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Sun, 30 Jan 2022 20:13:05 +0100
Subject: [PATCH 12/15] Update NGINX example

---
 docs/nginx_photos | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/docs/nginx_photos b/docs/nginx_photos
index 77454b4..51f5fb3 100644
--- a/docs/nginx_photos
+++ b/docs/nginx_photos
@@ -37,13 +37,17 @@ server {
     # Allow 2Go upload at once
     client_max_body_size 2G;
 
+    add_header "X-XSS-Protection" "1; mode=block";
+    add_header "Content-Security-Policy" "default-src 'self' 'unsafe-inline';";
+
     # Django statics and media
     # Do not directly serve media, it must be authorized
     # by a Django view to check permissions
-    location /protected/media  {
+    location /protected/media {
         internal;
         alias /var/www/photos/photo21/media;
     }
+
     location /static {
         alias /var/www/photos/photo21/static;
     }
@@ -51,5 +55,9 @@ server {
     location / {
         uwsgi_pass unix:///var/run/uwsgi/app/uwsgi_photos/socket;
         include /etc/nginx/uwsgi_params;
+        proxy_connect_timeout 600;
+        proxy_send_timeout 600;
+        proxy_read_timeout 600;
+        send_timeout 600;
     }
 }
-- 
GitLab


From 970db0dc05284e0af48f4fb6b6ee4409b119e65d Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Fri, 25 Feb 2022 07:47:34 +0100
Subject: [PATCH 13/15] Fix AttributeError on EXIF load

---
 photologue/models.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/photologue/models.py b/photologue/models.py
index ede27c3..fab1f3c 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -290,6 +290,10 @@ class ImageModel(models.Model):
             setattr(self, name, result)
             return result
         else:
+            # When this attribute error is raised, it's usually because
+            # something tried to access a missing attribute on the photo.
+            # 99% of the time is due to a typo in the attribute name we are
+            # trying to access.
             raise AttributeError
 
     def size_exists(self, photosize):
@@ -354,7 +358,7 @@ class ImageModel(models.Model):
         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]])
+                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)
-- 
GitLab


From 95243c4d06868cbeb54a838dc5fe01decd3828af Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Fri, 25 Feb 2022 07:48:12 +0100
Subject: [PATCH 14/15] Hide photos field in Gallery admin

---
 photologue/admin.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/photologue/admin.py b/photologue/admin.py
index 0d1db81..63cb50a 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -10,7 +10,8 @@ class GalleryAdmin(admin.ModelAdmin):
     date_hierarchy = 'date_start'
     prepopulated_fields = {'slug': ('title',)}
     model = Gallery
-    autocomplete_fields = ['photos', 'tags']
+    exclude = ['photos']
+    autocomplete_fields = ['tags']
     search_fields = ['title', ]
 
     def get_tags(self, obj):
-- 
GitLab


From 8855a4003d4f9d78c53b5a2c7fd84dba76063cd6 Mon Sep 17 00:00:00 2001
From: Alexandre Iooss <erdnaxe@crans.org>
Date: Fri, 25 Feb 2022 07:51:07 +0100
Subject: [PATCH 15/15] Set secret key for CI

---
 photo21/settings.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/photo21/settings.py b/photo21/settings.py
index 41a845b..e0d25b6 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -23,7 +23,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = ''
+SECRET_KEY = 'CHANGE ME'
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = False
-- 
GitLab