BBS项目博客论坛系统
1.BBS项目开发流程
1 # 1.需求分析 2 架构师+产品经理+开发者组长 3 在跟客户谈需求之前,会大致先了解客户的需求,然后自己先设计一套比较好写方案 4 在跟客户沟通交流中引导客户往我们之前想好的方案上面靠 5 形成一个初步的方案 6 7 # 2.项目设计 8 架构师干的活 9 编程语言选择 10 框架选择 11 数据库选择 12 主库:MySQL,postgreSQL,... 13 缓存数据库:redis、mongodb、memcache... 14 功能划分 15 将整个项目划分成几个功能模块 16 找组长开会 17 给每个组分发任务 18 项目报价 19 技术这块需要多少人,多少天(一个程序员一天1500~2000计算(大致)) 20 产品经理公司层面 再加点钱 21 公司财务签字确认 22 公司老板签字确认 23 产品经理去跟客户沟通 24 25 后续需要加功能 继续加钱 26 27 # 3.分组开发 28 组长找组员开会,安排各自功能模块 29 我们其实就是在架构师设计好的框架里面填写代码而已(码畜) 30 31 我们在写代码的时候 写完需要自己先测试是否有bug 32 如果是一些显而易见的bug,你没有避免而是直接交给了测试部门测出来 33 那你可能就需要被扣绩效了(一定要跟测试小姐姐搞好关系) 34 薪资组成 15K(合理合规合法的避税) 35 底薪 10K 36 绩效 3K 37 岗位津贴 1K 38 生活补贴 1K 39 40 # 4.测试 41 测试部门测试你的代码 42 压力测试 43 ... 44 # 5.交付上线 45 1.交给对方的运维人员 46 2.直接上线到我们的服务器上 收取维护费用 47 3.其他...
2.表设计
1 """ 2 一个项目中最最最重要的不是业务逻辑的书写 3 而是前期的表设计,只要将表设计好了,后续的功能书写才会一帆风顺 4 5 bbs表设计 6 1.用户表 7 继承AbstractUser 8 扩展 9 phone 电话号码 10 avatar 用户头像 11 create_time 创建时间 12 13 外键字段 14 一对一个人站点表 15 16 2.个人站点表 17 site_name 站点名称 18 site_title 站点标题 19 site_theme 站点样式 20 21 3.文章标签表 22 name 标签名 23 24 外键字段 25 一对多个人站点 26 27 4.文章分类表 28 name 分类名 29 30 外键字段 31 一对多个人站点 32 33 5.文章表 34 title 文章标题 35 desc 文章简介 36 content 文章内容 37 create_time 发布时间 38 39 数据库字段设计优化(******) 40 (虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率低) 41 up_num 点赞数 42 down_num 点踩数 43 comment_num 评论数 44 45 外键字段 46 一对多个人站点 47 多对多文章标签 48 一对多文章分类 49 50 51 52 6.点赞点踩表 53 记录哪个用户给哪篇文章点了赞还是点了踩 54 user ForeignKey(to="User") 55 article ForeignKey(to="Article") 56 is_up BooleanField() 57 58 1 1 1 59 1 2 1 60 1 3 0 61 2 1 1 62 63 64 7.文章评论表 65 记录哪个用户给哪篇文章写了哪些评论内容 66 user ForeignKey(to="User") 67 article ForeignKey(to="Article") 68 content CharField() 69 comment_time DateField() 70 # 自关联 71 parent ForeignKey(to="Comment",null=True) 72 # ORM专门提供的自关联写法 73 parent ForeignKey(to="self",null=True) 74 75 id user_id article_id parent_id 76 1 1 1 77 2 2 1 1 78 79 根评论子评论的概念 80 根评论就是直接评论当前发布的内容的 81 82 子评论是评论别人的评论 83 1.PHP是世界上最牛逼的语言 84 1.1 python才是最牛逼的 85 1.2 java才是 86 87 根评论与子评论是一对多的关系 88 89 90 """
3.models表创建
由于在用户表中我们需要扩展额外的字段
所以我们需要自己在 models.py 文件中创建一个 UserInfo 表,来代替auth模块创建的User表
在 settings.py 配置文件中进行如下配置:
# settings.py AUTH_USER_MODEL = 'app01.UserInfo' # 应用名.类名 2 # 指定用UserInfo表扩展User表
由于Django中自带的sqlite数据库对数据不敏感,所以我们需要将数据库换成MySQL。在BBS项目中,我们需要创建8张表。注意:我们在创建表的时候,通常先创建普通字段,然后再创建外键字段。
1 from django.db import models 2 from django.contrib.auth.models import User,AbstractUser 3 4 # Create your models here. 5 6 class UserInfo(AbstractUser): # 扩展表需要继承AbstractUser类 7 phone = models.BigIntegerField(verbose_name='手机号', null=True) 8 avatar = models.FileField(upload_to='avatar/',default='avatar/default.png',verbose_name='用户头像') 9 create_time = models.DateTimeField(auto_now_add=True) 10 # 外键字段 11 blog = models.OneToOneField(to='Blog',null=True) # 用户表跟站点表是一对一关系 12 13 14 class Blog(models.Model): 15 site_name = models.CharField(max_length=32,verbose_name='站点名') 16 site_title = models.CharField(max_length=64,verbose_name='站点标题') 17 site_theme = models.CharField(max_length=64,verbose_name='站点样式') # 存css/js链接 18 19 20 class Category(models.Model): 21 name = models.CharField(max_length=32,verbose_name='文章分类') 22 # 外键字段 23 blog = models.ForeignKey(to='Blog') # 一个站点有多个分类,外键在多的一方 24 25 class Tag(models.Model): 26 name = models.CharField(max_length=32,verbose_name='文章标签') 27 # 外键字段 28 blog = models.ForeignKey(to='Blog',null=True) # 一个站点有多个标签,外键在多的一方 29 30 class Article(models.Model): 31 title = models.CharField(max_length=32,verbose_name='文章标题') 32 desc = models.CharField(max_length=255,verbose_name='文章简介') 33 content = models.TextField(verbose_name='文章内容') 34 create_time = models.DateTimeField(auto_now_add=True) 35 # 数据库字段优化,如果每次都用联表查询点赞/踩数、评论数,对数据库压力太大,所以可以用普通字段 36 up_num = models.BigIntegerField(default=0,verbose_name='点赞数') 37 down_num = models.BigIntegerField(default=0,verbose_name='点踩数') 38 comment_num = models.BigIntegerField(default=0,verbose_name='留言数') 39 # 外键字段 40 blog = models.ForeignKey(to='Blog',null=True) # 一个站点有多篇文章 41 category = models.ForeignKey(to='Category',null=True) # 一个分类有多篇文章 42 tags = models.ManyToManyField(to='Tag', 43 through='Tag2Article', 44 through_fields=('article','tag')) 45 46 47 48 class UpAndDown(models.Model): 49 user = models.ForeignKey(to='UserInfo') 50 article = models.ForeignKey(to='Article') 51 is_up = models.BooleanField() # 传布尔值,存0/1 52 53 54 class Comment(models.Model): 55 user = models.ForeignKey(to='UserInfo') 56 article = models.ForeignKey(to='Article') 57 comment_content = models.CharField(max_length=255,verbose_name='评论内容') 58 comment_time = models.DateTimeField(auto_now_add=True,verbose_name='评论时间') 59 # 自关联 60 parent = models.ForeignKey(to='self',null=True) 61 62 63 class Tag2Article(models.Model): 64 article = models.ForeignKey(to='Article') 65 tag = models.ForeignKey(to='Tag')
一、注册功能实现
4.1.创建注册功能 forms 组件
为了项目的解耦合程度,通常我们需要有一个地方用于存储 forms 组件代码,如果我们的项目至始至终只用到一个 forms 组件那么你可以直接建一个py文件书写即可(这里我们就只创建一个 myforms.py 文件),但是如果你的项目需要使用多个 forms 组件,那么你可以创建一个文件夹在文件夹内根据 orms 组件功能的不同创建不同的py文件如下
myforms.py
1 from django import forms 2 from app01 import models 3 4 5 class MyRegisterForm(forms.Form): 6 username = forms.CharField(label='用户名', max_length='8', min_length=3, 7 error_messages={ 8 'required': '用户名不能为空', 9 'max_length': '用户名最多8位', 10 'min_length': '用户名最少3位', }, 11 widget=forms.widgets.TextInput(attrs={'class': 'form-control'})) 12 13 email = forms.EmailField(label='邮箱', 14 error_messages={ 15 'invalid': '邮箱格式不正确', 16 'required': '邮箱密码不能为空', 17 }, 18 widget=forms.widgets.EmailInput(attrs={'class': 'form-control'})) 19 20 password = forms.CharField(label='密码', max_length=8, min_length=3, 21 error_messages={ 22 'required': '密码不能为空', 23 'max_length': '密码最多8位', 24 'min_length': '密码最少3位', 25 }, 26 widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})) 27 28 re_password = forms.CharField(label='确认密码', max_length=8, min_length=3, 29 error_messages={ 30 'required': '确认密码不能为空', 31 'max_length': '确认密码最多8位', 32 'min_length': '确认密码最少3位', 33 }, 34 widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})) 35 36 def clean_username(self): 37 username = self.cleaned_data.get('username') 38 is_exists = models.UserInfo.objects.filter(username=username) 39 if is_exists: 40 self.add_error('username', '用户名已存在!') 41 return username # 将数据返回给username 42 43 def clean(self): 44 password = self.cleaned_data.get('password') 45 re_password = self.cleaned_data.get('re_password') 46 if not password == re_password: 47 self.add_error('re_password', '两次密码不一致!') 48 return self.cleaned_data
register.html
1 # myforms.py 组件代码写好后, 2 # 我们需要在 views.py 中实例化出一个 form_obj对象, 3 # 然后通过 render 传给前端,通过 forms 组件渲染搭建注册页面, 4 5 # 注意:这里我们不用 form 表单提交数据, 6 # 而是用 ajax 进行前后端数据交互,不需要用到 form 表单提交数据, 7 # 但是我们需要用 form 标签来包含我们所有获取数据的html代码, 8 # 并在 form 标签中定义一个id, 9 # 通过$('#id_form').serializeArray()获取到form标签内所有用户普通键值对的数据,数据格式为列表套字典的格式[{},{},{},{}]
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 {% load static %} 7 <script src="{% static 'jquery-3.6.0.min.js' %}"></script> 8 <link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"> 9 <script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script> 10 <link rel="stylesheet" href="{% static 'dist/sweetalert.css' %}"> 11 <script src="{% static 'dist/sweetalert.min.js' %}"></script> 12 <script src="{% static 'js/mysetup.js' %}"></script> 13 14 </head> 15 <body> 16 <div class="container"> 17 <div class="row"> 18 <div class="col-md-6 col-md-offset-3"> 19 <h3 class="text-center">注册</h3> 20 <form id="id_form"> 21 {% csrf_token %} 22 {% for form in form_obj %} 23 <div class="form-group"> 24 <label for="{{ form.auto_id }}">{{ form.label }}</label> 25 {{ form }} 26 <span style="color: red" class="pull-right"></span> 27 </div> 28 {% endfor %} 29 <label for="id_file"> 30 <p>上传文件</p> 31 <img src="{% static 'img/default.png' %}" alt="" id="id_img" width="100px"> 32 </label> 33 <input type="file" id="id_file" name="avatar" style="display: none"> 34 <input type="button" id="id_btn" value="注册" class="btn btn-info btn-block"> 35 </form> 36 </div> 37 </div> 38 </div> 39 40 <script> 41 // 实时渲染头像 可以为头像添加文本域变换事件 42 $('#id_file').change(function(){ 43 // 要先读取更换的文件的信息 所以要用到文件阅读器 44 let fileReaderObj = new FileReader(); //生成文件阅读器 DOM对象 45 let fileObj = $(this)[0].files[0]; // 获取文件对象 46 fileReaderObj.readAsDataURL(fileObj) // fileObj传给阅读器读取 IO操作 异步操作 47 // 上面一步操作属于IO操作 又属于异步操作 所以我们需要等到文件读取完毕 再进行下一步操作 48 fileReaderObj.onload = function(){ 49 // 文件阅读器读取加载完毕后 我们需要修改img的src属性 达到头像实时渲染的目的 50 $('#id_img').attr('src',fileReaderObj.result) // 修改src的值为fileReaderObj读取的结果 51 // attr()内如果只有一个参数就是获取该属性的值,如果有两个,就是修改值 52 } 53 }) 54 55 56 // ajax提交数据 57 $('#id_btn').click(function(){ 58 // 因为ajax提交不止有普通键值对数据 还有头像图片的文件数据 59 // 所以我们需要发送FormData编码格式的数据 需要借助FormData对象 60 let formDataObj = new FormData() 61 // 如何获取form表单的数据 这里需要用到jQuery中的序列化方法 serializeArray 62 // console.log($('#id_form').serializeArray()) // 数据格式:[{},{},{}...] 63 // 通过$.each()来循环取值 可以得到一个个字典 {name: 'username', value: ''} 64 $.each($('#id_form').serializeArray(),function(index,obj){ 65 // console.log(index,obj) // obj得到的就是字典 66 // 将字典对应的值添加到formDataObj对象中 67 formDataObj.append(obj.name,obj.value) 68 }); 69 // 将头像的文件数据也添加到formDataObj对象中 70 formDataObj.append('avatar',$('#id_file')[0].files[0]) 71 // 将数据通过ajax请求发送给后端 72 $.ajax({ 73 url:'', // 不写默认朝当前地址提交 74 type:'post', 75 data:formDataObj, 76 processData:false, // ajax发送formData数据一定要加的两个参数之一 77 contentType:false, // ajax发送formData数据一定要加的两个参数之一 78 success:function(args){ 79 if(args.code === 1000){ 80 window.location.href = args.url 81 }else { 82 // 因为我们用的是ajax提交数据 错误信息无法像用form表单提交一样返回给对应标签 83 // 所以我们需要获取对应的标签的id 才能指定该标签返回错误信息 84 // 通过观察forms组件渲染出来的input标签 发现input框的id值都为 id_字段名 的格式 85 // 因此我们就可以手动拼接来得到input框对应的id 而循环后端返回过来的args.msg可以获取错误的字段名 86 $.each(args.msg,function(name,error_msg){ 87 // console.log(name,error_msg) // 数据格式: username ['用户名最少3位'] 88 let targetId = '#id_' + name // 直接在id前加#号 89 // 通过jQuery链式调用获取对应的标签 并且将对应的input框颜色变红 90 $(targetId).next().text(error_msg).parent().addClass('has-error') 91 }) 92 93 } 94 } 95 }) 96 }) 97 98 // 通过input框文本聚焦 移除错误信息和边框颜色 99 $('input').focus(function(){ 100 $(this).next().text('').parent().removeClass('has-error') 101 }) 102 </script> 103 104 </body> 105 </html>
views.py
register.html 页面搭建完毕后,我们通过 ajax 提交数据 然后再 views.py 中书写代码如下:
1 def register(request): 2 form_obj = myforms.MyRegForm() 3 if request.is_ajax(): 4 # 用ajax前后端交互 必须定义一个字典 用于给前端传输数据 5 back_dict = {'code':1000,'msg':''} 6 if request.method == 'POST': 7 # 将数据传给form组件进行校验 8 form_obj = myforms.MyRegForm(request.POST) 9 if form_obj.is_valid(): 10 # {'username': 'yesir', 'email': '983410683@qq.com', 'password': '123', 're_password': '123'} 11 # print(form_obj.cleaned_data) 12 cleaned_data = form_obj.cleaned_data 13 cleaned_data.pop('re_password') 14 # 通过request.FILES获取头像文件的数据 15 file_obj = request.FILES.get('avatar') 16 # 判断头像文件是否有值 如果没有说明用户没传 就用默认头像 17 if file_obj: 18 cleaned_data['avatar'] = file_obj # 这里的键一定要是models.UserInfo中的avatar字段 19 models.UserInfo.objects.create_user(**cleaned_data) # 拆包操作 直接将数据一次性存入数据库 20 back_dict['url'] = '/app01/login/' # 如果成功 默认code为1000 然后跳转到url路径 21 else: 22 back_dict['code'] = 1001 23 back_dict['msg'] = form_obj.errors # 直接将错误信息返回给前端 24 return JsonResponse(back_dict) 25 return render(request,'register.html',locals())
这样,我们就完成了整个注册功能的搭建。
二、登录功能实现
1 # 搭建登录页面的时候我们需要用到验证码 2 # 所以我们在视图函数中创建出随机验证码的图片 3 # 然后html页面通过访问验证码对应的路由路径 得到该验证码 4 # 并通过验证码点击事件实现验证码刷新 5 # 通过ajax提交数据 并在视图层编写逻辑代码进行 交由数据库验证 6 # 将验证结果通过字典返回给html页面的回调函数 7 # 回调函数接收到数据信息 做相应的操作
urls.py
1 from django.conf.urls import url 2 from app01 import views 3 4 urlpatterns = [ 5 # url(r'^admin/', admin.site.urls), 6 # 注册 7 url(r'^reg/', views.register), 8 # 登录 9 url(r'^login/',views.login), 10 # 登录验证码 11 url(r'^get_code/',views.get_code), 12 13 ]
views.py
1 图片验证码如何手动完成 2 # img标签src属性后面可以写的内容 3 # 1.直接写网络图片地址 4 # 2.仅仅只是一个url后缀(自动朝该url发送get请求获取数据) 5 # 3.图片二进制数据 6 7 # 1.需要借助于pillow模块 8 # Image,ImageDraw,ImageFont 9 # 2.需要借助于内存管理器io模块 10 # BytesIo,StringIO 11 # 3.字体样式其实是受.ttf结尾的文件控制的 12 # 4.手动产生随机验证码(搜狗公司的笔试题) 13 # random模块 14 # chr内置方法 15 # 在session中保存验证码
1 from django.shortcuts import render,HttpResponse,redirect 2 from django.http import JsonResponse 3 from app01 import myforms 4 from app01 import models 5 from PIL import Image,ImageFont,ImageDraw 6 from io import BytesIO,StringIO 7 import random 8 from django.contrib import auth 9 10 11 from app01 import models 12 from django.http import JsonResponse 13 from django.contrib import auth 14 import random 15 from PIL import Image,ImageDraw,ImageFont 16 """ 17 Image:生成图片 18 ImageDraw:能够在图片上乱涂乱画 19 ImageFont:控制字体样式 20 """ 21 from io import BytesIO,StringIO 22 """ 23 内存管理器模块 24 BytesIO:临时帮你存储数据 返回的时候数据是二进制 25 StringIO:临时帮你存储数据 返回的时候数据是字符串 26 """ 27 28 29 def get_random(): 30 # return会默认将他们封装到一个元组中 31 return random.randint(0,255),random.randint(0,255),random.randint(0,255) 32 33 def get_code(request): 34 # # 推导步骤1:直接获取后端现成的图片二进制数据发送给前端 35 # with open(r'static/img/222.jpg','rb') as f: 36 # data = f.read() 37 # return HttpResponse(data) 38 39 # 推导步骤2:利用pillow模块动态产生图片 40 # 生成一个Image对象,固定写法,传入的尺寸要跟页面定义尺寸一致 41 # img_obj = Image.new('RGB',(360,35),'green') 42 # img_obj = Image.new('RGB',(360,35),(188,222,252)) 43 # 利用random模块随机生成图片颜色 44 # img_obj = Image.new('RGB',(360,35),get_random()) 45 # 保存图片 46 # with open('xxx.png','wb') as f: 47 # img_obj.save(f,'png') 48 # 再将图片读取出来 49 # with open('xxx.png','rb') as f: 50 # data = f.read() 51 # return HttpResponse(data) 52 53 # 推导步骤3:文件存储繁琐IO操作效率低 借助于内存管理器模块 54 # img_obj = Image.new('RGB',(360,35),get_random()) 55 # 生成一个内存管理器 可以看成是文件句柄fp 56 # io_obj = BytesIO() 57 # 把图片对象存入io_obj,以指定格式 58 # img_obj.save(io_obj,'png') 59 # # 从内存管理器中读取二进制的图片数据返回给前端 60 # return HttpResponse(io_obj.getvalue()) 61 62 # 最终步骤4:写图片验证码 63 img_obj = Image.new('RGB', (430, 35), get_random()) 64 img_draw = ImageDraw.Draw(img_obj) # 产生一个画笔对象,写在图片上 65 # 生成一个字体对象 传入字体样式,字体大小(可以不传 用默认大小) 66 img_font = ImageFont.truetype('static/font/111.ttf',30) 67 # 定义code用于存储获取到的随机验证码 68 code = '' 69 # 随机验证码 五位数的随机验证码 数字 小写字母 大写字母 70 for i in range(5): 71 # 通过chr将随机出来的大写字母、小写字母、数字对应的ascii码转为对应的字符 72 random_upper = chr(random.randint(65,90)) 73 random_lower = chr(random.randint(97,122)) 74 random_int = chr(random.randint(48,57)) 75 # 从上面的三种形式中随机选一个 76 tmp = random.choice((random_upper,random_lower,random_int)) 77 # 将产生的随机字符串写入到图片上 78 """ 79 为什么一个个写而不是生成好了之后再写 80 因为一个个写能够控制每个字体的间隙 而生成好之后再写的话 81 间隙就没法控制了 82 """ 83 # 通过img_draw画笔对象写入文本,传入坐标 内容 颜色 字体4个值 84 img_draw.text((i*60+60,5),tmp,get_random(),img_font) 85 # 拼接随机字符串 86 code += tmp 87 print(code) 88 # 随机验证码在登陆的视图函数里面需要用到 要比对 所以要找地方存起来并且其他视图函数也能拿到 89 request.session['code'] = code 90 io_obj = BytesIO() # 生成一个IO内存管理器 可以看成一个文件句柄 91 img_obj.save(io_obj, 'png') # 保存图片 92 return HttpResponse(io_obj.getvalue()) # 从IO内存管理器对象中获取值 93 94 95 def login(request): 96 if request.method == 'POST': 97 back_dict={'code':1000, 'msg':''} 98 username = request.POST.get('username') 99 password = request.POST.get('password') 100 code = request.POST.get('code') 101 # 判断验证码是否正确,并忽略大小写 102 if request.session.get('code').lower() == code.lower(): 103 # 校验用户名或密码是否正确 返回一个用户对象 104 user_obj = auth.authenticate(username=username,password=password) 105 if user_obj: 106 # 保存用户登录状态 107 auth.login(request,user_obj) 108 back_dict['msg'] = '/app01/home/' 109 else: 110 back_dict['code'] = 2000 111 back_dict['msg'] = '用户名或密码错误!' 112 else: 113 back_dict['code'] = 3000 114 back_dict['msg'] = '验证码错误!' 115 return JsonResponse(back_dict) 116 return render(request,'login.html')
login.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 {% load static %} 7 <script src="{% static 'jquery-3.6.0.min.js' %}"></script> 8 <link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"> 9 <script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script> 10 <link rel="stylesheet" href="{% static 'dist/sweetalert.css' %}"> 11 <script src="{% static 'dist/sweetalert.min.js' %}"></script> 12 <script src="{% static 'js/mysetup.js' %}"></script> 13 14 </head> 15 <body> 16 <div class="container"> 17 <div class="row"> 18 <div class="col-md-6 col-md-offset-3"> 19 <h3 class="text-center">登录</h3> 20 {% csrf_token %} 21 <div class="form-group"> 22 <label for="id_username">用户名</label> 23 <input type="text" name="username" id="id_username" class="form-control"> 24 </div> 25 <div class="form-group"> 26 <label for="id_password">密码</label> 27 <input type="password" name="password" id="id_password" class="form-control"> 28 </div> 29 <div class="form-group"> 30 <label for="id_re_password">确认密码</label> 31 <input type="password" name="re_password" id="id_re_password" class="form-control"> 32 </div> 33 <div class="form-group"> 34 <label for="id_code">验证码</label> 35 <div class="row"> 36 <div class="col-md-6"> 37 <input type="text" name="code" id="id_code" class="form-control"> 38 </div> 39 <div class="col-md-6"> 40 <img src="/app01/get_code/" alt="" id="id_img" width="260px" height="30px"> 41 </div> 42 </div> 43 </div> 44 <div class="form-group"> 45 <input type="button" id="id_btn" value="登录" class="btn btn-success btn-block"> 46 <span style="color: red" id="id_error"></span> 47 </div> 48 49 </div> 50 </div> 51 </div> 52 53 <script> 54 // 点击验证码刷新 利用验证码点击事件 55 $('#id_img').click(function () { 56 // 验证码图片对应的src属性后面的值变了的时候 验证码都会刷新 57 // 所以我们可以利用这个机制 点击时让src的值不断自增 就可以一直刷新验证码 58 let oldSrc = $(this).attr('src') // 获取验证码原来的src值 59 $(this).attr('src', oldSrc += 1) 60 }) 61 62 $('#id_btn').click(function(){ 63 $.ajax({ 64 url:'', 65 type:'post', 66 data:{ 67 'username':$('#id_username').val(), 68 'password':$('#id_password').val(), 69 're_password':$('#id_re_password').val(), 70 'code':$('#id_code').val(), 71 'csrfmiddlewaretoken':'{{ csrf_token }}' 72 }, 73 success:function(args){ 74 if(args.code === 1000){ 75 window.location.href = args.msg 76 }else{ 77 $('#id_error').text(args.msg) 78 } 79 }, 80 }) 81 }) 82 </script> 83 </body> 84 </html>
登录功能实现完毕
三、首页功能搭建
1 ''' 2 1.先搭建导航栏,通过bootstrap搭建一个导航栏,用户登录时显示用户名和更多操作,未登录显示登录、注册 3 1.用if request.user.is_authneticated 判断右侧的用户是否登录 4 登录则显示用户名和更多操作,否则显示登录、注册 5 6 2.导航栏的修改密码不再跳转页面 而是直接通过 a 标签触发 模态框来完成 用ajax提交数据 7 1.在模态框内书搭建表单页面,通过ajax跟后端进行数据交互 8 9 3.除导航栏外首页搭建布局利用2-8-2布局 10 1.左右用面板 中间放用户文章简介以及内容 11 12 4.中间文章部分通过 admin 后台管理来完成 13 1.先在terminal窗口通过 python manage.py createsuperuser 创建超级用户 14 2.在 admin.py 中注册我们创建的8张表 15 3.在 models.py 中定义 __str__方法查看对象对应名字 16 1.并定义 class Meta 类 利用 verbose_name_plural 可以修改名字为中文 17 2.models.py 中 null=True只是设置表字段可以为空 blank=True 可以设置后台值可以为空 18 3.绑定用户表 个人站点 文章 标签 分类等表的关系 19 4.添加文章内容 20 5.去数据库中查询文章表 传给前端 利用for循环渲染前端页面 21 '''
urls.py
1 from django.conf.urls import url 2 from app01 import views 3 from bbs1 import settings 4 5 urlpatterns = [ 6 # url(r'^admin/', admin.site.urls), 7 # 注册 8 url(r'^register/', views.register,name='reg'), 9 # 登录 10 url(r'^login/',views.login,name='log'), 11 # 登录验证码 12 url(r'^get_code/',views.get_code), 13 # 首页搭建 14 url(r'^home/',views.home,name='home'),
views.py
1 def home(request): 2 article_list = models.Article.objects.all() # 查询所有文章渲染到前端hml页面 3 return render(request,'home.html',locals())
home.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>home</title> 6 {% load static %} 7 <script src="{% static 'jquery-3.6.0.min.js' %}"></script> 8 <link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"> 9 <script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script> 10 <link rel="stylesheet" href="{% static 'dist/sweetalert.css' %}"> 11 <script src="{% static 'dist/sweetalert.min.js' %}"></script> 12 <script src="{% static 'js/mysetup.js' %}"></script> 13 14 </head> 15 <body> 16 <!--导航栏开始--> 17 <nav class="navbar navbar-inverse"> 18 <div class="container-fluid"> 19 <!-- Brand and toggle get grouped for better mobile display --> 20 <div class="navbar-header"> 21 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" 22 data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> 23 <span class="sr-only">Toggle navigation</span> 24 <span class="icon-bar"></span> 25 <span class="icon-bar"></span> 26 <span class="icon-bar"></span> 27 </button> 28 <a class="navbar-brand" href="#">BBS</a> 29 </div> 30 31 <!-- Collect the nav links, forms, and other content for toggling --> 32 <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> 33 <ul class="nav navbar-nav"> 34 <li class="active"><a href="/app01/{{ request.user.username }}/">文章 <span class="sr-only">(current)</span></a></li> 35 <li><a href="/app01/{{ request.user.username }}/">随笔</a></li> 36 <li class="dropdown"> 37 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" 38 aria-expanded="false">详细信息 <span class="caret"></span></a> 39 <ul class="dropdown-menu"> 40 <li><a href="#">Action</a></li> 41 <li><a href="#">Another action</a></li> 42 <li><a href="#">Something else here</a></li> 43 <li role="separator" class="divider"></li> 44 <li><a href="#">Separated link</a></li> 45 <li role="separator" class="divider"></li> 46 <li><a href="#">One more separated link</a></li> 47 </ul> 48 </li> 49 </ul> 50 <form class="navbar-form navbar-left"> 51 <div class="form-group"> 52 <input type="text" class="form-control" placeholder="搜索文章/作者"> 53 </div> 54 <button type="submit" class="btn btn-default">搜索</button> 55 </form> 56 <ul class="nav navbar-nav navbar-right"> 57 {% if request.user.is_authenticated %} 58 <li><a href="/app01/{{ request.user.username }}/">{{ request.user }}</a></li> 59 <li class="dropdown"> 60 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" 61 aria-expanded="false">更多操作 <span class="caret"></span></a> 62 <ul class="dropdown-menu"> 63 <li><a href="#" data-toggle="modal" data-target="#myModal">修改密码</a></li> 64 <li><a href="#">后台管理</a></li> 65 <li><a href="#">修改头像</a></li> 66 <li role="separator" class="divider"></li> 67 <li><a href="{% url 'log_out' %}">退出登录</a></li> 68 </ul> 69 </li> 70 {% else %} 71 <li><a href="{% url 'reg' %}">注册</a></li> 72 <li><a href="{% url 'log' %}">登录</a></li> 73 {% endif %} 74 </ul> 75 <!--模态框开始--> 76 <!-- Button trigger modal --> 77 <!-- Modal --> 78 <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> 79 <div class="modal-dialog" role="document"> 80 <div class="modal-content"> 81 <div class="modal-header"> 82 <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> 83 <h4 class="modal-title text-center" id="myModalLabel">修改密码</h4> 84 </div> 85 <div class="modal-body"> 86 <div class="row"> 87 <div class="col-md-8 col-md-offset-2"> 88 <div class="form-group"> 89 <label for="id_username">用户名</label> 90 <input type="text" id="id_username" value="{{ request.user.username }}" class="form-control" disabled> 91 </div> 92 <div class="form-group"> 93 <label for="old_pwd">原密码</label> 94 <input type="password" id="old_pwd" class="form-control"> 95 </div> 96 <div class="form-group"> 97 <label for="new_pwd">新密码</label> 98 <input type="password" id="new_pwd" class="form-control"> 99 </div> 100 <div class="form-group"> 101 <label for="re_pwd">确认密码</label> 102 <input type="password" id="re_pwd" class="form-control"> 103 </div> 104 </div> 105 </div> 106 </div> 107 <div class="modal-footer"> 108 <button type="button" class="btn btn-default" data-dismiss="modal">取消</button> 109 <button type="button" id="id_btn" class="btn btn-primary">修改</button> 110 <span style="color: red"></span> 111 </div> 112 </div> 113 </div> 114 </div> 115 <!--模态框结束--> 116 </div><!-- /.navbar-collapse --> 117 </div><!-- /.container-fluid --> 118 </nav> 119 <!--导航栏结束--> 120 <div class="container-fluid"> 121 <div class="row"> 122 <div class="col-md-2"> 123 <div class="panel panel-primary"> 124 <div class="panel-heading">欢乐斗地主</div> 125 <div class="panel-body"> 126 每天领取大量欢乐豆 尽在欢乐斗地主 127 </div> 128 </div> 129 <div class="panel panel-danger"> 130 <div class="panel-heading">热血江湖</div> 131 <div class="panel-body"> 132 有人的地方,就有江湖,我在热血江湖等你哦 133 </div> 134 </div> 135 <div class="panel panel-success"> 136 <div class="panel-heading">传奇</div> 137 <div class="panel-body"> 138 传奇在线,豪华装备爆不停 139 </div> 140 </div> 141 </div> 142 <div class="col-md-8"> 143 {# 血夜之末 2022-04-15 01:40 1 0 #} 144 <ul class="media-list"> 145 {% for article_obj in article_list %} 146 <li class="media"> 147 <h4 class="media-heading"><a href="">{{ article_obj.title }}</a></h4> 148 <div class="media-left"> 149 <a href="#"> 150 <img class="media-object" src="/app01/media/{{article_obj.blog.userinfo.avatar}}" alt="..." width="80px"> 151 </a> 152 </div> 153 <div class="media-body"> 154 {{ article_obj.desc }} 155 </div> 156 <br> 157 <div> 158 <span><a href="/app01/{{ article_obj.blog.userinfo.username }}/">{{ article_obj.blog.userinfo.username }} </a></span> 159 <span>{{ article_obj.create_time|date:'Y-m-d H:m' }} </span> 160 <span>点赞<span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }} </span> 161 <span>评论<span class="glyphicon glyphicon-comment"></span> {{ article_obj.comment_num }}</span> 162 </div> 163 </li> 164 <hr> 165 {% endfor %} 166 </ul> 167 168 </div> 169 <div class="col-md-2"> 170 <div class="panel panel-primary"> 171 <div class="panel-heading">欢乐斗地主</div> 172 <div class="panel-body"> 173 每天领取大量欢乐豆 尽在欢乐斗地主 174 </div> 175 </div> 176 <div class="panel panel-danger"> 177 <div class="panel-heading">热血江湖</div> 178 <div class="panel-body"> 179 有人的地方,就有江湖,我在热血江湖等你哦 180 </div> 181 </div> 182 <div class="panel panel-success"> 183 <div class="panel-heading">传奇</div> 184 <div class="panel-body"> 185 传奇在线,豪华装备爆不停 186 </div> 187 </div> 188 </div> 189 </div> 190 </div> 191 192 193 <script> 194 // 修改密码 195 $('#id_btn').click(function(){ 196 $.ajax({ 197 url:'/app01/set_password/', 198 type:'post', 199 data:{ 200 'old_password':$('#old_pwd').val(), 201 'new_password':$('#new_pwd').val(), 202 're_password':$('#re_pwd').val(), 203 'csrfmiddlewaretoken':'{{ csrf_token }}' 204 }, 205 success:function(args){ 206 if(args.code === 1000){ 207 window.location.href = args.msg 208 }else{ 209 $('#id_btn').next().text(args.msg) 210 } 211 } 212 }) 213 }) 214 </script> 215 </body> 216 </html>
首页搭建完毕
四、个人站点搭建/侧边栏/侧边栏筛选功能
1 ''' 2 1.个人站点页面和首页区别不大,只是首页是2-8-2布局,个人站点则是3-9布局 3 2.个人站点的路径是输入用户名来访问 4 1.通过正则匹配用户名,结尾必须加$ 5 3.个人站点的导航栏直接用首页的, 6 4.个人站点侧边栏需要通过进行分组查询以及聚合函数查询来获取每个类目的文章数 7 5.个人站点内容区域直接显示用户名下所有的文章 8 6.个人站点的头像渲染需要在settings.py配置文件中如下配置: 9 MEDIA_ROOT=os.path.join(BASE_DIR,"media") 10 7.通过urls.py中配置: 11 url(r'^media/(?P<path>.*)', server,{'document_root':settings.MEDIA_ROOT}) 12 可以开放相对应的资源接口暴露给外界 13 '''
侧边栏筛选功能和个人站点两个url对应同一个视图函数,所以视图函数需要用**kwargs来接收额外的关键字参数
urls.py
1 from django.conf.urls import url,include 2 from django.contrib import admin 3 from app01 import views 4 from django.views.static import serve 5 from bbs import settings 6 7 urlpatterns = [ 8 9 # 注册 10 url(r'^reg/', views.register, name='reg'), 11 # 登录功能 12 url(r'^login/', views.login, name='log'), 13 # 登录验证码 14 url(r'^get_code/', views.get_code, name='g_code'), 15 # 首页 16 url(r'^home/', views.home, name='home'), 17 # 修改密码 18 url(r'^set_password/',views.set_password, name='set_pwd'), 19 # 退出登录 20 url(r'^logout', views.logout,name='logout'), 21 # 指定暴露后端文件资源 22 url(r'media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), 23 # 个人站点 24 url(r'^(?P<username>\w+)/$', views.site,name='site'), 25 # 个人站点侧边栏筛选 26 # url(r'^(?P<username>\w+)/category/(\d+)/',views.site), 27 # url(r'^(?P<username>\w+)/tag/(\d+)/',views.site), 28 # url(r'^(?P<username>\w+)/archive/(\w+)/',views.site), 29 # 以上三个可以合并为一条 30 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site,name='site'), 31 32 ]
views.py
1 @login_required 2 def site(request,username,**kwargs): 3 # 通过用户名获取对应的用户对象 4 user_obj = models.UserInfo.objects.filter(username=username).first() 5 if not user_obj: 6 return render(request,'error.html') 7 # 获取用户对象对应的个人站点 8 blog = user_obj.blog 9 # 查询个人站点下的所有文章 10 article_list = models.Article.objects.filter(blog=blog) 11 if kwargs: 12 # print(kwargs) # {'condition': 'tag', 'param': '2'} 13 condition = kwargs.get('condition') 14 param = kwargs.get('param') 15 if condition == 'category': 16 article_list = article_list.filter(category_id=param) 17 elif condition == 'tag': 18 article_list = article_list.filter(tags__pk=param) 19 else: 20 year,month = param 21 article_list = article_list.filter(create_time__year=year,create_time__month=month) 22 # 获取当前用户所有的分类以及分类下的文章数 23 category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk') 24 print(category_list) 25 # 获取当前用户所有的标签及标签下的文章数 26 tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk') 27 print(tag_list) 28 # 将当前用户所有的文章按照年月分组 29 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') 30 # print(date_list) 31 return render(request,'site.html',locals())
site.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>site</title> 6 {% load static %} 7 <script src="{% static 'jquery-3.6.0.min.js' %}"></script> 8 <link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"> 9 <script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script> 10 <link rel="stylesheet" href="{% static 'dist/sweetalert.css' %}"> 11 <script src="{% static 'dist/sweetalert.min.js' %}"></script> 12 <script src="{% static 'js/mysetup.js' %}"></script> 13 14 </head> 15 <body> 16 <!--导航栏开始--> 17 <nav class="navbar navbar-inverse"> 18 <div class="container-fluid"> 19 <!-- Brand and toggle get grouped for better mobile display --> 20 <div class="navbar-header"> 21 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" 22 data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> 23 <span class="sr-only">Toggle navigation</span> 24 <span class="icon-bar"></span> 25 <span class="icon-bar"></span> 26 <span class="icon-bar"></span> 27 </button> 28 <a class="navbar-brand" href="#">{{ blog.site_title }}</a> 29 </div> 30 31 <!-- Collect the nav links, forms, and other content for toggling --> 32 <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> 33 <ul class="nav navbar-nav"> 34 <li class="active"><a href="#">文章 <span class="sr-only">(current)</span></a></li> 35 <li><a href="#">随笔</a></li> 36 <li class="dropdown"> 37 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" 38 aria-expanded="false">详细信息 <span class="caret"></span></a> 39 <ul class="dropdown-menu"> 40 <li><a href="#">Action</a></li> 41 <li><a href="#">Another action</a></li> 42 <li><a href="#">Something else here</a></li> 43 <li role="separator" class="divider"></li> 44 <li><a href="#">Separated link</a></li> 45 <li role="separator" class="divider"></li> 46 <li><a href="#">One more separated link</a></li> 47 </ul> 48 </li> 49 </ul> 50 <form class="navbar-form navbar-left"> 51 <div class="form-group"> 52 <input type="text" class="form-control" placeholder="搜索文章/作者"> 53 </div> 54 <button type="submit" class="btn btn-default">搜索</button> 55 </form> 56 <ul class="nav navbar-nav navbar-right"> 57 {% if request.user.is_authenticated %} 58 <li><a href="#">{{ request.user }}</a></li> 59 <li class="dropdown"> 60 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" 61 aria-expanded="false">更多操作 <span class="caret"></span></a> 62 <ul class="dropdown-menu"> 63 <li><a href="#" data-toggle="modal" data-target="#myModal">修改密码</a></li> 64 <li><a href="#">后台管理</a></li> 65 <li><a href="#">修改头像</a></li> 66 <li role="separator" class="divider"></li> 67 <li><a href="{% url 'log_out' %}">退出登录</a></li> 68 </ul> 69 </li> 70 {% else %} 71 <li><a href="{% url 'reg' %}">注册</a></li> 72 <li><a href="{% url 'log' %}">登录</a></li> 73 {% endif %} 74 </ul> 75 <!--模态框开始--> 76 <!-- Button trigger modal --> 77 <!-- Modal --> 78 <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> 79 <div class="modal-dialog" role="document"> 80 <div class="modal-content"> 81 <div class="modal-header"> 82 <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> 83 <h4 class="modal-title text-center" id="myModalLabel">修改密码</h4> 84 </div> 85 <div class="modal-body"> 86 <div class="row"> 87 <div class="col-md-8 col-md-offset-2"> 88 <div class="form-group"> 89 <label for="id_username">用户名</label> 90 <input type="text" id="id_username" value="{{ request.user.username }}" class="form-control" disabled> 91 </div> 92 <div class="form-group"> 93 <label for="old_pwd">原密码</label> 94 <input type="password" id="old_pwd" class="form-control"> 95 </div> 96 <div class="form-group"> 97 <label for="new_pwd">新密码</label> 98 <input type="password" id="new_pwd" class="form-control"> 99 </div> 100 <div class="form-group"> 101 <label for="re_pwd">确认密码</label> 102 <input type="password" id="re_pwd" class="form-control"> 103 </div> 104 </div> 105 </div> 106 </div> 107 <div class="modal-footer"> 108 <button type="button" class="btn btn-default" data-dismiss="modal">取消</button> 109 <button type="button" id="id_btn" class="btn btn-primary">修改</button> 110 <span style="color: red"></span> 111 </div> 112 </div> 113 </div> 114 </div> 115 <!--模态框结束--> 116 </div><!-- /.navbar-collapse --> 117 </div><!-- /.container-fluid --> 118 </nav> 119 <!--导航栏结束--> 120 <div class="container-fluid"> 121 <div class="row"> 122 <div class="col-md-3"> 123 <div class="panel panel-primary"> 124 <div class="panel-heading">文章分类</div> 125 <div class="panel-body"> 126 {% for category in category_list %} 127 <p><a href="/app01/{{ username }}/category/{{ category.2 }}/">{{ category.0 }}({{ category.1 }})</a></p> 128 {% endfor %} 129 130 </div> 131 </div> 132 <div class="panel panel-danger"> 133 <div class="panel-heading">文章标签</div> 134 <div class="panel-body"> 135 {% for tag in tag_list %} 136 <p><a href="/app01/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }}({{ tag.1 }})</a></p> 137 {% endfor %} 138 </div> 139 </div> 140 <div class="panel panel-success"> 141 <div class="panel-heading ">日期归档</div> 142 <div class="panel-body"> 143 {% for date in date_list %} 144 <p><a href="/app01/{{ username }}/archive/{{ date.0|date:'Y-m' }}">{{ date.0|date:'Y年m月' }}({{ date.1 }})</a></p> 145 {% endfor %} 146 147 </div> 148 </div> 149 </div> 150 <div class="col-md-9"> 151 <ul class="media-list"> 152 {% for article_obj in article_list %} 153 <li class="media"> 154 <h4 class="media-heading"><a href="">{{ article_obj.title }}</a></h4> 155 <div class="media-left"> 156 <a href="#"> 157 <img class="media-object" src="/app01/media/{{article_obj.blog.userinfo.avatar}}" alt="..." width="80px"> 158 </a> 159 </div> 160 <div class="media-body"> 161 {{ article_obj.desc }} 162 </div> 163 <div class="pull-right"> 164 {# posted @ 2020-05-06 09:05 武沛齐 阅读(10622) 评论(43) 推荐(34) 编辑#} 165 <span>posted @ </span> 166 <span>{{ article_obj.create_time|date:'Y-m-d H:m' }} </span> 167 <span>{{ article_obj.blog.userinfo.username }} </span> 168 <span>评论<span class="glyphicon glyphicon-comment"></span> {{ article_obj.comment_num }} </span> 169 <span>点赞<span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }} </span> 170 <span><a href="#">编辑</a></span> 171 </div> 172 </li> 173 <hr> 174 {% endfor %} 175 </ul> 176 </div> 177 </div> 178 </div> 179 </body> 180 </html>
个人站点功能完毕
五、文章内容展示/侧边栏 inclusion_tag 制作
1 我们在做文章内容的展示的时候,创建一个 base.html 的模板, 2 该模板与 site.html 个人站点的页面完全一样,只是划定了9部分的区域为空 3 用site.html 页面继承该模板,然后单独书写9部分的内容,同时文章详情页也是继承该模板, 4 但是我们发现,文章详情页无法显示侧边栏的信息, 5 因为缺少数据,同时后面有多个页面都需要该部分数据, 6 因此我们将这部分数据单独抽取出来,制作成 inlusion_tag , 7 这样我们就可以在多个页面直接引用 inclusion_tag 减少代码冗余
inclusion_tag创建步骤:
1.在应用文件下创建名字必须叫 templatetags 的文件夹
2.在该文件夹下创建任意名字的py文件
3.在该py文件下书写以下两行代码:
from django import template
register=template.Library()
urls.py
1 from django.conf.urls import url 2 from app01 import views 3 from bbs1 import settings 4 from django.views.static import serve 5 6 urlpatterns = [ 7 # url(r'^admin/', admin.site.urls), 8 # 注册 9 url(r'^register/', views.register,name='reg'), 10 # 登录 11 url(r'^login/',views.login,name='log'), 12 # 登录验证码 13 url(r'^get_code/',views.get_code), 14 # 首页搭建 15 url(r'^home/',views.home,name='home'), 16 # 修改密码 17 url(r'^set_password/',views.set_password,name='set_pwd'), 18 # 退出登录 19 url(r'^logout/',views.logout,name='log_out'), 20 # 暴露后端指定资源 21 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), 22 # url(r'^app01/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}) 23 # 个人站点表 24 url(r'^(?P<username>\w+)/$',views.site,name='self_blog'), 25 # 侧边栏筛选功能 26 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site), 27 # 文章内容展示 28 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article_detail), 29 30 ]
views.py
1 def article_detail(request,username,article_id): 2 user_obj = models.UserInfo.objects.filter(username=username).first() 3 blog = user_obj.blog 4 article_obj = models.Article.objects.filter(pk=article_id,blog__userinfo__username=username).first() 5 # 判断如果文章对象不存在,则跳转到404界面 6 if not article_obj: 7 return render(request,'error.html') 8 return render(request,'article.html',locals())
mytag.py
1 from django import template 2 from app01 import models 3 from django.db.models import Count 4 from django.db.models.functions import TruncMonth 5 6 7 register = template.Library() 8 9 @register.inclusion_tag('left_menu.html') 10 def left_menu(username): 11 user_obj = models.UserInfo.objects.filter(username=username).first() 12 blog = user_obj.blog 13 # 获取当前用户所有的分类以及分类下的文章数 14 category_list = models.Category.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list( 15 'name', 'count_num', 'pk') 16 # print(category_list) 17 # 获取当前用户所有的标签及标签下的文章数 18 tag_list = models.Tag.objects.filter(blog=blog).annotate(count_num=Count('article__pk')).values_list('name', 19 'count_num', 20 'pk') 21 # print(tag_list) 22 # 将当前用户所有的文章按照年月分组 23 date_list = models.Article.objects.filter(blog=blog).annotate(month=TruncMonth('create_time')).values( 24 'month').annotate(count_num=Count('pk')).values_list('month', 'count_num') 25 # print(date_list) 26 return locals()
base.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>site</title> 6 {% load static %} 7 <script src="{% static 'jquery-3.6.0.min.js' %}"></script> 8 <link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}"> 9 <script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script> 10 <link rel="stylesheet" href="{% static 'dist/sweetalert.css' %}"> 11 <script src="{% static 'dist/sweetalert.min.js' %}"></script> 12 <script src="{% static 'js/mysetup.js' %}"></script> 13 {% block css %} 14 15 {% endblock %} 16 17 </head> 18 <body> 19 <!--导航栏开始--> 20 <nav class="navbar navbar-inverse"> 21 <div class="container-fluid"> 22 <!-- Brand and toggle get grouped for better mobile display --> 23 <div class="navbar-header"> 24 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" 25 data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> 26 <span class="sr-only">Toggle navigation</span> 27 <span class="icon-bar"></span> 28 <span class="icon-bar"></span> 29 <span class="icon-bar"></span> 30 </button> 31 <a class="navbar-brand" href="#">{{ blog.site_title }}</a> 32 </div> 33 34 <!-- Collect the nav links, forms, and other content for toggling --> 35 <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> 36 <ul class="nav navbar-nav"> 37 <li class="active"><a href="#">文章 <span class="sr-only">(current)</span></a></li> 38 <li><a href="#">随笔</a></li> 39 <li class="dropdown"> 40 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" 41 aria-expanded="false">详细信息 <span class="caret"></span></a> 42 <ul class="dropdown-menu"> 43 <li><a href="#">Action</a></li> 44 <li><a href="#">Another action</a></li> 45 <li><a href="#">Something else here</a></li> 46 <li role="separator" class="divider"></li> 47 <li><a href="#">Separated link</a></li> 48 <li role="separator" class="divider"></li> 49 <li><a href="#">One more separated link</a></li> 50 </ul> 51 </li> 52 </ul> 53 <form class="navbar-form navbar-left"> 54 <div class="form-group"> 55 <input type="text" class="form-control" placeholder="搜索文章/作者"> 56 </div> 57 <button type="submit" class="btn btn-default">搜索</button> 58 </form> 59 <ul class="nav navbar-nav navbar-right"> 60 {% if request.user.is_authenticated %} 61 <li><a href="#">{{ request.user }}</a></li> 62 <li class="dropdown"> 63 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" 64 aria-expanded="false">更多操作 <span class="caret"></span></a> 65 <ul class="dropdown-menu"> 66 <li><a href="#" data-toggle="modal" data-target="#myModal">修改密码</a></li> 67 <li><a href="#">后台管理</a></li> 68 <li><a href="#">修改头像</a></li> 69 <li role="separator" class="divider"></li> 70 <li><a href="{% url 'log_out' %}">退出登录</a></li> 71 </ul> 72 </li> 73 {% else %} 74 <li><a href="{% url 'reg' %}">注册</a></li> 75 <li><a href="{% url 'log' %}">登录</a></li> 76 {% endif %} 77 </ul> 78 <!--模态框开始--> 79 <!-- Button trigger modal --> 80 <!-- Modal --> 81 <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> 82 <div class="modal-dialog" role="document"> 83 <div class="modal-content"> 84 <div class="modal-header"> 85 <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> 86 <h4 class="modal-title text-center" id="myModalLabel">修改密码</h4> 87 </div> 88 <div class="modal-body"> 89 <div class="row"> 90 <div class="col-md-8 col-md-offset-2"> 91 <div class="form-group"> 92 <label for="id_username">用户名</label> 93 <input type="text" id="id_username" value="{{ request.user.username }}" class="form-control" disabled> 94 </div> 95 <div class="form-group"> 96 <label for="old_pwd">原密码</label> 97 <input type="password" id="old_pwd" class="form-control"> 98 </div> 99 <div class="form-group"> 100 <label for="new_pwd">新密码</label> 101 <input type="password" id="new_pwd" class="form-control"> 102 </div> 103 <div class="form-group"> 104 <label for="re_pwd">确认密码</label> 105 <input type="password" id="re_pwd" class="form-control"> 106 </div> 107 </div> 108 </div> 109 </div> 110 <div class="modal-footer"> 111 <button type="button" class="btn btn-default" data-dismiss="modal">取消</button> 112 <button type="button" id="id_btn" class="btn btn-primary">修改</button> 113 <span style="color: red"></span> 114 </div> 115 </div> 116 </div> 117 </div> 118 <!--模态框结束--> 119 </div><!-- /.navbar-collapse --> 120 </div><!-- /.container-fluid --> 121 </nav> 122 <!--导航栏结束--> 123 <div class="container-fluid"> 124 <div class="row"> 125 <div class="col-md-3"> 126 {% load mytag %} 127 {% left_menu username %} 128 </div> 129 <div class="col-md-9"> 130 {% block content %} 131 132 {% endblock %} 133 134 </div> 135 </div> 136 </div> 137 </body> 138 </html>
site.html
1 {% extends 'base.html' %} 2 3 {% block content %} 4 <ul class="media-list"> 5 {% for article_obj in article_list %} 6 <li class="media"> 7 <h4 class="media-heading"><a href="/app01/{{ username }}/article/{{ article_obj.pk }}/">{{ article_obj.title }}</a></h4> 8 <div class="media-left"> 9 <a href="#"> 10 <img class="media-object" src="/app01/media/{{article_obj.blog.userinfo.avatar}}" alt="..." width="80px"> 11 </a> 12 </div> 13 <div class="media-body"> 14 {{ article_obj.desc }} 15 </div> 16 <div class="pull-right"> 17 {# posted @ 2020-05-06 09:05 武沛齐 阅读(10622) 评论(43) 推荐(34) 编辑#} 18 <span>posted @ </span> 19 <span>{{ article_obj.create_time|date:'Y-m-d H:m' }} </span> 20 <span>{{ article_obj.blog.userinfo.username }} </span> 21 <span>评论<span class="glyphicon glyphicon-comment"></span> {{ article_obj.comment_num }} </span> 22 <span>点赞<span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }} </span> 23 <span><a href="#">编辑</a></span> 24 </div> 25 </li> 26 <hr> 27 {% endfor %} 28 </ul> 29 {% endblock %}
left_menu.html
1 <div class="panel panel-primary"> 2 <div class="panel-heading">文章分类</div> 3 <div class="panel-body"> 4 {% for category in category_list %} 5 <p><a href="/app01/{{ username }}/category/{{ category.2 }}/">{{ category.0 }}({{ category.1 }})</a></p> 6 {% endfor %} 7 8 </div> 9 </div> 10 <div class="panel panel-danger"> 11 <div class="panel-heading">文章标签</div> 12 <div class="panel-body"> 13 {% for tag in tag_list %} 14 <p><a href="/app01/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }}({{ tag.1 }})</a></p> 15 {% endfor %} 16 </div> 17 </div> 18 <div class="panel panel-success"> 19 <div class="panel-heading ">日期归档</div> 20 <div class="panel-body"> 21 {% for date in date_list %} 22 <p><a href="/app01/{{ username }}/archive/{{ date.0|date:'Y-m' }}">{{ date.0|date:'Y年m月' }}({{ date.1 }})</a></p> 23 {% endfor %} 24 25 </div> 26 </div>
article.html
1 {% extends 'base.html' %} 2 3 4 {% block content %} 5 <h1>{{ article_obj.title }}</h1> 6 <div class="artile_content"> 7 {{ article_obj.content|safe }} 8 </div> 9 10 {% endblock %}
文章内容展示页功能完成
六、文章点赞点踩功能
1 1.在原有的文章内容展示页上添加点赞点踩的html代码和css样式, 2 2.给两个按钮添加同一个类属性,通过有无特有属性来判断用户点击的是哪个按钮 3 3.通过ajax方式请求,后端通过用户名和文章id进行逻辑判断 4 1.用户是否登录 未登录不能点赞/踩 5 2.当前用户是否是该文章的作者 是则无法给自己点赞/踩 6 3.判断用户是否给该文章点过赞/踩 点过则无法再点 7 4.返回一个字典给前端回调函数 8 1.回调函数通过状态码来判断显示的消息 9 2.同时在点赞/点踩成功时 需要在前端页面点赞/踩的数量+1 进行实时渲染
urls.py
1 from django.conf.urls import url 2 from app01 import views 3 from bbs1 import settings 4 from django.views.static import serve 5 6 urlpatterns = [ 7 # url(r'^admin/', admin.site.urls), 8 # 注册 9 url(r'^register/', views.register,name='reg'), 10 # 登录 11 url(r'^login/',views.login,name='log'), 12 # 登录验证码 13 url(r'^get_code/',views.get_code), 14 # 首页搭建 15 url(r'^home/',views.home,name='home'), 16 # 修改密码 17 url(r'^set_password/',views.set_password,name='set_pwd'), 18 # 退出登录 19 url(r'^logout/',views.logout,name='log_out'), 20 # 点赞点踩功能 21 url(r'^up_or_down/',views.up_or_down), 22 # 暴露后端指定资源 23 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), 24 # url(r'^app01/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}) 25 # 个人站点表 26 url(r'^(?P<username>\w+)/$',views.site,name='self_blog'), 27 # 侧边栏筛选功能 28 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site), 29 # 文章内容展示 30 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article_detail), 31 32 33 ]
views.py
1 def up_or_down(request): 2 ''' 3 1.用户是否登录 未登录不能点赞 4 2.用户是否给自己点赞 5 3.用户是否已经点过 6 ''' 7 if request.is_ajax(): 8 if request.is_ajax(): 9 back_dict = {'code':1000,'msg':''} 10 if request.method == 'POST': 11 article_id = request.POST.get('article_id') 12 is_up = request.POST.get('is_up') 13 is_up = json.loads(is_up) # 得到的是字符串 需要转换类型 14 # print(type(is_up)) # 布尔类型 15 if request.user.is_authenticated: 16 article_obj = models.Article.objects.filter(pk=article_id).first() 17 # 判断登录用户是否是文章的作者 如果是则不能给自己点赞点踩 18 if not request.user == article_obj.blog.userinfo: 19 # 判断用户是否已经给该文章点过赞/踩 20 is_click = models.UpAndDown.objects.filter(user=request.user,article=article_obj).first() 21 # print(is_click) # 结果为对象或者None 22 if not is_click: 23 if is_up: # 通过is_up来判断点赞还是点踩 24 back_dict['msg'] = '点赞成功' 25 models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1) 26 else: 27 back_dict['msg'] = '点踩成功' 28 models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1) 29 models.UpAndDown.objects.create(user=request.user,article=article_obj,is_up=is_up) 30 else: 31 # 查看数据库中用户给当前文章点的是赞还是踩 32 clicked = is_click.is_up 33 if clicked: 34 back_dict['code'] = 1001 35 back_dict['msg'] = '你已经点过赞啦' 36 else: 37 back_dict['code'] = 1001 38 back_dict['msg'] = '你已经点过踩啦' 39 else: 40 back_dict['code'] = 1002 41 back_dict['msg'] = '自己不能给自己点赞/踩哦!' 42 else: 43 back_dict['code'] = 1003 44 back_dict['msg'] = '请先<a href="/app01/login/">登录!</a>' 45 return JsonResponse(back_dict) 46 return HttpResponse('ok')
article.html
1 {% extends 'base.html' %} 2 3 {% block css %} 4 {% load static %} 5 <style> 6 #div_digg { 7 float: right; 8 margin-bottom: 10px; 9 margin-right: 30px; 10 font-size: 12px; 11 width: 128px; 12 text-align: center; 13 margin-top: 10px; 14 } 15 .diggit { 16 float: left; 17 width: 46px; 18 height: 52px; 19 background: url({% static 'img/upup.gif' %}) no-repeat; 20 text-align: center; 21 cursor: pointer; 22 margin-top: 2px; 23 padding-top: 5px; 24 } 25 .buryit { 26 float: right; 27 margin-left: 20px; 28 width: 46px; 29 height: 52px; 30 background: url({% static 'img/downdown.gif' %}) no-repeat; 31 text-align: center; 32 cursor: pointer; 33 margin-top: 2px; 34 padding-top: 5px; 35 } 36 .clear { 37 clear: both; 38 } 39 .diggword { 40 margin-top: 5px; 41 margin-left: 0; 42 font-size: 12px; 43 color: #808080; 44 } 45 </style> 46 {% endblock %} 47 48 {% block content %} 49 <h1>{{ article_obj.title }}</h1> 50 <div class="artile_content"> 51 {{ article_obj.content|safe }} 52 </div> 53 {# 点赞功能开始#} 54 <div id="div_digg"> 55 <div class="diggit action"> 56 <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span> 57 </div> 58 <div class="buryit action"> 59 <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span> 60 </div> 61 <div class="clear"></div> 62 <div class="diggword" id="digg_tips"> 63 </div> 64 </div> 65 {#点赞功能结束#} 66 {% endblock %} 67 68 {% block js %} 69 <script> 70 $('.action').click(function () { 71 let isUp = $(this).hasClass('diggit') 72 // 将当前点击的按钮以变量的形式保存 73 let $div = $(this) 74 // console.log(isUp) 75 $.ajax({ 76 url:'/app01/up_or_down/', 77 type:'post', 78 data:{ 79 'article_id':'{{ article_id }}', 80 'is_up':isUp, 81 'csrfmiddlewaretoken':'{{ csrf_token }}' 82 }, 83 success:function(args){ 84 if(args.code === 1000){ 85 $('#digg_tips').text(args.msg) 86 // 获取点击的键的子标签的文本值 87 let oldNum = $div.children().text() 88 // 通过给页面点赞点踩数同步加1进行实时渲染 89 $div.children().text(Number(oldNum) + 1) 90 }else{ 91 $('#digg_tips').html(args.msg) 92 } 93 } 94 }) 95 }) 96 </script> 97 98 {% endblock %}
文章点赞点踩功能完成
七、文章评论功能
1 根评论 2 ''' 3 1.我们在做评论的时候先做根评论的部分不考虑子评论 根评论完成后再来做子评论 4 2.首先在页面搭建评论框的样式,点击按钮通过ajax提交数据,数据需要有文章的id和评论内容 5 3.后端接收数据进行逻辑校验:是否登录,如果登录直接获取用户提交数据,在文章表的普通字段评论数+1,并且评论表也添加相应数据,然后给前端返回一个字典 6 4.前端接收到数据后:需要对数据进行渲染 7 1.评论成功需要自动清空评论框内容 8 2.利用模板语法临时渲染评论内容到评论楼中 9 10 ''' 11 12 13 子评论 14 ''' 15 1.点击回复按钮时做如下三件事: 16 1.@被评论人的姓名 17 2.自动换行 18 3.自动聚焦评论框 19 通过给回复按钮添加自定义属性即可完成 20 ''' 21 子评论完成后需要对根评论进行修改: 22 1.添加评论判断功能:通过有无parentId判断是否子评论,传输给后端的数据中也需要添加parentId的数据 23 1.如果是子评论需要移除@username部分评论内容 24 同时html页面渲染的时候也要进行判断,如果是子评论需要在渲染评论内容的时候加上@username
urls.py
1 from django.conf.urls import url 2 from app01 import views 3 from bbs1 import settings 4 from django.views.static import serve 5 6 urlpatterns = [ 7 # url(r'^admin/', admin.site.urls), 8 # 注册 9 url(r'^register/', views.register,name='reg'), 10 # 登录 11 url(r'^login/',views.login,name='log'), 12 # 登录验证码 13 url(r'^get_code/',views.get_code), 14 # 首页搭建 15 url(r'^home/',views.home,name='home'), 16 # 修改密码 17 url(r'^set_password/',views.set_password,name='set_pwd'), 18 # 退出登录 19 url(r'^logout/',views.logout,name='log_out'), 20 # 文章点赞点踩功能 21 url(r'^up_or_down/',views.up_or_down), 22 # 文章评论功能 23 url(r'^comment/',views.comment), 24 # 暴露后端指定资源 25 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), 26 # url(r'^app01/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}) 27 # 个人站点表 28 url(r'^(?P<username>\w+)/$',views.site,name='self_blog'), 29 # 侧边栏筛选功能 30 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site), 31 # 文章内容展示 32 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article_detail), 33 34 35 ]
views.py
1 def comment(request): 2 if request.user.is_authenticated: 3 back_dict = {'code':1000,'msg':''} 4 if request.method == 'POST': 5 article_id = request.POST.get('article_id') 6 parent_id = request.POST.get('parent_id') 7 comment_content = request.POST.get('comment_content') 8 # 判断用户是否登录 如果没登录无法评论 9 if request.user.is_authenticated: 10 models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1) 11 models.Comment.objects.create(user=request.user,article_id=article_id,comment_content=comment_content,parent_id=parent_id) 12 back_dict['msg'] = '评论成功' 13 else: 14 back_dict['code'] = 1001 15 back_dict['msg'] = '用户未登录' 16 return JsonResponse(back_dict)
article.html
1 {% extends 'base.html' %} 2 3 {% block css %} 4 {% load static %} 5 <style> 6 #div_digg { 7 float: right; 8 margin-bottom: 10px; 9 margin-right: 30px; 10 font-size: 12px; 11 width: 128px; 12 text-align: center; 13 margin-top: 10px; 14 } 15 16 .diggit { 17 float: left; 18 width: 46px; 19 height: 52px; 20 background: url({% static 'img/upup.gif' %}) no-repeat; 21 text-align: center; 22 cursor: pointer; 23 margin-top: 2px; 24 padding-top: 5px; 25 } 26 27 .buryit { 28 float: right; 29 margin-left: 20px; 30 width: 46px; 31 height: 52px; 32 background: url({% static 'img/downdown.gif' %}) no-repeat; 33 text-align: center; 34 cursor: pointer; 35 margin-top: 2px; 36 padding-top: 5px; 37 } 38 39 .clear { 40 clear: both; 41 } 42 43 .diggword { 44 margin-top: 5px; 45 margin-left: 0; 46 font-size: 12px; 47 color: #808080; 48 } 49 </style> 50 {% endblock %} 51 52 {% block content %} 53 <h1>{{ article_obj.title }}</h1> 54 <div class="artile_content"> 55 {{ article_obj.content|safe }} 56 </div> 57 {# 点赞功能开始#} 58 <div class="clearfix"> 59 <div id="div_digg"> 60 <div class="diggit action"> 61 <span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span> 62 </div> 63 <div class="buryit action"> 64 <span class="burynum" id="bury_count">{{ article_obj.down_num }}</span> 65 </div> 66 <div class="clear"></div> 67 <div class="diggword" id="digg_tips"> 68 </div> 69 </div> 70 </div> 71 {#点赞功能结束#} 72 73 74 {# 文章评论楼开始#} 75 <div> 76 <ul class="list-group"> 77 {% for comment in comment_list %} 78 <li class="list-group-item"> 79 {# #29楼 2020-11-08 21:23 python_zzy#} 80 <div> 81 <span># {{ forloop.counter }}楼 </span> 82 <span>{{ comment.create_time|date:'Y-m-d H:m' }} </span> 83 <span>{{ comment.user.username }} </span> 84 <span class="pull-right reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}"><a>回复</a></span> 85 </div> 86 <div> 87 {# 判断是否是子评论 如果是 获取评论人的姓名 渲染@username#} 88 {% if comment.parent_id %} 89 <div>@{{ comment.parent.user.username }}</div> 90 {% endif %} 91 <div>{{ comment.comment_content }}</div> 92 </div> 93 </li> 94 {% endfor %} 95 </ul> 96 </div> 97 {# 文章评论楼结束#} 98 99 100 {# 文章评论功能开始#} 101 {% if request.user.is_authenticated %} 102 <div> 103 <p><span class="glyphicon glyphicon-comment"> 发表评论</span></p> 104 <div><textarea name="" id="id_comment" cols="60" rows="10"></textarea></div> 105 <button id="id_submit" class="btn btn-primary">提交评论</button> 106 <span style="color: red" id="id_info"></span> 107 </div> 108 {% else %} 109 <li><a href="{% url 'reg' %}">注册</a></li> 110 <li><a href="{% url 'log' %}">登录</a></li> 111 {% endif %} 112 {# 文章评论功能结束#} 113 {% endblock %} 114 115 {% block js %} 116 <script> 117 // 点赞点踩功能 118 $('.action').click(function () { 119 let isUp = $(this).hasClass('diggit') 120 // 将当前点击的按钮以变量的形式保存 121 let $div = $(this) 122 // console.log(isUp) 123 $.ajax({ 124 url: '/app01/up_or_down/', 125 type: 'post', 126 data: { 127 'article_id': '{{ article_id }}', 128 'is_up': isUp, 129 'csrfmiddlewaretoken': '{{ csrf_token }}' 130 }, 131 success: function (args) { 132 if (args.code === 1000) { 133 $('#digg_tips').text(args.msg) 134 // 获取点击的键的子标签的文本值 135 let oldNum = $div.children().text() 136 // 通过给页面文本值同步加1进行实时渲染 137 $div.children().text(Number(oldNum) + 1) 138 } else { 139 $('#digg_tips').html(args.msg) 140 } 141 } 142 }) 143 }) 144 // 设置全局的parentId字段 145 let parentId = null; 146 // 文章根评论功能 147 $('#id_submit').click(function () { 148 // 获取用户评论内容 149 let conTent = $('#id_comment').val() 150 // 判断当前评论是否子评论 如果是子评论 需要将我们之前手动渲染的@username去除 151 if(parentId){ 152 // 获取换行符的索引 进行切割 153 //切片顾头不顾尾 需要加1 把'\n'也切除 154 let indexNum = conTent.indexOf('\n') + 1 155 conTent = conTent.slice(indexNum) // 切除前面的 保留后面的值 156 } 157 $.ajax({ 158 url: '/app01/comment/', 159 type: 'post', 160 data: { 161 'article_id': '{{ article_id }}', 162 'parent_id':parentId, 163 'comment_content': conTent, 164 'csrfmiddlewaretoken': '{{ csrf_token }}' 165 }, 166 success: function (args) { 167 if (args.code === 1000) { 168 $('#id_info').text(args.msg) 169 $('#id_comment').val('') // 评论成功则清空评论框内容 170 let userName = '{{ request.user.username }}' 171 tmp = ` 172 <li class="list-group-item"> 173 <div> 174 <span>${userName} </span> 175 </div> 176 <div> 177 <div>${conTent}</div> 178 </div> 179 </li> 180 ` 181 // 通过模板语法将评论添加到评论楼中进行临时渲染 182 $('.list-group').append(tmp); 183 parentId = null; 184 } else { 185 $('#id_info').text(args.msg) 186 } 187 } 188 }) 189 }) 190 191 192 // 文章子评论 193 // 这里要注意回复按钮在循环内 不能用id 只能用class 194 $('.reply').click(function(){ 195 //点击回复按钮自动聚焦到评论框 @用户 换行 196 // 所以这里需要评论人的评论姓名 评论人的主键值parent_id 197 // 通过给回复按钮添加自定义属性可以获得这两个值 198 199 // 获取评论人姓名 200 let commentUserName = $(this).attr('username') 201 // 修改parentId值 获取评论人的主键值 202 parentId = $(this).attr('comment_id') 203 // 拼接信息 塞给评论框并聚焦到评论框 204 $('#id_comment').val('@' + commentUserName + '\n').focus() 205 }) 206 </script> 207 208 {% endblock %}
评论功能完毕
八、后台管理+添加文章
1 1.首先需要搭建后台管理页面 2 复制导航栏,下面部分进行2-10布局 3 在templates文件夹下创建backend文件 4 backend文件夹内的html文件专门用于存放后台管理功能的html页面 5 创建一个模板html文件,其他的用模板继承来继承该页面布局 6 2.创建文章,使用kindeditor富文本编辑器,通过阅读文档 调整样式 7 这里需要通过beautifulSoup模块来去除script标签,防止xss攻击 8 利用bs4模块来截取文本作为文章简介
urls.py
from django.conf.urls import url from app01 import views from django.contrib import admin 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='log'), # 验证码 url(r'^get_code/',views.get_code,name='get_code'), # 首页搭建 url(r'^home/',views.home,name='home'), # 修改密码 模态框 url(r'^set_password/',views.set_password,name='set_pwd'), # 退出登录 url(r'^logout',views.logout,name='logout'), # 点赞点踩表 url(r'^up_and_down/', views.up_and_down), # 评论功能 url(r'^comment/', views.comment), # 后台管理 url(r'^backend/',views.backend), # 添加文章 url(r'^add/article/',views.add_article,name='add_article'), # 文章内容展示页 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article,name='article'), # 对外开放资源接口 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), # 个人站点 url(r'^(?P<username>\w+)/$',views.site,name='site'), # 侧边栏筛选 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site), ]
views.py
@login_required def backend(request): article_list = models.Article.objects.filter(blog=request.user.blog) # 分页器 # 获取当前的页数 如果没有 默认第1页 current_page = request.GET.get('page',1) # 获取所有的文章数 all_count = article_list.count() page_obj = Pagination(current_page=current_page,all_count=all_count,per_page_num=4) # 当前页面文章展示数 利用切片获取 page_article_list = article_list[page_obj.start:page_obj.end] # 当前页文章开始数 article_first = ((page_obj.current_page - 1) * page_obj.per_page_num) + 1 # 当前页文章结尾数 article_end = page_obj.current_page * page_obj.per_page_num # 如果当前文章结尾数大于总数时 让他等于总数 if article_end > all_count: article_end = all_count return render(request,'backend/backend.html',locals()) from bs4 import BeautifulSoup def add_article(request): if request.method == 'POST': title = request.POST.get('title') content = request.POST.get('article') category_id = request.POST.get('radio_button') tag_id_list = request.POST.getlist('checkbox_button') # 实例出beautifulSoup对象 soup = BeautifulSoup(content,'html.parser') # 获取html所有标签 tags = soup.find_all() for tag in tags: # print(tag.name) # xss攻击预防 如果有script标签 直接删除 if tag.name == 'script': tag.decompose() # 删除该标签 # desc = content[0:150] # 用beautifulSoup获取文本 然后截取文本 desc = soup.text[0:150] # 添加文章 article_obj = models.Article.objects.create(title=title, desc=desc, # 内容要用beautifulSoup对象处理后的内容 content=str(soup), category_id=category_id, blog=request.user.blog) # 添加文章和标签关联表对应关系数据 article_obj_list = [] for tag_id in tag_id_list: article_tag_obj = models.Article2Tag(article=article_obj,tag_id=tag_id) article_obj_list.append(article_tag_obj) # 批量插入 models.Article2Tag.objects.bulk_create(article_obj_list) category_list = models.Category.objects.filter(blog=request.user.blog) tag_list = models.Tag.objects.filter(blog=request.user.blog) return render(request,'backend/add_article.html',locals())
base.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> {% load static %} <script src="{% static 'jQuery.js' %}"></script> <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> <!--导航栏开始--> <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="#">{{ request.user.blog.site_title }}</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="{% url 'home' %}">博客 <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="搜索文章/作者"> </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="/app01/{{ request.user }}/">{{ request.user }}</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="/app01/backend/">后台管理</a></li> <li role="separator" class="divider"></li> <li><a href="{% url 'logout' %}">退出登录</a></li> </ul> {% else %} <li><a href="{% url 'reg' %}">注册</a></li> <li><a href="{% url 'log' %}">登录</a></li> {% endif %} </ul> </li> </ul> <!-- Modal --> <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">×</span></button> <h4 class="modal-title text-center" id="myModalLabel">修改密码</h4> </div> <div class="modal-body"> <div class="row"> <div class="col-md-8 col-lg-offset-2"> {% csrf_token %} <div class="form-group"> <label for="id_username">用户名</label> <input type="text" id="id_username" class="form-control" value="{{ request.user.username }}" disabled> </div> <div class="form-group"> <label for="old_pwd">原密码</label> <input type="password" id="old_pwd" class="form-control"> </div> <div class="form-group"> <label for="new_pwd">新密码</label> <input type="password" id="new_pwd" class="form-control"> </div> <div class="form-group"> <label for="re_pwd">确认密码</label> <input type="password" id="re_pwd" class="form-control"> </div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">取消</button> <button type="button" id="id_btn" class="btn btn-primary">确认修改</button> <span id="id_info" style="color: red"></span> </div> </div> </div> </div> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <!--导航栏结束--> <div class="container-fluid"> <div class="row"> <div class="col-md-2"> <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true"> <div class="panel panel-default"> <div class="panel-heading" role="tab" id="headingOne"> <h4 class="panel-title"> <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne"> 更多操作 </a> </h4> </div> <div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne"> <div class="panel-body"><a href="{% url 'add_article' %}">添加文章</a></div> <div class="panel-body"><a href="">添加随笔</a></div> <div class="panel-body"><a href="">草稿箱</a></div> <div class="panel-body"><a href="">文件</a></div> </div> </div> </div> </div> <div class="col-md-10"> <div> <!-- Nav tabs --> <ul class="nav nav-tabs" role="tablist"> <li role="presentation" class="active"><a href="#article" aria-controls="article" role="tab" data-toggle="tab">文章</a></li> <li role="presentation"><a href="#note" aria-controls="note" role="tab" data-toggle="tab">随笔</a> </li> <li role="presentation"><a href="#myfile" aria-controls="myfile" role="tab" data-toggle="tab">文件</a> </li> <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">设置</a></li> </ul> <!-- Tab panes --> <div class="tab-content"> <div role="tabpanel" class="tab-pane active" id="article"> {% block article %} {% endblock %} </div> <div role="tabpanel" class="tab-pane" id="note"> {% block note %} {% endblock %} </div> <div role="tabpanel" class="tab-pane" id="myfile"> {% block myfile %} {% endblock %} </div> <div role="tabpanel" class="tab-pane" id="settings"> {% block setting %} {% endblock %} </div> </div> </div> </div> </div> </div> {% block js %} {% endblock %} </body> </html>
backend/backen.html
{% extends 'backend/base.html' %} {% block article %} <table class="table table-hover table-striped"> {# 标题 发布时间 点赞 评论 操作 操作#} <tr> <th>标题</th> <th>发布时间</th> <th>评论数</th> <th>点赞数</th> <th>操作</th> <th>操作</th> </tr> {% for article in page_article_list %} <tr> <td>{{ article.title }}</td> <td>{{ article.create_time|date:'Y-m-d H:m' }}</td> <td>{{ article.up_num }}</td> <td>{{ article.comment_num }}</td> <td><a href="" class="btn btn-primary">编辑</a></td> <td><a href="" class="btn btn-danger">删除</a></td> </tr> {% endfor %} </table> <p>第{{ article_first }}-{{ article_end }}篇,共{{ page_obj.all_count }}篇</p> <div class="pull-right"> {{ page_obj.page_html|safe }} </div> {% endblock %}
backend/add_article.html
{% extends 'backend/base.html' %} {% block article %} <form action="" method="post"> {% csrf_token %} <h3>添加文章</h3> <label for="id_title">标题</label> <p><input type="text" class="form-control" name="title"></p> <div> <textarea name="article" id="article_content" cols="30" rows="10"></textarea> </div> <p> {% for category in category_list %} <label for=""><input type="radio" name="radio_button" value="{{ category.pk }}">{{ category.name }} </label> {% endfor %} </p> <p> {% for tag in tag_list %} <label for=""><input type="checkbox" name="checkbox_button" value="{{ tag.pk }}">{{ tag.name }} </label> {% endfor %} </p> <p><input type="submit" id="add_article_btn" class="btn btn-primary" value="发表文章"></p> </form> {% endblock %} {% block js %} {% load static %} <script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script> <script> KindEditor.ready(function (K) { window.editor = K.create('#article_content', { width: '100%', height:'600px', resizeType:1, }); }); </script> {% endblock %}
后台管理和添加文章功能完成
九、添加文章中的上传文件
# 添加文章页面中的上传图片实现思路: 1.这里的图片上传代码不是我们自己书写的 而是用kindeditor富文本编辑器的 所以我们需要添加对应的参数uploadJson和extraFileUploadParams两个 参数 2.然后再views.py中按照文档的要求返回指定的数据格式
在添加文章的html页面添加上传文件参数,填入路径,并开设相应的url接口
1.点击文档
2.点击编辑器使用方法
3.点击next,在编辑器初始化参数中查找到extraFileUploadParams参数,添加csrf参数,就不会出现403错误
点击添加文章,点击富文本编辑器中的图片,可以访问成功,但是却会提示上传错误
这里,是因为我们需要返回文档指定的格式,才能显示上传成功,如下:
具体代码如下:
urls.py
from django.conf.urls import url from app01 import views from django.contrib import admin 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='log'), # 验证码 url(r'^get_code/',views.get_code,name='get_code'), # 首页搭建 url(r'^home/',views.home,name='home'), # 修改密码 模态框 url(r'^set_password/',views.set_password,name='set_pwd'), # 退出登录 url(r'^logout',views.logout,name='logout'), # 点赞点踩表 url(r'^up_and_down/', views.up_and_down), # 评论功能 url(r'^comment/', views.comment), # 后台管理 url(r'^backend/',views.backend), # 添加文章 url(r'^add/article/',views.add_article,name='add_article'), # 上传文件 url(r'^upload_img/',views.upload_img), # 文章内容展示页 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article,name='article'), # 对外开放资源接口 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), # 个人站点 url(r'^(?P<username>\w+)/$',views.site,name='site'), # 侧边栏筛选 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site), ]
views.py
import os from bbs1 import settings def upload_img(request): ''' 这里返回数据格式必须按照kindeditor指定的方式返回一个字典格式,如下: // 成功时 { "error": 0, "url": "http://www.example.com/path/to/file.ext" } // 失败时 { "error": 1, "message": "错误信息" } ''' # 用户写文章上传的图片 也算静态资源 应该放在media文件夹下 # 先提前定义一个返回给编辑器的数据格式 back_dict = {'error':0,'url':''} if request.method == 'POST': # 这里的编辑器代码不是我们自己封装的 需要通过打印来获取上传文件的键名 发现是固定的'imgFile' # <MultiValueDict: {'imgFile': [<InMemoryUploadedFile: 444.jpg (image/jpeg)>]}> # print(request.FILES) file_obj = request.FILES.get('imgFile') # 拼接文件存储路径 file_dir = os.path.join(settings.BASE_DIR,'media','article_img') # 优化操作 判断文件路径是否为文件夹 如果不是 则创建 if not os.path.isdir(file_dir): os.mkdir(file_dir) # 拼接文件完整路径 file_path = os.path.join(file_dir,file_obj.name) # 路径:D:\python项目\bbs1\media\article_img\444.jpg # print(file_path) # 保存文件 with open(file_path,'wb') as f: for line in file_obj: f.write(line) # 这里的路径不能直接放file_path 因为file_path路径为D:\python项目\bbs1\... # 而我们没有开设相应的url接口 无法访问到file_path对应路径 back_dict['url'] = '/app01/media/article_img/%s/' % file_obj.name return JsonResponse(back_dict)
add_article.html
{% extends 'backend/base.html' %} {% block article %} <form action="" method="post"> {% csrf_token %} <h3>添加文章</h3> <label for="id_title">标题</label> <p><input type="text" class="form-control" name="title"></p> <div> <textarea name="article" id="article_content" cols="30" rows="10"></textarea> </div> <p> {% for category in category_list %} <label for=""><input type="radio" name="radio_button" value="{{ category.pk }}">{{ category.name }} </label> {% endfor %} </p> <p> {% for tag in tag_list %} <label for=""><input type="checkbox" name="checkbox_button" value="{{ tag.pk }}">{{ tag.name }} </label> {% endfor %} </p> <p><input type="submit" id="add_article_btn" class="btn btn-primary" value="发表文章"></p> </form> {% endblock %} {% block js %} {% load static %} <script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script> <script> KindEditor.ready(function (K) { window.editor = K.create('#article_content', { width: '100%', height:'600px', resizeType:1, uploadJson : '/app01/upload_img/', // 上传文件路径,在urls.py中开设对应的url extraFileUploadParams : { // 在上传文件额外参数中添加csrf参数 'csrfmiddlewaretoken':'{{ csrf_token }}' } }); }); </script> {% endblock %}
添加文章上传图片功能完成
十、修改头像功能
1.修改头像功能直接继承base.html的末班,然后在content部分书写修改头像部分代码 2.这里继承需要在views.py文件中通过username和blog给html传值, 3.要注意的是保存用户头像的时候,要通过类.属性的方式来保存用户头像数据,如果用update的话需要自己手动添加前缀
urls.py
from django.conf.urls import url from app01 import views from django.contrib import admin 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='log'), # 验证码 url(r'^get_code/',views.get_code,name='get_code'), # 首页搭建 url(r'^home/',views.home,name='home'), # 修改密码 模态框 url(r'^set_password/',views.set_password,name='set_pwd'), # 退出登录 url(r'^logout',views.logout,name='logout'), # 点赞点踩表 url(r'^up_and_down/', views.up_and_down), # 评论功能 url(r'^comment/', views.comment), # 后台管理 url(r'^backend/',views.backend), # 添加文章 url(r'^add/article/',views.add_article,name='add_article'), # 上传文件 url(r'^upload_img/',views.upload_img), # 修改头像 url(r'^change_avatar/',views.change_avatar), # 文章内容展示页 url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article,name='article'), # 对外开放资源接口 url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}), # 个人站点 url(r'^(?P<username>\w+)/$',views.site,name='site'), # 侧边栏筛选 url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site), ]
views.py
def change_avatar(request): if request.method == 'POST': avatar = request.FILES.get('avatar') # print(avatar) user_obj = request.user user_obj.avatar = avatar user_obj.save() return redirect('/app01/home/') username = request.user.username blog = request.user.blog return render(request,'change_avatar.html',locals())
change_avatar.html
{% extends 'base.html' %} {% block content %} {% load static %} <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} <p><img src="/app01/media/{{ request.user.avatar }}/" alt="" width="200px"></p> <div> <label for="myFile">修改头像 <img src="{% static 'img/default.png' %}" alt="" width="80px" id="idImg"> </label> <input type="file" id="myFile" style="display: none" name="avatar"> </div> <input type="submit" value="修改头像" class="btn btn-primary"> </form> {% endblock %} {% block js %} <script> $('#myFile').change(function(){ console.log(123) let readObj = new FileReader() let fileObj = $(this)[0].files[0] readObj.readAsDataURL(fileObj) readObj.onload = function(){ $('#idImg').attr('src',readObj.result) } }) </script> {% endblock %}
修改头像功能完成
目录结构:
至此,bbs项目结束!