掌握Django(十三)–上传文件

从本文开始介绍Django的一些高级特性,本文先介绍文件上传相关知识。


设置环境

为了使演示一致,可以先下载附件。

https://box.zjenergy.com.cn/l/z5imhn(提取码:kelemi)

  1. 解压附件后,将 Code\1- Getting Started\Start 下的 storefront3 文件夹拖到vscode打开。
  2. 再创建一个数据库:create database storefront3
  3. 修改settings.py下的mysql数据库登录信息.
  4. 安装依赖:pipenv install
  5. 激活虚拟环境:pipenv shell , 并在vscode中正确设置虚拟环境python解释器的正确路径
  6. 进行数据库迁移:python manage.py migrate

以上几步之前都介绍过,不再赘述。下面开始填充数据表信息,以使下面操作可以进行。这次不直接打开数据库执行sql文件,我们用python脚本来完成。

我们执行:python manage.py , 不带任何参数,可以看到有一些列表,包括我们之前熟悉的 runserver、migrate等。也看到在store应用下有一个seed_db,这个脚本位于 store/management/commands目录下,只要将代码放在该约定的目录下就能在manage.py中被搜索到。seed_db.py的功能是执行同目录下的seed.sql数据库命令。我们运行下:

python manage.py seed_db

再创建超级用户:python manage.py createsuperuser

启动web服务器:python manage.py runserver

访问API端点:

 http://127.0.0.1:8000/store/products

 http://127.0.0.1:8000/store/collections

确认已有相关数据。

访问 http://127.0.0.1:8000/admin , 输入刚设的超级用户和密码,确认没有错误。

这样我们就完成了环境的设置。


管理媒体文件

首先我们要确定上传的文件放在哪里。一般习惯而言,是创建在根目录里的media文件件,当然也可以是uploads等,这里我们创建一个media文件夹。

再到settings.py里,我们看到有STATIC_URL=’/static/’ , 我们在这行下面再创建:

MEDIA_URL = ‘/media/’

MEDIA_ROOT = os.path.join(BASE_DIR, ‘media’)

为了使这两句生效,需要在文件头 import os

再到storefront — urls.py里,添加:

from django.conf import settings

from django.conf.urls.static import static

if settings.DEBUG:

    urlpatterns += static(settings.MEDIA_URL,

                          document_root=settings.MEDIA_ROOT)

完成之后,我们测试一下,在media文件夹拖入一张图片,比如dog.jpg。然后访问 http://127.0.0.1:8000/media/dog.jpg

确认能显示这张图片。


给产品添加图片

我们修改模型文件store — models.py, 添加产品图像模型。

class ProductImage(models.Model):

    product = models.ForeignKey(

        Product, on_delete=models.CASCADE, related_name=’images’)

    image = models.ImageField(upload_to=’store/images’)

image 使用ImageField,upload_to是相对于媒体文件夹的路径,我们这里的媒体路径在上节设置是 根目录下的 media文件夹。除了ImageField,还有FileField,区别在于ImageField会验证确实是图像文件。

另外,使用ImageField,需要安装pillow:

pipenv install pillow

再进行数据库迁移:

python manage.py makemigrations

python manage.py migrate


创建API上传图片

我们期望有这样的API格式:products/1/images/1

我们已建过多次了,很简单,来实现它。

首先是序列化类,store — serializers.py.

class ProductImageSerializer(serializers.ModelSerializer):

    class Meta:

        model = ProductImage

        fields = [‘id’, ‘image’]

    def create(self, validated_data):

        product_id = self.context[‘product_id’]

        return ProductImage.objects.create(product_id=product_id, **validated_data)

然后是视图类, store — views.py.

class ProductImageViewSet(ModelViewSet):

    serializer_class = ProductImageSerializer

    def get_queryset(self):

        return ProductImage.objects.filter(product_id=self.kwargs[‘product_pk’])

    def get_serializer_context(self):

        return {‘product_id’: self.kwargs[‘product_pk’]}

最后是添加 url 路由。store –urls.py

products_router.register(‘images’, views.ProductImageViewSet,

                         basename=’product-images’)

完成之后,访问 /store/products/1/images , 上传一张图片测试。确认无错误,然后可以在 media/store/images 可以看到刚上传的图片。


从API返回图片

目前我们访问 /store/products 时,是没有image信息的,我们加上。很简单,修改 store — serializers.py — ProductSerializer, 添加images字段,注意加上many=True 和 read_only=True.

class ProductSerializer(serializers.ModelSerializer):

    images = ProductImageSerializer(many=True, read_only=True)

    class Meta:

        model = Product

        fields = […, ‘images’]

    …

我们查看调试信息,执行的SQL语句比较多,主要是每一条product都要额外向数据表请求images信息,我们需要预加载images 提高性能, 修改 store — views — ProduceViewSet, 通过prefetch_related预加载。前面有说过的,prefetch对应多端,而select_related对应一的那端。

class ProductViewSet(ModelViewSet):

    queryset = Product.objects.prefetch_related(“images”).all()

    …


验证上传的文件

如何验证上传的文件,比如我们要限制上传的图片的大小。在store应用下新建一个文件 validators.py:

from django.core.exceptions import ValidationError

def validate_file_size(file):

    max_size_kb = 50

    if file.size > max_size_kb * 1024:

        raise ValidationError(f’File cannot be larger than {max_size_kb}KB!’)

