这是本教程系列的第六部分! 在本教程中,我们将详细探索基于类的视图。 我们还将重构一些现有的视图,以便利用内置的基于通用类的视图。
我们将在本教程中介绍许多其他主题,例如如何使用分页,如何使用Markdown以及如何添加简单的编辑器。 我们还将探索一个名为Humanize的内置包,它用于为数据提供“人性化”。
好吧,伙计们! 让我们实现一些代码。 我们今天有很多工作要做!
观点策略
在一天结束时,所有Django视图都是函数 。 甚至是基于类的视图(CBV)。 在幕后,它完成所有魔术并最终返回视图功能。
引入了基于类的视图,使开发人员可以更轻松地重用和扩展视图。 使用它们有很多好处,例如可扩展性,使用OO技术(如多重继承)的能力,HTTP方法的处理是在不同的方法中完成的,而不是使用条件分支,还有基于通用的类。意见(GCBV)。
在我们继续前进之前,让我们澄清这三个术语的含义:
- 基于功能的视图(FBV)
- 基于类的视图(CBV)
- 通用的基于类的视图(GCBV)
FBV是Django视图的最简单表示:它只是一个接收HttpRequest对象并返回HttpResponse的函数。
CBV是每个Django视图定义为扩展django.views.generic.View抽象类的Python类。 CBV本质上是一个包裹FBV的类。 CBV非常适合扩展和重用代码。
GCBV是内置的CBV,可以解决列出视图,创建,更新和删除视图等特定问题。
下面我们将探讨不同实现策略的一些示例。
基于功能的视图
views.py
def new_post ( request ): if request . method == 'POST' : form = PostForm ( request . POST ) if form . is_valid (): form . save () return redirect ( 'post_list' ) else : form = PostForm () return render ( request , 'new_post.html' , { 'form' : form })
urls.py
urlpatterns = [ url ( r'^new_post/$' , views . new_post , name = 'new_post' ), ]
基于类的视图
CBV是扩展View类的视图 。 这里的主要区别是请求是在HTTP方法命名的类方法中处理的,例如get , post , put , head等。
所以,在这里我们不需要做一个条件来检查请求是POST还是GET 。 代码直接用于正确的方法。 此逻辑在View类内部处理。
views.py
from django.views.generic import View class NewPostView ( View ): def post ( self , request ): form = PostForm ( request . POST ) if form . is_valid (): form . save () return redirect ( 'post_list' ) return render ( request , 'new_post.html' , { 'form' : form }) def get ( self , request ): form = PostForm () return render ( request , 'new_post.html' , { 'form' : form })
我们在urls.py模块中引用CBV的方式有点不同:
urls.py
urlpatterns = [ url ( r'^new_post/$' , views . NewPostView . as_view (), name = 'new_post' ), ]
这里我们需要使用as_view()类方法,该方法将视图函数返回到url模式。 在某些情况下,我们还可以使用一些关键字参数提供as_view() ,以便自定义CBV的行为,就像我们使用一些身份验证视图来自定义模板一样。
无论如何,关于CBV的好处是我们可以添加更多方法,也许可以这样做:
from django.views.generic import View class NewPostView ( View ): def render ( self , request ): return render ( request , 'new_post.html' , { 'form' : self . form }) def post ( self , request ): self . form = PostForm ( request . POST ) if self . form . is_valid (): self . form . save () return redirect ( 'post_list' ) return self . render ( request ) def get ( self , request ): self . form = PostForm () return self . render ( request )
也可以创建一些完成某些任务的通用视图,以便我们可以在整个项目中重用它。
但这几乎是你需要了解的关于CBV的所有知识。 就那么简单。
通用的基于类的视图
现在关于GCBV。 那是一个不同的故事。 正如我之前提到的,这些视图是针对常见用例的内置CBV。 它们的实现大量使用了多个继承(mixins)和其他OO策略。
它们非常灵活,可以节省大量的工作时间。 但是在开始时,与它们合作可能很困难。
当我第一次开始使用Django时,我发现GCBV难以使用。 起初,很难说出发生了什么,因为代码流并不明显,因为父类中隐藏了大量代码。 文档也很难跟上,主要是因为属性和方法有时会分布在八个父类中。 使用GCBV时,打开ccbv.co.uk以便快速参考总是好的。 不用担心,我们将一起探索它。
现在让我们看一个GCBV示例。
views.py
from django.views.generic import CreateView class NewPostView ( CreateView ): model = Post form_class = PostForm success_url = reverse_lazy ( 'post_list' ) template_name = 'new_post.html'
这里我们使用用于创建模型对象的通用视图。 如果表单有效,它会执行所有表单处理并保存对象。
由于它是CBV,我们在urls.py中以与任何其他CBV相同的方式引用它:
urls.py
urlpatterns = [ url ( r'^new_post/$' , views . NewPostView . as_view (), name = 'new_post' ), ]
GCBV的其他示例是DetailView , DeleteView , FormView , UpdateView , ListView 。
更新视图
让我们回到我们项目的实施。 这次我们将使用GCBV来实现编辑后视图:
boards / views.py (查看完整的文件内容)
from django.shortcuts import redirect from django.views.generic import UpdateView from django.utils import timezone class PostUpdateView ( UpdateView ): model = Post fields = ( 'message' , ) template_name = 'edit_post.html' pk_url_kwarg = 'post_pk' context_object_name = 'post' def form_valid ( self , form ): post = form . save ( commit = False ) post . updated_by = self . request . user post . updated_at = timezone . now () post . save () return redirect ( 'topic_posts' , pk = post . topic . board . pk , topic_pk = post . topic . pk )
使用UpdateView和CreateView ,我们可以选择定义form_class或fields属性。 在上面的示例中,我们使用fields属性即时创建模型表单。 在内部,Django将使用模型表单工厂来组成Post模型的一种形式。 由于它只是一个非常简单的形式,只有消息字段,我们可以像这样工作。 但对于复杂的表单定义,最好在外部定义模型表单并在此处引用它。
pk_url_kwarg将用于标识用于检索Post对象的关键字参数的名称。 它与我们在urls.py中定义的相同。
如果我们不设置context_object_name属性,则Post对象将在模板中作为“object”使用。因此,这里我们使用context_object_name将其重命名为post 。 您将在下面的模板中看到我们如何使用它。
在这个特定的例子中,我们必须覆盖form_valid()方法,以便设置一些额外的字段,例如updated_by和updated_at 。 您可以在此处查看基本form_valid()方法的内容: UpdateView#form_valid 。
myproject / urls.py (查看完整的文件内容)
from django.conf.urls import url from boards import views urlpatterns = [ # ... url ( r'^boards/(?P<pk> \ d+)/topics/(?P<topic_pk> \ d+)/posts/(?P<post_pk> \ d+)/edit/$' , views . PostUpdateView . as_view (), name = 'edit_post' ), ]
现在我们可以添加指向编辑页面的链接:
templates / topic_posts.html (查看完整的文件内容)
{% if post.created_by == user %} <div class= "mt-3" > <a href= " {% url 'edit_post' post.topic.board.pk post.topic.pk post.pk %} " class= "btn btn-primary btn-sm" role= "button" > Edit </a> </div> {% endif %}
templates / edit_post.html (查看完整的文件内容)
{% extends 'base.html' %} {% block title %} Edit post {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item" ><a href= " {% url 'home' %} " > Boards </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'board_topics' post.topic.board.pk %} " > {{ post.topic.board.name }} </a></li> <li class= "breadcrumb-item" ><a href= " {% url 'topic_posts' post.topic.board.pk post.topic.pk %} " > {{ post.topic.subject }} </a></li> <li class= "breadcrumb-item active" > Edit post </li> {% endblock %} {% block content %} <form method= "post" class= "mb-4" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-success" > Save changes </button> <a href= " {% url 'topic_posts' post.topic.board.pk post.topic.pk %} " class= "btn btn-outline-secondary" role= "button" > Cancel </a> </form> {% endblock %}
现在观察我们如何浏览post对象: post.topic.board.pk 。 如果我们没有将context_object_name设置为post ,则它将以: object.topic.board.pk的形式提供。 得到它了?
测试更新视图
在boards / tests文件夹中创建一个名为test_view_edit_post.py的新测试文件。 单击下面的链接,您将看到许多例行测试,就像我们在本教程中一样。 所以我将在这里重点介绍新的部分:
boards / tests / test_view_edit_post.py (查看完整的文件内容)
from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse from ..models import Board , Post , Topic from ..views import PostUpdateView class PostUpdateViewTestCase ( TestCase ): ''' Base test case to be used in all `PostUpdateView` view tests ''' def setUp ( self ): self . board = Board . objects . create ( name = 'Django' , description = 'Django board.' ) self . username = 'john' self . password = '123' user = User . objects . create_user ( username = self . username , email = 'john@doe.com' , password = self . password ) self . topic = Topic . objects . create ( subject = 'Hello, world' , board = self . board , starter = user ) self . post = Post . objects . create ( message = 'Lorem ipsum dolor sit amet' , topic = self . topic , created_by = user ) self . url = reverse ( 'edit_post' , kwargs = { 'pk' : self . board . pk , 'topic_pk' : self . topic . pk , 'post_pk' : self . post . pk }) class LoginRequiredPostUpdateViewTests ( PostUpdateViewTestCase ): def test_redirection ( self ): ''' Test if only logged in users can edit the posts ''' login_url = reverse ( 'login' ) response = self . client . get ( self . url ) self . assertRedirects ( response , '{login_url}?next={url}' . format ( login_url = login_url , url = self . url )) class UnauthorizedPostUpdateViewTests ( PostUpdateViewTestCase ): def setUp ( self ): ''' Create a new user different from the one who posted ''' super () . setUp () username = 'jane' password = '321' user = User . objects . create_user ( username = username , email = 'jane@doe.com' , password = password ) self . client . login ( username = username , password = password ) self . response = self . client . get ( self . url ) def test_status_code ( self ): ''' A topic should be edited only by the owner. Unauthorized users should get a 404 response (Page Not Found) ''' self . assertEquals ( self . response . status_code , 404 ) class PostUpdateViewTests ( PostUpdateViewTestCase ): # ... class SuccessfulPostUpdateViewTests ( PostUpdateViewTestCase ): # ... class InvalidPostUpdateViewTests ( PostUpdateViewTestCase ): # ...
目前,重要的部分是: PostUpdateViewTestCase是我们定义为在其他测试用例中重用的类。 这只是基本设置,创建用户,主题,电路板等。
LoginRequiredPostUpdateViewTests类将测试视图是否受@login_required装饰器保护。 也就是说,只有经过身份验证的用户才能访问编辑页面。
UnauthorizedPostUpdateViewTests类创建一个新用户,与发布并尝试访问编辑页面的用户不同。 应用程序应仅授权帖子的所有者进行编辑。
让我们运行测试:
python manage.py test boards.tests.test_view_edit_post
Creating test database for alias 'default'... System check identified no issues (0 silenced). ..F.......F ====================================================================== FAIL: test_redirection (boards.tests.test_view_edit_post.LoginRequiredPostUpdateViewTests) ---------------------------------------------------------------------- ... AssertionError: 200 != 302 : Response didn't redirect as expected: Response code was 200 (expected 302) ====================================================================== FAIL: test_status_code (boards.tests.test_view_edit_post.UnauthorizedPostUpdateViewTests) ---------------------------------------------------------------------- ... AssertionError: 200 != 404 ---------------------------------------------------------------------- Ran 11 tests in 1.360s FAILED (failures=2) Destroying test database for alias 'default'...
首先,让我们用@login_required装饰器修复问题。 我们在基于类的视图上使用视图装饰器的方式有点不同。 我们需要额外的导入:
boards / views.py (查看完整的文件内容)
from django.contrib.auth.decorators import login_required from django.shortcuts import redirect from django.views.generic import UpdateView from django.utils import timezone from django.utils.decorators import method_decorator from .models import Post @method_decorator ( login_required , name = 'dispatch' ) class PostUpdateView ( UpdateView ): model = Post fields = ( 'message' , ) template_name = 'edit_post.html' pk_url_kwarg = 'post_pk' context_object_name = 'post' def form_valid ( self , form ): post = form . save ( commit = False ) post . updated_by = self . request . user post . updated_at = timezone . now () post . save () return redirect ( 'topic_posts' , pk = post . topic . board . pk , topic_pk = post . topic . pk )
我们不能直接使用@login_required装饰器来装饰类。 我们必须使用实用程序@method_decorator ,并传递装饰器(或装饰器列表)并告诉应该装饰哪个方法。 在基于类的视图中,装饰调度方法很常见。 这是Django使用的内部方法(在View类中定义)。 所有请求都通过此方法,因此装饰它是安全的。
运行测试:
python manage.py test boards.tests.test_view_edit_post
Creating test database for alias 'default'... System check identified no issues (0 silenced). ..........F ====================================================================== FAIL: test_status_code (boards.tests.test_view_edit_post.UnauthorizedPostUpdateViewTests) ---------------------------------------------------------------------- ... AssertionError: 200 != 404 ---------------------------------------------------------------------- Ran 11 tests in 1.353s FAILED (failures=1) Destroying test database for alias 'default'...
好的! 我们修复了@login_required问题。 现在我们必须处理其他用户编辑任何帖子的问题。
解决此问题的最简单方法是重写get_queryset方法。 您可以在此处查看原始方法的内容UpdateView#get_queryset 。
boards / views.py (查看完整的文件内容)
@method_decorator ( login_required , name = 'dispatch' ) class PostUpdateView ( UpdateView ): model = Post fields = ( 'message' , ) template_name = 'edit_post.html' pk_url_kwarg = 'post_pk' context_object_name = 'post' def get_queryset ( self ): queryset = super () . get_queryset () return queryset . filter ( created_by = self . request . user ) def form_valid ( self , form ): post = form . save ( commit = False ) post . updated_by = self . request . user post . updated_at = timezone . now () post . save () return redirect ( 'topic_posts' , pk = post . topic . board . pk , topic_pk = post . topic . pk )
使用行queryset = super().get_queryset()我们重用了父类的get_queryset方法,即UpateView类。 然后,我们在查询集中添加一个额外的过滤器,该过滤器使用登录用户过滤帖子,该用户在请求对象中可用。
再试一次:
python manage.py test boards.tests.test_view_edit_post
Creating test database for alias 'default'... System check identified no issues (0 silenced). ........... ---------------------------------------------------------------------- Ran 11 tests in 1.321s OK Destroying test database for alias 'default'...
都好!
列表显示
我们可以重构一些现有的视图以利用CBV功能。 以主页为例。 我们只是抓住数据库中的所有主板并将其列在HTML中:
板/ views.py
from django.shortcuts import render from .models import Board def home(request): boards = Board.objects.all() return render(request, 'home.html', {'boards': boards})
以下是我们如何使用GCBV重写模型列表:
boards / views.py (查看完整的文件内容)
from django.views.generic import ListView from .models import Board class BoardListView ( ListView ): model = Board context_object_name = 'boards' template_name = 'home.html'
然后我们必须更改urls.py模块中的引用:
myproject / urls.py (查看完整的文件内容)
from django.conf.urls import url from boards import views urlpatterns = [ url ( r'^$' , views . BoardListView . as_view (), name = 'home' ), # ... ]
如果我们检查主页,我们会看到没有真正改变,一切都按预期工作。 但是我们必须稍微调整一下我们的测试,因为现在我们正在处理基于类的视图:
boards / tests / test_view_home.py (查看完整的文件内容)
from django.test import TestCase from django.urls import resolve from ..views import BoardListView class HomeTests ( TestCase ): # ... def test_home_url_resolves_home_view ( self ): view = resolve ( '/' ) self . assertEquals ( view . func . view_class , BoardListView )
分页
我们可以非常轻松地实现基于类的视图的分页。 但首先我想手工分页,以便我们可以更好地探索它背后的机制,所以它看起来不像魔术。
对电路板列表视图进行分页是没有意义的,因为我们不希望有很多电路板。 但绝对是主题列表和帖子列表需要一些分页。
从现在开始,我们将开发board_topics视图。
首先,让我们添加一些帖子。 我们可以使用应用程序的用户界面并添加几个帖子,或者打开Python shell并编写一个小脚本来为我们完成:
python manage.py shell
from django.contrib.auth.models import User from boards.models import Board , Topic , Post user = User . objects . first () board = Board . objects . get ( name = 'Django' ) for i in range ( 100 ): subject = 'Topic test #{}' . format ( i ) topic = Topic . objects . create ( subject = subject , board = board , starter = user ) Post . objects . create ( message = 'Lorem ipsum...' , topic = topic , created_by = user )
很好,现在我们有一些数据可以使用。
在我们进入代码之前,让我们使用Python shell进行更多实验:
python manage.py shell
from boards.models import Topic # All the topics in the app Topic . objects . count () 107 # Just the topics in the Django board Topic . objects . filter ( board__name = 'Django' ) . count () 104 # Let's save this queryset into a variable to paginate it queryset = Topic . objects . filter ( board__name = 'Django' ) . order_by ( '-last_updated' )
始终为要分页的QuerySet定义一个排序非常重要! 否则,它会给你不一致的结果。
现在让我们导入Paginator实用程序:
from django.core.paginator import Paginator paginator = Paginator ( queryset , 20 )
在这里,我们告诉Django以20页为单位对QuerySet进行分页。 现在让我们探讨一些paginator属性:
# count the number of elements in the paginator paginator . count 104 # total number of pages # 104 elements, paginating 20 per page gives you 6 pages # where the last page will have only 4 elements paginator . num_pages 6 # range of pages that can be used to iterate and create the # links to the pages in the template paginator . page_range range ( 1 , 7 ) # returns a Page instance paginator . page ( 2 ) < Page 2 of 6 > page = paginator . page ( 2 ) type ( page ) django . core . paginator . Page type ( paginator ) django . core . paginator . Paginator
在这里我们必须要注意,因为如果我们尝试获取一个不存在的页面,Paginator将抛出一个异常:
paginator . page ( 7 ) EmptyPage : That page contains no results
或者,如果我们尝试传递一个不是页码的任意参数:
paginator . page ( 'abc' ) PageNotAnInteger : That page number is not an integer
在设计用户界面时,我们必须牢记这些细节。
现在让我们稍微探讨一下Page类提供的属性和方法:
page = paginator . page ( 1 ) # Check if there is another page after this one page . has_next () True # If there is no previous page, that means this one is the first page page . has_previous () False page . has_other_pages () True page . next_page_number () 2 # Take care here, since there is no previous page, # if we call the method `previous_page_number() we will get an exception: page . previous_page_number () EmptyPage : That page number is less than 1
FBV分页
以下是我们使用常规基于函数的视图的方法:
boards / views.py (查看完整的文件内容)
from django.db.models import Count from django.core.paginator import Paginator , EmptyPage , PageNotAnInteger from django.shortcuts import get_object_or_404 , render from django.views.generic import ListView from .models import Board def board_topics ( request , pk ): board = get_object_or_404 ( Board , pk = pk ) queryset = board . topics . order_by ( '-last_updated' ) . annotate ( replies = Count ( 'posts' ) - 1 ) page = request . GET . get ( 'page' , 1 ) paginator = Paginator ( queryset , 20 ) try : topics = paginator . page ( page ) except PageNotAnInteger : # fallback to the first page topics = paginator . page ( 1 ) except EmptyPage : # probably the user tried to add a page number # in the url, so we fallback to the last page topics = paginator . page ( paginator . num_pages ) return render ( request , 'topics.html' , { 'board' : board , 'topics' : topics })
现在的技巧部分是使用Bootstrap 4分页组件正确呈现页面。 但是花点时间阅读代码,看看它是否对你有意义。 我们在这里使用我们以前玩过的所有方法。 在这种情况下, topics不再是QuerySet而是paginator.Page实例。
在主题HTML表之后,我们可以渲染分页组件:
templates / topics.html (查看完整的文件内容)
{% if topics.has_other_pages %} <nav aria-label= "Topics pagination" class= "mb-4" > <ul class= "pagination" > {% if topics.has_previous %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ topics.previous_page_number }} " > Previous </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Previous </span> </li> {% endif %} {% for page_num in topics.paginator.page_range %} {% if topics. number == page_num %} <li class= "page-item active" > <span class= "page-link" > {{ page_num }} <span class= "sr-only" > (current) </span> </span> </li> {% else %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_num }} " > {{ page_num }} </a> </li> {% endif %} {% endfor %} {% if topics.has_next %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ topics.next_page_number }} " > Next </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Next </span> </li> {% endif %} </ul> </nav> {% endif %}
GCBV分页
下面,相同的实现,但这次使用ListView 。
boards / views.py (查看完整的文件内容)
class TopicListView ( ListView ): model = Topic context_object_name = 'topics' template_name = 'topics.html' paginate_by = 20 def get_context_data ( self , ** kwargs ): kwargs [ 'board' ] = self . board return super () . get_context_data ( ** kwargs ) def get_queryset ( self ): self . board = get_object_or_404 ( Board , pk = self . kwargs . get ( 'pk' )) queryset = self . board . topics . order_by ( '-last_updated' ) . annotate ( replies = Count ( 'posts' ) - 1 ) return queryset
在使用基于类的视图分页时,我们与模板中的分页器交互的方式略有不同。 它将在模板中提供以下变量: paginator , page_obj , is_paginated , object_list ,以及具有我们在context_object_name中定义的名称的变量。 在我们的例子中,这个额外的变量将被命名为topic ,它将等同于object_list 。
现在关于整个get_context_data事情,那就是我们在扩展GCBV时向请求上下文添加内容的方式。
但这里的要点是paginate_by属性。 在某些情况下,只需添加它就足够了。
请记住更新urls.py :
myproject / urls.py (查看完整的文件内容)
from django.conf.urls import url from boards import views urlpatterns = [ # ... url ( r'^boards/(?P<pk> \ d+)/$' , views . TopicListView . as_view (), name = 'board_topics' ), ]
现在让我们修复模板:
templates / topics.html (查看完整的文件内容)
{% block content %} <div class= "mb-4" > <a href= " {% url 'new_topic' board.pk %} " class= "btn btn-primary" > New topic </a> </div> <table class= "table mb-4" > <!-- table content suppressed --> </table> {% if is_paginated %} <nav aria-label= "Topics pagination" class= "mb-4" > <ul class= "pagination" > {% if page_obj.has_previous %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_obj.previous_page_number }} " > Previous </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Previous </span> </li> {% endif %} {% for page_num in paginator.page_range %} {% if page_obj. number == page_num %} <li class= "page-item active" > <span class= "page-link" > {{ page_num }} <span class= "sr-only" > (current) </span> </span> </li> {% else %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_num }} " > {{ page_num }} </a> </li> {% endif %} {% endfor %} {% if page_obj.has_next %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_obj.next_page_number }} " > Next </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Next </span> </li> {% endif %} </ul> </nav> {% endif %} {% endblock %}
现在花点时间运行测试并在需要时修复。
板/测试/ test_view_board_topics.py
from django.test import TestCase from django.urls import resolve from ..views import TopicListView class BoardTopicsTests ( TestCase ): # ... def test_board_topics_url_resolves_board_topics_view ( self ): view = resolve ( '/boards/1/' ) self . assertEquals ( view . func . view_class , TopicListView )
可重复使用的分页模板
就像我们使用form.html部分模板一样,我们也可以为分页HTML片段创建类似的东西。
让我们对主题帖子页面进行分页,然后找到一种重用分页组件的方法。
boards / views.py (查看完整的文件内容)
class PostListView ( ListView ): model = Post context_object_name = 'posts' template_name = 'topic_posts.html' paginate_by = 2 def get_context_data ( self , ** kwargs ): self . topic . views += 1 self . topic . save () kwargs [ 'topic' ] = self . topic return super () . get_context_data ( ** kwargs ) def get_queryset ( self ): self . topic = get_object_or_404 ( Topic , board__pk = self . kwargs . get ( 'pk' ), pk = self . kwargs . get ( 'topic_pk' )) queryset = self . topic . posts . order_by ( 'created_at' ) return queryset
现在更新urls.py (查看完整的文件内容)
from django.conf.urls import url from boards import views urlpatterns = [ # ... url ( r'^boards/(?P<pk> \ d+)/topics/(?P<topic_pk> \ d+)/$' , views . PostListView . as_view (), name = 'topic_posts' ), ]
现在我们从topics.html模板中获取分页HTML片段,并在templates / includes文件夹中创建一个名为pagination.html的新文件,以及forms.html文件:
myproject/ |-- myproject/ | |-- accounts/ | |-- boards/ | |-- myproject/ | |-- static/ | |-- templates/ | | |-- includes/ | | | |-- form.html | | | +-- pagination.html <-- here! | | +-- ... | |-- db.sqlite3 | +-- manage.py +-- venv/
模板/包括/ pagination.html
{% if is_paginated %} <nav aria-label= "Topics pagination" class= "mb-4" > <ul class= "pagination" > {% if page_obj.has_previous %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_obj.previous_page_number }} " > Previous </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Previous </span> </li> {% endif %} {% for page_num in paginator.page_range %} {% if page_obj. number == page_num %} <li class= "page-item active" > <span class= "page-link" > {{ page_num }} <span class= "sr-only" > (current) </span> </span> </li> {% else %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_num }} " > {{ page_num }} </a> </li> {% endif %} {% endfor %} {% if page_obj.has_next %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_obj.next_page_number }} " > Next </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Next </span> </li> {% endif %} </ul> </nav> {% endif %}
现在在topic_posts.html模板中我们使用它:
templates / topic_posts.html (查看完整的文件内容)
{% block content %} <div class= "mb-4" > <a href= " {% url 'reply_topic' topic.board.pk topic.pk %} " class= "btn btn-primary" role= "button" > Reply </a> </div> {% for post in posts %} <div class= "card {% if forloop.last %} mb-4 {% else %} mb-2 {% endif %} {% if forloop.first %} border-dark {% endif %} " > {% if forloop.first %} <div class= "card-header text-white bg-dark py-2 px-3" > {{ topic.subject }} </div> {% endif %} <div class= "card-body p-3" > <div class= "row" > <div class= "col-2" > <img src= " {% static 'img/avatar.svg' %} " alt= " {{ post.created_by.username }} " class= "w-100" > <small> Posts: {{ post.created_by.posts.count }} </small> </div> <div class= "col-10" > <div class= "row mb-3" > <div class= "col-6" > <strong class= "text-muted" > {{ post.created_by.username }} </strong> </div> <div class= "col-6 text-right" > <small class= "text-muted" > {{ post.created_at }} </small> </div> </div> {{ post.message }} {% if post.created_by == user %} <div class= "mt-3" > <a href= " {% url 'edit_post' post.topic.board.pk post.topic.pk post.pk %} " class= "btn btn-primary btn-sm" role= "button" > Edit </a> </div> {% endif %} </div> </div> </div> </div> {% endfor %} {% include 'includes/pagination.html' %} {% endblock %}
不要忘记将主要的forloop更改为{ % for post in posts % } { % for post in posts % } { % for post in posts % } 。
我们还可以更新以前的模板, topics.html模板以使用分页部分模板:
templates / topics.html (查看完整的文件内容)
{% block content %} <div class= "mb-4" > <a href= " {% url 'new_topic' board.pk %} " class= "btn btn-primary" > New topic </a> </div> <table class= "table mb-4" > <!-- table code suppressed --> </table> {% include 'includes/pagination.html' %} {% endblock %}
仅出于测试目的,您可以添加一些帖子(或使用Python Shell创建一些帖子)并将paginate_by更改为较小的数字,例如2,并查看它的外观:
(查看完整的文件内容)
更新测试用例:
板/测试/ test_view_topic_posts.py
from django.test import TestCase from django.urls import resolve from ..views import PostListView class TopicPostsTests ( TestCase ): # ... def test_view_function ( self ): view = resolve ( '/boards/1/topics/1/' ) self . assertEquals ( view . func . view_class , PostListView )
我的帐户视图
好的,这将是我们最后的观点。 之后,我们将致力于改进现有功能。
accounts / views.py (查看完整的文件内容)
from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.views.generic import UpdateView @method_decorator ( login_required , name = 'dispatch' ) class UserUpdateView ( UpdateView ): model = User fields = ( 'first_name' , 'last_name' , 'email' , ) template_name = 'my_account.html' success_url = reverse_lazy ( 'my_account' ) def get_object ( self ): return self . request . user
myproject / urls.py (查看完整的文件内容)
from django.conf.urls import url from accounts import views as accounts_views urlpatterns = [ # ... url ( r'^settings/account/$' , accounts_views . UserUpdateView . as_view (), name = 'my_account' ), ]
模板/ my_account.html
{% extends 'base.html' %} {% block title %} My account {% endblock %} {% block breadcrumb %} <li class= "breadcrumb-item active" > My account </li> {% endblock %} {% block content %} <div class= "row" > <div class= "col-lg-6 col-md-8 col-sm-10" > <form method= "post" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-success" > Save changes </button> </form> </div> </div> {% endblock %}
添加Markdown
让我们通过在文本区域添加Markdown来改善用户体验。 你会发现它非常容易和简单。
首先,让我们安装一个名为Python-Markdown的库:
pip install markdown
我们可以为Post模型添加一个新方法:
boards / models.py (查看完整的文件内容)
from django.db import models from django.utils.html import mark_safe from markdown import markdown class Post ( models . Model ): # ... def get_message_as_markdown ( self ): return mark_safe ( markdown ( self . message , safe_mode = 'escape' ))
这里我们处理用户输入,所以我们必须小心。 当使用markdown函数时,我们首先指示它转义特殊字符,然后解析markdown标记。 之后,我们将输出字符串标记为在模板中使用是安全的。
现在在模板topic_posts.html和reply_topic.html中只需更改:
{{ post.message }}
至:
{{ post.get_message_as_markdown }}
从现在开始,用户可以在帖子中使用markdown:
Markdown编辑
我们还可以添加一个名为SimpleMD的非常酷的Markdown编辑器。
下载JavaScript库或使用他们的CDN:
<link rel= "stylesheet" href= "https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css" > <script src= "https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js" ></script>
现在编辑base.html以为额外的JavaScripts腾出空间:
templates / base.html (查看完整的文件内容)
<script src= " {% static 'js/jquery-3.2.1.min.js' %} " ></script> <script src= " {% static 'js/popper.min.js' %} " ></script> <script src= " {% static 'js/bootstrap.min.js' %} " ></script> {% block javascript %}{% endblock %} <!-- Add this empty block here! -->
首先编辑reply_topic.html模板:
templates / reply_topic.html (查看完整的文件内容)
{% extends 'base.html' %} {% load static %} {% block title %} Post a reply {% endblock %} {% block stylesheet %} <link rel= "stylesheet" href= " {% static 'css/simplemde.min.css' %} " > {% endblock %} {% block javascript %} <script src= " {% static 'js/simplemde.min.js' %} " ></script> <script> var simplemde = new SimpleMDE (); </script> {% endblock %}
默认情况下,此插件会将找到的第一个文本区域转换为markdown编辑器。 所以只需要代码就足够了:
现在使用edit_post.html模板执行相同的操作:
templates / edit_post.html (查看完整的文件内容)
{% extends 'base.html' %} {% load static %} {% block title %} Edit post {% endblock %} {% block stylesheet %} <link rel= "stylesheet" href= " {% static 'css/simplemde.min.css' %} " > {% endblock %} {% block javascript %} <script src= " {% static 'js/simplemde.min.js' %} " ></script> <script> var simplemde = new SimpleMDE (); </script> {% endblock %}
赋予人性
我只是觉得添加内置的人性化软件包会很不错。 它是一组实用程序函数,用于为数据添加“人性化”。
例如,我们可以使用它更自然地显示日期和时间字段。 我们可以简单地显示:“2分钟前”,而不是显示整个日期。
我们开始做吧。 首先,将django.contrib.humanize添加到INSTALLED_APPS 。
的myproject / settings.py
INSTALLED_APPS = [ 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , 'django.contrib.humanize' , # <- here 'widget_tweaks' , 'accounts' , 'boards' , ]
现在我们可以在模板中使用它。 首先,让我们编辑topics.html模板:
templates / topics.html (查看完整的文件内容)
{% extends 'base.html' %} {% load humanize %} {% block content %} <!-- code suppressed --> <td> {{ topic.last_updated | naturaltime }} </td> <!-- code suppressed --> {% endblock %}
我们所要做的就是使用{ % load humanize % }加载模板标签 { % load humanize % } { % load humanize % }然后应用模板过滤器: { { topic.last_updated|naturaltime } } { { topic.last_updated|naturaltime } }
您可以将它添加到您喜欢的其他地方。
的Gravatar
添加用户个人资料图片的一种非常简单的方法是使用Gravatar 。
在boards / templatetags文件夹中,创建一个名为gravatar.py的新文件:
板/ templatetags / gravatar.py
import hashlib from urllib.parse import urlencode from django import template from django.conf import settings register = template . Library () @register.filter def gravatar ( user ): email = user . email . lower () . encode ( 'utf-8' ) default = 'mm' size = 256 url = 'https://www.gravatar.com/avatar/{md5}?{params}' . format ( md5 = hashlib . md5 ( email ) . hexdigest (), params = urlencode ({ 'd' : default , 's' : str ( size )}) ) return url
基本上我正在使用他们提供的代码片段 。 我只是改编它以使用Python 3。
好的,现在我们可以在模板中加载它,就像我们使用Humanize模板过滤器一样:
templates / topic_posts.html (查看完整的文件内容)
{% extends 'base.html' %} {% load gravatar %} {% block content %} <!-- code suppressed --> <img src= " {{ post.created_by | gravatar }} " alt= " {{ post.created_by.username }} " class= "w-100 rounded" > <!-- code suppressed --> {% endblock %}
最终调整
也许你已经注意到了,但是当有人回复帖子时会出现一个小问题。 它不会更新last_update字段,因此主题的排序现在就被打破了。
我们来解决它:
板/ views.py
@login_required def reply_topic ( request , pk , topic_pk ): topic = get_object_or_404 ( Topic , board__pk = pk , pk = topic_pk ) if request . method == 'POST' : form = PostForm ( request . POST ) if form . is_valid (): post = form . save ( commit = False ) post . topic = topic post . created_by = request . user post . save () topic . last_updated = timezone . now () # <- here topic . save () # <- and here return redirect ( 'topic_posts' , pk = pk , topic_pk = topic_pk ) else : form = PostForm () return render ( request , 'reply_topic.html' , { 'topic' : topic , 'form' : form })
接下来我们要做的是尝试更多地控制视图计数系统。 我们不希望同一用户刷新页面计数为多个视图。 为此,我们可以使用会话:
板/ views.py
class PostListView ( ListView ): model = Post context_object_name = 'posts' template_name = 'topic_posts.html' paginate_by = 20 def get_context_data ( self , ** kwargs ): session_key = 'viewed_topic_{}' . format ( self . topic . pk ) # <-- here if not self . request . session . get ( session_key , False ): self . topic . views += 1 self . topic . save () self . request . session [ session_key ] = True # <-- until here kwargs [ 'topic' ] = self . topic return super () . get_context_data ( ** kwargs ) def get_queryset ( self ): self . topic = get_object_or_404 ( Topic , board__pk = self . kwargs . get ( 'pk' ), pk = self . kwargs . get ( 'topic_pk' )) queryset = self . topic . posts . order_by ( 'created_at' ) return queryset
现在我们可以在主题列表中提供更好的导航。 目前唯一的选择是用户单击主题标题并转到第一页。 我们可以锻炼这样的东西:
板/ models.py
import math from django.db import models class Topic ( models . Model ): # ... def __str__ ( self ): return self . subject def get_page_count ( self ): count = self . posts . count () pages = count / 20 return math . ceil ( pages ) def has_many_pages ( self , count = None ): if count is None : count = self . get_page_count () return count > 6 def get_page_range ( self ): count = self . get_page_count () if self . has_many_pages ( count ): return range ( 1 , 5 ) return range ( 1 , count + 1 )
然后在topics.html模板中,我们可以实现以下内容:
模板/ topics.html
<table class= "table table-striped mb-4" > <thead class= "thead-inverse" > <tr> <th> Topic </th> <th> Starter </th> <th> Replies </th> <th> Views </th> <th> Last Update </th> </tr> </thead> <tbody> {% for topic in topics %} {% url 'topic_posts' board.pk topic.pk as topic_url %} <tr> <td> <p class= "mb-0" > <a href= " {{ topic_url }} " > {{ topic.subject }} </a> </p> <small class= "text-muted" > Pages: {% for i in topic.get_page_range %} <a href= " {{ topic_url }} ?page= {{ i }} " > {{ i }} </a> {% endfor %} {% if topic.has_many_pages %} ... <a href= " {{ topic_url }} ?page= {{ topic.get_page_count }} " > Last Page </a> {% endif %} </small> </td> <td class= "align-middle" > {{ topic.starter.username }} </td> <td class= "align-middle" > {{ topic.replies }} </td> <td class= "align-middle" > {{ topic.views }} </td> <td class= "align-middle" > {{ topic.last_updated | naturaltime }} </td> </tr> {% endfor %} </tbody> </table>
就像每个主题的小分页一样。请注意,我还花时间添加table-striped类以更好地设置表格样式。
在回复页面中,我们目前列出了所有主题回复。我们可以将它限制在最后十个帖子中。
板/ models.py
class Topic ( models . Model ): # ... def get_last_ten_posts ( self ): return self . posts . order_by ( '-created_at' )[: 10 ]
模板/ reply_topic.html
{% block content %} <form method= "post" class= "mb-4" novalidate > {% csrf_token %} {% include 'includes/form.html' %} <button type= "submit" class= "btn btn-success" > Post a reply </button> </form> {% for post in topic.get_last_ten_posts %} <!-- here! --> <div class= "card mb-2" > <!-- code suppressed --> </div> {% endfor %} {% endblock %}
另一件事是,当用户回复帖子时,我们会再次将用户重定向到第一页。我们可以通过将用户发送到最后一页来改进它。
我们可以在明信片上添加一个ID:
模板/ topic_posts.html
{% block content %} <div class= "mb-4" > <a href= " {% url 'reply_topic' topic.board.pk topic.pk %} " class= "btn btn-primary" role= "button" > Reply </a> </div> {% for post in posts %} <div id= " {{ post.pk }} " class= "card {% if forloop.last %} mb-4 {% else %} mb-2 {% endif %} {% if forloop.first %} border-dark {% endif %} " > <!-- code suppressed --> </div> {% endfor %} {% include 'includes/pagination.html' %} {% endblock %}
这里重要的是<div id=”{{ post.pk }}” …>。
然后我们可以在视图中像这样玩:
板/ views.py
@login_required def reply_topic ( request , pk , topic_pk ): topic = get_object_or_404 ( Topic , board__pk = pk , pk = topic_pk ) if request . method == 'POST' : form = PostForm ( request . POST ) if form . is_valid (): post = form . save ( commit = False ) post . topic = topic post . created_by = request . user post . save () topic . last_updated = timezone . now () topic . save () topic_url = reverse ( 'topic_posts' , kwargs = { 'pk' : pk , 'topic_pk' : topic_pk }) topic_post_url = '{url}?page={page}#{id}' . format ( url = topic_url , id = post . pk , page = topic . get_page_count () ) return redirect ( topic_post_url ) else : form = PostForm () return render ( request , 'reply_topic.html' , { 'topic' : topic , 'form' : form })
在topic_post_url中,我们使用最后一页构建一个URL,并向id等于帖子ID的元素添加一个锚点。
有了这个,我们需要更新以下测试用例:
板/测试/ test_view_reply_topic.py
class SuccessfulReplyTopicTests ( ReplyTopicTestCase ): # ... def test_redirection ( self ): ''' A valid form submission should redirect the user ''' url = reverse ( 'topic_posts' , kwargs = { 'pk' : self . board . pk , 'topic_pk' : self . topic . pk }) topic_posts_url = '{url}?page=1#2' . format ( url = url ) self . assertRedirects ( self . response , topic_posts_url )
下一个问题,正如您在上一个屏幕截图中看到的那样,当页面数量过高时,可以解决分页问题。
最简单的方法是调整pagination.html模板:
模板/包括/ pagination.html
{% if is_paginated %} <nav aria-label= "Topics pagination" class= "mb-4" > <ul class= "pagination" > {% if page_obj. number > 1 %} <li class= "page-item" > <a class= "page-link" href= "?page=1" > First </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > First </span> </li> {% endif %} {% if page_obj.has_previous %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_obj.previous_page_number }} " > Previous </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Previous </span> </li> {% endif %} {% for page_num in paginator.page_range %} {% if page_obj. number == page_num %} <li class= "page-item active" > <span class= "page-link" > {{ page_num }} <span class= "sr-only" > (current) </span> </span> </li> {% elif page_num > page_obj. number | add : '-3' and page_num < page_obj. number | add : '3' %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_num }} " > {{ page_num }} </a> </li> {% endif %} {% endfor %} {% if page_obj.has_next %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ page_obj.next_page_number }} " > Next </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Next </span> </li> {% endif %} {% if page_obj. number != paginator.num_pages %} <li class= "page-item" > <a class= "page-link" href= "?page= {{ paginator.num_pages }} " > Last </a> </li> {% else %} <li class= "page-item disabled" > <span class= "page-link" > Last </span> </li> {% endif %} </ul> </nav> {% endif %}
结论
通过本教程,我们完成了Django板应用程序的实现。我可能会发布一个后续实现教程来改进代码。我们可以一起探索很多东西。例如,数据库优化,改进用户界面,播放文件上传,创建审核系统等。
下一个教程将侧重于部署。这将是一个关于如何将代码投入生产的完整指南,以处理所有重要细节。
我希望你喜欢本教程系列的第六部分!最后一部分将于2017年10月16日下周发布。如果您希望在最后一部分发布时收到通知,您可以订阅我们的邮件列表。
该项目的源代码可在GitHub上获得。可以在发布标记v0.6-lw下找到项目的当前状态。以下链接将带您到正确的位置:
https://github.com/sibtc/django-beginners-guide/tree/v0.6-lw