一、项目配置
暴露静态文件
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">上传头像    <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;">
<strong>HTML内容</strong>
</textarea>
</div>
<div>
<p>
标签:
{% for tag in tag_list %}
<input type="checkbox" name="tags" value="{{ tag.id }}">{{ tag.name }}
{% endfor %}
</p>
<p>
分类:
{% for category in category_list %}
<input type="radio" name="category" value="{{ category.id }}">{{ category.name }}
{% 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.后台管理
后台管理需要渲染文章标题,编辑和删除操作按钮。