Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Python Django Web开发 入门到实践 搭建博客网站 Blog 视频地址:

NotificationsYou must be signed in to change notification settings

able8/Django-Course

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Python Django Web开发 入门到实践 视频地址:https://space.bilibili.com/252028233/

其它学习资料

看视频整理要点笔记:

01.什么是Django

1. 什么是Django

  • 官网: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开发繁琐的事,令使用者专注业务
    • 令人放心的安全
    • 可拓展性强

2. Django版本选择

02.入门 Hello World

  • 入门仪式:创建项目,输出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/

03.Django基本应用结构

  • 创建Django Apppython 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 makemigrations
    • python 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'

04.使用模版显示内容

  • 查看文章页面
    • 如何通过一个处理方法获取文章唯一的标识
  • 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'),]

05.定制后台和修改模型

  • 定制后台
    • 设置模型显示__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 makemigrations
    • python 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,为了提高查询排序的速度。
    • 聚集索引,在索引页里直接存放数据,而非聚集索引在索引页里存放的是索引,这些索引指向专门的数据页的数据。
    • 主键和外键是把多个表组织为一个有效的关系数据库的粘合剂。主键和外键的设计对物理数据库的性能和可用性都有着决定性的影响。

06.开始完整制作网站

  • 想清楚为什么做网站,动力影响学习热情,原因决定最终结果
    • 兴趣爱好
    • 学习一门技术
    • 工作需要,业务需求
    • 创业项目需要
  • 如何用Django开发网站
    • 要做什么,设计网站原型
      • 业务流程
      • 功能模块
      • 前端布局
      • 后端模型
  • 接下来的教程
    • 目的
      1. 通过完整的开发过程学习Django
      2. 对一般的网站开发有全面的认识
    • 途径
      • 制作个人博客网站
  • 个人博客网站
    • 项目管理
      • IDE
      • 本地虚拟开发环境
      • 版本控制Git/Github
    • 前端开发
      • html+javascript+CSS
      • jQuery
      • Bootstrap
      • ajax
    • 后端开发
      • 博客管理和展示
      • 用户登录和注册
      • 评论和回复
      • 点赞
    • 数据库和服务器
      • MySQL
      • Linux
      • 网站部署

07.构建个人博客网站

  • 网站的功能模块 即 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',)

08.常用的模版标签和过滤器

  • 继续搭建blog

    • models
    • admin
    • views
    • urls
    • templates
  • 常用的模版标签

    • 循环 for
    • 条件 if, ifequal, ifnoequal
    • 链接 url
    • 模版嵌套 block、extends、include
    • 注释 {# #}
  • 常用的过滤器

    • 日期 data
    • 字数截取 truncatechars truncatechars_html
    • 长度 length
  • 参考:Django Built-in template tags and filters

<p>一共有{{blogs|length}} 篇博客</p>context['blogs_count']=Blog.objects.all().count{{blogs_count}}

09.模版嵌套

  • {% 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里的文件路径

10.使用CSS美化页面

  • 页面设计

    • 导航栏: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' %}">

11.CSS框架协助前端布局

  • 为什么使用CSS框架
    • 让web开发更迅速、简单
    • 使用现成的框架 省时又省力
  • 如何选择CSS框架
    • 易用性
    • 兼容性
    • 大小、效果和功能
  • Bootstrap
    • 文档齐全,使用简单
    • 兼容较多浏览器,非轻量级
    • 响应式布局、移动设备优先
    • 组件齐全,扁平简洁
  • 部署Bootstrap

12.Bootstrap响应式布局

13.分页和shell命令行模式

  • 通过讲解分页功能进一步夯实基础,包括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

14.优化分页展示

  • 优化分页显示,提升用户体验
    • 不要显示太多页码选择,影响页面布局
    • 高亮显示当前页码
  • 页码栏,优化显示的页码范围,这部分看似简单,内容不少
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
  • 分页组件

15.上下篇博客和按月分类

  • 对比当前博客,得到上一篇或下一篇
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 按降序排列

16.统计分类博客的数量

  • 获取博客分类的对应博客数量
  • 方法一,附加一个数量属性
# 获取博客分类的对应博客数量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

17.博客后台富文本编辑

  • 使用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()

18.博客阅读简单计数

  • 简单计数处理

    • 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 计数方法的缺点
    • 后台编辑博客可能影响计数,而且计数的更新也会更新了博客的时间
    • 功能单一,无法统计某一天的阅读量

19.博客阅读计数优化

方法一

  • 添加新的计数模型,计数功能独立,减少对原博客的影响
    • 计数字段 和 博客 通过 外键 关联
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
  • 最后,拆分优化,重新封装公共应用,抽出公用的方法

20.阅读计数统计和显示

  • 添加统计每天的阅读量
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()
# 每天阅读量 + 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}

21.热门阅读博客排行及缓存提速

进一步使用阅读量的数据,得到热门博客并将其显示在首页。而获取热门数据可能计算需要一点时间(如果很复杂很多的话),使用服务器缓存保存数据,达到提速的效果

  • 利用阅读量数据,得到热门博客并将其显示在首页
    • 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}]>
  • 每次计数统计数量,非常耗时

  • 数据库缓存

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')

22.评论功能设计和用户登录

  • 主要设计评论模型、用户登录、简单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 }}
  • Authentication in Web requests

#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.        ...

23.html表单提交评论

  • 获取请求时网址,登录成功后返回原页面
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

24.使用Django Form表单

  • 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']
# 定制登录表单显示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)

25.表单富文本编辑和ajax异步提交评论

  • 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()

26.回复功能设计和树结构

  • 完善评论模块,使用树结构的知识实现回复功能。主要包括树结构的知识和前端页面的代码。

  • 评论回复

  • 如何设计回复功能

    • 评论可以被回复
    • 回复也可以被回复
  • 评论模型设计, 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();});}

27.获取评论数和细节处理

  • 如何获取评论数

    • 方法: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': {},

28.实现点赞功能, 看似简单,内容很多

  • 点赞功能设计

    • 博客和评论、回复都可以点赞
    • 可以取消点赞
    • 可看到点赞总数
    • 用户登录后才可以点赞 (视频里这样设计的,如何改进不用用户登录呢?)
  • 创建点赞likes app

    • python 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

29.完善点赞功能

  • 完善点赞功能,让新增的评论和回复可以点赞。

    • 这里涉及到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">&times;</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)

30.导航栏添加用户操作

  • 之前评论和点赞的时候,需要登录和登出,操作有点麻烦。所以在导航栏添加用户操作,并且将相关用户的处理方法集中变成一个django应用,为后面自定义用户模型准备

  • 方便登录和退出

// 登录状态显示用户名,未登录状态显示登录和注册<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里面
  • 手动迁移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文件中,随处可用
# 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',            ],        },    },]

31.自定义用户模型

  • 两种自定义用户模型的方式

    • 继承Django的User类
    • 用新的Profile模型拓展关联的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%}

32.修改用户信息

  • 修改用户信息,实现修改昵称、绑定邮箱(可发送邮件功能)

  • 实现修改昵称

    • 前端页面添加 修改昵称 链接<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%}

33.发挥邮箱的作用

  • 邮箱作用

    • 减少垃圾用户
    • 保证账号安全
    • 推送消息通知
  • 引导用户填邮箱,可以从注册的时候要求填写邮箱

    • 发送邮件,填验证码
    • 发送验证邮件链接
    • 直接使用邮箱注册
    • 也可以不要去填邮箱,建议绑定后不可解绑
  • 修改登录方式,用户名和邮箱都可以登录

    • 修改登录表单的数据验证方式
# 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中的验证码

34.评论发送邮件通知

  • 利用邮件提高访问量
    • 进一步发挥邮箱作用
    • 一旦被评论(回复)了,发送邮件通知,让用户再次访问网站
# 添加获取邮箱和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 让邮件更好看,但也容易为判为垃圾邮件
// 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

35.部署准备(一):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 all
    • git reset --hard commit_id 危险,会丢失更改。回退重置 工作区 和 暂存区,丢失tracked文件的更改! Resets the index and working tree. Any changes to tracked files in the working tree since commit are discarded.

36.部署准备(二):MySQL

  • MySQL 是一款框平台的开源的关系型数据库

    • 为服务器端而设计,高并发访问
    • SQlite 轻量级,可嵌入,不能高并发访问,适用桌面应用和手机应用
  • wheel whl 包,是编译好的包,可以直接安装,不会出编译错误

  • SQlite 迁移 MySQL, 先导出数据,再更改为 MySQL 数据库设置, 再导入数据

    • 使用 Django 导出导入数据的命令完成迁移
    • python manage.py dumpdata > data.json
    • python 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
    1. 启动数据库,创建数据库,创建用户,添加权限
    2. 导出数据库,修改settings设置新数据库参数
    3. 迁移数据库,提示安装pip install mysqlclient
    4. 导入数据,python manage.py runserver
    5. 无法启动,python manage.py createcachetable 可以了
    6. 成功,数据都正常,没问题
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

37.部署准备(三):服务器

  • 服务器,流通消息,存储数据
    • 服务器就是为我们提供服务的计算机
    • 访问某个网站实际上是访问某个服务器给我们提供的信息

38.用Apache+mod_wsgi部署

  • web 服务器怎么提供服务

web 服务器怎么提供服务

  • Linux 常见的web服务器软件

    • Apache 模块多,功能强大
    • Nginx(Engine-x) 轻量级,抗高并发,速度快
  • Deployment checklist¶

About

Python Django Web开发 入门到实践 搭建博客网站 Blog 视频地址:

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp