diff --git a/photologue/admin.py b/photologue/admin.py
index 63cb50a..af7e428 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -5,40 +5,51 @@ from .models import Gallery, Photo, Tag
class GalleryAdmin(admin.ModelAdmin):
- list_display = ('title', 'date_start', 'photo_count', 'get_tags')
- list_filter = ['date_start', 'tags']
- date_hierarchy = 'date_start'
- prepopulated_fields = {'slug': ('title',)}
+ list_display = ("title", "date_start", "photo_count", "get_tags")
+ list_filter = ["date_start", "tags"]
+ date_hierarchy = "date_start"
+ prepopulated_fields = {"slug": ("title",)}
model = Gallery
- exclude = ['photos']
- autocomplete_fields = ['tags']
- search_fields = ['title', ]
+ 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')
+
+ 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_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 = 25
- prepopulated_fields = {'slug': ('title',)}
- readonly_fields = ('date_taken',)
+ prepopulated_fields = {"slug": ("title",)}
+ readonly_fields = ("date_taken",)
model = Photo
def get_owner(self, obj):
return obj.owner.username
- get_owner.admin_order_field = 'owner'
- get_owner.short_description = _('owner')
+
+ get_owner.admin_order_field = "owner"
+ get_owner.short_description = _("owner")
class TagAdmin(admin.ModelAdmin):
- list_display = ('name',)
- search_fields = ('name',)
- prepopulated_fields = {'slug': ('name',)}
+ list_display = ("name",)
+ search_fields = ("name",)
+ prepopulated_fields = {"slug": ("name",)}
model = Tag
diff --git a/photologue/apps.py b/photologue/apps.py
index 19a451f..dad807a 100644
--- a/photologue/apps.py
+++ b/photologue/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class PhotologueConfig(AppConfig):
- default_auto_field = 'django.db.models.AutoField'
- name = 'photologue'
+ default_auto_field = "django.db.models.AutoField"
+ name = "photologue"
diff --git a/photologue/forms.py b/photologue/forms.py
index 8ef1f1c..ed4eab5 100644
--- a/photologue/forms.py
+++ b/photologue/forms.py
@@ -12,40 +12,46 @@ from .models import Gallery, Tag
class UploadForm(forms.Form):
file_field = forms.FileField(
label="",
- widget=forms.FileInput(attrs={
- 'accept': 'image/*',
- 'multiple': True,
- 'class': 'mb-3',
- }),
+ widget=forms.FileInput(
+ attrs={
+ "accept": "image/*",
+ "multiple": True,
+ "class": "mb-3",
+ }
+ ),
)
gallery = forms.ModelChoiceField(
Gallery.objects.all(),
- label=_('Gallery'),
+ label=_("Gallery"),
required=False,
- empty_label=_('-- Create a new gallery --'),
- help_text=_('Select a gallery to add these images to. Leave this empty to '
- 'create a new gallery from the supplied title.')
+ empty_label=_("-- Create a new gallery --"),
+ help_text=_(
+ "Select a gallery to add these images to. Leave this empty to "
+ "create a new gallery from the supplied title."
+ ),
)
new_gallery_title = forms.CharField(
- label=_('New gallery title'),
+ label=_("New gallery title"),
max_length=250,
required=False,
)
new_gallery_date_start = forms.DateField(
- label=_('New gallery event start date'),
+ label=_("New gallery event start date"),
initial=datetime.date.today,
required=False,
)
new_gallery_date_end = forms.DateField(
- label=_('New gallery event end date'),
+ label=_("New gallery event end date"),
initial=datetime.date.today,
required=False,
)
new_gallery_tags = forms.ModelMultipleChoiceField(
Tag.objects.all(),
- label=_('New gallery tags'),
+ label=_("New gallery tags"),
required=False,
- help_text=_('Hold down "Control", or "Command" on a Mac, to select more than one.')
+ help_text=_(
+ 'Hold down "Control", or "Command" on a Mac, to select more than one.'
+ ),
)
def __init__(self, *args, **kwargs):
@@ -53,31 +59,35 @@ class UploadForm(forms.Form):
self.helper = FormHelper()
self.helper.use_custom_control = False
self.helper.layout = Layout(
- 'file_field',
- 'gallery',
- 'new_gallery_title',
+ "file_field",
+ "gallery",
+ "new_gallery_title",
Div(
- Div('new_gallery_date_start', css_class='col'),
- Div('new_gallery_date_end', css_class='col'),
- css_class='row'
+ Div("new_gallery_date_start", css_class="col"),
+ Div("new_gallery_date_end", css_class="col"),
+ css_class="row",
),
- 'new_gallery_tags',
- Submit('submit', _('Upload'), css_class='btn btn-success mt-2')
+ "new_gallery_tags",
+ Submit("submit", _("Upload"), css_class="btn btn-success mt-2"),
)
def clean_new_gallery_title(self):
- title = self.cleaned_data['new_gallery_title']
+ title = self.cleaned_data["new_gallery_title"]
if title and Gallery.objects.filter(title=title).exists():
- raise forms.ValidationError(_('A gallery with that title already exists.'))
+ raise forms.ValidationError(_("A gallery with that title already exists."))
return title
def clean(self):
cleaned_data = super().clean()
# Check that either an existing gallery is chosen, or new_gallery_title is filled
- if not (bool(cleaned_data['gallery']) ^ bool(cleaned_data.get('new_gallery_title', None))):
+ if not (
+ bool(cleaned_data["gallery"])
+ ^ bool(cleaned_data.get("new_gallery_title", None))
+ ):
raise forms.ValidationError(
- _('Select an existing gallery, or enter a title for a new gallery.'))
+ _("Select an existing gallery, or enter a title for a new gallery.")
+ )
return cleaned_data
@@ -85,16 +95,16 @@ class UploadForm(forms.Form):
"""
Get or create gallery
"""
- gallery = self.cleaned_data['gallery']
+ gallery = self.cleaned_data["gallery"]
if not gallery:
# Create new gallery
- title = self.cleaned_data.get('new_gallery_title')
+ title = self.cleaned_data.get("new_gallery_title")
gallery = Gallery.objects.create(
title=title,
slug=slugify(title),
- date_start=self.cleaned_data['new_gallery_date_start'],
- date_end=self.cleaned_data['new_gallery_date_end'],
+ date_start=self.cleaned_data["new_gallery_date_start"],
+ date_end=self.cleaned_data["new_gallery_date_end"],
)
- for tag in self.cleaned_data['new_gallery_tags']:
+ for tag in self.cleaned_data["new_gallery_tags"]:
gallery.tags.add(tag)
return gallery
diff --git a/photologue/management/commands/duplicate.py b/photologue/management/commands/duplicate.py
index 56ce185..80786e4 100644
--- a/photologue/management/commands/duplicate.py
+++ b/photologue/management/commands/duplicate.py
@@ -5,26 +5,36 @@ from photologue.models import Gallery
class Command(BaseCommand):
- help = 'List all duplicate for chosen galleries'
+ help = "List all duplicate for chosen galleries"
def add_arguments(self, parser):
parser.add_argument(
- '--slugs', nargs='+', help='Try to find duplicate in the selected galleries', default=[])
- parser.add_argument('-a', '--all', action='store_true',
- help='Try to find duplicate in all galleries, overide any slugs given')
- parser.add_argument('-d', '--delete', action='store_true')
+ "--slugs",
+ nargs="+",
+ help="Try to find duplicate in the selected galleries",
+ default=[],
+ )
+ parser.add_argument(
+ "-a",
+ "--all",
+ action="store_true",
+ help="Try to find duplicate in all galleries, overide any slugs given",
+ )
+ parser.add_argument("-d", "--delete", action="store_true")
def handle(self, *args, **options):
# Collect all required galleries
- if options['all']:
+ if options["all"]:
galleries = Gallery.objects.all()
else:
galleries = []
- for slug in options['slugs']:
+ for slug in options["slugs"]:
gallery_query = Gallery.objects.filter(slug=slug)
if not gallery_query:
- raise CommandError(f"Slug {slug} does not correspond to a "
- "gallery in the database.")
+ raise CommandError(
+ f"Slug {slug} does not correspond to a "
+ "gallery in the database."
+ )
galleries += gallery_query
# Find duplicates in all galleries
@@ -32,18 +42,16 @@ class Command(BaseCommand):
duplicates = find_duplicate(gallery)
self.stdout.write(f"Gallery {gallery.slug}:")
for original, copies in duplicates:
- self.stdout.write(f" {original.slug} is duplicated:", ending='')
+ self.stdout.write(f" {original.slug} is duplicated:", ending="")
for copy in copies:
self.stdout.write(f" {copy.slug}")
# Delete them if --delete
- if options['delete']:
- self.stdout.write(
- ' Deleting duplicate in {} :'.format(gallery.slug))
+ if options["delete"]:
+ self.stdout.write(" Deleting duplicate in {} :".format(gallery.slug))
for (_original, copies) in duplicates:
for copy in copies:
- self.stdout.write(
- ' Deleting {}...'.format(copy.slug))
+ self.stdout.write(" Deleting {}...".format(copy.slug))
copy.delete()
diff --git a/photologue/management/commands/plcache.py b/photologue/management/commands/plcache.py
index 4884957..5d66fbb 100644
--- a/photologue/management/commands/plcache.py
+++ b/photologue/management/commands/plcache.py
@@ -7,22 +7,21 @@ from photologue.models import ImageModel, PhotoSize
class Command(BaseCommand):
- help = 'Manages Photologue cache file for the given sizes.'
+ help = "Manages Photologue cache file for the given sizes."
def add_arguments(self, parser):
- parser.add_argument('sizes',
- nargs='*',
- type=str,
- help='Name of the photosize.')
- parser.add_argument('--reset',
- action='store_true',
- default=False,
- dest='reset',
- help='Reset photo cache before generating.')
+ parser.add_argument("sizes", nargs="*", type=str, help="Name of the photosize.")
+ parser.add_argument(
+ "--reset",
+ action="store_true",
+ default=False,
+ dest="reset",
+ help="Reset photo cache before generating.",
+ )
def handle(self, *args, **options):
- reset = options['reset']
- sizes = options['sizes']
+ reset = options["reset"]
+ sizes = options["sizes"]
if not sizes:
photosizes = PhotoSize.objects.all()
@@ -30,13 +29,13 @@ class Command(BaseCommand):
photosizes = PhotoSize.objects.filter(name__in=sizes)
if not len(photosizes):
- raise CommandError('No photo sizes were found.')
+ raise CommandError("No photo sizes were found.")
- print('Caching photos, this may take a while...')
+ print("Caching photos, this may take a while...")
for cls in ImageModel.__subclasses__():
for photosize in photosizes:
- print('Cacheing %s size images' % photosize.name)
+ print("Cacheing %s size images" % photosize.name)
for obj in cls.objects.all():
if reset:
obj.remove_size(photosize)
diff --git a/photologue/management/commands/plcreatesize.py b/photologue/management/commands/plcreatesize.py
index 48d4e31..b49ddb0 100644
--- a/photologue/management/commands/plcreatesize.py
+++ b/photologue/management/commands/plcreatesize.py
@@ -6,17 +6,15 @@ from photologue.models import PhotoSize
class Command(BaseCommand):
- help = ('Creates a new Photologue photo size interactively.')
+ help = "Creates a new Photologue photo size interactively."
requires_model_validation = True
can_import_settings = True
def add_arguments(self, parser):
- parser.add_argument('name',
- type=str,
- help='Name of the new photo size')
+ parser.add_argument("name", type=str, help="Name of the new photo size")
def handle(self, *args, **options):
- create_photosize(options['name'])
+ create_photosize(options["name"])
def get_response(msg, func=int, default=None):
@@ -27,10 +25,12 @@ def get_response(msg, func=int, default=None):
try:
return func(resp)
except Exception:
- print('Invalid input.')
+ print("Invalid input.")
-def create_photosize(name, width=0, height=0, crop=False, pre_cache=False, increment_count=False):
+def create_photosize(
+ name, width=0, height=0, crop=False, pre_cache=False, increment_count=False
+):
try:
size = PhotoSize.objects.get(name=name)
exists = True
@@ -38,15 +38,20 @@ def create_photosize(name, width=0, height=0, crop=False, pre_cache=False, incre
size = PhotoSize(name=name)
exists = False
if exists:
- msg = 'A "%s" photo size already exists. Do you want to replace it? (yes, no):' % name
- if not get_response(msg, lambda inp: inp == 'yes', False):
+ msg = (
+ 'A "%s" photo size already exists. Do you want to replace it? (yes, no):'
+ % name
+ )
+ if not get_response(msg, lambda inp: inp == "yes", False):
return
print('\nWe will now define the "%s" photo size:\n' % size)
- w = get_response('Width (in pixels):', lambda inp: int(inp), width)
- h = get_response('Height (in pixels):', lambda inp: int(inp), height)
- c = get_response('Crop to fit? (yes, no):', lambda inp: inp == 'yes', crop)
- p = get_response('Pre-cache? (yes, no):', lambda inp: inp == 'yes', pre_cache)
- i = get_response('Increment count? (yes, no):', lambda inp: inp == 'yes', increment_count)
+ w = get_response("Width (in pixels):", lambda inp: int(inp), width)
+ h = get_response("Height (in pixels):", lambda inp: int(inp), height)
+ c = get_response("Crop to fit? (yes, no):", lambda inp: inp == "yes", crop)
+ p = get_response("Pre-cache? (yes, no):", lambda inp: inp == "yes", pre_cache)
+ i = get_response(
+ "Increment count? (yes, no):", lambda inp: inp == "yes", increment_count
+ )
size.width = w
size.height = h
size.crop = c
diff --git a/photologue/management/commands/plflush.py b/photologue/management/commands/plflush.py
index 9f902e6..b92dcd0 100644
--- a/photologue/management/commands/plflush.py
+++ b/photologue/management/commands/plflush.py
@@ -6,16 +6,13 @@ from photologue.models import ImageModel, PhotoSize
class Command(BaseCommand):
- help = 'Clears the Photologue cache for the given sizes.'
+ help = "Clears the Photologue cache for the given sizes."
def add_arguments(self, parser):
- parser.add_argument('sizes',
- nargs='*',
- type=str,
- help='Name of the photosize.')
+ parser.add_argument("sizes", nargs="*", type=str, help="Name of the photosize.")
def handle(self, *args, **options):
- sizes = options['sizes']
+ sizes = options["sizes"]
if not sizes:
photosizes = PhotoSize.objects.all()
@@ -23,12 +20,12 @@ class Command(BaseCommand):
photosizes = PhotoSize.objects.filter(name__in=sizes)
if not len(photosizes):
- raise CommandError('No photo sizes were found.')
+ raise CommandError("No photo sizes were found.")
- print('Flushing cache...')
+ print("Flushing cache...")
for cls in ImageModel.__subclasses__():
for photosize in photosizes:
- print('Flushing %s size images' % photosize.name)
+ print("Flushing %s size images" % photosize.name)
for obj in cls.objects.all():
obj.remove_size(photosize)
diff --git a/photologue/management/commands/rename_media.py b/photologue/management/commands/rename_media.py
index 1f8767e..95f5926 100644
--- a/photologue/management/commands/rename_media.py
+++ b/photologue/management/commands/rename_media.py
@@ -7,17 +7,17 @@ from photologue.models import Gallery
class Command(BaseCommand):
- help = 'Rename uploaded media file to match gallery and photo names'
+ help = "Rename uploaded media file to match gallery and photo names"
def add_arguments(self, parser):
- parser.add_argument('--apply', action='store_true')
+ parser.add_argument("--apply", action="store_true")
def handle(self, *args, **options):
media_dir = Path(settings.MEDIA_ROOT)
for gallery in Gallery.objects.all():
# Create gallery directory
gallery_year = str(gallery.date_start.year)
- gallery_dir = Path('photos') / gallery_year / gallery.slug
+ gallery_dir = Path("photos") / gallery_year / gallery.slug
gallery_path = media_dir / gallery_dir
if not gallery_path.exists():
self.stdout.write(f"Creating {gallery_dir}")
diff --git a/photologue/migrations/0001_initial.py b/photologue/migrations/0001_initial.py
index 47bca5b..2f0927c 100644
--- a/photologue/migrations/0001_initial.py
+++ b/photologue/migrations/0001_initial.py
@@ -1,10 +1,11 @@
# Generated by Django 3.2.11 on 2022-01-30 10:14
-from django.conf import settings
import django.core.validators
-from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
import photologue.models
@@ -18,79 +19,313 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='PhotoSize',
+ name="PhotoSize",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(help_text='Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".', max_length=40, unique=True, validators=[django.core.validators.RegexValidator(message='Use only plain lowercase letters (ASCII), numbers and underscores.', regex='^[a-z0-9_]+$')], verbose_name='name')),
- ('width', models.PositiveIntegerField(default=0, help_text='If width is set to "0" the image will be scaled to the supplied height.', verbose_name='width')),
- ('height', models.PositiveIntegerField(default=0, help_text='If height is set to "0" the image will be scaled to the supplied width', verbose_name='height')),
- ('quality', models.PositiveIntegerField(choices=[(30, 'Very Low'), (40, 'Low'), (50, 'Medium-Low'), (60, 'Medium'), (70, 'Medium-High'), (80, 'High'), (90, 'Very High')], default=70, help_text='JPEG image quality.', verbose_name='quality')),
- ('upscale', models.BooleanField(default=False, help_text='If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting.', verbose_name='upscale images?')),
- ('crop', models.BooleanField(default=False, help_text='If selected the image will be scaled and cropped to fit the supplied dimensions.', verbose_name='crop to fit?')),
- ('pre_cache', models.BooleanField(default=False, help_text='If selected this photo size will be pre-cached as photos are added.', verbose_name='pre-cache?')),
- ('increment_count', models.BooleanField(default=False, help_text='If selected the image\'s "view_count" will be incremented when this photo size is displayed.', verbose_name='increment view count?')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ help_text='Photo size name should contain only letters, numbers and underscores. Examples: "thumbnail", "display", "small", "main_page_widget".',
+ max_length=40,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ message="Use only plain lowercase letters (ASCII), numbers and underscores.",
+ regex="^[a-z0-9_]+$",
+ )
+ ],
+ verbose_name="name",
+ ),
+ ),
+ (
+ "width",
+ models.PositiveIntegerField(
+ default=0,
+ help_text='If width is set to "0" the image will be scaled to the supplied height.',
+ verbose_name="width",
+ ),
+ ),
+ (
+ "height",
+ models.PositiveIntegerField(
+ default=0,
+ help_text='If height is set to "0" the image will be scaled to the supplied width',
+ verbose_name="height",
+ ),
+ ),
+ (
+ "quality",
+ models.PositiveIntegerField(
+ choices=[
+ (30, "Very Low"),
+ (40, "Low"),
+ (50, "Medium-Low"),
+ (60, "Medium"),
+ (70, "Medium-High"),
+ (80, "High"),
+ (90, "Very High"),
+ ],
+ default=70,
+ help_text="JPEG image quality.",
+ verbose_name="quality",
+ ),
+ ),
+ (
+ "upscale",
+ models.BooleanField(
+ default=False,
+ help_text="If selected the image will be scaled up if necessary to fit the supplied dimensions. Cropped sizes will be upscaled regardless of this setting.",
+ verbose_name="upscale images?",
+ ),
+ ),
+ (
+ "crop",
+ models.BooleanField(
+ default=False,
+ help_text="If selected the image will be scaled and cropped to fit the supplied dimensions.",
+ verbose_name="crop to fit?",
+ ),
+ ),
+ (
+ "pre_cache",
+ models.BooleanField(
+ default=False,
+ help_text="If selected this photo size will be pre-cached as photos are added.",
+ verbose_name="pre-cache?",
+ ),
+ ),
+ (
+ "increment_count",
+ models.BooleanField(
+ default=False,
+ help_text='If selected the image\'s "view_count" will be incremented when this photo size is displayed.',
+ verbose_name="increment view count?",
+ ),
+ ),
],
options={
- 'verbose_name': 'photo size',
- 'verbose_name_plural': 'photo sizes',
- 'ordering': ['width', 'height'],
+ "verbose_name": "photo size",
+ "verbose_name_plural": "photo sizes",
+ "ordering": ["width", "height"],
},
),
migrations.CreateModel(
- name='Tag',
+ name="Tag",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=250, unique=True, verbose_name='name')),
- ('slug', models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', max_length=250, unique=True, verbose_name='slug')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(max_length=250, unique=True, verbose_name="name"),
+ ),
+ (
+ "slug",
+ models.SlugField(
+ help_text='A "slug" is a unique URL-friendly title for an object.',
+ max_length=250,
+ unique=True,
+ verbose_name="slug",
+ ),
+ ),
],
options={
- 'verbose_name': 'tag',
- 'verbose_name_plural': 'tags',
- 'ordering': ['name'],
+ "verbose_name": "tag",
+ "verbose_name_plural": "tags",
+ "ordering": ["name"],
},
),
migrations.CreateModel(
- name='Photo',
+ name="Photo",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('image', models.ImageField(upload_to=photologue.models.get_storage_path, verbose_name='image')),
- ('date_taken', models.DateTimeField(blank=True, help_text='Date image was taken; is obtained from the image EXIF data.', null=True, verbose_name='date taken')),
- ('view_count', models.PositiveIntegerField(default=0, editable=False, verbose_name='view count')),
- ('crop_from', models.CharField(blank=True, choices=[('top', 'Top'), ('right', 'Right'), ('bottom', 'Bottom'), ('left', 'Left'), ('center', 'Center (Default)')], default='center', max_length=10, verbose_name='crop from')),
- ('title', models.CharField(max_length=250, unique=True, verbose_name='title')),
- ('slug', models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', max_length=250, unique=True, verbose_name='slug')),
- ('caption', models.TextField(blank=True, verbose_name='caption')),
- ('date_added', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date added')),
- ('license', models.CharField(blank=True, max_length=255, verbose_name='license')),
- ('is_public', models.BooleanField(default=True, help_text='Public photographs will be displayed in the default views.', verbose_name='is public')),
- ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "image",
+ models.ImageField(
+ upload_to=photologue.models.get_storage_path,
+ verbose_name="image",
+ ),
+ ),
+ (
+ "date_taken",
+ models.DateTimeField(
+ blank=True,
+ help_text="Date image was taken; is obtained from the image EXIF data.",
+ null=True,
+ verbose_name="date taken",
+ ),
+ ),
+ (
+ "view_count",
+ models.PositiveIntegerField(
+ default=0, editable=False, verbose_name="view count"
+ ),
+ ),
+ (
+ "crop_from",
+ models.CharField(
+ blank=True,
+ choices=[
+ ("top", "Top"),
+ ("right", "Right"),
+ ("bottom", "Bottom"),
+ ("left", "Left"),
+ ("center", "Center (Default)"),
+ ],
+ default="center",
+ max_length=10,
+ verbose_name="crop from",
+ ),
+ ),
+ (
+ "title",
+ models.CharField(max_length=250, unique=True, verbose_name="title"),
+ ),
+ (
+ "slug",
+ models.SlugField(
+ help_text='A "slug" is a unique URL-friendly title for an object.',
+ max_length=250,
+ unique=True,
+ verbose_name="slug",
+ ),
+ ),
+ ("caption", models.TextField(blank=True, verbose_name="caption")),
+ (
+ "date_added",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="date added"
+ ),
+ ),
+ (
+ "license",
+ models.CharField(
+ blank=True, max_length=255, verbose_name="license"
+ ),
+ ),
+ (
+ "is_public",
+ models.BooleanField(
+ default=True,
+ help_text="Public photographs will be displayed in the default views.",
+ verbose_name="is public",
+ ),
+ ),
+ (
+ "owner",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="owner",
+ ),
+ ),
],
options={
- 'verbose_name': 'photo',
- 'verbose_name_plural': 'photos',
- 'ordering': ['-date_added'],
- 'get_latest_by': 'date_added',
+ "verbose_name": "photo",
+ "verbose_name_plural": "photos",
+ "ordering": ["-date_added"],
+ "get_latest_by": "date_added",
},
),
migrations.CreateModel(
- name='Gallery',
+ name="Gallery",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('date_added', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date published')),
- ('title', models.CharField(max_length=250, unique=True, verbose_name='title')),
- ('slug', models.SlugField(help_text='A "slug" is a unique URL-friendly title for an object.', max_length=250, unique=True, verbose_name='title slug')),
- ('date_start', models.DateField(default=django.utils.timezone.now, verbose_name='start date')),
- ('date_end', models.DateField(blank=True, null=True, verbose_name='end date')),
- ('description', models.TextField(blank=True, verbose_name='description')),
- ('is_public', models.BooleanField(default=True, help_text='Public galleries will be displayed in the default views.', verbose_name='is public')),
- ('photos', models.ManyToManyField(blank=True, related_name='galleries', to='photologue.Photo', verbose_name='photos')),
- ('tags', models.ManyToManyField(blank=True, related_name='galleries', to='photologue.Tag', verbose_name='tags')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "date_added",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="date published"
+ ),
+ ),
+ (
+ "title",
+ models.CharField(max_length=250, unique=True, verbose_name="title"),
+ ),
+ (
+ "slug",
+ models.SlugField(
+ help_text='A "slug" is a unique URL-friendly title for an object.',
+ max_length=250,
+ unique=True,
+ verbose_name="title slug",
+ ),
+ ),
+ (
+ "date_start",
+ models.DateField(
+ default=django.utils.timezone.now, verbose_name="start date"
+ ),
+ ),
+ (
+ "date_end",
+ models.DateField(blank=True, null=True, verbose_name="end date"),
+ ),
+ (
+ "description",
+ models.TextField(blank=True, verbose_name="description"),
+ ),
+ (
+ "is_public",
+ models.BooleanField(
+ default=True,
+ help_text="Public galleries will be displayed in the default views.",
+ verbose_name="is public",
+ ),
+ ),
+ (
+ "photos",
+ models.ManyToManyField(
+ blank=True,
+ related_name="galleries",
+ to="photologue.Photo",
+ verbose_name="photos",
+ ),
+ ),
+ (
+ "tags",
+ models.ManyToManyField(
+ blank=True,
+ related_name="galleries",
+ to="photologue.Tag",
+ verbose_name="tags",
+ ),
+ ),
],
options={
- 'verbose_name': 'gallery',
- 'verbose_name_plural': 'galleries',
- 'ordering': ['-date_added'],
- 'get_latest_by': 'date_added',
+ "verbose_name": "gallery",
+ "verbose_name_plural": "galleries",
+ "ordering": ["-date_added"],
+ "get_latest_by": "date_added",
},
),
]
diff --git a/photologue/migrations/0002_auto_20220130_1020.py b/photologue/migrations/0002_auto_20220130_1020.py
index 21338b0..9355753 100644
--- a/photologue/migrations/0002_auto_20220130_1020.py
+++ b/photologue/migrations/0002_auto_20220130_1020.py
@@ -6,16 +6,21 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('photologue', '0001_initial'),
+ ("photologue", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
- name='gallery',
- options={'get_latest_by': 'date_start', 'ordering': ['-date_start'], 'verbose_name': 'gallery', 'verbose_name_plural': 'galleries'},
+ name="gallery",
+ options={
+ "get_latest_by": "date_start",
+ "ordering": ["-date_start"],
+ "verbose_name": "gallery",
+ "verbose_name_plural": "galleries",
+ },
),
migrations.RemoveField(
- model_name='gallery',
- name='date_added',
+ model_name="gallery",
+ name="date_added",
),
]
diff --git a/photologue/migrations/0003_remove_gallery_is_public.py b/photologue/migrations/0003_remove_gallery_is_public.py
index 8092038..2554495 100644
--- a/photologue/migrations/0003_remove_gallery_is_public.py
+++ b/photologue/migrations/0003_remove_gallery_is_public.py
@@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
- ('photologue', '0002_auto_20220130_1020'),
+ ("photologue", "0002_auto_20220130_1020"),
]
operations = [
migrations.RemoveField(
- model_name='gallery',
- name='is_public',
+ model_name="gallery",
+ name="is_public",
),
]
diff --git a/photologue/models.py b/photologue/models.py
index 0072f6d..29054bb 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -25,31 +25,37 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from PIL import Image, ImageFile, ImageFilter
-logger = logging.getLogger('photologue.models')
+logger = logging.getLogger("photologue.models")
# Default limit for gallery.latest
-LATEST_LIMIT = getattr(settings, 'PHOTOLOGUE_GALLERY_LATEST_LIMIT', None)
+LATEST_LIMIT = getattr(settings, "PHOTOLOGUE_GALLERY_LATEST_LIMIT", None)
# max_length setting for the ImageModel ImageField
-IMAGE_FIELD_MAX_LENGTH = getattr(settings, 'PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH', 100)
+IMAGE_FIELD_MAX_LENGTH = getattr(settings, "PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH", 100)
# Modify image file buffer size.
-ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10)
+ImageFile.MAXBLOCK = getattr(settings, "PHOTOLOGUE_MAXBLOCK", 256 * 2 ** 10)
# Look for user function to define file paths
-PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None)
+PHOTOLOGUE_PATH = getattr(settings, "PHOTOLOGUE_PATH", None)
if PHOTOLOGUE_PATH is not None:
if callable(PHOTOLOGUE_PATH):
get_storage_path = PHOTOLOGUE_PATH
else:
- parts = PHOTOLOGUE_PATH.split('.')
- module_name = '.'.join(parts[:-1])
+ parts = PHOTOLOGUE_PATH.split(".")
+ module_name = ".".join(parts[:-1])
module = import_module(module_name)
get_storage_path = getattr(module, parts[-1])
else:
+
def get_storage_path(instance, filename):
- fn = unicodedata.normalize('NFKD', force_str(filename)).encode('ascii', 'ignore').decode('ascii')
- return os.path.join('photos', fn)
+ fn = (
+ unicodedata.normalize("NFKD", force_str(filename))
+ .encode("ascii", "ignore")
+ .decode("ascii")
+ )
+ return os.path.join("photos", fn)
+
# Exif Orientation values
# Value 0thRow 0thColumn
@@ -73,42 +79,47 @@ IMAGE_EXIF_ORIENTATION_MAP = {
# Quality options for JPEG images
JPEG_QUALITY_CHOICES = (
- (30, _('Very Low')),
- (40, _('Low')),
- (50, _('Medium-Low')),
- (60, _('Medium')),
- (70, _('Medium-High')),
- (80, _('High')),
- (90, _('Very High')),
+ (30, _("Very Low")),
+ (40, _("Low")),
+ (50, _("Medium-Low")),
+ (60, _("Medium")),
+ (70, _("Medium-High")),
+ (80, _("High")),
+ (90, _("Very High")),
)
# choices for new crop_anchor field in Photo
CROP_ANCHOR_CHOICES = (
- ('top', _('Top')),
- ('right', _('Right')),
- ('bottom', _('Bottom')),
- ('left', _('Left')),
- ('center', _('Center (Default)')),
+ ("top", _("Top")),
+ ("right", _("Right")),
+ ("bottom", _("Bottom")),
+ ("left", _("Left")),
+ ("center", _("Center (Default)")),
)
IMAGE_TRANSPOSE_CHOICES = (
- ('FLIP_LEFT_RIGHT', _('Flip left to right')),
- ('FLIP_TOP_BOTTOM', _('Flip top to bottom')),
- ('ROTATE_90', _('Rotate 90 degrees counter-clockwise')),
- ('ROTATE_270', _('Rotate 90 degrees clockwise')),
- ('ROTATE_180', _('Rotate 180 degrees')),
+ ("FLIP_LEFT_RIGHT", _("Flip left to right")),
+ ("FLIP_TOP_BOTTOM", _("Flip top to bottom")),
+ ("ROTATE_90", _("Rotate 90 degrees counter-clockwise")),
+ ("ROTATE_270", _("Rotate 90 degrees clockwise")),
+ ("ROTATE_180", _("Rotate 180 degrees")),
)
# Prepare a list of image filters
filter_names = []
for n in dir(ImageFilter):
klass = getattr(ImageFilter, n)
- if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \
- hasattr(klass, 'name'):
+ if (
+ isclass(klass)
+ and issubclass(klass, ImageFilter.BuiltinFilter)
+ and hasattr(klass, "name")
+ ):
filter_names.append(klass.__name__)
-IMAGE_FILTERS_HELP_TEXT = _('Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE"'
- '. Image filters will be applied in order. The following filters are available: %s.'
- % (', '.join(filter_names)))
+IMAGE_FILTERS_HELP_TEXT = _(
+ 'Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE"'
+ ". Image filters will be applied in order. The following filters are available: %s."
+ % (", ".join(filter_names))
+)
size_method_map = {}
@@ -119,22 +130,22 @@ class TagField(models.CharField):
"""
def __init__(self, **kwargs):
- default_kwargs = {'max_length': 255, 'blank': True}
+ default_kwargs = {"max_length": 255, "blank": True}
default_kwargs.update(kwargs)
super().__init__(**default_kwargs)
def get_internal_type(self):
- return 'CharField'
+ return "CharField"
class Gallery(models.Model):
- title = models.CharField(_('title'),
- max_length=250,
- unique=True)
- slug = models.SlugField(_('title slug'),
- unique=True,
- max_length=250,
- help_text=_('A "slug" is a unique URL-friendly title for an object.'))
+ title = models.CharField(_("title"), max_length=250, unique=True)
+ slug = models.SlugField(
+ _("title slug"),
+ unique=True,
+ max_length=250,
+ help_text=_('A "slug" is a unique URL-friendly title for an object.'),
+ )
date_start = models.DateField(
default=now,
verbose_name=_("start date"),
@@ -144,30 +155,31 @@ class Gallery(models.Model):
null=True,
verbose_name=_("end date"),
)
- description = models.TextField(_('description'),
- blank=True)
+ description = models.TextField(_("description"), blank=True)
tags = models.ManyToManyField(
- 'photologue.Tag',
- related_name='galleries',
- verbose_name=_('tags'),
+ "photologue.Tag",
+ related_name="galleries",
+ verbose_name=_("tags"),
+ blank=True,
+ )
+ photos = models.ManyToManyField(
+ "photologue.Photo",
+ related_name="galleries",
+ verbose_name=_("photos"),
blank=True,
)
- photos = models.ManyToManyField('photologue.Photo',
- related_name='galleries',
- verbose_name=_('photos'),
- blank=True)
class Meta:
- ordering = ['-date_start']
- get_latest_by = 'date_start'
- verbose_name = _('gallery')
- verbose_name_plural = _('galleries')
+ ordering = ["-date_start"]
+ get_latest_by = "date_start"
+ verbose_name = _("gallery")
+ verbose_name_plural = _("galleries")
def __str__(self):
return f"{ self.title } ({self.date_start})"
def get_absolute_url(self):
- return reverse('photologue:pl-gallery', args=[self.slug])
+ return reverse("photologue:pl-gallery", args=[self.slug])
def sample(self, public=True):
"""Return a sample of photos, ordered at random."""
@@ -191,26 +203,28 @@ class Gallery(models.Model):
"""Return a count of private photos in this gallery."""
return self.photos.filter(is_public=False).count()
- photo_count.short_description = _('count')
- photo_private_count.short_description = _('private count')
+ photo_count.short_description = _("count")
+ photo_private_count.short_description = _("private count")
class ImageModel(models.Model):
- image = models.ImageField(_('image'),
- max_length=IMAGE_FIELD_MAX_LENGTH,
- upload_to=get_storage_path)
- date_taken = models.DateTimeField(_('date taken'),
- null=True,
- blank=True,
- help_text=_('Date image was taken; is obtained from the image EXIF data.'))
- view_count = models.PositiveIntegerField(_('view count'),
- default=0,
- editable=False)
- crop_from = models.CharField(_('crop from'),
- blank=True,
- max_length=10,
- default='center',
- choices=CROP_ANCHOR_CHOICES)
+ image = models.ImageField(
+ _("image"), max_length=IMAGE_FIELD_MAX_LENGTH, upload_to=get_storage_path
+ )
+ date_taken = models.DateTimeField(
+ _("date taken"),
+ null=True,
+ blank=True,
+ help_text=_("Date image was taken; is obtained from the image EXIF data."),
+ )
+ view_count = models.PositiveIntegerField(_("view count"), default=0, editable=False)
+ crop_from = models.CharField(
+ _("crop from"),
+ blank=True,
+ max_length=10,
+ default="center",
+ choices=CROP_ANCHOR_CHOICES,
+ )
class Meta:
abstract = True
@@ -220,38 +234,44 @@ class ImageModel(models.Model):
if file:
tags = exifread.process_file(file)
else:
- with self.image.storage.open(self.image.name, 'rb') as file:
+ with self.image.storage.open(self.image.name, "rb") as file:
tags = exifread.process_file(file, details=False)
return tags
except Exception:
return {}
def admin_thumbnail(self):
- func = getattr(self, 'get_admin_thumbnail_url', None)
+ func = getattr(self, "get_admin_thumbnail_url", None)
if func is None:
return _('An "admin_thumbnail" photo size has not been defined.')
else:
- if hasattr(self, 'get_absolute_url'):
- return mark_safe('
'.format(self.get_absolute_url(), func()))
+ if hasattr(self, "get_absolute_url"):
+ return mark_safe(
+ '
'.format(
+ self.get_absolute_url(), func()
+ )
+ )
else:
- return mark_safe('
'.format(self.image.url, func()))
+ return mark_safe(
+ '
'.format(self.image.url, func())
+ )
- admin_thumbnail.short_description = _('Thumbnail')
+ admin_thumbnail.short_description = _("Thumbnail")
admin_thumbnail.allow_tags = True
def cache_path(self):
return os.path.join(os.path.dirname(self.image.name), "cache")
def cache_url(self):
- return '/'.join([os.path.dirname(self.image.url), "cache"])
+ return "/".join([os.path.dirname(self.image.url), "cache"])
def image_filename(self):
return os.path.basename(force_str(self.image.name))
def _get_filename_for_size(self, size):
- size = getattr(size, 'name', size)
+ size = getattr(size, "name", size)
base, ext = os.path.splitext(self.image_filename())
- return ''.join([base, '_', size, ext])
+ return "".join([base, "_", size, ext])
def _get_size_photosize(self, size):
return PhotoSizeCache().sizes.get(size)
@@ -261,8 +281,9 @@ class ImageModel(models.Model):
if not self.size_exists(photosize):
self.create_size(photosize)
try:
- return Image.open(self.image.storage.open(
- self._get_size_filename(size))).size
+ return Image.open(
+ self.image.storage.open(self._get_size_filename(size))
+ ).size
except Exception:
return None
@@ -272,14 +293,18 @@ class ImageModel(models.Model):
self.create_size(photosize)
if photosize.increment_count:
self.increment_count()
- return '/'.join([
- self.cache_url(),
- filepath_to_uri(self._get_filename_for_size(photosize.name))])
+ return "/".join(
+ [
+ self.cache_url(),
+ filepath_to_uri(self._get_filename_for_size(photosize.name)),
+ ]
+ )
def _get_size_filename(self, size):
photosize = PhotoSizeCache().sizes.get(size)
- return smart_str(os.path.join(self.cache_path(),
- self._get_filename_for_size(photosize.name)))
+ return smart_str(
+ os.path.join(self.cache_path(), self._get_filename_for_size(photosize.name))
+ )
def increment_count(self):
self.view_count += 1
@@ -291,7 +316,7 @@ class ImageModel(models.Model):
init_size_method_map()
di = size_method_map.get(name, None)
if di is not None:
- result = partial(getattr(self, di['base_name']), di['size'])
+ result = partial(getattr(self, di["base_name"]), di["size"])
setattr(self, name, result)
return result
else:
@@ -313,38 +338,45 @@ class ImageModel(models.Model):
new_width, new_height = photosize.size
if photosize.crop:
ratio = max(float(new_width) / cur_width, float(new_height) / cur_height)
- x = (cur_width * ratio)
- y = (cur_height * ratio)
+ x = cur_width * ratio
+ y = cur_height * ratio
xd = abs(new_width - x)
yd = abs(new_height - y)
x_diff = int(xd / 2)
y_diff = int(yd / 2)
- if self.crop_from == 'top':
+ if self.crop_from == "top":
box = (int(x_diff), 0, int(x_diff + new_width), new_height)
- elif self.crop_from == 'left':
+ elif self.crop_from == "left":
box = (0, int(y_diff), new_width, int(y_diff + new_height))
- elif self.crop_from == 'bottom':
+ elif self.crop_from == "bottom":
# y - yd = new_height
box = (int(x_diff), int(yd), int(x_diff + new_width), int(y))
- elif self.crop_from == 'right':
+ elif self.crop_from == "right":
# x - xd = new_width
box = (int(xd), int(y_diff), int(x), int(y_diff + new_height))
else:
- box = (int(x_diff), int(y_diff), int(x_diff + new_width), int(y_diff + new_height))
+ box = (
+ int(x_diff),
+ int(y_diff),
+ int(x_diff + new_width),
+ int(y_diff + new_height),
+ )
im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
else:
if not new_width == 0 and not new_height == 0:
- ratio = min(float(new_width) / cur_width,
- float(new_height) / cur_height)
+ ratio = min(
+ float(new_width) / cur_width, float(new_height) / cur_height
+ )
else:
if new_width == 0:
ratio = float(new_height) / cur_height
else:
ratio = float(new_width) / cur_width
- new_dimensions = (int(round(cur_width * ratio)),
- int(round(cur_height * ratio)))
- if new_dimensions[0] > cur_width or \
- new_dimensions[1] > cur_height:
+ new_dimensions = (
+ int(round(cur_width * ratio)),
+ int(round(cur_height * ratio)),
+ )
+ if new_dimensions[0] > cur_width or new_dimensions[1] > cur_height:
if not photosize.upscale:
return im
im = im.resize(new_dimensions, Image.ANTIALIAS)
@@ -360,10 +392,16 @@ class ImageModel(models.Model):
# Save the original format
im_format = im.format
# Rotate if found & necessary
- if 'Image Orientation' in self.exif() and \
- self.exif().get('Image Orientation').values[0] in IMAGE_EXIF_ORIENTATION_MAP:
+ 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)
@@ -371,14 +409,13 @@ class ImageModel(models.Model):
im_filename = getattr(self, "get_%s_filename" % photosize.name)()
try:
buffer = BytesIO()
- if im_format != 'JPEG':
+ if im_format != "JPEG":
im.save(buffer, im_format)
else:
# Issue #182 - test fix from https://github.com/bashu/django-watermark/issues/31
- if im.mode.endswith('A'):
+ if im.mode.endswith("A"):
im = im.convert(im.mode[:-1])
- im.save(buffer, 'JPEG', quality=int(photosize.quality),
- optimize=True)
+ im.save(buffer, "JPEG", quality=int(photosize.quality), optimize=True)
buffer_contents = ContentFile(buffer.getvalue())
self.image.storage.save(im_filename, buffer_contents)
except OSError as e:
@@ -411,7 +448,7 @@ class ImageModel(models.Model):
self._old_image = self.image
def save(self, *args, **kwargs):
- recreate = kwargs.pop('recreate', False)
+ recreate = kwargs.pop("recreate", False)
image_has_changed = False
if self._get_pk_val() and (self._old_image != self.image):
image_has_changed = True
@@ -423,26 +460,39 @@ class ImageModel(models.Model):
self.image = self._old_image
self.clear_cache()
self.image = new_image # Back to the new image.
- self._old_image.storage.delete(self._old_image.name) # Delete (old) base image.
+ self._old_image.storage.delete(
+ self._old_image.name
+ ) # Delete (old) base image.
if self.date_taken is None or image_has_changed:
# Attempt to get the date the photo was taken from the EXIF data.
try:
- exif_date = self.exif(self.image.file).get('EXIF DateTimeOriginal', None)
+ exif_date = self.exif(self.image.file).get(
+ "EXIF DateTimeOriginal", None
+ )
if exif_date is not None:
d, t = exif_date.values.split()
- year, month, day = d.split(':')
- hour, minute, second = t.split(':')
- self.date_taken = datetime(int(year), int(month), int(day),
- int(hour), int(minute), int(second))
+ year, month, day = d.split(":")
+ hour, minute, second = t.split(":")
+ self.date_taken = datetime(
+ int(year),
+ int(month),
+ int(day),
+ int(hour),
+ int(minute),
+ int(second),
+ )
except Exception:
- logger.error('Failed to read EXIF DateTimeOriginal', exc_info=True)
+ logger.error("Failed to read EXIF DateTimeOriginal", exc_info=True)
super().save(*args, **kwargs)
self.pre_cache(recreate)
def delete(self):
- assert self._get_pk_val() is not None, \
- "%s object can't be deleted because its %s attribute is set to None." % \
- (self._meta.object_name, self._meta.pk.attname)
+ assert (
+ self._get_pk_val() is not None
+ ), "%s object can't be deleted because its %s attribute is set to None." % (
+ self._meta.object_name,
+ self._meta.pk.attname,
+ )
self.clear_cache()
# Files associated to a FileField have to be manually deleted:
# https://docs.djangoproject.com/en/dev/releases/1.3/#deleting-a-model-doesn-t-delete-associated-files
@@ -454,17 +504,15 @@ class ImageModel(models.Model):
class Photo(ImageModel):
- title = models.CharField(_('title'),
- max_length=250,
- unique=True)
- slug = models.SlugField(_('slug'),
- unique=True,
- max_length=250,
- help_text=_('A "slug" is a unique URL-friendly title for an object.'))
- caption = models.TextField(_('caption'),
- blank=True)
- date_added = models.DateTimeField(_('date added'),
- default=now)
+ title = models.CharField(_("title"), max_length=250, unique=True)
+ slug = models.SlugField(
+ _("slug"),
+ unique=True,
+ max_length=250,
+ help_text=_('A "slug" is a unique URL-friendly title for an object.'),
+ )
+ caption = models.TextField(_("caption"), blank=True)
+ date_added = models.DateTimeField(_("date added"), default=now)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
@@ -475,15 +523,17 @@ class Photo(ImageModel):
blank=True,
verbose_name=_("license"),
)
- is_public = models.BooleanField(_('is public'),
- default=True,
- help_text=_('Public photographs will be displayed in the default views.'))
+ is_public = models.BooleanField(
+ _("is public"),
+ default=True,
+ help_text=_("Public photographs will be displayed in the default views."),
+ )
class Meta:
# We do not have a reliable date for ordering, so let's use
# the title which is incremented by most cameras
- ordering = ['title']
- get_latest_by = 'date_added'
+ ordering = ["title"]
+ get_latest_by = "date_added"
verbose_name = _("photo")
verbose_name_plural = _("photos")
@@ -502,7 +552,7 @@ class Photo(ImageModel):
super().save(*args, **kwargs)
def get_absolute_url(self):
- return reverse('photologue:pl-photo', args=[self.pk])
+ return reverse("photologue:pl-photo", args=[self.pk])
def public_galleries(self):
"""Return the public galleries to which this photo belongs."""
@@ -513,10 +563,10 @@ class Photo(ImageModel):
We assume that the gallery and all its photos are on the same site.
"""
if not self.is_public:
- raise ValueError('Cannot determine neighbours of a non-public photo.')
+ raise ValueError("Cannot determine neighbours of a non-public photo.")
photos = gallery.photos.filter(is_public=True)
if self not in photos:
- raise ValueError('Photo does not belong to gallery.')
+ raise ValueError("Photo does not belong to gallery.")
previous = None
for photo in photos:
if photo == self:
@@ -528,10 +578,10 @@ class Photo(ImageModel):
We assume that the gallery and all its photos are on the same site.
"""
if not self.is_public:
- raise ValueError('Cannot determine neighbours of a non-public photo.')
+ raise ValueError("Cannot determine neighbours of a non-public photo.")
photos = gallery.photos.filter(is_public=True)
if self not in photos:
- raise ValueError('Photo does not belong to gallery.')
+ raise ValueError("Photo does not belong to gallery.")
matched = False
for photo in photos:
if matched:
@@ -546,51 +596,79 @@ class PhotoSize(models.Model):
so the name has to follow the same restrictions as any Python method name,
e.g. no spaces or non-ascii characters."""
- name = models.CharField(_('name'),
- max_length=40,
- unique=True,
- help_text=_(
- 'Photo size name should contain only letters, numbers and underscores. Examples: '
- '"thumbnail", "display", "small", "main_page_widget".'),
- validators=[RegexValidator(regex='^[a-z0-9_]+$',
- message='Use only plain lowercase letters (ASCII), numbers and '
- 'underscores.'
- )]
- )
- width = models.PositiveIntegerField(_('width'),
- default=0,
- help_text=_(
- 'If width is set to "0" the image will be scaled to the supplied height.'))
- height = models.PositiveIntegerField(_('height'),
- default=0,
- help_text=_(
- 'If height is set to "0" the image will be scaled to the supplied width'))
- quality = models.PositiveIntegerField(_('quality'),
- choices=JPEG_QUALITY_CHOICES,
- default=70,
- help_text=_('JPEG image quality.'))
- upscale = models.BooleanField(_('upscale images?'),
- default=False,
- help_text=_('If selected the image will be scaled up if necessary to fit the '
- 'supplied dimensions. Cropped sizes will be upscaled regardless of this '
- 'setting.')
- )
- crop = models.BooleanField(_('crop to fit?'),
- default=False,
- help_text=_('If selected the image will be scaled and cropped to fit the supplied '
- 'dimensions.'))
- pre_cache = models.BooleanField(_('pre-cache?'),
- default=False,
- help_text=_('If selected this photo size will be pre-cached as photos are added.'))
- increment_count = models.BooleanField(_('increment view count?'),
- default=False,
- help_text=_('If selected the image\'s "view_count" will be incremented when '
- 'this photo size is displayed.'))
+ name = models.CharField(
+ _("name"),
+ max_length=40,
+ unique=True,
+ help_text=_(
+ "Photo size name should contain only letters, numbers and underscores. Examples: "
+ '"thumbnail", "display", "small", "main_page_widget".'
+ ),
+ validators=[
+ RegexValidator(
+ regex="^[a-z0-9_]+$",
+ message="Use only plain lowercase letters (ASCII), numbers and "
+ "underscores.",
+ )
+ ],
+ )
+ width = models.PositiveIntegerField(
+ _("width"),
+ default=0,
+ help_text=_(
+ 'If width is set to "0" the image will be scaled to the supplied height.'
+ ),
+ )
+ height = models.PositiveIntegerField(
+ _("height"),
+ default=0,
+ help_text=_(
+ 'If height is set to "0" the image will be scaled to the supplied width'
+ ),
+ )
+ quality = models.PositiveIntegerField(
+ _("quality"),
+ choices=JPEG_QUALITY_CHOICES,
+ default=70,
+ help_text=_("JPEG image quality."),
+ )
+ upscale = models.BooleanField(
+ _("upscale images?"),
+ default=False,
+ help_text=_(
+ "If selected the image will be scaled up if necessary to fit the "
+ "supplied dimensions. Cropped sizes will be upscaled regardless of this "
+ "setting."
+ ),
+ )
+ crop = models.BooleanField(
+ _("crop to fit?"),
+ default=False,
+ help_text=_(
+ "If selected the image will be scaled and cropped to fit the supplied "
+ "dimensions."
+ ),
+ )
+ pre_cache = models.BooleanField(
+ _("pre-cache?"),
+ default=False,
+ help_text=_(
+ "If selected this photo size will be pre-cached as photos are added."
+ ),
+ )
+ increment_count = models.BooleanField(
+ _("increment view count?"),
+ default=False,
+ help_text=_(
+ 'If selected the image\'s "view_count" will be incremented when '
+ "this photo size is displayed."
+ ),
+ )
class Meta:
- ordering = ['width', 'height']
- verbose_name = _('photo size')
- verbose_name_plural = _('photo sizes')
+ ordering = ["width", "height"]
+ verbose_name = _("photo size")
+ verbose_name_plural = _("photo sizes")
def __str__(self):
return self.name
@@ -607,7 +685,10 @@ class PhotoSize(models.Model):
if self.crop is True:
if self.width == 0 or self.height == 0:
raise ValidationError(
- _("Can only crop photos if both width and height dimensions are set."))
+ _(
+ "Can only crop photos if both width and height dimensions are set."
+ )
+ )
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
@@ -615,8 +696,12 @@ class PhotoSize(models.Model):
self.clear_cache()
def delete(self):
- assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." \
- % (self._meta.object_name, self._meta.pk.attname)
+ assert (
+ self._get_pk_val() is not None
+ ), "%s object can't be deleted because its %s attribute is set to None." % (
+ self._meta.object_name,
+ self._meta.pk.attname,
+ )
self.clear_cache()
super().delete()
@@ -648,33 +733,41 @@ class PhotoSizeCache:
def init_size_method_map():
global size_method_map
for size in PhotoSizeCache().sizes.keys():
- size_method_map['get_%s_size' % size] = \
- {'base_name': '_get_size_size', 'size': size}
- size_method_map['get_%s_photosize' % size] = \
- {'base_name': '_get_size_photosize', 'size': size}
- size_method_map['get_%s_url' % size] = \
- {'base_name': '_get_size_url', 'size': size}
- size_method_map['get_%s_filename' % size] = \
- {'base_name': '_get_size_filename', 'size': size}
+ size_method_map["get_%s_size" % size] = {
+ "base_name": "_get_size_size",
+ "size": size,
+ }
+ size_method_map["get_%s_photosize" % size] = {
+ "base_name": "_get_size_photosize",
+ "size": size,
+ }
+ size_method_map["get_%s_url" % size] = {
+ "base_name": "_get_size_url",
+ "size": size,
+ }
+ size_method_map["get_%s_filename" % size] = {
+ "base_name": "_get_size_filename",
+ "size": size,
+ }
class Tag(models.Model):
name = models.CharField(
max_length=250,
unique=True,
- verbose_name=_('name'),
+ verbose_name=_("name"),
)
slug = models.SlugField(
unique=True,
max_length=250,
- verbose_name=_('slug'),
+ verbose_name=_("slug"),
help_text=_('A "slug" is a unique URL-friendly title for an object.'),
)
class Meta:
- ordering = ['name']
- verbose_name = _('tag')
- verbose_name_plural = _('tags')
+ ordering = ["name"]
+ verbose_name = _("tag")
+ verbose_name_plural = _("tags")
def __str__(self):
return self.name
diff --git a/photologue/urls.py b/photologue/urls.py
index bb47615..eaddfa0 100644
--- a/photologue/urls.py
+++ b/photologue/urls.py
@@ -1,19 +1,39 @@
from django.urls import path, re_path
-from .views import (GalleryDetailView, GalleryArchiveIndexView,
- GalleryDownload, GalleryUpload, GalleryYearArchiveView,
- PhotoDetailView, PhotoDeleteView, PhotoReportView, TagDetail)
+from .views import (
+ GalleryArchiveIndexView,
+ GalleryDetailView,
+ GalleryDownload,
+ GalleryUpload,
+ GalleryYearArchiveView,
+ PhotoDeleteView,
+ PhotoDetailView,
+ PhotoReportView,
+ TagDetail,
+)
-app_name = 'photologue'
+app_name = "photologue"
urlpatterns = [
- path('tag//', TagDetail.as_view(), name='tag-detail'),
- path('gallery/', GalleryArchiveIndexView.as_view(), name='pl-gallery-archive'),
- re_path(r'^gallery/(?P\d{4})/$', GalleryYearArchiveView.as_view(), name='pl-gallery-archive-year'),
- path('gallery//', GalleryDetailView.as_view(), name='pl-gallery'),
- path('gallery///', GalleryDetailView.as_view(), name='pl-gallery-owner'),
- path('gallery//download/', GalleryDownload.as_view(), name='pl-gallery-download'),
- path('photo//', PhotoDetailView.as_view(), name='pl-photo'),
- path('photo//delete/', PhotoDeleteView.as_view(), name='pl-photo-delete'),
- path('photo//report/', PhotoReportView.as_view(), name='pl-photo-report'),
- path('upload/', GalleryUpload.as_view(), name='pl-gallery-upload'),
+ path("tag//", TagDetail.as_view(), name="tag-detail"),
+ path("gallery/", GalleryArchiveIndexView.as_view(), name="pl-gallery-archive"),
+ re_path(
+ r"^gallery/(?P\d{4})/$",
+ GalleryYearArchiveView.as_view(),
+ name="pl-gallery-archive-year",
+ ),
+ path("gallery//", GalleryDetailView.as_view(), name="pl-gallery"),
+ path(
+ "gallery///",
+ GalleryDetailView.as_view(),
+ name="pl-gallery-owner",
+ ),
+ path(
+ "gallery//download/",
+ GalleryDownload.as_view(),
+ name="pl-gallery-download",
+ ),
+ path("photo//", PhotoDetailView.as_view(), name="pl-photo"),
+ path("photo//delete/", PhotoDeleteView.as_view(), name="pl-photo-delete"),
+ path("photo//report/", PhotoReportView.as_view(), name="pl-photo-report"),
+ path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"),
]
diff --git a/photologue/views.py b/photologue/views.py
index 3df8fa6..0b245ec 100644
--- a/photologue/views.py
+++ b/photologue/views.py
@@ -7,18 +7,17 @@ from io import BytesIO
from pathlib import Path
from django.contrib import messages
-from django.contrib.auth.mixins import (LoginRequiredMixin,
- PermissionRequiredMixin)
+from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.mail import mail_managers
from django.db import IntegrityError
from django.http import HttpResponse
+from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.text import slugify
from django.views.generic.dates import ArchiveIndexView, YearArchiveView
from django.views.generic.detail import DetailView
-from django.views.generic.edit import FormView, DeleteView
+from django.views.generic.edit import DeleteView, FormView
from PIL import Image
-from django.shortcuts import redirect
from .forms import UploadForm
from .models import Gallery, Photo, Tag
@@ -26,7 +25,7 @@ from .models import Gallery, Photo, Tag
class GalleryDateView(LoginRequiredMixin):
model = Gallery
- date_field = 'date_start'
+ date_field = "date_start"
def get_queryset(self):
"""Hide galleries with only private photos"""
@@ -59,19 +58,19 @@ class PhotoDetailView(LoginRequiredMixin, DetailView):
class PhotoDeleteView(PermissionRequiredMixin, DeleteView):
model = Photo
- permission_required = 'photologue.delete_photo'
+ permission_required = "photologue.delete_photo"
def get_success_url(self):
galleries = self.object.galleries.all()
if not galleries:
- return reverse_lazy('photologue:pl-gallery-archive')
+ return reverse_lazy("photologue:pl-gallery-archive")
slug = galleries[0].slug
- return reverse_lazy('photologue:pl-gallery', args=[slug])
+ return reverse_lazy("photologue:pl-gallery", args=[slug])
class PhotoReportView(LoginRequiredMixin, DetailView):
model = Photo
- template_name = 'photologue/photo_confirm_report.html'
+ template_name = "photologue/photo_confirm_report.html"
def post(self, request, *args, **kwargs):
"""
@@ -84,10 +83,10 @@ class PhotoReportView(LoginRequiredMixin, DetailView):
# Get gallery
galleries = photo.galleries.all()
- gallery_slug = galleries[0].slug if galleries else ''
+ gallery_slug = galleries[0].slug if galleries else ""
if not gallery_slug:
- url = reverse_lazy('photologue:pl-gallery-archive')
- url = reverse_lazy('photologue:pl-gallery', args=[gallery_slug])
+ url = reverse_lazy("photologue:pl-gallery-archive")
+ url = reverse_lazy("photologue:pl-gallery", args=[gallery_slug])
# Send mail to managers
mail_managers(
@@ -108,8 +107,9 @@ class TagDetail(LoginRequiredMixin, DetailView):
"""
current_tag = self.get_object().slug
context = super().get_context_data(**kwargs)
- context['galleries'] = Gallery.objects.filter(tags__slug=current_tag) \
- .order_by('-date_start')
+ context["galleries"] = Gallery.objects.filter(tags__slug=current_tag).order_by(
+ "-date_start"
+ )
return context
@@ -117,6 +117,7 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
"""
Gallery detail view to filter on photo owner
"""
+
model = Gallery
def get_context_data(self, **kwargs):
@@ -124,19 +125,19 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
# Non-staff members only see public photos
if self.request.user.is_staff:
- context['photos'] = self.object.photos.all()
+ context["photos"] = self.object.photos.all()
else:
- context['photos'] = self.object.photos.filter(is_public=True)
+ context["photos"] = self.object.photos.filter(is_public=True)
# List owners
- context['owners'] = []
- for photo in context['photos']:
- if photo.owner not in context['owners']:
- context['owners'].append(photo.owner)
+ context["owners"] = []
+ for photo in context["photos"]:
+ if photo.owner not in context["owners"]:
+ context["owners"].append(photo.owner)
# Filter on owner
- if 'owner' in self.kwargs:
- context['photos'] = context['photos'].filter(owner__id=self.kwargs['owner'])
+ if "owner" in self.kwargs:
+ context["photos"] = context["photos"].filter(owner__id=self.kwargs["owner"])
return context
@@ -158,8 +159,10 @@ class GalleryDownload(LoginRequiredMixin, DetailView):
zip_file.close()
# Return zip file
- response = HttpResponse(byte_data.getvalue(), content_type='application/x-zip-compressed')
- response['Content-Disposition'] = f"attachment; filename={gallery.slug}.zip"
+ response = HttpResponse(
+ byte_data.getvalue(), content_type="application/x-zip-compressed"
+ )
+ response["Content-Disposition"] = f"attachment; filename={gallery.slug}.zip"
return response
@@ -167,15 +170,16 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
"""
Form to upload new photos in a gallery
"""
+
form_class = UploadForm
template_name = "photologue/upload.html"
success_url = reverse_lazy("photologue:pl-gallery-upload")
- permission_required = 'photologue.add_gallery'
+ permission_required = "photologue.add_gallery"
def form_valid(self, form):
# Upload photos
# We take files from the request to support multiple upload
- files = self.request.FILES.getlist('file_field')
+ files = self.request.FILES.getlist("file_field")
gallery = form.get_or_create_gallery()
gallery_year = Path(str(gallery.date_start.year))
gallery_dir = gallery_year / gallery.slug
@@ -187,7 +191,9 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
opened.verify()
except Exception:
# Pillow doesn't recognize it as an image, skip it
- messages.error(self.request, f"{photo_file.name} was not recognized as an image")
+ messages.error(
+ self.request, f"{photo_file.name} was not recognized as an image"
+ )
failed_upload += 1
continue
@@ -203,7 +209,10 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
photo.save()
photo.galleries.set([gallery])
except IntegrityError:
- messages.error(self.request, f"{photo_file.name} was not uploaded. Maybe the photo was already uploaded.")
+ messages.error(
+ self.request,
+ f"{photo_file.name} was not uploaded. Maybe the photo was already uploaded.",
+ )
failed_upload += 1
# Notify user then managers
@@ -211,9 +220,13 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
messages.success(self.request, "All photos has been successfully uploaded.")
else:
n_success = len(files) - failed_upload
- messages.warning(self.request, f"Only {n_success} photos were successfully uploaded !")
+ messages.warning(
+ self.request, f"Only {n_success} photos were successfully uploaded !"
+ )
- gallery_title = form.cleaned_data['gallery'] or form.cleaned_data.get('new_gallery_title', '')
+ gallery_title = form.cleaned_data["gallery"] or form.cleaned_data.get(
+ "new_gallery_title", ""
+ )
photos = ", ".join(f.name for f in files)
mail_managers(
subject="New photos upload",