2. 博客项目(一)

博客项目(一)

版权声明:本博客来自路飞学城Python全栈开发培训课件,仅用于学习之用,严禁用于商业用途。
欢迎访问路飞学城官网:https://www.luffycity.com/

1. 项目分析

本项目是在图书管理项目的基础上开发一个BBS博客系统,博客系统具有用户主页、文章详情页、评论、点赞与踩等功能。

项目需求如下:

  • 基于ajax和用户认证组件实现登录验证
  • 基于ajax和form组件实现注册功能
  • 系统首页文章列表的渲染
  • 个人站点页面设计
  • 文章详情的继承
  • 点赞与踩
  • 评论功能
  • 富文本编辑器的使用
  • 防止xss攻击

2. 表结构

表结构设计:

  • 用户表UserInfo:nid、telephone、avatar(用户头像)、create_time、blog(一对一)

  • 个人博客Blog:nid、title(个人博客标题) 、 site_name(个人博客站点名称)、 theme(个人博客主题样式)

  • 文章Article:

    • nid
    • title(文章标题)
    • desc(文章简述)
    • create_time
    • content(文章内容)
    • comment_count(评论数量)
    • up_count(点赞数量)
    • down_count(踩数量)
    • user(文章发布人)
    • category(一对多,文章分类)
    • tags(多对多,文章标签)
  • 文章分类Category:nid 、title、blog(外键,一对多)

  • 文章标签Tag:nid 、title、blog(多对多)

  • 文章标签关系Article2Tag:nid、article(外键,一对多)、tag(外键,一对多)

  • 文章点赞与踩ArticleUpDown:

    • nid
    • user(外键,一对多,点赞或踩的人)
    • article(外键,一对多,点赞或踩的文章)
    • is_up(点赞还是踩)
  • 评论表Comment:

    • nid
    • article(评论文章)
    • user(评论人)
    • content(评论内容)
    • create_time
    • parent_comment(自关联外键,用于标识是一级评论还是对评论的评论)

创建和配置数据库(略)

创建模型

from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.


