内容分享功能
Posted 一个处女座的测试
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内容分享功能相关的知识,希望对你有一定的参考价值。
第五章 内容分享功能
在上一章我们使用内置验证框架迅速的建立了整个网站的用户相关功能,还学习了如何通过一对一字段扩展用户信息,以及为网站添加第三方认证登录功能。
这一章会学习使用javascript小书签程序,将其他网站的图片内容分享到本站,还将学习使用jQuery在Django中使用AJAX技术。本章包含如下要点
创建多对多关系
自定义表单行为
在Django中使用jQuery
创建jQuery小书签程序
使用sorl-thumbnail创建缩略图
使用jQuery发送AJAX请求和创建AJAX视图
创建视图的自定义装饰器
AJAX动态加载页面
1创建图片分享功能
我们的站点将让用户可以收藏然后分享他们在互联网上看到的图片到本站来,为此将要做以下工作:
用一个数据类存放图片和相关信息
建立表单和视图用于处理图片上传
需要建立一个系统,让用户将外站图片贴到本站来。
这是一个独立与用户验证系统的新功能,为此新建一个应用images:
Copydjango-admin startapp images
然后在settings.py中激活该应用:
CopyINSTALLED_APPS = [
# ...'images.apps.ImagesConfig',
]
1.1创建图片模型
编辑images应用的models.py文件,添加如下代码:
Copyfrom django.db import models
from django.conf import settings
classImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='images_created', on_delete=models.CASCADE)
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200,blank=True)
url = models.URLField()
image = models.ImageField(upload_to='images/%Y/%m/%d')
description = models.TextField(blank=True)
created = models.DateField(auto_now_add=True,db_index=True)
def__str__(self):
return self.title
这是我们用于存储图片的模型,来看一下具体的字段:
user:这是一个连接到User模型的外键,体现了用户与图片的一对多关系,即一个用户可以上传多个图片。
title:图片的名称
slug:该图片的简称,用于动态建立该图片的URL
image:图片文件字段,用于存放图片
description:可选的关于图片的描述
created:图片分享到本站来的时间,使用了auto_now_add自动生成创建时间,并且使用了db_index=True创建索引
数据库索引可以有效的提高数据库查询效率。对于频繁使用filter(),exclude()或者order_by()等方法的字段推荐创建字段。ForeignKey和设置了unique=True的字段默认会被创建索引。还可以使用Meta.index_together创建联合索引。
译者注:为created字段创建索引是常用做法。
这里我们需要自定义该模型的行为,重写Image模型的save()方法,使图片在保存到数据库时,自动根据title字段生成slug字段的内容。导入slugify()然后为Image模型添加一个save()方法:
Copyfrom django.utils.text import slugify
classImage(models.Model):
# ......defsave(self, *args, kwargs):
ifnot self.slug:
self.slug = slugify(self.title)
super(Image, self).save(*args, kwargs)
译者注:原书代码缩进有误,此处已经修改为正确版本。
在这段代码里,使用了Django内置的slugify()自动生成了slug字段的内容。之后调用超类的方法保存图片,这样用户无需手工输入。
1.2创建多对多关系
我们将在Image模型中再添加一个外键,用于存储哪些用户喜欢该图片。由于一个用户可能喜欢多个图片,一个图片也可能被多个用户喜欢,因此图片和用户之间多对多的关系,需要修改Image模型添加如下字段:
Copyusers_like = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='images_liked', blank=True)
当定义了ManyToManyField多对多外键字段时,Django会创建一张中间表,中间表分别通过外键关联到当前的模型和ManyToManyField()的第一个参数对应的模型,多对多关系可以用于任意两个有关系的模型。
与ForeignKey一样,related_name属性定义了多对多字段反向查询的名称,多对多字段提供了一个多对多模型管理器用来进行查询,类似image.users_like.all(),如果是从user对象查询,则类似user.images_liked.all()。
之后进行Image类的数据迁移。
1.3添加图片模型至管理后台
编辑images应用的admin.py文件,将Image类添加至管理后台:
Copyfrom django.contrib import admin
from .models import Image
@admin.register(Image)classImageAdmin(admin.ModelAdmin):
list_display = ['title', 'slug', 'image', 'created']
list_filter = ['created']
启动站点,打开http://127.0.0.1:8000/admin/,可以看到Image已经被加入管理后台,如图所示:
2从外站分享内容至本站
我们实现用户将外站图片分享到本站的方式是:用户提供图片的URL,一个标题和可选的秒数,我们的站点会将该图片下载下来,建立一个对应的新Image对象,然后保存进数据库。
已经建立完了图片模型,这里我们需要建立一个表单供用户提交图片信息。在Images应用下建立forms.py文件,然后添加如下代码:
Copyfrom django import forms
from .models import Image
classImageCreateForm(forms.ModelForm):
classMeta:
model = Image
fields = ('title', 'url', 'description',)
widgets =
'url': forms.HiddenInput,
这里使用了ModelForm类,基于Image模型创建了表单,仅包含title,url和description字段。用户无需直接在表单中输入图片URL,我们将使用一个JavaScript小书签程序来从外站选择一个图片并将其URL作为Get请求的参数,然后访问我们的站点。所以我们使用了HiddenInput小插件替代了默认的url字段的设置。我们这么做是希望这个字段不被用户看到。
2.1验证表单字段
为了验证这个URL是一个图片,需要检查URL中的文件名是否以.jpg或.jpeg扩展名结尾。像在之前章节那样,我们将针对url字段编写一个自定义验证器clean_url(),这样表单对象调用is_valid()时,我们的验证器就可以修改数据或者报错。添加如下方法到ImageCreateForm:
Copydefclean_url(self):
url = self.cleaned_data['url']
valid_extensions = ['jpg', 'jpeg']
extension = url.rsplit('.', 1)[1].lower()
if extension notin valid_extensions:
raise forms.ValidationError('The given URL does not match valid image extensions.')
return url
在上边的代码中,定义了clean_URL()方法来验证url字段,该方法解释如下:
从cleaned_data中获取url字段的值
将URL通过从右边开始的第一个.进行切分,然后取切分结果的第二个元素,也就是扩展名进行比较。如果验证失败,则抛出一个ValidationError错误。这里我们采用的验证方式比较简陋,而且仅支持jpg类型图片,你可以采用正则表达式或者其他高级方法来验证URL是否是一个有效的图片文件地址。
除了验证URL之外,我们还必须在验证成功的时候将图片下载并保存到数据库中。我们可以使用处理该表单的视图来完成这个操作,但更常用的方式是重写表单的save()来实现此功能。
2.2重写表单的save()方法
在之前已经知道,ModelForm有一个save()方法,将当前的模型数据存储到数据库中并且返回该对象。这个方法还接受一个commit布尔值参数,用于确定是否实际将数据持久化到数据库中。如果commit=False,则save()方法仅返回当前的数据对象,但不执行数据库写入操作。因此我们可以重写save()方法,让其下载图片之后,再将数据对象写入数据库。
添加如下导入语句到forms.py文件:
Copyfrom urllib import request
from django.core.files.base import ContentFile
from django.utils.text import slugify
之后添加下列save()方法至ImageCreateForm类中:
Copydefsave(self, force_insert=False, force_update=False, commit=True):
image = super(ImageCreateForm, self).save(commit=False)
image_url = self.cleaned_data['url']
image_name = '.'.format(slugify(image.title), image_url.rsplit('.', 1)[1].lower())
# 根据URL下载图片
response = request.urlopen(image_url)
image.image.save(image_name, ContentFile(response.read()), save=False)
if commit:
image.save()
return image
我们重写了save()方法,保持与原来方法一样的默认参数设置。重写的方法工作逻辑如下:
先调用父类的save()方法,使用现有表单数据建立一个新的image数据对象但不保存
从cleaned_data中获取URL
将image.slug与扩展名拼成新的文件名
使用Python的urllib模块下载图片,然后使用image字段的save()方法保存到MEDIA目录中。image字段的save()方法的参数之一ContentFile是下载的图片内容,这里使用了save=False防止直接将字段写入数据库。
为了和原save()方法的行为保持一致,仅当commit=True的时候写入数据库。
译者注:本章到现在为止出现了模型的save()方法,表单的save()方法和image字段的save()方法,读者不要混淆。
之后来编写处理表单的视图,编辑images应用的views.py文件,添加如下代码:
Copyfrom django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import ImageCreateForm
@login_requireddefimage_create(request):
if request.method == "POST":
# 表单被提交
form = ImageCreateForm(request.POST)
if form.is_valid():
# 表单验证通过
cd = form.cleaned_data
new_item = form.save(commit=False)
# 将当前用户附加到数据对象上
new_item.user = request.user
new_item.save()
messages.success(request, 'Image added successfully')
# 重定向到新创建的数据对象的详情视图return redirect(new_item.get_absolute_url())
else:
# 根据GET请求传入的参数建立表单对象
form = ImageCreateForm(data=request.GET)
return render(request, 'images/image/create.html', 'section': 'images', 'form': form)
使用@login_required装饰器令image_create视图仅供登录后的用户使用,这个视图工作逻辑如下::
我们通过一个Get请求附加的参数创建表单对象,参数会带着url和title字段对应的内容。这个Get请求是由之后我们创建的JavaScript小书签程序发起的,现在,我们就假设该表单已经被初始化而且被用户确认并提交。
表单提交后,如果验证通过,那么建立一个新的Image对象,但是不存入数据库。
取得当前的用户,赋给Image对象的外键后进行保存,这样就可以知道该图片由哪个用户上传。
将图片写入数据库。
创建一个成功保存图片的消息,然后将用户重定向到规范化的图片对象的URL,现在还没有为Image模型创建get_absolute_url()方法,稍后会进行创建。
在images应用中建立urls.py文件,添加如下代码:
Copyfrom django.urls import path
from . import views
app_name = 'images'
urlpatterns = [
path('create/', views.image_create, name='create'),
]
然后编辑bookmarks项目的根urls.py文件,为images应用增加一条二级路由匹配:
Copyurlpatterns = [
path('admin/', admin.site.urls),
path('account/', include('account.urls')),
path('social-auth/', include('social_django.urls', namespace='social')),
path('images/', include('images.urls', namespace='images')),
]
最后来建立对应的模板,在images应用的目录下创建如下目录和文件结构:
Copytemplates/
images/
image/
create.html
然后编辑刚刚创建的create.html文件,添加如下代码:
Copy# create.html #
% extends "base.html" %
% block title %Bookmark an image% endblock %
% block content %
<h1>Bookmark an image</h1><imgsrc=" request.GET.url "class="image-preview"><formaction="."method="post">
form.as_p
% csrf_token %
<inputtype="submit"value="Bookmark it!"></form>
% endblock %
现在启动站点,输入类似http://127.0.0.1:8000/images/create/?title=...&url=...的链接,其中包含title和url两个参数,分别表示图片的名称和URL地址。可以使用下边这个测试地址:
应该可以看到下面的页面:
在description内输入一些内容,然后点击BOOKMARK IT!按钮,一个新的Image对象会被存入数据库。由于此时get_absolute_url()方法还未编写,所以会报错如下:
此时不用担心这个错误信息,通过刚才编写的视图可以知道,执行到这里报错说明图片已经成功存入数据库,打开http://127.0.0.1:8000/admin/images/image/即可看到该图片的信息,如下图所示:
2.3使用jQuery创建小书签程序
小书签程序是一段JavaScript代码,可以被浏览器保存为书签,在点击该小书签时,其中的JavaScript代码被执行,从而实现一些功能。
一些比较知名的站点,如Pinterest,使用小书签程序让用户可以从其他网站将内容分享到其网站上。我们建立的程序和这个小书签程序类似,让用户将图片分享到我们的站点来。
我们将使用jQuery建立小书签程序,jQuery是一个得到广泛使用的JavaScript库,可以快速开发基于JavaScript的程序,可以访问其官方站点https://jquery.com/了解更多信息。
用户将会这样使用我们的小书签:
用户将我们网站上的一个链接拖到浏览器的书签栏中,这个链接的href属性中保存着JS代码,这个链接被保存到浏览器书签成为一个可点击的书签
用户在其他网站上看到想分享的图片,点击这个小书签,小书签里边的程序被运行,让用户选择要分享的图片然后自动以GET请求访问我们的网站。
由于小书签程序保存在用户的浏览器上,在用户第一次保存后,想要更新该程序就很困难,所以一般小书签程序实际上是一个程序启动器,实际执行的程序位于我们的网站上。这就是我们创建小书签的方法解说,现在来实现:
在images/templates/目录下创建一个文件,叫做bookmarklet_launcher.js,添加如下JavaScript代码:
Copy(function ()
if (window.myBookmarklet !== undefined)
myBookmarklet()
else
document.body.appendChild(document.createElement('script')).src = 'http://127.0.0.1:8000/static/js/bookmarklet.js?r=' + Math.floor(Math.random() * 99999999999999999999);
)();
这段JavaScript代码首先检查myBookmarklet这个名称是否存在于当前环境,这样用户反复点击小书签程序也不会多次运行相同程序。如果名称不存在,就在当前的页面中增加一个<script>标签,也就是导入了我们网站的一段JavaScript程序并且执行。之后的r参数生成了一段随机数,目的是让浏览器每次都去请求实际的JavaScript文件,而不从缓存中直接读取
新增的<script>标签的src属性为"http://127.0.0.1:8000/static/js/bookmarklet.js?r=xxxxxxxxxxxxxxxxxxxx",指向我们网站自己的JavaScript程序文件,这样小程序每次执行的时候,都会将我们网站上的JavaScript程序在当前页面执行。下边我们把小程序链接加入到用户登录首页,以让用户可以将其保存成书签。
这就是一个启动器,用于加载实际上位于我们站点上的bookmarklet.js然后在当前页面运行。
编辑account应用的模板目录中的account/dashboard.html,让其看起来像下边这样:
Copy% extends "base.html" %
% block title %Dashboard% endblock %
% block content %
<h1>Dashboard</h1>
% with total_images_created=request.user.images_created.count %
<p>Welcome to your dashboard. You have bookmarked total_images_created image total_images_created|pluralize .</p>
% endwith %
<p>Drag the following button to your bookmarks toolbar to bookmark images from other websites <ahref="javascript:% include "bookmarklet_launcher.js" %" class="button">Bookmark it</a></p><p>You can also <ahref="% url "edit" %">edit your profile</a> or <ahref="% url "password_change" %">change your password</a>.<p>
% endblock %
现在首页已经当前用户已经分享了多少图片到本站,使用了% with %标签用于设置一个变量名给图片总数,可以避免反复查询数据库。然后包含了一个href属性是小标签启动器程序的链接,供用户将其拖动到浏览器的书签栏上。这里使用了include将JavaScript文件的内容导入。
译者注:这里灵活使用了include标签,可见引入的模板文件不需要是HTML文件,只要是文本文件即可,这里就通过该标签将bookmarklet_launcher.js文件引入,避免了在此处硬编码JavaScript代码。
在浏览器中打开http://127.0.0.1:8000/account/,可以看到如下页面:
现在开始来编写实际执行的JavaScript程序,在images应用下建立如下目录和文件结构:
Copystatic/
js/
bookmarklet.js
在随书代码中可以看到images应用目录下有static/css/目录,将其中的css/目录拷贝到你的应用的static/目录下,小书签程序将要使用其中的bookmarklet.css文件。
打开刚建立的bookmarklet.js文件,添加如下代码:
Copy(function ()
let jquery_version = '3.3.1';
let site_url='http://127.0.0.1:8000/';
let static_url = site_url + 'static/';
let min_width = 100;
let min_height = 100;
functionbookmarklet(msg)
//这里是分享图片的代码
// 检查页面是否加载了jQuery,如果没有就进行加载,尝试15次if(typeofwindow.jQuery !== 'undefined')
bookmarklet();
else
let conflict = typeofwindow.$ !== 'undefined';
let script = document.createElement('script');
script.src = '//ajax.googleapis.com/ajax/libs/jquery/' + jquery_version + '/jquery.min.js';
document.head.appendChild(script);
let attempts = 15;
(function()
if(typeofwindow.jQuery === 'undefined')
if(--attempts>0)
window.setTimeout(arguments.callee, 250)
else
alert("An error ocurred while loading jQuery")
else
bookmarklet()
)();
)();
这是加载jQuery的代码。如果jQuery已经在当前页面加载,则会使用当前页面的jQuery,如果没有加载,则将jQuery位于google的CDN地址加入到页面中。当jQuery被成功加载的时候,就去执行bookmarklet()函数,该函数含有实际的分享图片代码。在文件开始的地方还定义了如下几个全局变量:
jquery_version:jQuery的版本号
site_url和static_url:我们网站的地址和静态文件地址
min_width和min_height:用于控制程序寻找的最小图片宽高,小于这个宽或高的图片不会出现在供分享的清单中。
现在来编写bookmarklet()函数,编辑文件里的bookmarklet()函数的代码如下:
Copyfunctionbookmarklet(msg)
// 加载CSS文件let css = jQuery('<link>');
css.attr(
rel:'stylesheet',
type:'text/css',
href:static_url + 'css/bookmarklet.css?r=' + Math.floor(Math.random()*99999999999999999999)
);
jQuery('head').append(css);
// 加载HTML结构
box_html = '<div id="bookmarklet"><a href="#" id="close">×</a><h1>Select an image to bookmark:</h1><div class="images"></div></div>';
jQuery('body').append(box_html);
// 关闭事件jQuery('#boorkmarklet #close').click(function ()
jQuery("#bookmarklet").remove();
);
;
这段代码的逻辑如下:
加载bookmarklet.css,使用随机数确保浏览器不从缓存中读取
加入一块HTML结构代码到当前页面的<body>标签中,在页面的右上方显示一个浮动的图片列表区域
加入了一个事件,用户点击新增的区域的关闭按钮时,将我们添加的HTML结构代码从当前页面中删除。使用jQuery,通过父元素ID为bookmarklet的#bookmarklet和#close选择器定位我们的HTML元素。关于jQuery的选择器,可以参考https://api.jquery.com/category/selectors/。
在加载了HTML结构和对应的CSS样式后,接下来要添加分享功能,将如下代码追加在bookmarklet()函数的内部:
Copy// 寻找页面内所有图片然后显示在新增的HTML结构中
jQuery.each(jQuery('img[src$="jpg"]'), function(index, image)
if (jQuery(image).width() >= min_width && jQuery(image).height() >= min_height)
image_url = jQuery(image).attr('src');
jQuery('#bookmarklet .images').append('<a href="#"><img src="'+ image_url +'" /></a>');
);
这段代码使用了img[src$="jpg"]选择器来选择所有jpg格式的<img>元素,然后使用each()方法,对其中每个图片检查是否大于最小宽高,如果大于就将其加入到我们HTML结构的<div class="images">标签中。
在开始试验编写的功能之前,还必须进行最后的设置。现在HTTPS协议使用的很广泛,为了安全起见,浏览器一般不会允许HTTP协议的小书签程序运行,因此必须给我们自己的网站一个HTTPS地址,但是Django的测试服务器无法自动支持HTTPS,为了测试小书签的功能,使用Ngrok可以建立一个隧道将自己的本机通过HTTP和HTTPS地址向外提供服务。
在https://ngrok.com/download下载Ngrok,之后在系统命令行里运行如下命令:
Copy./ngrok http 8000
Ngrok建立一个隧道连接到本机的8000端口,然后为其分配一个域名,可以看到窗口里显示:
Copyngrok by @inconshreveable (Ctrl+C to quit)
Session Status online
Session Expires 7 hours, 58 minutes
Version 2.2.8
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://d0de3ca5.ngrok.io -> localhost:8000
Forwarding https://d0de3ca5.ngrok.io -> localhost:8000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
其中的https://d0de3ca5.ngrok.io就是可以访问到本机Django服务的HTTPS地址,把这个地址加入到settings.py文件的的ALLOWED_HOSTS里:
CopyALLOWED_HOSTS = [
'mysite.com',
'localhost',
'127.0.0.1',
`'d0de3ca5.ngrok.io'`
]
译者注:最好按照Ngrok官网的教程注册一个用户再使用,否则HTTPS的域名很快过期,需要重新启动Ngrok并进行相关配置。
启动站点,然后访问这个HTTPS地址,应该可以看到站点的登录页面,说明HTTPS服务正常。
获得HTTPS地址之后,编辑bookmarklet_launcher.js文件,将其中的http://127.0.0.1:8000/替换为新获得的HTTPS地址:
Copy(function ()
if (window.myBookmarklet !== undefined)
myBookmarklet()
else
document.body.appendChild(document.createElement('script')).src = 'https://d0de3ca5.ngrok.io/static/js/bookmarklet.js?r=' + Math.floor(Math.random() * 99999999999999999999);
)();
再将js/bookmarklet.js文件中的这一行:
Copylet site_url='http://127.0.0.1:8000/';
修改为:
Copylet site_url='https://d0de3ca5.ngrok.io/';
然后打开https://d0de3ca5.ngrok.io/account/,将页面上的BOOKMART IT的绿色按钮拖到浏览器的书签栏上,如图所示:
打开任意一个图片比较多的网站,点击小书签,应该可以看到屏幕右上方显示一块新区域,里边列出了当前站点可供分享的图片,如下所示:
我们希望用户点击一张图片,就可以将该图片分享到我们的网站,进入之前编写的视图对应的表单填写页面上,编辑js/bookmarklet.js文件,在bookmarklet()函数底部追加:
Copy// 点击图片时按照指定URL访问我们的网站jQuery('#bookmarklet .images a').click(function(e)
let selected_image = jQuery(this).children('img').attr('src');
// hide bookmarkletjQuery('#bookmarklet').hide();
// open new window to submit the imagewindow.open(site_url +'images/create/?url='
+ encodeURIComponent(selected_image)
+ '&title='
+ encodeURIComponent(jQuery('title').text()),
'_blank');
);
这个函数的逻辑如下:
为每个图片元素绑定一个click()事件
当用户点击一个图片时,设置一个变量selected_image,是这个图片的URL地址。
之后隐藏新增的HTML结构,使用selected_image和网站的的<title>的内容外加我们的网站地址,生成一个链接然后在新窗口中打开链接,实现GET请求附带参数访问我们自己的网站。
打开一个网站,然后点击小书签,在右上方出现的窗口中点击一张图片,会被重定向到我们网站的图片创建页面,如下所示:
撒花庆祝,我们实现了第一个小书签程序,然后将其集成到了我们的Django项目中。
3创建图片详情视图
完成了图片分享并保存的功能之后,现在需要建立一个详情视图用来展示具体图片,编辑images应用的views.py文件,添加如下代码:
Copyfrom django.shortcuts import get_object_or_404
from .models import Image
defimage_detail(request, id, slug):
image = get_object_or_404(Image, id=id,slug=slug)
return render(request, 'images/image/detail.html', 'section':'images','image':image)
这是一个简单的用于展示某个图片详情的视图,编辑images应用的urls.py文件为该视图添加一行URL:
Copypath('detail/<int:id>/<slug:slug>/', views.image_detail, name='detail'),
有过上个项目的经验,此时可以知道必须编写Image类的get_absolute_url()方法用于生成规范化链接,打开images应用的models.py文件,添加get_absolute_url()方法如下:
Copyfrom django.urls import reverse
classImage(models.Model):
# ...defget_absolute_url(self):
return reverse('images:detail', args=[self.id, self.slug])
记住在每个编写的模型中加入该方法,以快捷的生成对应的URL。
译者注:在django 2里,urls.py文件中使用include()方法并通过namespace参数指定命名空间,还需要在对应的下一级urls.py里写上app_name = 'namespace' 来设置命名空间。如果include()方法中设置了命名空间,其对应的urls.py文件中的app_name必须一致,否则会报错。如果include()方法未设置命名空间,则以app_name的设置为准。
最后就是建立模板了,在images应用的模板目录中的/images/image/路径下创建detail.html文件并添加如下代码:
Copy#/templates/images/image/detail.html#
% extends 'base.html' %
% block title %
image.title
% endblock %
% block content %
<h1> image.title </h1><imgsrc=" image.image.url "class="image-detail">
% with total_likes=image.users_like.count %
<divclass="image-info"><div><spanclass="count">
total_likes like total_likes|pluralize
</span></div>
image.description|linebreaks
</div><divclass="image-likes">
% for user in image.users_like.all %
<div><imgsrc=" user.profile.photo.url "><p> user.first_name </p></div>
% empty %
Nobody likes this image yet.
% endfor %
</div>
% endwith %
% endblock %
这是展示具体某个图片的模板,其中使用% with %保存查询结果到total_likes变量中避免了查询两次数据库。然后展示图片的discription字段,之后迭代image.users_like.all,显示出所有喜欢该图片的用户。
在一个模板中反复使用某一个QuerySet时,可以通过% with %将其查询结果保存到一个变量中,避免重复查询。
译者注:image.image.url和user.profile.photo.url:这两个字段不是Image类中的url字段,而是在定义Imagefield字段时upload_to的路径名称。
现在可以通过小书签程序再导入一个新图片,保存成功之后,会被重定向到图片的详情页,如下所示:
4创建图片缩略图
现在我们的图片详情页展示的是原始的图片,但是图片的尺寸可能差异很大,而且原始图片的大小可能会很大,载入时间较长。一般网站需要大量展示图片的通用做法是生成图片的缩略图然后展示缩略图。我们使用一个第三方应用sorl-thumbnail来生成缩略图。
在系统命令行中输入以下命令安装sorl-thumbnail:
Copypip install sorl-thumbnail==12.4.1
然后在settings.py文件中激活该应用:
CopyINSTALLED_APPS = [
# ...'sorl.thumbnail',
]
之后按照惯例执行数据迁移程序,可以看到数据库中增添了该应用的一个数据表。
这个模块采用了两种方法显示缩略图:一是提供了新的模板标签% thumbnail %直接在模板内显示缩略图,二是基于Imagefield自定义的图片字段,用于在模型内设置缩略图字段。这两种方式都可以显示缩略图。
我们采用模板标签的方式。编辑images/image/detail.html,找到如下这行:
Copy<imgsrc=" image.image.url "class="image-detail">
将其替换成下列代码:
Copy% load thumbnail %
% thumbnail image.image "300" as im %
<ahref=" image.image.url "><imgsrc=" im.url "class="image-detail"></a>
% endthumbnail %
这里我们定义了个固定宽度为300像素的缩略图,当用户第一次打开图片详情页时,一个缩略图会被创建在静态文件夹下,页面的原图片链接会被缩略图链接所代替。启动站点然后打开某个图片详情页,可以在项目根目录的media/cache/找到该图片对应的缩略图。
sorl-thumbnail可以使用很多算法生成各种缩略图。如果生成不了缩略图,在settings.py里增加一行THUMBNAIL_DEBUG=True,之后在命令行窗口中可以看到debug信息。具体文档可以看https://sorl-thumbnail.readthedocs.io/。
5使用jQuery发送AJAX请求
现在要给站点增加AJAX相关的功能,AJAX是Asynchronous JavaScript and XML的简称,这个技术使用一系列方式实现异步HTTP请求,可以从服务器异步取得数据并无需重载全部页面。不像名字里边必须采取XML格式,发送和收取数据可以采用JSON,HTML甚至纯文本。
AJAX的相关内容可以参考在Django中使用jQuery发送AJAX请求和使用原生JS发送AJAX请求的方法。
我们将要给图片详情页面增加一个按钮,让用户可以点击该按钮表示喜欢该图片,之后再点击该按钮可以取消喜欢该图片。首先我们先为这个功能建立视图函数,编写images应用的views.py文件,添加如下代码:
Copyfrom django.http import JsonResponse
from django.views.decorators.http import require_POST
@login_required@require_POSTdefimage_like(request):
image_id = request.POST.get('id')
action = request.POST.get('action')
if image_id and action:
try:
image = Image.objects.get(id=image_id)
if action == "like":
image.users_like.add(request.user)
else:
image.users_like.remove(request.user)
return JsonResponse('status': 'ok')
except:
passreturn JsonResponse('status': 'ko')
这个视图使用了两个装饰器,@login_required的作用是仅供已登录用户使用,@require_POST的作用是让该视图仅接受POST请求,否则返回一个HttpResponseNotAllowed对象,即HTTP 405错误。Django还提供了一个@require_GET装饰器用于只接受GET请求,还提供了一个@require_http_methods装饰器,可以指定允许哪些类型的HTTP请求。
在这个视图中,我们还是用了两个Post.get取得数据:
image_id:用户正在喜欢/不喜欢的图片的ID
action:用户执行的动作,用字符串like表示喜欢,unlike表示不喜欢
这里还使用了多对多字段的管理器users_like查询图片与喜欢用户之间的关系,然后使用add()和remove()方法用于添加和去除多对多关系。add()方法即使传入已经存在的数据对象,也不会重复建立关系,remove()即使传入不存在的对象,也不会报错。还有一个clear()方法可以快速的从关联表中全部清除多对多关系。
最后,使用了JsonResponse类,这个类的作用是将一个HTTP请求附加上application/json请求头,并将其中的内容序列化为JSON格式的字符串
编辑images应用的urls.py,为该视图配置URL:
Copy path('like/', views.image_like, name='like'),
5.1加载jQuery
我们将使用jQuery来发送AJAX请求,为此需要在页面内加载jQuery,为了可以让jQuery在所有的模板内都生效,将其加载代码放入base.html文件中,编辑account应用的base.html文件,在之前增加下列代码:
Copy<scriptsrc="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script><script>
$(document).ready(function ()
% block domready %
% endblock %
);
</script>
我们从Google CDN中加载了jQuery,可以直接在https://jquery.com/下载jQuery并将其放入本应用的static文件夹内。
在引入jQuery之后,增加了一个<script>标签,定义了一个$(document).ready(),这是一个jQuery方法,在DOM加载完毕后会执行该方法。DOM是Document Object Model的简称,由浏览器在加载页面时生成,以树形结构保存当前页面的所有节点数据。这样保证了JS代码执行时,其要操作的对象已经全部生成。
domready块,用于存放在DOM加载完毕后执行的JS代码,我们将在需要执行JS代码的具体模板中编写该块内容。
注意不要混淆JavaScript代码和Djanog模板标签。Django的模板语言在服务端进行处理,转换最终的HTML字节流,浏览器取得HTML字节流创建页面和DOM对象,并执行JavaScript代码。有时候动态的生成JavaScript代码非常方便。
在这一章里,我们直接将JS代码通过模板内块的形式编写进来,这是为了教学方便。最好的方式是从静态文件中导入.js文件,以做到有效解耦HTML与JS。
5.2AJAX中使用CSRF
在第二章中已经了解到POST请求中需要包含% csrf_token %生成的token数据,以防止跨站伪造请求攻击。不过在AJAX中发送CRSF token有点不方便,所以Django允许在AJAX请求中设置一个X-CSRFToken请求头,其中包含CSRF token的数据。jQuery在发送AJAX请求的时候设置上该请求头,就可以完成CRSF的发送了。
为了在AJAX请求中设置CSRF token,需要做如下事情:
从csrftoken cookie中取得CSRF token,如果开启了CSRF中间件,cookie中一直会有CSRF token数据
将CSRF token数据设置在AJAX请求的X-CSRFToken请求头中
可以在https://docs.djangoproject.com/en/2.0/ref/csrf/#ajax阅读更多关于Django中CSRF与AJAX的信息。
修改刚刚在base.html中增加的JS代码部分,修改成下边这样:
Copy<scriptsrc="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script><script>let csrftoken = Cookies.get('csrftoken');
functioncsrfSafeMethon(method)
// 如下的HTTP请求不需要设置CRSF信息return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
$.ajaxSetup(
beforeSend: function (xhr, settings)
if (!csrfSafeMethon(settings.type) && !this.crossDomain)
xhr.setRequestHeader("X-CSRFToken", csrftoken);
);
$(document).ready(function ()
% block domready %
% endblock %
);
</script>
以上代码解释如下::
通过外部CDN导入了一个JS库js-cookie--一个轻量级的操作cookie的第三方库,可以在https://github.com/js-cookie/js-cookie找到该库的详细信息。
通过Cookies.get()方法拿到csrftoken的值
创建csrfSafeMethod()函数,使用正则验证HTTP请求种类,GET,HEAD,OPTIONS和TRACE类型的请求无需添加CSRF信息
调用$.ajaxSetup()方法,在AJAX请求发送之前,为请求设置X-CSRFToken请求头信息,这个设置会影响到所有jQuery发送的AJAX请求。
这样所有的不安全的HTTP请求,比如GET或PUT,都会被添加上CRSF信息。
5.3jQuery发送AJAX请求
编辑images应用的images/image/detail.html文件,找到下边这行:
Copy% with total_likes=image.users_like.count %
将其修改成:
Copy% with total_likes=image.users_like.count users_like=image.users_like.all %
然后修改<div class="image-info">其中的内容,如下:
Copy<divclass="image-info"><div><spanclass="count"><spanclass="total"> total_likes </span>
like total_likes|pluralize
</span><ahref="#"data-id=" image.id "data-action="% if request.user in users_like %un% endif %like"class="like button">
% if request.user not in users_like %
Like
% else %
Unlike
% endif %
</a></div>
image.description|linebreaks
</div>
模板内首先通过% with %指定了新的变量users_like,用于存放所有喜欢该图片的用户,可以避免反复查询。然后显示总的喜欢该图片的人数,还包含一个按钮样式的<a>标签。这个按钮根据当前用户是否在users_like中,显示like或unlike,还为<a>标签设置了两个HTML5自定义属性:
data-id:当前页面显示图片的ID
data-action:用户的动作,喜欢或者不喜欢,值是like或unlike
我们将把这两个HTML5自定义属性的值通过AJAX发送给image_like视图,当用户点击喜欢/不喜欢按钮的时候,我们需要在客户端做如下操作:
调用AJAX视图,传入两个参数:id和action
如果AJAX请求成功返回,更新按钮的data-action属性为相反的操作(原来是like则更新为unlike,反之亦反)
更新喜欢当前图片的用户总数
为此来编写页面所需的JS代码,在images/image/detail.html中添加domready块的内容:
Copy% block domready %
$('a.like').click(function (e)
e.preventDefault();
$.post('% url 'images:like' %',
id: $(this).data('id'),
action: $(this).data('action'),
,
function (data)
if (data['status'] === 'ok')
let previous_action = $('a.like').data('action');
//切换 data-action 属性
$('a.like').data('action', previous_action === 'like' ? 'unlike' : 'like');
//切换按钮文本
$('a.like').text(previous_action === 'like' ? 'Unlike' : 'Like');
//更新总的喜欢人数
let previous_likes = parseInt($('span.count.total').text());
$('span.count.total').text(previous_action === 'like' ? previous_likes + 1 : previous_likes - 1);
);
);
% endblock %
这段代码的逻辑解释如下:
使用$('a.like')选择所有属于like类的<a>标签
给<a>标签绑定click事件,每次点击就发送AJAX请求。
在事件处理函数内,使用e.preventDefault()阻止<a>的默认功能,即阻止打开新的超链接
使用$.post()发送异步的POST请求。jQuery还提供了$.get()用于发送异步的GET请求,和一个更底层的$.ajax()方法。
使用% url %反向解析出AJAX的请求目标地址
创建要发送的数据字典,通过<a>标签的data-id和data-action设置id和action键值对。
设置回调函数,当成功收到AJAX响应时执行,响应数据被包含在对象data中。
根据data中的status判断值是否为ok,如果是则切换data-action和按钮文本。
根据刚才执行的结果,对总喜欢人数增加1或者减少1
译者注:原书这里的逻辑是为了让读者可以迅速看出操作结果。在多用户的环境中,不能如此简单的增减1,因为每次执行动作后,该人数的变化未必是1。
打开任意图片详情页,可以看到新增的总人数和按钮,如下所示:
点击一下LIKE按钮,可以看到如下所示:
如果再点击UNLIKE按钮,可以看到按钮变回LIKE,人数也减少1
如果提示The 'photo' attribute has no file associated with it错误,原书作者在这里没有讲清楚,错误原因是detail.html页面用了user.profile.photo.url,但没有上传用户头像。在管理后台给每个用户上传头像,再访问任意详情图片页,就不会报错了。直接修改多对多的关系再查看这张表,就能发现显示出同样喜欢了这张图的用户头像和名称。这里如果要完善的话,应该判断用户是否上传头像,如果没有就用默认头像代替。
当编写JavaScript代码发送AJAX请求时,为了方便调试,推荐使用开发工具而不是在Django中编写代码。现代浏览器都带有开发工具用于调试页面和JavaScript代码,通常可以按F12或者在页面上右击选“检查”来启动开发工具。
6创建自定义装饰器
在AJAX视图中使用了@require_POST装饰器以限制视图仅接受POST请求,这显然还不够,需要让这个视图仅接受AJAX请求才行。Django对于HTTP请求对象提供了一个is_ajax()方法,通过HTTP请求头部字段HTTP_X_REQUESTED_WITH HTTP判断该请求是否是一个XMLHttpRequest对象,即一个AJAX请求。
我们准备自行编写一个装饰器,用于检查HTTP请求的HTTP_X_REQUESTED_WITH头部信息,从而限制我们的视图仅接受AJAX请求。Python中的装饰器是接受一个函数为参数的函数,为参数函数附加执行额外功能而不改变原函数的功能。 如果对装饰器不太了解,可以参考Python官方文档:https://www.python.org/dev/peps/pep-0318/。
我们准备编写的装饰器是通用的,所以在bookmarks项目根目录下建立一个common包,其中的文件如下:
Copycommon/
__init__.py
decorators.py
编辑decorators.py文件,添加下列代码:
Copyfrom django.http import HttpResponseBadRequest
defajax_required(func):
defwrap(request, *args, kwargs):
ifnot request.is_ajax():
return HttpResponseBadRequest()
else:
return func(request, *args, kwargs)
wrap.__doc__ = func.__doc__
wrap.__name__ = func.__name__
return wrap
这段代码就是自定义的ajax_required装饰器函数。其中定义了一个wrap函数,如果请求不是AJAX请求,就返回HttpResponseBadRequest即HTTP 400错误。如果是AJAX请求,则原来视图的功能正常执行。
然后编辑images应用的views.py文件,导入新的包然后为视图添加自定义装饰器:
Copyfrom common.decorators import ajax_required
@ajax_required@login_required@require_POSTdefimage_like(request):
# ......
如果用浏览器直接访问http://127.0.0.1:8000/images/like/,会得到400错误。(未添加该装饰器之前,得到的是由@require_POST返回的405错误)。
如果你发现
以上是关于内容分享功能的主要内容,如果未能解决你的问题,请参考以下文章