6.2 - BBS + BLOG系统
一、简介
博客系统开发:
1.注册,登录,首页
2.个人站点,分组:(分类,标签,归档)
3.文章详细页
4.点赞,踩灭
5.评论楼,评论树
6.后台管理,发布文章,文件上传
7.BeautifulSoup
8.日志
数据库 models.py
注册 /reg/
上传头像 request.FILES.get('avatar')
登录 /login/
随机验证码 /get_valid_img/
首页 /index/
个人站点
分类,标签,归档 /blog/egon/
文章详细页 /blog/egon/articles/2/
点赞,踩灭 /blog/poll/
ajax的post 事务
评论楼,评论树 /blog/comment/
根评论,子评论
render显示,ajax显示
后台管理,发布文章 /backend/index/
新建APP
认证装饰器
编辑器(KindEditor)
文件上传 /media/article_imgs/...
防止XSS攻击
BeautifulSoup
二、数据库
知识点:
1.继承AbstractUser
目的:为了使用用户认证组件 auth User
配置:AUTH_USER_MODEL = "blog.UserInfo"
class UserInfo(AbstractUser):
...
2.中介模型
多对多,第三张表自己生成。
class Article(models.Model):
...
tags = models.ManyToManyField(to='Tag', through='Article2Tag', through_fields=('article', 'tag'))
class Article2Tag(models.Model):
...
class Meta: # 联合唯一
unique_together = [
('article', 'tag'),
]
3.联合唯一
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)
class Meta: # 联合唯一
unique_together = [
('user', 'article'),
]
from django.db import models from django.contrib.auth.models import AbstractUser 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 = models.CharField(verbose_name='个人博客后缀', max_length=32, unique=True) 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): """ 文章信息 comment_count up_count down_count 为了查询时,效率高。 """ nid = models.AutoField(primary_key=True) title = models.CharField(max_length=50, verbose_name='文章标题') desc = models.CharField(max_length=255, verbose_name='文章描述') create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True) 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', null=True, on_delete=models.CASCADE) tags = models.ManyToManyField(to='Tag', through='Article2Tag', through_fields=('article', 'tag')) def __str__(self): return self.title class ArticleDetail(models.Model): """ 文章详细表 """ nid = models.AutoField(primary_key=True) content = models.TextField() article = models.OneToOneField(to='Article', to_field='nid', on_delete=models.CASCADE) 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): return self.article.title + '--' + self.tag.title 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 = [ ('user', 'article'), ] class Comment(models.Model): """ 评论表 """ nid = models.AutoField(primary_key=True) user = models.ForeignKey(verbose_name='评论者', to='UserInfo', to_field='nid', on_delete=models.CASCADE) article = models.ForeignKey(verbose_name='评论文章', to='Article', 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
三、admin
from django.contrib import admin
# Register your models here.
from blog.models import *
admin.site.register(UserInfo)
admin.site.register(Blog)
admin.site.register(Category)
admin.site.register(Tag)
admin.site.register(Article)
admin.site.register(ArticleDetail)
admin.site.register(Article2Tag)
admin.site.register(ArticleUpDown)
admin.site.register(Comment)
http://127.0.0.1:8020/admin/
四、注册
知识点:
1.form组件
class RegForm(forms.Form):pass
局部钩子 全局钩子
2.上传头像 avatar
图像预览
var reader = new FileReader();
上传文件
formdata = new FormData();
3.用户文件配置
avatar = models.FileField(upload_to='avatars/', default='avatars/default.png')
MEDIA_ROOT = os.path.join(BASE_DIR, 'blog', 'media')
MEDIA_URL = '/media/'
re_path(r'media/(?P<path>.*)$',serve,{'document_root':settings.MEDIA_ROOT})
五、登录
知识点:
1.验证码
随机生成5个字符,0-9 a-z A-Z
pip install pillow
from PIL import Image, ImageDraw, ImageFont
image = Image.new()
在内存中生成图片直接返回
from io import BytesIO
f = BytesIO()
2.request.session['valid_str'] = valid_str
存在session中,为了之后登录,验证是否通过
3.验证码点击刷新:
$('#valid_img').click(function () {
$(this)[0].src += '?'
});
4.认证组件
valid_str = request.session.get('valid_str')
if valid_str.upper() == valid_code.upper():
user = auth.authenticate(username = user, password = pwd)
if user:
auth.login(request, user)
六、首页
知识点:
1.bootstrap搭建页面
2.导航条
登录: username / 注销
未登录: 登录 / 注册
3.for循环
{% for article in article_list %}
{% endfor %}
七、个人站点
知识点:
1.文章列表,分类列表,标签列表,日期归档列表
文章列表: /blog/egon/
分类列表: /blog/egon/cate/python
标签列表: /blog/egon/tag/生活
日期归档列表: /blog/egon/archive/2018-06
2.模板继承
{% extends 'base.html' %}
{% block content %}
{% endblock content%}}
3.自定义标签
/blog/templatetags/my_tag.py
@register.inclusion_tag('menu.html')
def get_menu(username):
...
return {} # 去渲染 menu.html
4.分组查询 .annotate() / extra()应用
多表分组
tag_list = Tag.objects.filter(blog=blog).annotate(
count = Count('article')).values_list('title', 'count')
单表分组 / DATE_FORMAT() / extra()
date_list = Article.objects.filter(user=user).extra(
select={"create_ym": "DATE_FORMAT(create_time,'%%Y-%%m')"}).values('create_ym').annotate(
c = Count('nid')).values_list('create_ym', 'c')
5. 时间、区域配置
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
八、文章详细页
知识点:
1.模板继承
article = Article.objects.filter(pk=article_id).first()
{% extends 'base.html' %}
{% block content %}
...
{{ article.articledetail.content|safe }}
{% endblock content %}
九、点赞、踩灭
知识点:
1.ajax的post
var csrfmiddlewaretoken = $('input[name="csrfmiddlewaretoken"]').val();
2.事务
try: # article_id 与 user_id 联合唯一 所有使用 try ...
with transaction.atomic():
ArticleUpDown.objects.create(is_up=is_up, article_id=article_id, user_id=user_id)
...
Article.objects.filter(pk=article_id).update(up_count=F('up_count')+1)
except Exception as e:
...
3.F查询:
Article.objects.filter(pk=article_id).update(up_count=F('up_count')+1)
十、评论楼、评论树
知识点:
1.提交根评论
2.显示根评论
--- render显示
--- ajax显示
3.提交子评论
4.显示子评论
--- render显示
--- ajax显示
评论楼
评论树
1.ajax提交评论
post (csrfmiddlewaretoken)
pid = "" 根评论
pid = value 子评论
2.回复事件
@alex
val ="@" + $(this).attr('username')+ '\n';
3.事务
with transaction.atomic():
...
多个orm sql操作!
4.F查询,更新
Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)
评论树:
5.js匿名函数
(function(){})()
6.ajax get方式获取comment_list
$.each(comment_list,function(index,comment)){
...
s = '...'
if(pid){ //子评论
$('#'+pid).append(s)
}else{ //根评论
$('.comment_tree').append(s)
}
}
7.JsonResponse() 返回 non-dict objects 需要 safe=False
def get_comment_tree(request, article_id):
ret = list(Comment.objects.filter(article_id=article_id).values(
'pk', 'content', 'parent_comment_id', 'user__username').order_by('nid'))
return JsonResponse(ret, safe=False)
十一、后台管理、KindEditor、BeautifulSoup
知识点:
1.新建APP(backend)
settings:
INSTALLED_APPS = [..., 'backend.apps.BackendConfig',]
2.url分配
re_path(r'backend/', include(('backend.urls', 'backend'))),
3.认证装饰器
@login_required
settings:
LOGIN_URL = '/login/'
4.static配置
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'blog', 'static'),
os.path.join(BASE_DIR, 'backend', 'static'),
]
5.编辑器(KindEditor)
<textarea name="article_con" id="article_box" cols="30" rows="10"></textarea>
<script src="/static/kindeditor/kindeditor-all.js"></script>
KindEditor.ready(function (k) {
window.editor = k.create('#article_box', {
...
...
uploadJson: 'upload_img/',
extraFileUploadParams: {"csrfmiddlewaretoken":$('input[name="csrfmiddlewaretoken"]').val()},
filePostName: 'img'
})
})
6.文件上传
用户文件存在 /media/article_imgs/...
media_path = settings.MEDIA_ROOT
path = os.path.join(media_path, 'article_imgs', img_obj.name)
返回json
img_obj = request.FILES.get('img')
res = {
"url": "/media/article_imgs/"+img_obj.name,
"error": 0
}
return HttpResponse(json.dumps(res))
7.发布文章
防止XSS攻击 BeautifulSoup,对网页,解析数据
article_con = request.POST.get('article_con')
soup = BeautifulSoup(article_con, 'html.parser')
# 过滤script, 删除了所有的script标签
for tag in soup.find_all():
if tag.name == 'script':
tag.decompose()
# soup.prettify() == str(soup)
return redirect(reverse('backend:index'))
十二、code
https://github.com/alice-bj/cnblog
结构:
cnblog
├── backend
│ ├── static
│ │ ├── css
│ │ │ ├── backend.css
│ │ ├── js
│ │ │ ├── add_article.js
│ │ │ ├── jquery-3.2.1.js
│ │ │ └── jquery-3.2.1.min.js
│ │ └── kindeditor
│ │ ├── kindeditor-all.js
│ │ ├── kindeditor-all-min.js
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── blog
│ ├── admin.py
│ ├── apps.py
│ ├── media
│ │ ├── article_imgs
│ │ │ ├── girl.jpg
│ │ │ ├── jiqimao.gif
│ │ │ ├── jiqimao.jpg
│ │ │ └── lufei.jpg
│ │ ├── avatars
│ │ │ ├── girl.jpg
│ │ │ ├── lufei.jpg
│ ├── models.py
│ ├── myforms.py
│ ├── settings.py
│ ├── static
│ │ ├── bootstrap-3.3.7
│ │ │ ├── css
│ │ │ │ ├── bootstrap.css
│ │ │ ├── fonts
│ │ │ └── js
│ │ │ ├── bootstrap.js
│ │ │ ├── bootstrap.min.js
│ │ ├── css
│ │ │ ├── article_detail.css
│ │ │ ├── login.css
│ │ │ └── reg.css
│ │ ├── font
│ │ │ └── kumo.ttf
│ │ ├── img
│ │ │ ├── default.png
│ │ │ ├── downdown.gif
│ │ │ ├── icon_form.gif
│ │ │ └── upup.gif
│ │ ├── js
│ │ │ ├── article_detail.js
│ │ │ ├── jquery-3.2.1.js
│ │ │ ├── jquery-3.2.1.min.js
│ │ │ ├── login.js
│ │ │ └── reg.js
│ │ └── theme
│ │ ├── egon.css
│ │ └── yuan.css
│ ├── templatetags
│ │ ├── my_tags.py
│ ├── tests.py
│ ├── urls.py
│ ├── valid_img.py
│ └── views.py
├── cnblog
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── log
│ ├── cnblog_collect.log
│ ├── cnblog_err.log
│ ├── cnblog_info.log
├── manage.py
└── templates
├── add_article.html
├── article_detail.html
├── backend.html
├── base.html
├── homesite.html
├── index.html
├── login.html
├── menu.html
└── reg.html
账号:
egon egon1234 / yuan yuan1234 / alex alex1234