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; } } diff --git a/photo21/settings.py b/photo21/settings.py index e6773ec..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 = 'CHANGE_ME' +SECRET_KEY = 'CHANGE ME' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False @@ -36,15 +36,19 @@ 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 +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 @@ -144,14 +148,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 +183,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" 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..63cb50a 100644 --- a/photologue/admin.py +++ b/photologue/admin.py @@ -5,21 +5,26 @@ 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', 'get_tags') + list_filter = ['date_start', 'tags'] 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): + 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', '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 +38,7 @@ class PhotoAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin): list_display = ('name',) search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} model = Tag 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 b7e975c..fab1f3c 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 @@ -52,13 +51,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 @@ -160,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'), @@ -181,22 +169,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 +183,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'), @@ -315,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): @@ -375,24 +354,14 @@ 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: 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) - # 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 +484,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: @@ -530,7 +499,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/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..4975d54 100644 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -70,7 +70,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans "Published" %} {{ object.date_added }}
-{{ object.caption|safe }}
{% endif %} - -{% trans "This photo is found in the following galleries" %}:
-{{ object.caption|safe }}
{% endif %} + +