首页的设计我们就按照简化的博客园的效果来搞!但是利用了Bootstrap的框架。大概思路是这样的
最上面是导航条,然后下面是左中右的方式按照2-8-2的布局来分。
导航条我们就用Bootstrap里给的基础的样式就可以,
注意右边的a标签,如果当前状态是已经登录的话就是上面的样式,如果没有登录,就是一个登录和一个注册。
<!-- 导航条开始 --> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">博客主页</a> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li> <li><a href="#">Link</a></li> </ul> <!--右侧链接按钮 --> <ul class="nav navbar-nav navbar-right"> {% if request.user.username%} <li><a href="">{{request.user.username}}</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">用户中心 <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="/blog/{{request.user.username}}">我的博客</a></li> <li><a href="#">短消息</a></li> <li><a href="#">设置</a></li> <li><a href="/logout/">注销</a></li> </ul> </li> {%else%} <li><a href="/login/">登录</a></li> <li><a href="/signup/">注册</a></li> {%endif%} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <!-- 导航条结束 -->
注意里面的判断的结构。对当前的登录状态进行用户校验。
最上面的那个图中,我们就按照2-8-2的布局进行排列,结构就像下面的代码所示
<!-- 主体开始 --> <div class="container-fluid"> <div class="row"> <div class="col-md-2 left-body"></div> <div class="col-md-8 main-body"></div> <div class="col-md-2 right-body"></div> </div> </div> <!-- 主体结束 -->
左右侧的部分我们可以用来显示一些辅助内容,例如广告、分类什么的。下面就是这一节最主要的部分,模板的填充。
和博客园一样,在主页里我们用来显示所有的文章 。
这里先把main-body里的html代码放出来,我们一步步来分析
1 <!-- 文章列表 开始 --> 2 <div class="article_list"> 3 {% for article in article_list %} 4 <div class="article"> 5 <h3><a href="">{{article.title}}</a></h3> 6 <div class="media"> 7 <div class="media-left"> 8 <a href="#"> 9 <img class="media-object author-img" src="/media/{{article.user.avatar}}" alt="..."> 10 </a> 11 </div> 12 <div class="media-body"> 13 {{article.desc}} 14 </div> 15 <div class="article-footer"> 16 <span><a href="">{{article.user.username}}</a></span> 17 <span>发布于{{article.create_time|date:'Y-m-d H:i:s'}}</span> 18 <span class="glyphicon glyphicon-comment">评论({{article.comment_count}})</span> 19 <span class="glyphicon glyphicon-thumbs-up">点赞({{article.up_count}})</span> 20 <span class="glyphicon glyphicon-thumbs-down">踩灭({{article.down_count}})</span> 21 </div> 22 </div> 23 </div> 24 {% endfor %} 25 </div> 26 <!-- 文章列表 结束 -->
这个模板对应的视图很简单,只有一行代码就行了
def home(request): article_list = models.Article.objects.all()return render(request,'home.html',{'article_list':article_list})
也就是说整个过程就是我们在视图中通过ORM索引到所有的文章列表,然后把这个列表render给主页的模板。先看一下显示出来的效果
上面的图就是通过模板渲染出来的效果。下面对几个模板使用的知识点分析一下。
模板使用
这里使用了Bootstrap组件的媒体对象的默认样式,左边一个头像,右边放p标签或者span标签来显示相应内容。
就是在上面的列表中通过for循环生成的代码段。
<div class="media"> <div class="media-left"> <a href="#"> <img class="media-object" src="..." alt="..."> </a> </div> <div class="media-body"> <h4 class="media-heading">Media heading</h4> ... </div> </div>
我们只需要显示出来文章中下面几条内容就可以了
看一下models里对于文章Article是怎么定义的
class Article(models.Model): """ 文章 """ nid = models.AutoField(primary_key=True) title = models.CharField(max_length=50, verbose_name="文章标题") # 文章标题 desc = models.CharField(max_length=255) # 文章描述 create_time = models.DateTimeField() # 创建时间 --> datetime() # 评论数 comment_count = models.IntegerField(verbose_name="评论数", default=0) # 点赞数 up_count = models.IntegerField(verbose_name="点赞数", default=0) # 踩灭数量 down_count = models.IntegerField(verbose_name="踩数", default=0) category = models.ForeignKey(to="Category", to_field="nid", null=True,on_delete=models.CASCADE) user = models.ForeignKey(to="UserInfo", to_field="nid",on_delete=models.CASCADE) tags = models.ManyToManyField( # 中介模型 to="Tag", through="Article2Tag", through_fields=("article", "tag"), # 注意顺序!!! ) def __str__(self): return self.title class Meta: verbose_name = "文章" verbose_name_plural = verbose_name
这里就要我们回顾一下ORM里的两种数据查询方法了:
基于对象的查询和基于QuerySet的查询
看一下下面的代码段,我们回顾一下前面的ORM的查询方式
# ORM基于对象的查询和基于QuerySet的查询方法 import os if __name__ =='__main__': os.environ.setdefault("DJANGO_SETTINGS_MODULE", "BLOG.settings") import django django.setup() from blog import models #基于对象的查询:SQL里的子查询(select里嵌套select) ret = models.Article.objects.first() print(ret.user,type(ret.user)) #123 <class 'blog.models.UserInfo'> # ret.user是一个对象,就可以直接用对象+.的形式来获取我们需要的内容,像下面一样 print(ret.user.username) print(ret.user.avatar) #基于QuerySet的方式查询:SQL里的join查询 ret2 = models.Article.objects.filter(nid = 1) print(ret2,type(ret2)) # <QuerySet [<Article: 博客项目——〇一需求分析(占位)>]> <class 'django.db.models.query.QuerySet'> avatar = ret2.values('user__avatar') print(avatar)
为了便于我们在模板中获取相应的数据,先通过all()方法获取到一个QuerySet对象(article_list),在通过render函数的时候,我们用for循环把这个拿到具体的article对象,就可以用基于对象的方式获取到所需要的内容
标题
{{article.title}}
简介
{{article.desc}}
头像
{{article.user.avatar}}
头像图片的获取
如果仅仅通过下面的代码是显示不了头像的
<img class="media-object author-img" src="{{article.user.avatar}}" alt="...">
这样图片是无法打开的
检查一下页面上的img标签属性,能发现图片的url是没问题的:http://127.0.0.1:8000/home/avatar/img/hmbb.png,而所有用户上传的头像都放在了根目录下的avatar文件夹中。把这个url复制到地址栏中,f直接访问一下是没有任何效果的
但是如果把这个文件放在static文件夹里,是可以通过url直接访问的(截图太大就不掩饰了,放一个访问css文件的截图)。
因为我们在settings.py里指定了静态文件的路径和URL
STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR,'static') ]
如果把这个注释掉,就没办法访问了。这样就流出了可以通过URL访问的一些文件的接口,没有暴露的是无法访问的。而静态文件是每次浏览器都要去拿到然后渲染出效果的。这样就保护了我们的代码文件。
在这里我们就做另外一个路径:在Django里,所有用户上传的文件都叫做media,所以我们需要把在settings.py中对media的属性进行设置
#Django 用户上传的文件都叫media文件 MEDIA_URL = '/media/' #media配置,用户上传的文件都放在这个文件夹下 MEDIA_ROOT = os.path.join(BASE_DIR,'media')
然后新建一个文件夹,把原先的avatar这个文件夹拖进去
这样就完成了第一步,访问一下这个路径http://127.0.0.1:8000/media/avatars/hmbb.png,发现还是404报错,因为我们这个media的相关路由还没有设置,必须要新指定(STATIC不用手动指定路由是因为STATIC里的静态文件基本上是浏览器在渲染页面的时候必须用的,所有在Django内部就直接设定好了)
记得要先导入serve模块,下面的语法直接记住就行了。
from django.views.static import serve from django.conf import settings url(r'^media/(?P<path>.*)$',serve,{'document_root':settings.MEDIA_ROOT}) #设置静态文件路由
发布时间的显示
发布时间我们用了一个模板中的tag,前面应该有的
{{article.create_time|date:'Y-m-d H:i:s'}}
最后通过一个css文件指定了一下margin-left,避免所有标签挤在一起不美观
最后把整个html文件放下面
<!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/home.css"> <title>BLOG主页</title> </head> <body> <!-- 导航条开始 --> <nav class="navbar navbar-default"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">博客主页</a> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li> <li><a href="#">Link</a></li> </ul> <!--右侧链接按钮 --> <ul class="nav navbar-nav navbar-right"> {% if request.user.username%} <li><a href="">{{request.user.username}}</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">用户中心 <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="/blog/{{request.user.username}}">我的博客</a></li> <li><a href="#">短消息</a></li> <li><a href="#">设置</a></li> <li><a href="/logout/">注销</a></li> </ul> </li> {%else%} <li><a href="/login/">登录</a></li> <li><a href="/signup/">注册</a></li> {%endif%} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <!-- 导航条结束 --> <!-- 主体开始 --> <div class="container-fluid"> <div class="row"> <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 class="col-md-8"> <!-- 文章列表 开始 --> <div class="article_list"> {% for article in article_list %} <div class="article"> <h3><a href="">{{article.title}}</a></h3> <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> <!-- 文章列表 结束 --> </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>
在前面讲数据可视性操作的时候讲过了怎么添加数据,因为整个数据结构互相牵扯的比较多,简易还是从admin页面里添加新的数据,但是要记得修改admin文件,把所有的model都注册了
from django.contrib import admin from blog import models # Register your models here. admin.site.register(models.UserInfo) admin.site.register(models.Article) admin.site.register(models.Blog) admin.site.register(models.Tag) admin.site.register(models.Category) admin.site.register(models.Comment) admin.site.register(models.ArticleUpDown) admin.site.register(models.ArticleDetail) admin.site.register(models.Article2Tag)
对Article新加几条数据,就能在主页上显示出来
由于在导航栏里有用户管理功能,用户的注册和登录方法我们在前面已经讲过了,这里就把代码直接放出来了,不在过多解释。
FORM组件
在注册的页面里我只用了一部分form组件,但是还是把一些以后可能会用到的字段放出来了,便于以后的使用,登录的页面未使用这个form组件
""" 用到的form类 """ from django import forms from django.core.exceptions import ValidationError from . import models class RegForm(forms.Form): username = forms.CharField( max_length=16, label='用户名', error_messages={ 'max_length': '用户名最长为16位!', 'required': '用户名不能为空!' }, widget=forms.widgets.TextInput( attrs={ 'class': 'form-control' } ) ) password = forms.CharField( min_length=6, label='密 码', widget=forms.widgets.PasswordInput( attrs={ 'class': 'form-control' } ), error_messages={ 'min_length': '密码不能少于6位!', 'required': '密码不能为空!' } ) re_password = forms.CharField( max_length=32, label='确认密码', widget=forms.widgets.PasswordInput( attrs={ 'class': 'form-control' } ), error_messages={ 'max_length': '用户名最长为16位!', 'required': '用户名不能为空!' } ) email = forms.EmailField( label='邮箱', widget=forms.widgets.EmailInput( attrs={ 'class': 'form-control' } ), error_messages={ 'invalid': '邮箱格式不正确', 'required': '邮箱不能为空' } ) # #重构局部的钩子对username字段进行校验 def clean_username(self): username = self.cleaned_data.get('username') user = models.UserInfo.objects.filter(username=username) if user: self.add_error('username',ValidationError('用户名已存在!')) return username #一定记得加这个返回值,否则在视图中cleaned_data里返回值为空 #重写全局钩子,对确认密码进行校验 def clean(self): username = self.cleaned_data.get('username') pwd = self.cleaned_data.get('password') re_pwd = self.cleaned_data.get('re_password') if re_pwd and re_pwd != pwd: self.add_error('re_password',ValidationError("两次密码不一致")) else: return self.cleaned_data
form里的局部钩子一定一定要记得加返回值,在调试的过程中忘记加返回值了,拿到cleaned_data的时候一直显示username为null,搞了快一天才高清是怎么回事。
用户管理视图
我把所有用户管理方面的视图都放在了一个py文件里,便于后期的维护
# user_manage.py #用户管理视图 from django.shortcuts import render,redirect,HttpResponse from django.contrib import auth from django.http import JsonResponse from . import models,forms from django.views import View #注册 def signup(request): if request.method == 'POST': print(request.POST) ret = {'status':0,'msg':''} reg_FORM_obj = forms.RegForm(request.POST) if reg_FORM_obj.is_valid(): reg_FORM_obj.cleaned_data.pop('re_password') avatar_img = request.FILES.get('avatar') models.UserInfo.objects.create_user(**reg_FORM_obj.cleaned_data,avatar=avatar_img) ret['msg'] = '/home/' else: ret['status'] =1 ret['msg'] = reg_FORM_obj.errors return JsonResponse(ret) user_form = forms.RegForm() return render(request,'signup.html',{'reg_form':user_form}) #校验用户名是否存在 def name_check(request): ret = {} if request.method == 'POST': username = request.POST.get('username') user = models.UserInfo.objects.filter(username = username) if user: ret['state'] = 1 ret['error'] = '用户名已存在' return JsonResponse(ret) #登录 def login(request): message={} if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') if models.UserInfo.objects.filter(username = username): user = auth.authenticate(username=username,password=password) if user: auth.login(request,user) return redirect('/home/') else: message['error'] = '密码错误' else: message['error'] = '用户名不存在' return render(request,'login.html',{'message':message}) #注销 def logout(request): auth.logout(request) return redirect('/home/')
对应的用户管理的路由没什么要注意的,但是注册('/signup/')和用户名校验('/signup/namechk')
模板文件
用户的注册是通过了FORM组件完成的,但是登录功能因为只有两个input标签就直接用form标签完成了。数据的提交都是通过AJAX完成。
用户注册的模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>用户注册</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/mystyle.css"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form action="{{RegURL}}" method="POST" novalidate class="form-horizontal reg-form" enctype="multipart/form-data"> {% csrf_token %} <div class="form-group"><h3>用户注册</h3></div> <div class="form-group"> <label for="{{reg_form.username.id_for_label}}" class="col-sm-2 control-label">{{reg_form.username.label}}</label> <div class="col-sm-10"> {{reg_form.username}} <span class="help-block">{{reg_form.username.errors.0}}</span> </div> </div> <div class="form-group"> <label for="{{reg_form.password.id_for_label}}" class="col-sm-2 control-label">{{reg_form.password.label}}</label> <div class="col-sm-10"> {{reg_form.password}} <span class="help-block">{{reg_form.password.errors.0}}</span> </div> </div> <div class="form-group"> <label for="{{reg_form.re_password.id_for_label}}" class="col-sm-2 control-label">{{reg_form.re_password.label}}</label> <div class="col-sm-10"> {{reg_form.re_password}} <span class="help-block">{{reg_form.re_password.errors.0}}</span> </div> </div> <div class="form-group"> <label for="{{reg_form.email.id_for_label}}" class="col-sm-2 control-label">{{reg_form.email.label}}</label> <div class="col-sm-10"> {{reg_form.email}} <span class="help-block"></span> </div> </div> <div class="form-group"> <label for="" class="col-sm-2 control-label">头像</label> <div class="col-sm-10"> <label for="id_avatar"><img src="/static/img/default.png" alt="" id="avatar-img"></label> <input type="file" name="avatar" id="id_avatar" style="display: none;"> </div> </div> </form> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-success" id="btn">AJAX提交</button> </div> </div> </div> </div> </div> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.js"></script> <script> // 上传图像事件 $('#id_avatar').change(function(){ var filePath = this.files[0]; var fileReader = new FileReader(); fileReader.readAsDataURL(filePath); fileReader.onload = function(){ $('#avatar-img').attr('src',fileReader.result); }; }); // 登录按钮事件 $('#btn').click(function(){ //点击按钮 ,触发AJAX请求事件 var formData = new FormData() formData.append('username',$('#id_username').val()); formData.append('password',$('#id_password').val()); formData.append('re_password',$('#id_re_password').val()); formData.append('email',$('#id_email').val()); formData.append('avatar',$('#id_avatar')[0].files[0]); formData.append('csrfmiddlewaretoken','{{csrf_token}}'); $.ajax({ url:'/signup/', type:'post', processData:false, contentType:false, data:formData, success:function(data){ console.log(data); if (data.status){ //有错误状态,错误为data.msg //将错误填写至标签内 $.each(data.msg,function(k,v){ $('#id_'+k).next('span').text(v[0]) }) } else{//没有错误跳转到指定页面 location.href = data.msg; } } }) }) //将所有的input框绑定的获取焦点信息 $('input').focus(function(){ $(this).next('span').text(''); }) </script> </body> </html>
还有用户登录的模板
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" href="../../favicon.ico"> <title>Signin Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <!-- Custom styles for this template --> <link href="/static/css/login.css" rel="stylesheet"> </head> <body> <div class="container"> <form class="form-signin" method="POST" action="/login/"> {%csrf_token%} <h2 class="form-signin-heading">请登录</h2> <label for="inputEmail" class="sr-only">用户名</label> <input type="text" id="inputusername" class="form-control" name="username" placeholder="用户名" required autofocus> <label for="inputPassword" class="sr-only">Password</label> <input type="password" id="inputPassword" class="form-control" name="password" placeholder="Password" required> <div class="checkbox"> <label> <input type="checkbox" value="remember-me"> Remember me </label> </div> <div class="error_message">{{message.error}}</div> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> </form> </div> <!-- /container --> </body> </html>
但是本章节最后的三个html文件(home,login和signup)还有许多细节需要完善,比如有些a标签的链接地址都还没有定义,图片也没有做只是预留了个位置,在后面我会慢慢补齐!