django之BBS项目小结

一、项目配置

暴露静态文件

settings.py
--------------------
MEDIA_ROOT = os.path.join(BASE_DIR,'media')


urls.py
-----------------
url(r'^media/(?P<path>.*)',serve,{"document_root":settings.MEDIA_ROOT})

views.py下的模块导入

from django.shortcuts import render, HttpResponse, reverse, redirect
from django.http import JsonResponse
from app01.myforms import MyRegForm
from app01 import models
from django.contrib import auth
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django.db.models import F
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
from app01.common import get_random, get_random_color
from app01.common import Pagination
from bs4 import BeautifulSoup

二、功能实现

1.登录功能

登录功能是采用AJAX请求将用户名、密码以及验证码发送到后台进行校验后,再由后台将校验数据返回。

验证码功能的实现:

<div class="row">
    <div class="col-md-6">
        <label for="id_code">验证码</label>
        <input type="text" name="code" id="id_code" class="form-control">
    </div>
    <div class="col-md-6">
        <img src="{% url 'app01_get_code' %}" alt="" width="400" height="35" style="margin-top: 24px"
             id="id_img">
    </div>
</div>

        {#  登录ajax请求  #}
        $('#id_login').click(function () {
            $login_btn = $(this);
            $.ajax({
                url: '',
                type: 'post',
                data: {'username': $('#id_username').val(),
						'password': $('#id_password').val(),
						'code':$('#id_code').val()
						},
                success: function (data) {
                    if (data.code === 1000) {
                        location.href = data.url
                    } else {
                        $login_btn.prev().prev().text(data.msg)
                    }
                }
            })
        })

验证码划出一块img区域,src指向后台验证码生成的返回函数。验证码实际上是后台随机生成的一张带有5个数字字母的图片。img中的src有三种常用的写法:1.完整URL 2.后台相对位置 3.一个视图函数。当采用第三种时,只要前端页面上src的值发生了变化,就会向后台重新发生post请求获取新的验证码。

# 前端代码
$('#id_img').click(function () {
    let _url = $(this).attr('src');
    $(this).attr('src', _url += '?')
});

# 后端验证码生成代码
def get_code(request):
    # 图片对象(模式,尺寸,颜色rgb)
    img_obj = Image.new('RGB', (400, 35), get_random_color())
    # 有imgobj生成imgdraw对象
    draw_obj = ImageDraw.Draw(img_obj)
    # imgFont对象
    img_font = ImageFont.truetype(r'static/font/111.ttf', 24)
    # 随机产生5个数字字母
    code = get_random()
    # img_obj对象调用text方法,写上验证码
    for index, c in enumerate(list(code)):
        draw_obj.text((45 + index * 60, 0), c, get_random_color(), font=img_font)
    
    # 用bytesio对象暂存图片对象
    io_obj = BytesIO()
    img_obj.save(io_obj, 'png')
    
    # request的session中保存验证码以做校验
    request.session['code'] = code
    print(code)
    # 直接返回图片的二进制数据
    return HttpResponse(io_obj.getvalue())
@csrf_exempt  # 无需csrf装饰器
def login(request):
    back_dict = {
        'code': 1000,
        'msg': '登录成功',
        'url': reverse('app01_home')
    }
    if request.method == 'GET':
        return render(request, 'login.html', locals())
    username = request.POST.get('username')
    password = request.POST.get('password')
    code = request.POST.get('code')
    
    # 校验用户名密码
    user_obj = auth.authenticate(username=username, password=password)
    
    # 校验 验证码
    if code != request.session.get('code'):
        back_dict['code'] = 2000
        back_dict['msg'] = '验证码错误'
        return JsonResponse(back_dict)
    if not user_obj:
        back_dict['code'] = 2000
        back_dict['msg'] = '用户名或密码错误'
        return JsonResponse(back_dict)
    auth.login(request, user_obj)
    return JsonResponse(back_dict)

2.注册功能

注册功能主要由强大的form表单完成。

前端页面需要渲染头像批量渲染错误信息聚焦输入框时消除错误信息

后端需要form表单校验(用户名是否存在等等)

form表单

class MyRegForm(forms.Form):
    username = forms.CharField(label='用户名', error_messages={'required': '用户名不能为空'},
                               validators=[
                                   RegexValidator(
                                       r'^[a-zA-Z0-9_-]{4,16}$',
                                       '用户名由大小写字母及数字下划线组成(4-16位)')],
                               widget=forms.widgets.TextInput(attrs={'class': 'form-control'}))
    password = forms.CharField(label='密码',error_messages={'required': '密码不能为空'},
                               validators=[
                                   RegexValidator(r'^.*(?=.{3,})(?=.*d).*$',
                                                  '最少3位,包括字母,1个数字,1个特殊字符')],
                               widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}))

    re_password = forms.CharField(label='确认密码',error_messages={'required': '密码不能为空'},
                                  widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}))

    email = forms.EmailField(label='邮箱',
                             error_messages={'required': '邮箱不能为空', 'invalid': '邮箱格式不正确'},
                             widget=forms.EmailInput(attrs={'class': 'form-control'}))

    def clean(self):
        password = self.cleaned_data.get('password')
        re_password = self.cleaned_data.get('re_password')
        if password and password != re_password:
            self.add_error('re_password', '两次密码不一致')
        return self.cleaned_data

    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

