django项目(博客二)
扩展1:admin路由分发的本质
路由分发本质 include 可以无限制的 嵌套N多层
url(r'^index/',([],None,None))
扩展2:
由于url方法第一个参数是正则表达式,所有当路由特别多的时候,可能会出现被顶替的情况,针对这种情况有两种解决方式
建好路由,先和视图函数继续试验一下,避免路由被顶替
1.修改正则表达式
2.调整url方法的位置,越往上,url的优先级越高
扩展3:
时区问题报错,setting.py文件里设置,修改时区
TIME_ZONE = 'Asia/Shanghai' USE_TZ = False
一 文章详情页
''' 文章详情页和个人站点基本一致,所以用模版继承 个人站点和文章详情的导航条和左侧边栏一样,这该两个html页面就继承base.html,个人站点页面和文章详情页面的内容区,写自己的内容, 个人站点页面和文章详情页面的左侧栏页面,需要向视图函数要同样的数据,解决代码冗余,就用到自定义inclusion_tags 侧边栏的渲染需要传输数据才能渲染,并且该侧边栏在很多页面都需要使用 1. 在那个地方用就拷贝需要的代码(不推荐 有点繁琐) 2. 将侧边栏制作成inclusion_tag,作用就是,不同的页面,需要共同的视图函数数据,就自定义inclusion_tag,不同的页面,都向inclusion要数据,解决代码冗余的问题 '''
# url设计 https://www.cnblogs.com/xiaoyuanqujing/articles/12208376.html /username/article/1 # 先验证url是否被其他url顶替,排查BUG # 文章详情页 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/', views.article_detail)
# 文章详情页视图函数 def article_detail(request, username, article_id): ''' 可以先进行用户名校验是否存在 :param request: :param username: :param article_id: :return: ''' # 筛选出文章对象,显示对应的用户访问对应的文章,文章查个人站点,正向,个人查用户,反向,跨表用__ article_obj = models.Article.objects.filter(id=article_id, blog__userinfo__username=username).first() if not article_obj: # return redirect('/errors/') # 从定向,需要走url路由,没设置路由,走不通 return render(request, 'errors.html') # 导航条需要个人站点名,就从数据库取给前端 user_obj = models.UserInfo.objects.filter(username=username).first() blog = user_obj.blog # 获取当前文章所有的评论内容 comment_list = models.Comment.objects.filter(article=article_obj) return render(request, 'article_detail.html', locals())
base.html
个人站点和文章详情页都从base中模版继承,左侧边栏需要向不同 的视图函数要同样的数据,
用到自定义inclusion_tag
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="referrer" content="no-referrer" /> <title>{{ blog }}的个人站点</title> <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css"> <script src="/static/js/jQuery-3.6.1.js"></script> <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script> <script src="/static/js/my_setup.js"></script> {% block css %} {% endblock %} </head> <body> <!--个人站点导航条--> <div class="container-fluid"> <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="#">{{ blog.site_title }}</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <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="#">test</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> <li role="separator" class="divider"></li> <li><a href="#">One more separated link</a></li> </ul> </li> </ul> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" placeholder="Search"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> <ul class="nav navbar-nav navbar-right"> {% if request.user.is_authenticated %} <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="#" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li> <li><a href="#">修改头像</a></li> <li><a href="#">后台管理</a></li> <li role="separator" class="divider"></li> <li><a href="{% url 'logout' %}" id="logout">退出登录</a></li> </ul> <!-- Large modal --> <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <h3 class="text-center">修改密码</h3> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="form-group"> <label for="">用户名</label> <input type="text" disabled value="{{ request.user.username }}" class="form-control"> </div> <div class="form-group"> <label for="old_password">原密码</label> <input type="password" id="old_password" class="form-control"> <span style="color: red" class="pull-right"></span> </div> <div class="form-group"> <label for="">新密码</label> <input type="password" id="new_password" class="form-control"> </div> <div class="form-group"> <label for="">确认密码</label> <input type="password" id="confirm_password" class="form-control"> <span style="color: red" class="pull-right"></span> </div> <div class="form-group"> <button type="button" class="btn btn-default btn-sm" data-dismiss="modal"> 取消 </button> <button type="button" class="btn btn-primary btn-sm" id="set_commit"> 确认修改 </button> </div> </div> </div> </div> </div> </div> </li> {% else %} <li><a href="{% url 'reg' %}">注册</a></li> <li><a href="{% url 'log' %}">登录</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> </div> <!--主板 3,9比例--> <div class="container-fluid"> <div class="row"> <div class="col-md-3"> <!--侧边栏调用mytags.py--> {% load mytags %} <!--调用mytags里面的left_menu函数--> {% left_menu username %} </div> <!--主板内容--> <div class="col-md-9"> {% block content %} {% endblock %} </div> </div> </div> {% block js %} {% endblock %} </body> </html>
mytags.py
在应用aap01文件下,建mytags.py
''' 步骤: 1.在应用下创建一个名字必须叫templatetags文件夹 2.在该文件夹内创建一个任意名称的py文件 3.在该py文件内先固定写两行代码 from django import template register = template.Library() 自定义过滤器 自定义标签 自定义inclusion_tag ''' from django import template register = template.Library() # 自定义inclusion_tag @register.inclusion_tag('left_menu.html') def left_menu(username): # 构造侧边栏需要的数据 from app01 import models from django.db.models import Count # 先从site视图函数,剪切过来 ,需要什么参数,就给什么参数 user_obj = models.UserInfo.objects.filter(username=username).first() blog = user_obj.blog # 1 查询当前用户所有的分类及分类下的文章数 category_queryset = models.Category.objects.filter(blog=blog).annotate( count_num=Count('article__pk')).values_list('name', 'count_num', 'pk') # pk是分组后新的虚拟结果的主键值,也就是queryset的索引值 # values是列表套字典,values_list是列表套元祖 # 2 查询当前用户所有的标签及标签下的文章数 tag_queryset = models.Tag.objects.filter(blog=blog).annotate( count_num=Count('article__pk')).values_list('name', 'count_num', 'pk') # 3 按照年月统计所有的文章 from django.db.models.functions import TruncMonth # 第一个.annotate是按文章进行分组,第二个.annotate是跟在values('month'),month的虚拟表基础上在进行分组,计数取值 article_month = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values( 'month').annotate( num=Count('pk') ).values_list('month', 'num') # print(article_month) return locals()
left_menu.html
自定义inclusio_tag中的左侧边栏页面
div class="panel panel-info"> <div class="panel-heading text-center">文章分类</div> <div class="panel-body"> {% for category in category_queryset %} <p><a href="/{{ username }}/category/{{ category.2 }}">{{ category.0 }}({{ category.1 }})</a></p> {% endfor %} </div> </div> <div class="panel panel-danger"> <div class="panel-heading text-center">文章标签</div> <div class="panel-body"> {% for tag in tag_queryset %} <a href="/{{ username }}/tag/{{ tag.2 }}"><p>{{ tag.0 }}<{{ tag.1 }}></p></a> {% endfor %} </div> </div> <div class="panel panel-success"> <div class="panel-heading text-center">日期归档</div> <div class="panel-body"> {% for month in article_month %} <p><a href="/{{ username }}/archive/{{ month.0|date:'Y-m' }}">{{ month.0|date:'Y年m月' }}({{ month.1 }})</a> </p> {% endfor %} </div> </div>
article_detail.html
文章详情页
{% extends 'base.html' %} {% block content %} <div class="container"> <h1>{{ article_obj.title }}</h1> <div> {{ article_obj.content }} </div> </div> {% endblock %}
site.html
通过模版继承后的个人站点页面
{% extends 'base.html' %} {% block content %} <ul class="media-list"> {% for article_obj in article_list %} <li class="media"> <h3 class="media-heading"><a href="/{{ username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h3> <div class="media-left"> <a href="#"> <img class="media-object" src="/media/{{ article_obj.blog.userinfo.avatar }}" alt="..." width="50px" height="60px"> </a> </div> <div class="media-body"> {{ article_obj.desc }} </div> <div class="pull-right"> <span>posted</span> <span>@</span> <span>{{ article_obj.create_time|date:'Y-m-d' }} </span> <span><a href="#">{{ username }}</a> </span> <span><span class="glyphicon glyphicon-comment"></span>评论数({{ article_obj.comment_num }}) </span> <span><span class="glyphicon glyphicon-thumbs-up"></span>点赞数({{ article_obj.up_num }}) </span> <span><a href="#">编辑</a></span> </div> </li> <hr> <!--分割线--> {% endfor %} </ul> {% endblock %}
二 文章点赞点踩
文章点赞点踩功能是在,文章详情页面
''' 浏览器上你看到的花里胡哨的页面,内部都是HTML(前端)代码 在文章内容应该写什么? --->html代码 如何拷贝文章 copy outerhtml 解决加载不了图片,用到图片防盗链技术 <meta name="referrer" content="no-referrer" /> 1.拷贝文章 2.拷贝点赞点踩 1) 拷贝前端点赞点踩图片,只拷贝html 2) css也要拷贝 由于有图片防盗链的问题,所以将图片直接下载到本地 课下思考: 前端如何区分用户是点了赞还是点了踩 1.给标签各自绑定一个事件 两个标签对应的代码其实基本一样,仅仅是否点赞点踩这一个参数不一样而已 2.二合一 给两个标签绑定一个事件 // 给所有的action类绑定事件 $('.action').click(function () { alert($(this).hasClass('diggit')) }) 3.书写ajax代码朝后端提交数 由于点赞点踩内部有一定的业务逻辑,所以后端单独开设视图函数 建议在写项目的时候吗,先把业务逻辑写完,在进行修正 4.后端逻辑书写完毕之后,前端针对点赞点踩动作实现需要动态展示提示信息 1)前端点赞点踩数字自增1 需要注意数据类型的问题 2)用户没有登录 需要展示没有登录提示 并且登录可以点击跳转 html() |safe mark_safe() 后端向前端发送html代码 ''' ''' 后端逻辑(先写成功逻辑,再写错误逻辑 1.先判断用户是否登录 request.user.authenticated() 2.再判断当前文字是否是当前用户自己写的 通过文章主键值获取文章对象 之后利用orm查询获取文章对象对应的用户对象与request.user比对 3.判断当前用户是否已经给当前文章点了 利用article_obj文章对象和request.user用户对象去点赞点踩表中筛选数据如果 有数据则点没哟 4.操作数据库,需要注意要同时操作两张表 前端发送过来的是否点赞是一个字符串 需要你自己转成布尔值或者用字符串判断 ''' ''' 总结:在书写较为复杂的业务逻辑的时候,可以先按照一条线写下去 之后再去弥补其他线路情况 类似于先走树的主干 之后再分散 '''
# 点赞点踩 url(r'^up_or_down/', views.up_or_down),
# 点赞点踩视图函数 def up_or_down(request): ''' 1.校验用户是否登录 2.判断当前文章是否是当前用户自己写的(自己不能点自己的文章) 3.当前用户是否已经给当前文章点过了 4.操作数据库 :param request: :return: ''' if request.is_ajax(): back_dic = {'code': 1000, 'msg': ''} # 1.先判断用户是否登录 if request.user.is_authenticated(): article_id = request.POST.get('article_id') print(article_id, type(article_id)) is_up = request.POST.get('is_up') import json # 前端发送过来的,是json格式的,需要转成python格式的bool值 is_up = json.loads(is_up) print(is_up, type(is_up)) # 2.判断当前文章是否是当前用户自己写的 根据文章id查询文章对象,根据文章对象查作者 跟request.user.username比对 article_obj = models.Article.objects.filter(pk=article_id).first() print(article_obj.blog.userinfo.username, type(article_obj.blog.userinfo.username)) print(request.user, type(request.user)) if not article_obj.blog.userinfo.username == request.user.username: # 3. 校验当前用户是否已经点了 那个地方记录了用户到底点没点 is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj) if not is_click: # 4 操作数据库 记录数据 要同步操作普通字段 # 判断当前用户点了赞还是踩 从而决定给那个字段加一 from django.db.models import F # F新的虚拟字段和原有字段进行比对 if is_up: # 给点赞数加一 models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1) back_dic['msg'] = '点赞成功' else: # 给点踩数加一 models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1) back_dic['msg'] = '点踩成功' # 操作点赞点踩表,记录数据 models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up) else: back_dic['code'] = 2000 back_dic['msg'] = '你已点过了,不能在点' else: back_dic['code'] = 3000 back_dic['msg'] = '不能自己点自己' else: back_dic['code'] = 4000 back_dic['msg'] = '需<a href="/login/">登录</a>了,才能点击' return JsonResponse(back_dic)
{% extends 'base.html' %} {% block css %} <style> /* 点赞点踩样式 */ #div_digg { float: right; margin-bottom: 10px; margin-right: 30px; font-size: 12px; width: 128px; text-align: center; margin-top: 10px; } .diggit { float: left; width: 46px; height: 52px; background: url('/static/img/upup.gif') no-repeat; text-align: center; cursor: pointer; margin-top: 2px; padding-top: 5px; } .buryit { float: right; margin-left: 20px; width: 46px; height: 52px; background: url('/static/img/downdown.gif') no-repeat; text-align: center; cursor: pointer; margin-top: 2px; padding-top: 5px; } .clear { /*清除浮动*/ clear: both; } </style> {% endblock %} {% block content %} <h1 class="text-center">{{ article_obj.title }}</h1> <div class="article_content" style="overflow: hidden"> {{ article_obj.content|safe }} </div> <!--点赞点踩开始--> <div class="clearfix"> <div id="div_digg"> <div class="diggit action"> <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span> </div> <div class="buryit action"> <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span> </div> <div class="clear"></div> <div class="diggword" id="digg_tips" style="color: red;"></div> </div> </div> <!--点赞点踩结束--> <!--根评论开始--> <!--评论楼开始--> <ul class="list-group" id="id_floor"> {% for comment in comment_list %} <span>#{{ forloop.counter }}楼</span> <span>{{ comment.content_time|date:'Y-m-d h:i:s' }}</span> <span>{{ comment.user.username }}</span> <li class="list-group-item"> <div> {% if comment.parent_id %} <!--字段parent是自关联字段,跨表还是跨的自己,在再通过外键user跨表查根评论用户--> <p>@{{ comment.parent.user.username }}</p> {% endif %} <span>{{ comment.content }}</span> <!--a标签自定义属性--> <span class="pull-right"><a class="reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}">回复</a></span> </div> </li> {% endfor %} </ul> <!--评论楼结束--> <!--前端判断是否登录--> {% if request.user.is_authenticated %} <div> <p><span class="glyphicon glyphicon-comment"></span>发表评论</p> <div> <textarea name="comment" id="id_comment" cols="60" rows="10"></textarea> </div> <input type="button" class="btn btn-success" id="id_submit" value="提交评论"> <span style="color: red" id="id_error"></span> </div> {% else %} <ul class="nav navbar-nav navbar-link"> <li><a href="{% url 'reg' %}">注册</a></li> <li><a href="{% url 'log' %}">登录</a></li> </ul> {% endif %} <!--根评论结束--> {% endblock %} {% block js %} <script> $(function () { // ajax向后端发送点赞点踩数 $('.action').click(function () { // alert($(this).hasClass('diggit')) // isUp是bool值 let isUp = $(this).hasClass('diggit'); // 将点击过的div存起来 let cilckDiv = $(this); // 朝后端发送ajax请求 $.ajax({ url: '/up_or_down/', type: 'post', data: { 'article_id': '{{ article_obj.pk }}', 'is_up': isUp, }, success: function (args) { if (args.code == 1000) { // 取得原来的数,并存给变量Num let Num = cilckDiv.children().text(); // 点击过,该数加1,并修改过来,记得将数转成数字形,不然就是字符串拼接 cilckDiv.children().text(Number(Num) + 1); $('#digg_tips').text(args.msg) } else { $('#digg_tips').html(args.msg) } } }) }); // ajax向后端发送评论 // 全局设置一个parentId let parentId = null; console.log(parentId) $('#id_submit').click(function () { let dataComment = $('#id_comment').val(); // 判断当前评论是否是子评论,如果是,需要将我们之前手动渲染的@username去除 if (parentId){ // 找到\n对应的索引值,然后利用切片,但是切片顾头不顾尾,所以索引要加1 let indexNum = dataComment.indexOf('\n')+1 dataComment = dataComment.slice(indexNum) // 把换行符之前的切去 } $.ajax({ url: '/comment/', type: 'post', data: { 'article_id': '{{ article_obj.pk }}', 'comment': dataComment, // 前端parentId没有值,后端就存null 'parent_id':parentId }, success: function (args) { if (args.code = 1000) { $('#id_error').text(args.msg); // 情况评论框里的内容 $('#id_comment').val(''); // 临时渲染评论数 let userName = '{{ request.user.username }}' let temp = ` <span style="color: red">${userName}</span> <li class="list-group-item"> <div> ${dataComment} </div> </li> ` // 将生成好的标签添加到ul标签内 $('#id_floor').append(temp); // 提交后,需要将全局变量parentId恢复初始值 parentId = null; } } }) }); // 点击根评论的回复点击事件 $('.reply').click(function () { // 需要评论对应的评论人姓名 还需要评论的主键值 // 获取用户名 let commentUsername = $(this).attr('username'); // 获取主键值 直接修改parentId全局变量名 parentId = $(this).attr('comment_id'); // 拼接信息,聚焦事件传给评论框 $('#id_comment').val('@'+commentUsername+'\n').focus(); }) }) </script> {% endblock %}
''' 先写根评论 再写子评论 点击评论按钮需要将评论框里面的内容清空 根评论有两步渲染方式 1.DOM临时渲染 2.页面刷新render渲染 子评论 点击回复按钮发生了几件事 1.评论框自动聚焦 2.将回复按钮所在的哪一行评论人的姓名 @username 3.评论框内部自动换行 根评论子评论都是点击一个按钮朝后端提交数据的 parent_id 根评论子评论区别在哪? parent_id '''
# 点赞点踩 url(r'^up_or_down/', views.up_or_down),
# 评论视图函数 def comment(request): if request.is_ajax(): back_dic = {'code': 1000, 'msg': ''} article_id = request.POST.get('article_id') comment = request.POST.get('comment') parent_id = request.POST.get('parent_id') print(comment) print(parent_id) # 后端判断用户是否登录 if request.user.is_authenticated(): # 操作数据库,两张表,复习一下数据库的事务(两张表操作成功,才能成功) from django.db import transaction # 给文章表的评论数更新数据 with transaction.atomic(): from django.db.models import F models.Article.objects.filter(pk=article_id).update(comment_num=(F('comment_num') + 1)) # 记录评论表 models.Comment.objects.create(user=request.user, article_id=article_id, content=comment, parent_id=parent_id) back_dic['msg'] = '评论成功' else: back_dic['code'] = '2000' back_dic['msg'] = '请先登录' return JsonResponse(back_dic)
{% extends 'base.html' %} {% block css %} <style> /* 点赞点踩样式 */ #div_digg { float: right; margin-bottom: 10px; margin-right: 30px; font-size: 12px; width: 128px; text-align: center; margin-top: 10px; } .diggit { float: left; width: 46px; height: 52px; background: url('/static/img/upup.gif') no-repeat; text-align: center; cursor: pointer; margin-top: 2px; padding-top: 5px; } .buryit { float: right; margin-left: 20px; width: 46px; height: 52px; background: url('/static/img/downdown.gif') no-repeat; text-align: center; cursor: pointer; margin-top: 2px; padding-top: 5px; } .clear { /*清除浮动*/ clear: both; } </style> {% endblock %} {% block content %} <h1 class="text-center">{{ article_obj.title }}</h1> <div class="article_content" style="overflow: hidden"> {{ article_obj.content|safe }} </div> <!--点赞点踩开始--> <div class="clearfix"> <div id="div_digg"> <div class="diggit action"> <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span> </div> <div class="buryit action"> <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span> </div> <div class="clear"></div> <div class="diggword" id="digg_tips" style="color: red;"></div> </div> </div> <!--点赞点踩结束--> <!--根评论开始--> <!--评论楼开始--> <ul class="list-group" id="id_floor"> {% for comment in comment_list %} <span>#{{ forloop.counter }}楼</span> <span>{{ comment.content_time|date:'Y-m-d h:i:s' }}</span> <span>{{ comment.user.username }}</span> <li class="list-group-item"> <div> {% if comment.parent_id %} <!--字段parent是自关联字段,跨表还是跨的自己,在再通过外键user跨表查根评论用户--> <p>@{{ comment.parent.user.username }}</p> {% endif %} <span>{{ comment.content }}</span> <!--a标签自定义属性--> <span class="pull-right"><a class="reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}">回复</a></span> </div> </li> {% endfor %} </ul> <!--评论楼结束--> <!--前端判断是否登录--> {% if request.user.is_authenticated %} <div> <p><span class="glyphicon glyphicon-comment"></span>发表评论</p> <div> <textarea name="comment" id="id_comment" cols="60" rows="10"></textarea> </div> <input type="button" class="btn btn-success" id="id_submit" value="提交评论"> <span style="color: red" id="id_error"></span> </div> {% else %} <ul class="nav navbar-nav navbar-link"> <li><a href="{% url 'reg' %}">注册</a></li> <li><a href="{% url 'log' %}">登录</a></li> </ul> {% endif %} <!--根评论结束--> {% endblock %} {% block js %} <script> $(function () { // ajax向后端发送点赞点踩数 $('.action').click(function () { // alert($(this).hasClass('diggit')) // isUp是bool值 let isUp = $(this).hasClass('diggit'); // 将点击过的div存起来 let cilckDiv = $(this); // 朝后端发送ajax请求 $.ajax({ url: '/up_or_down/', type: 'post', data: { 'article_id': '{{ article_obj.pk }}', 'is_up': isUp, }, success: function (args) { if (args.code == 1000) { // 取得原来的数,并存给变量Num let Num = cilckDiv.children().text(); // 点击过,该数加1,并修改过来,记得将数转成数字形,不然就是字符串拼接 cilckDiv.children().text(Number(Num) + 1); $('#digg_tips').text(args.msg) } else { $('#digg_tips').html(args.msg) } } }) }); // ajax向后端发送评论 // 全局设置一个parentId let parentId = null; console.log(parentId) $('#id_submit').click(function () { let dataComment = $('#id_comment').val(); // 判断当前评论是否是子评论,如果是,需要将我们之前手动渲染的@username去除 if (parentId){ // 找到\n对应的索引值,然后利用切片,但是切片顾头不顾尾,所以索引要加1 let indexNum = dataComment.indexOf('\n')+1 dataComment = dataComment.slice(indexNum) // 把换行符之前的切去 } $.ajax({ url: '/comment/', type: 'post', data: { 'article_id': '{{ article_obj.pk }}', 'comment': dataComment, // 前端parentId没有值,后端就存null 'parent_id':parentId }, success: function (args) { if (args.code = 1000) { $('#id_error').text(args.msg); // 情况评论框里的内容 $('#id_comment').val(''); // 临时渲染评论数 let userName = '{{ request.user.username }}' let temp = ` <span style="color: red">${userName}</span> <li class="list-group-item"> <div> ${dataComment} </div> </li> ` // 将生成好的标签添加到ul标签内 $('#id_floor').append(temp); // 提交后,需要将全局变量parentId恢复初始值 parentId = null; } } }) }); // 点击根评论的回复点击事件 $('.reply').click(function () { // 需要评论对应的评论人姓名 还需要评论的主键值 // 获取用户名 let commentUsername = $(this).attr('username'); // 获取主键值 直接修改parentId全局变量名 parentId = $(this).attr('comment_id'); // 拼接信息,聚焦事件传给评论框 $('#id_comment').val('@'+commentUsername+'\n').focus(); }) }) </script> {% endblock %}
四 后台管理
新建页面,将文章个人文章展示出来,用到table标签,新增文章和其他功能的页面,导航条和左侧边栏一样,需要用到模版继承。
''' 当一个文件夹下文件比较多的时候,你还可以继续创建文件夹分类处理 templates文件夹 backend文件夹 应用1文件 应用2文件 '''
# 后台管理 url(r'^backend/$', views.backend),
@login_required def backend(request): article_list = models.Article.objects.filter(blog=request.user.blog) return render(request, 'backend/backend.html', locals())
base.html
用了 bootstrap中标签页,列表组
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{ request.user.username }}的后台管理</title> <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css"> <script src="/static/js/jQuery-3.6.1.js"></script> <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js"></script> </head> <body> <!--导航条--> <div class="container"> <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="/home/">{{ request.user.blog.site_title }}</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <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="#">test</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> <li role="separator" class="divider"></li> <li><a href="#">One more separated link</a></li> </ul> </li> </ul> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" placeholder="Search"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> <ul class="nav navbar-nav navbar-right"> {% if request.user.is_authenticated %} <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="#" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li> <li><a href="#">修改头像</a></li> <li><a href="#">后台管理</a></li> <li role="separator" class="divider"></li> <li><a href="{% url 'logout' %}" id="logout">退出登录</a></li> </ul> <!-- Large modal --> <div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <h3 class="text-center">修改密码</h3> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="form-group"> <label for="">用户名</label> <input type="text" disabled value="{{ request.user.username }}" class="form-control"> </div> <div class="form-group"> <label for="old_password">原密码</label> <input type="password" id="old_password" class="form-control"> <span style="color: red" class="pull-right"></span> </div> <div class="form-group"> <label for="">新密码</label> <input type="password" id="new_password" class="form-control"> </div> <div class="form-group"> <label for="">确认密码</label> <input type="password" id="confirm_password" class="form-control"> <span style="color: red" class="pull-right"></span> </div> <div class="form-group"> <button type="button" class="btn btn-default btn-sm" data-dismiss="modal"> 取消 </button> <button type="button" class="btn btn-primary btn-sm" id="set_commit"> 确认修改 </button> </div> </div> </div> </div> </div> </div> </li> {% else %} <li><a href="{% url 'reg' %}">注册</a></li> <li><a href="{% url 'log' %}">登录</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> </div> <!--主板2,10--> <div class="container"> <div class="row"> <!--左侧栏--> <div class="col-md-2"> <div class="list-group"> <a href="#" class="list-group-item active"> 操作 </a> <a href="/add/article/" class="list-group-item">新建文章</a> <a href="#" class="list-group-item">新建随笔</a> <a href="#" class="list-group-item">回收站</a> <a href="#" class="list-group-item">建议</a> </div> </div> <!--右侧内容区--> <div class="col-md-10"> <div> <!-- Nav tabs --> <ul class="nav nav-tabs" role="tablist"> <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab" data-toggle="tab">文章</a></li> <li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">随笔</a> </li> <li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">文件</a> </li> <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">设置</a> </li> </ul> <!-- Tab panes --> <div class="tab-content"> <div role="tabpanel" class="tab-pane active" id="home"> {% block content %} {% endblock %} </div> <div role="tabpanel" class="tab-pane" id="profile">随笔页面</div> <div role="tabpanel" class="tab-pane" id="messages">文件页面</div> <div role="tabpanel" class="tab-pane" id="settings">设置</div> </div> </div> </div> </div> </div> {% block js %} {% endblock %} </body> </html>
五 文章添加
属于后台管理页面中的功能
''' 有两个需要注意的问题 1.文章的简介 不能直接切去 应该先想办法获取到当前页面的文本内容之后截取150个文本字符 2.XSS攻击 针对支持用户直接编写html代码的网址 针对用户直接书写的script标签 需要处理 (1)注释标签内部的内容 (2)直接将script删除 如何解决? 针对1 后端通过正则表达式筛选 针对2 首先需要确定及获取script标签 beautifulsoup模块 bs4模块 专门用来帮你处理html页面的 该模块主要用于爬虫程序 pip3 install beautifulsoup4 ''' # 模块使用 soup = BeautifulSoup(content, 'html.parser') # 获取所有的标签 tags = soup.find_all() for tag in tags: # print(tag.name) # 获取页面所有的标签 # print(tags) # 针对script标签 直接删除 if tag.name == 'script': # 删除标签 tag.decompose() # 文章简介 # 1 先简单暴力的直接切去content 150个字符 # desc = content[0:150] # 2 截取文本150个 desc = soup.text[0:150] ''' 当你发现一个数据起来不是很方便的时候 可以考虑百度搜索有没有现成的模块帮你完成相应的功能 '''
# 增加文章 url(r'^add/article/', views.add_article),
@login_required def add_article(request): if request.method == 'POST': title = request.POST.get('title') # content是含html标签的字符串 content = request.POST.get('content') category_id = request.POST.get('category') # print(category_id) tag_id_list = request.POST.getlist('tag') # print(tag_id_list) # beautifulsoup模块使用 from bs4 import BeautifulSoup # 'html.parser'说明是用的python内置标准库 soup = BeautifulSoup(content, 'html.parser') print(soup, type(soup)) # tags是整个html标签页,含文本内容 tags = soup.find_all() # 从tags中获取标签,只有标签 for tag in tags: print(tag.name) # 获取页面所有的标签 # 防止xss攻击,删除script标签 if tag.name == 'script': # 删除script标签,删除给script标签 tag.decompose() # 文章简介 # 1 截取文本150个字符,之前的直接截取,是含有html标签的 desc = soup.text[0:150] # 操作数据,记录文章 article_obj = models.Article.objects.create( title=title, desc=desc, # 接受删除script标签的内容 content=str(soup), category_id=category_id, blog=request.user.blog ) # 文章和标签的关系表 半自动是我们自己创建的 没法使用add set remove clear方法 # 自己去操作关系表 一次性可能需要创建多条数据 批量插入bulk_create() article_obj_list = [] for i in tag_id_list: # 用批量插入,是对象添加到列表,bulk_create(列表),是对象,不能用orm,不然要报错 tag_article_obj = models.ArticleToTag(article=article_obj, tag_id=i) print(tag_article_obj) article_obj_list.append(tag_article_obj) # 批量插入,文章和标签的第三张表 print(article_obj_list) try: models.ArticleToTag.objects.bulk_create(article_obj_list) except Exception as e: print(e) obj = models.ArticleToTag.objects.filter(pk=1).first() print(obj) return redirect('/backend/') category_list = models.Category.objects.filter(blog=request.user.blog) tag_list = models.Tag.objects.filter(blog=request.user.blog) return render(request, 'backend/add_article.html', locals())
{% extends 'backend/base.html' %} {% block content %} <h3>添加文章</h3> <form action="" method="post"> {% csrf_token %} <p>标题</p> <input type="text" name="title" id="id_title" class="form-control"> <div> <p>内容</p> <textarea name="content" id="editor_id" cols="40" rows="10"></textarea> </div> <p>分类</p> <div> <select name="category"> {% for category in category_list %} <option value="{{ category.pk }}">{{ category.name }}</option> {% endfor %} </select> </div> <p>标签</p> <div> <select multiple name="tag"> {% for tag in tag_list %} <option value="{{ tag.pk }}">{{ tag.name }}</option> {% endfor %} </select> </div> <input type="submit" class="btn btn-danger"> </form> {% endblock %} {% block js %} {% load static %} <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script> <script> KindEditor.ready(function (K) { window.editor = K.create('#editor_id', { // 设置编辑 器大小 width: '100%', height: '600px', // 编辑器编辑按钮设置 items: [ 'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste', 'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright', 'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript', 'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/', 'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold', 'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage', 'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak', 'anchor', 'link', 'unlink', '|', 'about' ], // 编辑器,左右固定,上下可以拖动,改变大小 resizeType: 1, // 上传图片的后端提交路径 uploadJson: '/upload/', // 上传图片、Flash、视音频、文件时,支持添加别的参数一并传到服务器。 extraFileUploadParams: { 'csrfmiddlewaretoken': '{{ csrf_token }}', } }); }); </script> {% endblock %}
编辑器的种类很多,可以自己去网上搜索,
<a href="http://kindeditor.net/doc.php"> </a>
绑定textarea标签的id
<div> <p>内容</p> <textarea name="content" id="editor_id" cols="40" rows="10"></textarea> </div>
七 编辑器如何上传图片
别人写好了接口 但是接口不是你自己的 需要手动去修改
在使用别人的框架或者模块的时候 出现了问题不要慌 看看文档就用对应的处理方法
# 修改用户头像 url(r'^set/avatar/', views.set_avatar),
# 富文本编辑器上传文件视图函数 def upload(request): ''' 富文本编辑器,需要返回的格式数据 //成功时 { "error" : 0, "url" : "http://www.example.com/path/to/file.ext" } //失败时 { "error" : 1, "message" : "错误信息" } :param request: :return: ''' # 用户写文章上传的图片 也算静态资源 也应该放到media文件夹下 back_dic = {'error': 0} # 先定义给返回给前端编辑器的数据格式 if request.method == 'POST': # 获取用户上传的图片对象 print(request.FILES) # {'imgFile': [<InMemoryUploadedFile: 1603159551507.mp4 (video/mp4)>]} file_obj = request.FILES.get('imgFile') print(file_obj, file_obj.name) # 手动拼接存储文件的路径 from bbs01 import settings import os file_dir = os.path.join(settings.BASE_DIR, 'media', 'article') # 优化操作,先判断当前文件夹是否存在 不存在 自动创建 if not os.path.isdir(file_dir): os.mkdir(file_dir) # 创建一层目录结构 article # 拼接图片的完整路径,如果要 唯一的文件名,可以用uuid模块 file_path = os.path.join(file_dir, file_obj.name) print(file_path) with open(file_path, 'wb') as f: for line in file_obj: f.write(line) # 返回给编辑器的数据 back_dic['url'] = '/media/article/{}'.format(file_obj.name) return JsonResponse(back_dic)
八 修改用户头像
用的方式跟注册用户 头像一致,需要注意的是,修改后,在操作数据库的时候,
不用update方式修改数据库,obj.save(),第一种,要修改保存路径
# 修改用户头像 url(r'^set/avatar/', views.set_avatar),
# 修改用户头像 @login_required def set_avatar(request): if request.method == 'POST': file_obj = request.FILES.get('avatar') # update的更新方式,会将userinfo表的avatar字段的路径改动了,所以跳转到home页面时,显示不了头像 # models.UserInfo.objects.filter(blog=request.user.blog).update(avatar=file_obj) # 所以要用一下方式 # 1 自己手动添加avatar路径 # 2 新的更新方式 user_obj = request.user user_obj.avatar = file_obj user_obj.save() return redirect('/home/') blog = request.user.blog username = request.user.username return render(request, 'set_avatar.html', locals())
set_avatar.html
后期,建议用模态框的形式进行修改,或者另起一页
{% extends 'base.html' %} {% block content %} <h3 class="text-center">修改头像</h3> <!--form表单上传文件,一定要设置enctype--> <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} <p> 原头像: <img src="/media/{{ request.user.avatar }}" alt=""> </p> <p> 新头像: <label for="my_file">头像 {% load static %} <img src="{% static 'img/defaut.png' %}" id="my_img" alt="" class="img-rounded" style="margin-left: 10px"> </label> <input type="file" id="my_file" name="avatar" style="display: none"> </p> <input type="submit" class="btn btn-info"> </form> {% endblock %} {% block js %} <script> $(function () { $("#my_file").change(function () { // 文件阅读器对象 // 1 先生成一个文件阅读器对象,内置对象 let myFileReaderObj = new FileReader(); // 2 获取用户上传的头像文件 let fileObj = $(this)[0].files[0]; // console.log(fileObj) // 3 将文件对象交给阅读器对象读取 myFileReaderObj.readAsDataURL(fileObj) // 异步操作 IO操作 // 4 利用文件阅读器将文件展示到前端页面 修改src属性 // 等待文件阅读器加载完毕之后再执行 myFileReaderObj.onload = function () { // 修改图像地址属性 $('#my_img').attr('src', myFileReaderObj.result) } }) }) </script> {% endblock %}
九 BBS项目总结
在开发任意的web项目的时候,到了后期需要写的代码越来越少 都是用已经写好的url填写到a标签href属性完成跳转即可
主要功能总结
表设计
开发流程(粗糙流程 还可以细化)
注册功能 forms组件使用 头像动态展示 错误信息提示
登录功能 图片验证码 滑动验证码
首页展示 media配置 主动暴露任意资源接口
个人站点展示 侧边栏展示 侧边栏筛选 侧边栏inclusion_tag
文章详情页 点赞点踩 评论
后台管理
针对bbs需要掌握每一个功能的书写思路 内部逻辑 之后再去敲代码熟悉 找感觉