Пользовательская команда для загрузки фотографии в Photologue из оболочки Django?

Я успешно использовал Photologue для представления галерей регулярно создаваемых изображений графиков данных. Конечно, теперь, когда возможность установлена, создается непристойное количество графиков данных, и ими нужно делиться!

Сценарий процесса загрузки изображений и добавления их в галереи с помощью manage.py из оболочки Django — это следующий шаг; однако, как любитель с Django, у меня есть некоторые трудности.

Вот пользовательская команда addphoto.py, которую я сейчас разработал:

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()

К сожалению, при выполнении с --traceback это приводит к следующему:

./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)

Очевидно, что копия файла изображения не была помещена в каталог media/. Кроме того, столбцы image, title и date_added заполняются в таблице photologue_photos базы данных веб-сайта, а столбец slug — нет.

Как можно загрузить файл в каталог MEDIA_ROOT?


Вот соответствующие фрагменты моделей Photo и ImageModel из файла Photologue models.py для справки:

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()

Вот функция get_storage_path, как и просили:

# 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)

person Alex Willison    schedule 03.08.2017    source источник
comment
Не могли бы вы добавить текущее значение MEDIA_ROOT, пожалуйста? Вы можете получить его, запустив python manage.py diffsettings.   -  person Richard Barran    schedule 04.08.2017
comment
@RichardBarran MEDIA_ROOT сейчас /home/user/mysite/media/   -  person Alex Willison    schedule 10.08.2017
comment
Спасибо за информацию - что-то не так с базовыми настройками вашего сайта. MEDIA_ROOT равно /home/user/mysite/media/, но в сообщении об ошибке указано, что это /home/website/mywebsite/media. Соглашения FWIW Django заключаются в том, что папки static и media должны быть размещены внутри проекта Django в файловой системе, например. если ваш проект Django находится в файловой системе по адресу /home/website/mywebsite/, тогда носитель может перейти, например, в /home/website/mywebsite/media, но не /home/website/somewhereelse/media   -  person Richard Barran    schedule 12.08.2017
comment
@RichardBarran Arg, несоответствие между MEDIA_ROOT и сообщением об ошибке - это моя ошибка в запутывании имен пользователя и проекта ... Я исправил.   -  person Alex Willison    schedule 15.08.2017
comment
Опубликуйте определение или значение get_storage_path из поля ImageModel.image upload_to=get_storage_path части вашего кода.   -  person Brendan Goggin    schedule 16.08.2017


Ответы (3)


Только в одной части вашего вопроса: столбец slug пуст, когда Photo сохраняется.

Он должен автоматически заполняться при сохранении Photo, как ясно видно из вашего копирования и вставки исходного кода Photologue выше if self.slug is None: self.slug = slugify(self.title).

Это говорит о том, что исходный код Photologue фактически не вызывается из вашей команды управления — вы можете проверить это, добавив код быстрой отладки в локальную копию кода Photologue, например. оператор print() в методе save() и проверьте, запускается ли ли он.

person Richard Barran    schedule 04.08.2017
comment
Помещение оператора print("test") в метод save() привело к его выводу, поэтому он выполняется. Тем не менее, я также добавил оператор print(self.slug) после его объявления, который ничего не выводил. Я только что заметил, что модель Photo вызывает ImageModel, а НЕ models.Model из django.db. Я добавлю соответствующий код для ImageModel к вопросу. - person Alex Willison; 10.08.2017
comment
Я поставил операторы print() в ImageModel - один в самом начале под Class ImageModel(models.Model) , а один в метод save(): первый выполнился, а второй нет. Я подозреваю, что это приближает меня к моей проблеме... - person Alex Willison; 10.08.2017
comment
Модель Photo расширяет модель ImageModel, которая сама является производной от models.Model — хороший пример наследования. save() на ImageModel следует запустить... Отладчику может быть интересно пошагово просмотреть, что происходит в вашей команде управления, когда вы запускаете строку p.save(). - person Richard Barran; 12.08.2017
comment
Я добавил полное сообщение об ошибке в вопрос. - person Alex Willison; 15.08.2017
comment
См. ответ @Brendan-Goggin на вопрос slug. - person Alex Willison; 16.08.2017

Изменить/обновить 2: кажется, я знаю, почему ваше поле слага пусто. Я думаю, проблема в том, что self.slug не является None, даже если поле не содержит строку или содержит пустую строку (см. this ответ). Поэтому попробуйте обновить if self.slug is None до этого:

class Photo(ImageModel):
    ...
    def save(self, *args, **kwargs):
        # test if slug is not truthy, including empty string or None-ness
        if not self.slug:
            self.slug = slugify(self.title)
        super(Photo, self).save(*args, **kwargs)

Изменить/обновить 1: ознакомьтесь с этим ответом. Это из Django 1.4 (древний, я знаю), но он должен решить вашу проблему. Если вы скопируете или переместите файлы, которые вы добавляете в MEDIA_ROOT, перед созданием экземпляров Photo, все будет готово. Вот ответ, показывающий, как копировать файлы в python. Я предлагаю вам изменить свою пользовательскую команду на это:

from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from photologue.models import Photo, Gallery

# so you can access settings.MEDIA_ROOT
from django.conf import settings

