【问题标题】:Custom command to upload photo to Photologue from within Django shell?从 Django shell 中将照片上传到 Photologue 的自定义命令?
【发布时间】:2018-01-11 06:22:45
【问题描述】:

我已成功聘请 Photologue 来展示galleries of regularly-created data plot images。当然,现在能力已经建立,大量的数据图正在被创建,它们需要共享!

下一步是使用 Django shell 中的manage.py 编写上传图像并将其添加到画廊的过程;但是,作为 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/ 目录中。此外,虽然 imagetitledate_added 列已填充到网站数据库的 photologue_photos 表中,但 slug 列未填充。

如何将文件上传到MEDIA_ROOT目录?


以下是 Photologue models.py 文件中 PhotoImageModel 模型的相关 sn-ps,供参考:

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)

【问题讨论】:

  • 请问您可以添加MEDIA_ROOT 的当前值吗?您可以通过运行python manage.py diffsettings 来获取它。
  • @RichardBarran MEDIA_ROOT/home/user/mysite/media/
  • 感谢您提供信息 - 您网站的基本设置听起来有些问题。 MEDIA_ROOT/home/user/mysite/media/,但错误消息表明它是 /home/website/mywebsite/media。 FWIW Django 约定用于将 staticmedia 文件夹放在文件系统上的 Django 项目内部,例如如果您的 Django 项目位于 /home/website/mywebsite/ 的文件系统中,则媒体可以进入例如 /home/website/mywebsite/media,但 /home/website/somewhereelse/media
  • @RichardBarran Arg,MEDIA_ROOT 和错误消息之间的差异是我在混淆用户和项目名称时的错误......我已经修复了。
  • 请在代码的 ImageModel.image 字段 upload_to=get_storage_path 部分发布 get_storage_path 的定义或值。

标签: python django photologue django-shell django-commands


【解决方案1】:

仅针对您问题的一部分:保存Photo 时,slug 列为空。

应该在保存 Photo 时自动填充 - 正如您在 if self.slug is None: self.slug = slugify(self.title) 上方复制和粘贴 Photologue 源代码时所做的那样。

这表明 Photologue 源代码实际上不是从您的管理命令中调用的 - 您可以通过向 Photologue 代码的本地副本添加一些快速调试代码来检查这一点,例如save() 方法中的 print() 语句,并检查它是否正在运行。

【讨论】:

  • save() 方法中加入print("test") 语句确实会产生它的输出——所以它正在运行。也就是说,我还在其声明后添加了一个print(self.slug) 语句,它没有输出任何内容。我刚刚注意到Photo 模型从django.db 调用ImageModelNOT models.Model。我会将ImageModel 的相关代码添加到问题中。
  • 我将print() 语句放在ImageModel 中 - 在Class ImageModel(models.Model) 下的最开始处放置一个,在save() 方法中放置一个:第一个被执行,但第二个没有。我怀疑这让我更接近我的问题......
  • Photo 模型扩展了 ImageModel 模型,它本身派生自 models.Model - 一个很好的继承示例。 ImageModel 上的 save() 应该 运行...当您运行 p.save() 行时,调试器可能会单步执行管理命令中发生的事情。
  • 我已将完整的错误消息添加到问题中。
  • 查看@Brendan-Goggin 对slug 问题的回答
【解决方案2】:

编辑/更新 2: 我想我知道为什么您的 slug 字段为空。我认为问题在于self.slug 不是None,即使该字段不包含字符串或包含空字符串(参见this answer)。所以尝试将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: 查看this answer。它来自 Django 1.4(我知道很古老),但它应该可以解决您的问题。如果您在创建 Photo 实例之前复制或移动要添加到 MEDIA_ROOT 的文件,那么您应该很高兴。 Here is an answer 展示了如何在 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_sourceimage_destination,或者您可以包含source_media_rootimagefile,其中../source_media_root 的一部分,而imagefile 是相对于source_media_rootMEDIA_ROOT 中的所需目标位置...有很多解决方案,但在我知道 get_storage_path 是什么之前,我无法完全提供正确的代码(我假设它是一个函数返回或可以以一种或另一种方式返回 imagefile 参数)。

【讨论】:

  • 很有趣,就在你发布之前,我决定尝试将图像文件直接移动到MEDIA_ROOT/photologue/photos,并以./manage.py addphoto 'photologue/photos/test.png' 运行命令并且它工作(尽管我仍然必须定义@987654368 @ 在addphoto.py)。我会按照您的要求在问题中发布get_storage_path;但是,您是对的,我不希望项目目录中的 dataplots 文件夹。我最终可能会使用通过cron 执行的BASH 脚本将图像从dataplots 复制到MEDIA_ROOT/photologue/photos,然后再上传到Photologue。
  • if not self.slug 解决了这个问题,有趣的是,我在之前的项目中看到了你之前链接的第二个答案。至于您的其余答案-我一直在尝试实施它;但是,我再次收到django.core.exceptions.SuspiciousFileOperation 错误。 Photologue 还将照片的缓存创建为带有上传的缩略图,这似乎就是原因。感谢您的帮助,但我将使用我的命令版本。
【解决方案3】:

这是我最终创建的自定义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))

【讨论】:

  • 看看我的回答。我为您的命令添加了将图像文件复制到 MEDIA_ROOT 的功能,如果它不存在的话。我几乎是在您发布此内容时添加的,这总是令人困惑的情况
  • 我还添加了针对空 slug field 问题的解决方案。
猜你喜欢
  • 2013-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多