BBS项目 未完待续

2023-02-16,,,

项目开发基本流程

1.需求分析
2.架构设计
3.分组开发
4.提交测试
5.交付上线

创建项目配置

环境配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
数据库配置 提前在终端创建好空数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'bbs',
'USER': 'root',
'PASSWORD': '222',
'HOST': '127.0.0.1',
'PORT': 3306,
'CHARSET': 'utf8',
}
} 静态文件配置 STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
AUTH_USER_MODEL = 'app01.UserInfo' # 前端校验forms """
时区问题报错
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
"""

额外扩展

官网提供了针对日期字段的切割处理

id  content      create_time     month
1 111 2020-11-11 2020-11
2 222 2020-11-12 2020-11
3 333 2020-11-13 2020-11
4 444 2020-11-14 2020-11
5 555 2020-11-15 2020-11
"""
django官网提供的一个orm语法
from django.db.models.functions import TruncMonth
-官方提供
from django.db.models.functions import TruncMonth
Sales.objects
.annotate(month=TruncMonth('timestamp')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.values('month', 'c') # (might be redundant, haven't tested) select month and count

后端导入的部分模块


import json from django.shortcuts import render, HttpResponse, redirect
from app01 import models
from app01 import myforms
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth.decorators import login_required
from app01 import mypage
from django.db.models import Sum, Avg, Count, Min, Max
from django.db.models import F

路由代码

from django.contrib import admin
from django.urls import path, re_path
from app01 import views
from django.views.static import serve
from django.conf import settings urlpatterns = [
path('admin/', admin.site.urls),
# 注册功能
path('register/', views.register_func, name='register_view'),
# 登录功能
path('login/', views.login_func, name='login_view'),
# 图片验证码相关功能
path('get_code/', views.get_code_func),
# 网站首页
path('home/', views.home_func, name='home_view'),
# 修改密码功能
path('set_pwd/', views.set_pwd_func),
# 注销登录功能
path('logout/', views.logout),
# 文章点赞点踩
path('up_or_down/', views.up_or_down_func),
# 文章评论
path('comment/', views.comment_func),
# 后台管理接口
path('backend/', views.backend_func),
# 后台管理之添加文章接口
path('add_article/', views.add_article_func), # 后台管理之文章上传文件
path('upload_img/', views.upload_img_func),
# 后台管理之编辑文章功能
path('edit_article/<int:article_pk>/', views.edit_article_func),
# 后台管理之文章删除
path('delete_article/', views.delete_article_func),
# 用户头像修改
path('set_avatar/', views.set_avatar_func), # 自定义暴露资源接口
re_path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}),
# 个人站点接口
path('<str:username>/', views.site_func),
# 侧边栏筛选接口
# path('<str:username>/category/<int:category_id>/', views.site_func),
# path('<str:username>/tag/<int:tag_id>/', views.site_func),
# path('<str:username>/archive/<str:yearAndmonth>/', views.site_func),
# # 上述三个路由可以合并成一个路由
re_path('^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*?)/', views.site_func), # 文章详情页
path('<str:username>/article/<str:article_id>/', views.article_detail_func), ]

项目流程

仿造博客园项目
核心:文章的增删改查 表分析
先确定表的数量 再确定表的基础字段 最后确定表的外键字段
1.用户表
2.个人站点表
3.文章表
4.文章分类表
5.文章标签表
6.点赞点踩表
7.文章评论表 基础字段分析
'''下列表字段设计仅供参考 你可以有更多的想法'''
用户表
替换auth_user表并扩展额外的字段
电话号码、头像、注册时间
个人站点表
站点名称(jason\lili\kevin)
站点标题(努力奋斗哈哈哈哈)
站点样式(css文件)
文章表
文章标题
文章简介
文章内容
发布时间
文章分类表
分类名称
文章标签表
标签名称
点赞点踩表:记录哪个用户给哪篇文章点了推荐(赞)还是反对(踩)
用户字段(用户主键)>>>:外键字段
文章字段(文章主键)>>>:外键字段
点赞点踩
文章评论表:记录哪个用户给哪篇文章评论了什么内容
用户字段(用户主键)>>>:外键字段
文章字段(文章主键)>>>:外键字段
外键字段(自关联)
"""
id user_id article_id content parent_id
1 1 1 哈哈哈 null
2 2 1 哈尼没 1
3 3 1 讲文明 2
"""
外键字段
用户表
用户与个人站点是一对一外键关系
个人站点表 文章表
文章表与个人站点表是一对多外键关系
文章表与文章分类表是一对多外键关系
文章表与文章标签表是多对多外键关系
'''
数据库字段优化设计:我们想统计文章的评论数 点赞数
通过文章数据跨表查询到文章评论表中对应的数据统计即可
但是文章需要频繁的展示 每次都跨表查询的话效率极低
我们在文章表中再创建三个普通字段
之后只需要确保每次操作评论表或者点赞点踩表时同步修改上述三个普通字段即可 '''
文章评论数
文章点赞数
文章点踩数 文章分类表
文章分类与个人站点是一对多外键关系
文章标签表
文章标签与个人站点是一对多外键关系

创建表字段

from django.db import models

# Create your models here.
from django.contrib.auth.models import AbstractUser class UserInfo(AbstractUser):
"""用户表"""
phone = models.BigIntegerField(verbose_name='手机号', null=True, blank=True) # blank参数用于控制admin后台管理 与数据库无关
avatar = models.FileField(upload_to='avatar/', default='avatar/default.jpg', verbose_name='用户头像')
register_time = models.DateTimeField(verbose_name='注册时间', auto_now_add=True) """用户表与个人站点一对一外键"""
site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True) # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '用户表' def __str__(self):
return f'用户对象:{self.username}' class Site(models.Model):
"""个人站点表"""
site_name = models.CharField(verbose_name='站点名称', max_length=32)
site_title = models.CharField(verbose_name='站点标题', max_length=32)
site_theme = models.CharField(verbose_name='站点样式', max_length=32, null=True) # 简单模拟样式文件 # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '个人站点表' def __str__(self):
return f'个人站点:{self.site_theme}' class Article(models.Model):
"""文章表"""
title = models.CharField(verbose_name='文章标题', max_length=32)
desc = models.CharField(verbose_name='文章简介', max_length=255)
content = models.TextField(verbose_name='文章内容')
create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
# 三个优化字段
comment_num = models.IntegerField(verbose_name='评论数', default=0)
up_num = models.IntegerField(verbose_name='点赞数', default=0)
down_num = models.IntegerField(verbose_name='点踩数', default=0) # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '文章表' """文章与个人站点一对多外键"""
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
"""文章表与分类表外键"""
category = models.ForeignKey(to='Category', on_delete=models.CASCADE, null=True)
"""文章表与标签表是多对多 半自动创建"""
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article', 'tag'),
) def __str__(self):
return f'文章对象:{self.title}' class Category(models.Model):
"""文章分类表"""
name = models.CharField(verbose_name='分类名称', max_length=32) """个人站点和文章分类的外键 """
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True) # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '文章分类表' def __str__(self):
return f'文章分类:{self.name}' class Tag(models.Model):
"""文章标签表"""
name = models.CharField(verbose_name='标签名称', max_length=32)
"""个人站点和文章标签的外键 """
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True) # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '文章标签表' def __str__(self):
return f'文章标签:{self.name}' """文章表与标签表是多对多 半自动创建""" class Article2Tag(models.Model):
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE, null=True) # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '文章表与标签表关系' class UpAndDown(models.Model):
"""文章点赞点踩表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
is_up = models.BooleanField(verbose_name='点赞点踩') # 传布尔值存 0或者1 # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '文章点赞点踩表' class Comment(models.Model):
"""文章评论表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
content = models.TextField(verbose_name='评论内容')
comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True) # 修改admin后台管理的表名
class Meta:
verbose_name_plural = '文章评论表'

注册功能