# so you can copy file to MEDIA_ROOT if need be
from shutil import copyfile

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)

        # where the imagefile is currently located, assumes MEDIA_ROOT
        parser.add_argument('--media_source', 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):

        # the path of the file relative to media_source
        imagefile = options['imagefile']

        # if file is not in media root, copy it to there
        if options['media_source']:
            media_source = os.path.realpath(options['media_source'])
            media_target = os.path.realpath(settings.MEDIA_ROOT)
            if media_source != media_target:
                copyfile(imagefile, os.path.join(media_target, imagefile)
        # note: if media_source was not provided, assume file is already in MEDIA_ROOT

        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()

Теперь ваш imagefile относится к источнику вашего мультимедиа (../dataplots), копируется в MEDIA_ROOT, и все должно работать, как и планировалось. Вот как будет выглядеть ваша команда

manage.py addphoto 'monitoring/test.png' --media_source='../dataplots'

Который должен скопировать ваш график данных в MEDIA_ROOT, а затем создать объект Photo, как и ожидалось.

Исходный ответ:

Можете ли вы опубликовать, что такое get_storage_path в следующей строке:

class ImageModel(models.Model):
     image = models.ImageField(_('image'),
                          max_length=IMAGE_FIELD_MAX_LENGTH,
                          upload_to=get_storage_path)  # <-- here

Этот ответ будет несколько неполным, пока я не узнаю, что это такое, но я думаю, что вижу вашу проблему. Посмотрите на команду, которую вы запускаете:

./manage.py addphoto '../dataplots/monitoring/test.png' --traceback

Ваш аргумент imagefile равен ../dataplots/monitoring/test.png. Если get_storage_path возвращает тот же путь с ../ в начале пути, то вы будете указывать путь загрузки, который находится не в вашем каталоге MEDIA_ROOT, а в его родительском каталоге. Я думаю, что он пытается загрузить на MEDIA_ROOT/../dataplots/monitoring/test.png, как показано первой SuspiciousFileOperation в вашей трассировке:

# note: linebreaks and indentation added for readability
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)

Таким образом, он пытается загрузить на MEDIA_ROOT/imagefile, но imagefile начинается с ../, что не должно быть разрешено.

Если это действительно проблема (сложно сказать, пока не выложишь код get_storage_path), то есть много способов решить проблему. Возможно, самое быстрое решение — просто переместить ваш каталог dataplots в тот же каталог, что и ваш manage.py:

mv ../dataplots ./dataplots

Это должно сразу решить вашу проблему, потому что вам больше не понадобится ../, но вы, вероятно, не хотите, чтобы все эти графики данных находились в каталоге вашего проекта, поэтому это быстрое, но слабое решение. Основная проблема заключается в том, что путь к исходному файлу и путь, по которому вы загружаете исходный файл, не должны совпадать. Я думаю, вы должны изменить свои аргументы команды, чтобы включить image_source и image_destination, или вы могли бы включить source_media_root и imagefile, где ../ является частью source_media_root, а imagefile относится как к source_media_root, так и к желаемому целевому местоположению в MEDIA_ROOT... Есть много решений, но я не могу предоставить правильный код для одного, пока не узнаю, что такое get_storage_path (я предполагаю, что это функция, которая так или иначе возвращает или может вернуть аргумент imagefile).

person Brendan Goggin    schedule 16.08.2017
comment
Забавно, как раз перед тем, как вы опубликовали, я решил попробовать переместить файл изображения прямо в MEDIA_ROOT/photologue/photos и запустить команду как ./manage.py addphoto 'photologue/photos/test.png', и это сработало (хотя мне все еще нужно было определить slug в addphoto.py). Я опубликую get_storage_path в вопросе, как вы просили; однако вы правы в том, что мне не нужна папка dataplots в каталоге проекта. Скорее всего, я буду использовать скрипт BASH, выполняемый через cron, для копирования изображений из dataplots в MEDIA_ROOT/photologue/photos перед загрузкой в ​​Photologue. - person Alex Willison; 16.08.2017
comment
if not self.slug исправил эту проблему, и, как ни странно, я видел второй ответ, на который вы ссылались ранее, когда работали над предыдущим проектом. Что касается остальной части вашего ответа - я пытался его реализовать; однако я снова получаю django.core.exceptions.SuspiciousFileOperation ошибки. Photologue также создает кеш фотографии в виде эскиза при загрузке, и это, по-видимому, причина. Спасибо за вашу помощь, но я собираюсь использовать свою версию команды. - person Alex Willison; 16.08.2017

Вот рабочая версия пользовательской команды addphoto.py, которую я создал.

Файл изображения должен находиться в пределах MEDIA_ROOT/photologue/photos для облегчения импорта. Команда выполняется с помощью ./manage.py addphoto 'photologue/photos/test.png'. Обратите внимание, что есть опция --gallery для добавления изображения в галерею при условии, что галерея 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))
person Alex Willison    schedule 16.08.2017
comment
Взгляните на мой ответ. Я добавил для вашей команды возможность копировать файл изображения в MEDIA_ROOT, если его там еще нет. Я добавил это почти сразу, когда вы опубликовали это, что всегда приводит к запутанной ситуации. - person Brendan Goggin; 16.08.2017
comment
Я также добавил решение проблемы с пустым полем slug. - person Brendan Goggin; 16.08.2017