1.功能模块
1.1.写博客
1.2.首页banner、推荐
1.3.首页博客列表
1.4.分类、标签(添加、列表、点击)
1.5.博客详情、评论
2.URL配置
3.models
class Category(models.Model):
name = models.CharField(max_length=32, verbose_name='分类名')
des = models.CharField(max_length=100, verbose_name='备注', null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
class Tag(models.Model):
name = models.CharField(max_length=32, verbose_name='标签名')
des = models.CharField(max_length=100, verbose_name='备注', null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
class Blog(models.Model):
title = models.CharField(max_length=70, verbose_name='文章标题')
body = RichTextUploadingField(verbose_name='文章正文', config_name='default')
created_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
modified_time = models.DateTimeField(verbose_name='修改时间', auto_now=True)
excerpt = models.CharField(max_length=200, blank=True, verbose_name='摘要') # CharField类型默认不能为空,指定blank=True允许为空
# on_delete=models.CASCADE,指明如果在Category中删除一条记录与这条记录有关联的博客记录也被删除
category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name='分类')
tags = models.ManyToManyField(Tag, blank=True, verbose_name='标签')
author = models.ForeignKey(LogUser, on_delete=models.CASCADE, verbose_name='作者')
views = models.IntegerField(default=0, verbose_name='查看次数')
cover_img = models.ImageField(upload_to='coverimage', blank=True, null=True, verbose_name='封面')
blog_type = [
("1", "banner"),
("2", "recommend"),
("3", "common")
]
type = models.CharField(max_length=1, choices=blog_type, default="common", verbose_name='类型')
def get_absolute_url(self):
"""
调用redirect(obj)函数时,如果obj是这个数据模型实例对象(相当一条表记录),
redirect()执行后重定向到该obj对象的get_absolute_url()方法返回的URL.
reverse()函数是一个URL反向解析函数
"""
return reverse('blog:detail', kwargs={'pk': self.pk})
def increase_views(self):
self.views += 1
self.save(update_fields=['views'])
def save(self, *args, **kwargs):
# 如果没有填写博客文章的摘要内容
if not self.excerpt:
"""
由于博客文章是由富文本编辑器编写的,文件中带有大量HTML标签
用strip()函数可能会把HTML标签截断这样博客文章的摘要在页面显示时,可能会有乱码或不易查看
strip_tags() 会把字段中的HTML 标签删去,然后在纯文本中截取字符串
"""
self.excerpt = strip_tags(self.body)[:118]
# 调用父类的save()方法将数据保存到数据库中
super(Blog, self).save(*args, **kwargs)
else:
# 重写save()必须调用父类的save()方法,否则数据不会保存到数据库
super(Blog, self).save(*args, **kwargs)
def __str__(self):
return self.title
class Meta:
# 设置按created_time的值倒序排列,这样最新的博客文章排在前面
# 指定倒序需在字段名前加负号
ordering = ['-created_time']
verbose_name = '文档管理表'
verbose_name_plural = '文档管理表'
4.配置settings.py
第一行命令安装CKEditor富文本编辑器,第二行命令安装图形处理模块。
pip install django-ckeditor
pip install pillow
注册富文本编辑器:
配置富文本编辑器:
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
AUTH_USER_MODEL = "blog.LogUser"
# CKEDITOR_UPLOAD_PATH设置富文本编辑器的上传文件的相对路径
# 它与MEDIA_ROOT组成完整的路径,也就是/djangoProject_blog/media/ upload/
CKEDITOR_UPLOAD_PATH = 'upload/'
# 设置图片处理的引擎为pillow,用于生成图片缩略图,在编辑器里浏览上传的图片
CKEDITOR_IMAGE_BACKEND = 'pillow'
CKEDITOR_CONFIGS = {
# 配置名是default时,django-ckeditor默认使用这个配置
'default': {
# 使用简体中文
'language': 'zh-cn',
# 设置富文本编辑器的宽度与高度
'width': '860px',
'height': '600px',
# 设置工具栏为自定义,名字为Custom
'toolbar': 'Custom',
# 添加富文本编辑器的工具栏上的按钮
'toolbar_Custom': [
# 字体风格
['Bold', 'Italic', 'Underline'],
# 字体颜色
['TextColor', 'BGColor'],
# 格式、字体、大小
['Format', 'Font', 'FontSize'],
# 列表
['Numbered List', 'Bulleted List'],
# 链接
['Image', 'Link', 'Unlink'],
# 居左,居中,居右
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
# 最大化
['Maximize']
]
},
# 设置另一个django-ckeditor配置,名为test
'test': {}
}
5.博客首页通用视图函数
视图IndexView继承于ListView通用视图类,ListView视图类主要用于获取数据模型的记录集合并以列表显示场景
class IndexView(ListView):
# 指定数据模型(数据库表),默认取全部记录
model = models.Blog
# 指定模块文件的地址与名字
template_name = 'blog/index.html'
# 指定传递给模板文件的模板变量名
context_object_name = 'blog_list'
6.博客母版文件
base.html:
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>博客</title>
<!-- 导入Bootstrap框架样式-->
<link href="{% static '/css/bootstrap.min.css' %}" rel="stylesheet">
<script src="{% static '/js/jquery-3.6.0.js' %}"></script>
<script src="{% static '/js/bootstrap.min.js' %}"></script>
<script src="{% static '/js/bootstrap.bundle.js' %}"></script>
</head>
<body>
<div class="container">
<header class="blog-header lh-1 py-3">
<div class="row flex-nowrap justify-content-between align-items-center">
<div class="col-4 pt-1">
<a class="link-secondary" href="/blog/write_blog/">写博客</a>
</div>
<div class="col-4 text-center">
<h1 class="blog-header-logo text-dark" href="#">blog</h1>
</div>
<div class="col-4 d-flex justify-content-end align-items-center">
<a class="link-secondary" href="#" aria-label="Search">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="mx-3" role="img" viewBox="0 0 24 24"><title>Search</title><circle cx="10.5" cy="10.5" r="7.5"/><path d="M21 21l-5.2-5.2"/></svg>
</a>
{% if request.user.username %}
<div class="dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">{{ request.user.nikename }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">个人资料</a></li>
<li><a class="dropdown-item" href="#">博客管理</a></li>
<li><a class="dropdown-item" href="/blog/logout">退出登录</a></li>
</ul>
</div>
{% else %}
<a class="btn btn-sm btn-outline-secondary" href="/blog/login">登录</a>
<a class="btn btn-sm btn-outline-secondary" href="/blog/register">注册</a>
{% endif %}
</div>
</div>
</header>
{% block main %}{% endblock main %}
</div>
<footer class="blog-footer">
<p>Blog built for <a href="#">Bootstrap</a> by <a href="#">zj</a>.</p>
<p>
<a href="#">Back to top</a>
</p>
</footer>
</body>
</html>
7.博客首页模板文件
{% extends 'base.html' %}
<!-- 导入自定义模板标签 -->
{% load custom_tags %}
{% block main %}
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>博客首页</title>
<!-- 导入Bootstrap框架样式-->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet" >
<meta name="theme-color" content="#712cf9">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
</style>
<link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900&display=swap" rel="stylesheet">
<link href="{% static 'css/blog.css' %}" rel="stylesheet">
</head>
<body>
<div class="container">
<ul class="nav nav-pills mb-3 nav-justified" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-article-tab" data-bs-toggle="pill" data-bs-target="#pills-article" type="button" role="tab" aria-controls="pills-article" aria-selected="true" onclick="location.href='/blog/index/'">Article</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-author-tab" data-bs-toggle="pill" data-bs-target="#pills-author" type="button" role="tab" aria-controls="pills-author" aria-selected="false" onclick="location.href='/blog/author/'">Author</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-article" role="tabpanel" aria-labelledby="pills-article-tab" tabindex="0"></div>
<div class="tab-pane fade" id="pills-author" role="tabpanel" aria-labelledby="pills-author-tab" tabindex="0"></div>
</div>
</div>
<main class="container">
<div>
<!-- 调用自定义标签文件custom_tags中定义的get_banner_blog()函数,获取type=banner的博客 -->
{% get_banner_blog as banner_blog %}
{% if banner_blog %}
<div class="mb-4" style="position: relative;">
<img src="/media/{{ banner_blog.cover_img }}" alt= "" width="100%" height="600px">
<div class="col-md-6 px-0" style="position: absolute;font-size: 40px;letter-spacing: 10px;top: 40%;left: 5%;">
<h1 class="display-4 fst-italic">{{ banner_blog.title }}</h1>
<p class="lead my-3">{{ banner_blog.excerpt }}</p>
<p class="lead mb-0"><a href="{{ banner_blog.get_absolute_url }}" class="text-white fw-bold">Continue reading...</a></p>
</div>
</div>
{% else %}
<p>暂无banner</p>
{% endif %}
<!-- 调用自定义标签文件custom_tags中定义的get_recommend_blog()函数,获取type=banner的博客 -->
<div class="row mb-2">
{% get_recommend_blog as recommend_blog_list %}
{% for recommend_blog in recommend_blog_list %}
<div class="col-md-6">
<div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
<div class="col p-4 d-flex flex-column position-static">
<strong class="d-inline-block mb-2 text-primary">{{ recommend_blog.category.name }}</strong>
<h3 class="mb-0">{{ recommend_blog.title }}</h3>
<div class="mb-1 text-muted">{{ recommend_blog.created_time }}</div>
<p class="card-text mb-auto" style="overflow: hidden;text-overflow: ellipsis;">{{ recommend_blog.excerpt }}</p>
<a href="{{ recommend_blog.get_absolute_url }}" class="stretched-link">Continue reading</a>
</div>
<div class="col-auto d-none d-lg-block">
<img class="media-object bd-placeholder-img" src="/media/{{ recommend_blog.cover_img }}" width="200" height="250">
</div>
</div>
</div>
{% empty %}
<p>暂无recommend</p>
{% endfor %}
</div>
<div class="row g-5">
<div class="col-md-8">
<h3 class="pb-4 mb-4 fst-italic border-bottom">
博客列表
</h3>
<article class="blog-post">
{% for blog in blog_list %}
<!-- h2 class="blog-post-title mb-1" onclick="location.href='{{blog.get_absolute_url }}'">{{ blog.title }}</h2 -->
<h2 class="blog-post-title mb-1"><a href="{{ blog.get_absolute_url }}" style="text-decoration: none">{{ blog.title }}</a></h2>
<p class="blog-post-meta">
<time class="entry-date" datetime="{{ blog.created_time }}">{{blog.created_time }}</time>by <a href="#">{{ blog.author.nikename }}</a>
</p>
<p>{{ blog.excerpt|safe }}......</p>
<hr>
{% endfor %}
</article>
</div>
<div class="col-md-4">
<div class="position-sticky" style="top: 2rem;">
<div class="p-4 mb-3 bg-light rounded">
<h4 class="fst-italic">About</h4>
<p class="mb-0">博客列表</p>
<ul>
<li>默认查询所有博客数据</li>
<li>点击作者,跳转到该作者的博客列表</li>
</ul>
<p>分类列表</p>
<ol>
<li>默认查询全部分类,通过自定义custom_tags实现</li>
<li>点击分类,左侧博客列表查询该分类下的博客</li>
</ol>
<p>标签列表</p>
<ol>
<li>默认查询有博客数据的标签,通过自定义custom_tags实现</li>
<li>点击标签,左侧博客列表查询该标签下的博客</li>
</ol>
</div>
<div class="p-4">
<div class="d-grid gap-2 d-md-flex justify-content-between">
<h4 class="">分类管理</h4>
<button class="btn btn-outline-primary h-25" type="button"
style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
onclick="show_add_category()">添加分类
</button>
</div>
<form id="add_category" style="display: none" method="post" action="{% url 'blog:add_category' %}">
{% csrf_token %}
<div class="form-floating mb-3">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com" name="name">
<label for="floatingInput">分类名称</label>
</div>
<div class="form-floating mb-3">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com" name="des">
<label for="floatingInput">分类描述</label>
</div>
<button type="submit" onclick="cancel_add_category()">保存</button>
<button type="button" onclick="cancel_add_category()">取消</button>
</form>
<div>
<!-- 调用自定义标签文件custom_tags中定义的get_categories()函数,显示每个类中的文章篇数 -->
{% get_categories as category_list %}
<a href="/blog/">全部分类</a>
{% for category in category_list %}
<ol class="list-unstyled mb-0">
<li><a href="{% url 'blog:category' category.pk %}">{{ category.name }}
<!-- 显示每个类中的文章篇数-->
<span class="post-count">({{ category.num_blogs }})</span>
</a></li>
</ol>
{% empty %}
<span>暂无分类</span>
{% endfor %}
</div>
</div>
<div class="p-4">
<div class="d-grid gap-2 d-md-flex justify-content-between">
<h4 class="">标签管理</h4>
<button class="btn btn-outline-primary h-25" type="button"
style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
onclick="show_add_tag()">添加标签</button>
</div>
<form id="add_tag" style="display: none" method="post" action="{% url 'blog:add_tag' %}">
{% csrf_token %}
<div class="form-floating mb-3">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com" name="name">
<label for="floatingInput">标签名称</label>
</div>
<div class="form-floating mb-3">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com" name="des">
<label for="floatingInput">标签描述</label>
</div>
<button type="submit" onclick="cancel_add_tag()">保存</button>
<button type="button" onclick="cancel_add_tag()">取消</button>
</form>
<div>
<!-- 调用自定义标签文件custom_tags中定义的get_categories()函数,显示每个类中的文章篇数 -->
{% get_tags as tag_list %}
{% for tag in tag_list %}
<ol class="list-unstyled mb-0">
<li><a href="{% url 'blog:tag' tag.pk %}">{{ tag.name }}
<!-- 显示每个类中的文章篇数-->
<span class="post-count">({{ tag.num_blogs }})</span>
</a></li>
</ol>
{% empty %}
<span>暂无标签</span>
{% endfor %}
<a href="/blog/">全部标签</a>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
<script>
function show_add_category() {
document.getElementById("add_category").style.display="block";
}
function cancel_add_category() {
document.getElementById("add_category").style.display="none";
}
function show_add_tag() {
document.getElementById("add_tag").style.display="block";
}
function cancel_add_tag() {
document.getElementById("add_tag").style.display="none";
}
</script>
</html>
{% endblock main %}
8.分类、标签
8.1.添加分类、标签表单forms.py
class CategoryForm(forms.ModelForm):
class Meta:
# 指定数据模型
model = models.Category
fields = "__all__"
class TagForm(forms.ModelForm):
class Meta:
# 指定数据模型
model = models.Tag
fields = "__all__"
8.2.添加分类、标签视图函数views.py
def add_category(request):
if request.method == 'POST':
form_category = CategoryForm(request.POST)
if form_category.is_valid():
form_category.save()
return redirect('/blog')
else:
print("添加失败")
return redirect('/blog')
form_category = forms.CategoryForm()
return render(request, 'blog/index.html', {'form_category': form_category})
def add_tag(request):
if request.method == 'POST':
form = TagForm(request.POST)
if form.is_valid():
form.save()
return redirect('/blog')
else:
print("添加失败")
return redirect('/blog')
form = forms.TagForm()
return form
html:
8.3.分类列表、标签列表
新建blog/templatetags文件夹,在这个文件夹下创建一个custom_tags.py文件,存放自定义的模板标签
# 导入 template 这个模块
from django import template
# 导入用到的数据模型,由于models文件存放在本文件的上一级目录,所以在models前加上..
from ..models import Blog, Category, Tag
# 导入聚合模块相关函数
from django.db.models.aggregates import Count
# 实例化了一个 template.Library 类,是固定写法
register = template.Library()
# 将函数装饰为 register.simple_tag
# 这样就可以在模板文件中使用 {% get_categories %} 调用这个函数
@register.simple_tag
def get_categories():
# 通过Django分类聚合函数统计每个分类中的文章的数量
return Category.objects.annotate(num_blogs=Count('blog'))
@register.simple_tag
def get_tags():
# 通过Django分类聚合函数统计每个标签中的文章的数量,并过滤掉没有文章的标签
return Tag.objects.annotate(num_blogs=Count('blog')).filter(num_blogs__gt=0)
html:
8.4.点击分类/标签,左侧博客列表显示对应博客数据
html:
视图函数:
class CategoryView(ListView):
# 设置数据模型,指定数据取自Blog,默认取全部记录
model = models.Blog
# 指定模板文件
template_name = 'blog/index.html'
# 指定传递给模板文件的参数名
context_object_name = 'blog_list'
def get_queryset(self):
"""
在普通视图函数中将模板变量传递给模板
通过给 render() 函数向模板文件传递一个字典来实现
例如 render(request, 'blog/index.html', context={'Blog_list':blog_list})
或者 render(request, 'blog/index.html',{'Blog_list':blog_list})
在通用类视图中,如果不是获取数据模型的全部记录
需要重写 get_context_data()方法,获得条件过滤后的记录
在复写该方法时,还可以增加一些自定义的模板变量
"""
cate = get_object_or_404(models.Category, pk=self.kwargs.get('pk'))
# 继承父类的get_queryset()方法,并通过filter()函数对记录进行过滤
# 通过order_by()进行排序
return super(CategoryView, self).get_queryset().filter(category=cate).order_by('-created_time')
class TagView(ListView):
model = models.Blog
template_name = 'blog/index.html'
context_object_name = 'blog_list'
def get_queryset(self):
tag = get_object_or_404(models.Tag, pk=self.kwargs.get('pk'))
return super(TagView, self).get_queryset().filter(tags=tag).order_by('created_time')
9.写博客
博客模型:
class Blog(models.Model):
title = models.CharField(max_length=70, verbose_name='文章标题')
body = RichTextUploadingField(verbose_name='文章正文', config_name='default')
created_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
modified_time = models.DateTimeField(verbose_name='修改时间', auto_now=True)
excerpt = models.CharField(max_length=200, blank=True, verbose_name='摘要') # CharField类型默认不能为空,指定blank=True允许为空
# on_delete=models.CASCADE,指明如果在Category中删除一条记录与这条记录有关联的博客记录也被删除
category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name='分类')
tags = models.ManyToManyField(Tag, blank=True, verbose_name='标签')
author = models.ForeignKey(LogUser, on_delete=models.CASCADE, verbose_name='作者')
views = models.IntegerField(default=0, verbose_name='查看次数')
cover_img = models.ImageField(upload_to='coverimage', blank=True, null=True, verbose_name='封面')
blog_type = [
("1", "banner"),
("2", "recommend"),
("3", "common")
]
type = models.CharField(max_length=1, choices=blog_type, default="common", verbose_name='类型')
def get_absolute_url(self):
"""
调用redirect(obj)函数时,如果obj是这个数据模型实例对象(相当一条表记录),
redirect()执行后重定向到该obj对象的get_absolute_url()方法返回的URL.
reverse()函数是一个URL反向解析函数
"""
return reverse('blog:detail', kwargs={'pk': self.pk})
def increase_views(self):
self.views += 1
self.save(update_fields=['views'])
def save(self, *args, **kwargs):
# 如果没有填写博客文章的摘要内容
if not self.excerpt:
"""
由于博客文章是由富文本编辑器编写的,文件中带有大量HTML标签
用strip()函数可能会把HTML标签截断这样博客文章的摘要在页面显示时,可能会有乱码或不易查看
strip_tags() 会把字段中的HTML 标签删去,然后在纯文本中截取字符串
"""
self.excerpt = strip_tags(self.body)[:118]
# 调用父类的save()方法将数据保存到数据库中
super(Blog, self).save(*args, **kwargs)
else:
# 重写save()必须调用父类的save()方法,否则数据不会保存到数据库
super(Blog, self).save(*args, **kwargs)
def __str__(self):
return self.title
class Meta:
# 设置按created_time的值倒序排列,这样最新的博客文章排在前面
# 指定倒序需在字段名前加负号
ordering = ['-created_time']
verbose_name = '文档管理表'
verbose_name_plural = '文档管理表'
视图函数:
save() 方法接受一个可选参数 commit ,它的值是 True 或者 False ,默认为 True。如果调用 save() 的时候使用 commit=False ,那么它会返回一个尚未保存到数据库的对象。在这种情况下,需要在生成的模型实例上调用 save() 。
如果模型具有多对多关系,并且在保存表单时指定了 commit=False ,Django无法立即保存多对多关系的表单数据。这是因为实例的多对多数据只有实例在数据库中存在时才能保存。
要解决这个问题,在手动保存表单生成的实例后,可以调用 save_m2m() 来保存多对多的表单数据。
def write_blog(request):
if request.method == 'POST':
blog_form = forms.BlogForm(request.POST, request.FILES)
if blog_form.is_valid():
blog = blog_form.save(commit=False)
blog.author = request.user
blog.save()
blog_form.save_m2m()
return redirect('/blog')
else:
print("添加失败")
form = forms.BlogForm()
return render(request, 'blog/write_blog.html', {'form_blog': form})
html:
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- 导入Bootstrap框架样式-->
<link href="{% static '/css/bootstrap.min.css' %}" rel="stylesheet">
<!--导入ckeditor的初始化Java Script脚本,src的值是默认路径 -->
<script src="{% static 'ckeditor/ckeditor-init.js' %}"></script>
<!--导入ckeditor的Java Script脚本,src的值是默认路径 -->
<script src="{% static 'ckeditor/ckeditor/ckeditor.js'%}"></script>
<title>写博客</title>
</head>
<body>
<div class="row">
<div class="col-md-offset-2 col-md-8">
<!--<form>标签中的enctype属性设为"multipart/form-data",才能实现图片上传-->
<form novalidate action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
<!-- 以下几个字段调用了Bootstrap样式 -->
<div class="form-group">
<label for="title" class="col-sm-2 control-label">{{ form_blog.title.label }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" name="title">
</div>
</div>
<div class="form-group">
<label for="title" class="col-sm-2 control-label">{{ form_blog.excerpt.label }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="excerpt" name="excerpt">
</div>
</div>
<div class="form-group">
<label for="title" class="col-sm-2 control-label">{{ form_blog.category.label }}</label>
<div class="col-sm-10">
{{ form_blog.category }}
</div>
</div>
<div class="form-group">
<label for="title" class="col-sm-2 control-label">{{ form_blog.tags.label }}</label>
<div class="col-sm-10">
{{ form_blog.tags }}
</div>
</div>
<div class="form-group">
<label for="title" class="col-sm-2 control-label">{{ form_blog.type.label }}</label>
<div class="col-sm-10">
{{ form_blog.type }}
</div>
</div>
<div class="form-group">
<label for="title" class="col-sm-2 control-label">{{ form_blog.body.label }}</label>
<div class="col-sm-10">
{{ form_blog.body }}
</div>
</div>
<div class="form-group">
<label>
{{ form_blog.cover_img.label }}
</label>
<div>
<label for="{{ form_blog.cover_img.id_for_label }}" class="col-sm-2 control-label">
{{ form_blog.cover_img }}
<!-- <image>标签的src属性先指向一个默认图片地址 //-->
<img id="file-img" src="{% static 'images/image-regular.svg' %}" style="height:100px;width:100px;">
<script>
var fileInput = document.querySelector('input[type="file"]'),
previewImg = document.querySelector('img');
fileInput.addEventListener('change', function () {
var file = this.files[0];
var reader = new FileReader();
// 监听reader对象的的onload事件,当图片加载完成时,把base64编码賦值给预览图片
reader.addEventListener("load", function () {
previewImg.src = reader.result;
}, false);
// 调用reader.readAsDataURL()方法,把图片转成base64
reader.readAsDataURL(file);
}, false);
</script>
</label>
</div>
</div>
<button type="submit">保存</button>
<button type="button" onclick="location.href='/blog/'">取消</button>
</form>
</div>
</div>
</body>
<script src="{% static '/js/jquery-3.6.0.js' %}"></script>
<script src="{% static '/js/bootstrap.min.js' %}"></script>
</html>
10.博客详情
视图函数
class BlogDetailView(DetailView):
# 指定数据模型
model = models.Blog
# 指定模板文件
template_name = 'blog/detail.html'
# 指定模板变量名
context_object_name = 'blog'
# 指定主键,'pk'为配置文件中的URL参数名
pk_url_kwarg = 'pk'
# 重写父类的get_object()方法,这个方法可以返回主键等于URL参数值的记录对象
# 可以进一步对这个记录对象进行操作,如调用该对象的方法
def get_object(self,queryset=None):
# 调用父类get_object()取得一条记录,主键等于URL参数pk的值
blog=super(BlogDetailView,self).get_object(queryset=None)
# 调用这条记录对象的increase_views()方法,把views字段值加1
blog.increase_views()
return blog
def get_context_data(self,**kwargs):
# 通过调用父类方法得到一个包含模板变量的字典
context=super(BlogDetailView,self).get_context_data(**kwargs)
# 初始化Commetn Form表单
form=CommentForm()
# 取得本条记录对象的所有评论
comment_list=self.object.comment_set.all()
# 在模板变量字典中加入新的字典项
context.update({'form':form,'comment_list':comment_list})
return context
html:
{% extends 'base.html' %}
{% load static %}
{% block main %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>博客详情</title>
<!-- 导入Bootstrap框架样式-->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet" >
<link href="{% static 'css/blog.css' %}" rel="stylesheet">
</head>
<body>
<article class="blog blog-{{ blog.pk }}">
<header class="entry-header">
<h1 class="entry-title">{{ blog.title }}</h1>
<div class="entry-meta-detail">
<!-- 用5个<span>标签显示文章类别、发布时间、作者、评论数、阅读数-->
<span class="blog-category">
<a href="#">{{blog.category.name }}</a>
</span>
<span class="blog-date">
<a href="#"><time class="entry-date" datetime="{{ blog.created_time }}">{{ blog.created_time }}</time></a>
</span>
<span class="blog-author"><a href="#">{{blog.author.nikename }}</a></span>
<span class="comments-link">
<a href="#">共
<span>{{ comment_list | length }}</span> 条评论</a>
</span>
<span class="views-count">
<a href="#">{{ blog.views }} 阅读</a>
</span>
</div>
</header>
<div class="entry-content clearfix">
<!-- 文章内容,用safe过滤器防止文章的样式被转义-->
{{ blog.body| safe }}
</div>
</article>
<!-- 判断用户是否登录,用户登录后才可以发表评论-->
{% if request.user.username %}
<section class="comment-area">
<h3>发表评论</h3>
<hr>
<!-- 页面表单,{% url 'comments:blog_comment' blog.pk %}反向解析成 URL, blog.pk 作为参数传给URL表达式-->
<form action="{% url 'comments:blog_comment' blog.pk %}" method="post">
{% csrf_token %}
<div class="form-group">
<label class="col-md-2">名字:</label>
{{ request.user.nikename }}
</div>
<div class="form-group">
<label class="col-md-2">邮箱:</label>
{{ request.user.email }}
</div>
<div class="form-group">
<label for="{{ form.text.id_for_label }}" class="col-md-2">评论:</label>
{{ form.text }}
{{ form.text.errors.0 }}
</div>
<div class="form-group">
<div class="col-8 mx-auto">
<button type="submit" class="btn btn-outline-primary btn-sm">发表</button>
</div>
</div>
</form>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<h5>评论列表,共 <span>{{comment_list|length}}</span> 条评论</h5>
</div>
<div class="panel-body">
<ul class="comment-list list-unstyled">
<!-- 通过for循环取出每一条记录-->
{% for comment in comment_list %}
<li><span style="color: #777;font-size: 14px;">{{comment.name }} · </span>
<time style="color: #777;font-size: 14px;">{{comment.created_time }}</time>
<div style="padding-top: 5px;font-size: 16px;">{{ comment.text }}</div>
</li>
{% empty %}
暂无评论
{% endfor %}
</ul>
</div>
</div>
</section>
</body>
</html>
{% endblock main %}
11.评论
创建评论应用程序,数据模型:
from django.db import models
# Create your models here.
class Comment(models.Model):
name = models.CharField(max_length=32)
email = models.EmailField(max_length=60)
# 评论可能有较长的文本,因此用TextField类型
text = models.TextField()
# created_time字段的auto_now_add=True,这样自动取出本记录保存时的时间
created_time = models.DateTimeField(auto_now_add=True)
# 一条评论只能属于一篇文章,一篇文章可以有多条评论# 文章与评论是一对多的关系,所以使用 ForeignKey类型
blog = models.ForeignKey('blog.Blog', on_delete=models.CASCADE,)
def __str__(self):
# 取评论前20个字符
return self.text[:20]
URL配置:
from django.urls import path
from . import views
# 指定命名空间
app_name = 'comments'
urlpatterns = [
path('comment/post/<int:blog_pk>/', views.blog_comment, name='blog_comment'),
]
文章评论视图函数:
from django.shortcuts import render,get_object_or_404, redirect
from blog.models import Blog
from . forms import CommentForm
# blog_pk是URL实名参数
def blog_comment(request, blog_pk):
# get_object_or_404()函数的作用是当要获取的文章(Blog)存在时,则获取该文章
# 否则返回 404 页面给用户
blog = get_object_or_404(Blog, pk=blog_pk)
# HTTP 请求使用最多的是 GET 和 POST 两种
# 如果是POST 请求,说明是前端页面提交数据
# 因此当请求为 POST时才需要处理表单数据
if request.method == 'POST':
# request.POST是一个字典类型的对象,表单提交的数据也保存在这个对象中
# 因此可用request.POST给CommentForm对象赋值
form = CommentForm(request.POST)
# form.is_valid() 方法检查表单的数据是否符合格式要求
if form.is_valid():
# 由于form是ModelForm类型,可以直接调用save() 方法保存数据到数据库表中
# commit=False 的作用是生成 Comment 类的实例对象
# 但不立刻保存数据到数据库表中
comment = form.save(commit=False)
# 用户登录后,request.user保存用户的nikename、email等值
comment.name = request.user.nikename
comment.email = request.user.email
# 通过外键关系将评论和被评论的文章关联起来
comment.blog = blog
# 真正保存到数据库表中
comment.save()
# redirect(blog)调用数据模型Blog的实例对象blog的get_absolute_url()方法
# 然后重定向到 get_absolute_url()方法返回的 URL
return redirect(blog)
else:
# 数据校验不通过,需要重新渲染页面
# 需要传递3个模板变量给 detail.html,文章(blog)、评论列表、表单对象(form)
comment_list = blog.comment_set.all()
context = {'blog': blog,'form': form,'comment_list': comment_list}
return render(request, 'blog/detail.html',context=context)
# 如果请求方法不是POST,说明是第一次打开页面
# 重定向到实例对象blog的 get_absolute_url()方法返回的地址
return redirect(blog)