Python Django Web开发 入门到实践 视频地址: https://space.bilibili.com/252028233/
看视频整理要点笔记:
django-admin startproject mysite
mysite
├ mysite Pyhton 包
│ └ - _init__.py
│ └ - settings.py 全局设置文件
│ └ - urls.py 全局路由控制
│ └ - wsgi.py 服务器使用的wsgi部署文件
└ manage.py 项目管理
python manage.py runserver
python manage.py migrate
python manage.py createsuperuser
python manage.py startapp article
from django.db import models
# Create your models here.
class Article(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
# 生成的数据库迁移文件
class Migration(migrations.Migration):
initial = True
dependencies = [
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.py
from .models import Article
# Register your models here.
admin.site.register(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 .models import Article
Article.objects.get(条件) # 根据条件获取数据
Article.objects.all() # 获取所有数据
Article.objects.filter(条件) # 根据条件过滤数据
article = Article.objects.get(id=article_id)
return HttpResponse('<h2>文章标题:%s </h2><hr> 文章内容:%s' % (article.title, article.content))
try:
article = Article.objects.get(id=article_id)
except Article.DoesNotExist:
raise Http404('not exit')
使用模版,前端页面和后端代码分离,降低耦合性
查看 django 源码,了解函数功能
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)
return render(request, 'article_detail.html', context) # 不需要request参数l
<a href="/article/{{ article.pk }}">
<a href="{% url 'article_detail' article.pk %}">
def article_list(request):
articles = Article.objects.all()
context = {}
context['articles'] = articles
return render(request, 'article_list.html', context)
# 总路由
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__
# 设置模型显示 models.py
class Article(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) # 使用装饰器更方便醒目
class ArticleAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'content')
# ordering = ('-id', ) 倒序
ordering = ('id', )
#admin.site.register(Article, ArticleAdmin)
python manage.py makemigrations
python manage.py migrate
class Article(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)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'author','is_deleted', 'created_time', 'last_updated_time', 'content')
# ordering = ('-id', ) 倒序
ordering = ('id', )
# 使用,过滤删除的 views.py
def article_list(request):
# articles = Article.objects.all()
articles = Article.objects.filter(is_deleted=False)
primary key
,是能确定一条记录的唯一标识,如idforeign key
,外键用于与另一张表的关联,用于保持数据的一致性,表的外键是另一表的主键。index
,为了提高查询排序的速度。# models.py
# Create your models here.
class BlogType(models.Model):
type_name = models.CharField(max_length=15)
def __str__(self):
return self.type_name
class Blog(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.py
from django.contrib import admin
from .models import BlogType, Blog
# Register your models here.
@admin.register(BlogType)
class BlogTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'type_name')
@admin.register(Blog)
class BlogAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'blog_type', 'author', 'created_time', 'last_updated_time')
ordering = ('id',)
继续搭建blog
常用的模版标签
常用的过滤器
参考:Django Built-in template tags and filters
<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文件夹
使用CSS 层叠样式表,修饰html
新建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' %}">
mini
是压缩过的体积小static/appname/xxx.css
通过讲解分页功能进一步夯实基础,包括shell命令行模式、模型操作、模版标签、分页器、GET请求。
为什么需要分页?
shell 命令行模式快速学习实践,添加博客
python manage.py shell
模型新增对象
python manage.py shell
from blog.models import Blog
blog = Blog() # 实例化
blog.title = 'xxx'
blog.save()
>>> from blog.models import Blog
>>> dir()
['Blog', '__builtins__']
>>> Blog.objects.all()
<QuerySet [<Blog: <Blog: 第一篇博客>>, <Blog: <Blog: 第二篇博客>>, <Blog: <Blog: 第三篇博客>>, <Blog: <Blog: 第四篇长内容>>]>
>>> Blog.objects.count()
>>> blog = Blog()
>>> dir()
['Blog', '__builtins__', 'blog']
>>> blog.title = 'shell 下第1篇'
>>> blog.content = 'xxxx'
>>> from blog.models import BlogType
>>> BlogType.objects.all()
<QuerySet [<BlogType: 随笔>, <BlogType: 感悟>, <BlogType: 其他>]>
>>> BlogType.objects.all()[2]
<BlogType: 其他>
>>> blog_type = BlogType.objects.all()[2]
>>> blog.blog_type = blog_type
>>> from django.contrib.auth.models import User
>>> 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) # 查看所有 属性和方法,方便稍后调用
# 批量添加
>>> for i in range(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()
from django.core.paginator import Paginator
paginator = Paginator(object_list, each_page_count)
page1 = paginator.page(1)
>>> from django.core.paginator import Paginator
>>> from blog.models import Blog
>>> blogs = Blog.objects.all()
>>> blogs.count()
>>> paginator = Paginator(blogs, 10)
<string>:1: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'blog.models.Blog'> QuerySet.
# 需要给模型添加 排序方式
class Meta:
ordering = ['-created_time']
# 然后数据迁移
python manage.py makemigrations
python manage.py migrate
>>> paginator = Paginator(blogs, 10)
>>> paginator
<django.core.paginator.Paginator object at 0x1021de550>
>>> dir(paginator)
>> paginator.count
>>> paginator.num_pages
>>> page1 = paginator.page(1)
>>> page1
<Page 1 of 4>
>>> page1.object_list
current_page_num = page_of_blogs.number # 获取当前页码
# 获取当前页的前后2页的页码范围
page_range = [x for x in range(current_page_num - 2, current_page_num + 3) if x in paginator.page_range ]
# 加上省略号间隔页码
if page_range[0] - 1 >= 2:
page_range.insert(0, '...')
if paginator.num_pages - page_range[-1] >= 2:
page_range.append('...')
# 加上首页和尾页
if page_range[0] != 1:
page_range.insert(0, 1)
if page_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
>>> from blog.models import Blog
>>> 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相反,都是得到查询QuerySetBlog.objects.dates('created_time', 'month', order='DESC')
# 获取博客分类的对应博客数量
blog_types = BlogType.objects.all()
blog_types_list = []
for blog_type in blog_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
from django.db.models import Count
BlogType.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 = {}
for blog_date in blog_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_count
context['blog_dates'] = blog_dates_dict
{{ blog.content|safe }} # 安全的,可以识别html tab
{{ blog.content|striptags|truncatechars:120 }} # 有时不用显示tag,过滤掉tag
pip install django-ckeditor
ckeditor
pip install pillow
ckeditor_uploader
# media
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# 配置ckeditor
CKEDITOR_UPLOAD_PATH = 'upload/'
# urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
# from blog.views import blog_list
from . import views
urlpatterns = [
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)
# models
from ckeditor_uploader.fields import RichTextUploadingField
content = RichTextUploadingField()
简单计数处理
自定义计数规则, 怎样才算阅读一次
通过设置浏览器cookie计数,防止一人多次计数
# 如果浏览器中没有设置的cookie了,就计数
if not request.COOKIES.get('blog_%s_readed' % blog_pk):
blog.readed_num += 1
blog.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') # 默认退出浏览器失效
return response
class ReadNum(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)
class ReadNumAdmin(admin.ModelAdmin):
list_display = ('read_num', 'blog')
if not request.COOKIES.get('blog_%s_readed' % blog_pk):
# blog.readed_num += 1
# blog.save()
if ReadNum.objects.filter(blog=blog):
# 存在记录
readnum = ReadNum.objects.get(blog=blog)
else:
# 不存在记录
readnum = ReadNum(blog=blog)
# 计数加1
readnum.read_num += 1
readnum.save()
创建专门用于计数的应用,独立出更加通用的计数功能,可以对任意模型计数
创建专门用于计数的应用
python manage.py startapp read_statistics
添加计数 models
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
# Create your models here.
class ReadNum(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')
from django.contrib import admin
from .models import ReadNum
# Register your models here.
@admin.register(ReadNum)
class ReadNumAdmin(admin.ModelAdmin):
list_display = ('read_num', 'content_object')
# shell 中实践 使用
>>> from read_statistics.models import ReadNum
>>> from blog.models import Blog
>>> from django.contrib.contenttypes.models import ContentType
>>> 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.pk
>>> ReadNum.objects.filter(content_type=ct, object_id=blog.pk)
<QuerySet [<ReadNum: ReadNum object (2)>]>
>>> rn = ReadNum.objects.filter(content_type=ct, object_id=blog.pk)[0]
<ReadNum: ReadNum object (2)>
>>> rn.read_num
class ReadDetail(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)
class ReadDetailAdmin(admin.ModelAdmin):
list_display = ('date', 'read_num', 'content_object')
# 每天阅读量 + 1
date = timezone.now().date()
if ReadDetail.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)
# 计数加1
readDetail.read_num += 1
readDetail.save()
# 每天阅读量 + 1
date = timezone.now().date()
readDetail, created = ReadDetail.objects.get_or_create(content_type=ct, object_id=obj.pk, date=date)
readDetail.read_num += 1
readDetail.save()
# 统计最近7天阅读量
def get_seven_days_read_data(content_type):
today = timezone.now().date()
read_nums = []
for i in range(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'] or 0) # 空则为0
return read_nums
def home(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_nums
return render(request, 'home.html', context)
# shell 实践理解 Sum 和 aggregate
>>> from django.db.models import Sum
>>> from read_statistics.models import ReadDetail
>>> rds = ReadDetail.objects.all()
>>> rds.aggregate(read_num_sum=Sum('read_num')) # 返回结果的dict
{'read_num_sum': 8}
进一步使用阅读量的数据,得到热门博客并将其显示在首页。而获取热门数据可能计算需要一点时间(如果很复杂很多的话),使用服务器缓存保存数据,达到提速的效果
# 获取今日热门文章
def get_today_hot_data(content_type):
today = timezone.now().date()
read_details = ReadDetail.objects.filter(
content_type=content_type, date=today).order_by('-read_num')
return read_details[:7] # 取前7条
# 获取昨天热门文章
def get_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')
return read_details[:7]
# 获取7天热门文章
def get_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')
return blogs[:7]
# 数据分组 聚合 查询 实践 GenericRelation
from django.contrib.contenttypes.fields import GenericRelation
class Blog(models.Model, ReadNumExpandMethod):
read_details = GenericRelation(ReadDetail)
>>> from blog.models import Blog
>>> blog = Blog.objects.first()
<Blog: <Blog: 第一篇博客 随笔>>
>>> blog.read_details.all()
<QuerySet [<ReadDetail: ReadDetail object (4)>]>
>>> import datetime
>>> from django.utils import timezone
>>> 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篇博客 随笔'}]>
>>> from django.db.models import Sum
>>> 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 table
python manage.py createcachetable
#Basic usage¶
#The basic interface is set(key, value, timeout) and get(key):
from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'
from django.core.cache import cache
hot_data_for_7_days = cache.get('hot_data_for_7_days')
if hot_data_for_7_days is None:
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
实现评论功能的方式
创建评论模型
python manage.py startapp comment
# models
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
class Comment(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)
# admin
from django.contrib import admin
from .models import Comment
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('content_object', 'text', 'comment_time', 'user')
python manage.py makemigrations
python manage.py migrate
评论需要用户登录
如何判断用户是否登录
context['user'] = request.user # 获取用户信息
render(request, 'blog/blog_detail.html', context)
render
代替 render_to_response
{{ 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.
if request.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():
from django.contrib.auth import authenticate, login
def my_view(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
else:
# Return an 'invalid login' error message.
from django.urls import reverse
# referer = request.META.get('HTTP_REFERER', '/') # 获取请求时网址,登录成功后返回
referer = request.META.get('HTTP_REFERER', reverse('home')) #别名找到链接
if user is not None:
auth.login(request, user)
return redirect(referer)
else:
return render(request, 'error.html', {'message': '用户名或密码错误'})
def update_comment(request):
referer = request.META.get('HTTP_REFERER', reverse('home'))
# 数据检查
if not request.user.is_authenticated:
return render(request, 'error.html', {'message': '请先登录', 'redirect_to': referer})
text = request.POST.get('text', '').strip() # 多个空格也是空内容
if text == '':
return render(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)
except Exception as e:
return render(request, 'error.html', {'message': '评论对象不存在', 'redirect_to': referer})
# 通过则保存数据
comment = Comment()
comment.user = user
comment.text = text
comment.content_object = model_obj
comment.save()
return redirect(referer) # 提交后重定向到原页面
class Comment(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)
class Meta:
ordering = ['-comment_time'] # 时间逆序,最新的在最前面
def blog_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'] = comments
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
response = render(request, 'blog/blog_detail.html', context)
response.set_cookie(read_cookie_key, 'true') # 阅读cookie标记
return response
Django 用 Form 类描述 html 表单,简化操作,方便快速开发
Django Form 的使用
定制登录表单
# forms.py
from django import forms
from django.contrib import auth
# 定制登录表单
class LoginForm(forms.Form):
username = forms.CharField(label='用户名', required=True) # 默认为True
password = forms.CharField(label='密码', widget=forms.PasswordInput)
# views.py
def login(request):
if request.method == 'POST':
login_form = LoginForm(request.POST)
if login_form.is_valid():
# 验证通过
username = login_form.cleaned_data['username']
password = login_form.cleaned_data['password']
user = auth.authenticate(request, username=username, password=password)
if user is not None:
auth.login(request, user)
return redirect(request.GET.get('from', reverse('home'))) # 没有就跳转首页
else:
login_form.add_error(None, '用户名或密码错误') # 添加错误提示
else:
# get 加载页面
login_form = LoginForm() # 实例化表单
context = {}
context['login_form'] = login_form
return render(request, 'login.html', context)
# login.html
<form action="" method="POST">
{% csrf_token %}
{{ login_form }}
<input type="submit" value="登录">
</form>
# 登录时的页面,带着当时的路径
未登录,登录后方可评论
<a href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a>
# 拿到路径,如果没有就跳转首页
redirect(request.GET.get('from', reverse('home'))) # 没有就跳转首页
class LoginForm(forms.Form):
username = forms.CharField(label='用户名', required=True) # 默认为True
password = forms.CharField(label='密码', widget=forms.PasswordInput)
# 验证数据方法
def clean(self):
username = self.cleaned_data['username']
password = self.cleaned_data['password']
user = auth.authenticate(username=username, password=password)
if user is None:
raise forms.ValidationError('用户名或密码错误')
elif:
self.cleaned_data['user'] = user
return self.cleaned_data
# 优化后的调用
def login(request):
if request.method == 'POST':
login_form = LoginForm(request.POST)
if login_form.is_valid():
user = login_form.cleaned_data['user']
auth.login(request, user)
return redirect(request.GET.get('from', reverse('home')))
else:
# get 加载页面
login_form = LoginForm() # 实例化表单
context = {}
context['login_form'] = login_form
return render(request, 'login.html', context)
>>> from django import forms
>>> filter(lambda x: 'Input' in x, dir(forms))
<filter object at 0x10304ea58>
>>> list(filter(lambda x: 'Input' in x, dir(forms)))
['CheckboxInput', 'ClearableFileInput', 'DateInput', 'DateTimeInput', 'EmailInput', 'FileInput', 'HiddenInput', 'MultipleHiddenInput', 'NumberInput', 'PasswordInput', 'TextInput', 'TimeInput', 'URLInput']
>>> filter(lambda x: 'Field' in x, dir(forms))
<filter object at 0x10304e908>
>>> list(filter(lambda x: 'Field' in x, 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']
# 定制登录表单显示
class LoginForm(forms.Form):
username = forms.CharField(label='用户名',
required=True, # 默认为True
widget=forms.TextInput(attrs={'class': 'form-control',
'placeholder':'请输入用户名'}))
# 设置渲染后的html的属性
password = forms.CharField(label='密码',
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder':'请输入密码'}))
<div class="containter">
<div class="row">
<div class="col-xs-4 col-xs-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">登录</h3>
</div>
<div class="panel-body">
<form action="" method="POST">
{% csrf_token %}
{% comment %} {{ login_form }} 定制显示 {% endcomment %}
{% for field in login_form %}
<label for="field.id_for_label">{{ field.label }}</label>
{{ field }}
<p class="text-danger">
{{ field.errors.as_text }}
{% endfor %}
<span class="pull-left text-danger">{{ login_form.non_field_errors }}</span>
<input type="submit" value="登录" class="btn btn-primary pull-right">
</form>
</div>
</div>
</div>
</div>
</div>
class RegForm(forms.Form):
username = forms.CharField(label='用户名',
required=True, # 默认为True
max_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':'再输入一次密码'}))
# 验证数据, 是否有效,是否存在
def clean_username(self):
username = self.cleaned_data['username']
if User.objects.filter(username=username).exists():
raise forms.ValidationError('用户名已存在')
return username
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError('邮箱已存在')
return email
def clean_password_again(self):
password = self.cleaned_data['password']
password_again = self.cleaned_data['password_again']
if password != password_again:
raise forms.ValidationError('两次输入的密码不一致')
return password_again
未登录,登录后方可评论
<a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a>
<span>or</span>
<a class="btn btn-danger" href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a>
def register(request):
if request.method == 'POST':
reg_form = RegForm(request.POST)
if reg_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)
# 跳转注册之前的页面
return redirect(request.GET.get('from', reverse('home')))
else:
reg_form = RegForm() # 实例化表单
context = {}
context['reg_form'] = reg_form
return render(request, 'register.html', context)
django-ckeditor 富文本表单
from ckeditor.widget import CKEditorWidget
将评论表单独立出来,放在评论应用里,定制评论表单类,添加验证表单逻辑
# comment/forms.py
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db.models import ObjectDoesNotExist
class CommentForm(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' in kwargs:
self.user = kwargs.pop('user') # 接收用户信息, 并剔除,为了下一句不出错
super(CommentForm, self).__init__(*args, **kwargs)
# 验证数据
def clean(self):
# 判断用户是否登录
if self.user.is_authenticated:
self.cleaned_data['user'] = self.user
else:
raise forms.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_obj
except ObjectDoesNotExist:
raise forms.ValidationError('评论对象不存在')
return self.cleaned_data
<form action="{% url 'update_comment' %}" method="POST" style="overflow: hidden">
{% csrf_token %}
<label for="comment_text">{{ user.username }},欢迎评论~</label>
{{ comment_form }}
<input type="submit" value="评论" class="btn btn-primary" style="float:right">
</form>
# 处理逻辑
def update_comment(request):
referer = request.META.get('HTTP_REFERER', reverse('home'))
comment_form = CommentForm(request.POST, user=request.user) # 实例化, 传递了用户信息,直接有表单类验证登录
if comment_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()
return redirect(referer) # 提交后重定向到原页面
else:
return render(request, 'error.html', {'message': comment_form.errors, 'redirect_to': referer})
# forms.py
from ckeditor.widgets import CKEditorWidget
class CommentForm(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
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
div.django-ckeditor-widget {
width: 100%;
ajax 异步提交数据方式:jQuery - AJAX 简介
ajax请求
{# ajax 异步提交, 因为直接提交会刷新页面 #}
{% block script_extends %}
<script type="text/javascript">
$('#comment_form').submit(function(){
// 判断评论内容是否为空 包括空的换行
$("#comment_error").text('');
if(CKEDITOR.instances['id_text'].document.getBody().getText().trim() == ''){
$("#comment_error").text('评论内容为空');
return false;
// 更新数据到textarea里面
CKEDITOR.instances['id_text'].updateElement();
// 异步提交
$.ajax({
url: "{% url 'update_comment' %}",
type: 'POST',
data: $(this).serialize(), // this 即 #comment_form
cache: false,
success: function(data){ // 提交成功后调用的方法
console.log(data);
// 如果成功,就插入显示数据
$("#no_comment").remove();
if(data['status']=='SUCCESS'){
var comment_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>
{% endblock script_extends %}
def update_comment(request):
# referer = request.META.get('HTTP_REFERER', reverse('home'))
comment_form = CommentForm(
request.POST, user=request.user) # 实例化, 传递了用户信息,直接有表单类验证登录
data = {}
if comment_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.username
data['comment_time'] = comment.comment_time.strftime(
'%Y-%m-%d %H:%M:%S')
data['text'] = comment.text
else:
data['status'] = 'ERROR'
data['message'] = list(comment_form.errors.values())[0][0]
return JsonResponse(data)
class CommentForm(forms.Form):
text = forms.CharField(
widget=CKEditorWidget(config_name='comment_ckeditor'),
error_messages={'required': '评论内容为空'})
<span id="no_comment">暂无评论</span>
完善评论模块,使用树结构的知识实现回复功能。主要包括树结构的知识和前端页面的代码。
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)# 评论表单
class CommentForm(forms.Form):
# 回复的哪条
reply_comment_id = forms.IntegerField(
widget=forms.HiddenInput(attrs={'id': 'reply_comment_id'}))
# 验证提交的数据
def clean_reply_comment_id(self):
reply_comment_id = self.cleaned_data['reply_comment_id']
if reply_comment_id < 0:
raise forms.ValidationError('回复出错')
elif reply_comment_id == 0:
self.cleaned_data['parent'] = None
elif Comment.objects.filter(pk=reply_comment_id).exists():
self.cleaned_data['parent'] = Comment.objects.get(
pk=reply_comment_id)
else:
raise forms.ValidationError('回复出错')
return reply_comment_id
<div class="comment-area">
<h3 class="comment-area-title">评论列表</h3>
<div id="comment_list" >
{% for comment in comments %}
<div id="root_{{ comment.pk }}" class="comment">
<span>{{ comment.user.username }}
<span>({{ comment.comment_time | date:"Y-m-d H:i:s" }}):</span>
<div id="comment_{{ comment.pk }}">
{{ comment.text | safe }}
</div>
<a href="javascript:reply({{ comment.pk }})">回复</a>
{% for reply in comment.root_comment.all %}
<div class="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>
<div id="comment_{{ reply.pk }}">
{{ reply.text|safe }}
</div>
<a href="javascript:reply({{ reply.pk }})">回复</a>
</div>
{% endfor %}
</div>
{% empty %}
<span id='no_comment'>暂无评论</span>
{% endfor %}
</div>
</div>
// 判断是 评论 还是 回复, 不同的插入位置的
// 没指定#reply_comment_id 就是评论, 回复是指定回复某一条的
if($('#reply_comment_id').val() == '0'){
// 插入评论
var comment_html = '<div id="root_' + data['pk'] + '" class="comment"> \
<span>' + data['username'] + '</span> \
<span>(' + data['comment_time'] + '):</span>\
<div id="comment_' + data['pk'] + '">' + data['text'] + '</div> \
<a href="javascript:reply(' + data['pk'] + ');">回复</a></div>';
$('#comment_list').prepend(comment_html);
}else{
// 插入回复
var reply_html = '<div class="reply"><span>' + data['username'] + '</span> \
<span>(' + data['comment_time'] + '):</span> \
<span> 回复 </span> \
<span>' + data['reply_to'] + ': </span> \
<div id="comment_' + data['pk'] + '">' + data['text'] + '</div> \
<a href="javascript:reply(' + data['pk'] + ');">回复</a></div>';
$('#root_' + data['root_pk']).append(reply_html);
function reply(reply_comment_id){
// 设置值
$('#reply_comment_id').val(reply_comment_id);
// 显示回复的哪条
var html = $('#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();
});
如何获取评论数
用自定义模板标签获取评论数
templatetags
包load
标签加载该文件, {% load comment_tags %}
文件名去掉py{% get_comment_count blog %}
注意标签是{% %}
, 参数也没有引号# 创建包,和文件
# Django_Course/mysite/comment/templatetags/comment_tags.py
# vscode cmd +k p 复制当前文件的路径
from django import template
from django.contrib.contenttypes.models import ContentType
from ..models import Comment
register = template.Library()
@register.simple_tag
def get_comment_count(obj):
content_type = ContentType.objects.get_for_model(
obj) # 根据具体对象获取contenttype
return Comment.objects.filter(
content_type=content_type, object_id=obj.pk).count()
{% load comment_tags %}
评论({% get_comment_count blog %}
@register.simple_tag
def get_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
return form
@register.simple_tag
def get_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)
return comments.order_by('-comment_time')
{% get_comment_form blog as comment_form %}
{% for field in comment_form %}
{% get_comment_list blog as comments %}
{% for comment in comments %}
//js 时间戳转当前时间,并格式化显示
function numFormat(num){
return ('00' + num).substr(-2);
function timeFormat(timestamp){
var datetime = new Date(timestamp * 1000);
var year = datetime.getFullYear();
var month = numFormat(datetime.getMonth()) + 1;
var day = numFormat(datetime.getDate());
var hour = numFormat(datetime.getHours());
var minute = numFormat(datetime.getMinutes());
var second = numFormat(datetime.getSeconds());
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
data['comment_time'] = comment.comment_time.timestamp()
div#reply_content_container {
border: 1px solid #d1d1d1;
border-bottom: none;
background-color: #f8f8f8;
overflow: hidden;
padding: 1em 1em 0.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
'default': {},
点赞功能设计
创建点赞 likes
app
python manage.py startapp likes
# Django_Course/mysite/likes/models.py
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
class LikeCount(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)
class LikeRecord(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 urls
path('like_change', views.like_change, name='like_change')
function likeChange(obj, content_type, object_id){
var is_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'){
// 更新点赞状态
var element = $(obj.getElementsByClassName('glyphicon'));
if(is_like){
element.addClass('active');
}else{
element.removeClass('active');
// 更新点赞数量
var liked_num = $(obj.getElementsByClassName('liked-num'));
liked_num.text(data['liked_num']);
}else{
alert(data['message']);
error: function(xhr){
console.log(xhr)
});
div.like {
color: #337ab7;
cursor: pointer;
display: inline-block;
padding: 0.5em 0.3em;
div.like .active{
color: red;
def ErrorResponse(code, message):
data = {}
data['status'] = 'ERROR'
data['code'] = code
data['message'] = message
return JsonResponse(data)
def SuccessResponse(liked_num):
data = {}
data['status'] = 'SUCCESS'
data['liked_num'] = liked_num
return JsonResponse(data)
def like_change(request):
# 获取请求传递的数据
# 获取用户,验证用户登录
user = request.user
if not user.is_authenticated:
return ErrorResponse(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)
except ObjectDoesNotExist:
return ErrorResponse(401, 'object not exist')
# 处理数据
if request.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)
if created:
# 未点赞过,点赞数加1
like_count, created = LikeCount.objects.get_or_create(content_type=content_type, object_id=object_id)
like_count.liked_num += 1
like_count.save()
return SuccessResponse(like_count.liked_num)
else:
# 已点赞过,不能重复点赞
return ErrorResponse(402, 'you were liked')
else:
# 取消点赞
if LikeRecord.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()
# 点赞总数 -1
like_count, created = LikeCount.objects.get_or_create(content_type=content_type, object_id=object_id)
if not created:
like_count.liked_num -= 1
like_count.save()
return SuccessResponse(like_count.liked_num)
else:
return ErrorResponse(404, 'data error')
else:
# 没点赞过,不能取消
return ErrorResponse(403, 'you were not liked')
# Django_Course/mysite/likes/templatetags/likes_tags.py
from django import template
from django.contrib.contenttypes.models import ContentType
from ..models import LikeCount, LikeRecord
register = template.Library()
@register.simple_tag
def get_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)
return like_count.liked_num
@register.simple_tag(takes_context=True) # 使用模版里面的变量
def get_like_status(context, obj):
content_type = ContentType.objects.get_for_model(obj)
user = context['user']
if not user.is_authenticated:
return ''
if LikeRecord.objects.filter(
content_type=content_type, object_id=obj.pk, user=user).exists():
return 'active'
else:
return ''
@register.simple_tag
def get_content_type(obj):
content_type = ContentType.objects.get_for_model(obj)
return content_type.model
{% load likes_tags %}
// 博客列表中引用
点赞({% get_like_count blog %})
// 添加点赞功能到评论列表
<div class="like" onclick="likeChange(this, '{% get_content_type comment %}', {{ comment.pk }})">
<span class="glyphicon glyphicon-thumbs-up {% get_like_status comment %}"></span>
<span class="liked-num">{% get_like_count comment %}</span>
</div>
// 添加点赞功能回复列表
<div class="like" onclick="likeChange(this, '{% get_content_type reply %}', {{ reply.pk }})">
<span class="glyphicon glyphicon-thumbs-up {% get_like_status reply %}"></span>
<span class="liked-num">{% get_like_count reply %}</span>
</div>
前后端开发建议
{% ... %} is used for statements.
{{ ... }} is used for variables
{# ... #} is used for to comment
完善点赞功能,让新增的评论和回复可以点赞。
新增评论和回复点赞
// 定义字符串格式化方法,解决字符串拼接麻烦问题
// '{0}+{1}'.format('a', 'b') -> "a+b"
String.prototype.format = function(){
var str = this;
for (var i = 0; i < arguments.length; i++) {
var str = str.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i])
return str;
// 异步提交
$.ajax({
url: "{% url 'update_comment' %}",
type: 'POST',
data: $(this).serialize(), // this 即 #comment_form
cache: false,
success: function(data){ // 提交成功后调用的方法, data是后端返回给前端的数据
console.log(data);
// 如果成功,就插入显示数
if(data['status']=='SUCCESS'){
// 判断是 评论 还是 回复, 不同的插入位置的
if($('#reply_comment_id').val() == '0'){
// 插入评论
var comment_html = '<div id="root_{0}" class="comment">' +
'<span>({2}):</span>' +
'<div id="comment_{0}"> {3} </div>' +
'<div class="like" onclick="likeChange(this, \'{4}\', {0})">' +
'<span class="glyphicon glyphicon-thumbs-up "></span>' +
'<span class="liked-num"> 0 </span></div>' +
'<a href="javascript:reply({0})">回复</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 -->
<div class="modal fade" id="login_modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<form id="login_modal_form" action="" method="POST">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">登录</h4>
</div>
<div class="modal-body">
{% csrf_token %}
{% for field in login_form %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% endfor %}
<span id="login_modal_tip" class="text-danger"></span>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">登录</button>
<button type="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('用户名或密码错误');
});
});
def login_for_modal(request):
login_form = LoginForm(request.POST)
data = {}
if login_form.is_valid():
user = login_form.cleaned_data['user']
auth.login(request, user)
data['status'] = 'SUCCESS'
else:
data['status'] = 'ERROR'
return JsonResponse(data)
之前评论和点赞的时候,需要登录和登出,操作有点麻烦。所以在导航栏添加用户操作,并且将相关用户的处理方法集中变成一个django应用,为后面自定义用户模型准备
方便登录和退出
// 登录状态显示用户名,未登录状态显示登录和注册
<ul class="nav navbar-nav navbar-right">
{% if not user.is_authenticated %}
<li><a href="{% url 'login' %}?from={{ request.get_full_path }}">登录</a></li>
<li><a href="{% url 'register' %}?from={{ request.get_full_path }}">注册</a></li>
{% else %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">{{ user.username }}
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'user_info' %}">个人资料</a></li>
<li role="separator" class="divider"></li>
<li><a href="{% url 'logout' %}?from={{ request.get_full_path }}">退出</a></li>
{% endif %}
// 用户中心页面
<div class="containter">
<div class="row">
<div class="col-xs-10 col-xs-offset-1">
{% if user.is_authenticated %}
<h2>{{ user.username }}</h2>
<li>昵称: <a href="#">修改昵称</a></li>
<li>邮箱:
{% if user.email %} {{ user.email }}
{% else %} 未绑定 <a href="#">绑定邮箱</a>
{% endif %}</li>
<li>上次登录的时间: {{ user.last_login|date:"Y-m-d H:i:s" }}</li>
<li><a href="#">修改密码</a></li>
{% else %}
<span>未登录,跳转到首页....</span>
<script type="text/javascript">
window.location.href = '/';
</script>
{% endif %}
</div>
</div>
</div>
# 注册用户中心url
path('user_info/', views.user_info, name='user_info'),
# 处理退出和用户中心请求
def logout(request):
auth.logout(request)
return redirect(request.GET.get('from', reverse('home')))
def user_info(request):
context = {}
return render(request, 'user_info.html', context)
迁移,将user独立成app,放到一起
python manage.py startapp appname
手动迁移user应用步骤
path('user/', include('user.urls')),
urls.py
, 统一处理用户相关的 urluser/
将登录表单和弹出的登录框放到公共模版里,独立出来,方便调用
context_processors.py
base.html
文件中,随处可用# Django_Course/mysite/user/context_processors.py
from .forms import LoginForm
def login_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',
两种自定义用户模型的方式
用新的Profile模型拓展关联的User
# Django_Course/mysite/user/models.py
from django.db import models
from django.contrib.auth.models import User
class Profile(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.py
from django.contrib import admin
from .models import Profile
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'nickname')
# admin.py
# Define an inline admin descriptor for Profile model
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
# Define a new User admin
class UserAdmin(BaseUserAdmin):
inlines = (ProfileInline,)
list_display = ('username', 'nickname', 'email', 'is_staff', 'is_active', 'is_superuser')
# 为了在用户列表显示昵称,需要加入一个自定义方法。上面就是调用user.nickname显示
def nickname(self, obj):
return obj.profile.nickname
nickname.short_description = '昵称' # 中文显示
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
用Profile模型拓展User方法的优缺点
加入调整 后台管理
链接
<ul class="dropdown-menu">
<li><a href="{% url 'user_info' %}">个人资料</a></li>
<li role="separator" class="divider"></li>
{% if user.is_staff or user.is_superuser %}
<li><a href="{% url 'admin:index' %}">后台管理</a></li> // admin是命名空间
{% endif %}
<li><a href="{% url 'logout' %}?from={{ request.get_full_path }}">退出</a></li>
优化 登录和主页 页面逻辑,如果是登录状态,就调整到首页
{% if not user.is_authenticated %}
... 注册或登录
{% else %}
<span>已登录,跳转到首页....</span>
<script type="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 用来 显示表单和提交信息
<form action="" method="POST">
{% csrf_token %}
{% for field in form %}
{% if not field.is_hidden %}
<label for="field.id_for_label">{{ field.label }}</label>
{% endif %}
{{ field }}
<p class="text-danger"> {{ field.errors.as_text }} </p>
{% endfor %}
<span class="pull-left text-danger">{{ form.non_field_errors }}</span>
<div class="pull-right">
<input type="submit" value="{{ submit_text }}" class="btn btn-primary">
<button class="btn btn-default" onclick="{{ return_back_url }}">返回</button>
</div>
</form>
# form.py 定义表单和验证表单的方法
class ChangeNicknameForm(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' in kwargs:
self.user = kwargs.pop('user') # 接收用户信息, 并剔除,为了下一句不出错
super(ChangeNicknameForm, self).__init__(*args, **kwargs)
# 验证数据
def clean(self):
# 判断用户是否登录
if self.user.is_authenticated:
self.cleaned_data['user'] = self.user
else:
raise forms.ValidationError('用户尚未登录')
return self.cleaned_data
def clean_nickname_new(self):
nickname_new = self.cleaned_data.get('nickname_new', '').strip()
if nickname_new == '':
raise forms.ValidationError('新的昵称不能为空')
return nickname_new
# views.py 处理
def change_nickname(request):
redirect_to = request.GET.get('from', reverse('home'))
if request.method == 'POST':
form = ChangeNicknameForm(request.POST, user=request.user)
if form.is_valid():
nickname_new = form.cleaned_data['nickname_new']
profile, created = Profile.objects.get_or_create(user=request.user)
profile.nickname = nickname_new
profile.save()
return redirect(redirect_to)
else:
form = ChangeNicknameForm()
context = {}
context['page_title'] = '修改昵称'
context['form_title'] = '修改昵称'
context['submit_text'] = '修改'
context['form'] = form
context['return_back_url'] = redirect_to
return render(request, 'form.html', context)
如何判断显示用户名和昵称
给user类添加获取昵称的类方法,获取昵称,是否有昵称,获得昵称或用户名
# 使用类方法的动态绑定,User类绑定获取昵称的方法
def get_nickname(self):
if Profile.objects.filter(user=self).exists():
profile = Profile.objects.get(user=self)
return profile.nickname
else:
return ''
def get_nickname_or_username(self):
if Profile.objects.filter(user=self).exists():
profile = Profile.objects.get(user=self)
return profile.nickname
else:
return self.username
def has_nickname(self):
return Profile.objects.filter(user=self).exists()
User.get_nickname = get_nickname
User.has_nickname = has_nickname
User.get_nickname_or_username = get_nickname_or_username
// base.html
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">
{% if user.has_nickname %}
{{ user.username }}({{ user.get_nickname }})
{% else %}
{{ user.username }}
{% endif %}
<span class="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()
if parent is not None:
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 = '[email protected]'
EMAIL_HOST_PASSWORD = 's' # 授权码
EMAIL_SUBJECT_PREFIX = '[able的博客]'
EMAIL_USE_TLS = True # 与smtp服务器通信时,是否启动TLS链接 安全链接
绑定邮箱的表单,即各种表单验证
class BindEmailForm(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' in kwargs:
self.request = kwargs.pop('request') # 接收传入的rquest信息, 并剔除,为了下一句不出错
super(BindEmailForm, self).__init__(*args, **kwargs)
# 验证数据
def clean(self):
# 判断用户是否登录
if self.request.user.is_authenticated:
self.cleaned_data['user'] = self.request.user
else:
raise forms.ValidationError('用户尚未登录')
# 判断用户数会否已经绑定邮箱
if self.request.user.email != '':
raise forms.ValidationError('你已经绑定了邮箱')
# 判断验证码
code = self.request.session.get('bind_email_code', '')
verification_code = self.cleaned_data.get('verification_code', '')
if not (code != '' and code == verification_code):
raise forms.ValidationError('验证码不正确')
return self.cleaned_data
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(email=email).exists():
raise forms.ValidationError('该邮箱已经被绑定')
return email
def clean_verification_code(self):
verification_code = self.cleaned_data.get('verification_code',
'').strip()
if verification_code == '':
raise forms.ValidationError('验证码不能为空')
return verification_code
views 处理 绑定邮箱和发送验证码
path('bind_email/', views.bind_email, name='bind_email'),
path('send_verification_code/', views.send_verification_code, name='send_verification_code'),
def bind_email(request):
redirect_to = request.GET.get('from', reverse('home'))
if request.method == 'POST':
form = BindEmailForm(request.POST, request=request)
if form.is_valid():
email = form.cleaned_data['email']
request.user.email = email
request.user.save()
return redirect(redirect_to)
else:
form = BindEmailForm()
context = {}
context['page_title'] = '绑定邮箱'
context['form_title'] = '绑定邮箱'
context['submit_text'] = '绑定'
context['form'] = form
context['return_back_url'] = redirect_to
return render(request, 'user/bind_email.html', context)
def send_verification_code(request):
email = request.GET.get('email', '')
data = {}
if email != '':
# 生成验证码
code = ''.join(random.sample(string.digits, 6))
now = int(time.time()) # 秒数
send_code_time = request.session.get('send_code_time', 0)
if now - send_code_time < 60:
data['status'] = 'ERROR'
else:
# session 存储用户请求信息,默认有效期两周
request.session['bind_email_code'] = code
request.session['send_code_time'] = now
# 发送邮箱
send_mail(
'绑定邮箱',
'验证码: %s' % code,
'[email protected]',
[email],
fail_silently=False,
data['status'] = 'SUCCESS'
else:
data['status'] = 'ERROR'
return JsonResponse(data)
{% extends "form.html" %}
{% block other_buttons %}
<button id="send_code" class="btn btn-primary">发送验证码</button>
{% endblock other_buttons %}
{% block script_extends %}
<script type="text/javascript">
$('#send_code').click(function(){
var email = $('#id_email').val(); // 拿到用户填的邮箱的 值
if(email == ''){
$('#tip').text('* 邮箱不能为空')
return false;
// 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('发送验证码');
return false;
time --;
$(this).text(time + 's 后重新发送');
}, 1000);
});
</script>
{% endblock script_extends %}
引导用户填邮箱,可以从注册的时候要求填写邮箱
修改登录方式,用户名和邮箱都可以登录
# Django_Course/mysite/user/forms.py
class LoginForm(forms.Form):
def clean(self):
username_or_email = self.cleaned_data['username_or_email']
password = self.cleaned_data['password']
user = auth.authenticate(username=username_or_email, password=password)
if user is None:
if User.objects.filter(email=username_or_email).exists():
username = User.objects.get(email=username_or_email).username
user = auth.authenticate(username=username, password=password)
if user is not None:
self.cleaned_data['user'] = user
return self.cleaned_data
raise forms.ValidationError('用户名或密码错误')
else:
self.cleaned_data['user'] = user
return self.cleaned_data
直接验证旧密码来设置新密码
忘记密码,发送邮件验证,修改密码
fix bug 注意清除session中的验证码
# 添加获取邮箱和url的方法 Django_Course/mysite/blog/models.py
class Blog(models.Model, ReadNumExpandMethod): # 继承 方法
author = models.ForeignKey(User, on_delete=models.CASCADE)
def get_url(self):
return reverse('blog_detail', kwargs={'blog_pk': self.pk})
def get_email(self):
return self.author.email
# 发送邮件通知
if comment.parent is None:
# 评论我的博客
# 发送邮箱
subject = '有人评论你的博客'
email = comment.content_object.get_email()
else:
# 回复评论
subject = '有人回复你的博客'
email = comment.reply_to.email
if email != '':
text = comment.text + '\n' + comment.content_object.get_url()
send_mail( subject, text, settings.EMAIL_HOST_USER, [email],
fail_silently=False,)
html_message
字段templates/comment/send_mail.html
self.text
是表单字段,会含有<p>
标签,所以模版里需要加safe
过滤掉// Django_Course/mysite/comment/templates/comment/send_mail.html
{{ comment_text | safe }}
<a href="{{ url }}">点击查看</a>
// 或者
{% autoescape off %}
{{ comment_text | safe }}
<a href="{{ url }}">点击查看</a>
{% autoescape%}
Git 是一款开源的分布式版本控制系统
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 是一款框平台的开源的关系型数据库
wheel whl 包,是编译好的包,可以直接安装,不会出编译错误
SQlite 迁移 MySQL, 先导出数据,再更改为 MySQL 数据库设置, 再导入数据
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 -p
show databases;
Django 数据库设置
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
'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 migrate
python manage.py createcachetable
python manage.py runserver
时区问题,加载时区描述表 myslq_tzinfo_to_sql
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 mysql
docker stop mysql-test
docker restart mysql-test
docker rm -f mysql-test
docker exec -it mysql-test bash
docker exec -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 -p
show databases;
django.db.utils.ProgrammingError: (1146, "Table 'mysite_db.my_cache_table' doesn't exist")
python manage.py createcachetable