再到ProductImage模型, store — models.py:

class ProductImage(models.Model):

    …

    image = models.ImageField(upload_to=’store/images’,

                              validators=[validate_file_size])

再测试上传大于50KB的图片,确认会收到错误文件太大的提示。

Django有内置验证器,比如扩展名验证,下面试一下。我们将ProductImage的image字段类型改成FileField,验证使用内置FileExtensionValidator。

class ProductImage(models.Model):

    …

    image = models.FileField(upload_to=’store/images’,

                              validators=[FileExtensionValidator(allowed_extensions=[‘pdf’])])

然后,我们就只能上传pdf文件了。


设置客户端App

前面的附件文件夹有客户端程序。

用vscode打开 \Code\2- Uploading Files\Client App下的

storefront3_client文件夹。

确认本机是否已安装node, 如果没有的话,访问https://nodejs.org/ 下载安装。安装完毕后,确认版本正常:node –version

然后,

先安装依赖:npm install

再启动项目:npm start

默认启用 8001端口,访问:

http://127.0.0.1:8001

可以看到有个上传文件的网页。


允许CORS

前面的客户端应用,我们选择一个图片文件,然后上传,提示”Could not reach the server!”,这涉及到CORS的问题。

CORS是浏览器一个通用安全保护机制,全称是 cross-origin Resource Sharing,意思是不同域的浏览器访问限制,比如 A域的浏览器访问 B域的某个json,将被禁止。我们可以在服务器启用某个域的访问。

在django中,使用的库是 django-cors-headers, 可以查看github上该库的用法:

https://github.com/adamchainz/django-cors-headers

根据官网指示:

首先安装:pipenv  install django-cors-headers

然后添加应用:

INSTALLED_APPS = [
...,
"corsheaders",
...,
]

再添加中间件:

MIDDLEWARE = [
...,
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
...,
]

最后设置允许的域:

CORS_ALLOWED_ORIGINS = [
"http://127.0.0.1:8001",
"http://localhost:8001",
]

设置完成之后,我们再打开 http://127.0.0.1:8001 上传图片,就能成功上传了。

小技巧:谷歌浏览器–开发者工具(F12)–网络,可以启用节流模式,比如选择“低速3G”,模拟慢速网络,可以看到上传过程的漂亮的进度条。

测试结束,别忘停用节流,否则访问其他网站也变慢了。






管理站点的图像管理

我们进入管理站点后面,查看某一条product,发现是没有images相关信息的,只有tag子项,这是我们在很早之前实现的。可以先回顾下。

查找ProductAdmin,可以看到有store/admin/ProductAdmin和core/admin/CustomProductAdmin。之前的CustomProductAdmin有明细tag信息,主要是添加 inlines 属性。我们如果想实现images明细,也需要实现images的inlines属性。我们在 store — admin.py中添加:

class ProductImageInline(admin.TabularInline):

    model = models.ProductImage

class ProductAdmin(admin.ModelAdmin):

    …

    inlines = [ProductImageInline]

    …

由于还存在core — admin.py的CustomProductAdmin, 它会覆盖ProductAdmin的inlines,所以有CustomProductAdmin的inlines属性也同样加上ProductImageInline类。store — admin.py:

class CustomProductAdmin(ProductAdmin):

    inlines = [TagInline, ProductImageInline]

刷新页面,能看到image,但只是链接地址,我们希望能看到缩略图。我们添加一个字段thumbnail。

class ProductImageInline(admin.TabularInline):

    model = models.ProductImage

    readonly_fields = [‘thumbnail’]

    def thumbnail(self, instance):

        if instance.image.name != ”:

            return format_html(f'<img src=”{instance.image.url}”/>’)

        return ”

thumbnail字段是只读字段readonly_fields, 并且需要通过方法实现。在方法实现中,如果有image的话,就显示img的html展示,注意需要用format_html包起来,否则不会显示。

刷新页面,发现图像出来了,但是显示是原图,尺寸太大,我们需要的是缩略图,所以我们要设置宽高,这就要用到css。先为 img 加上名为 thumbnail的css 的class.

format_html(f'<img src=”{instance.image.url}” class=”thumbnail”/>’)

然后在store 下创建 static文件夹,这是一个特定的文件夹。Django会在每个应用的static下查找静态文件比如css、图像等并收集过去。由于有多个应用,为避免其他应用也创建同名的静态文件导致互相覆盖,我们再在其下创建 store 文件夹,再在store文件夹新建 styles.css 文件。

.thumbnail {

    width: 100px;

    height: 100px;

    object-fit: cover;

}

这里的object-fit是为了适应图片的长宽比,否则所有图片都被压成正方形可能会失真。

然后我们还要告诉django 是哪里找 css文件。转到ProductAdmin类,添加内联类 Media,如下:

class ProductAdmin(admin.ModelAdmin):

    …

    class Media:

        css = {

            “all”: [‘store/styles.css’]

        }

内联类 Media可以指定要用的静态文件,比如 css , javascript等。这里的css 我们指定 store/style.css。all 表示 全部使用 这个css文件,或者指定是screen,print等,这里就用all。

完成之后,刷新网页,就能在产品明细中看到比较合适的缩略图了。


小结

本文我们详细介绍了文件上传相关知识。下一篇将介绍邮件发送、运行后台任务等相关高级知识点,敬请期待。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注