class UserInfo(AbstractUser):
    """
    用户信息
    """
    nid = models.AutoField(primary_key=True)
    telephone = models.CharField(max_length=11, null=True, unique=True)
    avatar = models.FileField(upload_to='avatars/', default="/avatars/default.png")
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    blog = models.OneToOneField(to='Blog', to_field='nid', null=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.username


class Blog(models.Model):
    """
    博客信息
    """
    nid = models.AutoField(primary_key=True)
    title = models.CharField(verbose_name='个人博客标题', max_length=64)
    site_name = models.CharField(verbose_name='站点名称', max_length=64)
    theme = models.CharField(verbose_name='博客主题', max_length=32)

    def __str__(self):
        return self.title


class Category(models.Model):
    """
    博主个人文章分类表
    """
    nid = models.AutoField(primary_key=True)
    title = models.CharField(verbose_name='分类标题', max_length=32)
    blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid', on_delete=models.CASCADE)

    def __str__(self):
        return self.title


class Tag(models.Model):
    """
    文章标签表
    """
    nid = models.AutoField(primary_key=True)
    title = models.CharField(verbose_name='标签名称', max_length=32)
    blog = models.ForeignKey(verbose_name='所属博客', to='Blog', to_field='nid', on_delete=models.CASCADE)

    def __str__(self):
        return self.title


class Article(models.Model):
    """
    文章表
    """
    nid = models.AutoField(primary_key=True)
    title = models.CharField(verbose_name='文章标题', max_length=50)
    desc = models.CharField(verbose_name='文章描述', max_length=255)
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    content = models.TextField()
    comment_count = models.IntegerField(default=0)
    up_count = models.IntegerField(default=0)
    down_count = models.IntegerField(default=0)
    user = models.ForeignKey(verbose_name='作者', to='UserInfo', to_field='nid', on_delete=models.CASCADE)
    category = models.ForeignKey(to='Category', to_field='nid', on_delete=models.CASCADE)
    tags = models.ManyToManyField(
        to='Tag',
        through='Article2Tag',
        through_fields=('article', 'tag'),
    )

    def __str__(self):
        return self.title


class Article2Tag(models.Model):
    nid = models.AutoField(primary_key=True)
    article = models.ForeignKey(verbose_name='文章', to='Article', to_field='nid', on_delete=models.CASCADE)
    tag = models.ForeignKey(verbose_name='标签', to='Tag', to_field='nid', on_delete=models.CASCADE)

    class Meta:
        unique_together = [
            ('article', 'tag'),
        ]

    def __str__(self):
        v = self.article.title + "---" + self.tag.title
        return v


class ArticleUpDown(models.Model):
    """
    点赞表
    """
    nid = models.AutoField(primary_key=True)
    user = models.ForeignKey('UserInfo', null=True, on_delete=models.CASCADE)
    article = models.ForeignKey('Article', null=True, on_delete=models.CASCADE)
    is_up = models.BooleanField(default=True)

    class Meta:
        unique_together = [
            ('article', 'user'),
        ]


class Comment(models.Model):
    """
    评论表
    """
    nid = models.AutoField(primary_key=True)
    article = models.ForeignKey(verbose_name='评论文章', to='Article', to_field='nid', on_delete=models.CASCADE)
    user = models.ForeignKey(verbose_name='评论者', to='UserInfo', to_field='nid', on_delete=models.CASCADE)
    content = models.CharField(verbose_name='评论内容', max_length=255)
    create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    parent_comment = models.ForeignKey('self', null=True, on_delete=models.CASCADE)

    def __str__(self):
        return self.content

创建表结构(略)

头像上传相关知识补充:

  • FileField与ImageFiled

    # modeld.py表模型
    class UserInfo(AbstractUser):
    
          avatar = models.FileField(upload_to='avatars/', default="/avatars/default.png")
    
    # views.py添加数据
    avatar_obj=request.FILES.get("avatar")
    user_obj=UserInfo.objects.create_user(username=user,password=pwd,email=email,avatar=avatar_obj)
    
    """
    Dajngo实现:
    会将文件对象下载到项目的根目录中avatars文件夹中(如果没有avatar文件夹,Django会自动建),
    user_obj的avatar存的是文件的相对路径。
    """
    
  • media配置

    Media 配置之MEDIA_ROOT:
    
    Dajngo有两种静态文件:
    
         /static/   :  js,css,img
         /media/     :   用户上传文件
    
    一旦在settings.py配置了
        MEDIA_ROOT=os.path.join(BASE_DIR,"media")
    
    Dajngo会将文件对象下载到MEDIA_ROOT中avatars文件夹中(如果没有avatar文件夹,Django会自动创建),user_obj的avatar存的是文件的相对路径。
    Media 配置之MEDIA_URl:
    
    浏览器如何能直接访问到media中的数据
    第一步:在settings.py中配置:
        MEDIA_URL="/media/"
    第二步:在urls.pt中配置一条路由:
        # media配置:
        re_path(r"media/(?P<path>.*)$",serve,{"document_root":settings.MEDIA_ROOT})
    

3. 用户功能

setting配置

# setting.py文件

...

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'cnblog',
        'USER': 'root',
        'PASSWORD': '123',
        'HOST': '127.0.0.1',
        'PORT': 3306
    }
}
...
AUTH_USER_MODEL = "blog.UserInfo"
...
# 静态文件配置
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]


LOGIN_URL = '/login/'

# 与用户上传相关的配置
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
...

创建From模型

# MyForms.py
import re
from django import forms
from django.forms import widgets
from blog import models
from django.core.exceptions import ValidationError

from django.contrib import auth


# 自定义验证规则
def mobile_validate(value):
    mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$')
    if not mobile_re.match(value):
        raise ValidationError('手机号码格式错误')


