在ModelForm clean()中访问上传的文件



我的系统有产品,并与它们相关联的图像,如下所示:

class Product(models.Model):
    name = models.CharField(max_length=100)
    ...
class Image(models.Model):
    product = models.ForeignKey(Product)
    image = models.ImageField(upload_to='products')

到目前为止一切顺利。当然,客户希望以csv格式批量上传他们的产品,并上传包含图像的zip文件。我将csv格式设置为:

product_name,image_1.jpg,image_2.jpg,...
product_2,image.jpg,...
到目前为止,我已经做了一个模型,只是作为一个帮助:
class BulkUpload(models.Model):
    csv = models.FileField(upload_to='tmp')
    img_zip = models.FileField(upload_to='tmp')

工作流程是这样的:

    用户通过django admin上传文件
  1. 获取zip文件内容并存储以供以后使用
  2. 解压到tmp目录
  3. 启动事务。如果这里发生任何意外,我们回滚
  4. csv文件中的每一行
    1. 使用第一列中指定的名称创建并保存产品。
    2. 从其他csv字段中抓取图像文件名
    3. 检查图像是否在zip中,否则回滚
    4. 检查目标目录中不存在的图像,否则回滚
    5. 将图像移动到目标目录,并将fk设置为保存的产品对象,如果有错误则回滚。
  5. 提交事务
  6. 删除zip和csv文件,并删除批量上传对象(或者干脆不保存)

如果我们在任何时候回滚,我们应该以某种方式通知用户哪里出错了。

我最初的想法是覆盖save或使用postrongave信号,但没有访问请求意味着我既不能使用消息也不能引发验证错误。在管理中重写model_save()有它自己的问题,不能做任何验证。

所以现在我的想法是改变ModelForm并把它给django管理员。我可以重写clean()方法,引发ValidationErrors并(大概)在事务中运行我的所有内容。但我正在努力弄清楚如何以这样一种方式访问文件,我可以在它们上使用Python的ZipFile和csv库。在表单验证方法中做实际工作也感觉有点脏,但我不确定我还能在哪里做到这一点。

我可能讲得太详细了,但是我想解释一下解决方案,以便大家可以提出其他的解决方案。

我认为您不应该使用BulkUpload或任何表示此操作的模型,至少如果您计划像您目前建议的那样同步执行过程。我会手动或使用第三方库向管理区域添加一个额外的视图,然后在那里处理表单并执行工作流。

但无论如何,鉴于您已经拥有BulkUpload模型,使用admin.ModelAdmin对象当然更容易。您主要关心的似乎是应该将交易代码放在哪里。正如你所提到的,有几种选择。在我看来,最好的选择是把这个过程分成两个部分:

首先,在你的模型的clean方法中,你应该检查所有可能由用户产生的潜在错误:已经存在的图像、缺失的图像、重复的产品等等。在这里,您应该检查上传的文件是否正确,例如使用如下命令:

def clean(self):
    if not zipfile.is_zipfile(self.img_zip.file):
        raise ValidationError('Not a zip file')

之后,您知道从这一点可能出现的任何错误都将由系统错误产生:bd失败,HD没有足够的空间等,因为所有其他可能的错误都应该在前一步检查过。在ModelAdmin.save_model方法中,您应该执行工作流程的其余部分。您可以使用ModelAdmin.message_user通知用户任何错误。

至于上传文件的实际处理,好吧,您命名了它:只需使用标准库中的zipfile和csv模块。您应该创建一个ZipFile对象并将其提取到某个地方。现在,您应该使用csv.reader检查csv文件的数据。如下所示(未测试):

def save_model(self, request, obj, form, change):
    # ...
    with open('tmp/' + obj.img_zip.name, 'r') as csvfile:
            productreader = csv.reader(csvfile)
            for product_details in productreader:
                p = Product(name=product_details[0])
                p.save()
                for image in product_details[1:]:
                    i = ImageField()
                    i.product = p
                    i.image = File(open('tmp/' + image)) # not tested
                    i.save() 

在所有这些之后,拥有BulkUpload实例将没有意义,因此您应该删除它。这就是为什么我一开始就说这个模型有点没用

显然,您需要为事务添加代码和其他一些东西,但我希望您了解总体思路。

最新更新