django 项目 bbs 博客园

今日内容简介
一:项目开发流程
二:表设计
三:数据库表创建及同步
四:注册功能
五:登录功能
六:首页搭建
七:admin后台管理
八:用户头像展示
九:图片防盗链
十:个人站点
十一:侧边栏筛选功能
十二:文章详情页
十三:文章点赞点踩
十四:文章评论
十五:后台管理
十六:添加文章
十七:kindeditor 富文本编辑器
十八:编辑器上传图片
十九:修改用户头像
二十:bbs项目总结

项目开始
一:项目开发流程

# 1.需求分析
架构师+产品经理+开发者组长
在跟客户谈需求之前,会大致先了解客户的需求,然后自己先设计一套比较好写方案
在跟客户沟通交流中引导客户往我们之前想好的方案上面靠
形成一个初步的方案

# 2.项目设计
架构师干的活
编程语言选择
框架选择
数据库选择
	主库:MySQL,postgreSQL,...
  缓存数据库:redis、mongodb、memcache...
功能划分
	将整个项目划分成几个功能模块
找组长开会
	给每个组分发任务
项目报价
	技术这块需要多少人,多少天(一个程序员一天1500~2000计算(大致))
  产品经理公司层面 再加点钱
  	公司财务签字确认
    公司老板签字确认
  产品经理去跟客户沟通
 	
  后续需要加功能 继续加钱

# 3.分组开发
组长找组员开会,安排各自功能模块
 我们其实就是在架构师设计好的框架里面填写代码而已(码畜)

 我们在写代码的时候 写完需要自己先测试是否有bug
 如果是一些显而易见的bug,你没有避免而是直接交给了测试部门测出来
 那你可能就需要被扣绩效了(一定要跟测试小姐姐搞好关系)
薪资组成	15K(合理合规合法的避税)
	底薪	10K
  绩效	3K
  岗位津贴 1K
  生活补贴 1K
  
# 4.测试
测试部门测试你的代码
压力测试
...
# 5.交付上线
1.交给对方的运维人员
 2.直接上线到我们的服务器上 收取维护费用
 3.其他...

二:表设计

"""
一个项目中最最最重要的不是业务逻辑的书写
而是前期的表设计,只要将表设计好了,后续的功能书写才会一帆风顺

bbs表设计
1.用户表
	继承AbstractUser
	扩展
		phone 电话号码
		avatar  用户头像
		create_time  创建时间
	
	外键字段
		一对一个人站点表

2.个人站点表
	site_name 站点名称
	site_title 	 站点标题
	site_theme	站点样式

3.文章标签表
	name		标签名
	
	外键字段
		一对多个人站点

4.文章分类表
	name		分类名
	
	外键字段
		一对多个人站点

5.文章表
	title	文章标题
	desc	文章简介
	content	文章内容
	create_time 发布时间
	
	数据库字段设计优化(******)
		(虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率)
	up_num					点赞数
	down_num				点踩数
	comment_num 		评论数
	
	外键字段
		一对多个人站点
		多对多文章标签
		一对多文章分类
		
	

6.点赞点踩表
	记录哪个用户给哪篇文章点了赞还是点了踩
	user						ForeignKey(to="User")				
	article					ForeignKey(to="Article")	
	is_up						BooleanField()
	
	1				1				1
	1				2				1
	1				3				0
	2				1				1
	

7.文章评论表
	记录哪个用户给哪篇文章写了哪些评论内容
	user						ForeignKey(to="User")				
	article					ForeignKey(to="Article")
	content					CharField()
	comment_time		DateField()
	# 自关联
	parent					ForeignKey(to="Comment",null=True)		
	# ORM专门提供的自关联写法	
	parent					ForeignKey(to="self",null=True)
	
id	user_id			article_id				parent_id
1		 1						1										
2		 2						1										1					
	
根评论子评论的概念
根评论就是直接评论当前发布的内容的
	
子评论是评论别人的评论
	1.PHP是世界上最牛逼的语言
		1.1 python才是最牛逼的
			1.2 java才是
	
根评论与子评论是一对多的关系

	
"""

三:数据库表创建及同步

"""
由于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
随机字符串
...
"""

五:登录功能

"""
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.直接写爬虫把对方网址的所有资源直接下载到我们自己的服务器上

十:个人站点

# 全是每个用户都可以有自己的站点样式
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())

十二:文章详情页
十三:文章点赞点踩
十四:文章评论
十五:后台管理
十六:添加文章
十七:kindeditor 富文本编辑器
十八:编辑器上传图片
十九:修改用户头像
二十:bbs项目总结

posted @ 2021-05-31 11:23  meng神  阅读(60)  评论(0)    收藏  举报