从本文开始介绍Django的一些高级特性,本文先介绍文件上传相关知识。
设置环境
为了使演示一致,可以先下载附件。
https://box.zjenergy.com.cn/l/z5imhn(提取码:kelemi)
- 解压附件后,将 Code\1- Getting Started\Start 下的 storefront3 文件夹拖到vscode打开。
- 再创建一个数据库:create database storefront3
- 修改settings.py下的mysql数据库登录信息.
- 安装依赖:pipenv install
- 激活虚拟环境:pipenv shell , 并在vscode中正确设置虚拟环境python解释器的正确路径
- 进行数据库迁移: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。
完成之后,刷新网页,就能在产品明细中看到比较合适的缩略图了。
小结
本文我们详细介绍了文件上传相关知识。下一篇将介绍邮件发送、运行后台任务等相关高级知识点,敬请期待。