博客项目——〇三 个人站点设计
我们在前面一章完成了博客项目主页的设计,然后就要完成个人站点的实现
先看一下个人站点在models文件中对应的数据结构
class Blog(models.Model): """ 博客信息 """ nid = models.AutoField(primary_key=True) title = models.CharField(max_length=64) # 个人博客标题 site = models.CharField(max_length=32, unique=True) # 个人博客后缀 theme = models.CharField(max_length=32) # 博客样式 def __str__(self): return self.title class Meta: verbose_name = "blog站点" verbose_name_plural = verbose_name
还是比较简单的,这样就基本拿到了个人页面的主页所需要所有内容了
对应博客园的个人站点我们看一下
左边是一个左侧的菜单,右边就是所有的个人博客内容。博客的内容我们可以通过url获取到指定的用户名,然后通过ORM获取用户名对应的文章列表,我们可以从url里获得到用户名,分析一下博客园的url:https://www.cnblogs.com/yinsedeyinse/,所以我在url里设置了一个二级路由,用include引导到blog里的二级url
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#主路由 from blog import urls as blog_urls urlpatterns = [ path('admin/', admin.site.urls), url('test/',blog_view.test), url('home/',blog_view.home), url('login',user_manage.login), url('logout/',user_manage.logout), url('signup/$',user_manage.signup), url(r'^blog/',include(blog_urls)), url(r'^media/(?P<path>.*)$',serve,{'document_root':settings.MEDIA_ROOT}) #设置静态文件路由 ] # blog二级路由 from django.conf.urls import url from . import views urlpatterns = [ url(r'(\w+)/$',views.blog), url(r'(?P<username>\w+)/tag/(?P<tag>\w+)',views.tag) ]
这个路由的设定大概了解一下就行了,注意路由携带参数的方法
url(r'(\w+)/$',views.blog) ->def blog(request,username) 或者*args url(r'(?P<username>\w+)',views.blog)->def blog(request,username)
第二种方法在匹配的时候直接就指定了变量名。所以直接通过url里blog/后面的参数直接拿到变量名,然后通过这个变量名拿到blog,要注意的是,这个username并不是我们登录的用户名,要区分开。
这里有个点要注意一下,因为我们在博客园里可以通过直接输入url来访问某个人的博客,但是这个博客站点可能是不存在的,所以要先对这个user进行判定,如果user不存在就返回一个404,存在的话就直接进入站点。
个人站点的样式效果
可以注意下我们在Blog里定义了一个theme字段,就是保存了用户的自定义样式,所以在模板中就可以直接调用
<link rel="stylesheet" href="/media/css/{{blogs.theme}}">
这样就可以直接使用用户自己定义的样式类了。
文章块
整个页面的布局和首页有点像,上面是一个导航栏,然后左侧是一个有各种分类的div,右侧显示了被访问的站点里的文章。右侧的文章显示和主页差不多,下面是整个div的代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!-- 文章列表 开始 --> <div class="article_list"> {% for article in article_list %} <div class="article"> <p class="article-title"><a href="">{{article.title}}</a></p> <div class="media"> <div class="media-left"> <a href="#"> <img class="media-object author-img" src="/media/{{article.user.avatar}}" alt="..."> </a> </div> <div class="media-body"> {{article.desc}} </div> <div class="article-footer"> <span><a href="">{{article.user.username}}</a></span> <span>发布于{{article.create_time|date:'Y-m-d H:i:s'}}</span> <span class="glyphicon glyphicon-comment">评论({{article.comment_count}})</span> <span class="glyphicon glyphicon-thumbs-up">点赞({{article.up_count}})</span> <span class="glyphicon glyphicon-thumbs-down">踩灭({{article.down_count}})</span> </div> </div> </div> {% endfor %} </div> <!-- 文章列表 结束 -->
文章列表的索引也比较简单
user = models.UserInfo.objects.filter(username=username).first()
article_list = models.Article.objects.filter(user = user)
这样直接通过用户名索引就可以了。
文章分类
下面的部分是这一章第一个重要的知识点,在左边的显示部分,我们先放一个按照分类显示的文章内容
这里主要讲一下如何通过ORM索引到需要的数据,看看整个过程是什么样的
1 #查询某个分类对应的文章 2 # 获取用户123 3 user = models.UserInfo.objects.filter(username='123').first() 4 # 获取用户123的blog站点 5 blogs = user.blog 6 7 8 # 获取123的blog里的分类(QuerySet) 9 ret = models.Category.objects.filter(blog=blogs) 10 # print(ret) 11 12 articles = ret[1].article_set.all() #blog分类里第一项下的文章QuerySet对象 13 14 #但是ret是个QuerySet对象,不能用article_set的方法反向查询,下面一行的代码是错误的 15 # ret.annotate(a=Count('article_set')) #错误的方法 16 17 #但是下面的ret1里可以通过表名加双下划线加字段名的方法来获取数据 18 ret1 = ret.annotate(a=Count('article')).values('title','article__title') 19 20 ret2 = ret.annotate(a=Count('article')) 21 for i in ret2: 22 print(i.title,i.a) 23 24 25 #下面的方法和ret2中的方法一样,是用了对QuerySet对象的count方法统计数量 26 for i in ret: 27 print(i,i.article_set.all().count()) 28 29 30 #最终的方法,按照字典的形式输出数据 31 ret = models.Category.objects.filter(blog = blogs).annotate(c=Count('article')).values('title','c')
首先通过用户名拿到用户user(一定要在注意这个用户名是url里的,不是request内注册的user)。
user里有个一对一关联的blog字段,可以通过user.blog获取到用户的blog(blogs)
文章分类表里有个字段是和blog表关联的,可以通过地7行的代码拿到用户123的博客里的分类,下面就是打印的ret的值,注意ret是个QuerySet对象
第12行的articles就是第1个分类里的文章QuerySet对象
在取到分类的数据以后(ret),我们就可以按照分组的方法,并且在分组的统计文章的数量,其实第20行的ret2的方法是最直观的,直接对对象进行_set反向查询,然后获得到一个文章的QuerySet对象,用count的方法统计一下数量。
其实还有另外一种统计的方法:在分组的时候使用了c=Count('字段')方法,新的表里会添加一个新的字段c,数据就是统计的值,然后再用values的方法输出一个字典就行了
就是最后一行里的代码。
由于在模板中,我们可以直接用count的方法进行统计,所以就直接使用了第一种方法。
我们在拿到这个对象以后直接render到模板中就可以了。
<div class="panel-heading"> <h3 class="panel-title">文章分类</h3> </div> <div class="panel-body"> {%for category in category_list%} <p> <a href="">{{category.title}}</a> ({{category.article_set.all.count}})</p> {%endfor%} </div>
对应获取category_list的ORM代码
category_list = models.Category.objects.filter(blog = blogs)
上面就是要替换的模板代码,我们在对象以后,可以直接用统计的方法拿到文章数量,要注意在模板中不用加().最后就能出现下面的效果,(记得在数据库里添加数据)
但是这里还有一个问题,我们还没有对分类的a标签进行赋值,也没有做好url对应的视图
把a标签定义成这样就行了
<p><a href="/blog/{{username}}/tag/{{tag.title}}">{{tag.title}} </a> ({{tag.article_set.all.count}})</p>
我们把分类按钮对应的url定义成下面的样式
<a href="/blog/123/category/音乐/">音乐</a>
然后路由还是需要正则的方式来做
url(r'(\w+)/category/(\w+)/$',views.category_detial),
匹配的第一个参数就是用户名,第二个是分类的字符。
在前面回忆ORM的时候的第12行就是拿到了一个文章分类下的文章列表。我们就需要把这个大概修改一下就可以满足使用了
def category_detial(request,username,category): """ :param username:被访问文章的作者 :param category:分类 """ user = models.UserInfo.objects.filter(username=username).first() blogs = user.blog category_obj = models.Category.objects.filter(blog=blogs,title = category).first() article_list = category_obj.article_set.all() category_list,tag_list,date_list = get_left_menu(username) return render(request,'test.html',{'username':username, 'blogs':blogs, 'article_list':article_list, 'category_list':category_list, 'tag_list':tag_list, 'date_list':date_list })
标签分类
按照标签的分类的方法和上面的思路完全一致,就是通过tab表去去查询blog表,ORM的方法
tag_list = models.Tag.objects.filter(blog=blogs)
HTML里的模板跟前面的按文章分类的方法一样。
有一点要注意的,这里的Tag表是通过一个多对多的关系建立了一个第三个表(因为一篇文章可以有多个标签,一个标签也可以对应多篇文章),而前面的是文章分类是按照一对多的外键来关联的。取数据的时候要注意方法。
时间分类
这里是本章第二个重点
时间分类的时候有一个以前的知识点要注:SQL语句中的时间格式化
我们现在mysql里试一下,新建一个表,表里有三个时间类型的字段
然后随便添加一些数据
这个时候,如果我们想按照固定的格式拿到数据,就要用到sql里的时间格式化的方法
select date_format(dt,'%y-%m') from test;
也就是date_format(字段,'时间格式化字符串')的方法
这样就出来了我们所需要的结果
所以在视图中我们还是要先按照前面的方法对文章进行分类,但是要加上原生SQL语句的执行extra。
date_list = models.Article.objects.filter(user=user).extra( select={"t":"date_format(create_time,'%%Y-%%m')"} #字段不用加引号 ).values('t').annotate(c=Count('nid')).values('t','c')
这样就拿到了按照时间分类的数据列表。要注意的是在进行format的时候时间字符串要用两个百分号来转义一下百分号。
留个悬念
注意一下左侧三个分类列表URL的样式其实是差不多的都是blog/(username)/(分类类型)/分类参数。那么我们就不需要把每个分类都写一个函数,只需要对分类类型的字符串进行判定然后执行相应操作就可以了。具体要怎么操作我们有空了再说!
我们可以注意一下博客园里的页面,在个人blog站点里和进入文章详情的页面和进入分类的页面都差不多,就是中间显示的内容有些区别。为了代码的简洁,我们可以做一个基础的模板用来显示
就像上面的图里所示,我们在每个页面中只需要把红色框框里的内容按要求显示出来就行了,红框外面的部分都是固定的。
先写一个模板
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/css/blog.css"> <link rel="stylesheet" href="/media/css/{{blogs.theme}}"> <title>BLOG主页</title> </head> <body> <div class="header header-custom"> {{blogs.title}} </div> <!-- 主体开始 --> <div class="container-fluid"> <div class="row"> <div class="col-md-2"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">文章分类</h3> </div> <div class="panel-body"> {%for category in category_list%} <p> <a href="">{{category.title}}</a> ({{category.article_set.all.count}})</p> {%endfor%} </div> </div> <!-- 标签分类 开始 --> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">标签分类</h3> </div> <div class="panel-body"> {%for tag in tag_list%} <p><a href="/blog/{{username}}/tag/{{tag.title}}">{{tag.title}} </a> ({{tag.article_set.all.count}})</p> {%endfor%} </div> </div> <!-- 标签分类 结束 --> <!-- 时间分类 开始 --> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">日期归档</h3> </div> <div class="panel-body"> {%for tag in date_list%} <p><a href="">{{tag.t}} </a> ({{tag.c}})</p> {%endfor%} </div> </div> <!-- 时间分类 结束 --> </div> <div class="col-md-8"> <!-- 文章列表 开始 --> {% block page-main %} {%endblock%} <!-- 文章列表 结束 --> </div> <div class="col-md-2"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> </div> </div> </div> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> </body> </html>
然后把需要替换的地方(blog)留出来并且起好名字
<!-- 文章列表 开始 --> {% block page-main %} {%endblock%} <!-- 文章列表 结束 -->
然后我们在视图里render的对象不是blog.html么,就把这个HTML文件写成下面这样就行了
{%extends 'blog_base.html'%} {%block page-main%} <div class="article_list"> {% for article in article_list %} <div class="article"> <p class="article-title"><a href="/blog/{{username}}/article/{{article.pk}}">{{article.title}}</a></p> <div class="media"> <div class="media-left"> <a href="#"> <img class="media-object author-img" src="/media/{{article.user.avatar}}" alt="..."> </a> </div> <div class="media-body"> {{article.desc}} </div> <div class="article-footer"> <span><a href="">{{article.user.username}}</a></span> <span>发布于{{article.create_time|date:'Y-m-d H:i:s'}}</span> <span class="glyphicon glyphicon-comment">评论({{article.comment_count}})</span> <span class="glyphicon glyphicon-thumbs-up">点赞({{article.up_count}})</span> <span class="glyphicon glyphicon-thumbs-down">踩灭({{article.down_count}})</span> </div> </div> </div> {% endfor %} </div> {%endblock%}
先通过extends继承母版的代码(blog_base.html),然后写出要替换的block。那么下面几章里其他的页面只需要写这个小HTML文件就行了。
就像上面讲的,假如我们有下面几个页面:个人站点、tag分类详情页、时间分类详情页、文章分类详情页、文章页面。每个页面都对应一个视图函数,并且一个被访问的用户的左侧的各个分页的详情都是一样的,所以我们需要赋值粘贴好多重复的代码。显然不符合平时编程的思路。那么就有下面两种方式来解决
函数的形式
这个没啥讲的,其实就是把获得到那几个列表的代码段放到一个函数中。每次调用这个函数就可以了。以上面讲的blog页面为例
def get_left_menu(username): user = models.UserInfo.objects.filter(username=username).first() blogs = user.blog #博客分类 category_list = models.Category.objects.filter(blog = blogs) #标签分类 tag_list = models.Tag.objects.filter(blog=blogs) #日期归档 date_list = models.Article.objects.filter(user=user).extra( select={"t":"date_format(create_time,'%%Y-%%m')"} #字段不用加引号 ).values('t').annotate(c=Count('nid')).values('t','c') return category_list,tag_list,date_list
我们先定义一个函数get_left_menu,然后在视图函数中调用这个函数就行了
def blog(request,username): user = models.UserInfo.objects.filter(username=username).first() if not user: return HttpResponse('用户'+username+'不存在') else: blogs = user.blog # 文章列表 article_list = models.Article.objects.filter(user = user) category_list,tag_list,date_list = get_left_menu(username) return render(request,'blog.html',{ "username":username, 'blogs':blogs, 'article_list':article_list, 'category_list':category_list, 'tag_list':tag_list, 'date_list':date_list })
是不是简单明了。
inclution_tag的使用
在前面讲Django模板的时候我们讲过inclution_tag的使用,我们可以用inclution_tag来直接生成一段html代码然后把这段代码套到母版。
创建包
先创建一个package(包含__init__.py的文件夹),名字就叫templatetags,路径在app下。
创建inclution_tag文件
在新建的包下创建一个文件(文件名就叫my_tags.py),里面主要用来放置刚才的get_left_menu函数
1 from django import template 2 3 register = template.Library() 4 5 from blog import models 6 from django.db.models import Count 7 8 9 10 @register.inclusion_tag('left_menu.html') 11 def get_left_menu(username): 12 user = models.UserInfo.objects.filter(username=username).first() 13 blogs = user.blog 14 #博客分类 15 category_list = models.Category.objects.filter(blog = blogs) 16 #标签分类 17 tag_list = models.Tag.objects.filter(blog=blogs) 18 19 #日期归档 20 date_list = models.Article.objects.filter(user=user).extra( 21 select={"t":"date_format(create_time,'%%Y-%%m')"} #字段不用加引号 22 ).values('t').annotate(c=Count('nid')).values('t','c') 23 24 return {'category_list':category_list, 25 'tag_list':tag_list, 26 'date_list':tag_list 27 }
修改母版文件
母版很简单,就是要调用这个自定义的tag
<!-- 左侧列表开始 --> <div class="col-md-2"> {%load my_tags%} {%get_left_menu username%} </div> <!-- 左侧列表结束 -->
就是把生成的html代码直接套到母版里的这一块
要替换的html代码
因为我们要生成一段html代码,这段代码是由一个模板生成的,这个模板里需要渲染的部分就是通过前面tag里的函数前面的装饰器送来的函数的返回值,也就是三个列表(要注意的是这里三个列表要以字典的形式返回)
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">文章分类</h3> </div> <div class="panel-body"> {%for category in category_list%} <p> <a href="">{{category.title}}</a> ({{category.article_set.all.count}})</p> {%endfor%} </div> </div> <!-- 标签分类 开始 --> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">标签分类</h3> </div> <div class="panel-body"> {%for tag in tag_list%} <p><a href="/blog/{{username}}/tag/{{tag.title}}">{{tag.title}} </a> ({{tag.article_set.all.count}})</p> {%endfor%} </div> </div> <!-- 标签分类 结束 --> <!-- 时间分类 开始 --> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">日期归档</h3> </div> <div class="panel-body"> {%for tag in date_list%} <p><a href="">{{tag.t}} </a> ({{tag.c}})</p> {%endfor%} </div> </div> <!-- 时间分类 结束 -->
也就是把base_blog里删除的代码放到这个left_menu.html文件里(注意这个文件名,就是装饰器里指定的这个文件)。
结束!
大概顺序是这样的