博客项目(三)
版权声明:本博客来自路飞学城Python全栈开发培训课件,仅用于学习之用,严禁用于商业用途。
欢迎访问路飞学城官网:https://www.luffycity.com/
本节实现博客主页、个人博客页、文章详情页、文章点赞与踩、文章评论(评论树和评论楼)等功能。
1. 博客主页、个人博客页
1.1 配置路由
urlpatterns = [
...
# 主页
path('index/', views.index),
re_path('^$', views.index),
# 个人博客url
re_path('^(?P<username>\w+)/$', views.home_site),
# 个人博客的跳转
re_path('^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*)/$', views.home_site),
# 文章详情页
re_path('^(?P<username>\w+)/articles/(?P<article_id>\d+)$', views.article_detail),
...
]
1.2 创建视图函数
# views.py
...
# 博客主页
def index(request):
article_list = models.Article.objects.all()
# 调用分页函数
article_list, paginator, currentPage, pageRange = page_util(request, article_list, 5)
return render(request, 'index.html', locals())
# 个人博客页
def home_site(request, username, **kwargs):
current_path = request.path
user = models.UserInfo.objects.filter(username=username).first()
if not user:
return render(request, "not_found.html")
blog = user.blog
article_list = models.Article.objects.filter(user=user)
if kwargs:
condition = kwargs.get("condition")
param = kwargs.get("param")
if condition == "category":
article_list = article_list.filter(category__title=param)
elif condition == "tag":
article_list = article_list.filter(tags__title=param)
else:
year, month = param.split("-")
article_list = article_list.filter(create_time__year=year, create_time__month=month)
# 调用分页函数
article_list, paginator, currentPage, pageRange = page_util(request, article_list, 5)
return render(request, "home_site.html", locals())
小技巧:
在个人博客页,用一个视图函数处理不同的页面跳转。
1.3 创建模板
博客主页: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页</title>
<link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
<script src="/static/js/jquery-3.2.1.min.js"></script>
<script src="/static/blog/bs/js/bootstrap.min.js"></script>
<style>
#user-icon{
margin-right: 10px;
font-size: 18px;
vertical-align: -3px;
}
.article_img{
width: 56px;
height: 56px;
}
.pub_info{
margin-top: 10px;
}
.pub_info .glyphicon{
vertical-align: -1px;
}
</style>
</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>
<!-- 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="#">随笔 <span class="sr-only">(current)</span></a></li>
<li><a href="#">新闻</a></li>
<li><a href="#">博文</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="/{{ request.user.username }}/">
<span id="user-icon" class="glyphicon glyphicon-user"></span>
{{ 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="/reset_pwd/">修改密码</a></li>
<li><a href="/logout/">注销</a></li>
<li role="separator" class="divider"></li>
<li><a href="/cn_backend/">后台管理</a></li>
</ul>
</li>
{% else %}
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="/">首页</a></li>
<li><a href="/{{ user.username }}/">我的主页</a></li>
<li><a href="/cn_backend/">后台管理</a></li>
</ul>
</div>
<!-- 内容区 -->
<div class="col-md-8">
<div class="article_list">
{% for article in article_list %}
<div class="article_item">
<h5><a href="/{{ article.user.username }}/articles/{{ article.pk }}">
{{ article.title }}
</a>
</h5>
<div class="article_desc media">
<div class="media-left">
<img class="article_img" src="media/{{ article.user.avatar }}" alt="">
</div>
<div class="media-body">
{{ article.desc }}
</div>
</div>
</div>
<div class="small pub_info">
<span><a href="/{{ article.user.username }}/">
{{ article.user.username }}
</a>
</span>
<span>发布于 {{ article.create_time | date:"Y-m-d H:i" }}</span>
<span class="glyphicon glyphicon-comment"></span>
评论({{ article.comment_count }})
<span class="glyphicon glyphicon-thumbs-up"></span>
点赞({{ article.up_count }})
</div>
<hr>
{% endfor %}
</div>
<!-- 分页 -->
<div class="text-center">
<ul class="pagination" id="pager">
{% if article_list.has_previous %}
<li class="previous">
<a href="/index/?page={{ article_list.previous_page_number }}">上一页</a>
</li>
{% else %}
<li class="previous disabled"><a href="#">上一页</a></li>
{% endif %}
{% for num in pageRange %}
{% if num == currentPage %}
<li class="item active"><a href="/index/?page={{ num }}">{{ num }}</a></li>
{% else %}
<li class="item"><a href="/index/?page={{ num }}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if article_list.has_next %}
<li class="next">
<a href="/index/?page={{ article_list.next_page_number }}">下一页</a>
</li>
{% else %}
<li class="next disabled"><a href="#">下一页</a></li>
{% endif %}
</ul>
</div>
</div>
<div class="col-md-2">
<div class="panel panel-info">
<div class="panel-heading">右边栏广告
<div class="panel-body">
广告内容区
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
个人博客基础模板:base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>个人主页</title>
<link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
<script src="/static/js/jquery-3.2.1.min.js"></script>
<script src="/static/blog/bs/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/static/blog/css/home_site.css">
<link rel="stylesheet" href="/static/blog/css/article_detail.css">
</head>
<body>
<div class="header">
<div class="content">
<p class="title">
<span>{{ blog.title }}</span>
<a href="/cn_backend/" class="backend">后台管理</a>
<a href="/index/" class="backend">返回首页</a>
</p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-2 menu">
{% load my_tags %}
{% get_classification_style username %}
</div>
<div class="col-md-10 content">
{% block content %}
{% endblock %}
</div>
</div>
</body>
</html>
个人博客页:home_site.html
{% extends "base.html" %}
{% block content %}
<div class="article_list">
{% for article in article_list %}
<div class="article_item clearfix">
<h5><a href="/{{ username }}/articles/{{ article.pk }}">{{ article.title }}</a></h5>
<div class="article_desc media">
<div class="media-body">
{{ article.desc }}
</div>
</div>
<div class="small pub_info pull-right">
<span>发布于 {{ article.create_time | date:"Y-m-d H:i" }}</span>
<span class="glyphicon glyphicon-comment"></span>
评论({{ article.comment_count }})
<span class="glyphicon glyphicon-thumbs-up"></span>
点赞({{ article.up_count }})
</div>
</div>
<hr>
{% endfor %}
</div>
<div class="text-center" >
<ul class="pagination" id="pager">
{% if article_list.has_previous %}
<li class="previous">
<a href="{{ current_path }}?page={{ article_list.previous_page_number }}">上一页</a>
</li>
{% else %}
<li class="previous disabled"><a href="#">上一页</a></li>
{% endif %}
{% for num in pageRange %}
{% if num == currentPage %}
<li class="item active"><a href="{{ current_path }}?page={{ num }}">{{ num }}</a></li>
{% else %}
<li class="item"><a href="{{ current_path }}?page={{ num }}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if article_list.has_next %}
<li class="next">
<a href="{{ current_path }}?page={{ article_list.next_page_number }}">下一页</a>
</li>
{% else %}
<li class="next disabled"><a href="#">下一页</a></li>
{% endif %}
</ul>
</div>
{% endblock %}
404页面:not_found.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/blog/bs/css/bootstrap.css">
</head>
<body>
<div class="container" style="margin-top: 100px">
<div class="text-center">
<a href="http://www.cnblogs.com/"><img src="/static/blog/img/logo_small.gif" alt="cnblogs"></a>
<p><b>404.</b> 抱歉! 您访问的资源不存在!</p>
<p class="d">请确认您输入的网址是否正确,如果问题持续存在,请发邮件至 404042726@qq.com 与 <strong style="font-size: 28px">老村长</strong> 联系。</p>
<p><a href="/">返回网站首页</a></p>
</div>
</div>
</body>
</html>
1.4 效果展示
博客主页:
个人主页:
404页面:
2. 博客详情页
2.1 自定义inclusion_tag
个人博客页面左边栏为文章分类、文章标签、发布日期的分类统计信息,一个博主的主页面的左侧栏和查看博主某篇文章的页面的左栅栏的一样的。为了不用重复写同样的代码。且提高页面的扩展性。左侧栏就用了inclusion_tag来实现。
-
在应用目录下创建 templatetags 目录(与 templates 目录同级,目录名只能是 templatetags)。
-
在 templatetags 目录下创建my_tags.py。
-
my_tags.py 文件代码如下(利用装饰器 @register.inclusion_tag 自定义标签):
from django import template from django.db.models import Count from django.db.models.functions import TruncMonth from blog import models register = template.Library() @register.inclusion_tag("classification.html") def get_classification_style(username): user = models.UserInfo.objects.filter(username=username).first() blog = user.blog # 查询当前站点的每一个分类名称以及对应的文章数 cate_list = models.Category.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values_list( "title", "c") # 查询当前站点的每一个标签名称以及对应的文章数 tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", "c") # 查询当前站点每一个年月的名称以及对应的文章数 # 方式1: # date_list=models.Article.objects.filter(user=user).extra(select={"y_m_date":"date_format(create_time,'%%Y/%%m')"}).values("y_m_date").annotate(c=Count("nid")).values_list("y_m_date","c") # 方式2: date_list = models.Article.objects.filter(user=user).annotate(month=TruncMonth("create_time")).values( "month").annotate(c=Count("nid")).values_list("month", "c") return {"user": user, "cate_list": cate_list, "date_list": date_list, "tag_list": tag_list}
-
写一个classification.html代码块:
<div> <div class="panel panel-warning"> <div class="panel-heading">文章分类</div> <div class="panel-body"> <ul class="list-group"> {% for cate in cate_list %} <p><a href="/{{ user.username }}/category/{{ cate.0 }}"> {{ cate.0 }}({{ cate.1 }}) </a> </p> {% endfor %} </ul> </div> </div> <div class="panel panel-success"> <div class="panel-heading">标签分类</div> <div class="panel-body"> <ul class="list-group"> {% for tag in tag_list %} <p><a href="/{{ user.username }}/tag/{{ tag.0 }}"> {{ tag.0 }}({{ tag.1 }}) </a> </p> {% endfor %} </ul> </div> </div> <div class="panel panel-info"> <div class="panel-heading">归档分类</div> <div class="panel-body"> <ul class="list-group"> {% for date in date_list %} <p><a href="/{{ user.username }}/archive/{{ date.0 | date:"Y-m" }}"> {{ date.0 | date:"Y-m"}}({{ date.1 }}) </a> </p> {% endfor %} </ul> </div> </div> </div>
-
页面上配置使用:
{% load my_tags %} {% get_classification_style username %}
上面的代码中username是一个参数。
-
效果展示
2.2 Django发邮件
-
获取qq邮箱授权码
-
什么是授权码
授权码是QQ邮箱推出的,用于登录第三方客户端的专用密码。
适用于登录以下服务:POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务。
温馨提醒:为了你的帐户安全,更改QQ密码以及独立密码会触发授权码过期,需要重新获取新的授权码登录。
-
怎么获取授权码
操作: 设置 —> 帐户 ,按照以下流程操作。
-
点击“生成授权码”
-
验证密保
-
获取授权码
-
-
-
setting.py中添加如下代码
# Host for sending email. EMAIL_HOST = 'smtp.qq.com' # 发送方的smtp服务器地址 # Port for sending email. EMAIL_PORT = 465 # smtp服务端口 # Optional SMTP authentication information for EMAIL_HOST. EMAIL_HOST_USER = 'you email@qq.com' # 发送方 邮箱地址 EMAIL_HOST_PASSWORD = '****************' # 获得的 授权码 EMAIL_USE_TLS = True # 必须为True EMAIL_USE_SSL = False EMAIL_SSL_CERTFILE = None EMAIL_SSL_KEYFILE = None EMAIL_TIMEOUT = None # Default email address to use for various automated correspondence from # the site managers. DEFAULT_FROM_EMAIL = 'you email@qq.com' # 和 EMAIL_HOST_USER 相同
注意:不添加授权码 报 SMTPAuthenticationError 错误 。
-
send_email.py 代码如下:
# 引入发送邮件的模块 from django.core.mail import send_mail, send_mass_mail, EmailMultiAlternatives from django.conf import settings res = send_mail( '测试邮件', '欢迎访问https://blog.csdn.net/mannixiang!', 'you email@qq.com', ['target email@aliyun.com'], ) if res == 1: return HttpResponse('邮件发送成功') else: return HttpResponse('邮件发送失败') # 对于send_mail方法, # 第一个参数是邮件主题subject; # 第二个参数是邮件具体内容; # 第三个参数是邮件发送方, # 第四个参数是接受方的邮件地址列表, 需要和你settings中的一致;
2.3 创建视图函数
# views.py
# 文章详情
def article_detail(request, username, article_id):
user = models.UserInfo.objects.filter(username=username).first()
if not user:
return render(request, "not_found.html")
blog = user.blog
article_obj = models.Article.objects.filter(pk=article_id).first()
comment_list = models.Comment.objects.filter(article_id=article_id)
return render(request, "article_detail.html",
{"username": username,
"blog": blog,
"article_obj": article_obj,
"comment_list": comment_list})
# 点赞
def digg(request):
response = {"flag": True, "msg": None}
article_id = request.POST.get("article_id")
is_up = json.loads(request.POST.get("is_up"))
user_id = request.user.pk
up_down_obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first()
if not up_down_obj:
models.ArticleUpDown.objects.create(user_id=user_id, is_up=is_up, article_id=article_id)
article_set = models.Article.objects.filter(pk=article_id)
if is_up:
article_set.update(up_count=F("up_count") + 1)
else:
article_set.update(down_count=F("down_count") + 1)
else:
response["flag"] = False
if up_down_obj.is_up:
response["msg"] = "您已经推荐过!"
else:
response["msg"] = "您已经反对过!"
return JsonResponse(response)
# 评论楼
def comment(request):
user = request.user
response = {"auth": None, "flag": None}
if user.is_authenticated:
response["auth"] = True
article_id = request.POST.get("article_id")
content = request.POST.get("content")
pid = request.POST.get("pid")
article_obj = models.Article.objects.filter(pk=article_id).first()
if pid:
parent_comment_obj = models.Comment.objects.filter(pk=pid).first()
parent_comment_username = parent_comment_obj.user.username
parent_comment_content = parent_comment_obj.content
response["flag"] = True
response["parent_comment_username"] = parent_comment_username
response["parent_comment_content"] = parent_comment_content
# 事务操作,新增评论记录和文章评论数增1保持一致性
with transaction.atomic():
comment_obj = models.Comment.objects.create(article_id=article_id,
user_id=user.pk,
content=content,
parent_comment_id=pid)
models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count") + 1)
response["username"] = user.username
response["content"] = content
response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
# 开启一个新的线程发邮件
t = threading.Thread(target=send_mail,
args=("您的文章%s新增了一条评论内容" % article_obj.title,
content,
settings.EMAIL_HOST_USER,
["mannixiang@126.com"]))
t.start()
return JsonResponse(response)
# 评论树
def comment_tree(request):
article_id = request.GET.get("article_id")
comment_list = list(
models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk",
"content",
"user__username",
"create_time",
"parent_comment_id"))
return JsonResponse(comment_list, safe=False)
2.4 创建模板
article_detail.html:
{% extends "base.html" %}
{% block content %}
{% csrf_token %}
<h3 class="text-center ">{{ article_obj.title }}</h3>
<div>
<a class="pull-right text-muted" href="/cn_backend/edit_article/{{ article_obj.pk }}">编辑</a>
</div>
<div>
{{ 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_count }}</span>
</div>
<div class="buryit action">
<span class="burynum" id="bury_count">{{ article_obj.down_count }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips"></div>
</div>
</div>
<div class="comments">
<h5>评论树</h5>
<div class="list-group comment_tree">
</div>
<hr>
<h5 >评论楼</h5>
<ul class="list-group conmment_list">
{% for comment in comment_list %}
<li class="list-group-item">
<div>
<a href="">#{{ forloop.counter }}楼</a>
<span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>
<a href="">{{ comment.user.username }}</a>
<a class="pull-right reply_btn"
username="{{ comment.user.username }}"
comment_pk="{{ comment.pk }}">回复</a>
</div>
{% if comment.parent_comment_id %}
<div class="pid_info well">
<p>
{{ comment.parent_comment.user.username }}:{{ comment.parent_comment.content }}
</p>
</div>
{% endif %}
<div class="comment_con">
<p>{{ comment.content }}</p>
</div>
</li>
{% endfor %}
</ul>
<p>发表评论</p>
<p>
昵称:
<input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
value="{{ request.user.username }}">
</p>
<p>评论内容:</p>
<textarea name="" id="comment_content" cols="60" rows="10"></textarea>
<p>
<button class="btn btn-default comment_btn">提交评论</button>
</p>
</div>
<script>
$(function () {
$("#div_digg .action").click(function(){
var is_up = $(this).hasClass("diggit");
var span_obj = $(this).children("span");
$.ajax({
url:"/digg/",
type:"post",
data:{
"is_up":is_up,
"article_id":"{{ article_obj.pk }}",
"csrfmiddlewaretoken":$("[name='csrfmiddlewaretoken']").val(),
},
success:function (data) {
if(data.flag){
span_obj.text(parseInt(span_obj.text())+1)
}else{
$("#digg_tips").html(data.msg);
setTimeout(function () {
$("#digg_tips").html("");
},1000)
}
}
})
});
// 评论请求
var pid = "";
$(".comment_btn").click(function () {
var content = $("#comment_content").val();
if(pid){
var index = content.indexOf("\n");
content = content.slice(index+1);
};
$.ajax({
url:"/comment/",
type:"post",
data:{
"pid":pid,
"content":content,
"article_id": "{{ article_obj.pk }}",
"csrfmiddlewaretoken":$("[name='csrfmiddlewaretoken']").val(),
},
success:function (data) {
var auth = data.auth;
if(!auth){
// 不允许匿名用户评论
alert("请登录后再评论!");
// 清空评论框
$("#comment_content").val("");
}else{
var flag = data.flag;
var username = data.username;
var content = data.content;
var create_time =data.create_time;
var comment_s = ""
if(flag){
var parent_comment_username = data.parent_comment_username;
var parent_comment_content = data.parent_comment_content;
comment_s = `
<li class="list-group-item">
<div>
<span>${ create_time }</span>
<a href="">${ username }</a>
</div>
<div class="pid_info well">
<p>
${parent_comment_username}:${parent_comment_content}
</p>
</div>
<div class="comment_con">
<p>${ content }</p>
</div>
</li>`;
}else{
comment_s = `
<li class="list-group-item">
<div>
<span>${ create_time }</span>
<a href="">${ username }</a>
</div>
<div class="comment_con">
<p>${ content }</p>
</div>
</li>`;
}
$("ul.conmment_list").append(comment_s);
// 清空评论框
$("#comment_content").val("");
// 重置pid
pid="";
}
}
})
});
// 发子评论,将父评论的评论人加到评论框内
$(".reply_btn").click(function () {
var val = "@" + $(this).attr("username") + "\n";
$("#comment_content").focus();
$("#comment_content").val(val);
pid = $(this).attr("comment_pk");
});
// 评论树
$.ajax({
url:"/comment_tree/",
method:'get',
data:{
"article_id": "{{ article_obj.pk }}",
"csrfmiddlewaretoken":$("[name='csrfmiddlewaretoken']").val(),
},
success:function (comment_tree) {
$.each(comment_tree,function (index, comment_obj) {
var comment_id = comment_obj.pk;
var username = comment_obj.user__username;
var content = comment_obj.content;
var create_time =comment_obj.create_time;
// 处理日期格式
if(create_time){
create_time =String(create_time).slice(0,19).replace("T"," ");
}
var s =`
<div class="list-group-item comment_tree_item"
comment_id = ${ comment_id }>
<div >
<span>${ create_time }</span>
<a href="">${ username }</a>
</div>
<div class="comment_con">
<p>${ content }</p>
</div>
</div>`
if(!comment_obj.parent_comment_id){
$(".comment_tree").append(s);
}else{
$("[comment_id="+ comment_obj.parent_comment_id +"]").append(s)
}
})
}
})
})
</script>
{% endblock %}
2.5 效果展示
评论楼逐级缩进小技巧:
- 利用ajax加载评论时,如果是一级评论,直接在评论ul里添加;
- 如果有父级评论,则找到父级评论,在父级评论内部追加评论;
- 所有评论均有.comment_tree_item的类
- 在CSS里设置该类左外边距为20px。则各级评论会相对于上一级元素往右偏移20px。
项目源码链接:
学python,找路飞,更多精彩,尽在路飞学城