views.py

def register(request):
    back_dict = {
        'code': 1000,
        'msg': '注册成功',
        'url': reverse('app01_login')
    }
    form_obj = MyRegForm()
    if request.method == 'GET':
        return render(request, 'register.html', locals())

    form_obj = MyRegForm(request.POST)
    # 注册成功
    if form_obj.is_valid():
        user_dict = form_obj.cleaned_data
        # 获取文件对象
        user_dict['avatar'] = request.FILES.get('avatar')
        user_dict.pop('re_password')
        print(user_dict)
        models.UserInfo.objects.create_user(**user_dict)
        return JsonResponse(back_dict)
    # 注册失败
    back_dict['code'] = 2000
    back_dict['msg'] = form_obj.errors
    return JsonResponse(back_dict)

# 如果你的MEDIA_ROOT是/media/文件夹,而你的上传文件夹upload_to=“avatar", 那么你上传的文件会自动存储到/media/avatar/文件夹。

前端页面主要代码:

<form action="" id="id_form">
    {% csrf_token %}
    {% for obj in form_obj %}
    <div class="form-group">
        <label for="{{ obj.id_for_label }}">{{ obj.label }}</label>
        {{ obj }}
        <span style="color: red"></span>
    </div>
    {% endfor %}
</form>
<div class="form-group">
    <label for="id_avatar">上传头像&nbsp&nbsp&nbsp&nbsp<img src="{% static 'default.jpg' %}" alt="" width="100px" height="100px" id="id_img"></label>
    <input type="file" id="id_avatar">
</div>

用户头像上传需要script控制实现

$('#id_avatar').change(function () {
    let file_obj = $(this)[0].files[0];
	
    let FileReader_obj = new FileReader();

    FileReader_obj.readAsDataURL(file_obj);
	<-!> 等待FileReader对象读取文件数据完毕</-!>
    FileReader_obj.onload = function () {
        $('#id_img').attr('src', FileReader_obj.result)
    }
});