class UserForm(forms.Form):
    username = forms.CharField(max_length=32,
                               min_length=5,
                               label="用户名",
                               error_messages={"required": "该字段不能为空",
                                               'min_length': '用户名最少为5个字符',
                                               'max_length': '用户名最多为32个字符'},
                               widget=widgets.TextInput(attrs={"class": "form-control",
                                                               'placeholder': '用户名'}, ))
    pwd = forms.CharField(max_length=32,
                          error_messages={"required": "该字段不能为空"},
                          label="密码",
                          widget=widgets.PasswordInput(attrs={"class": "form-control",
                                                              'placeholder': '密码'}, ))
    re_pwd = forms.CharField(max_length=32,
                             error_messages={"required": "该字段不能为空"},
                             label="确认密码",
                             widget=widgets.PasswordInput(attrs={"class": "form-control",
                                                                 'placeholder': '确认密码'}, ))
    email = forms.EmailField(max_length=32,
                             error_messages={"required": "该字段不能为空",
                                             'invalid': u'邮箱格式错误'},
                             label="邮箱",
                             widget=widgets.EmailInput(attrs={"class": "form-control",
                                                              'placeholder': u'邮箱'}, ))
    telephone = forms.CharField(validators=[mobile_validate, ],
                                error_messages={"required": "该字段不能为空"},
                                label="手机号码",

                                widget=widgets.TextInput(attrs={"class": "form-control",
                                                                'placeholder': u'手机号码'}, ))

    def clean_user(self):
        val = self.cleaned_data.get("user")

        user = models.UserInfo.objects.filter(username=val).first()
        if not user:
            return val
        else:
            raise ValidationError("该用户已注册!")

    def clean(self):
        pwd = self.cleaned_data.get("pwd")
        re_pwd = self.cleaned_data.get("re_pwd")

        if pwd and re_pwd:
            if pwd == re_pwd:
                return self.cleaned_data
            else:
                raise ValidationError("两次密码不一致!")
        else:
            return self.cleaned_data

配置路由

# url.py
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import serve
from blog import views
from cnblog import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('register/', views.register),
    path('login/', views.login),
    path('logout/', views.logout),
    path('reset_pwd/', views.reset_pwd),
    path('valid_code_img/', views.valid_code_img),
    ...
    
    # media配置:
    re_path(r"media/(?P<path>.*)$", serve, {"document_root": settings.MEDIA_ROOT}),
    ...
]

创建视图函数

# views.py文件
import os
import re
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db import transaction
from django.shortcuts import render, HttpResponse, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import auth
from django.http import JsonResponse
from django.db.models import F, aggregates, Count
import json
from django.core.mail import send_mail
from cnblog import settings
import threading
from bs4 import BeautifulSoup
from blog.utils import validCode
from blog import models
from blog import MyForms


# Create your views here.

def register(request):
    if request.method == "POST":
        form = MyForms.UserForm(request.POST)
        response = {"username": None, "msg": None}
        if form.is_valid():
            username = form.cleaned_data.get("username")
            pwd = form.cleaned_data.get("pwd")
            email = form.cleaned_data.get("email")
            avatar_obj = request.FILES.get("avatar")
            extra = {}
            # 头像为非必需参数,需要判断用户是否上传头像
            # 如果上传了该参数,则在创建用户时传入该参数;
            # 否则传入一个空字典,在创建用户时就会设置为默认头像
            if avatar_obj:
                extra["avatar"] = avatar_obj
            models.UserInfo.objects.create_user(username=username,
                                                password=pwd,
                                                email=email,
                                                **extra)
            response["username"] = username
        else:
            response["msg"] = form.errors
        return JsonResponse(response)
    else:
        form = MyForms.UserForm()
        return render(request, 'register.html', {"form": form})


