1. 项目需求
主要功能:
1.表设计
2.注册功能
forms组件使用
头像动态展示
错误信息提示
3.登陆功能
图片验证码
登入装饰器
3.首页展示
media配置
主动暴露任意资源接口
密码修改
4.个人站点展示
侧边栏展示
侧边栏筛选
侧边栏inclusion_tag
5.文章详情页
点赞点踩
评论
6.后台管理
Kind编辑器模块
添加文章
修改文章
删除文章
头像修改
2. 环境准备
前端 HTML + CSS + JavaScript
后端 Python 3.6
架构 Django 1.11.11
数据库 Mysql 5.6.47
2.1 新建Django项目
2.2 解决路径问题
项目名目录下的settings 第58行.
'DIRS': [BASE_DIR, 'templates']
2.3 建立bbs库
由于django自带的sqlite数据库对日期不敏感,所以我们换成MySQL.
使用Navicat创建bbs库.
数据库的名称 bbs 字符集 utf8mb4
CREATE DATABASE `bbs` CHARACTER SET 'utf8mb4';
2.4 Django连接MySQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1',
'POST': 3306,
'USER': 'root',
'PASSWORD': 123,
'NAME': 'bbs',
'CHARSET': 'UTF8'
}
}
在app01 应用下 __init__.py 中配置 pymysql 模块连接数据库.
import pymysql
pymysql.install_as_MySQLdb()
2.5 开放静态文件
0. 在项目目录下创建 static 静态文件目录.
1. 在static目录下 创建 js 目录, 将jQuery文件复制到 js 目录中.
2. 复制bootstarp框架文件到 static 目录中.
3. 复制SweetAlert框架文件到 SweetAlert 中, (后面加上的).
4. 去项目名文件下settings.py 中设置开放静态文件.
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'STATIC')
]
2.6 访问测试
0. 启动项目
1. 在浏览器中输入: 127.0.0.1:8000
3. 表设计
一个项目中最重要的不是业务逻辑的书写
而是前期的表设计,只要将表设计好了,后续的功能书写才会一帆风顺.
在app01 下的 models.py 使用ORM模块, 创建映射表的类.
先写普通的字段, 在写外键字段.
3.1用户表 User
用户表: 记录用户的信息
继承AbstractUser 使用Auth模块
扩展字段:
avatar 用户头像 FileField 文件类型
create_time 用户创建时间 DateField 日期 年月日
外键字段:
用户表一对一个人站点表, 外键字段建在查询评论多的一方.
blog 外键绑定个人博客表的id
from django.db import models
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
avatar = models.FileField(upload_to='avatar/', default='avatar/default.png', verbose_name='头像')
create_time = models.DateField(auto_now_add=True, verbose_name='用户创建时间')
blog = models.OneToOneField(to='Blog', null=True, verbose_name='关联博客表id')
AUTH_USER_MODEL = 'app01.UserInfo'
avatar 字段存放的文件路径 avatar/xxx.pnh
upload_to='avatar/', upload_to 参数 文本保存的位置,
default='avatar/default.png' 用户不上传头像,使用 default 参数设置的默认头像.
在项目下创建avatar目录, 找一个图片设置为 default.png 默认头像.
3.2 个人博客表 Blog
个人博客/个人站点表: 记录每个用户个人网站, 访问时查询有没有该有户的个人站点存在.
每个人站点有自己的站点名称, 站点标题, 站点样式.
https://www.cnblogs.com/python
https://www.cnblogs.com/java
https://www.cnblogs.com/python_21
字段:
site_name 站点名称 CharField 字符串类型
site_title 站点标题 CharField 字符串类型
site_theme 站点样式 CharField 字符串类型
class Blog(models.Model):
site_name = models.CharField(max_length=32, verbose_name='站点名称')
site_title = models.CharField(max_length=32, verbose_name='站点标题')
site_theme = models.CharField(max_length=32, verbose_name='站点样式')
站点样式 字段存放css/js文件的路径, 简单的操作下样式的切换.
3.3 文章表 Article
文章表记录文字的内容.
字段:
文章的标题 title CharField 字符串类型
文章简介 desc CharField 字符串类型
文章内容 content TextField 文本字段适合存大量文本信息 不需要指定 max_length
文章发布时间 create_time DateField 日期 年月日
查询优化:
虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率低下,
直接在文章表中创建三个普通的字段, 点赞点踩, 评论表, 记录数据的时候同步文章表的这三个字段.
文章的点赞数 up_num BigIntegerField 极大整数值 用8位表示
文章的点踩数 down_num BigIntegerField 极大整数值 用8位表示
文字的评论数 comment_num BigIntegerField 极大整数值 用8位表示
* 访问数不做, 换为点踩.
外键字段:
站点表 一对多 文章表 外键 blog_id 绑定站点表的id
文章标签表 多对多 文章表 虚拟字段 tag 半自动建立第三张表
文章分类表 一对一 文章表 外键 sort_id 绑定分类表id
class Article(models.Model):
title = models.CharField(max_length=32, verbose_name='标题')
desc = models.CharField(max_length=256, verbose_name='简介')
content = models.TextField(verbose_name='文章内容')
create_time = models.DateField(auto_now_add=True, verbose_name='文章创建时间')
up_num = models.BigIntegerField(default=0, verbose_name='点赞数')
down_num = models.BigIntegerField(default=0, verbose_name='点踩数')
comment_num = models.BigIntegerField(default=0, verbose_name='评论数')
blog = models.ForeignKey(to='Blog', null=True, verbose_name='关联博客表id')
sort = models.OneToOneField(to='Sort', null=True, verbose_name='关联分类表id')
tag = models.ManyToManyField(to='Tag', through='ArticleToTag',
through_fields=('article', 'tag'))
class ArticleToTag(models.Model):
article = models.ForeignKey(to='Article')
tag = models.ForeignKey(to='Tag')
3.4 文章的标签 Tag
一个文章可以打上很多个标签.
字段:
标签的名字 name CharField 字符串类型
外键字段: (这个字段存在的原因是需要在个人博客表中,侧边栏可以通过标签去查找对应的文章)
个人站点表 一对多 文章标签表
blog_id 绑定博客表的id
class Tag(models.Model):
name = models.CharField(max_length=32, verbose_name='文章标签')
blog = models.ForeignKey(to='Blog', null=True, verbose_name='绑定博客表的id')
3.5 文章的分类 Sort
一个文章属于一个类型.(一个文章就一个类, 不搞多个.)
字段:
类的名字 name CharField 字符串类型
外键字段:(这个字段存在的原因是需要在个人站点中, 侧边栏可以通过分类去查找对应的文章)
个人站点表 一对多 文章分类表
blog_id 绑定博客表的id
class Sort(models.Model):
name = models.CharField(max_length=32, verbose_name='文章分类')
blog = models.ForeignKey(to='Blog', null=True, verbose_name='绑定博客表的id')
3.6 点赞点踩表 UpAndDown
文章点赞点踩表: 记录那个用户给哪篇文章点了赞还是点了踩
字段:
用户名 user
文章名 article
点赞/点踩 is_up
id user_id article_id is_up
1 1 1 1 用户1 给第一篇文章点了赞
2 2 1 1 点赞 统计 article的 is_up 为1 数量
3 3 1 1 点踩 统计 article的 is_up 为1 数量
class UpAndDown(models.Model):
user = models.ForeignKey(to='UserInfo', verbose_name='关联用户id')
article = models.ForeignKey(to='Article', verbose_name='关联文章id')
is_up = models.BooleanField(verbose_name='是否点赞')
3.7 文字评论表 Comment
文章的评论表: 记录那个用户给哪篇文字评论了什么内容
字段:
用户名 user
文章名 article
评论内容 content
评论时间 comment_time
根评论子评论的概念
根评论就是直接评论当前发布的内容的
子评论是评论别人的评论
1.PHP是世界上最牛逼的语言
1.1 PHP是世界上最牛逼的语言.py
1.1.1 java才是.py
1.2.1 go才是.py
根评论与子评论是一对多的关系
自关联
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 (代表这个评论是给这个表第一个内容的)
class Comment(models.Model):
user = models.ForeignKey(to='UserInfo', verbose_name='关联用户id')
article = models.ForeignKey(to='Article', verbose_name='关联文章id')
content = models.CharField(max_length=256, verbose_name='评价内容')
comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
parent = models.ForeignKey(to='self', null=True, verbose_name='自关联')
只要关联了就代表它是子评论, 否则就是根评论, null=True, 不写就只能是评论了
3.8数据库迁移
python manage.py makemigrations
python manage.py migrate
4. 用户注册功能
4.1 路由层
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^register/', views.register)
]
4.2 forms组件
0. 在app01应用程序下创建 forms_module 目录
1. 在 forms_module 目录下创建 register_froms.py 文件
from django import forms
from app01 import models
class RegisterForms(forms.Form):
username = forms.CharField(
label='名称', min_length=3, max_length=8,
error_messages={
'required': '名称不能为空',
'min_length': '名称不能少于3位',
'max_length': '名称不能超过8位'},
widget=forms.widgets.TextInput(
attrs=(
{'class': 'form-control input-lg', 'style': 'border:none; background-color:transparent; color: #66AFE9;'})
)
)
password = forms.CharField(
label='密码', min_length=3, max_length=8,
error_messages={
'required': '密码不能为空',
'min_length': '密码不能少于3位',
'max_length': '密码不能超过8为'},
widget=forms.widgets.PasswordInput(
attrs=(
{'class': 'form-control input-lg', 'style': 'border:none; background-color:transparent; color: #ffaa00;'})
)
)
confirm_password = forms.CharField(
label='确认密码', min_length=3, max_length=8,
error_messages={
'required': '确认密码不能为空',
'min_length': '确认密码不能少于3位',
'max_length': '确认密码不能超过8位'},
widget=forms.widgets.PasswordInput(
attrs={'class': 'form-control input-lg',
'style': 'border:none; background-color:transparent; color: #ffaa00;'}
)
)
email = forms.EmailField(
label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式不正确'},
widget=forms.widgets.EmailInput(
attrs=(
{'class': 'form-control input-lg', 'style': 'border:none; background-color:transparent; color: #ffaa00;'})
)
)
def clean_username(self):
username = self.cleaned_data.get('username')
is_exist = models.UserInfo.objects.filter(username=username)
if is_exist:
self.add_error('username', '名称已经存在')
return username
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if password != confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data
4.3 视图层
from django.shortcuts import render, redirect, HttpResponse
from app01.forms_module.register_forms import RegisterForms
def register(request):
forms_obj = RegisterForms()
return render(request, 'register.html', locals())
4.4 模板层
在templates目录下创建 register.html
form表单 autocomplete="off" 浏览器不自动填充数据.
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册页面title>
{% load static %}
<script src="{% static 'js/jquery-3.6.0.min.js' %}">script>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}">script>
<style>
body {
background-image: url("{% static 'background/register.jpg'%}");
background-size: cover;
background-repeat: no-repeat;
color: #d4ff00;
font-size: 18px;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-transition-delay: 111111s;
-webkit-transition: color 11111s ease-out, background-color 111111s ease-out;
}
style>
head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center">注册h1>
<form action="" autocomplete="off" id="form_table">
{% csrf_token %}
{% for input in forms_obj %}
<div class="form-group">
<label for="{{ input.auto_id }}">{{ input.label }}label>
{{ input }}
<span style="color: red; " class="pull-right">span>
div>
{% endfor %}
<label for="my_avatar">头像
<img src="{% static 'img/default.png' %}" alt="" id='my_img' width="80px" style="margin-left:20px">
label>
<input type="file" name="avatar" id="my_avatar" style="display: none">
<p>
<input type="button" id='btn1' value="注册" class="btn-primary btn-lg btn-block"
style="border: none; opacity:0.3; color: orange; font-size: 20px">
p>
form>
div>
div>
div>
<script>
$('#my_avatar').change(function () {
let FileReaderObj = new FileReader()
let UpFileObj = $(this)[0].files[0];
FileReaderObj.readAsDataURL(UpFileObj)
FileReaderObj.onload = function () {
$('#my_img').attr('src', FileReaderObj.result)
console.log(FileReaderObj.result)
}
})
$('#btn1').on('click', function () {
let FormDataObj = new FormData()
let Text_Data = $('#form_table').serializeArray()
$.each(Text_Data, function (index, obj) {
FormDataObj.append(obj.name, obj.value)
})
FormDataObj.append('avatar', $('#my_avatar')[0].files[0])
$.ajax({
url: '',
type: 'post',
data: FormDataObj,
contentType: false,
processData: false,
success: function (args) {
if (args.code === 200) {
window.location.href = args.url
} else {
$.each(args.error, function (key, value) {
console.log(key, value)
let InputId = '#id_' + key
$(InputId).css('border', '')
$(InputId).next().text(value[0]).parent().addClass('has-error')
})
}
}
})
})
$('input').focus(function () {
$(this).next().text('').parent().removeClass('has-error')
$(this).css('border', 'none')
})
script>
body>
html>
let Text_Data = $('#form_table').serializeArray()
each遍历列表获取到两个参数,第一个是index索引, 第二个是索引对应的值.
$.each(Text_Data, function (index, obj) {
console.log(index, obj)...
args.error
each遍历对象获取到两个参数,第一个是属性, 第二个是属性值
$.each(args.error, function (key, value) {
console.log(key, value) ...
forms组件 表单错误信息提示
4.5 业务逻辑
from django.shortcuts import render, redirect, HttpResponse
from app01.forms_module.register_forms import RegisterForms
from app01 import models
from django.http import JsonResponse
def register(request):
forms_obj = RegisterForms()
if request.is_ajax():
forms_obj = RegisterForms(request.POST)
if forms_obj.is_valid():
cleaned_data = forms_obj.cleaned_data
cleaned_data.pop('confirm_password')
avatar_obj = request.FILES.get('avatar')
if avatar_obj:
cleaned_data['avatar'] = avatar_obj
models.UserInfo.objects.create_user(**cleaned_data)
print('写入数据成功!')
back_dir = {'code': 200, 'url': '/login/'}
else:
back_dir = {'code': 400, 'error': forms_obj.errors}
return JsonResponse(back_dir)
return render(request, 'register.html', locals())
5. 登入功能
5.1 路由层
url(r'^login/', views.login),
5.2 视图层
def login(request):
return render(request, 'login.html')
5.3 模板层
img标签的src属性
1. 图片路径
2. url
3. 图片的二进制数据
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登入页面title>
{% load static %}
<script src="{% static 'js/jquery-3.6.0.min.js' %}">script>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}">script>
<script src="{% static 'bootstrap-sweetalert-master/dist/sweetalert.min.js' %}">script>
<link rel="stylesheet" href="{% static 'bootstrap-sweetalert-master/dist/sweetalert.css' %}">
<style>
body {
background-image: url("{% static 'background/login.jpeg'%}");
background-size: cover;
background-repeat: no-repeat;
color: #364046;
font-size: 18px;
}
.transparent {
background-color: transparent;
border: none;
color: #01fdfc;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-transition-delay: 111111s;
-webkit-transition: color 11111s ease-out, background-color 111111s ease-out;
}
.sweetAlert {
width: 22em;
color: red;
background-color: transparent;
}
style>
head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="" id="id_form">
<h1 class="text-center">登入h1>
{% csrf_token %}
<div class="form-group">
<label for="id_username">账户label>
<input type="text" id="id_username" name="username" minlength="3" maxlength="8"
class="form-control transparent">
div>
<div class="form-group">
<label for="id_password">密码label>
<input type="password" id="id_password" name="password" class="form-control transparent">
div>
<label for="id_code">验证码(点击图片刷新)label>
<div class="row">
<div class="col-md-6">
<input type="text" id="id_code" name="code" class="form-control transparent">
div>
<div class="col-md-6">
<img src="/get_code/?num=0" id="refresh" alt="" width="445" height="35" style="opacity: 0.5;">
div>
div>
<input type="button" value="登入" class="btn-info btn-block" style="opacity: 0.3; margin-top: 30px">
form>
div>
div>
div>
<script>
$('#refresh').on('click', function () {
let url = $(this).attr('src')
let UrlList = url.split('=')
let num = parseInt(UrlList[1])
num += 1
UrlList[1] = num.toString()
url = UrlList.join('=')
$(this).attr('src', url)
})
$('input:button').on('click', function () {
let form_data = $('#id_form').serializeArray()
let dict_obj = new Object()
$.each(form_data, function (index, obj) {
dict_obj[obj.name] = obj.value
})
$.ajax({
url: '',
type: 'post',
data: dict_obj,
success: function (args) {
if (args.code === 200) {
window.location.href = args.url
} else {
swal({
title: args.error_msg,
type: 'error',
customClass: "sweetAlert"
})
}
}
})
})
script>
body>
html>
每次修改img标签src属性的值都会重新获取图片的数据.
在访问的路径后面修改任意的参数, 就能不影响访问地址的情况下重新获取一次图片数据,
当前登入页面的表单信息不被刷新.
$('#refresh').on('click', function () {
let url = $(this).attr('src')
let UrlList = url.split('=')
let num = parseInt(UrlList[1])
num += 1
UrlList[1] = num.toString()
url = UrlList.join('=')
$(this).attr('src', url)
})
swal({
title: args.error_msg,
type: 'error',
customClass: "sweetAlert"
})
5.4 业务逻辑
def login(request):
if request.is_ajax():
table_code = request.session.get('code')
submit_code = request.POST.get('code')
print(f'表中的验证码{table_code} 提交的验证码{submit_code}')
if table_code.upper() == submit_code.upper():
username = request.POST.get('username')
password = request.POST.get('password')
is_exists = auth.authenticate(username=username, password=password)
if is_exists:
auth.login(request, is_exists)
back_dir = {'code': 200, 'url': '/home/'}
else:
back_dir = {'code': 400, 'error_msg': '名称或密码错误'}
else:
back_dir = {'code': 400, 'error_msg': '验证码错误'}
print(back_dir)
return JsonResponse(back_dir)
return render(request, 'login.html')
6. 验证码功能
6.1 路由层
url(r'^get_code/', views.get_code),
6.2 视图层
ps: Django 是由 mange.py 启动的, 其他的子程序都是以模块的形式被到过来运行的
os.getcwd() 获取当前路径, 获取的都是项目的绝对路径.
import os
print(os.getcwd())
1. 验证码返回测试
返回一张验证码图片用于测试, 查看前端的效果.
def get_code(request):
with open('static/img/code_test.jpg', mode='rb') as rf:
data = rf.read()
return HttpResponse(data)
2. 随机背景图片1
pillow 图片相关模块
安装:
pip install pillow
导入:
from PIL import Image, ImageDraw, ImageFont
Image: 生成图片
ImageDraw: 在图片上写字
ImageTont: 控制字体的颜色
在PyChrm的Terminal中输入下载包的命令:
pip3.6 install pillow
Image的方法:
.new(mode, size, color=0) 生成图片对象
mode 模式 RGB
size 大小 (长, 宽) 是一个数组, 单位px.
color 颜色, 可以是三基色数组, 颜色的单词.
.save(fp, format=None)
fp: 文件句柄
format: 文件格式
from PIL import Image, ImageDraw, ImageFont
def get_color():
from random import randint
return randint(0, 255), randint(0, 255), randint(0, 255)
def get_code(request):
image_obj = Image.new('RGB', (440, 35), get_color())
with open('static/img/random_img.png', mode='wb') as wf:
image_obj.save(wf, format='png')
with open('static/img/random_img.png', mode='rb') as rf:
data = rf.read()
return HttpResponse(data)
缺点: IO操作频繁, 每次获取验证图片都要进行读写操作.
3. 随机背景图片2
优化: 避免频繁的IO操作.
内存管理模块(内置)
BytesIO: 临时存储数据, 返回的数据是二进制.
StringIO: 临时存储数据, 返回的数据是字符串.
导入模块
from IO import BytesIO, StringIO
生成对象
io_obj = BytesIO()
读数据:
io_obj.getvalue()
from PIL import Image, ImageDraw, ImageFont
def get_color():
from random import randint
return randint(0, 255), randint(0, 255), randint(0, 255)
def get_code(request):
image_obj = Image.new('RGB', (440, 35), get_color())
from io import BytesIO
io_obj = BytesIO()
image_obj.save(io_obj, format='png')
return HttpResponse(io_obj.getvalue())
效果与上面一致.
4. 图片上写字
ps: 电脑上展示的文字格式对应了一个.ttf/ .itf文件.
下载字体文件, 选一个免费商用, 找一个自己喜欢的字体点击下载.
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
在static目录下新建一个font目录.
将下载好的字体文件复制到font目录中.
图片对象
image_obj = Image.new('RGB', (440, 35), get_color())
画笔对象 = ImageDraw.Draw(图片对象)
img_draw = ImageDraw.Draw(image_obj)
产生一个字体对象
img_font = ImageFont.truetype(字体文件位置, 字体大小)
画笔对象将一个个字符写在图片上
img_draw.text ( (x 水平, y 垂直), 需要写的文字, 文字颜色, 字体对象)
from PIL import Image, ImageDraw, ImageFont
from random import randint, choice
def get_color():
return randint(0, 255), randint(0, 255), randint(0, 255)
def get_code(request):
image_obj = Image.new('RGB', (440, 35), get_color())
img_draw = ImageDraw.Draw(image_obj)
img_font = ImageFont.truetype('static/font/思源黑体TW-Normal.otf', 30)
code = ''
for i in range(5):
letter_upper = chr(randint(65, 90))
letter_lower = chr(randint(97, 122))
str_number = str(randint(0, 9))
character = choice([letter_upper, letter_lower, str_number])
img_draw.text((i * 80+50, -5), character, get_color(), img_font)
""" 字体文字 图片位置 440
i 0 ~ 4
0 * 80 + 40 = 40 1 * 80 + 40 = 120 2 * 80 + 40 = 200 3 * 80 + 40 = 280 4 * 80 + 40 = 360
---> 文字排序开来 40 120 200 280 360 --->
"""
code += character
request.session['code'] = code
print(f'验证码是:{code}')
from io import BytesIO
io_obj = BytesIO()
image_obj.save(io_obj, format='png')
return HttpResponse(io_obj.getvalue())