{#  ajax发送数据,发送文件时需要借助 formdata对象  #}
$('#id_submit').click(function () {
    let formdata_obj = new FormData();
    {#console.log($('#id_form').serializeArray());#}
    $.each($('#id_form').serializeArray(), function (index, obj) {
        formdata_obj.append(obj.name, obj.value);
    });
    formdata_obj.append('avatar', $('#id_avatar')[0].files[0]);
    $.ajax({
        url: '',
        type: 'post',
        processData: false,
        contentType: false,
        data: formdata_obj,
        success: function (data) {
            if (data.code === 1000) {
                swal('注册成功');
                setTimeout(function () {
                    location.href = data.url;   {# 跳转到后台返回的url #}
                },1500)
            }
            else{
                $.each(data.msg, function (name, value) {
                    let id = '#id_' + name;
                    $(id).addClass('has-error').next().text(value);
                })
            }
        }
    })
});

{#  聚焦动态效果  #}
$('input').focus(function () {
	$(this).removeClass('has-error').next().text('')
})

3.修改密码、修改头像

修改密码、修改头像较为简单,仅仅是数据的替换,修改密码前要对原密码进行校验。

@login_required
def set_password(request):
    back_dict = {
        'code': 1000,
        'msg': '修改密码成功',
        'url': '/login/',
    }
    if request.is_ajax():
        old_password = request.POST.get('old_password')
        new_password = request.POST.get('new_password')
        re_password = request.POST.get('re_password')
        if new_password != re_password:
            back_dict['code'] = 2000
            back_dict['msg'] = '两次密码不一致'
            return JsonResponse(back_dict)
        if not request.user.check_password(old_password):  # 调用check_password进行校验
            back_dict['code'] = 2000
            back_dict['msg'] = '原密码错误'
            return JsonResponse(back_dict)
        request.user.set_password(new_password)
        request.user.save()
        auth.logout(request)
        return JsonResponse(back_dict)

    
@login_required
def set_avatar(request):
    back_dict = {
        'code':1000,
    }
    if request.method == 'GET':
        return render(request,'backend/set_avatar.html',locals())
    avatar = request.FILES.get('avatar')
    request.user.avatar = avatar
    request.user.save()  # 使用save方法可自动加上路径前缀 与 update有所不同。

    return JsonResponse(back_dict)

4.home页面

home页面主要是前端页面布局的搭建,后端数据的提取,以及分页器的使用。

def home(request):
    cur_page = request.GET.get('page')
    try:
        cur_page = int(cur_page)
    except:
        cur_page = 1
    info_per_page = 2
    article_list = models.Article.objects.filter()
    pagination = Pagination(current_page=cur_page,all_count=article_list.count(),per_page_num=info_per_page)
    article_list = article_list[pagination.start:pagination.end]

    return render(request, 'home.html', locals())

5.文章内容展示页

前端需要渲染跟文章有关的数据

  • 文章标题、内容
  • 文章相关评论及子评论
  • 点赞、点踩
url(r'^(?P<username>w+)/articles/(?P<article_id>d+)', views.article, name='app01_article'),

def article(request, username, article_id):
   user_obj = models.UserInfo.objects.filter(username=username).first()
   blog_obj = user_obj.blog
   article_obj = models.Article.objects.filter(blog=blog_obj, pk=article_id).first()
   comment_list = models.Comment.objects.filter(article=article_obj)
   return render(request, 'article.html', locals())

6.评论功能

后端主要对评论进行保存,并向前端返回一个字典。

用js对当前页面html代码进行增删,可以实现评论临时渲染的效果。

url(r'^comment/', views.comment, name='app01_comment'),

@login_required
def comment(request):
    back_dict = {
        'code': 1000,
        'msg': '评论成功'
    }
    if request.is_ajax():
        article_id = request.POST.get('article_id')
        parent_id = request.POST.get('parent_id')
        content = request.POST.get('content')
        if parent_id:
            name, content = content.split('
', 1)
            print(content)
        models.Comment.objects.create(parent_id=parent_id, article_id=article_id, user=request.user, content=content)
        models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
        return JsonResponse(back_dict)
    else:
        return redirect('/home/')

前端页面要对评论内容进行ajax提交,在页面不刷新的情况下进行临时渲染。

$('#id_comment').click(function () {
	let content = $('#id_content').val()
    $.ajax({
        url: '/comment/',
        type: 'post',
        data: {
            'article_id':{{ article_id }},
            'content': content,
            'csrfmiddlewaretoken': '{{ csrf_token }}',
            'parent_id': parent_id,
        },
        success: function (data) {
            $('#id_content').val('');
            var temp =`
            <li class="list-group-item">
                <span>
                    <span class="glyphicon glyphicon-comment"></span>
                    <a 	href="/${UserName}/">${UserName}</a>
                </span>
                <div>
                    ${conTent}
                </div>
            </li>`;
        }
    })
});

${content}是js中的模版字符串语法,用于在模版字符串中获取content变量的值。

7.点赞点踩功能

点赞点踩功能主要是用ajax对用户点击事件在后台进行判断是否已经点击过,然后将后台的信息返回到前端,前端再根据后台的信息进行相关的渲染。

def up_or_down(request):
    back_dict = {
        'code': 1000,
        'msg': '',
    }
    if request.is_ajax():
        if request.user.is_authenticated:
            user_obj = request.user
            article_id = request.POST.get('article_id')
            user_id = user_obj.id
            flag = request.POST.get('flag')
            flag = 1 if flag == 'true' else 0
            up_obj = models.UpAndDown.objects.filter(article_id=article_id, user_id=user_id)
            if not up_obj:
                models.UpAndDown.objects.create(article_id=article_id, user_id=user_id, is_up=flag)
                if flag:
                    back_dict['msg'] = '点赞成功'
                    models.Article.objects.filter(pk=article_id).update(up_num=F('up_num') + 1)
                else:
                    back_dict['msg'] = '点踩成功'
                    models.Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
            else:
                back_dict['msg'] = '您已经点过了'
        else:
            back_dict['code'] = 2000
            back_dict['msg'] = '请先登录'
        return JsonResponse(back_dict)
    else:
        return redirect('/home')

8.个人站点页面

个人站点界面由左边菜单栏2-md及右边文章标题及摘要10-md组成。由于左边菜单栏显示的是文章的分类别展示,不仅在个人站点显示,而且在文章展示页也可以共用。所以可以制作成templatetag实现复用

templatesags-mytag.py

from django.template import Library
from django.db.models import Count
from django.db.models.functions import TruncMonth
from app01 import models

register = Library()
# 注册自定义tag标签
@register.inclusion_tag('left_menu.html', name='my_left')
def my_left(username):
    user_obj = models.UserInfo.objects.filter(username=username).first()
    blog_obj = user_obj.blog
    tag_list = models.Tag.objects.filter(blog=blog_obj).annotate(c=Count('article')).values('c', 'name', 'pk')
    category_list = models.Category.objects.filter(blog=blog_obj).annotate(c=Count('article')).values('c', 'name', 'pk')
    date_list = models.Article.objects.filter(blog=blog_obj).annotate(month=TruncMonth('create_time')).values(
        'month').annotate(c=Count('pk')).values('c', 'month')
    article_list = models.Article.objects.filter(blog=blog_obj)
    return locals()

left_menu.html

<div class="panel panel-danger">
    <div class="panel-heading">
        <h3 class="panel-title text-center">我的标签</h3>
    </div>
    <div class="panel-body text-center">
        {% for tag in tag_list %}
            <a href="/{{ user_obj.username }}/tags/{{ tag.pk }}">{{ tag.name }}({{ tag.c }})</a>
        {% endfor %}
    </div>
</div>
<div class="panel panel-info">
    <div class="panel-heading">
        <h3 class="panel-title text-center">我的分类</h3>
    </div>
    <div class="panel-body text-center">
        {% for category in category_list %}
            <a href="/{{ user_obj.username }}/category/{{ category.pk }}">{{ category.name }}({{ category.c }})</a>
        {% endfor %}

    </div>
</div>
<div class="panel panel-primary">
    <div class="panel-heading">
        <h3 class="panel-title text-center">随笔归档</h3>
    </div>
    <div class="panel-body text-center">
        {% for date in date_list %}
            <p><a href="/{{ user_obj.username }}/date/{{ date.month|date:'Y-m' }}">{{ date.month|date:'Y年m月' }}({{ date.c }})</a></p>
        {% endfor %}

    </div>
</div>

base.html

<div class="container-fluid">
    <div class="row">
        <div class="col-md-2">
        
        	{#  调用inclusion_tag #}
            {% load mytag %}
            {% my_left username %}
                
        </div>
        <div class="col-md-10">
            {% block content %}

            {% endblock %}
        </div>
    </div>
</div>

9.新增文章功能

后台除了对文章相关内容进行数据库的存储外。

  • 对用户的原文进行处理,防止XSS攻击。
  • 对用户上传的图片进行保存,并返回相应的信息。

前端需要对页面进行布局外,还需要用到第三方的在线文章编辑器。

  • 编辑器需要允许对用户上传图片,进行配置。

  • 用户上传图片实际上是post请求,需要加上csrftoken

url(r'^add_article/', views.add_article, name='app01_add_article'),

@login_required
def add_article(request):
    if request.method == 'GET':
        tag_list = models.Tag.objects.filter(blog=request.user.blog)
        category_list = models.Category.objects.filter(blog=request.user.blog)
        return render(request, 'backend/add_article.html', locals())
    title = request.POST.get('title')
    content = request.POST.get('content')
    soup = BeautifulSoup(content, 'html.parser')
    # 删掉script标签
    for tag in soup.find_all():
        if tag.name == 'script':
            tag.decompose()
            
    abstract = soup.text[0:150]
    tags = request.POST.getlist('tags')
    category = request.POST.get('category')
    # 插入文章
    article_obj = models.Article.objects.create(title=title, content=str(soup), category_id=category,blog=request.user.blog, abstract=abstract)
    tag_list = []
    for tag in tags:
        tag_list.append(models.Article2Tag(article=article_obj, tag_id=tag))
    models.Article2Tag.objects.bulk_create(tag_list)
    return redirect(reverse('app01_backend'))

add_article.html

{% extends 'backend/backend_base.html' %}

{% block css %}
    {% load static %}
    <script src="{% static 'kindeditor/kindeditor-all.js' %}"></script>
    <script src="{% static 'kindeditor/lang/zh-CN.js' %}"></script>

{% endblock %}

{% block article %}
    <h2>添加文章</h2>
    <form action="" method="post">
        <div>
            {% csrf_token %}
            <p>标题</p>
            <p><input type="text" class="form-control" name="title"></p>
        </div>
        <div>
            <p>内容</p>
            <textarea id="editor_id" name="content" style="700px;height:300px;">
        &lt;strong&gt;HTML内容&lt;/strong&gt;
        </textarea>
        </div>
        <div>
            <p>
                标签:
                {% for tag in tag_list %}
                    <input type="checkbox" name="tags" value="{{ tag.id }}">{{ tag.name }}&nbsp;&nbsp;&nbsp;
                {% endfor %}
            </p>
            <p>
                分类:
                {% for category in category_list %}
                    <input type="radio" name="category" value="{{ category.id }}">{{ category.name }}&nbsp;&nbsp;&nbsp;
                {% endfor %}
            </p>
        </div>
        <div>
            <input type="submit" class="pull-right btn btn-primary ">
        </div>
    </form>

{% endblock %}

{% block js %}
    <script>
        KindEditor.ready(function (K) {
            window.editor = K.create('#editor_id', {
                 '100%',
                minWidth: '100%',
                resizeType: 1,
                uploadJson:'/upload_img/',   {# 上传图片时发送的url地址 #}
                allowFileManager: true,
                extraFileUploadParams : {
                        'csrfmiddlewaretoken' : '{{ csrf_token }}', {# 携带csrf一起发送 #}
                }
            });
        });
    </script>
{% endblock %}

10.编辑文章

编辑文章功能与添加文章功能相似,要在用户编辑某文章时渲染好文章原来的数据。

编辑文章需要判断编辑文章的id是否是属于当前用户的,如果不属于直接跳转回主页面。

11.后台管理

后台管理需要渲染文章标题,编辑和删除操作按钮。

原文地址:https://www.cnblogs.com/Ghostant/p/12263595.html