BBS项目(持续更新中,预计3天左右完成!)
BBS项目
bbs是一个前后端不分离的全栈项目,前端和后端都需要我们自己一步步的完成
-
表创建及同步
-
注册功能
-
forms组件
-
用户头像前端实时展示
-
ajax
-
-
登陆功能
搭建bbs首页
-
自己实现图片验证码
-
ajax
-
-
导航条根据用户是否登陆展示不同的内容
数据库表创建及同步
""" 由于django自带的sqlite数据库对日期不敏感,所以我们换成MySQL """ 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) # 头像 avatar = models.FileField(upload_to='avatar/',default='avatar/default.png',verbose_name='用户头像') """ 给avatar字段传文件对象 该文件会自动存储到avatar文件下 然后avatar字段只保存文件路径avatar/default.png """ create_time = models.DateField(auto_now_add=True) blog = models.OneToOneField(to='Blog',null=True) class Blog(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=64) # 存css/js的文件路径 class Category(models.Model): name = models.CharField(verbose_name='文章分类',max_length=32) blog = models.ForeignKey(to='Blog',null=True) class Tag(models.Model): name = models.CharField(verbose_name='文章标签',max_length=32) blog = models.ForeignKey(to='Blog', null=True) class Article(models.Model): title = models.CharField(verbose_name='文章标题',max_length=64) desc = models.CharField(verbose_name='文章简介',max_length=255) # 文章内容有很多 一般情况下都是使用TextField content = models.TextField(verbose_name='文章内容') create_time = models.DateField(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(to='Blog', null=True) category = models.ForeignKey(to='Category',null=True) tags = models.ManyToManyField(to='Tag', through='Article2Tag', through_fields=('article','tag') ) class Article2Tag(models.Model): article = models.ForeignKey(to='Article') tag = models.ForeignKey(to='Tag') class UpAndDown(models.Model): user = models.ForeignKey(to='UserInfo') article = models.ForeignKey(to='Article') is_up = models.BooleanField() # 传布尔值 存0/1 class Comment(models.Model): user = models.ForeignKey(to='UserInfo') article = models.ForeignKey(to='Article') content = models.CharField(verbose_name='评论内容',max_length=255) comment_time = models.DateTimeField(verbose_name='评论时间',auto_now_add=True) # 自关联 parent = models.ForeignKey(to='self',null=True) # 有些评论就是根评论
注册功能
""" 我们之前是直接在views.py中书写的forms组件代码 但是为了接耦合 应该将所有的forms组件代码单独写到一个地方 如果你的项目至始至终只用到一个forms组件那么你可以直接建一个py文件书写即可 myforms.py 但是如果你的项目需要使用多个forms组件,那么你可以创建一个文件夹在文件夹内根据 forms组件功能的不同创建不同的py文件 myforms文件夹 regform.py loginform.py userform.py orderform.py ... """ def register(request): form_obj = MyRegForm() if request.method == 'POST': back_dic = {"code": 1000, 'msg': ''} # 校验数据是否合法 form_obj = MyRegForm(request.POST) # 判断数据是否合法 if form_obj.is_valid(): # print(form_obj.cleaned_data) # {'username': 'jason', 'password': '123', 'confirm_password': '123', 'email': '123@qq.com'} clean_data = form_obj.cleaned_data # 将校验通过的数据字典赋值给一个变量 # 将字典里面的confirm_password键值对删除 clean_data.pop('confirm_password') # {'username': 'jason', 'password': '123', 'email': '123@qq.com'} # 用户头像 file_obj = request.FILES.get('avatar') """针对用户头像一定要判断是否传值 不能直接添加到字典里面去""" if file_obj: clean_data['avatar'] = file_obj # 直接操作数据库保存数据 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()) <script> $("#myfile").change(function () { // 文件阅读器对象 // 1 先生成一个文件阅读器对象 let myFileReaderObj = new FileReader(); // 2 获取用户上传的头像文件 let fileObj = $(this)[0].files[0]; // 3 将文件对象交给阅读器对象读取 myFileReaderObj.readAsDataURL(fileObj) // 异步操作 IO操作 // 4 利用文件阅读器将文件展示到前端页面 修改src属性 // 等待文件阅读器加载完毕之后再执行 myFileReaderObj.onload = function(){ $('#myimg').attr('src',myFileReaderObj.result) } }) $('#id_commit').click(function () { // 发送ajax请求 我们发送的数据中即包含普通的键值也包含文件 let formDataObj = new FormData(); // 1.添加普通的键值对 {#console.log($('#myform').serializeArray()) // [{},{},{},{},{}] 只包含普通键值对#} $.each($('#myform').serializeArray(),function (index,obj) { {#console.log(index,obj)#} // obj = {} formDataObj.append(obj.name,obj.value) }); // 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(args.msg,function (index,obj) { {#console.log(index,obj) // username ["用户名不能为空"]#} let targetId = '#id_' + index; $(targetId).next().text(obj[0]).parent().addClass('has-error') }) } } }) }) // 给所有的input框绑定获取焦点事件 $('input').focus(function () { // 将input下面的span标签和input外面的div标签修改内容及属性 $(this).next().text('').parent().removeClass('has-error') }) </script> # 扩展 """ 一般情况下我们在存储用户文件的时候为了避免文件名冲突的情况 会自己给文件名加一个前缀 uuid 随机字符串 ... """
登陆功能
"""
1.自己手动搭建获取用户用户名 密码 验证码前端标签代码
2.图片验证码如何自己完成
ps:
img标签src属性后面可以写的内容
1.直接写网络图片地址
2.仅仅只是一个url后缀(自动朝该url发送get请求获取数据)
3.图片二进制数据
1.需要借助于pillow模块
Image,ImageDraw,ImageFont
2.需要借助于内存管理器io模块
BytesIo,StringIO
3.字体样式其实是受.ttf结尾的文件控制的
4.手动产生随机验证码(搜狗公司的笔试题)
random模块
chr内置方法
在session中保存验证码
"""
""" img标签的src属性 1.图片路径 2.url 3.图片的二进制数据 我们的计算机上面致所有能够输出各式各样的字体样式 内部其实对应的是一个个.ttf结尾的文件 http://www.zhaozi.cn/ai/2019/fontlist.php?ph=1&classid=32&softsq=%E5%85%8D%E8%B4%B9%E5%95%86%E7%94%A8 """ """ 图片相关的模块 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): # 推导步骤1:直接获取后端现成的图片二进制数据发送给前端 # with open(r'static/img/111.jpg','rb') as f: # data = f.read() # return HttpResponse(data) # 推导步骤2:利用pillow模块动态产生图片 # img_obj = Image.new('RGB',(430,35),'green') # img_obj = Image.new('RGB',(430,35),get_random()) # # 先将图片对象保存起来 # with open('xxx.png','wb') as f: # img_obj.save(f,'png') # # 再将图片对象读取出来 # with open('xxx.png','rb') as f: # data = f.read() # return HttpResponse(data) # 推导步骤3:文件存储繁琐IO操作效率低 借助于内存管理器模块 # img_obj = Image.new('RGB', (430, 35), get_random()) # io_obj = BytesIO() # 生成一个内存管理器对象 你可以看成是文件句柄 # img_obj.save(io_obj,'png') # return HttpResponse(io_obj.getvalue()) # 从内存管理器中读取二进制的图片数据返回给前端 # 最终步骤4:写图片验证码 img_obj = Image.new('RGB', (430, 35), get_random()) img_draw = ImageDraw.Draw(img_obj) # 产生一个画笔对象 img_font = ImageFont.truetype('static/font/222.ttf',30) # 字体样式 大小 # 随机验证码 五位数的随机验证码 数字 小写字母 大写字母 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(0,9)) # 从上面三个里面随机选择一个 tmp = random.choice([random_lower,random_upper,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()) <script> $("#id_img").click(function () { // 1 先获取标签之前的src let oldVal = $(this).attr('src'); $(this).attr('src',oldVal += '?') }) </script>
def login(request): if request.method == 'POST': back_dic = {'code':1000,'msg':''} username = request.POST.get('username') password = request.POST.get('password') code = request.POST.get('code') # 1 先校验验证码是否正确 自己决定是否忽略 统一转大写或者小写再比较 if request.session.get('code').upper() == code.upper(): # 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')
首页搭建
# 1.动态展示用户名称 {% 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="#">修改密码</a></li> <li><a href="#">修改头像</a></li> <li><a href="#">后台管理</a></li> <li role="separator" class="divider"></li> <li><a href="#">退出登陆</a></li> </ul> </li> {% else %} <li><a href="{% url 'reg' %}">注册</a></li> <li><a href="{% url 'login' %}">登陆</a></li> {% endif %} # 更多操作
admin后台管理
""" django给你提供了一个可视化的界面用来让你方便的对你的模型表 进行数据的增删改查操作 如果你先想要使用amdin后台管理操作模型表 你需要先注册你的模型表告诉admin你需要操作哪些表 去你的应用下的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.Category) admin.site.register(models.Tag) admin.site.register(models.Article) admin.site.register(models.Article2Tag) admin.site.register(models.UpAndDown) admin.site.register(models.Comment) """ # admin会给每一个注册了的模型表自动生成增删改查四条url http://127.0.0.1:8000/admin/app01/userinfo/ 查 http://127.0.0.1:8000/admin/app01/userinfo/add/ 增 http://127.0.0.1:8000/admin/app01/userinfo/1/change/ 改 http://127.0.0.1:8000/admin/app01/userinfo/1/delete/ 删 http://127.0.0.1:8000/admin/app01/blog/ 查 http://127.0.0.1:8000/admin/app01/blog/add/ 增 http://127.0.0.1:8000/admin/app01/blog/1/change/ 改 http://127.0.0.1:8000/admin/app01/blog/1/delete/ 删 """ 关键点就在于urls.py中的第一条自带的url 前期我们需要自己手动苦逼的录入数据,自己克服一下 """ # 1.数据绑定尤其需要注意的是用户和个人站点不要忘记绑定了 # 2.标签 # 3.标签和文章 千万不要把别人的文章绑定标签
用户头像展示
""" 1 网址所使用的静态文件默认放在static文件夹下 2 用户上传的静态文件也应该单独放在某个文件夹下 media配置 该配置可以让用户上传的所有文件都固定存放在某一个指定的文件夹下 # 配置用户上传的文件存储位置 MEDIA_ROOT = os.path.join(BASE_DIR,'media') # 文件名 随你 自己 会自动创建多级目录 如何开设后端指定文件夹资源? 首先你需要自己去urls.py书写固定的代码 from django.views.static import serve from BBS14 import settings # 暴露后端指定文件夹资源 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}) """
图片防盗链
# 如何避免别的网站直接通过本网站的url访问本网站资源 # 简单的防盗 我可以做到请求来的时候先看看当前请求是从哪个网站过来的 如果是本网站那么正常访问 如果是其他网站直接拒绝 请求头里面有一个专门记录请求来自于哪个网址的参数 Referer: http://127.0.0.1:8000/xxx/ # 如何避免 1.要么修改请求头referer 2.直接写爬虫把对方网址的所有资源直接下载到我们自己的服务器上
个人站点
# 全是每个用户都可以有自己的站点样式 <link rel="stylesheet" href="/media/css/{{ blog.site_theme }}/"> 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 时区问题报错 TIME_ZONE = 'Asia/Shanghai' USE_TZ = True """
侧边栏筛选功能
https://www.cnblogs.com/jason/tag/Python/ 标签 https://www.cnblogs.com/jason/category/850028.html 分类 https://www.cnblogs.com/jason/archive/2016/10.html 日期 https://www.cnblogs.com/jason/tag/1/ 标签 https://www.cnblogs.com/jason/category/1 分类 https://www.cnblogs.com/jason/archive/2020-11/ 日期 def site(request,username,**kwargs): """ :param request: :param username: :param kwargs: 如果该参数有值 也就意味着需要对article_list做额外的筛选操作 :return: """ # 先校验当前用户名对应的个人站点是否存在 user_obj = models.UserInfo.objects.filter(username=username).first() # 用户如果不存在应该返回一个404页面 if not user_obj: return render(request,'errors.html') blog = user_obj.blog # 查询当前个人站点下的所有的文章 article_list = models.Article.objects.filter(blog=blog) # queryset对象 侧边栏的筛选其实就是对article_list再进一步筛选 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 [('jason的分类一', 2), ('jason的分类二', 1), ('jason的分类三', 1)]> # 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 [('tank的标签一', 1), ('tank的标签二', 1), ('tank的标签三', 2)]> # 3 按照年月统计所有的文章 date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values('month').annotate(count_num=Count('pk')).values_list('month','count_num') # print(date_list) return render(request,'site.html',locals())
文章详情页
# url设计 /username/article/1 # 先验证url是否会被其他url顶替 # 文章详情页和个人站点基本一致 所以用模版继承 # 侧边栏的渲染需要传输数据才能渲染 并且该侧边栏在很多页面都需要使用 1.哪个地方用就拷贝需要的代码(不推荐 有点繁琐) 2.将侧边栏制作成inclusion_tag """ 步骤 1.在应用下创建一个名字必须叫templatetags文件夹 2.在该文件夹内创建一个任意名称的py文件 3.在该py文件内先固定写两行代码 from django import template register = template.Library() # 自定义过滤器 # 自定义标签 # 自定义inclusion_tag """ # 自定义inclusion_tag @register.inclusion_tag('left_menu.html') def left_menu(username): # 构造侧边栏需要的数据 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 [('jason的分类一', 2), ('jason的分类二', 1), ('jason的分类三', 1)]> # 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 [('tank的标签一', 1), ('tank的标签二', 1), ('tank的标签三', 2)]> # 3 按照年月统计所有的文章 date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values( 'month').annotate(count_num=Count('pk')).values_list('month', 'count_num') # print(date_list) return locals()
文章点赞点踩
""" 浏览器上你看到的花里胡哨的页面,内部都是HTML(前端)代码 那现在我们的文章内容应该写什么??? >>> html代码 如何拷贝文章 copy outerhtml 1.拷贝文章 2.拷贝点赞点踩 1.拷贝前端点赞点踩图标 只拷了html 2.css也要拷贝 由于有图片防盗链的问题 所以将图片直接下载到本地 思考: 前端如何区分用户是点了赞还是点了踩 1.给标签各自绑定一个事件 两个标签对应的代码其实基本一样,仅仅是是否点赞点踩这一个参数不一样而已 2.二合一 给两个标签绑定一个事件 // 给所有的action类绑定事件 $('.action').click(function () { alert($(this).hasClass('diggit')) }) 由于点赞点踩内部有一定的业务逻辑,所以后端单独开设视图函数处理 """ # 个人建议:写代码先把所有正确的逻辑写完再去考虑错误的逻辑 不要试图两者兼得 import json from django.db.models import F def up_or_down(request): """ 1.校验用户是否登陆 2.判断当前文章是否是当前用户自己写的(自己不能点自己的文章) 3.当前用户是否已经给当前文章点过了 4.操作数据库了 :param request: :return: """ 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') # print(is_up,type(is_up)) # true <class 'str'> is_up = json.loads(is_up) # 记得转换 # print(is_up, type(is_up)) # True <class 'bool'> # 2 判断当前文章是否是当前用户自己写的 根据文章id查询文章对象 根据文章对象查作者 根request.user比对 article_obj = models.Article.objects.filter(pk=article_id).first() if not article_obj.blog.userinfo == request.user: # 3 校验当前用户是否已经点了 哪个地方记录了用户到底点没点 is_click = models.UpAndDown.objects.filter(user=request.user,article=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'] = 1001 back_dic['msg'] = '你已经点过了,不能再点了' # 这里你可以做的更加的详细 提示用户到底点了赞还是点了踩 else: back_dic['code'] = 1002 back_dic['msg'] = '你个臭不要脸的!' else: back_dic['code'] = 1003 back_dic['msg'] = '请先<a href="/login/">登陆</a>' return JsonResponse(back_dic) <script> // 给所有的action类绑定事件 $('.action').click(function () { {#alert($(this).hasClass('diggit'))#} 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, 'csrfmiddlewaretoken':'{{ csrf_token }}' }, success:function (args) { if(args.code == 1000){ $('#digg_tips').text(args.msg) // 将前端的数字加一 // 先获取到之前的数字 let oldNum = $div.children().text(); // 文本 是字符类型 // 易错点 $div.children().text(Number(oldNum) + 1) // 字符串拼接了 1+1 = 11 11 + 1 = 111 }else{ $('#digg_tips').html(args.msg) } } }) }) </script>
文章评论
""" 我们先写根评论 再写子评论 点击评论按钮需要将评论框里面的内容清空 根评论有两步渲染方式 1.DOM临时渲染 2.页面刷新render渲染 子评论 点击回复按钮发生了几件事 1.评论框自动聚焦 2.将回复按钮所在的那一行评论人的姓名 @username 3.评论框内部自动换行 根评论子评论都是点击一个按钮朝后端提交数据的 parent_id 根评论子评论区别在哪? parent_id """