目录
2.1 创建django项目, 配置设置信息, 连接数据库等
注意:static文件夹已经默认开启,可以从浏览器进行访问,所以static和media文件夹下不能放重要文件。
学习完MySQL数据库和Django框架之后, 我们结合前后端来写一个项目.此项目主要是模仿博客园.
1.注册功能
2.登录功能
3.个人主页: 文章展示,侧边栏过滤>> 按照时间,标签, 分类
4. 文章详情 : 点赞点踩. 评论
5.后台管理; 个人文案行展示[增删改查]
6.发布文章>> 富文本编辑器, xss攻击处理
python 3.8 django 2.2.2 mysql 5.6.4 jquery 2.x bootstrap3
- # 时间国际化
-
- LANGUAGE_CODE = 'Asia-Shanghai'
-
- TIME_ZONE = 'UTC'
-
- USE_I18N = True
-
- USE_L10N = True
-
- USE_TZ = False
-
-
- # 连接数据库
-
-
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.mysql',
- 'NAME': 'bbs',
- 'HOST': '127.0.0.1',
- 'PORT': 3306,
- 'USER': 'root',
- 'PASSWORD':'123',
- 'CHARSET': 'UTF8'
-
-
- }
- }
-
-
-
- # 静态资源配置
-
- STATIC_URL = '/static/'
- STATICFILES_DIRS = [
- os.path.join(BASE_DIR,'static')
- ]
-
- class UserInfo(AbstractUser):
- phone = models.CharField(max_length=32,null=True,)
- avatar = models.FileField(upload_to='avatar',default='avatar/default.jpg')
- blog = models.OneToOneField(to='Blog',on_delete=models.CASCADE,null=True)
- from django.db import models
-
- # Create your models here.
- from django.contrib.auth.models import AbstractUser
-
-
- class UserInfo(AbstractUser): # 继承AbstractUser表 只用写auth表中没有的字段
- phone = models.CharField(max_length=32, null=True, verbose_name='用户手机号')
- # upload_to是文件保存在什么路径
- icon = models.FileField(upload_to='icon/', default='icon/default.png', null=True, verbose_name='用户头像')
- # 用户表和博客表一对一
- blog = models.OneToOneField(to='Blog', on_delete=models.CASCADE, null=True)
-
-
- class Blog(models.Model):
- title = models.CharField(max_length=32, null=True, verbose_name='主标题')
- site_title = models.CharField(max_length=32, null=True, verbose_name='副标题')
- site_style = models.CharField(max_length=64, null=True, verbose_name='站点样式')
-
-
- class Tag(models.Model):
- name = models.CharField(max_length=32, verbose_name='标签名', null=True)
- # 标签和博客是一对多 一个博客有多个标签
- blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
-
-
- class Classify(models.Model):
- name = models.CharField(max_length=32, verbose_name='分类名')
- # 分类和博客是一对多关系 一个博客有多个分类
- blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
-
-
- class Article(models.Model):
- title = models.CharField(max_length=32, verbose_name='文章标题')
- desc = models.CharField(max_length=255, verbose_name='文章摘要')
- content = models.TextField(verbose_name='文章内容')
- create_time = models.DateTimeField(auto_now_add=True) # 第一次创建时自动添加时间
- # 文章和分类表是一对多 一个分类有多篇文章
- classify = models.ForeignKey(to='Classify', on_delete=models.CASCADE)
- # 文章和标签是多对多关系 自动创建第三张表
- tag = models.ManyToManyField(to='Tag')
- # 文章和博客是一对多关系 一个博客对应多篇文章
- blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
-
-
- class UpAndDown(models.Model):
- create_time = models.DateTimeField(auto_now_add=True, verbose_name='点赞点踩时间')
- # 和用户表是一对多关系 一个用户可以有多条点赞点踩记录
- user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
- # 和文章也是一对多
- article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
- # 1代表点赞 0代表点踩
- is_up = models.BooleanField(verbose_name='是否点赞')
-
-
- class Comment(models.Model):
- content = models.CharField(max_length=64, verbose_name='评论内容')
- create_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
- user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
- article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
- # 自关联字段 只能存已有评论的主键值
- parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
- # 自关联的其他方式
- # parent = models.ForeignKey(to='Comment', on_delete=models.CASCADE)
- # parent = models.IntegerField(null=Ture)
- python manage.py makemigrations
-
- python manage.py migrate
- from django import forms
- from django.forms import widgets
- from blog.models import UserInfo
- from django.core.exceptions import ValidationError # 合法性错误
-
-
- class User(forms.Form):
- # 用户名 密码 确认密码 邮箱
- username = forms.CharField(max_length=8, min_length=3, label='用户名', required=True,
- error_messages={'max_length': '用户名最多只能输入8位',
- 'min_length': '用户名最少输入3位',
- 'required': '用户名必须填'
- },
- # 添加bootstr样式
- widget=widgets.TextInput(attrs={'class': 'form-control'})
- )
- password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
- error_messages={
- 'max_length': '密码最长16位',
- 'min_length': '密码最短8位',
- 'required': '密码不能为空',
- },
- widget=widgets.PasswordInput(attrs={'class': 'form-control'})
- )
- re_password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
- error_messages={
- 'max_length': '密码最长16位',
- 'min_length': '密码最短8位',
- 'required': '密码不能为空',
- },
- widget=widgets.PasswordInput(attrs={'class': 'form-control'})
- )
- email = forms.EmailField(label='邮箱地址', widget=widgets.EmailInput(attrs={'class': 'form-control'}))
-
- # 局部钩子 校验用户名是否存在
- def clean_username(self):
- name = self.cleaned_data.get('username')
- if UserInfo.objects.filter(username=name).first():
- # 用户已存在
- raise ValidationError('用户名已存在') # 校验错误抛出异常
- else:
- return name
-
- # 局部钩子 校验用户名是否存在
- # def clean_username(self):
- # username = self.cleaned_data.get('username')
- # try:
- # UserInfo.objects.get(username=username)
- # print(UserInfo.objects.get(username=username), type(UserInfo.objects.get(username=username)))
- # raise ValidationError('用户名已存在')
- # except Exception:
- # return username
-
- # 全局钩子 校验两次输入密码是否一致
- def clean(self):
- pwd = self.cleaned_data.get('password')
- re_pwd = self.cleaned_data.get('re_password')
- if pwd != re_pwd:
- raise ValidationError('两次密码不一致') # 主动抛出合法性错误
- else:
- return self.cleaned_data
- from django.contrib import admin
- from django.urls import path
- from blog import views
-
- urlpatterns = [
- path('admin/', admin.site.urls),
- path('register/', views.register),
- ]
views.py
- from django.shortcuts import render
- from blog.blog_forms import User
-
-
- def register(request):
- form_obj = User()
- if request.method == 'GET': # 当请求为get时返回注册界面,并返回forms组件对象进行数据校验
- return render(request, 'register.html', {'form_obj': form_obj})
register.html
- class="container-fluid">
- class="row">
- class="col-md-6 col-md-offset-3">
-
class="text-center text-info">注册功能
-
- {% csrf_token %}
- {% for foo in form_obj %}
- class="form-group">
-
- {{ foo }}
- class="pull-right text-danger">
-
- {% endfor %}
- class="form-group">
-
- "/static/default.png" alt="" height="100px" width="100px" style="margin-right: 20px"
- id="id_img">
- <input type="file" id="id_file" accept="image/*" style="display: none">
-
-
- class="form-group text-center">
-
- <input type="button" value="注册" class="btn btn-success" id="id_submit">
- class="text-danger error">
-
-
-
-
-
- <script>
- // 头像动态显示
- $('#id_file').change(function () {
- // 将上传的头像展示到img标签内 修改img标签内的src参数
-
- // 读出图片文件 借助于文件阅读器
- let reader = new FileReader()
-
- // 拿到文件对象
- let file = $('#id_file')[0].files[0]
-
- // 将文件对象读到文件阅读器中
- reader.readAsDataURL(file)
-
- // 文件加载完后修改img标签的src参数
- reader.onload = function () {
- // $('#id_img')[0].src=reader.result
- $('#id_img').attr('src', reader.result) # jquery对象方法
- }
- })
- script>
发送Ajax请求
- // 发送ajax请求
- $('#id_submit').click(function () {
- let data = new FormData // 可以传递文件数据
-
- // 方式一:根据id获取标签数据添加至data中
-
- // data.append('username', $('#id_username').val())
- // data.append('password', $('#id_password').val())
- // data.append('re_password', $('#id_re_password').val())
- // data.append('email', $('#id_email').val())
- // data.append('icon', $('#id_file')[0].files[0])
- // data.append('csrfmiddlewaretoken', $("[name='csrfmiddlewaretoken']").val())
- // ...发送ajax请求
-
-
- // 方式二:利用form组件批量处理
- let data_arr = $('#id_form').serializeArray() // 序列化数组
- console.log(data_arr) // 是一个数组套对象 对象中k是name v是value 自动添加csrf
-
- // 使用for循环把数据添加到data对象中
- $.each(data_arr, function (i, v) {
- console.log("index:",i)
- console.log("value:", v)
- console.log("-----------------------")
- data.append(v.name, v.value)
- })
-
- // 文件需要单独放入
- data.append('icon', $('#id_file')[0].files[0])
-
- // 使用ajax发送请求
- $.ajax({
- url: '/register/',
- type: 'post',
- data: data,
- processData: false,
- contentType: false,
- success: function (data) {
-
- }
现在后端可以收到数据 继续写后端
views.py
- def register(request):
- form_obj = User()
- if request.method == 'GET':
- return render(request, 'register.html', {'form_obj': form_obj})
- else: # 当发送post请求
- res = {'code': 100, 'msg': '注册成功'}
- forms_obj = User(data=request.POST) # forms组件检验
- if forms_obj.is_valid(): # 如果数据全部合法
- register_data = forms_obj.cleaned_data # 拿出所有的合法数据
- register_data.pop('re_password') # 弹出二次输入密码 因为用户表中不需要改字段
- if request.FILES.get('icon'): # 判断是否上传了图片文件
- register_data['icon'] = request.FILES.get('icon') # 上传了的话就添加进去
- # 一定要用create_user 密码是密文 后面才可以使用auth模块的功能
- UserInfo.objects.create_user(**register_data) # 将register_data打散保存至数据库
- return JsonResponse(res) # 注册成功返回信息
- else: # 弱国数据不是全部合法
- res['code'] = 101
- res['msg'] = '注册失败'
- res['errors'] = forms_obj.errors # 返回错误信息
- return JsonResponse(res)
前端ajax可以接受到后端返回的json字符串
- $.ajax({
- url: '/register/',
- type: 'post',
- data: data,
- processData: false,
- contentType: false,
- success: function (data) {
- console.log(data)
- if (data.code === 100) {
- // 注册成功跳转至登录界面
- location.href = '/login/'
- } else {
- // 在前端渲染出错误信息
- console.log(data)
- $.each(data.errors, function (k, v) {// for循环错误字典
- if (k === '__all__') {
- // 全局钩子错误 两次密码不一致
- $('.error').html(data.errors['__all__'][0])
- } else {
- // 其他错误找到相应的input框后的span标签渲染 父类标签加上has-error属性变红
- $('#id_' + k).next().html(v[0]).parent().addClass('has-error')
- }
- })
- }
- }
- })
- // 定时任务 渲染的错误信息三秒后清除
- setTimeout(function () {
- // 把所有的span标签的内容清除 父类中的属性has-error去除
- $('.text-danger').html('').parent().removeClass('has-error')
- }, 3000)
需求:当用户输入用户名后鼠标离开用户名框,校验用户名是否存在且不能刷新页面
前端
- <script>
- // 后端ajax校验用户名是否存在
- // 前端使用get请求传入用户名
-
- // 绑定一个失去焦点事件
- $('#id_username').blur(function () {
- $.ajax({
- url: '/check_name/?name=' + $('#id_username').val(),
- type: 'get',
- success: function (data) {
- if (data.code === 110) {// 当用户名存在 添加提示信息
- $('#id_username').next().html(data.msg)
- }else {// 当用户不存在时清除提示信息
- $('#id_username').next().html('')
- }
- }
- })
- })
- script>
后端
urls.py
path('check_name/', views.check_name),
views.py
- def check_name(request):
- # print(request.GET)
- res = {'msg': '用户已存在', 'code': 110}
- name = request.GET.get('name')
- obj = UserInfo.objects.filter(username=name).first()
- if obj:
- return JsonResponse(res)
- else:
- res['code'] = 100
- res['msg'] = '用户不存在'
- return JsonResponse(res)
注册成功跳转至/login/,创建login.html。
- html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Titletitle>
- <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>
- <div class="container-fluid">
- <div class="row">
- <div class="col-md-6 col-md-offset-3">
- <h1 class="text-center text-info">登录功能h1>
- <form action="" id="id_form" method="post">
- {% csrf_token %}
- <div class="form-group">
- <label for="id_username">用户名label>
- <input type="text" id="id_username" name="username" class="form-control">
- div>
- <div class="form-group">
- <label for="id_password">密码label>
- <input type="password" id="id_password" name="password" class="form-control">
- div>
- <div class="row">
- <div class="col-md-6 form-group">
- <label for="id_code">验证码label>
- <input type="text" id="id_code" class="form-control" name="code">
- div>
- <div class="col-md-6">
- <img src="/get_code/" alt="" id="id_img" width="350px" height="50px">
- div>
- div>
- <div class="form-group">
- <input type="button" value="登录" class="btn btn-block btn-danger" id="id_submit">
- <div class="text-center">
- <span class="text-danger error">span>
- div>
- div>
- form>
-
- <script>
- // 点击验证码图片刷新验证码
- $('#id_img').click(function () {
- let time = new Date().getTime()
- console.log(time)
- // 再次获取随机验证码图片
- $('#id_img')[0].src = '/get_code/?t=' + time
- })
-
- // 提交ajax
- $('#id_submit').click(function () {
- // 将form表单的input标签数据序列化成数组套对象 name value
- dataArray = $('#id_form').serializeArray()
- $.ajax({
- url: '/login/',
- type: 'post',
- data: dataArray,
- success: function (data) {
- console.log(data)
- if(data.code===100){
- location.href = '/'
- }else {
- $('.error').html(data.msg)
- }
- }
- })
- })
-
-
- // 定时器任务 自动关闭错误提示信息
- let test = function () {
- $('.error').html('')
- }
- // 可重复关闭
- timer = setInterval(test, 2000)
-
- //60秒后关闭循环定时任务
- setTimeout(function () {
- clearTimeout(timer)
- },60*1000)
- script>
- div>
- div>
- div>
- body>
- html>
验证码:字母数字共五位
views.py
- from PIL import Image, ImageDraw, ImageFont
- from io import BytesIO
- import random
-
- def get_code(request):
- # 1 生成一张图片 pillow模块
- img = Image.new('RGB', (350, 50), color=(255, 255, 255))
- # 2 生成一个画图对象 将img传入
- draw = ImageDraw.Draw(img)
- # 3 生成字体对象
- font = ImageFont.truetype(font='./static/font/1641263938811335.ttf', size=50)
- # 4 生成随机字符串
- ran_str = ''
- for i in range(5):
- ran_num = str(random.randint(0, 9))
- ran_upper = chr(random.randint(65,90))
- # 去除I和L
- while (ran_upper == 'L' or ran_upper == 'I'):
- ran_upper = chr(random.randint(65,90))
- ran_lower = chr(random.randint(97, 122))
- # 去除i和l
- while (ran_lower == i or ran_lower == l):
- ran_lower = chr(random.randint(97, 122))
- res = random.choice([ran_num , ran_upper, ran_lower])
- # 将生成的随机字符画到图片中
- # fill=get_color 字体颜色也随机
- draw.text(xy=(10 + i * 60, 0), text=res, font=font, fill=get_color())
- # 5 画线
- for i in range(10):
- draw.line([(random.randint(0, 350), random.randint(0, 50)), (random.randint(0, 350), random.randint(0, 50))],
- fill=get_color()) # 起点和终点
- # 6 画点
- for i in range(100):
- draw.point((random.randint(0, 350), random.randint(0, 50)), fill=get_color())
- # 7 将图片保存在内存中 BytesIo模块 并返回给前端
- byte_io = BytesIo()
- img.save(fp=byte_io, format='png')
- # 怎样校验前端传过来的验证码?
- # 可以存在session表中 前端访问返回给前端 前端再次访问携带session 后端取出data进行校验
- request.session['code'] = res
- return HttpResponse(byte_io.getvalue())
-
-
-
- def get_color():
- x, y = 0, 255
- return (random.randint(x, y), random.randint(x, y), random.randint(x, y))
上面是自定义的图片验证码,也可以使用第三方模块,比如gvcode模块
- from gvcode import VFCode
-
- """
- 使用方法:
- vc = VFCode(
- width=200, # 图片宽度
- height=80, # 图片高度
- fontsize=50, # 字体尺寸
- font_color_values=[
- '#ffffff',
- '#000000',
- '#3e3e3e',
- '#ff1107',
- '#1bff46',
- '#ffbf13',
- '#235aff'
- ], # 字体颜色值
- font_background_value='#ffffff', # 背景颜色值
- draw_dots=False, # 是否画干扰点
- dots_width=1, # 干扰点宽度
- draw_lines=True, # 是否画干扰线
- lines_width=3, # 干扰线宽度
- mask=False, # 是否使用磨砂效果
- font='arial.ttf' # 字体 内置可选字体 arial.ttf calibri.ttf simsun.ttc
- )
- # 验证码类型
- # 自定义验证码
- # vc.generate('abcd')
- # 数字验证码(默认5位)
- # vc.generate_digit()
- # vc.generate_digit(4)
- # 字母验证码(默认5位)
- # vc.generate_alpha()
- # vc.generate_alpha(5)
- # 数字字母混合验证码(默认5位)
- # vc.generate_mix()
- # vc.generate_mix(6)
- # 数字加减验证码(默认加法)
- vc.generate_op()
- # 数字加减验证码(加法)
- # vc.generate_op('+')
- # 数字加减验证码(减法)
- # vc.generate_op('-')
- # 图片字节码
- # print(vc.get_img_bytes())
- # 图片base64编码
- print(vc.get_img_base64())
- # 保存图片
- vc.save()
- """
- def get_code(request):
- vc = VFCode(width=350, height=50)
- vc.generate_mix()
- # vc.generate_op()
- print(vc.get_img_base64()[0])
- byte_io = BytesIO()
- vc.save(byte_io, fm='png')
- request.session['code'] = vc.get_img_base64()[0]
- return HttpResponse(byte_io.getvalue())
login.html
- <script>
-
- // 提交ajax
- $('#id_submit').click(function () {
- let dataArray = $('#id_form').serializeArray()
- console.log(dataArray)
- $.ajax({
- url: '/login/',
- type: 'post',
- data: dataArray,
- success: function (data) {
- console.log(data)
- if(data.code===100){
- // 登陆成功 去首页
- location.href = '/'
- }else {
- // 登陆失败 显示错误信息
- $('.error').html(data.msg)
- }
- }
- })
- })
-
- // 计时器 关闭错误提示
- let test = function () {
- $('.error').html('')
- }
- // 循环执行
- timer = setInterval(test, 2000)
-
- //60秒后关闭循环定时任务
- setTimeout(function () {
- clearTimeout(timer)
- },60*1000)
-
- script>
- def login(request):
- if request.method == 'GET':
- return render(request, 'login.html')
- res = {'code': 100, 'msg': '登陆成功'}
- code = request.POST.get('code')
- # 校验验证码
- if request.session.get('code').lower() == code.lower():
- username = request.POST.get('username')
- password = request.POST.get('password')
-
- # 如果认证成功(用户名和密码正确有效),便会返回一个 User 对象。
- obj = authenticate(username=username, password=password)
- if obj:
- return JsonResponse(res)
- res['code'] = 110
- res['msg'] = '用户名或密码错误'
- return JsonResponse(res)
- res['code'] = '120'
- res['msg'] = '验证码错误'
- return JsonResponse(res)
创建index.html,添加路由,get请求时返回index.html
- html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>indextitle>
- <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>
- <div class="my_nav">
- <nav class="navbar navbar-inverse">
- <div class="container-fluid">
- <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 navigationspan>
- <span class="icon-bar">span>
- <span class="icon-bar">span>
- <span class="icon-bar">span>
- button>
- <a class="navbar-brand" href="#">博客园a>
- div>
-
-
- <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
- <ul class="nav navbar-nav">
- <li class="active"><a href="#">首页 <span class="sr-only">(current)span>a>li>
- <li><a href="#">新闻a>li>
- ul>
- <ul class="nav navbar-nav navbar-right">
- <li><a href="#">jaspera>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>
- ul>
- div>
- div>
- nav>
- div>
- <div class="container-fluid">
- <div class="row">
- <div class="view_left">
- <div class="col-md-2">
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 头条
- a>
- <a href="#" class="list-group-item">286 亿元!败了的 Google 是否会为 Android 交最贵罚单?a>
- <a href="#" class="list-group-item">苹果灵动岛华而不实?网友整活改进,竟可以“一键抢大米”a>
- <a href="#" class="list-group-item">“AI 终有可能消灭人类!”a>
- <a href="#" class="list-group-item">Python 3.14 将比 C++ 更快a>
- div>
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 热点
- a>
- <a href="#" class="list-group-item">《羊了个羊》否认抄袭;安卓反垄断案再次败诉,罚款金额下降至286亿元a>
- <a href="#" class="list-group-item">聊聊Redis的数据热点问题a>
- <a href="#" class="list-group-item">抖音开放平台,究竟开放了什么?a>
- <a href="#" class="list-group-item">谷歌CEO皮查伊暗示要裁员;华为研发投入位居首位;Android 13首个安全更新|极客头条a>
- div>
- div>
- div>
- <div class="view_mid">
- <div class="col-md-7">
- <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
-
- <ol class="carousel-indicators">
- <li data-target="#carousel-example-generic" data-slide-to="0" class="active">li>
- <li data-target="#carousel-example-generic" data-slide-to="1">li>
- <li data-target="#carousel-example-generic" data-slide-to="2">li>
- ol>
-
-
- <div class="carousel-inner" role="listbox">
- <div class="item active">
- <img src="../media/slideshow/1.png" alt="...">
- <div class="carousel-caption">
- ...
- div>
- div>
- <div class="item">
- <img src="../media/slideshow/2.png" alt="...">
- <div class="carousel-caption">
- ...
- div>
- div>
- <div class="item">
- <img src="../media/slideshow/3.png" alt="...">
- <div class="carousel-caption">
- ...
- div>
- div>
- div>
-
-
- <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
- <span class="glyphicon glyphicon-chevron-left" aria-hidden="true">span>
- <span class="sr-only">Previousspan>
- a>
- <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
- <span class="glyphicon glyphicon-chevron-right" aria-hidden="true">span>
- <span class="sr-only">Nextspan>
- a>
- div>
- div>
- div>
- <div class="view_right">
- <div class="col-md-3">
- <div class="panel panel-primary">
- <div class="panel-heading">
- <h3 class="panel-title">广告招租h3>
- div>
- <div class="panel-body">
- vx:xxx
- div>
- div>
- <div class="panel panel-danger">
- <div class="panel-heading">
- <h3 class="panel-title">广告招租h3>
- div>
- <div class="panel-body">
- vx:xxx
- div>
- div>
- <div class="panel panel-info">
- <div class="panel-heading">
- <h3 class="panel-title">广告招租h3>
- div>
- <div class="panel-body">
- vx:xxx
- div>
- div>
- <div class="panel panel-success">
- <div class="panel-heading">
- <h3 class="panel-title">广告招租h3>
- div>
- <div class="panel-body">
- vx:xxx
- div>
- div>
- <div class="panel panel-warning">
- <div class="panel-heading">
- <h3 class="panel-title">广告招租h3>
- div>
- <div class="panel-body">
- vx:xxx
- div>
- div>
-
- div>
- div>
- div>
- div>
- body>
- html>
登陆超级管理员录入数据。
先在admin.py中将表注册后 可以看到表名
- from django.contrib import admin
- from .models import *
-
- # Register your models here.
- admin.site.register(UserInfo)
- admin.site.register(Blog)
- admin.site.register(Tag)
- admin.site.register(Classify)
- admin.site.register(Article)
- admin.site.register(UpAndDown)
- admin.site.register(Comment)
注意:
显示表名需在models.py中创建表时添加一个Meta类
- class Meta:
- verbose_name_plural = '博客表'
定义字段时添加属性verbose_name在添加数据时显示
在创建表时定义双下str方法可以自定义显示的创建对象名字
- def __str__(self):
- return self.title
Django中的media文件夹一般用来存放文件,图片等不重要的数据,想在前端通过路径访问media中的数据,是不可以的,需要开启media访问。
在settings.py中添加
MEDIA_ROOT = os.path.join(BASEDIR, 'media')
在urls中添加
- from django.views.static import serve
- from django.conf import settings
-
- path('media/
' , serve, {'document_root':settings.MEDIA_ROOT})
有的网站有上传图片功能,可以上传到该网站,然后再自己的网站使用,这样就不会消耗自己的带宽。
图片防盗链就是抑制这种行为,本质原理是:浏览器发送http请求,请求头中会携带referer参数,是一个url地址,表示上一次访问的地址,图片防盗链可以跟据这个地址判断是不是自己的网址发的请求,如果不是直接拒绝响应。
views.py
后端返回所有文章
- def index(request):
- article_query_set = Article.objects.all()
- return render(request, 'index.html', context={'article_query_set': article_query_set})
index.html
- <div class="article" style="margin-top: 20px">
- {% for foo in article_query_set %}
- <div style="margin-top: 20px">
- <h4 class="media-heading"><a href="">{{ foo.title }}a>h4>
- <hr>
- <div class="media">
- <div class="media-left">
- <a href="#">
- <img class="media-object" src="/media/{{ foo.blog.userinfo.icon }}" alt="..."
- width="60px" height="60px">
- a>
- div>
- <div class="media-body">
- <h4 class="media-heading">{{ foo.desc }}h4>
- div>
- div>
-
- <div class="" style="margin-top: 20px">
- <a href="{{ foo.blog.userinfo.username }}"><span
- style="padding: 10px;font-size: 13px">{{ foo.blog.userinfo.username }}span>a>
- <span style="padding: 5px;font-size: 13px">{{ foo.create_time|date:'Y-m-d H:s' }}span>
- <span style="padding: 5px;font-size: 13px"><i class="fa fa-thumbs-o-up"
- aria-hidden="true">i>{{ foo.up_num }}span>
- <span style="padding: 10px;font-size: 13px"><i class="fa fa-thumbs-o-down"
- aria-hidden="true">i>{{ foo.down_num }}span>
- <span style="padding: 10px;font-size: 13px"><i class="fa fa-commenting"
- aria-hidden="true">i>{{ foo.comment_num }}span>
- div>
- div>
-
- {% endfor %}
-
- div>
当点击用户名 则跳转到用户对应的站点(如果存在)不存在就返回404界面
- # 站点匹配 必须放最后
- path('
/' , views.site),
- def site(request, name, **kwargs):
- user = UserInfo.objects.filter(username=name).first()
- if not user:
- return render(request, 'error.html')
- article_set = user.blog.article_set.all()
- return render(request, 'site.html', locals())
- "en">
- "UTF-8">
-
404
使用模板的继承,变得只是中间文章的展示,分类标签随笔部分不变。
base.html
- <head>
- <meta charset="UTF-8">
- <title>
- {% block title %}
-
- {% endblock %}
- title>
- <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>
- <link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
- {% block link %}
-
- {% endblock %}
- head>
- <body>
- <div class="main">
- <div class="header">
- {% block handle %}
-
- {% endblock %}
- div>
-
- <div class="container-fluid">
- <div class="row">
- <div class="col-md-2">
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 我的标签
- a>
- {% for foo in tag_res %}
- <div class="list-group">
- <a href="/{{ user.username }}/tag/{{ foo.0 }}.html"
- class="list-group-item"><span>{{ foo.1 }}span>
- <span>({{ foo.2 }})span>a>
- {% endfor %}
- div>
-
- div>
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 我的分类
- a>
- {% for foo in classify_res %}
- <div class="list-group">
- <a href="/{{ user.username }}/classify/{{ foo.0 }}.html"
- class="list-group-item"><span>{{ foo.1 }}span>
- <span>({{ foo.2 }})span>a>
- {% endfor %}
- div>
- div>
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 随笔分类
- a>
- {% for foo in date_res %}
- <div class="list-group">
- <a href="/{{ user.username }}/archive/{{ foo.0|date:'Ym' }}.html"
- class="list-group-item"><span>{{ foo.0|date:'Y年m月' }}span>
- <span>({{ foo.1 }})span>a>
- {% endfor %}
- div>
- div>
- div>
- <div class="col-md-10">
- {% block crticle %}
-
- {% endblock %}
- div>
- div>
- div>
- div>
- body>
site.html
- {% extends 'base.html' %}
-
- {% block title %}
- {{ user.username }}
- {% endblock %}
-
- {% block link %}
-
- {% endblock %}
-
- {% block handle %}
- <div class="my_nav">
- <nav class="navbar navbar-inverse">
- <div class="container-fluid">
- <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 navigationspan>
- <span class="icon-bar">span>
- <span class="icon-bar">span>
- <span class="icon-bar">span>
- button>
- <a class="navbar-brand" href="#">{{ user.username }}a>
- div>
-
-
- <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
- <ul class="nav navbar-nav">
- ul>
- <ul class="nav navbar-nav navbar-right">
- <li>
- <button type="button" class="btn btn-danger navbar-btn">管理button>
- li>
- ul>
- div>
- div>
- nav>
- div>
- {% endblock %}
-
-
-
-
- {% block crticle %}
- <div class="article">
- {% for foo in article_set %}
- <div style="margin-top: 20px">
- <h4 class="media-heading"><a href="">{{ foo.title }}a>h4>
- <hr>
- <div class="media">
- <div class="media-body">
- <h4 class="media-heading">{{ foo.desc }}h4>
- div>
- div>
-
- <div class="" style="margin-top: 20px">
- <span
- style="padding: 10px;font-size: 13px">{{ foo.blog.userinfo.username }}span>
- <span style="padding: 5px;font-size: 13px">{{ foo.create_time|date:'Y-m-d H:s' }}span>
- <span style="padding: 5px;font-size: 13px"><i class="fa fa-thumbs-o-up"
- aria-hidden="true">i>{{ foo.up_num }}span>
- <span style="padding: 10px;font-size: 13px"><i class="fa fa-thumbs-o-down"
- aria-hidden="true">i>{{ foo.down_num }}span>
- <span style="padding: 10px;font-size: 13px"><i class="fa fa-commenting"
- aria-hidden="true">i>{{ foo.comment_num }}span>
- div>
- div>
-
- {% endfor %}
-
- div>
- {% endblock %}
urls.py
- # jasper/tag/4.html 标签匹配
- path('
/tag/.html' , views.site), - # jasper/classify/3.html
- path('
/classify/.html' , views.site), - # jasper/archive/202209.html
- path('
/archive/.html' , views.site),
views.py
- def site(request, name, **kwargs):
- # name是传过来的站点对应的用户名
- user = UserInfo.objects.filter(username=name).first()
- if not user:
- # 博主不存在 返回错误界面
- return render(request, 'error.html')
- # 查询该博主的所有文章
- article_set = user.blog.article_set.all()
- # 取名字后的路由后缀
- tag = kwargs.get('tag')
- classify = kwargs.get('classify')
- time = kwargs.get('time')
- # 有tag后缀
- if tag:
- # 返回当前标签的所有文章
- article_set = article_set.filter(tag__id=tag)
- elif classify:
- article_set = article_set.filter(classify__id=classify)
- # 按时间分类
- elif time:
- year = str(time)[:4]
- month = str(time)[4:]
- article_set = article_set.filter(create_time__year=year, create_time__month=month)
- # # 需要标签名和统计标签内文章数
- classify_res = Classify.objects.all().filter(blog=user.blog).values('id').annotate(
- c=Count('article__id')).values_list('id', 'name', 'c')
- tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
- 'id', 'name', 'c')
- date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
- 'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
- return render(request, 'site.html', locals())
- # 自定义标签
- 1. 在应用下创建templatetags包,必须是templatetags
- 2. 在templatetags中新建一个new_tag.py文件,py文件名随意。
- from django import template
- from blog.models import Classify, Tag, Article, UserInfo
- from django.db.models import Count
- from django.db.models.functions import TruncMonth
-
- register = template.Library() # 生成一个Library对象 名字必须叫register
-
-
- # 装饰函数
- @register.inclusion_tag(filename='left.html', name='left') # 返回html片段,第一个参数是html文件
- def left(name):
- # user 当前根据用户名查到的用户,需要传入用户名,一定会有user
- user = UserInfo.objects.filter(username=name).first()
- # 需要标签名和统计标签内文章数
- classify_res = Classify.objects.all().filter(blog=user.blog).values('id').annotate(
- c=Count('article__id')).values_list('id', 'name', 'c')
- tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
- 'id', 'name', 'c')
- date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
- 'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
- return {'classify_res': classify_res, 'tag_res': tag_res, 'date_res': date_res, 'user':user} # 字典中的数据可以在left中使用
- html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Titletitle>
- head>
- <body>
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 我的标签
- a>
- {% for foo in tag_res %}
- <div class="list-group">
- <a href="/{{ user.username }}/tag/{{ foo.0 }}.html"
- class="list-group-item"><span>{{ foo.1 }}span>
- <span>({{ foo.2 }})span>a>
- div>
- {% endfor %}
-
- div>
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 我的分类
- a>
- {% for foo in classify_res %}
- <div class="list-group">
- <a href="/{{ user.username }}/classify/{{ foo.0 }}.html"
- class="list-group-item"><span>{{ foo.1 }}span>
- <span>({{ foo.2 }})span>a>
- div>
- {% endfor %}
- div>
- <div class="list-group">
- <a href="#" class="list-group-item active">
- 随笔分类
- a>
- {% for foo in date_res %}
- <div class="list-group">
- <a href="/{{ user.username }}/archive/{{ foo.0|date:'Ym' }}.html"
- class="list-group-item"><span>{{ foo.0|date:'Y年m月' }}span>
- <span>({{ foo.1 }})span>a>
- div>
- {% endfor %}
-
- div>
- body>
- html>
在base中的左侧栅格使用inclusion_tag
需要先将自定义标签load过来,在使用标签并传入参数。
- class="col-md-2">
- {% load new_tag %}
- {% left name %}
渲染site.html页面时,返回的locals(),所以可以用到site函数的所有变量,site函数的name是它的形参,是点击首页博主用户名跳转过来的,name参数就是用文章取到的博主用户名,所以base可以用到name属性。
将那么属性传到new_tag文件中的left函数中,执行该函数。进行标签等数据的过滤,然后返回参数供left.html文件使用,left.html文件渲染完后,贴在base.html的相应位置。views.py中的
注意:添加templatetags模块后 需要重启服务器 才可以使用标签
点击首页文章和个人站点中的文章跳转到文章详情页面去。
path('/articles/' , views.article_detail),
views.py
- def article_detail(request, name, article_id):
- # 文章博主
- user = UserInfo.objects.filter(username=name).first()
- # 文章
- article = Article.objects.filter(id=article_id).first()
- if user and article:
- return render(request, 'article.html', context={'user': user, 'article': article, 'name': name})
- return render(request, 'error.html')
- {% extends 'base.html' %}
-
- {% block title %}
- {{ article.title }}
- {% endblock %}
-
- {% block link %}
- <link rel="stylesheet" href="/static/css/up.css">
- {% endblock %}
-
- {% block handle %}
- <div class="my_nav">
- <nav class="navbar navbar-inverse">
- <div class="container-fluid">
- <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 navigationspan>
- <span class="icon-bar">span>
- <span class="icon-bar">span>
- <span class="icon-bar">span>
- button>
- <a class="navbar-brand" href="#">{{ user.username }}a>
- div>
-
-
- <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
- <ul class="nav navbar-nav">
- ul>
- <ul class="nav navbar-nav navbar-right">
- <li>
- <button type="button" class="btn btn-danger navbar-btn">管理button>
- li>
- ul>
- div>
- div>
- nav>
- div>
- {% endblock %}
-
- {% block crticle %}
- <div>
- <h3>{{ article.title }}h3>
- div>
-
- <div>
- {{ article.content }}
- div>
-
- <div id="div_digg" class="pull-right">
- <div class="diggit is_up">
- <span class="diggnum" id="digg_count">{{ article.up_num }}span>
- div>
- <div class="buryit is_up">
- <span class="burynum" id="bury_count">{{ article.down_num }}span>
- div>
- <div class="clear">div>
- <div class="diggword" id="digg_tips">
- div>
- div>
- {% endblock %}
/static/css/up.css
- .diggit {
- float: left;
- width: 46px;
- height: 52px;
- background: url(/static/upup.gif) no-repeat;
- text-align: center;
- cursor: pointer;
- margin-top: 2px;
- padding-top: 5px;
- }
-
-
- .buryit {
- float: right;
- margin-left: 20px;
- width: 46px;
- height: 52px;
- background: url(/static/downdown.gif) no-repeat;
- text-align: center;
- cursor: pointer;
- margin-top: 2px;
- padding-top: 5px;
- }
-
- .clear {
- clear: both;
- }
-
- .diggword {
- margin-top: 5px;
- margin-left: 0;
- font-size: 12px;
- color: #808080;
- }
- <script>
- // 将点赞点踩设置成一个点击事件
- $('.is_up').click(function () {
- let is_up = ($(this).hasClass('diggit'))//根据类属性来判断是点赞还是点踩
- $.ajax({
- url: '/is_up/', //处理点赞点踩的接口
- type: 'post',
- // 需要传谁给哪篇文章点赞还是点踩了 谁点赞可以不传 只要后端登陆了就可以查到
- data: {
- article_id:{{ article.id }},
- is_up: is_up,
- csrfmiddlewaretoken: '{{ csrf_token }}'
- },
- success: function (data) {
- if (data.code == 100) {
- // 如果成功,点赞数+1
- $('#digg_count').html({{ article.up_num }} +1)
- } else if (data.code == 103) {
- // 如果失败,点踩数+1
- $('#bury_count').html({{ article.down_num }} +1)
- }
- // 每次打印提示信息
- $('.diggword').html(data.msg)
- }
- })
- })
- script>
设置路由
- # is_up 处理点赞相关路由
- path('is_up/', views.is_up),
- def is_up(request):
- article_id = request.POST.get('article_id')
- is_up = json.loads(request.POST.get('is_up')) # 直接取出来是字符串 需要转成bool值
- res = {'code': 100, 'msg': '点赞成功了'}
- # 1. 判断当前用户是否登录
- if not request.user.is_authenticated: # 只要用户登录就是当前用户 没有登陆就是匿名用户
- res['code'] = 101
- res['msg'] = '没有登录点击跳转登录'
- return JsonResponse(res)
- # 2. 判断当前用户是否已经给这篇文章点过赞或踩了
- if UpAndDown.objects.filter(user=request.user, article_id=article_id).first():
- res['code'] = 102
- res['msg'] = '已经点赞或点踩了'
- return JsonResponse(res)
- # 3. 用户是点赞还是点踩 存入点赞点踩表和文章表 并将点赞点踩数返回bbs
- # 开启事务
- with transaction.atomic():
- UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=is_up)
- if is_up:
- # 文章表点赞数加1
- Article.objects.filter(id=article_id).update(up_num=F('up_num') + 1)
- else:
- Article.objects.filter(id=article_id).update(down_num=F('down_num') + 1)
- res['code'] = 103
- res['msg'] = '点踩成功了'
- return JsonResponse(res)
- <div class="comment-show">
- <div style="margin-top: 60px">
- <b>评论列表b>
- div>
-
-
- <ul class="list-group comment-ajax">
- {% for foo in comment %}
- <li class="list-group-item">
- <div>
- <span># {{ forloop.counter }} 楼span> <span
- style="margin-left: 20px">{{ foo.create_time|date:'Y-m-d H:i' }}span>
- <a href="/{{ foo.user.username }}/"><span
- style="margin-left: 20px">{{ foo.user.username }}span>a>
- <div class="fa-pull-right">
- <a class="reply" parent_id="{{ foo.article.id }}" username="{{ foo.user.username }}">回复a>
- div>
-
- div>
- {% if foo.parent_id %}
- <p style="margin-top: 10px">@ {{ foo.parent.user.username }}p>
- <p>{{ foo.content|safe }}p>
- {% else %}
- <p style="margin-top: 10px">{{ foo.content }}p>
- {% endif %}
- li>
- {% endfor %}
- ul>
-
-
- div>
-
- <div>
- <a href="">刷新页面a>
- div>
- <div style="margin-top: 60px">
- <i class="fa fa-commenting-o" aria-hidden="true">i>
- <b>发表评论b>
- div>
- {% if request.user.is_authenticated %}
- <div>
- <label for="content">label>
- <textarea name="" id="content" cols="170" rows="10">textarea>
- div>
- <div class="pull-right">
- <button class="btn btn-info" id="comment" style="margin-bottom: 50px">提交评论button>
- div>
- {% else %}
- <div>
- <i class="fa fa-commenting-o" aria-hidden="true">i> <span style="margin-left: 10px">登录后才能发表评论,立即 <a
- href="/login/">登录a> 或者 <a
- href="/">逛逛a> 首页span>
- div>
- {% endif %}
js代码
- script>
- // 评论按钮点击事件
- var parent_id = ''
- $('#comment').click(function () {
- // 取出评价内容 包括子评论和跟评论
- var content = $('#content').val()
- // 判断 如果是子评论要删除 @ 名字 换行
- if (parent_id) {
- console.log(content)
- var i = content.indexOf('\n')//取到换行的索引
- content = content.slice(i)//从索引位置往后切
- }
- $.ajax({
- url: '/comment/',
- type: 'post',
- data: {// 谁给哪篇文章评论了什么 父评论的id
- parent_id: parent_id,
- article_id: {{ article.id }},
- content: content,
- csrfmiddlewaretoken: '{{ csrf_token }}' // 坑!!! 一定要加引号
- },
- success: function (data) {
- console.log(data)
- if (data.code == 100) {
- $('#content').val('') //评论成功将评论区文字清空
- var cur_name = data.people // 当前评论人
- var content = data.content // 评论内容
- var s = '' // 将评论拼接到评论列表中
- if (data.comment_name) {//如果是子评论
- var comment_name = data.comment_name
- s = `
- <li class="list-group-item" style="margin-top: 20px">
- <i class="fa fa-commenting" aria-hidden="true">i>
- <b><span>${cur_name}:span>b>
- <div><span>@${comment_name}span>div>
- <div><span>${content}span>div>
-
-
- li>`
- } else {
- s = `
- <li class="list-group-item" style="margin-top: 20px">
- <i class="fa fa-commenting" aria-hidden="true">i>
- <b><span>${cur_name}:span>b>
- <div>
- <span>${content}span>
- div>
- li>`
- }
- }
- $('.comment-ajax').append(s)//追加到评论组的最后边 ajax提交跟评论和子评论
- }
- })
- })
-
- // 回复事件
- $('.reply').click(function () {
- parent_id = $(this).attr('parent_id')
- console.log(parent_id)
- var name = $(this).attr('username')
- // 将 @ 名字 换行 加到输入框中
- $('#content').val(`@${name}\n`).focus()//光标聚焦
- })
- script>
- def comment(request):
- res = {'code': 100, 'msg': '评论成功'}
- if request.user.is_authenticated:
- article_id = request.POST.get('article_id')
- content = request.POST.get('content')
- parent_id = request.POST.get('parent_id')
- # 保存评论
- # 开启事务
- with transaction.atomic():
- res_comment = Comment.objects.create(content=content, user=request.user, article_id=article_id,
- parent_id=parent_id)
- # 文章表中评论数加1
- Article.objects.filter(id=article_id).update(comment_num=F('comment_num') + 1)
- # 评论成功发送邮件
- # 使用多线程
- article_title = Article.objects.filter(pk=article_id).first().title
- send = Article.objects.filter(pk=article_id).first().blog.userinfo.email
- t = Thread(target=send_mail,
- args=(f'[博客评论通知]Re:{article_title}', content, settings.EMAIL_HOST_USER, [send]))
- t.start()
- # send_mail(f'[博客评论通知]Re:{article_title}', content, settings.EMAIL_HOST_USER, ['xuxiaoxu152@163.com']) # subject, message, from_email, recipient_list,
- # 返回给前端当前评论人 和评论内容
- res['people'] = request.user.username
- res['content'] = content
- if parent_id: # 如果这是一条子评论 将他评论的这条评论的博主名返回
- res['comment_name'] = res_comment.parent.user.username
- return JsonResponse(res)
- res['code'] = 101
- res['msg'] = '未登录 不能评论'
- return JsonResponse(res)
base.html
- <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="false" aria-controls="collapseOne" class="collapsed">
- 博客后台
- a>
- h4>
- div>
- <div id="collapseOne" class="panel-collapse collapse" role="tabpanel"
- aria-labelledby="headingOne" aria-expanded="false" style="height: 0px;">
- <div class="panel-body">
- <a href="/add/">新建随笔a>
- div>
- <div class="panel-body">
- <a href="">草稿箱a>
- div>
- <div class="panel-body">
- <a href="">回收站a>
- div>
- div>
- div>
- <div class="panel panel-default">
- <div class="panel-heading" role="tab" id="headingTwo">
- <h4 class="panel-title">
- <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion"
- href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
- 分类
- a>
- h4>
- div>
- <div id="collapseTwo" class="panel-collapse collapse" role="tabpanel"
- aria-labelledby="headingTwo" aria-expanded="false" style="height: 0px;">
- <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 class="is-show">
- <h4 class="is-show">文章展示h4>
- <ul class="nav nav-tabs">
- <li role="presentation" class="active"><a href="#">文章a>li>
- <li role="presentation"><a href="#">新闻a>li>
- <li role="presentation"><a href="#">标签a>li>
- ul>
-
- <div class="tab-content">
- <div role="tabpanel" class="tab-pane fade in active" id="home">
- {% block crticle %}
-
- {% endblock %}
- div>
-
- div>
- div>
-
-
- {% block add %}
-
- {% endblock %}
- div>
-
- div>
- div>
index.html
- {% extends 'backend/base.html' %}
- {% block title %}
- 后台管理
- {% endblock %}
-
- {% block crticle %}
- <div class="bs-example" data-example-id="hoverable-table">
- <table class="table table-hover">
- <thead>
- <tr>
- <th>编号th>
- <th>标题th>
- <th>发布时间th>
- <th>评论数th>
- <th>操作th>
- <th>操作th>
- tr>
- thead>
- <tbody>
- {% for article in article_list %}
- <tr>
- <td>{{ forloop.counter }}td>
- <td><a href="/{{ article.blog.userinfo.username }}/articles/{{ article.id }}">{{ article.title }}/a>td>
- <td>{{ article.create_time|date:'Y-m-d H:i' }}td>
- <td>{{ article.comment_num }}td>
- <td><a href="/delete/?pk={{ article.id }}">删除a>td>
- <td><a href="/alter_article/?pk={{ article.id }}">修改a>td>
- tr>
- {% endfor %}
-
- tbody>
- table>
- div>
- {% endblock %}
- {% extends 'backend/base.html' %}
-
- {% block link %}
- <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js">script>
- <script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js">script>
- {% endblock %}
-
- {% block title %}
- 添加文章
- {% endblock %}
-
- {% block add %}
- <div class="text-center" style="background: #2aabd2">
- <h3>添加随笔h3>
- div>
- <form action="" method="post">
- {% csrf_token %}
- <div class="form-group">
- <label for="add-title">标题label>
- <input type="text" id="add-title" name="title" class="form-control">
- div>
- <div class="form-group">
- <label for="add-content">内容label>
- <div>
- <textarea name="content" id="editor_id" cols="300" rows="20">textarea>
- div>
-
- div>
-
- <div class="form-group">
- <label for="add-classify">分类label>
- <select class="form-control" name="category" id="add-classify">
- {% for classify in classify_list %}
- <option value="{{ classify.id }}">{{ classify.name }}option>
- {% endfor %}
- select>
- div>
-
- <div class="form-group">
- <label for="add-tag">标签label>
- <select class="form-control" name="tag" id="add-tag" multiple>
- {% for tag in tag_list %}
- <option value="{{ tag.id }}">{{ tag.name }}option>
- {% endfor %}
- select>
- div>
- <button class="btn btn-success form-control">上传文章button>
- form>
-
- {% endblock %}
-
- {% block js %}
- // 使用富文本编辑器
- <script>
- KindEditor.ready(function (K) {
- window.editor = K.create('#editor_id', {
- width: '100%',
- height: '300px',
- resizeType: '1',
- // 上传图片相关
- uploadJson: '/put_img/',
- //filePostName: 'myfile', //默认imgFile
- //extraFileUploadParams: {
- // 'csrfmiddlewaretoken': '{{ csrf_token }}'
- // } 后端没有取消校验 需要传csrf
- });
- });
- script>
- {% endblock %}
- def add(request):
- if request.method == 'GET':
- tag_list = Tag.objects.filter(blog=request.user.blog)
- classify_list = Classify.objects.filter(blog=request.user.blog)
- return render(request, 'backend/add.html', context={'tag_list': tag_list, 'classify_list': classify_list})
- title = request.POST.get('title')
- content = request.POST.get('content')
- # BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
- bs = BeautifulSoup(content, features='html.parser')
- # 截取html文本,将空格和换行替换成空,并截取70个字符
- desc = bs.text.replace(' ', '').replace('\n', '')[:70] + '...'
- # 剔除script标签
- script_list = bs.findAll('script')
- for i in script_list:
- i.decompose() # 将每个script标签删除
- classify = request.POST.get('category')
- tag = request.POST.getlist('tag') # 这是多对多的
- res = Article.objects.create(title=title, content=str(bs), desc=desc, classify_id=classify, blog=request.user.blog)
- # 多对多添加外键关系
- res.tag.add(*tag)
- return redirect('/backend/')
富文本编辑器图片处理,查看官方文档。
- # 文章图片处理
- # 需要处理csrf 可已经用掉这个接口的csrf
-
- @csrf_exempt # 免除校验
- def put_img(request):
- img = request.FILES.get('imgFile')
- path = os.path.join(settings.MEDIA_ROOT, 'upload', img.name)
- with open(path, 'wb') as f:
- for i in img:
- f.write(i)
- return JsonResponse({
- "error": 0,
- "url": f"http://127.0.0.1:8000/media/upload/{img.name}"
- })
xss跨站脚本,在内容中存script脚本,前端渲染时使用了safe,如果存在script脚本,就会执行。解决方案。富文本编辑器在输入代码块时会自动将尖括号转换成对应的字符,只需在后端将恶意的script清除即可。
需要使用beautifulsoup4模块。
- -pip3 install beautifulsoup4
- -删除script标签
- soup = BeautifulSoup(content, 'html.parser')
- script_list=soup.findAll('script') # 搜索到html中所有的script标签
- for script in script_list:
- script.decompose() # 把搜到的script标签一个个删除
用户登陆后展示用户名和和管理选项按钮
- {% if request.user.is_authenticated %}
- <li><a href="{{ request.user.username }}">{{ 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="/set_pwd/">修改密码a>li>
- <li><a href="/backend/">后台管理a>li>
- <li><a href="/alter_icon/">修改头像a>li>
- <li role="separator" class="divider">li>
- <li><a href="/login_out/">退出登录a>li>
- ul>
- li>
- {% else %}
- <a href="/login/">登录a>
- <a href="/register/">注册a>
- {% endif %}
- # 退出登录
- def login_out(request):
- logout(request) # request.session.flush() 清除掉session和cookie
- return redirect('/')
- {% extends 'backend/base.html' %}
-
- {% block title %}
- 修改头像
- {% endblock %}
-
- {% block add %}
- <form action="" method="post" enctype="multipart/form-data">
- {% csrf_token %}
- <div style="margin-top: 100px">
- <h3 style="color: darkslateblue">修改头像h3>
- <label for="icon">
- <img src="/media/{{ icon }}" alt="" width="100px" height="100px" id="img">
- label>
- <input type="file" id="icon" style="display: none" name="icon">
- <button class="btn btn-success">确认修改button>
- div>
- form>
-
-
- {% endblock %}
-
- {% block js %}
- <script>
- $('.is-show').toggle()
-
- // 头像动态显示 给文件标签绑定一个变化事件
- $('#icon').change(function () {
- var reader = new FileReader()
-
- // 获取文件内容
- var file = $('#icon')[0].files[0]
-
- reader.readAsDataURL(file)
-
- reader.onload = (function () {
- $('#img').attr('src', reader.result)
- })
-
- })
- script>
- {% endblock %}
- # 修改头像
- def alter_icon(request):
- if request.method == "GET":
- # 需要当前用户头像
- icon = request.user.icon
- return render(request, 'backend/alter_icon.html', context={'icon': icon})
- icon = request.FILES.get('icon')
- request.user.icon = icon
- request.user.save()
- return redirect('/')
- {% extends 'backend/base.html' %}
-
- {% block title %}
- 修改密码
- {% endblock %}
-
- {% block add %}
- <form action="" method="post">
- {% csrf_token %}
- <div class="form-group">
- <label for="pwd1">原密码label>
- <input type="password" id="pwd1" name="old_password" class="form-control">
- div>
- <div class="form-group">
- <label for="pwd2">新密码label>
- <input type="password" id="pwd2" name="new_password" class="form-control">
- div>
- <div class="form-group">
- <label for="pwd3">确认密码label>
- <input type="password" id="pwd3" name="re_password" class="form-control">
- div>
- <button class="form-control btn-success">提交button> <span style="color: red">{{ error }}span>
- form>
- {% endblock %}
- def set_pwd(request):
- if request.method == 'GET':
- return render(request, 'backend/set_pwd.html')
- old_password = request.POST.get('old_password')
- new_password = request.POST.get('new_password')
- re_password = request.POST.get('re_password')
- if request.user.check_password(old_password):
- if new_password == re_password:
- request.user.set_password(new_password)
- request.user.save()
- # 退出当前登录 跳转至登录
- login_out(request)
- return redirect(to='/login/')
- return render(request, 'backend/set_pwd.html', context={'error': '两次密码不一致'})
- return render(request, 'backend/set_pwd.html', context={'error': '原密码不一致'})
- {% extends 'backend/base.html' %}
-
- {% block link %}
- <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js">script>
- <script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js">script>
- {% endblock %}
-
- {% block title %}
- 修改文章
- {% endblock %}
-
- {% block add %}
- <div class="text-center" style="background: #2aabd2">
- <h3>修改文章h3>
- div>
- <form action="" method="post">
- {% csrf_token %}
- <div class="form-group">
- <label for="add-title">标题label>
- <input type="text" id="add-title" name="title" class="form-control" value="{{ article.title }}">
- div>
- <div class="form-group">
- <label for="add-content">内容label>
- <div>
- <textarea name="content" id="editor_id" cols="300" rows="20">{{ article.content }}textarea>
- div>
-
- div>
-
- <div class="form-group">
- <label for="add-classify">分类label>
- <select class="form-control" name="category" id="add-classify">
- {% for classify in classify_list %}
- {% if classify == article.classify %}
- <option value="{{ classify.id }}" selected>{{ classify.name }}option>
- {% else %}
- <option value="{{ classify.id }}">{{ classify.name }}option>
- {% endif %}
-
-
- {% endfor %}
- select>
- div>
-
- <div class="form-group">
- <label for="add-tag">标签label>
- <select class="form-control" name="tag" id="add-tag" multiple>
- {% for tag in tag_list %}
- {% if tag in tag_list %}
- <option value="{{ tag.id }}" selected>{{ tag.name }}option>
- {% else %}
- <option value="{{ tag.id }}">{{ tag.name }}option>
- {% endif %}
-
- {% endfor %}
- select>
- div>
- <button class="btn btn-success form-control">上传文章button>
- form>
-
- {% endblock %}
-
- {% block js %}
- <script>
- KindEditor.ready(function (K) {
- window.editor = K.create('#editor_id', {
- width: '100%',
- height: '300px',
- resizeType: '1',
- // 上传图片相关
- uploadJson: '/put_img/',
- //filePostName: 'myfile', //默认imgFile
- //extraFileUploadParams: {
- // 'csrfmiddlewaretoken': '{{ csrf_token }}'
- // } 后端没有取消校验 需要传csrf
- });
- });
- script>
- {% endblock %}
- def alter_article(request):
- pk = request.GET.get('pk')
- # 需要当前文章 当前用户的分类和标签
- if request.method == 'GET':
- article = Article.objects.filter(pk=pk).first()
- classify_list = Classify.objects.filter(blog=request.user.blog)
- tag_list = Tag.objects.filter(blog=request.user.blog)
- return render(request, 'backend/alter_article.html',
- context={'article': article, 'classify_list': classify_list, 'tag_list': tag_list})
- # post请求 修改文章
- title = request.POST.get('title')
- content = request.POST.get('content')
- # BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
- bs = BeautifulSoup(content, features='html.parser')
- # 截取html文本,将空格和换行替换成空,并截取70个字符
- desc = bs.text.replace(' ', '').replace('\n', '')[:70] + '...'
- # 剔除script标签
- script_list = bs.findAll('script')
- for i in script_list:
- i.decompose() # 将每个script标签删除
- classify = request.POST.get('category')
- tag = request.POST.getlist('tag') # 这是多对多的
- article = Article.objects.filter(pk=request.GET.get('pk')) # 必须是一个queryset
- # 还需要将该文章的评论点赞点踩一起更新
- up_num = Article.objects.filter(pk=pk).first().up_num
- down_num = Article.objects.filter(pk=pk).first().down_num
- comment_num = Article.objects.filter(pk=pk).first().comment_num
- with transaction.atomic():
- article.update(title=title, desc=desc, classify_id=classify, content=str(bs), blog=request.user.blog,
- up_num=up_num, down_num=down_num, comment_num=comment_num)
- article.first().save()
- # 多对多关系添加
- article.first().tag.set(tag)
- return redirect(f'/{request.user.username}/articles/{pk}')
- from django.core.mail import send_mail
- # (subject, message, from_email, recipient_list,)
- res1 = send_mail('邮件标题', '邮件内容', settings.EMAIL_HOST_USER, ["@qq.com"])
settings.py配置
- # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
- EMAIL_HOST = 'smtp.qq.com' # 如果是 163 改成 smtp.163.com
- EMAIL_PORT = 465
- EMAIL_HOST_USER = '@qq.com' # 帐号
- EMAIL_HOST_PASSWORD = '***' # 密码
- DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
- #这样收到的邮件,收件人处就会这样显示
- #DEFAULT_FROM_EMAIL = ''
- EMAIL_USE_SSL = True #使用ssl
- #EMAIL_USE_TLS = False # 使用tls
-
- #EMAIL_USE_SSL 和 EMAIL_USE_TLS 是互斥的,即只能有一个为 True
on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。models.CASCADE
删除关联数据,与之关联也删除models.DO_NOTHING
删除关联数据,引发错误IntegrityErrormodels.PROTECT
删除关联数据,引发错误ProtectedErrormodels.SET_NULL
删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空)models.SET_DEFAULT
删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值)
OneToOneField就是ForeignKey + unique
max_length (最大长度)
null=True (可以为空)
default=‘’ (设置默认值)
unique=True (数据值必须唯一)
db_index=True (设置索引)
verbose_name=‘’ (注释)
db_constraint=False (数据约束 放在ForeignKey中,不建立外键关联 可以使用正反向查询 可能存在脏数据 可在代码层面进行限制)
自动创建第三张表
手动创建第三张表(当中间表除了关联字段外还需其他字段)
手动创建第三章关系表
字段类属性中增加:through= 通过哪张表进行关联 througu_fields= 设置关联的字段
OneToOneField,ForeignKey,ManyToManyFieldrelated_name:反向操作时,使用的字段名,用于代替原反向查询的’表名_set‘。
related_query_name:反向操作时,使用的连接的前缀,用户替换表名。
终端执行数据库迁移命令
python38 manage.py makemigretions
python38 manage.py migrater
解决方案一:
在任意的双下init文件中编写以下代码:
- import pymysql
- pymysql.install_as_MySQLdb()
在django2.0.7及以后版本,需要改源码才能使用,operations.py中的146行,改成query = query.encode(errors=‘replace’)。
解决方法二:
pip3 instasll mysqlclient