Merge branch 'moderation_tools' into 'master'
Moderation tools See merge request bde/photo21!23
This commit is contained in:
commit
c84a4e1a22
11 changed files with 97 additions and 106 deletions
|
|
@ -37,13 +37,17 @@ server {
|
||||||
# Allow 2Go upload at once
|
# Allow 2Go upload at once
|
||||||
client_max_body_size 2G;
|
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
|
# Django statics and media
|
||||||
# Do not directly serve media, it must be authorized
|
# Do not directly serve media, it must be authorized
|
||||||
# by a Django view to check permissions
|
# by a Django view to check permissions
|
||||||
location /protected/media {
|
location /protected/media {
|
||||||
internal;
|
internal;
|
||||||
alias /var/www/photos/photo21/media;
|
alias /var/www/photos/photo21/media;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /static {
|
location /static {
|
||||||
alias /var/www/photos/photo21/static;
|
alias /var/www/photos/photo21/static;
|
||||||
}
|
}
|
||||||
|
|
@ -51,5 +55,9 @@ server {
|
||||||
location / {
|
location / {
|
||||||
uwsgi_pass unix:///var/run/uwsgi/app/uwsgi_photos/socket;
|
uwsgi_pass unix:///var/run/uwsgi/app/uwsgi_photos/socket;
|
||||||
include /etc/nginx/uwsgi_params;
|
include /etc/nginx/uwsgi_params;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
send_timeout 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/
|
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'CHANGE_ME'
|
SECRET_KEY = 'CHANGE ME'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
@ -36,15 +36,19 @@ ALLOWED_HOSTS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Admins receive server errors, this is useful to be notified of potential bugs
|
# Admins receive server errors, this is useful to be notified of potential bugs
|
||||||
|
# By default MANAGERS=ADMINS, so admins also receive upload notifications
|
||||||
ADMINS = [
|
ADMINS = [
|
||||||
('admin', 'photos-admin@lists.crans.org'),
|
('admin', 'photos-admin@lists.crans.org'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Managers receive notifications about new photos upload
|
# Use secure cookies in production
|
||||||
MANAGERS = [
|
SESSION_COOKIE_SECURE = not DEBUG
|
||||||
('moderation', 'photos-admin@lists.crans.org'),
|
CSRF_COOKIE_SECURE = not DEBUG
|
||||||
]
|
|
||||||
|
|
||||||
|
# Remember HTTPS for 1 year
|
||||||
|
SECURE_HSTS_SECONDS = 31536000
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
|
@ -144,14 +148,8 @@ PASSWORD_HASHERS = [
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Limit available languages to this subset
|
# Limit available languages to this subset
|
||||||
|
|
@ -185,16 +183,10 @@ LOCALE_PATHS = [os.path.join(BASE_DIR, 'photo21/locale')]
|
||||||
|
|
||||||
FIXTURE_DIRS = [os.path.join(BASE_DIR, 'photo21/fixtures')]
|
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:
|
if DEBUG:
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
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
|
# Mail will be sent from this address
|
||||||
SERVER_EMAIL = "photos@crans.org"
|
SERVER_EMAIL = "photos@crans.org"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@ class MediaAccess(LoginRequiredMixin, View):
|
||||||
|
|
||||||
|
|
||||||
class IndexView(LoginRequiredMixin, ListView):
|
class IndexView(LoginRequiredMixin, ListView):
|
||||||
queryset = Gallery.objects.filter(is_public=True)
|
queryset = Gallery.objects.all()
|
||||||
paginate_by = 4
|
paginate_by = 4
|
||||||
template_name = "index.html"
|
template_name = "index.html"
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,26 @@ from .models import Gallery, Photo, Tag
|
||||||
|
|
||||||
|
|
||||||
class GalleryAdmin(admin.ModelAdmin):
|
class GalleryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('title', 'date_start', 'photo_count', 'is_public')
|
list_display = ('title', 'date_start', 'photo_count', 'get_tags')
|
||||||
list_filter = ['date_start', 'is_public']
|
list_filter = ['date_start', 'tags']
|
||||||
date_hierarchy = 'date_start'
|
date_hierarchy = 'date_start'
|
||||||
prepopulated_fields = {'slug': ('title',)}
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
model = Gallery
|
model = Gallery
|
||||||
autocomplete_fields = ['photos', 'tags']
|
exclude = ['photos']
|
||||||
|
autocomplete_fields = ['tags']
|
||||||
search_fields = ['title', ]
|
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):
|
class PhotoAdmin(admin.ModelAdmin):
|
||||||
list_display = ('title', 'date_taken', 'date_added',
|
list_display = ('title', 'date_taken', 'date_added',
|
||||||
'is_public', 'view_count', 'admin_thumbnail', 'get_owner')
|
'is_public', 'view_count', 'admin_thumbnail', 'get_owner')
|
||||||
list_filter = ['date_added', 'is_public', 'owner']
|
list_filter = ['date_added', 'is_public', 'owner']
|
||||||
search_fields = ['title', 'slug', 'caption']
|
search_fields = ['title', 'slug', 'caption']
|
||||||
list_per_page = 10
|
list_per_page = 25
|
||||||
prepopulated_fields = {'slug': ('title',)}
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
readonly_fields = ('date_taken',)
|
readonly_fields = ('date_taken',)
|
||||||
model = Photo
|
model = Photo
|
||||||
|
|
@ -33,6 +38,7 @@ class PhotoAdmin(admin.ModelAdmin):
|
||||||
class TagAdmin(admin.ModelAdmin):
|
class TagAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name',)
|
list_display = ('name',)
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
model = Tag
|
model = Tag
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
17
photologue/migrations/0003_remove_gallery_is_public.py
Normal file
17
photologue/migrations/0003_remove_gallery_is_public.py
Normal file
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -15,7 +15,6 @@ import exifread
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import default_storage
|
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
|
|
@ -52,13 +51,6 @@ else:
|
||||||
fn = unicodedata.normalize('NFKD', force_str(filename)).encode('ascii', 'ignore').decode('ascii')
|
fn = unicodedata.normalize('NFKD', force_str(filename)).encode('ascii', 'ignore').decode('ascii')
|
||||||
return os.path.join('photos', fn)
|
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
|
# Exif Orientation values
|
||||||
# Value 0thRow 0thColumn
|
# Value 0thRow 0thColumn
|
||||||
# 1 top left
|
# 1 top left
|
||||||
|
|
@ -160,10 +152,6 @@ class Gallery(models.Model):
|
||||||
verbose_name=_('tags'),
|
verbose_name=_('tags'),
|
||||||
blank=True,
|
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',
|
photos = models.ManyToManyField('photologue.Photo',
|
||||||
related_name='galleries',
|
related_name='galleries',
|
||||||
verbose_name=_('photos'),
|
verbose_name=_('photos'),
|
||||||
|
|
@ -181,22 +169,13 @@ class Gallery(models.Model):
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('photologue:pl-gallery', args=[self.slug])
|
return reverse('photologue:pl-gallery', args=[self.slug])
|
||||||
|
|
||||||
def latest(self, limit=LATEST_LIMIT, public=True):
|
def sample(self, 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."""
|
"""Return a sample of photos, ordered at random."""
|
||||||
if not count:
|
count = 1
|
||||||
count = 1
|
|
||||||
if count > self.photo_count():
|
if count > self.photo_count():
|
||||||
count = self.photo_count()
|
count = self.photo_count()
|
||||||
if public:
|
if public:
|
||||||
photo_set = self.public()
|
photo_set = self.photos.filter(is_public=True)
|
||||||
else:
|
else:
|
||||||
photo_set = self.photos
|
photo_set = self.photos
|
||||||
return random.sample(set(photo_set), count)
|
return random.sample(set(photo_set), count)
|
||||||
|
|
@ -204,16 +183,12 @@ class Gallery(models.Model):
|
||||||
def photo_count(self, public=True):
|
def photo_count(self, public=True):
|
||||||
"""Return a count of all the photos in this gallery."""
|
"""Return a count of all the photos in this gallery."""
|
||||||
if public:
|
if public:
|
||||||
return self.public().count()
|
return self.photos.filter(is_public=True).count()
|
||||||
else:
|
else:
|
||||||
return self.photos.count()
|
return self.photos.count()
|
||||||
|
|
||||||
photo_count.short_description = _('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):
|
class ImageModel(models.Model):
|
||||||
image = models.ImageField(_('image'),
|
image = models.ImageField(_('image'),
|
||||||
|
|
@ -315,6 +290,10 @@ class ImageModel(models.Model):
|
||||||
setattr(self, name, result)
|
setattr(self, name, result)
|
||||||
return result
|
return result
|
||||||
else:
|
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
|
raise AttributeError
|
||||||
|
|
||||||
def size_exists(self, photosize):
|
def size_exists(self, photosize):
|
||||||
|
|
@ -375,24 +354,14 @@ class ImageModel(models.Model):
|
||||||
return
|
return
|
||||||
# Save the original format
|
# Save the original format
|
||||||
im_format = im.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
|
# Rotate if found & necessary
|
||||||
if 'Image Orientation' in self.exif() and \
|
if 'Image Orientation' in self.exif() and \
|
||||||
self.exif().get('Image Orientation').values[0] in IMAGE_EXIF_ORIENTATION_MAP:
|
self.exif().get('Image Orientation').values[0] in IMAGE_EXIF_ORIENTATION_MAP:
|
||||||
im = im.transpose(
|
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
|
# Resize/crop image
|
||||||
if (im.size != photosize.size and photosize.size != (0, 0)) or recreate:
|
if (im.size != photosize.size and photosize.size != (0, 0)) or recreate:
|
||||||
im = self.resize_image(im, photosize)
|
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
|
# Save file
|
||||||
im_filename = getattr(self, "get_%s_filename" % photosize.name)()
|
im_filename = getattr(self, "get_%s_filename" % photosize.name)()
|
||||||
try:
|
try:
|
||||||
|
|
@ -515,10 +484,10 @@ class Photo(ImageModel):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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
|
# update kwargs to force image recreation in parent class
|
||||||
current = Photo.objects.get(pk=self.pk) if self.pk else None
|
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)
|
kwargs.update(recreate=True)
|
||||||
|
|
||||||
if self.slug is None:
|
if self.slug is None:
|
||||||
|
|
@ -530,7 +499,7 @@ class Photo(ImageModel):
|
||||||
|
|
||||||
def public_galleries(self):
|
def public_galleries(self):
|
||||||
"""Return the public galleries to which this photo belongs."""
|
"""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):
|
def get_previous_in_gallery(self, gallery):
|
||||||
"""Find the neighbour of this photo in the supplied gallery.
|
"""Find the neighbour of this photo in the supplied gallery.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{% extends "admin/change_list.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{# Hide upload as zip #}
|
|
||||||
|
|
@ -70,7 +70,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<div class="card-body row" id="lightgallery">
|
<div class="card-body row" id="lightgallery">
|
||||||
{% for photo in photos %}
|
{% 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 }}">
|
<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>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
{% comment %}
|
{% comment %}
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load photologue_tags i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{{ object.title }}{% endblock %}
|
{% block title %}{{ object.title }}{% endblock %}
|
||||||
|
|
||||||
|
|
@ -13,22 +13,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<p class="text-muted small">{% trans "Published" %} {{ object.date_added }}</p>
|
<p class="text-muted small">{% trans "Published" %} {{ object.date_added }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
{% if object.caption %}<p>{{ object.caption|safe }}</p>{% endif %}
|
||||||
<div class="col-md-8">
|
<a href="{{ object.image.url }}">
|
||||||
{% if object.caption %}<p>{{ object.caption|safe }}</p>{% endif %}
|
<img src="{{ object.get_display_url }}" class="img-thumbnail" alt="{{ object.title }}">
|
||||||
<a href="{{ object.image.url }}">
|
</a>
|
||||||
<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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from .views import (CustomGalleryDetailView, GalleryArchiveIndexView,
|
from .views import (GalleryDetailView, GalleryArchiveIndexView,
|
||||||
GalleryDownload, GalleryUpload, GalleryYearArchiveView,
|
GalleryDownload, GalleryUpload, GalleryYearArchiveView,
|
||||||
PhotoDetailView, TagDetail)
|
PhotoDetailView, TagDetail)
|
||||||
|
|
||||||
|
|
@ -9,8 +9,8 @@ urlpatterns = [
|
||||||
path('tag/<slug:slug>/', TagDetail.as_view(), name='tag-detail'),
|
path('tag/<slug:slug>/', TagDetail.as_view(), name='tag-detail'),
|
||||||
path('gallery/', GalleryArchiveIndexView.as_view(), name='pl-gallery-archive'),
|
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'),
|
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>/', GalleryDetailView.as_view(), name='pl-gallery'),
|
||||||
path('gallery/<slug:slug>/<int:owner>/', CustomGalleryDetailView.as_view(), name='pl-gallery-owner'),
|
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('gallery/<slug:slug>/download/', GalleryDownload.as_view(), name='pl-gallery-download'),
|
||||||
path('photo/<slug:slug>/', PhotoDetailView.as_view(), name='pl-photo'),
|
path('photo/<slug:slug>/', PhotoDetailView.as_view(), name='pl-photo'),
|
||||||
path('upload/', GalleryUpload.as_view(), name='pl-gallery-upload'),
|
path('upload/', GalleryUpload.as_view(), name='pl-gallery-upload'),
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,16 @@ from .models import Gallery, Photo, Tag
|
||||||
|
|
||||||
|
|
||||||
class GalleryDateView(LoginRequiredMixin):
|
class GalleryDateView(LoginRequiredMixin):
|
||||||
queryset = Gallery.objects.filter(is_public=True)
|
model = Gallery
|
||||||
date_field = 'date_start'
|
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):
|
class GalleryArchiveIndexView(GalleryDateView, ArchiveIndexView):
|
||||||
|
|
@ -39,7 +45,15 @@ class GalleryYearArchiveView(GalleryDateView, YearArchiveView):
|
||||||
|
|
||||||
|
|
||||||
class PhotoDetailView(LoginRequiredMixin, DetailView):
|
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):
|
class TagDetail(LoginRequiredMixin, DetailView):
|
||||||
|
|
@ -51,23 +65,25 @@ class TagDetail(LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
current_tag = self.get_object().slug
|
current_tag = self.get_object().slug
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['galleries'] = Gallery.objects.filter(is_public=True) \
|
context['galleries'] = Gallery.objects.filter(tags__slug=current_tag) \
|
||||||
.filter(tags__slug=current_tag) \
|
|
||||||
.order_by('-date_start')
|
.order_by('-date_start')
|
||||||
return context
|
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_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
# Query with owner to reduce database lag
|
# Non-staff members only see public photos
|
||||||
context['photos'] = self.object.public().select_related('owner')
|
if self.request.user.is_staff:
|
||||||
|
context['photos'] = self.object.photos.all()
|
||||||
|
else:
|
||||||
|
context['photos'] = self.object.photos.filter(is_public=True)
|
||||||
|
|
||||||
# List owners
|
# List owners
|
||||||
context['owners'] = []
|
context['owners'] = []
|
||||||
|
|
@ -93,7 +109,7 @@ class GalleryDownload(LoginRequiredMixin, DetailView):
|
||||||
gallery = self.get_object()
|
gallery = self.get_object()
|
||||||
byte_data = BytesIO()
|
byte_data = BytesIO()
|
||||||
zip_file = zipfile.ZipFile(byte_data, "w")
|
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))
|
filename = os.path.basename(os.path.normpath(photo.image.path))
|
||||||
zip_file.write(photo.image.path, filename)
|
zip_file.write(photo.image.path, filename)
|
||||||
zip_file.close()
|
zip_file.close()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue