- Notifications
You must be signed in to change notification settings - Fork16
Python Django Web开发 入门到实践 搭建博客网站 Blog 视频地址:
able8/Django-Course
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Python Django Web开发 入门到实践 视频地址:https://space.bilibili.com/252028233/
- 自强学堂 Django教程
- Django 2 零基础 - 待办清单网站
- 千锋Django视频教程
- Django 官方文档
- python 数据结构与算法系列课程
- python 操作Mysql、Redis、MongoDB数据库
- Python Web 入坑指南
- Python 数据结构与算法 - 北大
- 算法图解 , 图解的形式很适合新手,示例使用的是 python。
- python 面试题
- Stack Overflow关于Python的部分
- Python Cookbook
- The Python Standard Library
- Python HOWTOs
- Python 3 Module of the Week
- Cpython 源码
- python-regex-cheatsheet
- python code examples
看视频整理要点笔记:
- Django_Course
- 其它学习资料
- 01.什么是Django
- 02.入门 Hello World
- 03.Django基本应用结构
- 04.使用模版显示内容
- 05.定制后台和修改模型
- 06.开始完整制作网站
- 07.构建个人博客网站
- 08.常用的模版标签和过滤器
- 09.模版嵌套
- 10.使用CSS美化页面
- 11.CSS框架协助前端布局
- 12.Bootstrap响应式布局
- 13.分页和shell命令行模式
- 14.优化分页展示
- 15.上下篇博客和按月分类
- 16.统计分类博客的数量
- 17.博客后台富文本编辑
- 18.博客阅读简单计数
- 19.博客阅读计数优化
- 20.阅读计数统计和显示
- 21.热门阅读博客排行及缓存提速
- 22.评论功能设计和用户登录
- 23.html表单提交评论
- 24.使用Django Form表单
- 25.表单富文本编辑和ajax异步提交评论
- 26.回复功能设计和树结构
- 27.获取评论数和细节处理
- 28.实现点赞功能, 看似简单,内容很多
- 29.完善点赞功能
- 30.导航栏添加用户操作
- 31.自定义用户模型
- 32.修改用户信息
- 33.发挥邮箱的作用
- 34.评论发送邮件通知
- 35.部署准备(一):Git
- 36.部署准备(二):MySQL
- 37.部署准备(三):服务器
- 38.用Apache+mod_wsgi部署
- 官网:https://www.djangoproject.com
- 文档:https://docs.djangoproject.com/en/2.0/
- The web framework for perfectionists with deadlines.
- 在截止日期内,完美主义者使用的Web框架。
- Django was invented to meet fast-moving newsroom deadlines, while satisfying the tough requirements of experienced Web developers.
- Django的发明是为了满足紧急新闻编辑部的最后期限,同时满足经验丰富的Web开发人员的苛刻要求。
- Django makes it easier to build better Web apps more quickly and with less code.
- Django让更快搭建好的Web应用变得更简单,并且代码更少。
- 开发快到离谱,免费开源,处理了许多Web开发繁琐的事,令使用者专注业务
- 令人放心的安全
- 可拓展性强
- https://www.djangoproject.com/download/
- 本项目基于 Python3.6+ 和 Django2.0
- 入门仪式:创建项目,输出Hello, world
- 创建项目命令:
django-admin startproject mysite - Django项目基本结构
mysite ├ mysite Pyhton 包 │ └ - _init__.py │ └ - settings.py 全局设置文件 │ └ - urls.py 全局路由控制 │ └ - wsgi.py 服务器使用的wsgi部署文件 └ manage.py 项目管理
- 响应请求
- 客户端 打开网址发送请求-》Urls 处理请求 -》Views 响应请求,返回内容
- 启动本地服务
python manage.py runserver - 执行数据库迁移,新建数据库
python manage.py migrate - 创建超级管理员用户
python manage.py createsuperuser - 管理员页面http://127.0.0.1:8000/admin/
- 创建Django App
python manage.py startapp article - 如果页面比较多,将相似的内容用模版来管理,数据抽象为模型Models
- 创建数据的模型models
fromdjango.dbimportmodels# Create your models here.classArticle(models.Model):title=models.CharField(max_length=30)content=models.TextField()
- 创建模型后,先需要生成数库迁移文件,再执行数据库迁移
- 首先要在
settings.py中,INSTALLED_APPS添加app name python manage.py makemigrationspython manage.py migrate
- 首先要在
# 生成的数据库迁移文件classMigration(migrations.Migration):initial=Truedependencies= [ ]operations= [migrations.CreateModel(name='Article',fields=[ ('id',models.AutoField(auto_created=True,primary_key=True,serialize=False,verbose_name='ID')), ('title',models.CharField(max_length=30)), ('content',models.TextField()), ], ), ]
- 将模型注册到后台管理页面
# admin.pyfrom .modelsimportArticle# Register your models here.admin.site.register(Article)
- 进入后台找到Article 管理,添加修改数据
- 设置语言和时区
settings.py
# LANGUAGE_CODE = 'en-us'LANGUAGE_CODE='zh-Hans'# TIME_ZONE = 'UTC'TIME_ZONE='Asia/Shanghai'
- 查看文章页面
- 如何通过一个处理方法获取文章唯一的标识
path('article/<int:article_id>', article_detail, name='article_detail'),<int:article_id>默认是字符串,添加int指定整型
- 模型的
objects是获取和操作模型的对象
from .modelsimportArticleArticle.objects.get(条件)# 根据条件获取数据Article.objects.all()# 获取所有数据Article.objects.filter(条件)# 根据条件过滤数据article=Article.objects.get(id=article_id)returnHttpResponse('<h2>文章标题:%s </h2><hr> 文章内容:%s'% (article.title,article.content))
- 获取不存在的文章,返回404页面
try:article=Article.objects.get(id=article_id)exceptArticle.DoesNotExist:raiseHttp404('not exit')
使用模版,前端页面和后端代码分离,降低耦合性
查看 django 源码,了解函数功能
- VS code 右键 速览定义 可以显示源码
- 找到安装路径
pip show django - 进入查看源码文件
简化,用render_to_response省略请求参数,用get_object_or_404代替异常处理
# article = Article.objects.get(id=article_id)article=get_object_or_404(Article,pk=article_id)context= {}context['article_obj']=article# return render(request, 'article_detail.html', context)returnrender(request,'article_detail.html',context)# 不需要request参数l
- 获取文章列表
- 用url模版代替硬编码,方便后续修改
<a href="/article/{{ article.pk }}"><a href="{% url 'article_detail' article.pk %}">
defarticle_list(request):articles=Article.objects.all()context= {}context['articles']=articlesreturnrender(request,'article_list.html',context)
- 路由管理,总urls包含app的urls,总分结构,便于维护
# 总路由urlpatterns= [path('admin/',admin.site.urls),path('',views.index),path('article/',include('article.urls'))]# app 路由urlpatterns= [# localhost:8000/article/path('',views.article_list,name='article_list'),path('<int:article_id>',views.article_detail,name='article_detail'),]
- 定制后台
- 设置模型显示
__str__ - 定制模型admin后台管理页面
- 设置模型显示
# 设置模型显示 models.pyclassArticle(models.Model):title=models.CharField(max_length=30)content=models.TextField()def__str__(self):return'<Article: %s>'%self.title# admin.py# Register your models here.@admin.register(Article)# 使用装饰器更方便醒目classArticleAdmin(admin.ModelAdmin):list_display= ('id','title','content')# ordering = ('-id', ) 倒序ordering= ('id', )#admin.site.register(Article, ArticleAdmin)
- 修改模型models,修改后台显示字段
- 每次修改模型需要更新数据库
python manage.py makemigrationspython manage.py migrate- 需要设置默认值
classArticle(models.Model):title=models.CharField(max_length=30)content=models.TextField()# created_time = models.DateTimeField(default=timezone.now)created_time=models.DateTimeField(auto_now_add=True)last_updated_time=models.DateTimeField(auto_now=True)author=models.ForeignKey(User,on_delete=models.CASCADE,default=1)is_deleted=models.BooleanField(default=False)readed_num=models.IntegerField(default=0)# admin.py 后台显示字段@admin.register(Article)classArticleAdmin(admin.ModelAdmin):list_display= ('id','title','author','is_deleted','created_time','last_updated_time','content')# ordering = ('-id', ) 倒序ordering= ('id', )# 使用,过滤删除的 views.pydefarticle_list(request):# articles = Article.objects.all()articles=Article.objects.filter(is_deleted=False)
- 数据库的几个概念:主键,外键,索引,唯一索引
- 主键
primary key,是能确定一条记录的唯一标识,如id - 外键
foreign key,外键用于与另一张表的关联,用于保持数据的一致性,表的外键是另一表的主键。 - 索引
index,为了提高查询排序的速度。 - 聚集索引,在索引页里直接存放数据,而非聚集索引在索引页里存放的是索引,这些索引指向专门的数据页的数据。
- 主键和外键是把多个表组织为一个有效的关系数据库的粘合剂。主键和外键的设计对物理数据库的性能和可用性都有着决定性的影响。
- 主键
- 想清楚为什么做网站,动力影响学习热情,原因决定最终结果
- 兴趣爱好
- 学习一门技术
- 工作需要,业务需求
- 创业项目需要
- 如何用Django开发网站
- 要做什么,设计网站原型
- 业务流程
- 功能模块
- 前端布局
- 后端模型
- 要做什么,设计网站原型
- 接下来的教程
- 目的
- 通过完整的开发过程学习Django
- 对一般的网站开发有全面的认识
- 途径
- 制作个人博客网站
- 目的
- 个人博客网站
- 项目管理
- IDE
- 本地虚拟开发环境
- 版本控制Git/Github
- 前端开发
- html+javascript+CSS
- jQuery
- Bootstrap
- ajax
- 后端开发
- 博客管理和展示
- 用户登录和注册
- 评论和回复
- 点赞
- 数据库和服务器
- MySQL
- Linux
- 网站部署
- 项目管理
网站的功能模块 即 Django App
- 博客
- 博文
- 博客分类
- 博客标签
- 评论
- 点赞
- 阅读
- 用户
- 博客
开启本地虚拟环境
- 隔开python项目的运行环境
- 避免多个项目之前python库的冲突
- 完整便捷导出python库的列表
pip install virtualenvvirtualevn mysit_env# 创建 虚拟环境activatedeactivatepip freeze> requirements.txt# 一键导出pip install -r requirements.txt# 一键安装
- 初步创建blog应用
- 博文 + 博客分类
- 为了好管理,约定一篇博客只属于一个分类
django-admin startproject mysitecd mysitepython manage.py startapp blogpython manage.py migratepython manage.py createsuperuser# 修改模型,先在INSTALLED_APPS中添加app namepython manage.py makemigrationspython manage.py migrate
- 创建Blog模型和注册admin后台模型管理页面
# models.py# Create your models here.classBlogType(models.Model):type_name=models.CharField(max_length=15)def__str__(self):returnself.type_nameclassBlog(models.Model):title=models.CharField(max_length=50)blog_type=models.ForeignKey(BlogType,on_delete=models.CASCADE)content=models.TextField()author=models.ForeignKey(User,on_delete=models.CASCADE)created_time=models.DateTimeField(auto_now_add=True)last_updated_time=models.DateTimeField(auto_now=True)def__str__(self):return'<Blog: %s>'%self.title# admin.pyfromdjango.contribimportadminfrom .modelsimportBlogType,Blog# Register your models here.@admin.register(BlogType)classBlogTypeAdmin(admin.ModelAdmin):list_display= ('id','type_name')@admin.register(Blog)classBlogAdmin(admin.ModelAdmin):list_display= ('id','title','blog_type','author','created_time','last_updated_time')ordering= ('id',)
继续搭建blog
- models
- admin
- views
- urls
- templates
常用的模版标签
- 循环 for
- 条件 if, ifequal, ifnoequal
- 链接 url
- 模版嵌套 block、extends、include
- 注释 {# #}
常用的过滤器
- 日期 data
- 字数截取 truncatechars truncatechars_html
- 长度 length
<p>一共有{{blogs|length}} 篇博客</p>context['blogs_count']=Blog.objects.all().count{{blogs_count}}
{% extends "base.html" %}引用基础模版<title>{% block title %}{% endblock title %}</title>块{% block content %}{% endblock content %}全局模版文件夹, 存放公共模版文件
- 在
manage.py目录创建文件夹templates,存放公共模版文件 - 设置能够找到目录
settings - TEMPLATES - DIRS os.path.join(BASE_DIR, 'templates'),- 将
base.html放到公共模版文件夹
- 在
模版文件设置建议,为了方便迁移和公有,放到project的templates文件夹
- 为了防止名字冲突,在templates新建app name的文件夹,防止混淆
- 修改views.py里的文件路径
页面设计
- 导航栏:xxx的网站 首页
- 主体内容
- 尾注
使用CSS 层叠样式表,修饰html
- 使用Chrome浏览器审查元素,方便调试修改css,改好了再复制样式
新建static文件夹,专门存放静态文件,css js 图片
- 在
manage.py目录创建文件夹static,存放静态文件 - 设置能够找到目录
settings - STATICFILES_DIRS os.path.join(BASE_DIR, 'static')- 引用
<link rel="stylesheet" href="/static/base.css"> - 或者 先
{% load staticfiles %} - 再
<link rel="stylesheet" href="{% static 'base.css' %}">
- 在
- 为什么使用CSS框架
- 让web开发更迅速、简单
- 使用现成的框架 省时又省力
- 如何选择CSS框架
- 易用性
- 兼容性
- 大小、效果和功能
- Bootstrap
- 文档齐全,使用简单
- 兼容较多浏览器,非轻量级
- 响应式布局、移动设备优先
- 组件齐全,扁平简洁
- 部署Bootstrap
- 打开官网http://www.Bootcss.com
- 下载链接 、引用、使用
mini是压缩过的体积小- 组件字体图标
- 布局容器
- 栅格系统
- html 自动补全技巧 div.nav li*2>2 按回车
- 添加博客列表和分类两栏,并根据屏幕自适应调整位置大小
- 栅格系统原理和代码
- 博客分类使用的面板代码
- 添加框架自带的图标
- Django静态文件命名空间
- 为了避免冲突问题
static/appname/xxx.css
- 为了避免冲突问题
通过讲解分页功能进一步夯实基础,包括shell命令行模式、模型操作、模版标签、分页器、GET请求。
为什么需要分页?
- 当博客文章数过多,全部加载过慢,就需要分页加载
shell 命令行模式快速学习实践,添加博客
python manage.py shell- for 执行新增博客代码
模型新增对象
pythonmanage.pyshellfromblog.modelsimportBlogblog=Blog()# 实例化blog.title='xxx'...blog.save()
- shell 命令行 模式 操作模型
>>>fromblog.modelsimportBlog>>>dir()['Blog','__builtins__']>>>Blog.objects.all()<QuerySet [<Blog:<Blog:第一篇博客>>,<Blog:<Blog:第二篇博客>>,<Blog:<Blog:第三篇博客>>,<Blog:<Blog:第四篇长内容>>]>>>>Blog.objects.count()4>>>blog=Blog()>>>dir()['Blog','__builtins__','blog']>>>blog.title='shell 下第1篇'>>>blog.content='xxxx'>>>fromblog.modelsimportBlogType>>>BlogType.objects.all()<QuerySet [<BlogType:随笔>,<BlogType:感悟>,<BlogType:其他>]>>>>BlogType.objects.all()[2]<BlogType:其他>>>>blog_type=BlogType.objects.all()[2]>>>blog.blog_type=blog_type>>>fromdjango.contrib.auth.modelsimportUser>>>User.objects.all()<QuerySet [<User:able>]>>>>user=User.objects.all()[0]>>>blog.author=user>>>blog.save()>>>Blog.objects.all()<QuerySet [<Blog:<Blog:第一篇博客>>,<Blog:<Blog:第二篇博客>>,<Blog:<Blog:第三篇博客>>,<Blog:<Blog:第四篇长内容>>,<Blog:<Blog:shell下第1篇>>]>>>>dir(blog)# 查看所有 属性和方法,方便稍后调用# 批量添加>>>foriinrange(1,31):...blog=Blog()...blog.title='for %s'%i...blog.content='xxxx:%s'%i...blog.blog_type=blog_type...blog.author=user...blog.save()...>>>Blog.objects.all().count()35
- 分页器实现分页
- 导入
from django.core.paginator import Paginator - 实例化
paginator = Paginator(object_list, each_page_count) - 具体页面
page1 = paginator.page(1)
- 导入
>>>fromdjango.core.paginatorimportPaginator>>>fromblog.modelsimportBlog>>>blogs=Blog.objects.all()>>>blogs.count()35>>>paginator=Paginator(blogs,10)<string>:1:UnorderedObjectListWarning:Paginationmayyieldinconsistentresultswithanunorderedobject_list:<class'blog.models.Blog'>QuerySet.# 需要给模型添加 排序方式classMeta:ordering= ['-created_time']# 然后数据迁移pythonmanage.pymakemigrationspythonmanage.pymigrate>>>paginator=Paginator(blogs,10)>>>paginator<django.core.paginator.Paginatorobjectat0x1021de550>>>>dir(paginator)>>paginator.count35>>>paginator.num_pages4>>>page1=paginator.page(1)>>>page1<Page1of4>>>>page1.object_list
- 优化分页显示,提升用户体验
- 不要显示太多页码选择,影响页面布局
- 高亮显示当前页码
- 页码栏,优化显示的页码范围,这部分看似简单,内容不少
current_page_num=page_of_blogs.number# 获取当前页码# 获取当前页的前后2页的页码范围page_range= [xforxinrange(current_page_num-2,current_page_num+3)ifxinpaginator.page_range ]# 加上省略号间隔页码ifpage_range[0]-1>=2:page_range.insert(0,'...')ifpaginator.num_pages-page_range[-1]>=2:page_range.append('...')# 加上首页和尾页ifpage_range[0]!=1:page_range.insert(0,1)ifpage_range[-1]!=paginator.num_pages:page_range.append(paginator.num_pages)
公用全局设置放在setting中,统一管理
- 引用
from django.conf import settings; settings.xxx
- 引用
- 对比当前博客,得到上一篇或下一篇
blog=get_object_or_404(Blog,pk=blog_pk)context['previous_blog']=Blog.objects.filter(created_time__gt=blog.created_time).last()context['next_blog']=Blog.objects.filter(created_time__lt=blog.created_time).first()context['blog']=blog
.objects.filter()筛选条件- 比较
__gt__gte__lt__lte - 包含
__contains - 开头是
__startswith - 结尾是
__endswith __in__range
- 比较
>>>fromblog.modelsimportBlog>>>Blog.objects.filter(title__contains='shell')<QuerySet [<Blog:<Blog:shell下第1篇>>]>>>>Blog.objects.filter(title__startswith='shell')<QuerySet [<Blog:<Blog:shell下第1篇>>]>>>>Blog.objects.filter(id__in=[1,2,3])<QuerySet [<Blog:<Blog:第一篇博客>>,<Blog:<Blog:第二篇博客>>,<Blog:<Blog:第三篇博客>>]>>>>Blog.objects.filter(id__range=(1,3))<QuerySet [<Blog:<Blog:第一篇博客>>,<Blog:<Blog:第二篇博客>>,<Blog:<Blog:第三篇博客>>]>
.objects.exclude()排出条件,和filter相反,都是得到查询QuerySet- 加入双下划线筛选,用于
- 字段查询类型
- 外键拓展,以博客分类为例
- 日期拓展,以按月份为例
- 支持链式重新,可以一直链接下去
- 按日期查询.objects.dates()
- "month" returns a list of all distinct year/month values for the field.
Blog.objects.dates('created_time', 'month', order='DESC')- asc 按升序排列, desc 按降序排列
- 获取博客分类的对应博客数量
- 方法一,附加一个数量属性
# 获取博客分类的对应博客数量blog_types=BlogType.objects.all()blog_types_list= []forblog_typeinblog_types:blog_type.blog_count=Blog.objects.filter(blog_type=blog_type).count()blog_types_list.append(blog_type)# context['blog_types'] = BlogType.objects.all()context['blog_types']=blog_types_list
- 方法二,使用annotate拓展查询字段,注释统计信息
fromdjango.db.modelsimportCountBlogType.objects.annotate(blog_count=Count('blog'))context['blog_types']=BlogType.objects.annotate(blog_count=Count('blog'))
- 获取日期归档对应的博客数量
blog_dates=Blog.objects.dates('created_time','month',order='DESC')blog_dates_dict= {}forblog_dateinblog_dates:blog_count=Blog.objects.filter(created_time__year=blog_date.year,created_time__month=blog_date.month).count()blog_dates_dict[blog_date]=blog_countcontext['blog_dates']=blog_dates_dict
- 使用html丰富页面
- 简单文本编辑 -》直接贴入html代码
{{ blog.content|safe }}# 安全的,可以识别html tab{{ blog.content|striptags|truncatechars:120 }}# 有时不用显示tag,过滤掉tag- 富文本编辑 -》 最终解析成html,富文本编辑器、markdown编辑器
- 使用django-ckeditor, 选择标准
- 具有基本的富文本编辑功能
- 有持续更新维护
- 可以查看源码
- 可以上传图片
- 安装django-ckeditor链接
pip install django-ckeditor- 注册应用
ckeditor - 配置models, 把字段改成RichTextField
- 执行数据库迁移,进后台编辑博客就可以看到
- 添加上传图片功能
pip install pillow- 注册应用
ckeditor_uploader - 配置setting, media路径
- 配置url
- 配置model,把字段改成RichTextUploadingField
# mediaMEDIA_URL='/media/'MEDIA_ROOT=os.path.join(BASE_DIR,'media')# 配置ckeditorCKEDITOR_UPLOAD_PATH='upload/'# urls.pyfromdjango.contribimportadminfromdjango.urlsimportpath,includefromdjango.confimportsettingsfromdjango.conf.urls.staticimportstatic# from blog.views import blog_listfrom .importviewsurlpatterns= [path('',views.home,name='home'),path('admin/',admin.site.urls),path('ckeditor',include('ckeditor_uploader.urls')),path('blog/',include('blog.urls')),]urlpatterns+=static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)# modelsfromckeditor_uploader.fieldsimportRichTextUploadingFieldcontent=RichTextUploadingField()
简单计数处理
- Blog模型添加数字字段记录
- 每次打开链接,记录+1
自定义计数规则, 怎样才算阅读一次
- 无视是否同一个人,每次打开都记录,会造成刷阅读量,刷新即可
- 若同一个人,间隔多久才算阅读1次
通过设置浏览器cookie计数,防止一人多次计数
# 如果浏览器中没有设置的cookie了,就计数ifnotrequest.COOKIES.get('blog_%s_readed'%blog_pk):blog.readed_num+=1blog.save()response=render(request,'blog/blog_detail.html',context)# response.set_cookie('blog_%s_readed' % blog_pk, 'true', max_age=60) # 60s 失效response.set_cookie('blog_%s_readed'%blog_pk,'true')# 默认退出浏览器失效returnresponse
- COOKIES 计数方法的缺点
- 后台编辑博客可能影响计数,而且计数的更新也会更新了博客的时间
- 功能单一,无法统计某一天的阅读量
- 添加新的计数模型,计数功能独立,减少对原博客的影响
- 计数字段 和 博客 通过 外键 关联
classReadNum(models.Model):read_num=models.IntegerField(default=0)blog=models.OneToOneField(Blog,on_delete=models.CASCADE)# 或者 blog = models.ForeignKey(Blog, on_delete=models.CASCADE)@admin.register(ReadNum)classReadNumAdmin(admin.ModelAdmin):list_display= ('read_num','blog')ifnotrequest.COOKIES.get('blog_%s_readed'%blog_pk):# blog.readed_num += 1# blog.save()ifReadNum.objects.filter(blog=blog):# 存在记录readnum=ReadNum.objects.get(blog=blog)else:# 不存在记录readnum=ReadNum(blog=blog)# 计数加1readnum.read_num+=1readnum.save()
创建专门用于计数的应用,独立出更加通用的计数功能,可以对任意模型计数
- 计数: 关联哪个模型 + 对应主键值
- ContentType
创建专门用于计数的应用
python manage.py startapp read_statistics
添加计数 models
fromdjango.dbimportmodelsfromdjango.contrib.contenttypes.fieldsimportGenericForeignKeyfromdjango.contrib.contenttypes.modelsimportContentType# Create your models here.classReadNum(models.Model):read_num=models.IntegerField(default=0)content_type=models.ForeignKey(ContentType,on_delete=models.CASCADE)object_id=models.PositiveIntegerField()content_object=GenericForeignKey('content_type','object_id')
- 注册应用
- 数据迁移
- 添加后台管理
fromdjango.contribimportadminfrom .modelsimportReadNum# Register your models here.@admin.register(ReadNum)classReadNumAdmin(admin.ModelAdmin):list_display= ('read_num','content_object')
- 在Blog模型中引用添加ReadNum
# shell 中实践 使用>>>fromread_statistics.modelsimportReadNum>>>fromblog.modelsimportBlog>>>fromdjango.contrib.contenttypes.modelsimportContentType>>>ContentType.objects.filter(model='blog')<QuerySet [<ContentType:blog>]>>>>ContentType.objects.get_for_model(Blog)<ContentType:blog>>>>ct=ContentType.objects.get_for_model(Blog)>>>blog=Blog.objects.first()>>>blog.pk1>>>ReadNum.objects.filter(content_type=ct,object_id=blog.pk)<QuerySet [<ReadNum:ReadNumobject (2)>]>>>>rn=ReadNum.objects.filter(content_type=ct,object_id=blog.pk)[0]>>>rn<ReadNum:ReadNumobject (2)>>>>rn.read_num11
- 最后,拆分优化,重新封装公共应用,抽出公用的方法
- 添加统计每天的阅读量
classReadDetail(models.Model):date=models.DateField(default=timezone.now)read_num=models.IntegerField(default=0)content_type=models.ForeignKey(ContentType,on_delete=models.CASCADE)object_id=models.PositiveIntegerField()content_object=GenericForeignKey('content_type','object_id')@admin.register(ReadDetail)classReadDetailAdmin(admin.ModelAdmin):list_display= ('date','read_num','content_object')# 每天阅读量 + 1date=timezone.now().date()ifReadDetail.objects.filter(content_type=ct,object_id=obj.pk,date=date).count():# 存在记录readDetail=ReadDetail.objects.get(content_type=ct,object_id=obj.pk,date=date)else:# 不存在记录readDetail=ReadDetail(content_type=ct,object_id=obj.pk,date=date)# 计数加1readDetail.read_num+=1readDetail.save()
- 简化代码,使用.objects.get_or_create
# 每天阅读量 + 1date=timezone.now().date()readDetail,created=ReadDetail.objects.get_or_create(content_type=ct,object_id=obj.pk,date=date)readDetail.read_num+=1readDetail.save()
- 统计最近7天的阅读量
# 统计最近7天阅读量defget_seven_days_read_data(content_type):today=timezone.now().date()read_nums= []foriinrange(6,-1,-1):date=today-datetime.timedelta(days=i)read_details=ReadDetail.objects.filter(content_type=content_type,date=date)result=read_details.aggregate(read_num_sum=Sum('read_num'))# 聚合read_nums.append(result['read_num_sum']or0)# 空则为0returnread_numsdefhome(request):blog_content_type=ContentType.objects.get_for_model(Blog)read_nums=get_seven_days_read_data(blog_content_type)context= {}context['read_nums']=read_numsreturnrender(request,'home.html',context)# shell 实践理解 Sum 和 aggregate>>>fromdjango.db.modelsimportSum>>>fromread_statistics.modelsimportReadDetail>>>rds=ReadDetail.objects.all()>>>rds.aggregate(read_num_sum=Sum('read_num'))# 返回结果的dict{'read_num_sum':8}
- 使用图表显示数据
- 后台 + 前端: 后台提供数据,前台使用数据
- 1 分钟上手 Highcharts
进一步使用阅读量的数据,得到热门博客并将其显示在首页。而获取热门数据可能计算需要一点时间(如果很复杂很多的话),使用服务器缓存保存数据,达到提速的效果
- 利用阅读量数据,得到热门博客并将其显示在首页
- 24小时内 今天数据统计
- 昨天数据统计
- 一周数据统计
- 一月数据统计
# 获取今日热门文章defget_today_hot_data(content_type):today=timezone.now().date()read_details=ReadDetail.objects.filter(content_type=content_type,date=today).order_by('-read_num')returnread_details[:7]# 取前7条# 获取昨天热门文章defget_yesterday_hot_data(content_type):today=timezone.now().date()yesterday=today-datetime.timedelta(days=1)read_details=ReadDetail.objects.filter(content_type=content_type,date=yesterday).order_by('-read_num')returnread_details[:7]# 获取7天热门文章defget_7_days_hot_data(content_type):today=timezone.now().date()date=today-datetime.timedelta(days=7)blogs=Blog.objects\ .filter(read_details__date__lt=today,read_details__date__gte=date)\ .values('id','title')\ .annotate(read_num_sum=Sum('read_details__read_num'))\ .order_by('-read_num_sum')returnblogs[:7]
# 数据分组 聚合 查询 实践 GenericRelationfromdjango.contrib.contenttypes.fieldsimportGenericRelationclassBlog(models.Model,ReadNumExpandMethod):read_details=GenericRelation(ReadDetail)>>>fromblog.modelsimportBlog>>>blog=Blog.objects.first()>>>blog<Blog:<Blog:第一篇博客随笔>>>>>blog.read_details.all()<QuerySet [<ReadDetail:ReadDetailobject (4)>]>>>>importdatetime>>>fromdjango.utilsimporttimezone>>>toda=timezone.now().date()>>>today=timezone.now().date()>>>date=today-datetime.timedelta(days=7)>>>Blog.objects.filter(read_details__date__lt=today,read_details__date__gte=date)<QuerySet [<Blog:<Blog:第一篇博客随笔>>,<Blog:<Blog:第2篇博客随笔>>]>>>>blogs=Blog.objects.filter(read_details__date__lt=today,read_details__date__gte=date)>>>blogs.values('id','title')<QuerySet [{'id':1,'title':'第一篇博客 随笔'}, {'id':2,'title':'第2篇博客 随笔'}]>>>>fromdjango.db.modelsimportSum>>>blogs.values('id','title').annotate(read_num_sum=Sum('read_details__read_num')).order_by('-read_num_sum')<QuerySet [{'id':2,'title':'第2篇博客 随笔','read_num_sum':20}, {'id':1,'title':'第一篇博客 随笔','read_num_sum':7}]>
每次计数统计数量,非常耗时
- 策略:缓存数据,不用每次都计算
- Django’s cache framework
- 内存缓存:Memcached, Redis
- 数据库缓存
- 文件缓存
数据库缓存
CACHES= {'default': {'BACKEND':'django.core.cache.backends.db.DatabaseCache','LOCATION':'my_cache_table',# 缓存表名 }}# Creating the cache tablepythonmanage.pycreatecachetable#Basic usage¶#The basic interface is set(key, value, timeout) and get(key):fromdjango.core.cacheimportcache>>>cache.set('my_key','hello, world!',30)>>>cache.get('my_key')'hello, world!'
- 获取7天热门博客的缓存数据
fromdjango.core.cacheimportcachehot_data_for_7_days=cache.get('hot_data_for_7_days')ifhot_data_for_7_daysisNone:hot_data_for_7_days=get_7_days_hot_data(blog_content_type)cache.set('hot_data_for_7_days',hot_data_for_7_days,20)print('calc')else:print('use cache')
主要设计评论模型、用户登录、简单form表单提交以及更正之前的render_to_response为render
实现评论功能的方式
- 第三方评论插件,如友言,多说,Disqus,网易更贴
- Django 评论库,django-comment
- 自己写代码实现
创建评论模型
- 评论对象
- 评论者
- 评论内容
- 评论时间
实现过程
- 创建应用
python manage.py startapp comment - 创建数据的模型models
- 注册后台管理页面
- 注册app
- 迁移数据库
- 创建应用
# modelsfromdjango.dbimportmodelsfromdjango.contrib.contenttypes.fieldsimportGenericForeignKeyfromdjango.contrib.contenttypes.modelsimportContentTypefromdjango.contrib.auth.modelsimportUserclassComment(models.Model):# 下面3行用来关联任意类型content_type=models.ForeignKey(ContentType,on_delete=models.CASCADE)object_id=models.PositiveIntegerField()content_object=GenericForeignKey('content_type','object_id')text=models.TextField()comment_time=models.DateTimeField(auto_now_add=True)user=models.ForeignKey(User,on_delete=models.CASCADE)# adminfromdjango.contribimportadminfrom .modelsimportComment@admin.register(Comment)classCommentAdmin(admin.ModelAdmin):list_display= ('content_object','text','comment_time','user')pythonmanage.pymakemigrationspythonmanage.pymigrate
评论需要用户登录
- 减少垃圾评论
- 但提高了评论门槛,可以使用第三方登录解决
- 还可以发送通知给用户
如何判断用户是否登录
context['user'] = request.user # 获取用户信息render(request, 'blog/blog_detail.html', context)- 因为需要使用request,需要用
render代替render_to_response - 因为模版settings中预先引用了auth,直接传request,模版中可以使用用户
{{ user }}
#If the current user has not logged in, this attribute will be set to an instance of AnonymousUser, otherwise it will be an instance of User.ifrequest.user.is_authenticated:# Do something for authenticated users. ...else:# Do something for anonymous users. ...#This example shows how you might use both authenticate() and login():fromdjango.contrib.authimportauthenticate,logindefmy_view(request):username=request.POST['username']password=request.POST['password']user=authenticate(request,username=username,password=password)ifuserisnotNone:login(request,user)# Redirect to a success page. ...else:# Return an 'invalid login' error message. ...
- 获取请求时网址,登录成功后返回原页面
fromdjango.urlsimportreverse# referer = request.META.get('HTTP_REFERER', '/') # 获取请求时网址,登录成功后返回referer=request.META.get('HTTP_REFERER',reverse('home'))#别名找到链接ifuserisnotNone:auth.login(request,user)returnredirect(referer)else:returnrender(request,'error.html', {'message':'用户名或密码错误'})
- 处理用户提交的评论
defupdate_comment(request):referer=request.META.get('HTTP_REFERER',reverse('home'))# 数据检查ifnotrequest.user.is_authenticated:returnrender(request,'error.html', {'message':'请先登录','redirect_to':referer})text=request.POST.get('text','').strip()# 多个空格也是空内容iftext=='':returnrender(request,'error.html', {'message':'评论内容不能为空','redirect_to':referer})try:content_type=request.POST.get('content_type','')object_id=int(request.POST.get('object_id',''))model_class=ContentType.objects.get(model=content_type).model_class()model_obj=model_class.objects.get(pk=object_id)exceptExceptionase:returnrender(request,'error.html', {'message':'评论对象不存在','redirect_to':referer})# 通过则保存数据comment=Comment()comment.user=usercomment.text=textcomment.content_object=model_objcomment.save()returnredirect(referer)# 提交后重定向到原页面
- 评论列表时间逆序显示, 最新的在最前面
- 修改完不需要数据迁移,直接生效
classComment(models.Model):# 下面3行用来关联任意类型content_type=models.ForeignKey(ContentType,on_delete=models.CASCADE)object_id=models.PositiveIntegerField()content_object=GenericForeignKey('content_type','object_id')text=models.TextField()comment_time=models.DateTimeField(auto_now_add=True)user=models.ForeignKey(User,on_delete=models.CASCADE)classMeta:ordering= ['-comment_time']# 时间逆序,最新的在最前面
- 获取评论列表
defblog_detail(request,blog_pk):blog=get_object_or_404(Blog,pk=blog_pk)read_cookie_key=read_statistics_one_read(request,blog)blog_content_type=ContentType.objects.get_for_model(blog)comments=Comment.objects.filter(content_type=blog_content_type,object_id=blog.pk)context= {}context['comments']=commentscontext['previous_blog']=Blog.objects.filter(created_time__gt=blog.created_time).last()context['next_blog']=Blog.objects.filter(created_time__lt=blog.created_time).first()context['blog']=blogresponse=render(request,'blog/blog_detail.html',context)response.set_cookie(read_cookie_key,'true')# 阅读cookie标记returnresponse
Django 用 Form 类描述 html 表单,简化操作,方便快速开发
- 接受和处理用户提交的数据
- 可以检查提交的数据,将数据类型转换成python的数据类型
- 可自动生成html代码
Django Form 的使用
- 创建 forms.py 文件
- 字段 就是 html input 标签
- 每个字段类型都有一个适当的默认Widget类
定制登录表单
# forms.pyfromdjangoimportformsfromdjango.contribimportauth# 定制登录表单classLoginForm(forms.Form):username=forms.CharField(label='用户名',required=True)# 默认为Truepassword=forms.CharField(label='密码',widget=forms.PasswordInput)# views.pydeflogin(request):ifrequest.method=='POST':login_form=LoginForm(request.POST)iflogin_form.is_valid():# 验证通过username=login_form.cleaned_data['username']password=login_form.cleaned_data['password']user=auth.authenticate(request,username=username,password=password)ifuserisnotNone:auth.login(request,user)returnredirect(request.GET.get('from',reverse('home')))# 没有就跳转首页else:login_form.add_error(None,'用户名或密码错误')# 添加错误提示else:# get 加载页面login_form=LoginForm()# 实例化表单context= {}context['login_form']=login_formreturnrender(request,'login.html',context)# login.html<formaction=""method="POST"> {%csrf_token%} {{login_form }}<inputtype="submit"value="登录"></form>
- 如何获取用户登录前的页面,方便登录后返回
# 登录时的页面,带着当时的路径未登录,登录后方可评论<ahref="{% url 'login' %}?from={{ request.get_full_path }}">登录</a># 拿到路径,如果没有就跳转首页redirect(request.GET.get('from',reverse('home')))# 没有就跳转首页
- 优化自定义表单,添加验证数据方法,让调用时的代码清晰明了
classLoginForm(forms.Form):username=forms.CharField(label='用户名',required=True)# 默认为Truepassword=forms.CharField(label='密码',widget=forms.PasswordInput)# 验证数据方法defclean(self):username=self.cleaned_data['username']password=self.cleaned_data['password']user=auth.authenticate(username=username,password=password)ifuserisNone:raiseforms.ValidationError('用户名或密码错误')elif:self.cleaned_data['user']=userreturnself.cleaned_data# 优化后的调用deflogin(request):ifrequest.method=='POST':login_form=LoginForm(request.POST)iflogin_form.is_valid():user=login_form.cleaned_data['user']auth.login(request,user)returnredirect(request.GET.get('from',reverse('home')))else:# get 加载页面login_form=LoginForm()# 实例化表单context= {}context['login_form']=login_formreturnrender(request,'login.html',context)
- Python内建的filter()函数用于过滤序列, 过滤出需要的属性或方法
- filter()接收一个函数和一个序列,把传入的函数依次作用于每个元素,然后根据返回值是True则保留
- filter()函数返回的是一个Iterator
>>>fromdjangoimportforms>>>filter(lambdax:'Input'inx,dir(forms))<filterobjectat0x10304ea58>>>>list(filter(lambdax:'Input'inx,dir(forms)))['CheckboxInput','ClearableFileInput','DateInput','DateTimeInput','EmailInput','FileInput','HiddenInput','MultipleHiddenInput','NumberInput','PasswordInput','TextInput','TimeInput','URLInput']>>>>>>filter(lambdax:'Field'inx,dir(forms))<filterobjectat0x10304e908>>>>list(filter(lambdax:'Field'inx,dir(forms)))['BooleanField','BoundField','CharField','ChoiceField','ComboField','DateField','DateTimeField','DecimalField','DurationField','EmailField','Field','FileField','FilePathField','FloatField','GenericIPAddressField','ImageField','IntegerField','ModelChoiceField','ModelMultipleChoiceField','MultiValueField','MultipleChoiceField','NullBooleanField','RegexField','SlugField','SplitDateTimeField','TimeField','TypedChoiceField','TypedMultipleChoiceField','URLField','UUIDField']
- 使用forms和bootstrap定制优化登录表单显示
# 定制登录表单显示classLoginForm(forms.Form):username=forms.CharField(label='用户名',required=True,# 默认为Truewidget=forms.TextInput(attrs={'class':'form-control','placeholder':'请输入用户名'}))# 设置渲染后的html的属性password=forms.CharField(label='密码',widget=forms.PasswordInput(attrs={'class':'form-control','placeholder':'请输入密码'}))
<divclass="containter"><divclass="row"><divclass="col-xs-4 col-xs-offset-4"><divclass="panel panel-default"><divclass="panel-heading"><h3class="panel-title">登录</h3></div><divclass="panel-body"><formaction=""method="POST">{%csrf_token%}{%comment%}{{ login_form}} 定制显示{%endcomment%}{%forfieldinlogin_form%}<labelfor="field.id_for_label">{{field.label}}</label>{{ field}}<pclass="text-danger">{{ field.errors.as_text}}</p>{%endfor%}<spanclass="pull-left text-danger">{{login_form.non_field_errors}}</span><inputtype="submit"value="登录"class="btn btn-primary pull-right"></form></div></div></div></div></div>
- 定制用户注册表单,并验证数据
classRegForm(forms.Form):username=forms.CharField(label='用户名',required=True,# 默认为Truemax_length=30,min_length=4,widget=forms.TextInput(attrs={'class':'form-control','placeholder':'请输入3-30位用户名'}))email=forms.EmailField(label='邮箱',widget=forms.TextInput(attrs={'class':'form-control','placeholder':'请输入邮箱'}))password=forms.CharField(label='密码',min_length=6,widget=forms.PasswordInput(attrs={'class':'form-control','placeholder':'请输入密码'}))password_again=forms.CharField(label='密码',min_length=6,widget=forms.PasswordInput(attrs={'class':'form-control','placeholder':'再输入一次密码'}))# 验证数据, 是否有效,是否存在defclean_username(self):username=self.cleaned_data['username']ifUser.objects.filter(username=username).exists():raiseforms.ValidationError('用户名已存在')returnusernamedefclean_email(self):email=self.cleaned_data['email']ifUser.objects.filter(email=email).exists():raiseforms.ValidationError('邮箱已存在')returnemaildefclean_password_again(self):password=self.cleaned_data['password']password_again=self.cleaned_data['password_again']ifpassword!=password_again:raiseforms.ValidationError('两次输入的密码不一致')returnpassword_again
- 优化前端 登录 或 注册 按钮
未登录,登录后方可评论<aclass="btn btn-primary"href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a><span>or</span><aclass="btn btn-danger"href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a>
- 注册用户 处理
defregister(request):ifrequest.method=='POST':reg_form=RegForm(request.POST)ifreg_form.is_valid():username=reg_form.cleaned_data['username']password=reg_form.cleaned_data['password']email=reg_form.cleaned_data['email']# 创建用户user=User.objects.create_user(username,email,password)user.save()# 或者''' user = User() user.username = username user.email = email user.set_password(password) user.save() '''# 登录用户user=auth.authenticate(username=username,password=password)auth.login(request,user)# 跳转注册之前的页面returnredirect(request.GET.get('from',reverse('home')))else:reg_form=RegForm()# 实例化表单context= {}context['reg_form']=reg_formreturnrender(request,'register.html',context)
django-ckeditor 富文本表单
- 每个字段类型都有一个适当的默认Widget类
- django-ckeditor 提供 widget
from ckeditor.widget import CKEditorWidget
将评论表单独立出来,放在评论应用里,定制评论表单类,添加验证表单逻辑
# comment/forms.pyfromdjangoimportformsfromdjango.contrib.contenttypes.modelsimportContentTypefromdjango.db.modelsimportObjectDoesNotExistclassCommentForm(forms.Form):content_type=forms.CharField(widget=forms.HiddenInput)object_id=forms.IntegerField(widget=forms.HiddenInput)text=forms.CharField(widget=forms.Textarea)def__init__(self,*args,**kwargs):if'user'inkwargs:self.user=kwargs.pop('user')# 接收用户信息, 并剔除,为了下一句不出错super(CommentForm,self).__init__(*args,**kwargs)# 验证数据defclean(self):# 判断用户是否登录ifself.user.is_authenticated:self.cleaned_data['user']=self.userelse:raiseforms.ValidationError('用户尚未登录')# 评论对象验证content_type=self.cleaned_data['content_type']object_id=self.cleaned_data['object_id']try:model_class=ContentType.objects.get(model=content_type).model_class()model_obj=model_class.objects.get(pk=object_id)self.cleaned_data['content_object']=model_objexceptObjectDoesNotExist:raiseforms.ValidationError('评论对象不存在')returnself.cleaned_data# 提交<formaction="{% url 'update_comment' %}"method="POST"style="overflow: hidden"> {%csrf_token%}<labelfor="comment_text">{{user.username }},欢迎评论~</label> {{comment_form }}<inputtype="submit"value="评论"class="btn btn-primary"style="float:right"></form># 处理逻辑defupdate_comment(request):referer=request.META.get('HTTP_REFERER',reverse('home'))comment_form=CommentForm(request.POST,user=request.user)# 实例化, 传递了用户信息,直接有表单类验证登录ifcomment_form.is_valid():# 通过则保存数据comment=Comment()comment.user=comment_form.cleaned_data['user']comment.text=comment_form.cleaned_data['text']comment.content_object=comment_form.cleaned_data['content_object']comment.save()returnredirect(referer)# 提交后重定向到原页面else:returnrender(request,'error.html', {'message':comment_form.errors,'redirect_to':referer})
- 添加并定制富文本评论表单
# forms.pyfromckeditor.widgetsimportCKEditorWidgetclassCommentForm(forms.Form):text=forms.CharField(widget=CKEditorWidget(config_name='comment_ckeditor'))# settings.py 添加设置即可,通过 comment_ckeditor# 配置ckeditor评论表单CKEDITOR_CONFIGS= {'comment_ckeditor': {'toolbar':'custom','toolbar_custom': [ ['Bold','Italic','Underline','Strike','Subscript','Superscript'], ['TextColor','BGColor','RemoveFormat'], ['NumberedList','BulletedList'], ['Link','Unlink'], ['Smiley','SpecialChar','Blockquote'], ],'width':'auto','height':'180','tabspace':4,'removePlugins':'elementspath','resize_enable':False, }}# 前端引入js<scripttype="text/javascript"src="{% static "ckeditor/ckeditor-init.js" %}"></script><scripttype="text/javascript"src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>div.django-ckeditor-widget {width:100%;}
ajax 异步提交数据方式:jQuery - AJAX 简介
- AJAX 是与服务器交换数据的艺术,它在不重载全部页面的情况下,实现了对部分网页的更新
- AJAX = 异步 JavaScript 和 XML(Asynchronous JavaScript and XML)
- 在不重载整个网页的情况下,AJAX 通过后台加载数据,并在网页上进行显示
- 使用 AJAX 的应用程序案例:谷歌地图、腾讯微博、优酷视频 等等
- 序列化表单值jQuery ajax - serialize() 方法
ajax请求
{#ajax异步提交,因为直接提交会刷新页面 #}{%blockscript_extends%}<scripttype="text/javascript"> $('#comment_form').submit(function(){// 判断评论内容是否为空 包括空的换行$("#comment_error").text('');if(CKEDITOR.instances['id_text'].document.getBody().getText().trim()==''){$("#comment_error").text('评论内容为空');returnfalse;} // 更新数据到textarea里面 CKEDITOR.instances['id_text'].updateElement(); // 异步提交 $.ajax({url:"{% url 'update_comment' %}",type:'POST',data:$(this).serialize(),// this 即 #comment_formcache:false,success:function(data){// 提交成功后调用的方法console.log(data);// 如果成功,就插入显示数据$("#no_comment").remove();if(data['status']=='SUCCESS'){varcomment_html='<div>'+data['username']+' ('+data['comment_time']+'): '+data['text']+'</div>';$("#comment_list").prepend(comment_html);// 清空评论区内容CKEDITOR.instances['id_text'].setData('');}else{// 显示错误信息$("#comment_error").text(data['message']);}},error:function(xhr){// 提交异常时调用的方法console.log(xhr);}}); return false;});</script>{%endblockscript_extends%}
- 处理ajax请求
defupdate_comment(request):# referer = request.META.get('HTTP_REFERER', reverse('home'))comment_form=CommentForm(request.POST,user=request.user)# 实例化, 传递了用户信息,直接有表单类验证登录data= {}ifcomment_form.is_valid():# 通过则保存数据comment=Comment()comment.user=comment_form.cleaned_data['user']comment.text=comment_form.cleaned_data['text']comment.content_object=comment_form.cleaned_data['content_object']comment.save()# 返回数据data['status']='SUCCESS'data['username']=comment.user.usernamedata['comment_time']=comment.comment_time.strftime('%Y-%m-%d %H:%M:%S')data['text']=comment.textelse:data['status']='ERROR'data['message']=list(comment_form.errors.values())[0][0]returnJsonResponse(data)
- 自定义表单错误提示信息
classCommentForm(forms.Form):text=forms.CharField(widget=CKEditorWidget(config_name='comment_ckeditor'),error_messages={'required':'评论内容为空'})
- 怎么把“暂无评论”的字样去掉(°∀°)
- 可以把“暂无评论”改成
<span>暂无评论</span> - 然后在ajax提交成功之后移除该节点 $("#no_comment").remove()
- 可以把“暂无评论”改成
完善评论模块,使用树结构的知识实现回复功能。主要包括树结构的知识和前端页面的代码。
如何设计回复功能
- 评论可以被回复
- 回复也可以被回复
评论模型设计, root, parent, reply_to, 没懂,稍后再整理类,这章有点难理解
- 评论为根 root,如果 parent is None 那就是根
- reply_to 回复谁
classComment(models.Model): ...text=models.TextField()comment_time=models.DateTimeField(auto_now_add=True)user=models.ForeignKey(User,related_name='comments',on_delete=models.CASCADE)root=models.ForeignKey('self',related_name='root_comment',null=True,on_delete=models.CASCADE)parent=models.ForeignKey('self',related_name='parent_comment',null=True,on_delete=models.CASCADE)reply_to=models.ForeignKey(User,related_name='replies',null=True,on_delete=models.CASCADE)
- related_name 代表的反向功能要怎么理解呢?
- 例如Comment有个uesr外键联系到User,这个是正向关系。而User是被联系,要从User得到相关Comment数据,这个是相对于那个外键关系来说是反过来,所以叫反向关系。若要从User获取Comment数据,是默认通过一个comment_set属性,模型名加“_set”。要更改这个属性名用related_name
# 评论表单classCommentForm(forms.Form):# 回复的哪条reply_comment_id=forms.IntegerField(widget=forms.HiddenInput(attrs={'id':'reply_comment_id'}))# 验证提交的数据defclean_reply_comment_id(self):reply_comment_id=self.cleaned_data['reply_comment_id']ifreply_comment_id<0:raiseforms.ValidationError('回复出错')elifreply_comment_id==0:self.cleaned_data['parent']=NoneelifComment.objects.filter(pk=reply_comment_id).exists():self.cleaned_data['parent']=Comment.objects.get(pk=reply_comment_id)else:raiseforms.ValidationError('回复出错')returnreply_comment_id
- 评论列表显示
<divclass="comment-area"><h3class="comment-area-title">评论列表</h3><divid="comment_list">{%forcommentincomments%}<divid="root_{{ comment.pk }}"class="comment"><span>{{ comment.user.username}}<span>({{ comment.comment_time|date:"Y-m-d H:i:s"}}):</span><divid="comment_{{ comment.pk }}">{{ comment.text|safe}}</div><ahref="#">回复</a>{%forreplyincomment.root_comment.all%}<divclass="reply"><span>{{ reply.user.username}}</span><span>({{ reply.comment_time|date:"Y-m-d H:i:s"}}):</span><span>回复</span><span>{{ reply.reply_to.username}}</span><divid="comment_{{ reply.pk }}">{{ reply.text|safe}}</div><ahref="#">回复</a></div>{%endfor%}</div>{%empty%}<spanid='no_comment'>暂无评论</span>{%endfor%}</div></div>
- js 判断提交的是评论还是回复
// 判断是 评论 还是 回复, 不同的插入位置的// 没指定#reply_comment_id 就是评论, 回复是指定回复某一条的if($('#reply_comment_id').val()=='0'){// 插入评论varcomment_html='<divpl-c1">+data['pk']+'"> \ <span>'+data['username']+'</span> \ <span>('+data['comment_time']+'):</span>\ <divpl-c1">+data['pk']+'">'+data['text']+'</div> \ <a href="#">回复</a></div>';$('#comment_list').prepend(comment_html);}else{// 插入回复varreply_html='<div><span>'+data['username']+'</span> \ <span>('+data['comment_time']+'):</span> \ <span> 回复 </span> \ <span>'+data['reply_to']+': </span> \ <divpl-c1">+data['pk']+'">'+data['text']+'</div> \ <a href="#">回复</a></div>';$('#root_'+data['root_pk']).append(reply_html);}
- js 点击回复后,屏幕滚动到评论表单, 并获得输入焦点
functionreply(reply_comment_id){// 设置值$('#reply_comment_id').val(reply_comment_id);// 显示回复的哪条varhtml=$('#comment_'+reply_comment_id).html();$('#reply_content').html(html);$('#reply_content_container').show();// 点击回复后,屏幕滚动到评论表单, 并获得焦点,$('html').animate({scrollTop:$('#comment_form').offset().top-60},300,function(){CKEDITOR.instances['id_text'].focus();});}
如何获取评论数
- 方法:filter筛选在用count方法计数
- 问题:在列表和详情页显示,会让代码变得很复杂
- 详情也有评论,是可以统计;列表页显示评论数就比较麻烦
用自定义模板标签获取评论数
- 实现评论功能独立,降低耦合性,代码独立,使用简单
- 在app内创建
templatetags包 - 创建py文件, 写方法,稍后会当标签使用, 注册方法后要重启应用
- 在模版中
load标签加载该文件,{% load comment_tags %}文件名去掉py - 模版中调用
{% get_comment_count blog %}注意标签是{% %}, 参数也没有引号 - 这样可以在详情和列表页轻松显示评论数,2句话,很简单
- Custom template tags and filters
# 创建包,和文件# Django_Course/mysite/comment/templatetags/comment_tags.py# vscode cmd +k p 复制当前文件的路径fromdjangoimporttemplatefromdjango.contrib.contenttypes.modelsimportContentTypefrom ..modelsimportCommentregister=template.Library()@register.simple_tagdefget_comment_count(obj):content_type=ContentType.objects.get_for_model(obj)# 根据具体对象获取contenttypereturnComment.objects.filter(content_type=content_type,object_id=obj.pk).count(){%loadcomment_tags%}评论({%get_comment_countblog%}
- 将views中的评论表单和评论列表分离到模版标签中,精简代码
@register.simple_tagdefget_comment_form(obj):content_type=ContentType.objects.get_for_model(obj)form=CommentForm(initial={'content_type':content_type,'object_id':obj.pk,'reply_comment_id':0 })returnform@register.simple_tagdefget_comment_list(obj):content_type=ContentType.objects.get_for_model(obj)comments=comments=Comment.objects.filter(content_type=content_type,object_id=obj.pk,parent=None)returncomments.order_by('-comment_time')# 引用{%get_comment_formblogascomment_form%}{%forfieldincomment_form%}{%get_comment_listblogascomments%}{%forcommentincomments%}
- 修复ajax获取的评论时间时区问题
//js 时间戳转当前时间,并格式化显示functionnumFormat(num){return('00'+num).substr(-2);}functiontimeFormat(timestamp){vardatetime=newDate(timestamp*1000);varyear=datetime.getFullYear();varmonth=numFormat(datetime.getMonth())+1;varday=numFormat(datetime.getDate());varhour=numFormat(datetime.getHours());varminute=numFormat(datetime.getMinutes());varsecond=numFormat(datetime.getSeconds());returnyear+'-'+month+'-'+day+' '+hour+':'+minute+':'+second}data['comment_time']=comment.comment_time.timestamp()
- 调整回复表单的CSS样式
div#reply_content_container {border:1px solid#d1d1d1;border-bottom: none;background-color:#f8f8f8;overflow: hidden;padding:1em1em0.5em;}p#reply_title {border-bottom:1px dashed#ccc;padding-bottom:0.5em;}
外键级联删除CASCADE,保证数据的完整性
user = models.ForeignKey(User, related_name='comments', on_delete=models.DO_NOTHING)User表是主,当删除用户后,DO_NOTHING还会保留用户的评论里的用户,造成数据不完整- 数据库会提示,
FOREIGN KEY constraint failed - 换成
on_delete=models.CASCADE, 删除用户后,包含在评论里的用户也删除
修复 django-ckeditor 报错
No configuration named 'default' found in your CKEDITOR_CONFIGS- settings中添加
'default': {},
点赞功能设计
- 博客和评论、回复都可以点赞
- 可以取消点赞
- 可看到点赞总数
- 用户登录后才可以点赞 (视频里这样设计的,如何改进不用用户登录呢?)
创建点赞
likesapppython manage.py startapp likes- 注册app
- 数据库迁移
- 设置请求的 总分 urls
# Django_Course/mysite/likes/models.pyfromdjango.dbimportmodelsfromdjango.contrib.contenttypes.fieldsimportGenericForeignKeyfromdjango.contrib.contenttypes.modelsimportContentTypefromdjango.contrib.auth.modelsimportUserclassLikeCount(models.Model):content_type=models.ForeignKey(ContentType,on_delete=models.CASCADE)object_id=models.PositiveIntegerField()content_object=GenericForeignKey('content_type','object_id')liked_num=models.IntegerField(default=0)classLikeRecord(models.Model):content_type=models.ForeignKey(ContentType,on_delete=models.CASCADE)object_id=models.PositiveIntegerField()content_object=GenericForeignKey('content_type','object_id')user=models.ForeignKey(User,on_delete=models.CASCADE)liked_time=models.DateTimeField(auto_now_add=True)# 总 urls 添加path('likes/',include('likes.urls')),# app urlspath('like_change',views.like_change,name='like_change')
- ajax 异步提交改变 点赞 请求
functionlikeChange(obj,content_type,object_id){varis_like=(obj.getElementsByClassName('active').length==0);console.log(is_like)// 异步提交$.ajax({url:"{% url 'like_change' %}",type:'GET',data:{content_type:content_type,object_id:object_id,is_like:is_like,},cache:false,success:function(data){console.log(data);if(data['status']=='SUCCESS'){// 更新点赞状态varelement=$(obj.getElementsByClassName('glyphicon'));if(is_like){element.addClass('active');}else{element.removeClass('active');}// 更新点赞数量varliked_num=$(obj.getElementsByClassName('liked-num'));liked_num.text(data['liked_num']);}else{alert(data['message']);}},error:function(xhr){console.log(xhr)}});}
- css 点赞样式
div.like {color:#337ab7;cursor: pointer;display: inline-block;padding:0.5em0.3em;}div.like .active{color: red;}
- views 处理点赞请求,验证各种状态
defErrorResponse(code,message):data= {}data['status']='ERROR'data['code']=codedata['message']=messagereturnJsonResponse(data)defSuccessResponse(liked_num):data= {}data['status']='SUCCESS'data['liked_num']=liked_numreturnJsonResponse(data)deflike_change(request):# 获取请求传递的数据# 获取用户,验证用户登录user=request.userifnotuser.is_authenticated:returnErrorResponse(400,'you were not login')content_type=request.GET.get('content_type')object_id=int(request.GET.get('object_id'))try:content_type=ContentType.objects.get(model=content_type)model_class=content_type.model_class()model_obj=model_class.objects.get(pk=object_id)exceptObjectDoesNotExist:returnErrorResponse(401,'object not exist')# 处理数据ifrequest.GET.get('is_like')=='true':# 要点赞print('hi')like_record,created=LikeRecord.objects.get_or_create(content_type=content_type,object_id=object_id,user=user)ifcreated:# 未点赞过,点赞数加1like_count,created=LikeCount.objects.get_or_create(content_type=content_type,object_id=object_id)like_count.liked_num+=1like_count.save()returnSuccessResponse(like_count.liked_num)else:# 已点赞过,不能重复点赞returnErrorResponse(402,'you were liked')else:# 取消点赞ifLikeRecord.objects.filter(content_type=content_type,object_id=object_id,user=user):# 有点赞,取消点赞like_record=LikeRecord.objects.get(content_type=content_type,object_id=object_id,user=user)like_record.delete()# 点赞总数 -1like_count,created=LikeCount.objects.get_or_create(content_type=content_type,object_id=object_id)ifnotcreated:like_count.liked_num-=1like_count.save()returnSuccessResponse(like_count.liked_num)else:returnErrorResponse(404,'data error')else:# 没点赞过,不能取消returnErrorResponse(403,'you were not liked')
- 设置模版标签, 方便模版引用,不在views中更加独立
# Django_Course/mysite/likes/templatetags/likes_tags.pyfromdjangoimporttemplatefromdjango.contrib.contenttypes.modelsimportContentTypefrom ..modelsimportLikeCount,LikeRecordregister=template.Library()@register.simple_tagdefget_like_count(obj):content_type=ContentType.objects.get_for_model(obj)like_count,created=LikeCount.objects.get_or_create(content_type=content_type,object_id=obj.pk)returnlike_count.liked_num@register.simple_tag(takes_context=True)# 使用模版里面的变量defget_like_status(context,obj):content_type=ContentType.objects.get_for_model(obj)user=context['user']ifnotuser.is_authenticated:return''ifLikeRecord.objects.filter(content_type=content_type,object_id=obj.pk,user=user).exists():return'active'else:return''@register.simple_tagdefget_content_type(obj):content_type=ContentType.objects.get_for_model(obj)returncontent_type.model
- 模版中引用模版标签
{%loadlikes_tags%}// 博客列表中引用点赞({% get_like_countblog%})// 添加点赞功能到评论列表<divclass="like"onclick="likeChange(this, '{% get_content_type comment %}', {{ comment.pk }})"><spanclass="glyphicon glyphicon-thumbs-up {% get_like_status comment %}"></span><spanclass="liked-num">{% get_like_countcomment%}</span></div>// 添加点赞功能回复列表<divclass="like"onclick="likeChange(this, '{% get_content_type reply %}', {{ reply.pk }})"><spanclass="glyphicon glyphicon-thumbs-up {% get_like_status reply %}"></span><spanclass="liked-num">{%get_like_countreply%}</span></div>
前后端开发建议
- 功能需求分析 -》模型设计 -》前端初步开发 -》后端实现 -》完善前端代码
模版标签
- the Jinja2 template engine was inspired by the Django template language
- therefore their syntax is quite similar!
- 表达式
{% ... %} is used for statements. - 变量
{{ ... }} is used for variables - 注释
{# ... #} is used for to comment
完善点赞功能,让新增的评论和回复可以点赞。
- 这里涉及到js字符串拼接的问题。
- 点赞时,未登录的情况下弹出一个模态框登录
新增评论和回复点赞
- 因为我们新增加的评论和回复没有添加onclick事件
- 解决js字符串拼接的问题
// 定义字符串格式化方法,解决字符串拼接麻烦问题// '{0}+{1}'.format('a', 'b') -> "a+b"String.prototype.format=function(){varstr=this;for(vari=0;i<arguments.length;i++){varstr=str.replace(newRegExp('\\{'+i+'\\}','g'),arguments[i])};returnstr;}// 异步提交$.ajax({url:"{% url 'update_comment' %}",type:'POST',data:$(this).serialize(),// this 即 #comment_formcache:false,success:function(data){// 提交成功后调用的方法, data是后端返回给前端的数据console.log(data);// 如果成功,就插入显示数if(data['status']=='SUCCESS'){// 判断是 评论 还是 回复, 不同的插入位置的if($('#reply_comment_id').val()=='0'){// 插入评论varcomment_html='<div>'+'<span>({2}):</span>'+'<div> {3} </div>'+'<div>'+'<span></span>'+'<span> 0 </span></div>'+'<a href="#">回复</a></div>';comment_html=comment_html.format(data['pk'],data['username'],timeFormat(data['comment_time']),data['text'],data['content_type'])$('#comment_list').prepend(comment_html);...
未登录时,弹出一个模态框登录
- 模态框
- 以弹出对话框的形式出现,具有最小和最实用的功能集。
登录框 代码, 引入之前的 login form
<!-- Modal --><divclass="modal fade"id="login_modal"tabindex="-1"role="dialog"><divclass="modal-dialog modal-sm"role="document"><divclass="modal-content"><formid="login_modal_form"action=""method="POST"><divclass="modal-header"><buttontype="button"class="close"data-dismiss="modal"aria-label="Close"><spanaria-hidden="true">×</span></button><h4class="modal-title"id="myModalLabel">登录</h4></div><divclass="modal-body">{%csrf_token%}{%forfieldinlogin_form%}<labelfor="{{ field.id_for_label }}">{{field.label}}</label>{{ field}}{%endfor%}<spanid="login_modal_tip"class="text-danger"></span></div><divclass="modal-footer"><buttontype="submit"class="btn btn-primary">登录</button><buttontype="button"class="btn btn-default"data-dismiss="modal">关闭</button></div></form></div></div></div>
添加路由url,用于弹出登录框提交请求
path('login_for_modal/', views.login_for_modal, name='login_for_modal'),
ajax 提交登录信息
// 但检查未登录时 显示登录框if(data['code']==400){$('#login_modal').modal('show');// 提交登录请求$('#login_modal_form').submit(function(eventt){event.preventDefault();// 阻止页面提交$.ajax({url:"{% url 'login_for_modal' %}",type:'POST',data:$(this).serialize(),cache:false,success:function(data){if(data['status']=='SUCCESS'){window.location.reload();// 刷新页面}else{$('#login_modal_tip').text('用户名或密码错误');}}});});
- 处理登录请求
deflogin_for_modal(request):login_form=LoginForm(request.POST)data= {}iflogin_form.is_valid():user=login_form.cleaned_data['user']auth.login(request,user)data['status']='SUCCESS'else:data['status']='ERROR'returnJsonResponse(data)
之前评论和点赞的时候,需要登录和登出,操作有点麻烦。所以在导航栏添加用户操作,并且将相关用户的处理方法集中变成一个django应用,为后面自定义用户模型准备
方便登录和退出
- 导航栏右侧添加“登录/注册”,用户个人信息,退出功能
- 下拉导航条
- User model django.contrib.auth
// 登录状态显示用户名,未登录状态显示登录和注册<ulclass="nav navbar-nav navbar-right">{%ifnotuser.is_authenticated%}<li><ahref="{% url 'login' %}?from={{ request.get_full_path }}">登录</a></li><li><ahref="{% url 'register' %}?from={{ request.get_full_path }}">注册</a></li>{%else%}<liclass="dropdown"><ahref="#"class="dropdown-toggle"data-toggle="dropdown"role="button">{{user.username}}<spanclass="caret"></span></a><ulclass="dropdown-menu"><li><ahref="{% url 'user_info' %}">个人资料</a></li><lirole="separator"class="divider"></li><li><ahref="{% url 'logout' %}?from={{ request.get_full_path }}">退出</a></li></ul></li>{%endif%}</ul>// 用户中心页面<divclass="containter"><divclass="row"><divclass="col-xs-10 col-xs-offset-1">{%ifuser.is_authenticated%}<h2>{{user.username}}</h2><ul><li>昵称:<ahref="#">修改昵称</a></li><li>邮箱:{%ifuser.email%}{{user.email}}{%else%} 未绑定<ahref="#">绑定邮箱</a>{%endif%}</li><li>上次登录的时间:{{ user.last_login|date:"Y-m-d H:i:s"}}</li><li><ahref="#">修改密码</a></li></ul>{%else%}<span>未登录,跳转到首页....</span><scripttype="text/javascript">window.location.href='/';</script>{%endif%}</div></div></div>
# 注册用户中心urlpath('user_info/',views.user_info,name='user_info'),# 处理退出和用户中心请求deflogout(request):auth.logout(request)returnredirect(request.GET.get('from',reverse('home')))defuser_info(request):context= {}returnrender(request,'user_info.html',context)
迁移,将user独立成app,放到一起
- 创建app可以用命令
python manage.py startapp appname - 也可以手动需要的文件,模拟命令, 再将文件分离出,放到app里面
- 创建app可以用命令
手动迁移user应用步骤
- 路由分离,总路由添加用户
path('user/', include('user.urls')), - user应用添加
urls.py, 统一处理用户相关的 url - user新建模版文件夹,将用户相关html放到里面的user目录,统一管理
- 修改views中加入
user/ - 因为模版文件中用到的url都是别名,所有迁移不影响
- 最后在settings中注册app
- 路由分离,总路由添加用户
将登录表单和弹出的登录框放到公共模版里,独立出来,方便调用
- user目录下新建
context_processors.py - settings 中的 TEMPLATES 添加
- 将之前views中引用的去掉,可以在模版中直接引用了
- 将登录弹框和ajax脚本分离独立出来,放到公共的
base.html文件中,随处可用
- user目录下新建
# Django_Course/mysite/user/context_processors.pyfrom .formsimportLoginFormdeflogin_modal_form(request):return {'login_modal_form':LoginForm()}TEMPLATES= [ {'BACKEND':'django.template.backends.django.DjangoTemplates','DIRS': [os.path.join(BASE_DIR,'templates'), ],'APP_DIRS':True,'OPTIONS': {'context_processors': [ ...'django.template.context_processors.request','user.context_processors.login_modal_form', ], }, },]
两种自定义用户模型的方式
- 继承Django的User类
- 优点是 自定义强,没有不必要的字段
- 缺点是 需要在项目开始时使用,配置admin麻烦
- Customizing authentication in Django
- 用新的Profile模型拓展关联的User
- 继承Django的User类
用新的Profile模型拓展关联的User
- 创建models
- 创建admin后台
- 迁移数据库生效
# Django_Course/mysite/user/models.pyfromdjango.dbimportmodelsfromdjango.contrib.auth.modelsimportUserclassProfile(models.Model):#一对一关系,一个用户一个资料, 重复会报错无法添加user=models.OneToOneField(User,on_delete=models.CASCADE)nickname=models.CharField(max_length=20)def__str__(self):return'<Profile: %s for %s>'% (self.nickname,self.user.username)# Django_Course/mysite/user/admin.pyfromdjango.contribimportadminfrom .modelsimportProfile@admin.register(Profile)classProfileAdmin(admin.ModelAdmin):list_display= ('user','nickname')
- 将profile模型信息添加到admin后台的用户信息页面
- To add a profile model’s fields to the user page in the admin, define an InlineModelAdmin (for this example, we’ll use a StackedInline) in your app’s admin.py and add it to a UserAdmin class which is registered with the User class
- 在用户列表显示昵称
# admin.py# Define an inline admin descriptor for Profile modelclassProfileInline(admin.StackedInline):model=Profilecan_delete=False# Define a new User adminclassUserAdmin(BaseUserAdmin):inlines= (ProfileInline,)list_display= ('username','nickname','email','is_staff','is_active','is_superuser')# 为了在用户列表显示昵称,需要加入一个自定义方法。上面就是调用user.nickname显示defnickname(self,obj):returnobj.profile.nicknamenickname.short_description='昵称'# 中文显示# Re-register UserAdminadmin.site.unregister(User)admin.site.register(User,UserAdmin)
用Profile模型拓展User方法的优缺点
- 优点是使用方便,不用删库重来,不影响整体架构
- 缺点是存在不必要的字段,对比继承的方法,查询速度会稍微慢一丁点
加入调整
后台管理链接
<ulclass="dropdown-menu"><li><ahref="{% url 'user_info' %}">个人资料</a></li><lirole="separator"class="divider"></li>{%ifuser.is_stafforuser.is_superuser%}<li><ahref="{% url 'admin:index' %}">后台管理</a></li> // admin是命名空间{%endif%}<li><ahref="{% url 'logout' %}?from={{ request.get_full_path }}">退出</a></li></ul>
- 优化 登录和主页 页面逻辑,如果是登录状态,就调整到首页
{%ifnotuser.is_authenticated%} ...注册或登录{%else%}<span>已登录,跳转到首页....</span><scripttype="text/javascript">window.location.href='/';</script>{%endif%}{%endif%}
修改用户信息,实现修改昵称、绑定邮箱(可发送邮件功能)
实现修改昵称
- 前端页面添加 修改昵称 链接
<a href="{% url 'change_nickname' %}">修改昵称</a> - urls 中添加 链接,和对应的处理方法
path('change_nickname/', views.change_nickname, name='change_nickname'), - views 中添加 渲染页面和修改昵称处理方法
- 渲染修改昵称表单,需要定义一个 修改昵称 的表单,
- 添加 form.html 用来 显示表单和提交信息
- 前端页面添加 修改昵称 链接
<formaction=""method="POST">{%csrf_token%}{%forfieldinform%}{%ifnotfield.is_hidden%}<labelfor="field.id_for_label">{{field.label}}</label>{%endif%}{{ field}}<pclass="text-danger">{{ field.errors.as_text}}</p>{%endfor%}<spanclass="pull-left text-danger">{{form.non_field_errors}}</span><divclass="pull-right"><inputtype="submit"value="{{ submit_text }}"class="btn btn-primary"><buttonclass="btn btn-default"onclick="{{ return_back_url }}">返回</button></div></form>
# form.py 定义表单和验证表单的方法classChangeNicknameForm(forms.Form):nickname_new=forms.CharField(label='新的昵称',max_length=20,widget=forms.TextInput(attrs={'class':'form-control','placeholder':'请输入新的昵称' }))# 下面2个函数用于判断用户是否登录def__init__(self,*args,**kwargs):if'user'inkwargs:self.user=kwargs.pop('user')# 接收用户信息, 并剔除,为了下一句不出错super(ChangeNicknameForm,self).__init__(*args,**kwargs)# 验证数据defclean(self):# 判断用户是否登录ifself.user.is_authenticated:self.cleaned_data['user']=self.userelse:raiseforms.ValidationError('用户尚未登录')returnself.cleaned_datadefclean_nickname_new(self):nickname_new=self.cleaned_data.get('nickname_new','').strip()ifnickname_new=='':raiseforms.ValidationError('新的昵称不能为空')returnnickname_new# views.py 处理defchange_nickname(request):redirect_to=request.GET.get('from',reverse('home'))ifrequest.method=='POST':form=ChangeNicknameForm(request.POST,user=request.user)ifform.is_valid():nickname_new=form.cleaned_data['nickname_new']profile,created=Profile.objects.get_or_create(user=request.user)profile.nickname=nickname_newprofile.save()returnredirect(redirect_to)else:form=ChangeNicknameForm()context= {}context['page_title']='修改昵称'context['form_title']='修改昵称'context['submit_text']='修改'context['form']=formcontext['return_back_url']=redirect_toreturnrender(request,'form.html',context)
- 如何判断显示用户名和昵称
- 给user类添加获取昵称的类方法,获取昵称,是否有昵称,获得昵称或用户名
# 使用类方法的动态绑定,User类绑定获取昵称的方法defget_nickname(self):ifProfile.objects.filter(user=self).exists():profile=Profile.objects.get(user=self)returnprofile.nicknameelse:return''defget_nickname_or_username(self):ifProfile.objects.filter(user=self).exists():profile=Profile.objects.get(user=self)returnprofile.nicknameelse:returnself.usernamedefhas_nickname(self):returnProfile.objects.filter(user=self).exists()User.get_nickname=get_nicknameUser.has_nickname=has_nicknameUser.get_nickname_or_username=get_nickname_or_username
// base.html<ahref="#"class="dropdown-toggle"data-toggle="dropdown"role="button">{%ifuser.has_nickname%}{{user.username}}({{user.get_nickname}}){%else%}{{user.username}}{%endif%}<spanclass="caret"></span></a>// blog_detail.html 评论<label>{{user.get_nickname_or_username}},欢迎评论~</label>// 还有 ajax 中,前面views返回的方法需要修改,得到昵称再直接返回昵称data['status']='SUCCESS'data['username']=comment.user.get_nickname_or_username()ifparentis notNone:data['reply_to']=comment.reply_to.get_nickname_or_username()
实现绑定邮箱功能
- 先思考绑定邮箱需要哪些字段,邮箱地址和验证码
- 设计绑定邮箱的表单
- views 中引入表单,添加处理方法
- 添加路由
- 前端页面添加链接
- Sending email 设置发件邮箱
- QQ 邮箱 开启设置 SMTP 服务。 改QQ密码后授权码会实效
- 表单验证信息
- ajax 发送验证码
- views 处理 绑定邮箱和发送验证码
邮箱设置
# 发送邮件设置# https://docs.djangoproject.com/en/2.0/ref/settings/#email# https://docs.djangoproject.com/en/2.0/topics/email/EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend'EMAIL_HOST='smtp.qq.com'EMAIL_PORT='25'EMAIL_HOST_USER='2@qq.com'EMAIL_HOST_PASSWORD='s'# 授权码EMAIL_SUBJECT_PREFIX='[able的博客]'EMAIL_USE_TLS=True# 与smtp服务器通信时,是否启动TLS链接 安全链接
- 绑定邮箱的表单,即各种表单验证
classBindEmailForm(forms.Form):email=forms.EmailField(label='邮箱',widget=forms.TextInput(attrs={'class':'form-control','placeholder':'请输入正确的邮箱' }))verification_code=forms.CharField(label='验证码',required=False,# 为了在不填的时候可以点击发送邮件widget=forms.TextInput(attrs={'class':'form-control','placeholder':'点击“发送验证码”发送到邮箱' }))# 下面2个函数用于判断用户是否登录def__init__(self,*args,**kwargs):if'request'inkwargs:self.request=kwargs.pop('request')# 接收传入的rquest信息, 并剔除,为了下一句不出错super(BindEmailForm,self).__init__(*args,**kwargs)# 验证数据defclean(self):# 判断用户是否登录ifself.request.user.is_authenticated:self.cleaned_data['user']=self.request.userelse:raiseforms.ValidationError('用户尚未登录')# 判断用户数会否已经绑定邮箱ifself.request.user.email!='':raiseforms.ValidationError('你已经绑定了邮箱')# 判断验证码code=self.request.session.get('bind_email_code','')verification_code=self.cleaned_data.get('verification_code','')ifnot (code!=''andcode==verification_code):raiseforms.ValidationError('验证码不正确')returnself.cleaned_datadefclean_email(self):email=self.cleaned_data['email']ifUser.objects.filter(email=email).exists():raiseforms.ValidationError('该邮箱已经被绑定')returnemaildefclean_verification_code(self):verification_code=self.cleaned_data.get('verification_code','').strip()ifverification_code=='':raiseforms.ValidationError('验证码不能为空')returnverification_code
- views 处理 绑定邮箱和发送验证码
# 路由path('bind_email/',views.bind_email,name='bind_email'),path('send_verification_code/',views.send_verification_code,name='send_verification_code'),defbind_email(request):redirect_to=request.GET.get('from',reverse('home'))ifrequest.method=='POST':form=BindEmailForm(request.POST,request=request)ifform.is_valid():email=form.cleaned_data['email']request.user.email=emailrequest.user.save()returnredirect(redirect_to)else:form=BindEmailForm()context= {}context['page_title']='绑定邮箱'context['form_title']='绑定邮箱'context['submit_text']='绑定'context['form']=formcontext['return_back_url']=redirect_toreturnrender(request,'user/bind_email.html',context)defsend_verification_code(request):email=request.GET.get('email','')data= {}ifemail!='':# 生成验证码code=''.join(random.sample(string.digits,6))now=int(time.time())# 秒数send_code_time=request.session.get('send_code_time',0)ifnow-send_code_time<60:data['status']='ERROR'else:# session 存储用户请求信息,默认有效期两周request.session['bind_email_code']=coderequest.session['send_code_time']=now# 发送邮箱send_mail('绑定邮箱','验证码: %s'%code,'2@qq.com', [email],fail_silently=False, )data['status']='SUCCESS'else:data['status']='ERROR'returnJsonResponse(data)
- 绑定邮箱前端页面和ajax发送验证码
{%extends"form.html"%}{% blockother_buttons%}<buttonid="send_code"class="btn btn-primary">发送验证码</button>{%endblockother_buttons%}{%blockscript_extends%}<scripttype="text/javascript"> $('#send_code').click(function(){varemail=$('#id_email').val();// 拿到用户填的邮箱的 值if(email==''){$('#tip').text('* 邮箱不能为空')returnfalse;} // ajax 异步发送验证码 $.ajax({url:"{% url 'send_verification_code' %}",type:'GET',data:{'email':email},cache:false,success:function(data){if(data['status']=='ERROR'){alert(data['status']);}}}); // 把按钮变灰 $(this).addClass('disabled'); $(this).attr('disabled', true); var time = 60; $(this).text(time + 's 后重新发送'); var interval = setInterval(() =>{if(time<=0){clearInterval(interval);$(this).removeClass('disabled');$(this).attr('disabled',false);$(this).text('发送验证码');returnfalse;} time --; $(this).text(time + 's 后重新发送');},1000);});</script>{%endblockscript_extends%}
邮箱作用
- 减少垃圾用户
- 保证账号安全
- 推送消息通知
引导用户填邮箱,可以从注册的时候要求填写邮箱
- 发送邮件,填验证码
- 发送验证邮件链接
- 直接使用邮箱注册
- 也可以不要去填邮箱,建议绑定后不可解绑
修改登录方式,用户名和邮箱都可以登录
- 修改登录表单的数据验证方式
# Django_Course/mysite/user/forms.pyclassLoginForm(forms.Form): ...defclean(self):username_or_email=self.cleaned_data['username_or_email']password=self.cleaned_data['password']user=auth.authenticate(username=username_or_email,password=password)ifuserisNone:ifUser.objects.filter(email=username_or_email).exists():username=User.objects.get(email=username_or_email).usernameuser=auth.authenticate(username=username,password=password)ifuserisnotNone:self.cleaned_data['user']=userreturnself.cleaned_dataraiseforms.ValidationError('用户名或密码错误')else:self.cleaned_data['user']=userreturnself.cleaned_data
修改密码
- 登录的情况下,直接验证旧密码来设置
- 未登录的情况下,忘记密码,发送验证码到邮箱
直接验证旧密码来设置新密码
- 添加修改密码表单
- 添加views处理逻辑
- 添加url
忘记密码,发送邮件验证,修改密码
fix bug 注意清除session中的验证码
- 利用邮件提高访问量
- 进一步发挥邮箱作用
- 一旦被评论(回复)了,发送邮件通知,让用户再次访问网站
# 添加获取邮箱和url的方法 Django_Course/mysite/blog/models.pyclassBlog(models.Model,ReadNumExpandMethod):# 继承 方法 ...author=models.ForeignKey(User,on_delete=models.CASCADE)defget_url(self):returnreverse('blog_detail',kwargs={'blog_pk':self.pk})defget_email(self):returnself.author.email# 发送邮件通知ifcomment.parentisNone:# 评论我的博客# 发送邮箱subject='有人评论你的博客'email=comment.content_object.get_email()else:# 回复评论subject='有人回复你的博客'email=comment.reply_to.emailifemail!='':text=comment.text+'\n'+comment.content_object.get_url()send_mail(subject,text,settings.EMAIL_HOST_USER, [email],fail_silently=False,)
- 异步发送邮件
- 因为发送邮件需要点时间,要稍微等一下才继续运行下面程序,所以需要异步发送
- 简单方案,多线程,简单、实现
- 复杂方案,Celery
- 可以防止任务过多
- 可定时执行一些任务
- 开销更大
# 多线程发送邮件# Django_Course/mysite/comment/models.pyclassSendMail(threading.Thread):def__init__(self,subject,text,email,fail_silently=False):self.subject=subjectself.text=textself.email=emailself.fail_silently=fail_silentlythreading.Thread.__init__(self)defrun(self):send_mail(self.subject,'',settings.EMAIL_HOST_USER, [self.email],fail_silently=self.fail_silently,html_message=self.text )...ifemail!='':context= {}context['comment_text']=self.textcontext['url']=self.content_object.get_url()text=render_to_string('comment/send_mail.html',context)send_comment_thread=SendMail(subject,text,email)send_comment_thread.start()
- html邮件模版
- 可以发送html邮件,
html_message字段 - 在commment应用下面新建
templates/comment/send_mail.html - 加上应用名,是为了方便应用的迁移,防止冲突
- 因为
self.text是表单字段,会含有<p>标签,所以模版里需要加safe过滤掉 - {{comment_text|safe}},这样传过去的没有html标签了
- html 让邮件更好看,但也容易为判为垃圾邮件
- 可以发送html邮件,
// Django_Course/mysite/comment/templates/comment/send_mail.html{{comment_text|safe}}<br><ahref="{{ url }}">点击查看</a>// 或者{%autoescapeoff%}{{comment_text| safe}}<br><ahref="{{ url }}">点击查看</a>{%autoescape%}
- 部署到互联网
- 服务器
- 域名
- 数据库,MySQL 开源 免费 好用
- 更新代码,版本控制 Git
Git 是一款开源的分布式版本控制系统
- 随着敲代码和修改代码,我们的代码会更新很多版本,不肯能复制好多份文件
- 就需要版本控制系统,管理代码版本
- 分布式 对比 集中式
- 快速控制服务器代码版本
- 有利于团队协作
Git 命令
- 未追踪Untracked -> tracked 工作区 working dir -> 暂存区 staging area -> 本地仓库localrepo
- HEAD指向的版本就是当前版本
- Git允许我们在版本的历史之间穿梭,使用命令
git reset commit_id - 穿梭前,用
git log可以查看提交历史,以便确定要回退到哪个版本 - 要重返未来,用
git reflog查看命令历史,以便确定要回到未来的哪个版本 git clean影响untracked的文件,git reset影响tracked的文件git clean命令用来从你的工作目录中删除所有没有tracked过的文件git reset只影响被track过的文件git clean -n是一次clean的演习, 告诉你哪些文件会被删除, 只是一个提醒git clean -df删除当前目录下没有被track过的 文件和文件夹git clean -f删除当前目录下所有没有track过的文件. 他不会删除.gitignore文件里面指定的文件夹和文件, 不管这些文件有没有被track过git clean -f <path>删除指定路径下的没有被track过的文件git clean -xf删除当前目录下所有没有track过的文件 和文件夹 . 不管他是否是.gitignore文件里面指定的文件夹和文件git reset commit_id只影响暂存区,将暂存修改的文件放到到工作区。 Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) and reports what has not been updated.git reset --soft commit_id无害,不丢失更改。重置版本指向,不影响 工作区和暂存区 文件更改 Does not touch the index file or the working tree at allgit reset --hard commit_id危险,会丢失更改。回退重置 工作区 和 暂存区,丢失tracked文件的更改! Resets the index and working tree. Any changes to tracked files in the working tree since commit are discarded.
MySQL 是一款框平台的开源的关系型数据库
- 为服务器端而设计,高并发访问
- SQlite 轻量级,可嵌入,不能高并发访问,适用桌面应用和手机应用
wheel whl 包,是编译好的包,可以直接安装,不会出编译错误
SQlite 迁移 MySQL, 先导出数据,再更改为 MySQL 数据库设置, 再导入数据
- 使用 Django 导出导入数据的命令完成迁移
python manage.py dumpdata > data.jsonpython manage.py loaddata data.json
默认字符集推荐 utf8mb4
myslq -u root -p# 修改密码alter user'root'@'localhost' identified by'pwd123456'# 创建数据库create database mysite_db default charset=utf8mb4 collate utf8mb4_general_ci;show databases;# 创建用户create user'able'@'localhost' identified by'pwd123456';# 添加权限,mysite_db 得所有表grant all privileges on mysite_db.* to'able'@'localhost';# 刷新权限flush privileges;myslq -u able -pshow databases;Django 数据库设置# Database# https://docs.djangoproject.com/en/2.0/ref/settings/#databasesDATABASES = {'default': {'ENGINE':'django.db.backends.mysql','NAME':'mysite_db','USER':'able','PASSWORD':'pwd123456','HOST':'127.0.0.1','PORT':'3306', }}django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module.Did you install mysqlclient?pip install mysqlclient然后 迁移数据库python manage.py migratepython manage.py createcachetablepython manage.py runserver时区问题,加载时区描述表 myslq_tzinfo_to_sql
- 实践迁移到 docker mysql
- 启动数据库,创建数据库,创建用户,添加权限
- 导出数据库,修改settings设置新数据库参数
- 迁移数据库,提示安装
pip install mysqlclient - 导入数据,
python manage.py runserver - 无法启动,
python manage.py createcachetable可以了 - 成功,数据都正常,没问题
Starting a MySQL instance is simple:# 加上端口 用localhost可以连接docker run --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysqldocker stop mysql-testdocker restart mysql-testdocker rm -f mysql-testdockerexec -it mysql-test bashdockerexec -it mysql-test mysql -uroot -p123456
- 出现问题及解决方法
- 无法连接数据库
(2002,"Can't connect to local MySQL server through socket '/tmp/mysql.sock'(2)")(1045,"Access denied for user 'able'@'172.17.0.1' (using password: YES)")CREATE USER'username'@'host' IDENTIFIED BY'password';说明:username:你将创建的用户名host:指定该用户在哪个主机上可以登陆,如果是本地用户可用localhost,如果想让该用户可以从任意远程主机登陆,可以使用通配符%CREATE USER'pig'@'%';删除用户 DROP USER'username'@'host';# 创建用户create user'able'@'%' identified by'pwd123456';# 添加权限,mysite_db 得所有表grant all privileges on mysite_db.* to'able'@'%';# 刷新权限flush privileges;mysql -u able -pshow databases;django.db.utils.ProgrammingError: (1146,"Table 'mysite_db.my_cache_table' doesn't exist")python manage.py createcachetable
- 服务器,流通消息,存储数据
- 服务器就是为我们提供服务的计算机
- 访问某个网站实际上是访问某个服务器给我们提供的信息
- web 服务器怎么提供服务
Linux 常见的web服务器软件
- Apache 模块多,功能强大
- Nginx(Engine-x) 轻量级,抗高并发,速度快
About
Python Django Web开发 入门到实践 搭建博客网站 Blog 视频地址:
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.




