Format code using black
This commit is contained in:
parent
2ad0c8dbc7
commit
59136050fb
14 changed files with 809 additions and 413 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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('<a href="{}"><img src="{}"></a>'.format(self.get_absolute_url(), func()))
|
||||
if hasattr(self, "get_absolute_url"):
|
||||
return mark_safe(
|
||||
'<a href="{}"><img src="{}"></a>'.format(
|
||||
self.get_absolute_url(), func()
|
||||
)
|
||||
)
|
||||
else:
|
||||
return mark_safe('<a href="{}"><img src="{}"></a>'.format(self.image.url, func()))
|
||||
return mark_safe(
|
||||
'<a href="{}"><img src="{}"></a>'.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
|
||||
|
|
|
|||
|
|
@ -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/<slug:slug>/', TagDetail.as_view(), name='tag-detail'),
|
||||
path('gallery/', GalleryArchiveIndexView.as_view(), name='pl-gallery-archive'),
|
||||
re_path(r'^gallery/(?P<year>\d{4})/$', GalleryYearArchiveView.as_view(), name='pl-gallery-archive-year'),
|
||||
path('gallery/<slug:slug>/', GalleryDetailView.as_view(), name='pl-gallery'),
|
||||
path('gallery/<slug:slug>/<int:owner>/', GalleryDetailView.as_view(), name='pl-gallery-owner'),
|
||||
path('gallery/<slug:slug>/download/', GalleryDownload.as_view(), name='pl-gallery-download'),
|
||||
path('photo/<int:pk>/', PhotoDetailView.as_view(), name='pl-photo'),
|
||||
path('photo/<int:pk>/delete/', PhotoDeleteView.as_view(), name='pl-photo-delete'),
|
||||
path('photo/<int:pk>/report/', PhotoReportView.as_view(), name='pl-photo-report'),
|
||||
path('upload/', GalleryUpload.as_view(), name='pl-gallery-upload'),
|
||||
path("tag/<slug:slug>/", TagDetail.as_view(), name="tag-detail"),
|
||||
path("gallery/", GalleryArchiveIndexView.as_view(), name="pl-gallery-archive"),
|
||||
re_path(
|
||||
r"^gallery/(?P<year>\d{4})/$",
|
||||
GalleryYearArchiveView.as_view(),
|
||||
name="pl-gallery-archive-year",
|
||||
),
|
||||
path("gallery/<slug:slug>/", GalleryDetailView.as_view(), name="pl-gallery"),
|
||||
path(
|
||||
"gallery/<slug:slug>/<int:owner>/",
|
||||
GalleryDetailView.as_view(),
|
||||
name="pl-gallery-owner",
|
||||
),
|
||||
path(
|
||||
"gallery/<slug:slug>/download/",
|
||||
GalleryDownload.as_view(),
|
||||
name="pl-gallery-download",
|
||||
),
|
||||
path("photo/<int:pk>/", PhotoDetailView.as_view(), name="pl-photo"),
|
||||
path("photo/<int:pk>/delete/", PhotoDeleteView.as_view(), name="pl-photo-delete"),
|
||||
path("photo/<int:pk>/report/", PhotoReportView.as_view(), name="pl-photo-report"),
|
||||
path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue