django_博客练习(注册/登录/首页)
表设计
表分析
先确认表的数量 再确认表的基础字段 最后确认表的外键字段
1.用户表(基于auth模块设计扩展,手机号,头像,注册时间)
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
phone = models.BinaryField(null=True, verbose_name='手机号')
avatar=models.FileField(upload_to='avatar/',
default='avatar/default.jpg', verbose_name='用户头像')
register_time = models.DateTimeField
(auto_now_add=True, verbose_name='注册时间')
site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True)
# 外键字段 关联个人站点 一对一关系
setting文件中配置:AUTH_USER_MODEL = 'app01.UserInfo'
2.个人站点表(每个用户都有单独的博客站点)
from django.db import models
class Site(models.Model):
name = models.CharField(max_length=32)
# 站点名称
title = models.CharField(max_length=32)
# 站点标题
theme = models.CharField(max_length=32, null=True)
# 模拟站点的css文件
3.文章表(重点)
title = models.CharField(max_length=32)
# 文章标题
desc = models.CharField(max_length=255)
# 文章简介
content = models.TextField
# 文章内容
create_time = models.DateTimeField(auto_now_add=True)
# 发布时间
"""优化字段"""
comment_num = models.IntegerField(verbose_name='评论数', default=0)
# 文章评论数,在用户评论时 在这里字段也对应加1 这样就不用跨表查询提高效率
up_num = models.IntegerField(verbose_name='点赞数', default=0)
# 文章评论数,在用户评论时 在这里字段也对应加1 这样就不用跨表查询提高效率
down_num = models.IntegerField(verbose_name='反对数', default=0)
# 文章评论数,在用户评论时 在这里字段也对应加1 这样就不用跨表查询提高效率
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='ArticleAndTag',
through_fields=('article', 'tag'), null=True)
# 外键字段 文章标签表 一个文章可以有多个标签 一个标签也可以有多个文章
# 这里选择的是半自动创建 指定了关联的表和表字段
'''
数据库字段优化设计:我们想统计文章的评论数 点赞数
通过文章数据跨表查询到文章评论表中对应的数据统计即可
但是文章需要频繁的展示 每次都跨表查询的话效率极低
所以我们在文章表中再创建三个普通字段
之后只需要确保每次操作评论表或者点赞点踩表时同步修改上述三个普通字段即可
减少了跨表查询
'''
4.文章分类表
class Category(models.Model):
name = models.CharField(max_length=22)
# 设置分类的名称
site = models.ForeignKey(to='Site',on_delete=models.CASCADE,null=True)
# 分类名称 对应生效的个人站点
文章 和 标签的多对多表
class ArticleAndTag(models.Model):
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
# 文章外键
tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE, null=True)
# 标签外键
5.文章标签表
class Tag(models.Model):
name = models.CharField(max_length=32)
site = models.ForeignKey(to='Site',on_delete=models.CASCADE,null=True)
# 标签也是对应生效的个人站点 因为每个人的标签都不一样
6.文章点赞点踩表
class Up_and_Down(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='1是点赞还是0是反对')
# 记录用户对这篇文章的操作
7.文章评论表
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.CharField(max_length=255)
# 评论的内容
create_time = models.DateTimeField(auto_now_add=True)
# 评论的时间
parent = models.ForeignKey(to='self',on_delete=models.CASCADE,null=True)
"""to=self 自关联 """
# 如果有子评论 需要记录 例如 评论了id为1 的评论
"""
id user_id article_id content parent_id
1 1 1 哈哈哈 null
2 2 1 哈你妹 1
3 3 1 讲文明 2
"""
# 哈你妹就是回复的 id为1的评论
注册功能
1.创建一个forms组件 因为是前后端一体的项目,在django中推荐使用
创建一个forms的py文件 用来存储编写的forms类
from django import forms
from app01 import models
class Register(forms.Form):
username = forms.CharField(min_length=5, max_length=18, label='用户名',
error_messages={'min_length': '用户名不能低于5位',
'max_length': '用户名不能高于18位',
'required': '用户名不能为空'},
widget=forms.widgets.TextInput(attrs={'class':'form-control'}))
# 给forms标签添加样式
password = forms.CharField(min_length=5, max_length=18, label='密码',
error_messages={ 'min_length': '密码不能低于5位',
'max_length': '密码不能高于18位',
'required': '密码不能为空'},
widget=forms.widgets.PasswordInput(attrs={'class':'form-control'}))
password1 = forms.CharField(min_length=5, max_length=18, label='确认密码',
error_messages={
'min_length': '密码不能低于5位',
'max_length': '密码不能高于18位',
'required': '密码不能为空'},
widget=forms.widgets.PasswordInput(attrs={'class':'form-control'}))
email = forms.EmailField(error_messages={ 'required': '邮箱不能为空'},
widget=forms.TextInput(attrs={'class':'form-control'}),
# 给forms标签添加样式
label='邮箱')
def clean(self):
username = self.cleaned_data.get('username')
user_obj = models.UserInfo.objects.filter(username=username)
if user_obj:
self.add_error('username', '用户名已存在')
if not self.cleaned_data.get('password') == self.cleaned_data.get('password1'):
self.add_error('password1', '两次密码不一致')
return self.cleaned_data
# 设置一个全局钩子函数,判断用户名是否存在,判断两次密码是否输入一致
2.导入forms类到视图函数 传递forms类至html文件渲染标签
from app01.myforms import Register
def register_func(request):
back_dict = {'code':10000,'msg':''}
# 设置一个空字典,用于返回给前端 前端用来展示数据
register_obj = Register()
# 生成一个空的forms组件对象 传入前端渲染标签
if request.method == 'POST':
register_obj = Register(request.POST)
if register_obj.is_valid():
# 判断数据是否全部符合forms类的规则
clean_data = register_obj.cleaned_data
# 储存已通过校验的数据 这是一个字典 里面包含了所有forms类字段的数据
clean_data.pop('password1')
# 移除创建账户时不需要的数据
avatar_obj = request.FILES.get('avatar')
# 获取用户上传头像的文件
if avatar_obj:
# 判断用户是否有上传头像,因为上传头像选填
clean_data['avatar'] = avatar_obj
# 如果用户有上传,就把用户上传头像的文件添加入刚创建的forms数据中 以便稍等储存
# 如果用户没有上传 则不添加头像数据 数据库也有默认的头像
UserInfo.objects.create_user(**clean_data)
# 创建用户对象,利用**打散字段 成为 username = xxxx,password = xxx的形式
# UserInfo表中录入数据
back_dict['msg'] = '注册成功'
# 给前端返回的字典添加数据,
back_dict['url'] = '/login/'
# 给前端返回的字典添加数据,
else:
back_dict['msg'] = register_obj.errors
# 给前端返回的字典添加数据,这里是不通过时 返回的错误信息
back_dict['code'] = 10001
# 错误时的id号 前端可以通过此id号判断正常还是异常
return JsonResponse(back_dict)
# 把字典返回给前端
return render(request, 'registerPage.html', locals())
3.前端页面接收数据反馈数据信息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册</title>
<!--设置页面小标题-->
<meta name='keywords' content="查询关键字,可以填写多个">
<meta name="description" content="网页简介">
{% load static %}
<!--动态加载配置文件-->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.css' %}">
<!--动态加载配置文件-->
</head>
<body>
<div class="row">
<div class="col-md-4 col-md-offset-4" style="margin-top: 100px">
<!--设定一块区域,距离顶部100px-->
<h2 class="text-center">注册</h2>
<form id="form">
{% csrf_token %}
<!--在form标签内添加 csrf_token 验证-->
{% for foo in register_obj %}
<!--对于后端返回的forms类for循环展示渲染标签-->
<div class="form-group">
<label for="{{ foo.auto_id }}">{{ foo.label }}</label>
<!--绑定标签id,展示标签别名-->
{{ foo }}
<!--渲染输入标签-->
<span style="color: #f15050;font-size: 13px"></span>
<!--错误提示标签-->
</div>
{% endfor %}
<div class="form-group">
<p><label for="myfile">头像</label></p>
<label for="myfile">
<img src="/static/img/001.png" alt="" width="100" id="myimg">
<!--设置一个待用户上传头像图标,并且绑定选择区域 绑定ID为myfile的上传
文件标签,-->
</label>
<p style="margin-top: 10px"><span style="color: #999999;font-size: 12px">
点击头像上传</span></p>
<input type="file" id="myfile" style="display: none">
<!--设置一个接收文件标签,用来接收用户上传的头像文件,并把这个标签隐藏,
因为头像图标继承了他,所以点击头像图标也是触发这个标签-->
</div>
<input type="button" id="subBtn" class="btn btn-success btn-block" value="注册">
<!--设置一个空的标签,后面会通过绑定ajax事件传递数据-->
</form>
</div>
</div>
</body>
</html>
<script>
<!--给头像图标绑定一个变化事件,只要头像图标有变化就会执行,上传头像实时展示-->
$('#myfile').change(function () {
let myFileReaderobj = new FileReader();
<!--1.产生一个文件阅读器对象-->
let fileObj = this.files[0];
<!--获取用户上传的头像文件-->
myFileReaderobj.readAsDataURL(fileObj);
<!--将头像文件交给文件阅读器对象读取-->
myFileReaderobj.onload = function () {
<!--.onload 是等待文件阅读器对象读取完成后在进行的意思-->
$('#myimg').attr('src', myFileReaderobj.result)
<!--给头像图标标签更换图片,修改图片标签的src属性-->
}})
<!--这样就完成了一个头像实时展示的功能-->
<!--给按钮标签绑定单击事件,发送ajax请求,并携带文件数据-->
$('#subBtn').click(function () {
let myFormData = new FormData();
<!--1.先产生一个内置对象,空的数据对象-->
$.each($('#form').serializeArray(), function (index, data) {
<!--2.for循环获取#form标签内的所有普通字段数据-->
myFormData.append(data.name, data.value)})
<!--3.for循环添加所有普通字段数据 到 数据对象中-->
myFormData.append('avatar', $('#myfile')[0].files[0])
<!--4.最后获取form标签内文件数据,也添加到数据对象中-->
$.ajax({
<!--5.发送ajax请求-->
url: '',
type: 'post',
data: myFormData,
<!--6.把刚刚携带所有数据的数据对象发送出去-->
contentType: false,
<!--固定格式,携带文件数据都需要-->
processData: false,
<!--固定格式,携带文件数据都需要-->
success: function (args) {
<!--接收后端返回数据-->
<!--ajax请求后端返回一个字典,这个字典就是args接收了-->
if (args.code === 10000) {
<!--通过后端返回的id号码判断是否注册成功-->
alert('注册成功')
window.location.href = args.url
} else {
let data_obj = args.msg;
<!--接收后端字典返回的错误信息-->
$.each(data_obj, function (k, v) {
<!--for循环 拿到错误信息对于的标签名k,和错误信息内容v-->
let eleId = '#id_' + k
<!--生成具体标签id 因为前端forms渲染标签id都是id_字段名-->
$(eleId).next().text(v[0]).parent().addClass('has-error')
<!--找到具体标签,然后.next()找到标签下我们提前设置好的span标签,然后给span标签添加文本错误信息,然后.parent()顺便更改输入框的边框颜色.addClass('has-error') -->
})
}
}
})
})
<!--给所有输入框绑定事件,当聚焦时-->
$('input').focus(function () {
$(this).next().text('').parent().removeClass('has-error')
<!--清除输入框的子span标签内的错误信息,并移除输入框的边框颜色-->
})
</script>
注册功能完成
登录功能(验证码源码)
前端代码
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" name="username" id="username">
# 用户输入用户名
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="text" class="form-control" name="password" id="password">
# 用户输入用户名
</div>
<div class="form-group">
<label>验证码</label>
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" id="code">
# 用户输入验证码
</div>
<div class="col-md-4">
<img src="/code_img/" alt="" width="140" height="35" id="code_img">
# 重要:图片的src可以填写为一个路径,刷新会自动提交get请求访问这个路由
# 这个路由方法后端可以产生一个验证码的图片
</div>
</div>
</div>
生成验证码图片后端代码
from PIL import Image, ImageDraw, ImageFont
# pillow模块,用来处理图片相关
from io import BytesIO, StringIO
# io模块用来储存
import random
def choice_num():
return random.randint(1, 255), random.randint(1, 255), random.randint(1, 255)
# 使用随机数模块 生成一个随机色号
def code_img_func(request):
img_obj = Image.new('RGB', (140, 35), choice_num())
# 生成一个图片 (颜色模式,图片大小,色号)
draw_obj = ImageDraw.Draw(img_obj)
# 将图片交给图片画笔工具 生成画笔对象
font_obj = ImageFont.truetype('static/LOVE.ttf', 28)
# 设置文字对象 字符 和 大小
code = ''
# 生成一个空的验证码
for i in range(5):
# 循环5次就是5位数验证码
random_lower = chr(random.randint(97, 122))
random_int = str(random.randint(1, 9))
temp_choice = random.choice([random_lower, random_int])
# 随机选择一个随机生成的数字或字母
draw_obj.text((i * 20 + 20, 0), temp_choice, font=font_obj)
# 使用画笔对象进行填写文字 (文字坐标,文字内容,文字字体)
code += temp_choice
# 添加入空的验证码
request.session['code'] = code
# 生成后的验证码加入request.session中 以便之后比对
io_obj = BytesIO()
# 生成一个二进制的文件
img_obj.save(io_obj, 'png')
# 保存文件 已 png的形式
return HttpResponse(io_obj.getvalue())
# 将文件数据返回给前端渲染
前端点击验证码刷新
$('#code_img').click(function () {
let oldSrc = $(this).attr('src');
$(this).attr('src',oldSrc + '?')
})
// 找到图片标签对象 绑定点击事件
// 因为每次图片的src只要发生变化就会自动再次发送get请求,发送一个get请求就自然会
// 触发一个生产验证码视图函数 所以我们只要想办法点击一次让图片src变一次就好了
// 点击后触发 更改图片标签src 在原来的src上面加一个?即可
前端发送数据给后端验证
发送ajax请求给后端验证
携带所有输入框的值
$('#subBtn').click(function () {
$.ajax({
url: '',
type: 'post',
data: {
'username': $('#username').val(),
'password': $('#password').val(),
'code': $('#code').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code ===10000){
alert(args.msg)
window.location.href = args.url
}else {
$('#span').text(args.msg)
}}})})
后端返回一个字典,通过判断后端返回字典的状态码 区分是否验证通过,
验证通过则跳转页面
验证失败则打印失败原因
后端验证数据
接受ajax的post请求 拿到数据比对
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')
user_obj = auth.authenticate(request, username=username, password=password)
# 利用auth模块比对是否有这个有这个用户
if not code == request.session.get('code'):
back_dict['code'] = 10001
back_dict['msg'] = '验证码错误'
return JsonResponse(back_dict)
if not user_obj:
back_dict['code'] = 10001
back_dict['msg'] = '用户名或密码错误'
else:
auth.login(request, user_obj)
back_dict['msg'] = '登录成功'
back_dict['url'] = '/home/'
return JsonResponse(back_dict)
# 针对不同的状态添加不同的提示和状态码
# 将字典返回给前端 展示信息
return render(request, 'loginPage.html', locals())
开发资源
因为要展示用户头像 用户上传图片等资源 需要配置
否则前端无法展示
在项目下建立media文件夹
settings.py
文件中配置开放文件目录
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
这样media文件夹下的资源都可以被前端暴露了
首页搭建
首页前端需要展示 导航栏
所有文章
编辑导航栏前端代码
通过后端request属性判断用户是否登录
<nav class="navbar navbar-default navbar-fixed-top" style="margin-bottom: 100px">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<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="/home/">首页<span class="sr-only">(current)</span></a></li>
<li><a href="#">Link</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><img src="/media/{{ request.user.avatar }}" alt=""
style="width: 40px;margin-top: 6px;height: 40px;border-radius: 50%;"></li>
<li><a href="#">{{ request.user.username }}</a></li>
<li><a href="#" data-toggle="modal" data-target="#myModal">修改头像</a></li>
<li><a href="#">修改密码</a></li>
<li><a href="/backend/" data-toggle="modal" data-target="#myModal">管理文章</a></li>
<li><a href="/logout/">退出</a></li>
{% else %}
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
内容区域所有文章展示
通过后端返回所有文章对象列表
并添加分页器
def home_func(request):
article_obj_list = models.Article.objects.all()
page_obj = mypage.Pagination(current_page=request.GET.get('page'),
all_count=article_obj_list.count())
# 生成分页器对象 传递参数 页面信息 列表文章总数
page_queryset = article_obj_list[page_obj.start:page_obj.end]
# 生成页面信息列表 对总文章个数进行取值
return render(request, 'homePage.html', locals())
前端在指定位置渲染分页器
{{ page_obj.page_html|safe }}
分页器源代码
建立py单独py文件 然后引用到视图函数中
class Pagination(object):
def __init__(self, current_page, all_count, per_page_num=8, pager_count=7):
"""
封装分页相关数据
:param current_page: 当前页
:param all_count: 数据库中的数据总条数
:param per_page_num: 每页显示的数据条数
:param pager_count: 最多显示的页码个数
"""
try:
current_page = int(current_page)
except Exception as e:
current_page = 1
if current_page < 1:
current_page = 1
self.current_page = current_page
self.all_count = all_count
self.per_page_num = per_page_num
# 总页码
all_pager, tmp = divmod(all_count, per_page_num)
if tmp:
all_pager += 1
self.all_pager = all_pager
self.pager_count = pager_count
self.pager_count_half = int((pager_count - 1) / 2)
@property
def start(self):
return (self.current_page - 1) * self.per_page_num
@property
def end(self):
return self.current_page * self.per_page_num
def page_html(self):
# 如果总页码 < 11个:
if self.all_pager <= self.pager_count:
pager_start = 1
pager_end = self.all_pager + 1
# 总页码 > 11
else:
# 当前页如果<=页面上最多显示11/2个页码
if self.current_page <= self.pager_count_half:
pager_start = 1
pager_end = self.pager_count + 1
# 当前页大于5
else:
# 页码翻到最后
if (self.current_page + self.pager_count_half) > self.all_pager:
pager_end = self.all_pager + 1
pager_start = self.all_pager - self.pager_count + 1
else:
pager_start = self.current_page - self.pager_count_half
pager_end = self.current_page + self.pager_count_half + 1
page_html_list = []
# 添加前面的nav和ul标签
page_html_list.append('''
<nav aria-label='Page navigation>'
<ul class='pagination'>
''')
first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
page_html_list.append(first_page)
if self.current_page <= 1:
prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
else:
prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)
page_html_list.append(prev_page)
for i in range(pager_start, pager_end):
if i == self.current_page:
temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
else:
temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
page_html_list.append(temp)
if self.current_page >= self.all_pager:
next_page = '<li class="disabled"><a href="#">下一页</a></li>'
else:
next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
page_html_list.append(next_page)
last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
page_html_list.append(last_page)
# 尾部添加标签
page_html_list.append('''
</nav>
</ul>
''')
return ''.join(page_html_list)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了