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