BBS仿博客园项目 项目需求分析 项目需求(产品经理,架构师,开发组组长) 项目设计(框架的选择,数据库的选择,主要功能模块) 报价(工期,开发人员工资) 任务分发(开发组长>>>小弟开发) 测试(本地测试+测试人员测试) 交付上线
项目分析 表设计 用户表(UserInfo) 用户电话phone 用户头像avatar 用户创建时间create_time blog 》》》site 一对一个人站点表 个人站点表(Blog) 站点名称site_name 站点标题site_title 站点样式site_theme 文章标签表(Tag) 标签名称name blog >>> Blog 一对多个人站点表 文章分类表 分类名称name blog >>> Blog 一对多个人站点表 文章表 文章标题title 文章简介desc 文章详情content 文章发布时间create_time # 数据库查询优化(可写可不写,主要是为了节省跨表查询,而与点赞点踩这表表要关联,联级,所以是要用要事物) 文章评论数comment_num 文章点赞数up_num 文章点踩数down_num blog 》》》 Blog 一对多个人站点表 tags >>> Tag 多对多标签表 category 》》》 Category 一对多分类表 点赞点踩表 用户名字段user 一对多 个人站点/用户 文章字段article 一对多 文章表 点赞点踩is_up 0/1 user article is_up 1 1 1 2 1 0 1 2 0 文章评论表 用户名字段user 一对多 个人站点/用户 文章字段article 一对多 文章表 评论内容content 父评论parent(自己跟自己关联) 一对多自身 user article content parent 1 1 666 1 2 888 2 1 666
1 from django.db import models 2 3 # Create your models here. 4 from django.contrib.auth.models import AbstractUser 5 6 7 8 class UserInfo(AbstractUser): 9 10 phone = models.BigIntegerField(null=True) 11 create_time = models.DateField(auto_now_add=True) 12 #文件存放的路径,avatar,自动给你创这个文件夹,默认头像 13 #如果用户上传了一个头像,就会把这个文件放到avatar这个文件夹下面,如果不上传头像我就用默认的这个 14 avatar = models.FileField(upload_to='avatar/',default='static/img/default.jpg') 15 blog = models.OneToOneField(to='Blog',null=True) #null=True 可以不写,不要样式什么的 16 17 #个人站点 18 class Blog(models.Model): 19 site_name = models.CharField(max_length=32) 20 site_title = models.CharField(max_length=64) 21 #存css样式文件的样式 22 theme = models.CharField(max_length=32) 23 24 class Category(models.Model): 25 name = models.CharField(max_length=32) 26 blog = models.ForeignKey(to='Blog') 27 28 class Tag(models.Model): 29 name = models.CharField(max_length=32) 30 blog = models.ForeignKey(to='Blog') 31 32 class Article(models.Model): 33 title = models.CharField(max_length=32) 34 desc = models.CharField(max_length=256) 35 #存大段文本 36 content = models.TextField() 37 create_time = models.DateField(auto_now_add=True) 38 #文章的评论数,点赞数,点踩数 39 comment_num = models.IntegerField(default=0) 40 up_num = models.IntegerField(default=0) 41 down_num = models.IntegerField(default=0) 42 blog = models.ForeignKey(to='Blog',null=True) 43 category = models.ForeignKey(to='Category',null=True) 44 tag = models.ManyToManyField(to='Tag',through='Article2Tag', through_fields=('article','tag')) #文章的标签 45 46 #自创的第三张表 47 class Article2Tag(models.Model): 48 article = models.ForeignKey(to='Article') 49 tag = models.ForeignKey(to='Tag') 50 51 class UpAndDown(models.Model): 52 user = models.ForeignKey(to='UserInfo') 53 article = models.ForeignKey(to='Article') 54 #存0,1 55 is_up = models.BooleanField() 56 57 58 59 class Comment(models.Model): 60 user = models.ForeignKey(to='UserInfo') 61 article = models.ForeignKey(to='Article') 62 content = models.CharField(max_length=128) 63 create_time = models.DateField(auto_now_add=True) 64 parent = models.ForeignKey(to='self',null=True)
注意(默认值设计):
UserInfo表需要注意的(默认值):
1.phone =models.BigIntegerField(null=True)
2.create_time=models.DateField(auto_now_add=True)
3.blog = models.OneToOneField(to='Blog',null=True) 可以不要样式
Article表
1.create_time = models.DateField(auto_now_add=True)
2.blog = models.ForeignKey(to='Blog',null=True)
3.category = models.ForeignKey(to='Category',null=True)
4.content = models.TextField() #存大段文本
Comment表
1.create_time = models.DateField(auto_now_add=True)
2.parent = models.ForeignKey(to='self',null=True)
UpAndDown表
1.is_up= models.BooleanField()
用户表用的是auth认证,
1.from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):需要继承
2.settings配置
指定自己的auth用户表
AUTH_USER_MODEL = 'app01.UserInfo'
连接数据库
1.navicat创建数据库
2.
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'bbs', 'HOST':'127.0.0.1', 'PORT':3306, 'USER':'root', 'PASSWORD':'123', }}3.到__init__文件中导入
import pymysqlpymysql.install_as_MySQLdb()配置静态文件
STATICFILES_DIRS= [ os.path.join(BASE_DIR, 'static')]
数据迁移,执行两行命令python3 manage.py makemigrationspython3 manage.py migrate创建自定义from组件在app01下创建文件myforms.py
1 from django import forms 2 from django.forms import widgets 3 from app01 import models 4 5 6 class RegForm(forms.Form): 7 username = forms.CharField(max_length=8,min_length=3,label='用户名',error_messages={ 8 'max_length':'用户名最长8位', 9 'min_length':'用户名最少3位', 10 'required':'用户名不能为空', 11 },widget=widgets.TextInput(attrs={'class':'form-control'})) 12 password = forms.CharField(max_length=8, min_length=3, label='密码', error_messages={ 13 'max_length': '密码最长8位', 14 'min_length': '密码最少3位', 15 'required': '密码不能为空', 16 }, widget=widgets.PasswordInput(attrs={'class': 'form-control'})) 17 confirm_password = forms.CharField(max_length=8, min_length=3, label='确认密码', error_messages={ 18 'max_length': '确认密码最长8位', 19 'min_length': '确认密码最少3位', 20 'required': '确认密码不能为空', 21 }, widget=widgets.PasswordInput(attrs={'class': 'form-control'})) 22 email = forms.EmailField(label='邮箱',error_messages={ 23 'invalid':'邮箱格式错误', 24 'required':'邮箱不能为空' 25 },widget=forms.EmailInput(attrs={"class":'form-control'})) 26 27 # 局部钩子 校验用户名是否已存在 28 def clean_username(self): 29 username = self.cleaned_data.get('username') 30 user = models.UserInfo.objects.filter(username=username).first() 31 if user: 32 self.add_error('username','用户名已存在') 33 else: 34 return username 35 36 # 全局钩子 校验密码是否一致 37 def clean(self): 38 password = self.cleaned_data.get('password') 39 confirm_password = self.cleaned_data.get('confirm_password') 40 if not password == confirm_password: 41 self.add_error('confirm_password','两次密码不一致') 42 else: 43 return self.cleaned_data
连接路由
1 from django.conf.urls import url 2 from django.contrib import admin 3 4 urlpatterns = [ 5 url(r'^admin/', admin.site.urls), 6 ]
包裹在div中,方便调节样式,这个加大间距<div class="form-group">{{ form.label }} {{ form }}<span class="errors"></span></div>‘注册居中’
<h2 class="text-center">注册</h2>分割线
<hr>
{#input框对象点auto_id能够直接获取input的id值#}<p><label for="{{ foo.auto_id }}">{{ foo.label }}</label> {{ foo }} <span class="errors pull-right" style="color: red"></span></p>
$('#id_submit').click(function () { console.log($('#myform').serializeArray())})
$('#id_submit').click(function () { var formData = new FormData(); $.each($('#myform').serializeArray(),function(index,obj){ formData.append(obj.name,obj.value) })
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> 7 <meta name="viewport" content="width=device-width, initial-scale=1"> 8 <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> 9 <script src="/static/bootstrap-3.3.7/js/bootstrap.min.js"></script> 10 </head> 11 <body> 12 <div class="container-fluid"> 13 <div class="row"> 14 <div class="col-md-6 col-md-offset-3"> 15 <h2 class="text-center">注册</h2> 16 <hr> 17 <form id="myform"> 18 {% csrf_token %} 19 {% for foo in form_obj %} 20 <div class="form-group"> 21 {#input框对象点auto_id能够直接获取input的id值#} 22 <p><label for="{{ foo.auto_id }}">{{ foo.label }}</label> 23 {{ foo }} 24 <span class="errors pull-right" style="color: red"></span> 25 </p> 26 </div> 27 {% endfor %} 28 <div class="form-group"> 29 <label for="id_myfile">头像 30 <img src="/static/img/default.jpg" alt="" width="80" style="margin-left: 20px" id="id_img"> 31 </label> 32 <input type="file" name="myfile" id="id_myfile" style="display: none;"> 33 </div> 34 <input type="button" class="btn btn-primary pull-right" id="id_button" value="提交"> 35 </form> 36 </div> 37 </div> 38 </div> 39 40 <script> 41 $('#id_myfile').change(function () { 42 // 获取当前用户上传到的文件对象 43 var myfileObj = $(this)[0].files[0]; 44 // 需要用文件阅读器这个内置对象 45 var fileReader = new FileReader(); 46 // 将文件对象丢给文件阅读器 47 fileReader.readAsDataURL(myfileObj); 48 // 将文件对象放入img标签的src属性中 49 // 当文件对象全部加载完毕再渲染 50 fileReader.onload = function(){ 51 $('#id_img').attr('src',fileReader.result); 52 } 53 }); 54 $('#id_button').click(function () { 55 var formData = new FormData(); 56 {#console.log($('#myform').serializeArray()); 自动获取form表单中所有input框键值对#} 57 $.each($('#myform').serializeArray(),function (index,obj) { 58 {#console.log(index,obj) 知识添加了普通的键值对,文件对象需要你手动添加#} 59 formData.append(obj.name,obj.value) 60 }); 61 {#手动添加文件对象#} 62 formData.append('myfile',$('#id_myfile')[0].files[0]); 63 $.ajax({ 64 url:'', 65 type:'post', 66 data:formData, 67 // 用formdata传数据的时候需要指定两个参数 68 processData:false, 69 contentType:false, 70 success:function (data) { 71 if(data.code == 100){ 72 location.href = data.url 73 }else{ 74 $.each(data.msg,function (index,obj) { 75 // 手动拼接处forms组件渲染的input的id值 id_字段的特点 76 var targetId = '#id_' + index; 77 $(targetId).next().html(obj[0]).parent().parent().addClass('has-error') 78 }) 79 } 80 } 81 }) 82 }); 83 $('input').focus(function () { 84 $(this).next().html('').parent().parent().removeClass('has-error') 85 }) 86 87 </script> 88 </body> 89 </html>
1 from django.shortcuts import render 2 from app01 import myforms 3 from app01 import models 4 from django.http import JsonResponse 5 # Create your views here. 6 def register(request): 7 back_dic = {'code':100,'msg':''} 8 form_obj = myforms.MyForm() 9 if request.method == 'POST': 10 form_obj = myforms.MyForm(request.POST) 11 if form_obj.is_valid(): 12 data = form_obj.cleaned_data 13 # 将confirm_password去掉 14 data.pop('confirm_password') 15 # 获取用户上传的文件对象 16 file_obj = request.FILES.get('myfile') 17 # 判断用户是否上传了自己的头像 18 if file_obj: 19 # 往data添加一组键值 20 data['avatar'] = file_obj 21 models.UserInfo.objects.create_user(**data) 22 back_dic['msg'] = '注册成功' 23 back_dic['url'] = '/login/' 24 else: 25 back_dic['code'] = 101 26 back_dic['msg'] = form_obj.errors 27 return JsonResponse(back_dic) 28 return render(request,'register.html',locals())
总结
BBS 项目流程 需求分析 项目设计(架构设计,框架选择,数据库...报价) 分任务开发(小组成员开发) 测试(测试) 交付上线(运行) 用户表(AbstractUser) settings文件一定要告诉django AUTH_USER_MODEL = 'app01.UserInfo' phone avatar create_time blog 一对一个人站点 个人站点(Blog) site_name site_title site_theme 分类表 name blog 一对多个人站点 标签表 name blog 一对多个人站点 文章表 title desc content create_time # 数据库优化 comment_num up_num down_num blog 一对多个人站点 category 一对多分类表 tag 多对多标签表 点赞点踩表 user 一对多用户 article 一对多文章 is_up 0/1 评论表 user 一对多用户 article 一对多文章 comment parent to='self',null=True 1.写forms组件 username password confirm_password email # 局部钩子 校验用户名是否存在 # 全局钩子 校验密码是否一致 2.搭建注册页面 1.利用forms组件渲染前端页面,手动添加获取用户头像的input框 2.将img标签放入label中,将input框隐藏 3.利用文件阅读器动态展示用户上传的头像 注意:需要等待文件阅读器读取完毕之后再赋值给src属性,利用onload 4.ajax发送post请求 利用内置对象FormData传递数据 利用form标签序列化数组 手动获取文件对象$('[input="file"]')[0].files[0] formdata发数据需要手动修改两个参数 processData:false contentType:false 后端 利用cleaned_data是一个大字典特性,将confirm_password键值去掉 手动获取用户头像,判断用户是否上传头像,再决定要不要放入cleaned_data字典中 利用**{}将字典打散成关键字参数的形式 ps:在用ajax做前后端交互的时候通常后端都会实现定义一个字典作为数据交互的媒介 ps:img标签src属性可以放文件路径,也可以放文件二进制数据,还可以放url!
# 昨日内容回顾# 登录# 1.搭建前端页面(用户名,密码,图片验证码)# 2.图片如何动态生成,实时变化# 1.img标签的src它既可以写文件路径,也可以放图片二进制数据,还可以放url# 2.推导步骤读取本地的图片二进制数据# 3.利用pillow模块动态生成图片(最新是6.0版本,建议使用4.0~5.0)# from PIL import Image,ImageDraw,ImageFont# Image.new("RGB",(长,宽),'red')# Image.new("RGB",(长,宽),(255,255,255))# 4.为了能够让图片的颜色也能动态变化# import random# def get_random():# return random.randint(0,255),random.randint(0,255),random.randint(0,255)# 5.利用内存管理器模块io# from io import BytesIO# io_obj = BytesIO() # 就当成文件句柄# img_obj = Image.new("RGB",(长,宽),'red')# img_obj.save(io_obj,'png')# 6.图片上写字,并生成随机验证码# 1.生成一个画笔对象(将生成好的图片当做参数传入实例化产生对象)# 2.生成一个字体对象(字体文件.ttf,字体大小)# 3.随机验证码(数字+小写字母+大写字母)# 外层for循环规定验证码的位数# 内部利用random.choice每次从数字+小写字母+大写字母三个中取一个# 利用画笔对象往图片上写字(写的位置xy,写的内容,随机颜色数,字体对象)# 4.将生成好的随机验证码保存到session中为了在之后的登录页面中校验用户输入的验证码是否正确# 7.为了页面更加人性化,给展示验证码的图片绑定了点击事件。用户每点一次局部刷新验证码# 利用img标签src属性在放url的时候。一旦url发生变化。会自动出现朝当前url地址请求数据## 3.获取用户输入的用户名 密码 验证码# 先校验验证码是否一致(可以不忽略大小写,统一转成小写或大写进行比较)# 利用auth模块校验用户名和密码是否正确user_obj = auth.authenticate(username=username,password=password)# 利用auth模块记录当前用户登录状态auth.login(request,user_obj)# 4.跳转到主页# 顶部导航条# 右侧根据用户是否登录动态控制展示的内容# 当用户登录的情况下展示用户名和更多操作# 修改密码# 利用auth模块修改密码# request.user.check_password()# request.user.set_password()# request.user.save() # 一定要save()# 修改头像# 注销# auth.logout(request)# 当用户没有登录的情况下展示注册和登录###### 主页搭建# 个人站点# 侧边栏# 分类展示# 标签展示# 日期归档# 侧边栏筛选功能## 文章详情页# 文章的点赞点踩#### django admin后台管理# 1.必须是超级用户才可以登录后台管理# createasuperuser# 2.需要将模型表注册到对应应用名下的admin.py文件中#### 网站用的静态文件都放在static文件夹下# 将用户上传的静态文件单独放在另外一个文件夹(media文件夹下)# media# avater# files# jianli# ziliao### 第一件事# 规定用户上传的文件都统一放到media文件夹下# settings文件中配置用户上传的文件存放位置## 第二件事# 将media文件夹暴露给外界可以直接访问## media配置# 能够制定暴露给用户后端服务器资源文件## MEDIA_ROOT = os.path.join(BASE_DIR,'media') # 用户上传的文件会自动存放到该文件夹并且该文件夹不要你手动创建## 手动配置路由# from django.views.static import servee# # 手动配置media文件路径# url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT})## 上述配置完,就会将后端media文件夹所有的资源暴露给用户## """# 基于该方法,你可以做到任意暴露后端任何资源给用户# 在作死的边缘试探# MEDIA_ROOT = os.path.join(BASE_DIR,'app01') # 用户上传的文件会自动存放到该文件夹并且该文件夹不要你手动创建## url(r'^app01/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT})# 上述配置会将后端逻辑代码数据库代码全部暴露给用户,慎重使用# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!FBI warining!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!# """### 图片防盗链# Referer: https://www.cnblogs.com/ # 标记你上一次是从哪个页面访问过来的## 该功能可以再中间件中做# 访问频率校验# referer# 身份验证##### 日期归档## id create_time month# 1 2018-06-21 2018-06# 2 2019-08-11 2019-08# 3 2019-08-21 2019-08# 4 2019-03-11 2019-03## -官方提供# from django.db.models.functions import TruncMonth# models.Article.objects# .annotate(month=TruncMonth('create_time')) # 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### 侧边栏# 1.先搭建前端页面# 分类# 标签# 日期归档## 如果按照日期归档的时候出错了##### 2.侧边栏筛选功能# 基于已经有了的当前用户所有的文章# 进行对应的筛选(利用queryset链式操作)#
# 上周五内容回顾# 首页搭建# 用户头像展示# 网站使用的静态文件统一都放在static文件夹# 用户上传的静态文件统一放到另外一个文件夹(media)# # 1.需要在settings文件中配置# MEDIA_ROOT = os.path.join(BASE_DIR,'media')# """# 1.自动创建media文件夹# 2.用户上传的文件会自动放入其中# """# # 2.手动去urls路由层配置# from django.view.static import serve# from BBS import settings# url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT})# """# 1.将media文件内所有的资源全部暴露给用户,用户只需要输入具体的文件路径# 就可以放到文件内容# """# # 3.上述配置可以暴露后端任何文件夹的资源# MEDIA_ROOT = os.path.join(BASE_DIR,'app01')# from django.view.static import serve# from BBS import settings# url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT})# """# 1.将media文件内所有的资源全部暴露给用户,用户只需要输入具体的文件路径# 就可以放到文件内容# """# 前端头像渲染# user_obj.avatar # avatar/111.png# <img src='/media/{{user_obj.avatar}}/'/># 个人站点# url(r'^(?P<username>\w+)/',views.site)# 左侧栏+筛选# 文章分类# # 统计当前用户每个分类名及分类下的文章数# 文章标签# # 统计当前用户每个标签名及标签下的文章数# 日期归档# # 按照年月对文章进行统计# id create_time month# 1 2018-06-21 2018-06# 2 2019-08-11 2019-08# 3 2019-08-21 2019-08# 4 2019-03-11 2019-03## -官方提供# from django.db.models.functions import TruncMonth# models.Article.objects# .annotate(month=TruncMonth('create_time')) # 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## # 1.写三条路由跟分类,标签,日期对应上,让后端能够知道你想按找什么条件进行筛选# url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site)## def site(request,username,**kwargs):# pass# # 通过判断kwargs是否有值从而决定返回的页面是个人所有文章还是筛选之后的文章## inclusion_tag# 自定义inclusion_tag完成侧边栏的渲染# # 1.在对应的应用名下创建一个名为templatetags文件夹# # 2.在该文件夹内创建一个任意名字的py文件(mytag)# # 3.在文件中固定先写以下两句# from django import template## register = template.Library()## @register.inclusion_tag('left_menu.html',name='lm')# def left_menu(username):# return {}### 前端页面# {% load mytag %}# {% lm xxx %}## 文章详情页# 文章内容其实就是一堆html代码# 文章内容的渲染需要转义## 点赞点踩# # 1.先搭建点赞点踩前端样式(直接拷贝博客园样式 把代码和对应的css样式都拷贝过来)# # 2.给点赞点踩标签(给他们取一个共同的类属性),给这个类属性绑定一个点击事件# # 3.利用$(this)指代的就是当前被点击对象的特点再通过判断当前被点击点击有没有具体的类属性从而区分是点赞还是点踩# # 4.发送ajax请求# """# 后端应该单独开一个专门处理点赞点踩逻辑的视图函数(因为点赞点踩后端逻辑比较复杂)# """# 后端# """# 1.先校验当前用户是否登录# 2.当前用户点击的文件是否是自己写的# 3.判断当前用户是否已经点过了# 4.记录的数据一定要在两个地方进行修改,保持数据的一致性# """##### 文章评论# 文章的根评论# 1.前端页面渲染(用户评论框)# 2.后端评论逻辑# 3.前端渲染评论列表# render渲染# bom渲染###### 文章的子评论# 1.前端渲染评论列表# render渲染# bom渲染### 1.点击回复按钮发生了几件事# 1.把你想评论的那条评论的人名自动添加到textarea中(@+用户名)# 2.自动换行# 3.textarea自动聚焦## 2.注意给回复按钮绑定点击事件的时候,尽量不要用id,因为回复按钮不止一个,而标签id是不能重复的## 3.获取评论人名及评论id的时候,可以利用标签可以支持自定义任意的属性来帮你快速的存储及获取数据## 4.子评论在存储的时候应该将@人名清除## 5.一定要将全局的根评论id清空#### 后台管理
来源:https://www.cnblogs.com/huangxuanya/p/11054938.html