def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    response = {"username": None, "msg": None}
    username = request.POST.get("username")
    pwd = request.POST.get("pwd")
    valid_code = request.POST.get("valid_code")
    valid_code_session = request.session.get("valid_code_str")
    if valid_code.upper() == valid_code_session.upper():
        user = auth.authenticate(username=username, password=pwd)
        if user:
            auth.login(request, user)
            response["username"] = user.username
        else:
            response["msg"] = "用户名或密码错误!"
    else:
        response["msg"] = "验证码错误!"
    return JsonResponse(response)


def logout(request):
    auth.logout(request)
    return redirect('/login/')


@login_required()
def reset_pwd(request):
    if request.method == "GET":
        return render(request, "reset_pwd.html")
    response = {"flag": False, "msg": None}
    username = request.user.username
    pwd_old = request.POST.get("pwd_old")
    pwd_new = request.POST.get("pwd_new")
    re_pwd_new = request.POST.get("re_pwd_new")
    if pwd_new and re_pwd_new:
        if pwd_new != re_pwd_new:
            response["msg"] = "两次密码不一致!"
        elif not re.match(r'^\w{1,33}$', pwd_new):
            response["msg"] = "密码格式不正确!"
        else:
            user_obj = auth.authenticate(username=username, password=pwd_old)
            if user_obj:
                user_obj.set_password(pwd_new)
                user_obj.save()
                response["flag"] = True
            else:
                response["msg"] = "原密码不正确!"
    else:
        response["msg"] = "新密码格式不正确!"
    return JsonResponse(response)


def valid_code_img(request):
    img_data = validCode.get_valid_code_img(request)
    return HttpResponse(img_data)

验证码函数

# utils/validCode.py文件
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
import random


def get_random_color():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


def get_valid_code_img(request):

    img = Image.new("RGB", (270, 40), color=get_random_color())
    draw = ImageDraw.Draw(img)
    kumo_font = ImageFont.truetype("static/font/kumo.ttf", size=32)

    valid_code_str = ""

    for i in range(5):
        random_num = str(random.randint(0, 9))
        random_low_alpha = chr(random.randint(95, 122))
        random_upper_alpha = chr(random.randint(65, 90))
        random_char = random.choice([random_num, random_low_alpha, random_upper_alpha])
        draw.text((i * 50 + 20, 5), random_char, get_random_color(), font=kumo_font)
        # 保存验证码字符串
        valid_code_str += random_char

    # 生成噪点
    # width=270
    # height=40
    # for i in range(10):
    #     x1=random.randint(0,width)
    #     x2=random.randint(0,width)
    #     y1=random.randint(0,height)
    #     y2=random.randint(0,height)
    #     draw.line((x1,y1,x2,y2),fill=get_random_color())
    #
    # for i in range(100):
    #     draw.point([random.randint(0, width), random.randint(0, height)], fill=get_random_color())
    #     x = random.randint(0, width)
    #     y = random.randint(0, height)
    #     draw.arc((x, y, x + 4, y + 4), 0, 90, fill=get_random_color())
    
    request.session["valid_code_str"] = valid_code_str
    f = BytesIO()
    img.save(f, "png")
    data = f.getvalue()
    return data

创建模板

