BBS项目
表设计
一个项目中最重要的不是业务逻辑的书写
而是前期的表设计,只要将表设计好了,后续的功能书写才会一帆风顺
'''
# bbs表设计
1.用户表
继承AbstractUser
扩展字段:
phone :电话号码
avatar :用户头像
create_time :创建时间
外键字段:
一对一个人站点表
2.个人站点表
site_name:站点名称
site_title;站点标题
site_theme:站点样式
外键字段:
一对多个人站点
3.文章标签表
name:标签名
外键字段:
一对多个人站点
4.文章分类表
name:分类名
外键字段:
一对多个人站点
5.文章表
title:文章标题
desc:文章简介
content:文章内容
create_time:发布时间
# 数据库字段设计优化(虽然下面的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率比较低下)
'''所以在文章表添加下面的三个字段,添加普通字段同步更新,减少跨表查询次数'''
up_num :点赞数
down_num :点踩数
comment_num :评论数
外键字段:
一对多个人站点
多对多文章标签
一对多文章分类
6.点赞点踩表
记录哪个用户给哪篇文章点了赞还是点了踩
user :用户 ForeignKey(to="User")
article :文章 ForeignKey(to="Article")
is_up :是否点赞 BooleanField()
7.文章评论表
记录哪个用哪个户给哪篇文章写了哪些评论内容
user ForeignKey(to="User")
article ForeignKey(to="Article")
content CharField()
comment_time DateField()
parent ForeignKey(to="Comment",null=True) # 自关联
# parent ForeignKey(to="self",null=True) # orm提供的语法自关联
评论分为根评论和子评论(可以评论别人评论的)
根评论:是评论文章内容的评论
字评论:是回复别人的评论
# 外键关系:是一对多的
'''
数据库表创建及同步
'''因为django自带的数据库不好用, 本次项目使用的是MySql数据库'''
from django.db import models
from django.contrib.auth.models import AbstractUser
# 用户表
class UserInfo(AbstractUser):
phone = models.CharField(verbose_name='手机号', max_length=32, blank=True)
# 头像
avatar = models.FileField(verbose_name='头像', upload_to='static/img', default='static/img/public.png')
'''
给avatar字段传文件对象,该文件会自动存储到avatar文件下,然后avatar字段只保存文件路径
'''
create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
# 一对一站点表:用户表和站点表是一对一关系
blog = models.OneToOneField(verbose_name='关联站点表', to='Blog', null=True)
def __str__(self):
return self.username
class Meta:
verbose_name_plural = '用户表'
# 站点表
class Blog(models.Model):
site_name = models.CharField(verbose_name='站点名称', max_length=64)
site_title = models.CharField(verbose_name='站点标题', max_length=64)
site_theme = models.CharField(verbose_name='站点样式', max_length=64) # 存css/js文件路径
class Meta:
verbose_name_plural = '站点表'
def __str__(self):
return self.site_name
# 标签表
class Tag(models.Model):
name = models.CharField(verbose_name='文章标签', max_length=32)
# 一对多站点表:标签表和站点表是一对多关系
blog = models.ForeignKey(verbose_name='关联站点表', to='Blog')
def __str__(self):
return self.name
class Meta:
verbose_name_plural = '标签表'
# 分类表
class Category(models.Model):
name = models.CharField(verbose_name='分类名称', max_length=32)
# 一对多站点表:分类表和站点表是一对多关系
blog = models.ForeignKey(verbose_name='关联站点表', to='Blog')
def __str__(self):
return self.name
class Meta:
verbose_name_plural = '分类表'
# 文章表
class Article(models.Model):
title = models.CharField(verbose_name='文章标题', max_length=128)
desc = models.CharField(verbose_name='文章简介', max_length=512)
content = models.TextField(verbose_name='文章内容')
create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
# 优化字段
up_num = models.BigIntegerField(verbose_name='点赞数', default=0)
down_num = models.BigIntegerField(verbose_name='点踩数', default=0)
comment_num = models.BigIntegerField(verbose_name='评论数', default=0)
# 外键关系
# 一对多站点表:文章表和站点表是一对多关系
blog = models.ForeignKey(verbose_name='关联站点表', to='Blog')
tags = models.ManyToManyField(to='Tag', through='Article2Tag',
through_fields=('article', 'tag'),
verbose_name='关联第三张表'
)
category = models.ForeignKey(verbose_name='关联分类表', to='Category')
def __str__(self):
return self.title
class Meta:
verbose_name_plural = '文章表'
# 自己创建的第三张表,用于扩建字段
class Article2Tag(models.Model):
article = models.ForeignKey(to='Article')
tag = models.ForeignKey(to='Tag')
class Meta:
verbose_name_plural = '第三张表'
# 点赞点踩表
class UpAndDown(models.Model):
user = models.ForeignKey(verbose_name='关联用户表', to='UserInfo')
article = models.ForeignKey(verbose_name='关联文章表', to='Article')
is_up = models.BooleanField() # 存的是0/1
class Meta:
verbose_name_plural = '点赞点踩表'
# 评论表
class Comment(models.Model):
user = models.ForeignKey(verbose_name='关联用户表', to='UserInfo')
article = models.ForeignKey(verbose_name='关联文章表', to='Article')
content = models.CharField(verbose_name='评论内容', max_length=512)
create_time = models.DateTimeField(verbose_name='评论时间', auto_now_add=True)
# 根评论和子评论一对多的关系,自关联
parent_id = models.ForeignKey(to='self', null=True) # self就是关联自己表
class Meta:
verbose_name_plural = '评论表'
总路由
from django.conf.urls import url
from django.contrib import admin
from app01 import views
from django.views.static import serve
from BBS1 import settings
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 注册功能
url(r'^register/', views.register, name='reg'),
# 登录功能
url(r'^login/', views.login, name='lo'),
# 图片验证码
url(r'^get_code/', views.get_code, name='gc'),
# 首页
url(r'^home/', views.home),
url(r'^$', views.home),
# 修改密码
url(r'^set_password/', views.set_password, name='set_pwd'),
# 退出登录
url(r'^logout/', views.logout, name='out'),
# 点赞点踩
url(r'^up_or_down/', views.up_or_down),
# 评论功能
url(r'^comment/', views.comment),
# 侧边栏筛选功能
url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/', views.site),
# 文章详情页
url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/', views.archive_detail),
# 个人站点
url(r'(?P<username>\w+)/$', views.site, name='site'),
# 开设后端文件夹资源
url(r'media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}),
]
注册功能
'''
如果自己的项目只用到一个forms组件,那么可以创建一个py文件书写
但是,项目需要使用多个forms组件,建议创建一个文件夹在文件夹根据forms组件功能的不同创建不同的py文件
'''
# 书写针对用户表的forms组件代码
from django import forms
from app01 import models
class MyRegForm(forms.Form):
username = forms.CharField(label='用户名', min_length=3, max_length=8,
error_messages={
'required': '用户名不能为空',
'min_length': '用户名最少3位',
'max_length': '用户名最大8位'
},
widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(label='密码', min_length=3, max_length=8,
error_messages={
'required': '密码不能为空',
'min_length': '密码名最少3位',
'max_length': '密码名最大8位'
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(label='确认密码', min_length=3, max_length=8,
error_messages={
'required': '确认密码不能为空',
'min_length': '确认密码名最少3位',
'max_length': '确认密码名最大8位'
},
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')
# 去数据库中校验
is_exist = models.UserInfo.objects.filter(username=username)
if is_exist:
# 提示信息
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
实时展示头像
<script>
$('#myfile').change(function () {
# 1.生成一个文本阅读器对象
let myFileReadweObj = new FileReader();
# 2.获取用户上传的头像
let fileObj = $(this)[0].files[0]; # this:当前被操作的对象,绑定的id
# 3.将文件对象交给阅读器对象读取
myFileReadweObj.readAsDataURL(fileObj); # 异步操作,io操作
# 4.利用文件阅读器将文件展示到前端页面,修改src属性
# 4.1 因为是异步操作,等待文件阅读器加载完毕之后在执行
myFileReadweObj.onload = function(){
# 4.2等待读取完毕,在执行下面的代码
$('#myimg').attr('src', myFileReadweObj.result)
};
})
</script>
注册页面搭建
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.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>#}
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1 class="text-center" style="color: #8a6d3b">注册页面</h1>
<form action="" id="myform">
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group">
{# 拿到input框id值#}
<label for="{{ form.auto_id }}">{{ form.label }}</label>
{{ form }}
<span style="color: red" class="pull-right">{{ form.errors.0 }}</span>
</div>
{% endfor %}
<div class="form-group">
<label for="myfile">头像
{% load static %}
<img src="{% static 'img/public.png' %}" id="myimg" alt="" style="margin-left: 10px"
width="80px">
</label>
<input type="file" id="myfile" name="avatar" style="display: none">
</div>
<input type="button" value="注册" class="btn btn-primary pull-right" id="id_commit">
</form>
</div>
</div>
</div>
<script>
$('#myfile').change(function () {
// 1.生成一个文本阅读器对象
let myFileReadweObj = new FileReader();
// 2.获取用户上传的头像
let fileObj = $(this)[0].files[0]; // this:当前被操作的对象,绑定的id
// 3.将文件对象交给阅读器对象读取
myFileReadweObj.readAsDataURL(fileObj); //异步操作,io操作
// 4.利用文件阅读器将文件展示到前端页面,修改src属性
// 4.1 因为是异步操作,等待文件阅读器加载完毕之后在执行
myFileReadweObj.onload = function () {
// 等待读取完毕,在执行下面的代码
$('#myimg').attr('src', myFileReadweObj.result)
};
});
// 绑定点击事件
$('#id_commit').on('click', function () {
// 1.生成一个FormData对象
let formDataObj = new FormData();
// 2.向该对象中添加数据(支持普通数据和文件数据)
{#console.log($('#myform').serializeArray()); // 用这个方法可以获取,form表单中所有的普通数据#}
// 2.1循环出所有的普通数据并添加,index,obj分别是索引和数据对象
$.each($('#myform').serializeArray(), function (index, obj) {
formDataObj.append(obj.name, obj.value);
});
// 2.2 添加文件数据
formDataObj.append('avatar', $('#myfile')[0].files[0]);
// 3.发送ajax请求
$.ajax({
url: '',
type: 'post',
data: formDataObj,
// 传文件必指定的两个参数
contentType: false,
processData: false,
success:function (args) {
if (args.code==1000){
// 跳转到登录界面
window.location.href = args.url
}else {
// 如何将对应的错误提示展示对应的input框下面
// forms组件渲染的标签的id都是:id_字段名, each:输出每一个文本
$.each(args.msg, function (index,obj) {
{#console.log(index,obj) // username ["用户名不能为空"]#}
{#拼接成:id="id_password"#}
let targetId = '#id_' + index;
// 拿到span错误标签,并展示提示信息
$(targetId).next().text(obj[0]).parent().addClass('has-error') // 现在是span表标签
})
}
}
})
});
// 给所有的input框获取焦点事件
$('input').focus(function () {
// 将input框下面的span标签和input外面的div标签修改内容及属性
$(this).next().text('').parent().removeClass('has-error')
// $(this):当前input, .text:span标签,parent:div标签,当点击时,去掉提示信息和样式
})
</script>
</body>
</html>
注册功能实现
from django.shortcuts import render
from app01.myforms import MyRegForm
from app01 import models
from django.http import JsonResponse
# Create your views here.
def register(request):
'''前后端交互,最好定义一个字典'''
back_dic = {'code': 1000, 'msg': ''}
# 1.生成一个不传参数的对象
form_obj = MyRegForm()
# 2.接收前端数据
if request.method == 'POST':
# 检测数据是否合法,并把数据添加到空对象中
form_obj = MyRegForm(request.POST)
# 3.判断数据是否合法
if form_obj.is_valid():
# print(form_obj.cleaned_data) # {'username': 'meng', 'password': '111', 'confirm_password': '111', 'email': '123@qq.com'}
# 将校验通过的数值赋值给一个变量
clean_data = form_obj.cleaned_data
# 将字典里的confirm_password键值对删除
clean_data.pop('confirm_password') # pop() 函数用于移除列表中的一个元素
# 获取用户头像
file_obj = request.FILES.get('avatar')
'''针对用户头像一定要判断是否传值,不能直接添加到字典里'''
if file_obj:
clean_data['avatar'] = file_obj # 添加到字典里
# 4.保存到数据库里,以普通用户的形式存入
models.UserInfo.objects.create_user(**clean_data)
back_dic['url'] = '/login/'
else:
back_dic['code'] = 2000
back_dic['msg'] = form_obj.errors
return JsonResponse(back_dic)
return render(request, 'register.html', locals())
'''
注册功能总结:
1.先搭建一个注册页面,并渲染出基本功能
2.给注册按钮绑定点击事件,发送ajax请求
3.根据返回来的数据,做出相应的逻辑判断
4.美化注册功能样式
'''
登录功能
'''书写img标签的src属性: 1.图片路径 2.src 3.图片二进制数据'''
随机验证码功能
'''
图片相关的模块:
pip3 install pillow
'''
from PIL import Image, ImageDraw, ImageFont
'''
Image;生成图片
ImageDraw:能够在图片上乱涂乱花
ImageFont:控制字体的样式
'''
from io import BytesIO, StringIO
'''
内存管理器模块
BytesIO:临时存储数据,返回的数据是二进制
StringIO:临时存储数据,返回的数据是字符串
'''
import random
def get_random():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def get_code(request):
img_obj = Image.new('RGB', (430, 35), get_random()) # 先产生一张图片
img_draw = ImageDraw.Draw(img_obj) # 产生一个画笔对象
img_font = ImageFont.truetype('static/font/111.ttf', 30) # 字体样式
# 随机验证码
code = ''
for i in range(4):
random_upper = chr(random.randint(65, 90))
random_lower = chr(random.randint(97, 122))
random_int = str(random.randint(0, 9))
# 从上面随机选择一个
tmp = random.choice([random_upper, random_lower, random_int])
# 将产生随机字符串写入到图片上
img_draw.text((i * 60 + 60, -2), tmp, get_random(), img_font)
# 拼接随机字符串
code += tmp
print(code)
# 把随机验证码存储起来,方便其他视图函数比对使用
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
*********************html*************************
# 点击图片刷新验证码
<script>
$('#id_img').on('click', function () {
// 1. 先获取标签之前的src
let oldVal = $(this).attr('src');
$(this).attr('src', oldVal += '?')
})
</script>
'''利用的是前端url更改就会,重新发送一遍get请求,达到刷新的效果'''
搭建登录功能页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1 class="text-center" style="color: #1E9FFF">登录</h1>
<div class="form-group">
<label for="username">用户名</label>
<input type="text" name="username" id="id_username" class="form-control">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" id="id_password" class="form-control">
</div>
<div class="form-group">
<label for="">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" name="code" id="id_code" class="form-control">
</div>
<div class="col-md-6">
<img src="/get_code/" alt="" width="480px" height="34px" id="id_img">
</div>
</div>
</div>
<input type="button" class="btn btn-primary pull-right" value="登录" id="id_commit">
<span style="color: red" id="error"></span>
</div>
</div>
</div>
<script>
// 点击图片更新验证码
$('#id_img').on('click', function () {
// 1. 先获取标签之前的src
let oldVal = $(this).attr('src');
$(this).attr('src', oldVal += '?')
});
// 给登录按钮绑定点击事件
$('#id_commit').click(function () {
$.ajax({
url:'',
type:'post',
data: {
'username':$('#id_username').val(),
'password':$('#id_password').val(),
'code':$('#id_code').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success:function (args) {
if (args.code == 1000){
// 直接跳转到首页
window.location.href = args.url
}else {
// 渲染错误信息
$('#error').text(args.msg)
}
}
})
})
</script>
</body>
</html>
登录功能实现
def login(request):
back_dic = {'code': 1000, 'msg': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
# 1.统一转成小写,校验验证码是否正确
if request.session.get('code').lower() == code.lower():
# 2.校验用户名和密码是否正确
user_obj = auth.authenticate(request, username=username, password=password)
if user_obj:
# 保存用户状态
auth.login(request, user_obj)
back_dic['url'] = '/home/'
else:
back_dic['code'] = 2000
back_dic['msg'] = '用户名或者密码错误'
else:
back_dic['code'] = 3000
back_dic['msg'] = '验证码错误'
return JsonResponse(back_dic)
return render(request, 'login.html')
首页
修改密码功能
from django.contrib.auth.decorators import login_required # 登录认证模块
@login_required
def set_password(request):
back_dic = {'code': 1000, 'msg': ''}
if request.method == 'POST':
old_password = request.POST.get('old_password')
new_password = request.POST.get('new_password')
confirm_password = request.POST.get('confirm_password')
# 校验原密码是否正确
is_right = request.user.check_password(old_password)
if is_right:
if new_password == confirm_password:
request.user.set_password(new_password)
request.user.save()
back_dic['msg'] = '修改成功'
back_dic['url'] = '/login/'
else:
back_dic['code'] = 1001
back_dic['msg'] = '两次密码不一致'
else:
back_dic['code'] = 1002
back_dic['msg'] = '原密码错误'
return JsonResponse(back_dic)
退出登录
@login_required
def logout(request):
auth.logout(request)
return redirect('/home/')
admin后台管理
'''
django提供了一个可视化的界面,用来对模型表进行数据的增删改查
如果想要使用admin后台管理操作模型表
必须先去admin.py注册需要操作的哪些表
admin会给每一个注册了的模型表自动生成增删改查四条url
'''
# admin.py
from django.contrib import admin
from app01 import models
# Register your models here.
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Tag)
admin.site.register(models.Category)
admin.site.register(models.Article)
admin.site.register(models.Article2Tag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)
# admin默认展示英文名称,想要变成中文,去models中定义一个类
class Meta:
verbose_name_plural = '用户表'
'''
自己录入数据顺序:
文章表
用户表
标签表
标签和文章表
'''
用户头像展示
'''
1.网址所使用的静态文件默认放在static文件夹下
2.用户上传的静态文件也应该单独放在一个文件夹下
'''
# media配置
该配置可以让用户上传的所有文件都固定存放在某一个指定的文件下
# 1.配置用户上传的文件存储位置
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 会自动创建多级目录
# 2.开设后端指定文件夹资源
需要去urls.py配置
from django.views.static import serve
from BBS1 import settings
# 开设后端文件夹资源
url(r'media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}) # 固定写法
# home.py
<img class="media-object" src="/media/{{ article_obj.blog.userinfo.avatar }}" alt="..." width="80px" > # 拼接路径
首页页面搭建
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
</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="#">BBS</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 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="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<!--导航条判断是否登录后,才展示的功能-->
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#" data-toggle="modal" data-target="#myModal">修改密码</a></li>
<li><a href="#">修改头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="{% url 'out' %}">退出登录</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'reg' %}">注册</a></li>
<li><a href="{% url 'lo' %}">登录</a></li>
{% endif %}
<!--修改密码,触发模态框-->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog"
aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h1 class="text-center">修改密码</h1>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="form_group">
<label for="">用户名</label>
<input type="text" disabled value="{{ request.user.username }}"
class="form-control">
</div>
<div class="form_group">
<label for="">原密码</label>
<input type="password" id="old_password" class="form-control">
</div>
<div class="form_group">
<label for="">新密码</label>
<input type="password" id="new_password" class="form-control">
</div>
<div class="form_group">
<label for="">确认密码</label>
<input type="password" id="confirm_password" class="form-control">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="id_edit">修改</button>
<span style="color: red" id="password_error"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">焦点</h3>
</div>
<div class="panel-body">
今日热点
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">焦点</h3>
</div>
<div class="panel-body">
今日热点
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">焦点</h3>
</div>
<div class="panel-body">
今日热点
</div>
</div>
</div>
<div class="col-md-8">
{% for article_obj in article_queryset %}
<div class="media">
<h4 class="media-heading"><a href="#">{{ article_obj.title }}</a></h4>
<div class="media-left">
<a href="#">
<!--实时展示头像-->
<img class="media-object" src="/media/{{ article_obj.blog.userinfo.avatar }}" alt="..." width="80px" >
</a>
</div>
<div class="media-body">
{{ article_obj.desc }}
</div>
<br>
<div>
{# xiaxveliang 2021-12-15 18:59 0 0 33#}
<span><a href="#">{{ article_obj.blog.userinfo.username }}</a> </span>
<span>{{ article_obj.create_time|date:'Y-m-d H:i' }} </span>
<span><span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }} </span>
<span><span class="glyphicon glyphicon-thumbs-down"></span> {{ article_obj.down_num }}</span>
</div>
</div>
<hr>
{% endfor %}
</div>
<div class="col-md-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">焦点</h3>
</div>
<div class="panel-body">
今日热点
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">焦点</h3>
</div>
<div class="panel-body">
今日热点
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">焦点</h3>
</div>
<div class="panel-body">
今日热点
</div>
</div>
</div>
</div>
</div>
<script>
$('#id_edit').click(function () {
$.ajax({
url: '/set_password/',
type: 'post',
data: {
'old_password': $('#old_password').val(),
'new_password': $('#new_password').val(),
'confirm_password': $('#confirm_password').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code == 1000) {
window.location.reload();
console.log(args.msg)
} else {
$('#password_error').text(args.msg)
}
}
})
})
</script>
</body>
</html>
首页功能实现
def home(request):
# 查询网站所有的文章
article_queryset = models.Article.objects.all()
return render(request, 'home.html', locals())
个人站点
图片防盗链
# 如何避免别的网站直接通过本网站的url访问本网站资源
# 简单的防盗
原理:
判断请求是从哪个网站发出的,如果是本网站正常访问,如果是其他网站直接拒绝
请求头里面有一个专门记录请求来自于哪个网址的参数:refer
Referer: http://127.0.0.1:8001/ # 不是自己的网站请求,直接拒绝
个人站点css样式
# 在static文件夹下css根据站点名不同创建css样式py文件
然后在页面上引入:
<link rel="stylesheet" href="/static/css/{{ blog.site_theme }}/"> # 动态引入
侧边栏展示功能
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-3">
<!--侧边栏展示-->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">文章分类</h3>
</div>
<div class="panel-body">
{% for category in category_list %}
<p><a href="">{{ category.0 }}({{ category.1 }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">文章标签</h3>
</div>
<div class="panel-body">
{% for tag in tag_list %}
<p><a href="">{{ tag.0 }}({{ tag.1 }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title" style="color: #5bc0de">日期归档</h3>
</div>
<div class="panel-body">
{% for date in date_list %}
<p><a href="">{{ date.0|date:'Y年m月' }}({{ date.1 }})</a></p>
{% endfor %}
</div>
</div>
</div>
</body>
from django.db.models import Count
from django.db.models.functions import TruncMonth
def site(request, username):
# 1.先校验个人站点是否存在
user_obj = models.UserInfo.objects.filter(username=username).first()
# 2.用户不存在,返回一个404页面
if not user_obj:
return render(request, '404.html')
blog = user_obj.blog # 获取个人站点
# 3.查询当前个人站点的所有文章
article_list = models.Article.objects.filter(blog=blog)
# 侧边栏搭建
# 1.查询当前用户所有的分类已经分类下的文章数
category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name', 'count_num', 'pk') # 当前用户所有的分类
# print(category_list) # <QuerySet [('zhang分类一', 1), ('zhang分类二', 0)]>
# 2.查询当前用户所有的标签及标签下的文章数
tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name', 'count_num', 'pk')
# print(tag_list) # <QuerySet [('meng标签一', 1), ('meng标签二', 1)]>
# 3.按照年月统计所有的文章
'''这里要在settins.py配置:USE_TZ = False'''
date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values_list('month').annotate(count_num=Count('pk')).values_list('month', 'count_num')
# print(date_list) # <QuerySet [(datetime.datetime(2021, 12, 1, 0, 0), 2)]>
return render(request, 'site.html', locals())
侧边栏筛选功能
def site(request, username,**kwargs):
# 1.先校验个人站点是否存在
user_obj = models.UserInfo.objects.filter(username=username).first()
# 2.用户不存在,返回一个404页面
if not user_obj:
return render(request, '404.html')
blog = user_obj.blog # 获取个人站点
# 3.查询当前个人站点的所有文章
article_list = models.Article.objects.filter(blog=blog)
'''侧边栏筛选功能'''
# 判断kwargs是否有值
if kwargs:
# print(kwargs) # {'condition': 'tag', 'param': '1'}
condition = kwargs.get('condition')
param = kwargs.get('param')
# 判断用户到底想要按照哪个条件筛选数据
if condition == 'category':
article_list = article_list.filter(category_id=param)
elif condition == 'tag':
article_list = article_list.filter(tags__id=param)
else:
year, month = param.split('-') # 解压赋值 2020-11 [2020,11]
article_list = article_list.filter(create_time__year=year,create_time__month=month)
# 侧边栏搭建
# 1.查询当前用户所有的分类已经分类下的文章数
category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name', 'count_num', 'pk') # 当前用户所有的分类
# print(category_list) # <QuerySet [('zhang分类一', 1), ('zhang分类二', 0)]>
# 2.查询当前用户所有的标签及标签下的文章数
tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name', 'count_num', 'pk')
# print(tag_list) # <QuerySet [('meng标签一', 1), ('meng标签二', 1)]>
# 3.按照年月统计所有的文章
'''这里要在settins.py配置:USE_TZ = False'''
date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values_list('month').annotate(count_num=Count('pk')).values_list('month', 'count_num')
# print(date_list) # <QuerySet [(datetime.datetime(2021, 12, 1, 0, 0), 2)]>
return render(request, 'site.html', locals())
# site.html
<p><a href="/{{ username }}/category/{{ category.2 }}">{{ category.0 }}({{ category.1 }})</a></p>
<p><a href="/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }}({{ tag.1 }})</a></p>
<p><a href="/{{ username }}/archive/{{ date.0|date:'Y-m' }}">{{ date.0|date:'Y年m月' }}({{ date.1 }})</a></p>
文章详情页
# 注意事项:
1.url设计
/username/article/1
2.添加完路由测试下,是否造成路由冲突
3.文章详情页和个人站点基本一致,可以使用模板继承
4.侧边栏的渲染需要传入数据才能渲染,并且该在很多页面都需要使用
1)直接拷贝 # 不推荐
2)将侧边栏制作成inclusion_tag
'''
步骤:
1.在该应用下创建一个名为:templatetags文件夹
2.在该文件内创建一个任意名称的py文件
3.在该文件中固定写两行代码
from django import template
register = template.Library()
# 自定义过滤器
'''
做一个继承的母版
{% extends 'base.html' %}
{% block content %}
<h1>{{ article_obj.title }}</h1>
<div class="article_content">
{{ article_obj.content| safe }}
</div>
{% endblock %}
'''实现所有文章都可以跳转'''
# 在site中补上文章详情的链接
<h4 class="media-heading"><a href="/{{ username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h4>
# 在home中补上文章详情的链接
<h4 class="media-heading"><a href="/{{ article_obj.blog.userinfo.username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h4>
制作一个inclusion_tag
# 制作成,在其他页面也可以使用侧边栏
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('left_menu.html')
def left_menu(username):
# 构造侧边栏需要的数据
# 1.先校验个人站点是否存在
user_obj = models.UserInfo.objects.filter(username=username).first()
blog = user_obj.blog # 获取个人站点
# 侧边栏搭建
# 1.查询当前用户所有的分类已经分类下的文章数
category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list(
'name', 'count_num', 'pk') # 当前用户所有的分类
# print(category_list) # <QuerySet [('zhang分类一', 1), ('zhang分类二', 0)]>
# 2.查询当前用户所有的标签及标签下的文章数
tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name','count_num', 'pk')
# print(tag_list) # <QuerySet [('meng标签一', 1), ('meng标签二', 1)]>
# 3.按照年月统计所有的文章
'''这里要在settins.py配置:USE_TZ = False'''
date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values_list(
'month').annotate(count_num=Count('pk')).values_list('month', 'count_num')
# print(date_list) # <QuerySet [(datetime.datetime(2021, 12, 1, 0, 0), 2)]>
return locals()
# 在把侧边栏的3份放到一个自定义html文件中,然后在base文件中引用
<div class="col-md-3">
{% load mytag %}
{% left_menu username %}
</div>
点赞点踩
# 如何区分是点赞还是点踩
'''
1.给两个标签绑定一个点击事件
// 给所有的action类绑定点击事件
$('.action').click(function () {
alert($(this).hasClass('diggit')) // 判断是否有这个属性,true是赞,反之踩
})
'''
{% block content %}
<h1>{{ article_obj.title }}</h1>
<div class="article_content">
{{ article_obj.content | safe }}
</div>
{# 点赞点踩样式#}
<div class="clearfix"> <!--清除塌陷-->
<div id="div_digg">
<div class="diggit action">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit action">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips">
</div>
</div>
</div>
{# 点赞点踩样式结束#}
{# 文章评论样式开始#}
{% if request.user.is_authenticated %}
<div>
<p><span class="glyphicon glyphicon-comment"></span> 发表评论</p>
<div>
<textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
</div>
<button class="btn btn-primary">提交评论</button>
</div>
{% else %}
<li><a href="{% url 'reg' %}">注册</a></li>
<li><a href="{% url 'lo' %}">登录</a></li>
{% endif %}
{# 文章评论样式结束#}
{% endblock %}
{% block js %}
<script>
// 给所有的action类绑定点击事件
$('.action').click(function () {
{#alert($(this).hasClass('diggit')) // 判断是否有这个属性,true是赞,反之踩#}
let isUp = $(this).hasClass('diggit');
let $div = $(this);
// 向后端发送ajax请求
$.ajax({
url: '/up_or_down/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'is_up': isUp,
'csrfmiddlwaretoken': '{{ csrf_token }}',
},
success: function (args) {
if (args.code == 1000) {
$('#digg_tips').text(args.msg);
// 将前端的数字加1,先获取之前的数据
let oldNum = $div.children().text(); // 文本是字符类型
$div.children().text(Number(oldNum + 1))
} else {
$('#digg_tips').html(args.msg)
}
}
})
})
// 用户点击评论按钮,后端发送ajax请求
</script>
{% endblock %}
def up_or_down(request):
'''
1.校验用户是否登录
2.自己不能给自己点赞点踩
3.当前用户是否已经点赞,点过之后不能在点赞
4.操作数据库
'''
if request.is_ajax():
back_dic = {'code': 1000, 'msg': ''}
# 1.判断当前用户是否登录
if request.user.is_authenticated():
article_id = request.POST.get('article_id')
is_up = request.POST.get('is_up')
is_up = json.loads(is_up) # 前端传的数字符串js格式,需要反序列化
# print(is_up) # True
# 1.1获取文章对象
article_obj = models.Article.objects.filter(pk=article_id).first() # 文章对象
# print(article_obj.pk) # 主键值
# 2.判断当前文章是否是自己的
if not article_obj.blog.userinfo == request.user: # article_obj.blog.userinfo:文章属于哪个用户,request.user:当前用户
# 3.校验当前用户是否点赞
is_click = models.UpAndDown.objects.filter(user=request.user, article_id=article_obj)
if not is_click:
# 4.入库
# 判断当前用户点了赞还是点了踩,从而给哪个字段加一
if is_up:
models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1)
back_dic['msg'] = '点赞成功'
else:
models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
back_dic['msg'] = '点踩成功'
# 操作点赞点踩表
models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
else:
back_dic['code'] = 1130
back_dic['msg'] = '已点赞'
else:
back_dic['code'] = 1131
back_dic['msg'] = '不能给自己点赞'
else:
back_dic['code'] = 1132
back_dic['msg'] = '<a href="/login/">请先登录</a>'
return JsonResponse(back_dic)
文章评论
{% extends 'base.html' %}
{% block css %}
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 128px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
width: 46px;
height: 52px;
background: url('/static/img/upup.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.buryit {
float: right;
margin-left: 20px;
width: 46px;
height: 52px;
background: url('/static/img/downdown.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
</style>
{% endblock %}
{% block content %}
<h1>{{ article_obj.title }}</h1>
<div class="article_content">
{{ article_obj.content | safe }}
</div>
{# 点赞点踩样式#}
<div class="clearfix"> <!--清除塌陷-->
<div id="div_digg">
<div class="diggit action">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit action">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips">
</div>
</div>
</div>
{# 点赞点踩样式结束#}
{# 评论楼渲染开始#}
<div>
<ul class="list-group">
{% for comment in comment_list %}
<li class="list-group-item list-group-item-info">
<span>#{{ forloop.counter }}楼</span>
<span>{{ comment.create_time|date:'Y-m-d H:i' }}</span>
<span>{{ comment.user.username }}</span>
<span><a href="javascript:;" class="pull-right reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}">回复</a></span>
<div>
{# 判断当前评论是否是子评论,如果是需要渲染人名#}
{% if comment.parent_id %}
<p>@{{ comment.parent.user.username }}</p>
{% endif %}
<p>{{ comment.content }}</p>
</div>
</li>
{% endfor %}
</ul>
</div>
{# 评论楼渲染结束#}
{# 文章评论样式开始#}
{% if request.user.is_authenticated %}
<div>
<p><span class="glyphicon glyphicon-comment"></span> 发表评论</p>
<div>
<textarea name="" id="id_commit" cols="60" rows="10"></textarea>
</div>
<button class="btn btn-primary" id="id_submit">提交评论</button>
<span style="color: red" id="error"></span>
</div>
{% else %}
<li><a href="{% url 'reg' %}">注册</a></li>
<li><a href="{% url 'lo' %}">登录</a></li>
{% endif %}
{# 文章评论样式结束#}
{% endblock %}
{% block js %}
<script>
// 给所有的action类绑定点击事件
$('.action').click(function () {
{#alert($(this).hasClass('diggit')) // 判断是否有这个属性,true是赞,反之踩#}
let isUp = $(this).hasClass('diggit');
let $div = $(this);
// 向后端发送ajax请求
$.ajax({
url: '/up_or_down/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'is_up': isUp,
'csrfmiddlwaretoken': '{{ csrf_token }}',
},
success: function (args) {
if (args.code == 1000) {
$('#digg_tips').text(args.msg);
// 将前端的数字加1,先获取之前的数据
let oldNum = $div.children().text(); // 文本是字符类型
$div.children().text(Number(oldNum + 1))
} else {
$('#digg_tips').html(args.msg)
}
}
})
});
// 设置全局parent字段
let parentId = null;
// 用户点击评论按钮,后端发送ajax请求
$('#id_submit').on('click', function () {
// 获取评论内容
let conTent = $('#id_commit').val();
// 判断是否为子评论,如果是需要将我们将@username去除
if (parentId){
// 找到\n对应的索引,然后利用切片,但是切片是顾头不顾尾,要+1
let indexNum = conTent.indexOf('\n') + 1;
conTent = conTent.slice(indexNum) // 将indexNum之前的数据切除,只保留后面的数据
}
$.ajax({
url: '/comment/',
type: 'post',
data: {
'article_id': '{{ article_id }}',
'content': conTent,
// 如果没有值,后端存入null
'parent_id': parentId,
'csrfmiddlwaretoken': '{{ csrf_token }}',
},
success: function (args) {
if (args.code == 1000) {
$('#error').text(args.msg);
// 将评论框里面的内容清空
$('#id_commit').val('');
// 临时渲染评论
let userName = '{{ request.user.username }}';
let temp = `
<li class="list-group-item list-group-item-info">
<span class="glyphicon glyphicon-comment">${userName}</span>
<span><a href="" class="pull-right">回复</a></span>
<div>
${conTent}
</div>
</li>
`;
// 将生成好的标签添加到ul标签类
$('.list-group').append(temp)
// 清空全局parent_id
parentId = null;
}
}
})
});
// 给回复按钮绑定点击事件
$('.reply').click(function () {
// 需要评论的对应人的姓名,还需要评论的主键值
// 获取用户名和主键值
let commentUseranme = $(this).attr('username');
parentId = $(this).attr('comment_id');
// 拼接信息,加到评论框
$('#id_commit').val('@' + commentUseranme + '\n').focus() // foncus:聚焦
})
</script>
{% endblock %}
def comment(request):
if request.is_ajax():
back_dic = {'code': 1000, 'msg': ''}
if request.method == 'POST':
# 判断当前用户是否登录
if request.user.is_authenticated:
article_id = request.POST.get('article_id')
content = request.POST.get('content')
parent_id = request.POST.get('parent_id')
# print(parent_id)
print(content)
print(article_id)
# 直接存储数据
with transaction.atomic():
# 修改普通字段
models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
# 存储评论内容
models.Comment.objects.create(user=request.user, article_id=article_id, content=content,
parent_id=parent_id)
back_dic['msg'] = '评论成功'
else:
back_dic['code'] = 130
back_dic['msg'] = '请先登录'
return JsonResponse(back_dic)