用户注册
1.渲染前端标签
2.校验用户数据
3.展示错误提示
ps:forms组件、modelform组件 单独开设py文件编写 解耦合!!!
渲染标签 前端校验
from django import forms
from app01 import models class RegisterForm(forms.Form):
"""用户注册form类"""
username = forms.CharField(min_length=3, max_length=8, label='用户注册',
error_messages={
'min_length': '用户名最短三位',
'max_length': '用户名最长八位',
'required': '用户名不能为空',
}, widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(min_length=3, max_length=8, label='密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空',
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(min_length=3, max_length=8, label='确认密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空',
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式不正确',
},
widget=forms.widgets.EmailInput(attrs={'class': 'form-control'})
) # 用户头像单独校验 不使用校验类 其他字段自己看加校验
# 钩子函数
# 局部钩子校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
res = models.UserInfo.objects.filter(username=username)
if res:
self.add_error('username', '用户名已存在')
return username # 全局钩子校验两次密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data

代码

前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户注册</h2>
<form id="form"> <!--不使用form表单提交数据 但是用一下form标签 它有一个序列化功能-->
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group"> <!--目的是让多个获取用户数据的标签上下间距更大一些-->
<label for="{{ form.auto_id }}">{{ form.label }}</label> <!--form.auto_id 自动获取渲染的标签id值-->
{{ form }}
<span style="color: red" class="pull-right"></span> </div>
{% endfor %}
<!--用户头像自己编写相关标签获取-->
<div class="form-group">
<label for="myfile">头像
<img src="/static/img/default.jpg" alt="" id="myimg" width="120">
</label>
<input type="file" id="myfile" style="display: none" > </div>
<input type="button" id="subBtn" class="btn btn-primary btn-block" value="用户注册"> </form> </div>
</div>
<script>
//1.用户头像的实时展示
$('#myfile').change(function (){
//1.产生一个文件阅读器对象
let myFilReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
//3.将文件对象交给阅读器对象读取
myFilReaderObj.readAsDataURL(fileObj); //异步
// 等待文件阅读器对象加载完毕之后再修改src
myFilReaderObj.onload = function (){
//4.修改img标签的src 属性展示图片
$('#myimg').attr('src',myFilReaderObj.result)
} }) //2.给注册按钮绑定点击事件 发送ajax 携带了文件数据
$('#subBtn').click(function (){
//1.先产生一个内置对象
let myFormDataObj = new FormData();
//2.添加普通数据(单个单个的编写效率极低)
{#console.log($('#form').serializeArray()) // 可以一次性获取form标签内所有普通字段数据 [{数组},{数组},{数组}]#}
$.each($('#form').serializeArray(),function (index,dataObj){ // 对结果for循环 然后交给后面的函数处理
myFormDataObj.append(dataObj.name,dataObj.value) // {'name':'','value':'',}
})
//3.添加文件数据
myFormDataObj.append('avatar',$('#myfile')[0].files[0])
//4.发送ajax请求
$.ajax({
url:'',
type: 'post',
data:myFormDataObj, contentType:false,
processData: false,
success: function (args){
if(args.code === 10000){
window.location.href = args.url
}else{
{#console.log(args.msg)#}
let dataObj = args.msg;
// 如何针对性的渲染错误提示 {'username'} id_username
$.each(dataObj,function (k,msgArray){
// 拼接标签的id值
let eleId = '#id_' + k
// 根据id查找标签 修改下面span标签的内容 并给父标签添加错误样式
$(eleId).next().text(msgArray[0]).parent().addClass('has-error')
})
}
}
})
}) //3.给所有的input标签绑定获取焦点事件 移除错误样式
$('input').focus(function (){
$(this).next().text('').parent().removeClass('has-error')
})
</script> </body>
</html> 后端代码
from django.shortcuts import render, HttpResponse, redirect
from app01 import models
# Create your views here.
from app01 import myforms
from django.http import JsonResponse def register_func(request):
# 前后段ajax交互 通常采用字段作为交互对象
back_dict = {'code': 10000, 'msg': ''} # 1.先产生一个空的form_obj
form_obj = myforms.RegisterForm()
if request.method == 'POST':
form_obj = myforms.RegisterForm(request.POST) # username password confirm_password email csrfmiddlewaretoken
if form_obj.is_valid():
clean_data = form_obj.cleaned_data # 存储符合校验的数据 {username password confirm_password email}
# 将confirm_password键值对移除
clean_data.pop('confirm_password') # {username password email}
# 获取用户上传的头像文件
avatar_obj = request.FILES.get('avatar') # 用户有可能没有上传
if avatar_obj:
clean_data['avatar'] = avatar_obj # {username password email avatar}
# 创建用户数据
models.UserInfo.objects.create_user(**clean_data) # 上述处理字典的目的就是为了创建数据省事
# return HttpResponse('注册成功!!!!')
back_dict['msg'] = '注册功能'
back_dict['url'] = '/login/'
else:
back_dict['code'] = 10001
back_dict['msg'] = form_obj.errors
return JsonResponse(back_dict)
return render(request, 'registerPage.html', locals())

登录功能

img标签的src属性
1.可以直接填写图片地址
2.还可以填写一个路由 会自动朝该路由发送get请求
如果结果是图片的二进制数据 那么自动渲染图片 报错终端执行
pip3.8 install pillow -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com

代码(不全 明天改)

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户登录</h2> {% csrf_token %}
<div class="form-group">
<label for="name">用户名</label>
<input type="text" id="name" class="form-control" name="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" class="form-control" name="password">
</div>
<div class="form-group">
<label for="code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" id="code" class="form-control" name="code">
</div>
<div class="col-md-6">
<img src="/get_code/" alt="" width="350" id="d1" height="35">
</div>
</div> </div> <input type="button" class="btn btn-success btn-block" value="用户登录" id="loginBtn">
</div> </div> <script>
// 1.图片验证码动态刷新
$('#d1').click(function () {
let oldSrc = $(this).attr('src');
$(this).attr('src', oldSrc + '?')
}) // 2.登录按钮发送ajax请求
$('#loginBtn').click(function () {
// 可以再次使用form标签序列化功能 也可以自己挨个获取
$.ajax({
url: '',
type: 'post',
data: {
'username': $('#name').val(),
'password': $('#password').val(),
'code': $('#code').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code === 10000) {
window.location.href = args.url
} else {
// 课下可以使用sweetalert 插件美化展示 需要引入sweetalert cdn
alert(args.msg)
{#swal(args.msg,'验证码输错了!!!')#}
}
}
})
})
</script> </body>
</html> 后端代码
from django.shortcuts import render, HttpResponse, redirect
from app01 import models def login_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
if code.upper() == request.session.get('code').upper():
user_obj = auth.authenticate(request, username=username, password=password)
if user_obj:
# 保存用户登录状态
auth.login(request, user_obj) # 执行之后就可以使用request.user获取登录用户对象
back_dict['msg'] = '登录成功!!!!'
back_dict['url'] = '/home/'
else:
back_dict['code'] = 10001
back_dict['msg'] = '用户名或密码不正确'
else:
back_dict['code'] = 10002
back_dict['msg'] = '验证码错误'
return JsonResponse(back_dict)
return render(request, 'loginPage.html') from PIL import Image, ImageFont, ImageDraw """
Image: 产生图片
ImageFont: 字体样式
ImageDraw: 画笔对象
"""
from io import BytesIO, StringIO """
BytesIO: 在内存中临时存储 读取的时候以bytes格式为准
StringIO: 在内存中临时存储 读取的时候以字符串格式为准
"""
import random def get_random():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) def get_code_func(request):
# 1.推导步骤1:直接读取图片文件返回
# with open(r'D:\djangoProject\BBS\app01\avatar\555.jpg', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# # 2.推导步骤2:随机产生图片动态返回 pillow模块
# img_obj = Image.new('RGB',(350,35),'yellow')
# with open(r'xxx.png','wb')as f:
# img_obj.save(f,'png')
# with open(r'xxx.png','rb')as f:
# data = f.read()
# return HttpResponse(data)
# 3.推导步骤3:针对图片的保存与读取 做优化 内存管理器
# img_obj = Image.new('RGB', (350, 35), 'yellow')
# io_obj = BytesIO()
# img_obj.save(io_obj, 'png')
# return HttpResponse(io_obj.getvalue())
# 4.推导步骤4:图片颜色可以随机变换
# img_obj = Image.new('RGB', (350, 35), get_random())
# io_obj = BytesIO()
# img_obj.save(io_obj, 'png')
# return HttpResponse(io_obj.getvalue())
# 5.推导步骤5:编写验证码
img_obj = Image.new('RGB', (350, 35), get_random()) # 先产生图片对象
# 将图片对象交给画笔对象
draw_obj = ImageDraw.Draw(img_obj)
# 确定字体样式(ttf文件)
font_obj = ImageFont.truetype('static/font/111.ttf', 35)
# 产生随机验证码
code = ''
for i in range(5):
random_upper = chr(random.randint(65, 90))
random_lower = chr(random.randint(97, 122))
random_int = str(random.randint(1, 9))
# 三选一
temp_choice = random.choice([random_upper, random_lower, random_int])
# 写到图片上
draw_obj.text((i * 45 + 45, 0), temp_choice, font=font_obj)
code += temp_choice
# 后端保存验证码 便于后续的比对
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())

主页展示

1.导航条 右上角登陆注册  及修改密码 注销登陆
2.侧边栏文章及标签,发布时间等展示
3.文章内容的展示及 文章标签下的样式
前端代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
{% block css %} {% endblock %}
</head>
<body>
<!--导航条开始-->
<nav class="navbar navbar-inverse">
<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="#">
{% block title %}
BBS
{% endblock %}
</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> </ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="搜索">
</div>
<button type="submit" class="btn btn-default">搜索</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="#myModal">修改密码</a></li>
<li><a href="#">修改头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="/logout/">注销登录</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'register_view' %}">注册</a></li>
<li><a href="{% url 'login_view' %}">登录</a></li>
{% endif %} </ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<!--导航条结束-->
<!--模态框开始-->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title text-center" id="myModalLabel">修改密码</h4>
</div>
<div class="modal-body"> <div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control">
</div>
<div class="form-group">
<label for="">原密码</label>
<input type="text" id="old_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">新密码</label>
<input type="text" id="new_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">确认密码</label>
<input type="text" id="confirm_pwd" class="form-control">
</div> </div>
<div class="modal-footer">
<span id="error" style="color: red" ></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="setBtn">修改</button>
</div>
</div>
</div>
</div>
<!--模态框结束-->
<!--内容区开始-->
<div class="container-fluid">
<div class="row">
{% block content %}
<div class="col-md-2">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">中秋节苦熬了</h3>
</div>
<div class="panel-body">
抓紧练习:wuyong
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">百万大奖</h3>
</div>
<div class="panel-body">
共享你幸运儿:22222
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">广告找找</h3>
</div>
<div class="panel-body">
旺铺难求
</div>
</div>
</div> <div class="col-md-8">
{% for article_obj in page_queryset %}
<div class="media">
<h4 class="media-heading"><a href="/{{ article_obj.site.userinfo.username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h4>
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ article_obj.site.userinfo.avatar }}" alt="..." width="80">
</a>
</div>
<div class="media-body">
{{ article_obj.desc }}
</div>
<br>
<div>
<span><a href="/{{ article_obj.site.userinfo.username }}/">{{ article_obj.site.userinfo.username }}&nbsp; &nbsp;</a></span>
<span>{{ article_obj.create_time|date:'Y-m-d H:i:s' }}&nbsp;&nbsp;</span>
<span class="glyphicon glyphicon-thumbs-up">{{ article_obj.up_num }}&nbsp;&nbsp;</span>
<span class="glyphicon glyphicon-thumbs-down">{{ article_obj.down_num }}&nbsp;&nbsp;</span>
<span class="glyphicon glyphicon-comment">{{ article_obj.comment_num }}&nbsp;&nbsp;</span>
</div> </div> <hr>
{% endfor %} <div class="text-center">{{ page_obj.page_html|safe }}</div>
</div> <div class="col-md-2">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">中秋节苦熬了</h3>
</div>
<div class="panel-body">
抓紧练习:wuyong
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">百万大奖</h3>
</div>
<div class="panel-body">
共享你幸运儿:22222
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">广告找找</h3>
</div>
<div class="panel-body">
旺铺难求
</div>
</div>
</div> {% endblock %} </div> </div>
<!--内容区结束--> <script>
$('#setBtn').click(function () {
$.ajax({
url: '/set_pwd/',
type: 'post',
data: {
'old_pwd': $('#old_pwd').val(),
'new_pwd': $('#new_pwd').val(),
'confirm_pwd': $('#confirm_pwd').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success: function (args) {
if(args.code === 10000){
window.location.href = args.url }else{
$('#error').text(args.msg)
} }
})
})
</script> {% block js %} {% endblock %} </body>
</html> 后端代码
具体导入模块在上面单独的一个框内 def home_func(request):
# 查询所有的用户编写的文章
article_queryset = models.Article.objects.all()
'''文章过多的情况下应该考虑添加分页器'''
page_obj = mypage.Pagination(current_page=request.GET.get('page'), all_count=article_queryset.count())
page_queryset = article_queryset[page_obj.start:page_obj.end] return render(request, 'homePage.html', locals()) @login_required
def set_pwd_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
old_pwd = request.POST.get('old_pwd')
new_pwd = request.POST.get('new_pwd')
confirm_pwd = request.POST.get('confirm_pwd')
# 先校验原密码是否正确
if request.user.check_password(old_pwd):
# 再校验两次密码是否一致 并且不能为空
if new_pwd == confirm_pwd and new_pwd:
request.user.set_password(new_pwd)
request.user.save()
back_dict['msg'] = '密码修改成功!!!'
back_dict['url'] = '/login/'
else:
back_dict['code'] = 10001
back_dict['msg'] = '两次密码不一致或者为空'
else:
back_dict['code'] = 10002
back_dict['msg'] = '原密码错误'
return JsonResponse(back_dict) @login_required
def logout(request):
auth.logout(request)
return redirect('home_view')

文章展示

侧边栏筛选功能:
1.先研究博客园三种情况下的筛选特性
分类筛选路由特性: 站点名称/category/数据主键值
标签筛选路由特性: 站点名称/tag/数据主键值
日期筛选路由特性: 站点名称/archive/文章年月
2.研究路由开设接口
多个路由使用相同的视图函数 因为个人站点的文章和侧边栏筛选的文章互为父子集 # 侧边栏筛选接口
# path('<str:username>/category/<int:category_id>/', views.site_func),
# path('<str:username>/tag/<int:tag_id>/', views.site_func),
# path('<str:username>/archive/<str:yearAndmonth>/', views.site_func),
# 上述三个路由可以合并成一个路由
re_path('^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*?)/', views.site_func) 文章详情页搭建:
1.路由的设计
站点名称/article/数据主键值
2.侧边栏制作inclusion_tag
点赞点踩样式搭建:
1.直接拷贝博客园样式即可 主要除了html还有css
2.针对路由匹配
含有动态匹配的路由很多时候可能会出现顶替的情况
这个时候我们可以将简单的路由放前面 复杂的放后面 甚至修改匹配策略
文章评论功能:
1.前端样式搭建
2.评论逻辑
先考虑根评论 之后再考虑子评论 不要乱!!!
3.根评论
点击提交评论按钮 发送ajax请求 携带必要的参数即可
文章展示前端代码
{% extends 'homePage.html' %} {% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/"> {% endblock %} {% block title %}
{{ site_obj.site_title }}
{% endblock %} {% block content %}
<div class="col-md-2">
{% load mytag %}
{% mymenu username %}
</div>
<div class="col-md-10">
{% for article_obj in article_queryset %} <div class="media">
<h4 class="media-heading"><a href="/{{ article_obj.site.userinfo.username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h4>
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ article_obj.site.userinfo.avatar }}" alt="..." width="80">
</a>
</div>
<div class="media-body" style="padding: 10px"> {{ article_obj.desc }}
</div>
<br>
<div class="pull-right">
<span>posted&nbsp;&nbsp;@</span> <span>{{ article_obj.create_time|date:'Y-m-d H:i:s' }}&nbsp;&nbsp;</span>
<span>{{ article_obj.site.userinfo.username }}&nbsp; &nbsp;</span> <span class="glyphicon glyphicon-thumbs-up">{{ article_obj.up_num }}&nbsp;&nbsp;</span>
<span class="glyphicon glyphicon-thumbs-down">{{ article_obj.down_num }}&nbsp;&nbsp;</span>
<span class="glyphicon glyphicon-comment">{{ article_obj.comment_num }}&nbsp;&nbsp;</span>
</div> </div> <hr>
{% endfor %} </div>
{% endblock %} 文章详情前端代码
{% extends 'homePage.html' %} {% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 125px;
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;
}
.clear {
clear: both;
} .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;
}
.diggword {
margin-top: 5px;
margin-left: 0;
font-size: 12px;
color: #808080;
} </style>
{% endblock %} {% block title %}
{{ site_obj.site_title }}
{% endblock %} {% block content %}
<div class="col-md-2">
{% load mytag %}
{% mymenu username %}
</div>
<div class="col-md-10">
<h2 class="text-center"> {{ article_obj.title }}</h2>
{{ article_obj.content|safe }} <!--文章点赞点踩样式开始-->
<div class="clearfix">
<div id="div_digg">
<div class="diggit upordown">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit upordown">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div> <div class="clear"></div>
<span style="color:red" id="d1"> </span>
<div class="diggword" id="digg_tips"></div>
</div>
</div>
<!--文章点赞点踩样式结束--> {# 文章评论楼的渲染开始#}
<div class="comment_list">
<ul class="list-group">
{% for comment_obj in comment_list %}
<li class="list-group-item" >
<span><a href="">#{{ forloop.counter }}楼</a></span>
<span>{{ comment_obj.comment_time|date:'H:i' }}</span>
<span><a href="/{{ comment_obj.user.username }}/">{{ comment_obj.user.username }}</a></span> <p class="pull-right"><a href="#">引用&nbsp;&nbsp;</a></p>
<p class="pull-right"><a href="#" class="reply" comment_id="{{ comment_obj.pk }}" username="{{ comment_obj.user.username }}">回复&nbsp;&nbsp;</a></p>
<p>
{% if comment_obj.parent_id %}
@{{ comment_obj.parent.user.username }}
{% endif %}
</p> <p>
{{ comment_obj.content }}
</p>
</li>
{% endfor %}
</ul>
</div>
{# 文章评论楼的渲染结束#} {# 文章评论样式开始#}
{% if request.user.is_authenticated %}
<div class="comment_area">
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<textarea name="" id="comment" cols="30" rows="5" class="form-control"></textarea>
<button class="btn btn-warning" id="commentBtn">提交评论</button> </div>
{% else %}
<p>
<a href="/register/">注册</a>
<a href="/login/">登录</a>
</p>
{% endif %} {# 文章评论样式结束#}
</div>
{% endblock %} {% block js %}
<script>
//给点赞点踩图标绑定点击事件
$('.upordown').click(function () {
let currentEle = $(this);
let isUp = $(this).hasClass('diggit') //判断标签是否含有某个class值 从而二选一区分赞和踩
// 发送ajax请求
$.ajax({
url: '/up_or_down/', // 点赞点踩有一定的逻辑 单独开设接口处理
type: 'post',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'article_pk': '{{ article_obj.pk }}',
'is_up': isUp,
},
success:function (args) {
if (args.code === 10000){
{#currentEle.find('span').text(Number(currentEle.find('span').text()) + 1)#}
// 找一下span标签 用number转数字
{#currentEle.children().first().text(Number(currentEle.find('span').text()) + 1)#}
currentEle.children().first().text(Number(currentEle.children().first().text()) + 1)
}
$('#d1').html(args.msg)
}
})
}) // 提前创建一个全局变量用于存储评论主键值
let parentId = null;
// 给提交评论的按钮绑定点击事件
$('#commentBtn').click(function () {
// 获取用户评论的内容
let commentMsg = $('#comment').val();
let currentUserName= '{{ request.user.username }}'; let oldCommentMsg = commentMsg;
// 如果发送的是子评论 那么需要处理掉前缀内容(前端可以做 后端也可以做)
if(parentId){
commentMsg = commentMsg.slice(commentMsg.indexOf('\n')+1)
}
$.ajax({
url:'/comment/',
type:'post',
data:{
'csrfmiddlewaretoken': '{{ csrf_token }}',
'article_pk': '{{ article_obj.pk }}',
'content': commentMsg,
'parent_id':parentId,
},
success:function (args) {
if (args.code === 10000){
// 清空评论框内容
$('#comment').val('');
// 动态创建标签 并添加到评论楼中
let tempComment = `
<li class="list-group-item" >
<span class="glyphicon glyphicon-comment"><a href="/${currentUserName}/">${currentUserName}</a></span>
<p>
${oldCommentMsg}
</p>
</li>
`
// 查找ul标签 然后添加上述的标签内容即可
$('.list-group').append(tempComment)
// 清空全局变量
parentId = null;
}
}
})
}) // 给回复按钮绑定点击事件
$('.reply').click(function () {
// 获取回复按钮所在的评论用户名
let targetUserName = $(this).attr('username');
// 获取回复按钮所在的评论主键值 修改全局变量
parentId = $(this).attr('comment_id')
$('#comment').val('@'+ targetUserName + '\n').focus();
}) </script>
{% endblock %} 侧边栏模板前端代码 (减少侧边栏因功能展示而造成的代码冗余)
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">文章分类</h3>
</div>
<div class="panel-body">
{% for category_obj in category_queryset %}
<p><a href="/{{ site_obj.site_name }}/category/{{ category_obj.pk }}/">{{ category_obj.name }}({{ category_obj.article_num }})</a></p>
{% endfor %} </div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">文章标签</h3>
</div>
<div class="panel-body">
{% for tag_obj in tag_queryset %}
<p><a href="/{{ site_obj.site_name }}/tag/{{ tag_obj.pk }}/">{{ tag_obj.name }}({{ tag_obj.article_num }})</a></p>
{% endfor %} </div>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">日期归档</h3>
</div>
<div class="panel-body">
{% for date_obj in date_queryset %}
<p><a href="/{{ site_obj.site_name }}/archive/{{ date_obj.month|date:'Y-m' }}/">{{ date_obj.month|date:'Y年m月' }}({{ date_obj.article_num }})</a></p>
{% endfor %} </div>
</div> 侧边栏模板后端代码 (减少侧边栏因功能展示而造成的代码冗余)
单独创建一个templatetags文件 下创建mytag.py
from django import template
from app01 import models
from django.db.models import Count
from django.db.models.functions import TruncMonth register = template.Library() @register.inclusion_tag('leftmenu.html', name='mymenu')
def index(username):
site_obj = models.Site.objects.filter(site_name=username).first()
# 查询个人站点下所以的分类名称以及每个分类下的文章数
category_queryset = models.Category.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values(
'name', 'article_num', 'pk')
# 查询个人站点下所有的标签名称以及每个标签下的文章数
tag_queryset = models.Tag.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values(
'name', 'article_num', 'pk')
# 年月分组并统计文章个数
date_queryset = models.Article.objects.filter(site=site_obj).annotate(month=TruncMonth('create_time')).values(
'month').annotate(
article_num=Count('pk')).values('month', 'article_num')
return locals() 后端代码
def site_func(request, username, **kwargs):
"""
:param kwargs: 接收多余的关键字参数 代码通过该参数是否有值从而得出是个人站点还是侧边栏筛选
"""
# print(kwargs)
# 查询个人站点是否存在
site_obj = models.Site.objects.filter(site_name=username).first()
if not site_obj:
return render(request, 'errorPage.html')
# 查询个人站点下所有的文章
article_queryset = models.Article.objects.filter(site=site_obj)
if kwargs:
condition = kwargs.get('condition')
params = kwargs.get('params')
if condition == 'category':
article_queryset = article_queryset.filter(category_id=params)
elif condition == 'tag':
article_queryset = article_queryset.filter(tags__pk=params)
else: # 年-月
year, month = params.split('-')
article_queryset = article_queryset.filter(create_time__year=year, create_time__month=month)
'''如果文章较多 加分页器'''
return render(request, 'sitePage.html', locals()) def article_detail_func(request, username, article_id):
# 筛选某篇具体的文章对象
article_obj = models.Article.objects.filter(site__site_name=username).filter(pk=article_id).first()
site_obj = models.Site.objects.filter(site_name=username).first()
'''这里也可以添加健壮性校验 防止用户自己瞎传数据'''
# 获取当前文章所有的评论数据
comment_list = models.Comment.objects.filter(article=article_obj) return render(request, 'articleDetailPage.html', locals()) def up_or_down_func(request):
print(request.POST)
"""
1.校验用户是否登录
2.校验当前文章是否是当前用户自己的
3.校验当前文章是否已经被当前用户点过
4.创建点赞点踩记录(不要忘记文章表中的优化字段 同步自增)
"""
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
'''判断用户是否登录'''
if request.user.is_authenticated:
article_pk = request.POST.get('article_pk')
is_up = request.POST.get('is_up') # true 普通的字符串
article_obj = models.Article.objects.filter(pk=article_pk).first()
# 校验当前文章是否是当前用户自己的
if not article_obj.site.userinfo == request.user:
is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj)
if not is_click:
is_up = json.loads(is_up) # 自动转换成python中得布尔值
print(is_up, type(is_up))
if is_up:
models.Article.objects.filter(pk=article_pk).update(up_num=F('up_num') + 1)
back_dict['msg'] = '点赞成功'
else:
models.Article.objects.filter(pk=article_pk).update(down_num=F('down_num') + 1)
back_dict['msg'] = '点踩成功'
models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
else:
back_dict['code'] = 10001
back_dict['msg'] = '您已经点过了'
else:
back_dict['code'] = 10002
back_dict['msg'] = '你个臭不要脸 不能给自己点'
else:
back_dict['code'] = 10003
# from django.utils.safestring import mark_safe
# back_dict['msg'] = mark_safe('请先<a herf="/login/">登录</a>')
back_dict['msg'] = '请先<a href="/login/">登录</a>'
print(back_dict)
return JsonResponse(back_dict) @login_required
def comment_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
article_pk = request.POST.get('article_pk')
content = request.POST.get('content')
parent_id = request.POST.get('parent_id') # 直接获取即可 无需关心是否有值
models.Article.objects.filter(pk=article_pk).update(comment_num=F('comment_num') + 1)
models.Comment.objects.create(user=request.user, article_id=article_pk, content=content, parent_id=parent_id)
back_dict['msg'] = '评论成功'
return JsonResponse(back_dict)

BBS项目 未完待续的相关教程结束。

《BBS项目 未完待续.doc》

下载本文的Word格式文档,以方便收藏与打印。