register.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户注册</title>
    <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
    <script src="/static/js/jquery-3.2.1.min.js"></script>
    <style>
        .error{
            color:red;
            margin-left:10px
        }
        #avatar {
            display: none;
        }
        #avatar_img{
            width: 60px;
            height: 60px;
            margin-left: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <h2>用户注册</h2>
                <hr>
                <form id="form">
                    {% csrf_token %}
                    {% for field in form %}
                        <div class="form-group">
                            <label for="{{ field.auto_id }}">{{ field.label }}</label>
                            {{ field }} <span class="error pull-right"></span>
                        </div>
                    {% endfor %}
                    <div>
                        <label for="avatar">
                            头像
                            <img  id="avatar_img" src="/media/avatars/default.png" alt="">
                        </label>
                        <input type="file" id="avatar" name="avatar">
                    </div>
                    <input type="button" class="btn btn-default reg_btn pull-right" value="提交">
                </form>
            </div>
        </div>
    </div>
    <script>
        $(function () {
            // 头像预览
            $("#avatar").change(function () {
                // 获取用户选中的文件对象
                var file_obj = $(this)[0].files[0];
                // 获取文件对象的绝对路径
                var reader = new FileReader();
                reader.readAsDataURL(file_obj);
                reader.onload = function(){
                    $("#avatar_img").attr("src", reader.result)
                };
            });
            // 基于Ajax提交数据并校验
            $(".reg_btn").click(function () {
                var form_data = new FormData();
                var request_data = $("#form").serializeArray();
                $.each(request_data,function (index, data) {
                    form_data.append(data.name, data.value);
                });
                form_data.append("avatar", $("#avatar")[0].files[0]);
                $.ajax({
                    url:"",
                    type:"post",
                    contentType:false,
                    processData:false,
                    data:form_data,
                    success:function (data) {
                        if(data.username){
                            location.href="/login/";
                        }else{
                            // 清空错误信息
                            $("span.error").html("");
                            // 设置form-group校验错误样式(bootstrap)
                            $(".form-group").removeClass("has-error");
                            // 显示此次提交的错误校验信息
                            $.each(data.msg, function (field, error_list) {
                                // 全局钩子校验错误信息
                                if(field=="__all__"){
                                    $("#id_re_pwd").next().html(error_list[0]).parent().addClass("has-error");
                                    $("#id_re_pwd").parent().addClass("has-error");
                                }
                                $("#id_" + field).next().html(error_list[0]);
                                $("#id_" + field).parent().addClass("has-error");
                            })
                        }
                    }
                })
            })
        })
    </script>
</body>
</html>

小技巧:

1、用户头像的表单通过label的for属性指向file标签,而file标签设置为隐藏状态。lable标签内包含头像图片,实现点击头像弹出文本框的功能。

2、js中FormData 对象的使用:

  • 用一些键值对来模拟一系列表单控件:即将form 中表单元素的 name 与 value 组装成一个 queryString
  • 异步上传二进制文件

3、$("#form").serializeArray():

  • serializeArray() 方法序列化表单元素,返回 JSON 数据结构数据。

login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
    <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
    <script src="/static/js/jquery-3.2.1.min.js"></script>
    <style>
        .error{
            color:red;
            margin-left: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <h2>用户登录</h2>
                <hr>
                <form >
                    {% csrf_token %}
                    <div class="form-group">
                        <label for="username">用户名</label>
                        <input type="text" id="username" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="pwd">密码</label>
                        <input type="password" id="pwd" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="valid_code">验证码</label>
                        <div class="row">
                            <div class="col-md-6">
                                <input type="text" class="form-control" id="valid_code">
                            </div>
                            <div class="col-md-6">
                                <img width="270" height="36" id="valid_code_img" src="/valid_code_img/" alt="">
                            </div>
                        </div>
                    </div>
                    <input type="button" class="btn btn-default login_btn" value="登录">
                    <span class="error"></span>
                    <a href="/register/" class="btn btn-primary  pull-right">注册</a>
                </form>
            </div>
        </div>
    </div>
    <script>
        $(function () {
            // 刷新验证码
            $("#valid_code_img").click(function () {
                $(this)[0].src +="?"
            })
            // 登录验证
            $('.login_btn').click(function () {
                $.ajax({
                    url:"",
                    type:"post",
                    data: {
                        username: $("#username").val(),
                        pwd: $("#pwd").val(),
                        valid_code: $("#valid_code").val(),
                        csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
                    },
                    success:function (data) {
                        if(data.username){
                            location.href="/index/";
                        }else{
                            $(".error").text(data.msg);
                        }
                    }
                })
            })
        })
    </script>
</body>
</html>

小技巧:

通过改变验证码图像的src实现验证码的异步刷新。

效果展示

用户登录:

在这里插入图片描述

用户注册:

在这里插入图片描述

修改密码:

在这里插入图片描述

未完待续
学python,找路飞!更多精彩,尽在路飞学城

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值