44.BBS项目
BBS项目
一般项目分为5个步骤:
1.需求分析
2.设计程序以及程序的架构
3.分任务开发程序
4.测试
5.上线
需求分析
-
登录ajax,图形验证码
-
注册forms和ajax,上传头像,头像预览
-
博客首页
-
个人站点
-
点赞,点踩
-
评论
- 根评论
- 子评论
-
后台展示
-
添加文章
- 防xss攻击
数据库设计
一个用户对应一个站点所以用户表和个人站点表属于一对一,个人站点表与文章表,分类表,标签表都属于一对多,一个分类有多篇文章,所以分类表对于文章表属于一对多,标签表对于文章表是属于多对多,文章表对于评论表,点赞点踩表属于一对多,评论表可以有根评论和子评论,可以自关联。
# 用户表
class UserInfo(AbstractUser):
nid = models.AutoField(primary_key=True)
# 头像:FileField文件(varchar类型),default:默认值,upload_to上传的路径
avatar = models.FileField(upload_to='avatar/', default='avatar/default.png')
#跟blog表一对一
#OneToOneField本质就是ForeignKey,只不过有个唯一性约束
blog = models.OneToOneField(to='Blog', to_field='nid',null=True)
# blog = models.ForeignKey(to='Blog', to_field='nid',null=True,unique=True)
class Meta:
# db_table='xxxx'
# 在admin中显示的表名
verbose_name='用户表'
#去掉 用户表 后的s
verbose_name_plural = verbose_name
# 个人站点表
class Blog(models.Model):
nid = models.AutoField(primary_key=True)
#站点名称
title = models.CharField(max_length=64)
#站点副标题
site_name = models.CharField(max_length=32)
#不同人不同主题
theme = models.CharField(max_length=64)
def __str__(self):
return self.site_name
# 分类表
class Category(models.Model):
nid = models.AutoField(primary_key=True)
#分类名称
title = models.CharField(max_length=64)
#跟博客是一对多的关系,关联字段写在多的一方
#to 是跟哪个表关联 to_field跟表中的哪个字段做关联, null=True 表示可以为空
blog = models.ForeignKey(to='Blog', to_field='nid', null=True)
def __str__(self):
return self.title
# 标签表
class Tag(models.Model):
nid = models.AutoField(primary_key=True)
#标签名字
title = models.CharField(max_length=64)
# 跟博客是一对多的关系,关联字段写在多的一方
blog = models.ForeignKey(to='Blog', to_field='nid', null=True)
def __str__(self):
return self.title
# 文章表
class Article(models.Model):
nid = models.AutoField(primary_key=True)
#verbose_name在admin中显示该字段的中文
title = models.CharField(max_length=64,verbose_name='文章标题')
#文章摘要
desc = models.CharField(max_length=255)
#文章内容 大文本
content = models.TextField()
#DateTimeField 年月日时分秒(注意跟datafield的区别)
#auto_now_add=True:插入数据会存入当前时间
#auto_now=True 修改数据会存入当前时间
create_time = models.DateTimeField(auto_now_add=True)
# commit_num=models.IntegerField(default=0)
# up_num=models.IntegerField(default=0)
# down_num=models.IntegerField(default=0)
#一对多的关系
blog = models.ForeignKey(to='Blog', to_field='nid', null=True)
# 一对多的关系
category = models.ForeignKey(to='Category', to_field='nid', null=True)
#多对多关系 through_fields 不能写反了:
tag = models.ManyToManyField(to='Tag', through='ArticleTOTag', through_fields=('article', 'tag'))
def __str__(self):
return self.title+'----'+self.blog.userinfo.username
# 文章对标签中间表
class ArticleTOTag(models.Model):
nid = models.AutoField(primary_key=True)
article = models.ForeignKey(to='Article', to_field='nid')
tag = models.ForeignKey(to='Tag', to_field='nid')
# 评论表
class Commit(models.Model):
#谁对那篇文章评论了什么内容
nid = models.AutoField(primary_key=True)
user = models.ForeignKey(to='UserInfo', to_field='nid')
article = models.ForeignKey(to='Article', to_field='nid')
content = models.CharField(max_length=255)
#评论时间
create_time = models.DateTimeField(auto_now_add=True)
#自关联()
parent = models.ForeignKey(to='self', to_field='nid',null=True,blank=True)
# 点赞点踩表
class UpAndDown(models.Model):
#谁对那篇文章点赞或点踩
nid = models.AutoField(primary_key=True)
user = models.ForeignKey(to='UserInfo', to_field='nid')
article = models.ForeignKey(to='Article', to_field='nid')
#布尔类型,本质也还是0和1
is_up = models.BooleanField()
class Meta:
#联合唯一,一个用户只能给一片文章点赞或点踩
unique_together = (('user', 'article'),)
配置文件
1.可以使用自带sqlite
2.使用mysql
# setting.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'bbs',
'HOST':'127.0.0.1',
'USER':'root',
'PASSWORD':'123',
'PORT':3306
}
}
#使用auth认证自定义的用户表
AUTH_USER_MODEL = "blog.UserInfo"
# __init__.py
import pymysql
pymysql.install_as_MySQLdb()
文件迁移,生成表格
python manage.py makemigrations
python manage.py migrate
登录
#login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
<link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-4 offset-4">
<h1>用户登录</h1>
{# <form action="" method="post">#}
{% csrf_token %}
<div class="form-group">
<label for="id_name">用户名</label>
<input type="text" id="id_name" class="form-control" name="name">
</div>
<div class="form-group">
<label for="id_pwd">密码</label>
<input type="text" id="id_pwd" class="form-control" name="pwd">
</div>
<div class="form-group">
<label for="id_code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" id="id_code" class="form-control" name="code">
</div>
<div class="col-md-6">
<img src="/get_code/" height="35" width="280" id="id_img">
</div>
</div>
</div>
<input type="submit" value="提交" class="btn btn-success" id="id_btn"><span class="error" style="color: red">{{ resp.msg }}</span>
{# </form>#}
</div>
</div>
</div>
</body>
<script>
$('#id_img').click(function () {
$(this)[0].src+='?';
});
$('#id_btn').click(function () {
$.ajax({
type:'POST',
url:'/login/',
data:{'name':$('#id_name').val(),
'pwd':$('#id_pwd').val(),
'code':$('#id_code').val(),
csrfmiddlewaretoken:'{{ csrf_token }}'
},
success:function (data) {
if(data.code==100){
location.href='/index/'
}else{
$('.error').text(data.msg)
}
}
})
});
</script>
</html>
# views.py
from django.shortcuts import render,HttpResponse,redirect
from django.http import JsonResponse
#Image导入
#ImageDraw在图片上写字
#ImageFont 写字的格式
from PIL import Image,ImageDraw,ImageFont
import random
# 相当于把文件以byte格式存到内存中
from io import BytesIO
# Create your views here.
def login(request):
if request.method=='GET':
return render(request,'login.html')
else:
resp={'code':100,'msg':None}
name=request.POST.get('name')
pwd=request.POST.get('pwd')
code=request.POST.get('code')
print(name,pwd,code)
if request.session['code'].upper()==code.upper():
user=auth.authenticate(request,username=name,password=pwd)
if user:
resp['msg']='登录成功'
auth.login(request,user)
# return JsonResponse(resp)
else:
resp['code'] = 101
resp['msg'] = "用户名或密码错误"
# return render(request,'login.html',{'resp':resp})
else:
resp['code'] = 102
resp['msg'] = "验证码错误"
# return render(request, 'login.html', {'resp': resp})
return JsonResponse(resp)
def get_color():
return (random.randint(0,255),random.randint(0,255),random.randint(0,255))
def get_code(request):
# 方式一.返回固定图片
# with open('static/img/lhf.jpg','rb') as fr:
# data=fr.read()
# return HttpResponse(data)
# 方式二,自动生成图片(需要借助第三方模块pillow)图像处理的模块
# img=Image.new('RGB',(280,35),get_color())
# with open('static/img/code.jpg','wb') as fw:
# img.save(fw)
#
# with open('static/img/code.jpg','rb') as fr:
# data=fr.read()
# return HttpResponse(data)
# 方式三(不把文件保存在硬盘上,保存在内存中)
# img = Image.new('RGB', (280, 35), get_color())
# f=BytesIO()
# img.save(f,'jpeg')
# f.getvalue()
# return HttpResponse(f.getvalue())
# 方式四:在图片上写文件,并且保存到内存中
# img = Image.new('RGB', (280, 35), get_color())
# font=ImageFont.truetype('static/font/kumo.ttf',34)
# draw=ImageDraw.Draw(img)
# draw.text((40,0),'xxxxx',font=font)
#
# f=BytesIO()
# img.save(f,'jpeg')
# f.getvalue()
# return HttpResponse(f.getvalue())
# 最终版
img = Image.new('RGB', (280, 35), get_color())
font=ImageFont.truetype('static/font/kumo.ttf',34)
draw=ImageDraw.Draw(img)
code=''
for i in range(5):
num = str(random.randint(0, 9))
up = chr(random.randint(65, 90))
low = chr(random.randint(97, 122))
num_letter = random.choice((num, up, low))
code+=num_letter
draw.text((20+55*i, 0), num_letter, font=font)
print(code)
request.session['code']=code
f=BytesIO()
img.save(f,'jpeg')
f.getvalue()
return HttpResponse(f.getvalue())
在登录视图函数中存在一个问题是怎样判断验证码code正确与否,我们可以将验证码放到session中,当前台提交信息到后台验证时,取出session中验证码判断是否正确,然后可以通过auth模块的认证来判断用户名密码是否正确,这里通过将用户名密码传入auth模块的authenticate方法中生成user对象,若存在user对象,则表明信息正确,通过auth.login(request,user),把用户信息放到session中,这样做的话之后在其他任意的视图函数中都可以通过request.user获取当前登陆用户对象。
在登录过程中可以使用form表单提交信息,form表单使用POST提交方式提交,在后台接收,若信息正确则直接跳转到首页,若错误重新渲染登录页面。
或者使用ajax请求提交信息,需要在提交按钮添加点击事件,对应按照ajax请求方式提交相应信息,接收返回值,若正确使用location.href直接跳转到首页即可。
注册
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>注册</title>
<link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-4 offset-4">
<h1>用户注册</h1>
<form id="id_form">
{% csrf_token %}
{% for foo in form %}
<div class="form-group">
<label for={{ foo.auto_id }}>{{ foo.label }}</label>
{{ foo }}<span style="color: red" class="error"></span>
</div>
{% endfor %}
<div class="form-group">
<label for="id_file">头像上传
<img src="/static/img/default.png" width="70" height="70" style="margin-left: 20px" id="id_img">
</label>
<input type="file" name="file" id="id_file" style="display: none">
</div>
<input type="button" value="提交" class="btn btn-success" id="id_btn">
</form>
</div>
</div>
</div>
</body>
<script>
//当该控件发生变化,响应该事件
$("#id_file").change(function () {
//alert(1)
//取到文件对象
var file=$("#id_file")[0].files[0];
//放到img控件上,借助于filereader 中间的东西,文件阅读器
//生成一个文件阅读器对象赋值给filereader
var filereader =new FileReader();
//把文件读到filereader对象中
//读文件需要时间,需要文件读完再去操作img
filereader.readAsDataURL(file);
filereader.onload=function(){
$("#id_img").attr('src',filereader.result)
}
});
</script>
<script>
$("#id_btn").click(function () {
//ajax 上传文件
var formdata=new FormData();
//form 对象的serializeArray,它会把form中的数据包装到一个对象中(不包含文件)
var my_formdata=$('#id_form').serializeArray();
//jq的循环,传两个参数,第一个是要循环的对象,第二个参数是一个匿名函数
$.each(my_formdata,function (k,v) {
formdata.append(v.name,v.value);
console.log(k);
console.log(v);
});
formdata.append('file',$("#id_file")[0].files[0]);
$.ajax({
url:'/register/',
type:'post',
processData:false, //告诉jQuery不要去处理发送的数据
contentType:false,// 告诉jQuery不要去设置Content-Type请求头
data:formdata,
success:function (data) {
console.log(data)
if(data.code==100){
location.href='/login/'
}else if(data.code==101){
$.each(data.msg,function (k,v) {
$('#id_'+k).next().html(v[0]);
if(k=='__all__'){
$('#id_re_pwd').next().html(v[0])
}
})
}
}
});
});
</script>
</html>
注册的话这里我们通过直接使用form组件来实现,可以添加姓名,密码,确认密码和邮箱等字段和相应的插件,
然后还需加入上传头像的功能,
首先在模板里搭出框架整体,这里注意上传头像的功能需要做到的是点击一个默认图片可以进行提交上传图片的功能,这里提交文件可以使用file类型的input标签用来添加图片文件,
然后我们使用label标签里套一个图片标签的形式,并设置一个默认图片,利用label标签中for属性可以指向input标签,这样点击图片便可添加文件。
再下一步就是选择好要上传的图片要覆盖默认图片显示在注册页面上,这里就要使用change事件,一旦改变就执行,这样可以取得图片文件对象,然后使用new filereader()生成一个文件阅读器对象,将图片读入,读图片文件时需要时间,需要文件读完再去操作别的。
我们使用readAsDataURL
方法会读取指定的的图片文件对象,在读取操作完成的时候,readyState
会变成已完成DONE
状态。
在读取操作完成时触发onload用来处理文件载入的load事件,这里就可以将文件的内容file.reault替换到默认图片处,在前台显示已经替换成功,等待最后提交。
form表单提交文件
提交时可以使用form表单提交文件,但是需要设置enctype属性,必须指定enctype=multipart/form-data,上传文件后可以在视图函数中通过request.FILES.get('设置的文件名'),便可以拿到django自带的文件对象<class 'django.core.files.uploadedfile.InMemoryUploadedFile'>,然后可以循环取出保存文件即可。
ajax请求提交文件
也可以使用ajax请求方法提交,需要使用FormData对象添加字段来上传文件。也就是FormData对象点append的方式,append中以key-value形式传入,传文件时,第二个参数为文件对象,还需将processData,contentType设置为false,告诉jQuery不要去处理发送的数据和不要去设置Content-Type请求头,然后将FormData对象提交到后台处理,后面视图函数的操作和form表单提交文件处理方式一样,这样就可以将注册信息发到后台中。
注册的form校验
通过继承form,然后设置了姓名,密码,确认密码和邮箱等字段的校验条件。还需要通过局部钩子来验证用户名是否已存在,就是对验证用户名的字段进行重写的过程,若通过就会返回用户名,并保存在cleaned_data字典中,全局钩子来验证2次密码是否一样,若通过直接返回cleaned_data。
#局部钩子,局部校验
def clean_username(self):
#取出name对应的值
name=self.cleaned_data.get('username')
# if name.startswith('sb'):
# #校验不通过,抛异常
# raise ValidationError('不能以sb开头')
# #校验通过,直接return name值
# else:
# return name
user=models.UserInfo.objects.filter(username=name).first()
if user:
#用户存在,抛异常
raise ValidationError('用户已经存在')
else:
return name
#全局钩子,全局校验
def clean(self):
pwd=self.cleaned_data.get('password')
r_pwd=self.cleaned_data.get('re_pwd')
if pwd==r_pwd:
#校验通过,返回清洗后的数据
return self.cleaned_data
else:
#校验不通过,抛异常
raise ValidationError('两次密码不一致')
检验通过后,拿到所有用户注册信息后就可以保存到用户表中。
def register(request):
if request.method=="GET":
reg_form=RegForm()
return render(request,'register.html',{'form':reg_form})
else:
resp={'code':100,'msg':None}
# print(request.POST)
# print(request.body)
form=RegForm(request.POST)
if form.is_valid():
clean_data=form.cleaned_data
clean_data.pop('re_pwd')
avatar=request.FILES.get('file')
if avatar:
clean_data['avatar']=avatar
user=models.UserInfo.objects.create_user(**clean_data)
if user:
resp['msg']='创建成功'
else:
resp['msg']='创建失败'
resp['code']=102
else:
resp['code']=101
resp['msg']=form.errors
print(form.errors)
return JsonResponse(resp,safe=False)
#上传头像时
# 在setting.py加这两句,以后再上传的图片,都放在media文件夹下
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
#开启media的口子,直接通过url访问到在media中图片
url(r'^media/(?P<path>.*)', serve,kwargs={'document_root':settings.MEDIA_ROOT}),