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)
注意(默认值设计):css
UserInfo表须要注意的(默认值):html
1.phone =models.BigIntegerField(null=True)前端
2.create_time=models.DateField(auto_now_add=True)python
3.blog = models.OneToOneField(to='Blog',null=True) 能够不要样式mysql
Article表jquery
1.create_time = models.DateField(auto_now_add=True)ajax
2.blog = models.ForeignKey(to='Blog',null=True)sql
3.category = models.ForeignKey(to='Category',null=True)数据库
4.content = models.TextField() #存大段文本django
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 pymysql
pymysql.install_as_MySQLdb()
配置静态文件
STATICFILES_DIRS= [
os.path.join(BASE_DIR, 'static')
]
数据迁移,执行两行命令
python3 manage.py makemigrations
python3 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
# 局部钩子 校验用户名是否存在
# 全局钩子 校验密码是否一致
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清空#### 后台管理