I have successfully employed Photologue to present galleries of regularly-created data plot images. Of course, now that the capability has been established, an obscene number of data plots are being created and they need to be shared!
Scripting the process of uploading images and adding them to galleries using manage.py
from the Django shell is the next step; however, as an amateur with Django, I am having some difficulties.
Here is the custom command addphoto.py
that I have currently developed:
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from photologue.models import Photo, Gallery
import os
from datetime import datetime
import pytz
class Command(BaseCommand):
help = 'Adds a photo to Photologue.'
def add_arguments(self, parser):
parser.add_argument('imagefile', type=str)
parser.add_argument('--title', type=str)
parser.add_argument('--date_added', type=str, help="datetime string in 'YYYY-mm-dd HH:MM:SS' format [UTC]")
parser.add_argument('--gallery', type=str)
def handle(self, *args, **options):
imagefile = options['imagefile']
if options['title']:
title = options['title']
else:
base = os.path.basename(imagefile)
title = os.path.splitext(base)[0]
if options['date_added']:
date_added = datetime.strptime(options['date_added'],'%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.UTC)
else:
date_added = timezone.now()
p = Photo(image=imagefile, title=title, date_added=date_added)
p.save()
Unfortunately, when executed with --traceback
, it results in the following:
./manage.py addphoto '../dataplots/monitoring/test.png' --traceback
Failed to read EXIF DateTimeOriginal
Traceback (most recent call last):
File "/home/user/mysite/photologue/models.py", line 494, in save
exif_date = self.EXIF(self.image.file).get('EXIF DateTimeOriginal', None)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/db/models/fields/files.py", line 51, in _get_file
self._file = self.storage.open(self.name, 'rb')
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 38, in open
return self._open(name, mode)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 300, in _open
return File(open(self.path(name), mode))
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 405, in path
return safe_join(self.location, name)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/utils/_os.py", line 78, in safe_join
'component ({})'.format(final_path, base_path))
django.core.exceptions.SuspiciousFileOperation: The joined path (/home/user/mysite/dataplots/monitoring/test.png) is located outside of the base path component (/home/user/mysite/media)
Traceback (most recent call last):
File "./manage.py", line 22, in <module>
execute_from_command_line(sys.argv)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/management/__init__.py", line 363, in execute_from_command_line
utility.execute()
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/management/__init__.py", line 355, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/management/base.py", line 283, in run_from_argv
self.execute(*args, **cmd_options)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/management/base.py", line 330, in execute
output = self.handle(*args, **options)
File "/home/user/mysite/photologue/management/commands/addphoto.py", line 36, in handle
p.save()
File "/home/user/mysite/photologue/models.py", line 553, in save
super(Photo, self).save(*args, **kwargs)
File "/home/user/mysite/photologue/models.py", line 504, in save
self.pre_cache()
File "/home/user/mysite/photologue/models.py", line 472, in pre_cache
self.create_size(photosize)
File "/home/user/mysite/photologue/models.py", line 411, in create_size
if self.size_exists(photosize):
File "/home/user/mysite/photologue/models.py", line 364, in size_exists
if self.image.storage.exists(func()):
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 392, in exists
return os.path.exists(self.path(name))
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 405, in path
return safe_join(self.location, name)
File "/home/user/mysite/venv/lib/python3.5/site-packages/django/utils/_os.py", line 78, in safe_join
'component ({})'.format(final_path, base_path))
django.core.exceptions.SuspiciousFileOperation: The joined path (/home/user/mysite/dataplots/monitoring/cache/test_thumbnail.png) is located outside of the base path component (/home/user/mysite/media)
Obviously, a copy of the image file was not put in the the media/
directory. Also, while the image
, title
, and date_added
columns are populated in the photologue_photos
table of the website database, the slug
column is not.
How can the file be uploaded to the MEDIA_ROOT
directory?
Here are the relevant snippets from the Photo
and ImageModel
models from the Photologue models.py
file, for reference:
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)
is_public = models.BooleanField(_('is public'),
default=True,
help_text=_('Public photographs will be displayed in the default views.'))
sites = models.ManyToManyField(Site, verbose_name=_(u'sites'),
blank=True)
objects = PhotoQuerySet.as_manager()
def save(self, *args, **kwargs):
if self.slug is None:
self.slug = slugify(self.title)
super(Photo, self).save(*args, **kwargs)
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)
effect = models.ForeignKey('photologue.PhotoEffect',
null=True,
blank=True,
related_name="%(class)s_related",
verbose_name=_('effect'))
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(ImageModel, self).__init__(*args, **kwargs)
self._old_image = self.image
def save(self, *args, **kwargs):
image_has_changed = False
if self._get_pk_val() and (self._old_image != self.image):
image_has_changed = True
# If we have changed the image, we need to clear from the cache all instances of the old
# image; clear_cache() works on the current (new) image, and in turn calls several other methods.
# Changing them all to act on the old image was a lot of changes, so instead we temporarily swap old
# and new images.
new_image = self.image
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.
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)
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))
except:
logger.error('Failed to read EXIF DateTimeOriginal', exc_info=True)
super(ImageModel, self).save(*args, **kwargs)
self.pre_cache()
Here is the get_storage_path
function, as requested:
# Look for user function to define file paths
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])
module = import_module(module_name)
get_storage_path = getattr(module, parts[-1])
else:
def get_storage_path(instance, filename):
fn = unicodedata.normalize('NFKD', force_text(filename)).encode('ascii', 'ignore').decode('ascii')
return os.path.join(PHOTOLOGUE_DIR, 'photos', fn)
Here is the working version of the custom addphoto.py
command that I ended up creating.
The image file needs to be within MEDIA_ROOT/photologue/photos
to facilitate the import. The command is executed using ./manage.py addphoto 'photologue/photos/test.png'
. Note that there is a --gallery
option to add the image to a gallery, provided the gallery's slug
.
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from photologue.models import Photo, Gallery
import os
from datetime import datetime
import pytz
class Command(BaseCommand):
help = 'Adds a photo to Photologue.'
def add_arguments(self, parser):
parser.add_argument('imagefile',
type=str)
parser.add_argument('--title',
type=str)
parser.add_argument('--date_added',
type=str,
help="datetime string in 'YYYY-mm-dd HH:MM:SS' format [UTC]")
parser.add_argument('--gallery',
type=str)
def handle(self, *args, **options):
imagefile = options['imagefile']
base = os.path.basename(imagefile)
if options['title']:
title = options['title']
else:
title = os.path.splitext(base)[0]
if options['date_added']:
date_added = datetime.strptime(options['date_added'],'%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.UTC)
else:
date_added = timezone.now()
try:
p = Photo(image=imagefile, title=title, slug=title, date_added=date_added)
except:
raise CommandError('Photo "%s" could not be added' % base)
p.save()
self.stdout.write(self.style.SUCCESS('Successfully added photo "%s"' % p))
if options['gallery']:
try:
g = Gallery.objects.get(slug=options['gallery'])
except:
raise CommandError('Gallery "%s" does not exist' % options['gallery'])
p.galleries.add(g.pk)
p.save()
self.stdout.write(self.style.SUCCESS('Successfully added photo to gallery "%s"' % g))